Compare commits
36 Commits
v0.1.8-beta.1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 49f190e944 | |||
| 9af09de0d3 | |||
| fbff9b3a86 | |||
| 7a01b7e3f4 | |||
| 097b6fb721 | |||
| adf37520cb | |||
| e99236b893 | |||
| 43ebc81f1d | |||
| 72701dee0a | |||
| f55264bb18 | |||
| 0d4776888f | |||
| 503e50a2d0 | |||
| e3ae1bc10e | |||
| e80a462d96 | |||
| d818baffa7 | |||
| c24f2e00e2 | |||
| dddcc9031b | |||
| fc02cda577 | |||
| ace50037ba | |||
| 5afe8a2119 | |||
| 76eaff7788 | |||
| 6ac8512dbc | |||
| 3b8e942e7f | |||
| 051457b11b | |||
| 92f1c6fe82 | |||
| 27df6894a4 | |||
| 1afd978059 | |||
| 146faca7cb | |||
| 4c99247566 | |||
| 81cbb83340 | |||
| 3f65b72cf0 | |||
| 2eb94c8f74 | |||
| 6354c0987c | |||
| 7df34eb1d0 | |||
| ca5d54882f | |||
| ca4664e0cc |
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
name: triage-findings
|
||||||
|
description: Merge code-review findings, sort and renumber them by severity, resolve real issues, and move false positives into exclusions.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Triage Findings
|
||||||
|
|
||||||
|
## When To Use
|
||||||
|
|
||||||
|
Use this skill when you receive multiple review findings, screenshots, comments, or issue lists that need to become one final triaged list.
|
||||||
|
It is also used when some findings are false positives and should be moved into the exclusions list.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Collect all findings into one list.
|
||||||
|
2. Merge duplicates into a single finding when they describe the same issue.
|
||||||
|
3. Sort the final list by severity:
|
||||||
|
- critical
|
||||||
|
- warning
|
||||||
|
- info
|
||||||
|
4. Renumber the sorted list from 1 upward.
|
||||||
|
5. Rewrite each finding concisely so the final list reads cleanly and consistently.
|
||||||
|
6. If a finding is a false positive, do not keep it in the final list.
|
||||||
|
7. Add false positives to the exclusions list as a top-level JSON array in `.gitea/ai-review/exclusions.json`, and preserve the original finding wording as much as possible, including language and semantics. Do not wrap the array in `exclusions` or `excluded_findings`.
|
||||||
|
|
||||||
|
## Resolution Flow
|
||||||
|
|
||||||
|
After the list is merged and ordered, resolve the remaining findings one by one.
|
||||||
|
|
||||||
|
1. Start from the highest severity item.
|
||||||
|
2. Identify the root cause in the relevant file or context.
|
||||||
|
3. Apply the smallest safe change that fixes the issue.
|
||||||
|
4. Add or update tests when behavior changes.
|
||||||
|
5. Re-check the issue after the change.
|
||||||
|
6. If the item is confirmed false positive, move it to exclusions instead of changing code.
|
||||||
|
7. Continue until the list is either fixed or explicitly excluded.
|
||||||
|
|
||||||
|
## Output Rules
|
||||||
|
|
||||||
|
- Keep the final findings list in severity order, then by any stable secondary order needed to make it readable.
|
||||||
|
- Keep numbering contiguous after filtering and merging.
|
||||||
|
- Preserve useful details like file path, location, and suggested fix.
|
||||||
|
- Keep exclusions entries minimal and consistent with the project schema.
|
||||||
|
- When writing exclusions, always output a top-level JSON array.
|
||||||
|
- When writing exclusions, prefer the original issue text and language; only paraphrase if needed to fit the schema.
|
||||||
|
- If the source already provides a severity or title, keep it unless it conflicts with the final ordering.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Triage Findings
|
||||||
|
|
||||||
|
## When To Use
|
||||||
|
|
||||||
|
Use this skill when you receive multiple review findings, screenshots, comments, or issue lists that need to become one final triaged list.
|
||||||
|
It is also used when some findings are false positives and should be moved into the exclusions list.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Collect all findings into one list.
|
||||||
|
2. Merge duplicates into a single finding when they describe the same issue.
|
||||||
|
3. Sort the final list by severity:
|
||||||
|
- critical
|
||||||
|
- warning
|
||||||
|
- info
|
||||||
|
4. Renumber the sorted list from 1 upward.
|
||||||
|
5. Rewrite each finding concisely so the final list reads cleanly and consistently.
|
||||||
|
6. If a finding is a false positive, do not keep it in the final list.
|
||||||
|
7. Add false positives to the exclusions list as a top-level JSON array in `.gitea/ai-review/exclusions.json`, and preserve the original finding wording as much as possible, including language and semantics. Do not wrap the array in `exclusions` or `excluded_findings`.
|
||||||
|
|
||||||
|
## Resolution Flow
|
||||||
|
|
||||||
|
After the list is merged and ordered, resolve the remaining findings one by one.
|
||||||
|
|
||||||
|
1. Start from the highest severity item.
|
||||||
|
2. Identify the root cause in the relevant file or context.
|
||||||
|
3. Apply the smallest safe change that fixes the issue.
|
||||||
|
4. Add or update tests when behavior changes.
|
||||||
|
5. Re-check the issue after the change.
|
||||||
|
6. If the item is confirmed false positive, move it to exclusions instead of changing code.
|
||||||
|
7. Continue until the list is either fixed or explicitly excluded.
|
||||||
|
|
||||||
|
## Output Rules
|
||||||
|
|
||||||
|
- Keep the final findings list in severity order, then by any stable secondary order needed to make it readable.
|
||||||
|
- Keep numbering contiguous after filtering and merging.
|
||||||
|
- Preserve useful details like file path, location, and suggested fix.
|
||||||
|
- Keep exclusions entries minimal and consistent with the project schema.
|
||||||
|
- When writing exclusions, always output a top-level JSON array.
|
||||||
|
- When writing exclusions, prefer the original issue text and language; only paraphrase if needed to fit the schema.
|
||||||
|
- If the source already provides a severity or title, keep it unless it conflicts with the final ordering.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
name: triage-findings
|
||||||
|
description: Merge code-review findings, sort and renumber them by severity, resolve real issues, and move false positives into exclusions.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Triage Findings
|
||||||
|
|
||||||
|
## When To Use
|
||||||
|
|
||||||
|
Use this skill when you receive multiple review findings, screenshots, comments, or issue lists that need to become one final triaged list.
|
||||||
|
It is also used when some findings are false positives and should be moved into the exclusions list.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Collect all findings into one list.
|
||||||
|
2. Merge duplicates into a single finding when they describe the same issue.
|
||||||
|
3. Sort the final list by severity:
|
||||||
|
- critical
|
||||||
|
- warning
|
||||||
|
- info
|
||||||
|
4. Renumber the sorted list from 1 upward.
|
||||||
|
5. Rewrite each finding concisely so the final list reads cleanly and consistently.
|
||||||
|
6. If a finding is a false positive, do not keep it in the final list.
|
||||||
|
7. Add false positives to the exclusions list as a top-level JSON array in `.gitea/ai-review/exclusions.json`, and preserve the original finding wording as much as possible, including language and semantics. Do not wrap the array in `exclusions` or `excluded_findings`.
|
||||||
|
|
||||||
|
## Resolution Flow
|
||||||
|
|
||||||
|
After the list is merged and ordered, resolve the remaining findings one by one.
|
||||||
|
|
||||||
|
1. Start from the highest severity item.
|
||||||
|
2. Identify the root cause in the relevant file or context.
|
||||||
|
3. Apply the smallest safe change that fixes the issue.
|
||||||
|
4. Add or update tests when behavior changes.
|
||||||
|
5. Re-check the issue after the change.
|
||||||
|
6. If the item is confirmed false positive, move it to exclusions instead of changing code.
|
||||||
|
7. Continue until the list is either fixed or explicitly excluded.
|
||||||
|
|
||||||
|
## Output Rules
|
||||||
|
|
||||||
|
- Keep the final findings list in severity order, then by any stable secondary order needed to make it readable.
|
||||||
|
- Keep numbering contiguous after filtering and merging.
|
||||||
|
- Preserve useful details like file path, location, and suggested fix.
|
||||||
|
- Keep exclusions entries minimal and consistent with the project schema.
|
||||||
|
- When writing exclusions, always output a top-level JSON array.
|
||||||
|
- When writing exclusions, prefer the original issue text and language; only paraphrase if needed to fit the schema.
|
||||||
|
- If the source already provides a severity or title, keep it unless it conflicts with the final ordering.
|
||||||
@@ -1,30 +1,46 @@
|
|||||||
---
|
---
|
||||||
name: triage-findings
|
name: triage-findings
|
||||||
description: Triage findings, fix real issues, and exclude false positives.
|
description: Merge code-review findings, sort and renumber them by severity, resolve real issues, and move false positives into exclusions.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Triage Findings
|
# Triage Findings
|
||||||
|
|
||||||
## Use
|
## When To Use
|
||||||
|
|
||||||
直接輸入:`triage-findings 問題原始檔(文字或截圖)`
|
Use this skill when you receive multiple review findings, screenshots, comments, or issue lists that need to become one final triaged list.
|
||||||
|
It is also used when some findings are false positives and should be moved into the exclusions list.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
1. Merge all findings.
|
1. Collect all findings into one list.
|
||||||
2. Sort by severity:
|
2. Merge duplicates into a single finding when they describe the same issue.
|
||||||
|
3. Sort the final list by severity:
|
||||||
- critical
|
- critical
|
||||||
- warning
|
- warning
|
||||||
- info
|
- info
|
||||||
3. Renumber from 1.
|
4. Renumber the sorted list from 1 upward.
|
||||||
4. Fix real issues.
|
5. Rewrite each finding concisely so the final list reads cleanly and consistently.
|
||||||
5. Put false positives into `.gitea/ai-review/exclusions.json` as a top-level JSON array, preserving the original wording, language, and semantics as much as possible. Do not wrap the array in `exclusions` or `excluded_findings`.
|
6. If a finding is a false positive, do not keep it in the final list.
|
||||||
6. Add tests when behavior changes.
|
7. Add false positives to the exclusions list as a top-level JSON array in `.gitea/ai-review/exclusions.json`, and preserve the original finding wording as much as possible, including language and semantics. Do not wrap the array in `exclusions` or `excluded_findings`.
|
||||||
|
|
||||||
|
## Resolution Flow
|
||||||
|
|
||||||
|
After the list is merged and ordered, resolve the remaining findings one by one.
|
||||||
|
|
||||||
|
1. Start from the highest severity item.
|
||||||
|
2. Identify the root cause in the relevant file or context.
|
||||||
|
3. Apply the smallest safe change that fixes the issue.
|
||||||
|
4. Add or update tests when behavior changes.
|
||||||
|
5. Re-check the issue after the change.
|
||||||
|
6. If the item is confirmed false positive, move it to exclusions instead of changing code.
|
||||||
|
7. Continue until the list is either fixed or explicitly excluded.
|
||||||
|
|
||||||
## Output Rules
|
## Output Rules
|
||||||
|
|
||||||
- Keep the final list short.
|
- Keep the final findings list in severity order, then by any stable secondary order needed to make it readable.
|
||||||
- Keep numbering contiguous.
|
- Keep numbering contiguous after filtering and merging.
|
||||||
- Preserve file path, location, and fix.
|
- Preserve useful details like file path, location, and suggested fix.
|
||||||
|
- Keep exclusions entries minimal and consistent with the project schema.
|
||||||
- When writing exclusions, always output a top-level JSON array.
|
- When writing exclusions, always output a top-level JSON array.
|
||||||
- When writing exclusions, prefer the original issue text over paraphrased rewrites.
|
- When writing exclusions, prefer the original issue text and language; only paraphrase if needed to fit the schema.
|
||||||
|
- If the source already provides a severity or title, keep it unless it conflicts with the final ordering.
|
||||||
|
|||||||
@@ -1,30 +1,46 @@
|
|||||||
---
|
---
|
||||||
name: triage-findings
|
name: triage-findings
|
||||||
description: Triage findings, fix real issues, and exclude false positives.
|
description: Merge code-review findings, sort and renumber them by severity, resolve real issues, and move false positives into exclusions.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Triage Findings
|
# Triage Findings
|
||||||
|
|
||||||
## Use
|
## When To Use
|
||||||
|
|
||||||
直接輸入:`triage-findings 問題原始檔(文字或截圖)`
|
Use this skill when you receive multiple review findings, screenshots, comments, or issue lists that need to become one final triaged list.
|
||||||
|
It is also used when some findings are false positives and should be moved into the exclusions list.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
1. Merge all findings.
|
1. Collect all findings into one list.
|
||||||
2. Sort by severity:
|
2. Merge duplicates into a single finding when they describe the same issue.
|
||||||
|
3. Sort the final list by severity:
|
||||||
- critical
|
- critical
|
||||||
- warning
|
- warning
|
||||||
- info
|
- info
|
||||||
3. Renumber from 1.
|
4. Renumber the sorted list from 1 upward.
|
||||||
4. Fix real issues.
|
5. Rewrite each finding concisely so the final list reads cleanly and consistently.
|
||||||
5. Put false positives into `.gitea/ai-review/exclusions.json` as a top-level JSON array, preserving the original wording, language, and semantics as much as possible. Do not wrap the array in `exclusions` or `excluded_findings`.
|
6. If a finding is a false positive, do not keep it in the final list.
|
||||||
6. Add tests when behavior changes.
|
7. Add false positives to the exclusions list as a top-level JSON array in `.gitea/ai-review/exclusions.json`, and preserve the original finding wording as much as possible, including language and semantics. Do not wrap the array in `exclusions` or `excluded_findings`.
|
||||||
|
|
||||||
|
## Resolution Flow
|
||||||
|
|
||||||
|
After the list is merged and ordered, resolve the remaining findings one by one.
|
||||||
|
|
||||||
|
1. Start from the highest severity item.
|
||||||
|
2. Identify the root cause in the relevant file or context.
|
||||||
|
3. Apply the smallest safe change that fixes the issue.
|
||||||
|
4. Add or update tests when behavior changes.
|
||||||
|
5. Re-check the issue after the change.
|
||||||
|
6. If the item is confirmed false positive, move it to exclusions instead of changing code.
|
||||||
|
7. Continue until the list is either fixed or explicitly excluded.
|
||||||
|
|
||||||
## Output Rules
|
## Output Rules
|
||||||
|
|
||||||
- Keep the final list short.
|
- Keep the final findings list in severity order, then by any stable secondary order needed to make it readable.
|
||||||
- Keep numbering contiguous.
|
- Keep numbering contiguous after filtering and merging.
|
||||||
- Preserve file path, location, and fix.
|
- Preserve useful details like file path, location, and suggested fix.
|
||||||
|
- Keep exclusions entries minimal and consistent with the project schema.
|
||||||
- When writing exclusions, always output a top-level JSON array.
|
- When writing exclusions, always output a top-level JSON array.
|
||||||
- When writing exclusions, prefer the original issue text over paraphrased rewrites.
|
- When writing exclusions, prefer the original issue text and language; only paraphrase if needed to fit the schema.
|
||||||
|
- If the source already provides a severity or title, keep it unless it conflicts with the final ordering.
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
{
|
{
|
||||||
"role": "Leo",
|
"role": "Leo",
|
||||||
"location": "app/llm.js",
|
"location": "app/llm.js",
|
||||||
"suggestion": "Authorization 標頭已有 provider !== 'ollama' 判斷,不會無條件加入,已正確處理"
|
"suggestion": "Authorization 標頭已有 provider !== \u0027ollama\u0027 判斷,不會無條件加入,已正確處理"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"role": "Zara",
|
"role": "Zara",
|
||||||
@@ -344,5 +344,12 @@
|
|||||||
"role": "Leo",
|
"role": "Leo",
|
||||||
"location": "app/log.js",
|
"location": "app/log.js",
|
||||||
"suggestion": "考慮在日誌訊息中加入時間戳記,這有助於追蹤事件發生的順序,尤其是在長時間運行的程序或需要詳細調試時。可以在每個日誌函式內部自動添加時間戳記。"
|
"suggestion": "考慮在日誌訊息中加入時間戳記,這有助於追蹤事件發生的順序,尤其是在長時間運行的程序或需要詳細調試時。可以在每個日誌函式內部自動添加時間戳記。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": "warning",
|
||||||
|
"role": "Leo",
|
||||||
|
"location": "Dockerfile, app/git.js, app/gitea.js",
|
||||||
|
"suggestion": "此變更引入了新的代理(agent)相關路徑(例如 `.agents/` 和 `AGENTS.md`),並在 `Dockerfile` 的 `COPY` 指令、`app/git.js` 中的 `SYNC_PATHS`、`FORCE_SYNC_FILE_PATHS`、`SYNC_TREE_PATHS` 陣列,以及 `app/gitea.js` 的 `filterDiff` 陣列中重複添加了這些路徑。這種模式導致了程式碼重複,每次新增一個代理都需要手動修改多個檔案和多個列表,增加了維護成本和出錯的可能性。建議考慮引入一個集中的設定檔或機制,例如透過掃描特定目錄來動態生成這些路徑列表,以提高模組化和可擴展性。",
|
||||||
|
"is_new": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -11,4 +11,4 @@ Use the triage-finding workflow for review issue lists:
|
|||||||
7. Add or update tests when behavior changes.
|
7. Add or update tests when behavior changes.
|
||||||
8. Re-check after each fix.
|
8. Re-check after each fix.
|
||||||
|
|
||||||
The full reusable skill lives in `.claude/skills/triage-findings/SKILL.md`.
|
The full reusable skill lives in `.github/skills/triage-findings/SKILL.md`.
|
||||||
|
|||||||
@@ -1,14 +1,41 @@
|
|||||||
# Triage Findings
|
# Triage Findings
|
||||||
|
|
||||||
Use the triage-finding workflow for review issue lists:
|
## When To Use
|
||||||
|
|
||||||
1. Merge findings into one list.
|
Use this skill when you receive multiple review findings, screenshots, comments, or issue lists that need to become one final triaged list.
|
||||||
2. Remove duplicates.
|
It is also used when some findings are false positives and should be moved into the exclusions list.
|
||||||
3. Sort by severity: `critical` -> `warning` -> `info`.
|
|
||||||
4. Renumber from 1.
|
|
||||||
5. Fix real issues with the smallest safe change.
|
|
||||||
6. Put false positives into `.gitea/ai-review/exclusions.json` as a top-level JSON array, preserving the original wording, language, and semantics as much as possible. Do not wrap the array in `exclusions` or `excluded_findings`.
|
|
||||||
7. Add or update tests when behavior changes.
|
|
||||||
8. Re-check after each fix.
|
|
||||||
|
|
||||||
The reusable skill lives in `.gemini/skills/triage-findings/SKILL.md`.
|
## Workflow
|
||||||
|
|
||||||
|
1. Collect all findings into one list.
|
||||||
|
2. Merge duplicates into a single finding when they describe the same issue.
|
||||||
|
3. Sort the final list by severity:
|
||||||
|
- critical
|
||||||
|
- warning
|
||||||
|
- info
|
||||||
|
4. Renumber the sorted list from 1 upward.
|
||||||
|
5. Rewrite each finding concisely so the final list reads cleanly and consistently.
|
||||||
|
6. If a finding is a false positive, do not keep it in the final list.
|
||||||
|
7. Add false positives to the exclusions list as a top-level JSON array in `.gitea/ai-review/exclusions.json`, and preserve the original finding wording as much as possible, including language and semantics. Do not wrap the array in `exclusions` or `excluded_findings`.
|
||||||
|
|
||||||
|
## Resolution Flow
|
||||||
|
|
||||||
|
After the list is merged and ordered, resolve the remaining findings one by one.
|
||||||
|
|
||||||
|
1. Start from the highest severity item.
|
||||||
|
2. Identify the root cause in the relevant file or context.
|
||||||
|
3. Apply the smallest safe change that fixes the issue.
|
||||||
|
4. Add or update tests when behavior changes.
|
||||||
|
5. Re-check the issue after the change.
|
||||||
|
6. If the item is confirmed false positive, move it to exclusions instead of changing code.
|
||||||
|
7. Continue until the list is either fixed or explicitly excluded.
|
||||||
|
|
||||||
|
## Output Rules
|
||||||
|
|
||||||
|
- Keep the final findings list in severity order, then by any stable secondary order needed to make it readable.
|
||||||
|
- Keep numbering contiguous after filtering and merging.
|
||||||
|
- Preserve useful details like file path, location, and suggested fix.
|
||||||
|
- Keep exclusions entries minimal and consistent with the project schema.
|
||||||
|
- When writing exclusions, always output a top-level JSON array.
|
||||||
|
- When writing exclusions, prefer the original issue text and language; only paraphrase if needed to fit the schema.
|
||||||
|
- If the source already provides a severity or title, keep it unless it conflicts with the final ordering.
|
||||||
|
|||||||
@@ -12,3 +12,5 @@ When the task is to triage review findings, follow this workflow:
|
|||||||
8. Re-check the issue after each fix.
|
8. Re-check the issue after each fix.
|
||||||
|
|
||||||
Use the repo-local `triage-findings` skill for the same workflow when running in Codex.
|
Use the repo-local `triage-findings` skill for the same workflow when running in Codex.
|
||||||
|
|
||||||
|
Trigger it with `/triage-findings`.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Triage Findings
|
||||||
|
|
||||||
|
Use the triage-finding workflow for review issue lists:
|
||||||
|
|
||||||
|
1. Merge findings into one list.
|
||||||
|
2. Remove duplicates.
|
||||||
|
3. Sort by severity: `critical` -> `warning` -> `info`.
|
||||||
|
4. Renumber from 1.
|
||||||
|
5. Fix real issues with the smallest safe change.
|
||||||
|
6. Put false positives into `.gitea/ai-review/exclusions.json`, preserving the original wording, language, and semantics as much as possible.
|
||||||
|
7. Add or update tests when behavior changes.
|
||||||
|
8. Re-check after each fix.
|
||||||
|
|
||||||
|
The reusable skill lives in `.antigravity/skills/triage-findings/SKILL.md`.
|
||||||
@@ -11,6 +11,6 @@ When the task is to triage review findings, follow this workflow:
|
|||||||
7. Add or update tests when behavior changes.
|
7. Add or update tests when behavior changes.
|
||||||
8. Re-check the issue after each fix.
|
8. Re-check the issue after each fix.
|
||||||
|
|
||||||
Use the repo-local `triage-findings` skill for the same workflow when running in Codex.
|
Use the repo-local `triage-findings` skill for the same workflow when running in Claude.
|
||||||
|
|
||||||
Trigger it with `/triage-findings`.
|
Trigger it with `/triage-findings`.
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ RUN cd /action/app && npm install
|
|||||||
|
|
||||||
COPY .amazonq/ /action/.amazonq/
|
COPY .amazonq/ /action/.amazonq/
|
||||||
COPY .codex/ /action/.codex/
|
COPY .codex/ /action/.codex/
|
||||||
|
COPY .agents/ /action/.agents/
|
||||||
COPY .claude/ /action/.claude/
|
COPY .claude/ /action/.claude/
|
||||||
COPY .gemini/ /action/.gemini/
|
COPY .gemini/ /action/.gemini/
|
||||||
COPY .github/ /action/.github/
|
COPY .github/ /action/.github/
|
||||||
|
COPY AGENTS.md /action/
|
||||||
COPY CLAUDE.md /action/
|
COPY CLAUDE.md /action/
|
||||||
COPY GEMINI.md /action/
|
COPY GEMINI.md /action/
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
4. 盡量將應用程式放在 ./app,修改 entrypoint.sh 與 Dockerfile 讓程式可以正常運行
|
4. 盡量將應用程式放在 ./app,修改 entrypoint.sh 與 Dockerfile 讓程式可以正常運行
|
||||||
5. 將提示詞放到 ./app/prompts 內供程式讀取
|
5. 將提示詞放到 ./app/prompts 內供程式讀取
|
||||||
6. API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,全部失敗則 exit 1
|
6. API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,全部失敗則 exit 1
|
||||||
7. 讀取 Git Diff 時排除 `.gitea/`、`.amazonq/`、`.claude/`、`.codex/`、`.gemini/`、`.github/` 資料夾,以及 `CLAUDE.md`、`GEMINI.md`、`TODO.md`、`README.md`,避免 AI 分析 workflow 設定、skill 入口與文件等非業務程式碼
|
7. 讀取 Git Diff 時排除 `.gitea/`、`.amazonq/`、`.agents/`、`.antigravity/`、`.claude/`、`.codex/`、`.gemini/`、`.github/` 資料夾,以及 `AGENTS.md`、`ANTIGRAVITY.md`、`CLAUDE.md`、`GEMINI.md`、`TODO.md`、`README.md`,避免 AI 分析 workflow 設定、skill 入口與文件等非業務程式碼
|
||||||
8. 階段七驗證來源分支中的 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,格式錯誤時先嘗試透過 AI 修正內容,再重新驗證;修正後仍不合法才 exit 1;之後才檢查檔案是否存在,不存在則建立並寫入 `[]`
|
8. 階段七驗證來源分支中的 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,格式錯誤時先嘗試透過 AI 修正內容,再重新驗證;修正後仍不合法才 exit 1;之後才檢查檔案是否存在,不存在則建立並寫入 `[]`
|
||||||
9. 傳給 AI 的 findings 只保留必要欄位(level、role、location、suggestion),排除 `is_new` 等內部欄位;system prompt 精簡為指令核心;exclusions hint 只傳 location 與 suggestion,減少 token 用量
|
9. 傳給 AI 的 findings 只保留必要欄位(level、role、location、suggestion),排除 `is_new` 等內部欄位;system prompt 精簡為指令核心;exclusions hint 只傳 location 與 suggestion,減少 token 用量
|
||||||
10. 執行時會額外記錄來源分支狀態、`findings.json` / `exclusions.json` 的檔案路徑、大小、mtime 與 raw/normalized 筆數,方便追查讀檔與分支內容不一致的問題
|
10. 執行時會額外記錄來源分支狀態、`findings.json` / `exclusions.json` 的檔案路徑、大小、mtime 與 raw/normalized 筆數,方便追查讀檔與分支內容不一致的問題
|
||||||
@@ -235,6 +235,7 @@ Copilot:`/triage-findings 問題原始檔(文字或截圖)`
|
|||||||
Claude:直接輸入 `triage-findings 問題原始檔(文字或截圖)`
|
Claude:直接輸入 `triage-findings 問題原始檔(文字或截圖)`
|
||||||
Gemini:直接輸入 `triage-findings 問題原始檔(文字或截圖)`
|
Gemini:直接輸入 `triage-findings 問題原始檔(文字或截圖)`
|
||||||
Amazon Q:直接輸入 `triage-findings 問題原始檔(文字或截圖)`
|
Amazon Q:直接輸入 `triage-findings 問題原始檔(文字或截圖)`
|
||||||
|
Antigravity:直接輸入 `triage-findings 問題原始檔(文字或截圖)`
|
||||||
|
|
||||||
### 適用情境
|
### 適用情境
|
||||||
|
|
||||||
@@ -242,4 +243,4 @@ Amazon Q:直接輸入 `triage-findings 問題原始檔(文字或截圖)`
|
|||||||
|
|
||||||
### 版本包含
|
### 版本包含
|
||||||
|
|
||||||
提交時一併包含 `triage-findings` skill 與各平台入口檔;已存在檔案一律覆蓋,同步到最新內容;若 workspace 沒有某個同步檔,記憶區會保留原檔,不做刪除。`findings.json` 與 `exclusions.json` 都從使用此 action 的存取庫來源分支讀取,而不是從 action 本地 workspace 讀取。寫入 `.gitea/ai-review/exclusions.json` 時,盡量保留原始問題文字的語言與語意,避免過度改寫。未來若新增任何 skill 或新增其他平台的 skill 入口,必須同時把對應檔案複製進 Docker image,並把同步清單更新到會使用此 action 的目標專案,避免 action 與目標專案內容脫節。
|
提交時一併包含 `triage-findings` skill 與各平台入口檔;其中 `AGENTS.md`、`ANTIGRAVITY.md`、`CLAUDE.md`、`GEMINI.md` 會在目標專案已存在時先做規則化合併,並在可用 LLM 時再用 AI 輔助檢查是否有遺失任何 skill、command 或規則;其餘同步檔則以來源覆蓋;若 workspace 沒有某個同步檔,記憶區會保留原檔,不做刪除。`findings.json` 與 `exclusions.json` 都從使用此 action 的存取庫來源分支讀取,而不是從 action 本地 workspace 讀取。寫入 `.gitea/ai-review/exclusions.json` 時,盡量保留原始問題文字的語言與語意,避免過度改寫。未來若新增任何 skill 或新增其他平台的 skill 入口,必須同時把對應檔案複製進 Docker image,並把同步清單更新到會使用此 action 的目標專案,避免 action 與目標專案內容脫節。
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
- 已驗收:`code-review` job 的 log 已完整出現 `Step1` 到 `Step8`,並以 `Pipeline 完成` 結束。
|
- 已驗收:`code-review` job 的 log 已完整出現 `Step1` 到 `Step8`,並以 `Pipeline 完成` 結束。
|
||||||
|
|
||||||
## 階段二:Git Diff 排除 .gitea/ 資料夾
|
## 階段二:Git Diff 排除 .gitea/ 資料夾
|
||||||
- 目標:讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,以及 `.amazonq/`、`.claude/`、`.codex/`、`.gemini/`、`.github/`、`CLAUDE.md`、`GEMINI.md`、`TODO.md`、`README.md`,避免 AI 分析 workflow 設定、skill 入口與文件等非業務程式碼。
|
- 目標:讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,以及 `.amazonq/`、`.antigravity/`、`.claude/`、`.codex/`、`.gemini/`、`.github/`、`ANTIGRAVITY.md`、`CLAUDE.md`、`GEMINI.md`、`TODO.md`、`README.md`,避免 AI 分析 workflow 設定、skill 入口與文件等非業務程式碼。
|
||||||
- 驗收:PR 中有上述路徑或檔案的變更時,diff 內容不包含該區塊,AI 分析結果不含這些路徑相關問題。
|
- 驗收:PR 中有上述路徑或檔案的變更時,diff 內容不包含該區塊,AI 分析結果不含這些路徑相關問題。
|
||||||
- 已驗收:`app/gitea.js` 已在取得 diff 時過濾 `.gitea/` 區塊,且相關單元測試已覆蓋。
|
- 已驗收:`app/gitea.js` 已在取得 diff 時過濾 `.gitea/` 區塊,且相關單元測試已覆蓋。
|
||||||
|
|
||||||
@@ -38,8 +38,8 @@
|
|||||||
- 已驗收:log 已明確顯示 `.gitea/ai-review/findings.json` 與 `.gitea/ai-review/exclusions.json` 都是 `JSON 格式正確`。
|
- 已驗收:log 已明確顯示 `.gitea/ai-review/findings.json` 與 `.gitea/ai-review/exclusions.json` 都是 `JSON 格式正確`。
|
||||||
|
|
||||||
## 階段八:記憶區 commit/push 與錯誤處理
|
## 階段八:記憶區 commit/push 與錯誤處理
|
||||||
- 目標:記憶區能成功 commit/push,且一併包含 `triage-findings` skill 與各平台入口檔;skill 檔案已存在時一律以來源覆蓋,workspace 沒有的同步檔則保留記憶區既有內容,不做刪除;錯誤時有明確 log,流程結束有總結訊息。
|
- 目標:記憶區能成功 commit/push,且一併包含 `triage-findings` skill 與各平台入口檔;`AGENTS.md`、`ANTIGRAVITY.md`、`CLAUDE.md`、`GEMINI.md` 在目標專案已存在時會先做規則化合併,並在可用 LLM 時再做 AI 輔助檢查以避免遺失 skill、command 或規則;其餘同步檔則以來源覆蓋;workspace 沒有的同步檔則保留記憶區既有內容,不做刪除;錯誤時有明確 log,流程結束有總結訊息。
|
||||||
- 驗收:log 有「persisted findings」、「commit=...」、「push=...」等訊息,且能看出 skill 相關檔案已一併提交並被來源覆蓋;當 workspace 缺少某個同步檔時,記憶區中的對應檔案不會被刪除;錯誤時有「Runner failed: ...」等明確錯誤說明。
|
- 驗收:log 有「persisted findings」、「commit=...」、「push=...」等訊息,且能看出四個入口檔會先規則合併、再由 AI 輔助檢查,其他同步檔會被來源覆蓋;當 workspace 缺少某個同步檔時,記憶區中的對應檔案不會被刪除;錯誤時有「Runner failed: ...」等明確錯誤說明。
|
||||||
- 已驗收:commit/push 成功時會出現 `persisted findings commit=... push=... review_outcome=...`,且同步規則與缺檔不刪除的行為都有單元測試覆蓋。
|
- 已驗收:commit/push 成功時會出現 `persisted findings commit=... push=... review_outcome=...`,且同步規則與缺檔不刪除的行為都有單元測試覆蓋。
|
||||||
|
|
||||||
## 階段九:阻擋嚴重問題 PR(第 8 點)
|
## 階段九:阻擋嚴重問題 PR(第 8 點)
|
||||||
|
|||||||
+179
-26
@@ -2,8 +2,8 @@ import { spawnSync } from 'child_process';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js';
|
import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH, getLLMConfig } from './config.js';
|
||||||
import { line, ok, warn } from './log.js';
|
import { line, ok, warn, error } from './log.js';
|
||||||
|
|
||||||
const ACTION_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
const ACTION_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
const GENERATED_SYNC_PATHS = [FINDINGS_PATH, '.gitea/ai-review/exclusions.json'];
|
const GENERATED_SYNC_PATHS = [FINDINGS_PATH, '.gitea/ai-review/exclusions.json'];
|
||||||
@@ -11,21 +11,36 @@ const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.gi
|
|||||||
export const BOT_COMMIT_MARKER = '[ai-review-bot]';
|
export const BOT_COMMIT_MARKER = '[ai-review-bot]';
|
||||||
export const SYNC_PATHS = [
|
export const SYNC_PATHS = [
|
||||||
'.amazonq/rules/triage-findings.md',
|
'.amazonq/rules/triage-findings.md',
|
||||||
|
'.agents/skills/triage-findings/SKILL.md',
|
||||||
|
'.antigravity/skills/triage-findings/SKILL.md',
|
||||||
'.codex/skills/triage-findings/SKILL.md',
|
'.codex/skills/triage-findings/SKILL.md',
|
||||||
'.codex/skills/triage-findings/agents/openai.yaml',
|
'.codex/skills/triage-findings/agents/openai.yaml',
|
||||||
'.claude/skills/triage-findings/SKILL.md',
|
'.claude/skills/triage-findings/SKILL.md',
|
||||||
'.gemini/skills/triage-findings/SKILL.md',
|
'.gemini/skills/triage-findings/SKILL.md',
|
||||||
'.github/copilot-instructions.md',
|
'.github/copilot-instructions.md',
|
||||||
'.github/skills/triage-findings/SKILL.md',
|
'.github/skills/triage-findings/SKILL.md',
|
||||||
|
'AGENTS.md',
|
||||||
|
'ANTIGRAVITY.md',
|
||||||
'CLAUDE.md',
|
'CLAUDE.md',
|
||||||
'GEMINI.md',
|
'GEMINI.md',
|
||||||
];
|
];
|
||||||
const FORCE_SYNC_FILE_PATHS = [
|
const FORCE_SYNC_FILE_PATHS = [
|
||||||
'.github/copilot-instructions.md',
|
'.github/copilot-instructions.md',
|
||||||
|
'AGENTS.md',
|
||||||
|
'ANTIGRAVITY.md',
|
||||||
'CLAUDE.md',
|
'CLAUDE.md',
|
||||||
'GEMINI.md',
|
'GEMINI.md',
|
||||||
];
|
];
|
||||||
|
const MERGE_SYNC_FILE_PATHS = new Set([
|
||||||
|
'AGENTS.md',
|
||||||
|
'ANTIGRAVITY.md',
|
||||||
|
'CLAUDE.md',
|
||||||
|
'GEMINI.md',
|
||||||
|
]);
|
||||||
|
let instructionMergeAssistantPromise = null;
|
||||||
const SYNC_TREE_PATHS = [
|
const SYNC_TREE_PATHS = [
|
||||||
|
'.agents/skills/triage-findings',
|
||||||
|
'.antigravity/skills/triage-findings',
|
||||||
'.codex/skills/triage-findings',
|
'.codex/skills/triage-findings',
|
||||||
'.claude/skills/triage-findings',
|
'.claude/skills/triage-findings',
|
||||||
'.gemini/skills/triage-findings',
|
'.gemini/skills/triage-findings',
|
||||||
@@ -62,35 +77,169 @@ function readGitOutput(run, args, cwd, env) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyTree(sourceRoot, repoDir, relDir) {
|
function normalizeText(text) {
|
||||||
|
return text.replace(/\r\n/g, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitTextBlocks(text) {
|
||||||
|
const normalized = normalizeText(text).replace(/\n+$/, '');
|
||||||
|
if (!normalized) return [];
|
||||||
|
return normalized.split(/\n{2,}/).map(block => block.trimEnd()).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeText(existingText, sourceText) {
|
||||||
|
const existing = normalizeText(existingText);
|
||||||
|
const source = normalizeText(sourceText);
|
||||||
|
if (existing === source) return existing;
|
||||||
|
|
||||||
|
const mergedBlocks = splitTextBlocks(existing);
|
||||||
|
const seenBlocks = new Set(mergedBlocks.map(block => block.trim()));
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
for (const block of splitTextBlocks(source)) {
|
||||||
|
const key = block.trim();
|
||||||
|
if (seenBlocks.has(key)) continue;
|
||||||
|
seenBlocks.add(key);
|
||||||
|
mergedBlocks.push(block);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) return existing;
|
||||||
|
return `${mergedBlocks.join('\n\n')}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueBlocksFromTexts(...texts) {
|
||||||
|
const seen = new Set();
|
||||||
|
const blocks = [];
|
||||||
|
for (const text of texts) {
|
||||||
|
for (const block of splitTextBlocks(text)) {
|
||||||
|
const key = block.trim();
|
||||||
|
if (!key || seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
blocks.push(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateMergedInstructionText(mergedText, requiredBlocks) {
|
||||||
|
const candidate = normalizeText(mergedText);
|
||||||
|
return requiredBlocks.every(block => candidate.includes(normalizeText(block).trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
class InstructionMergeError extends Error {
|
||||||
|
constructor(message, options) {
|
||||||
|
super(message, options);
|
||||||
|
this.name = 'InstructionMergeError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function abortInstructionMerge(message) {
|
||||||
|
error(message);
|
||||||
|
process.exit(1);
|
||||||
|
throw new InstructionMergeError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncFileOverwrite(sourceRoot, repoDir, relPath) {
|
||||||
|
const src = path.join(sourceRoot, relPath);
|
||||||
|
if (!fs.existsSync(src)) return null;
|
||||||
|
|
||||||
|
const dest = path.join(repoDir, relPath);
|
||||||
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||||
|
fs.copyFileSync(src, dest);
|
||||||
|
return relPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getInstructionMergeAssistant() {
|
||||||
|
const { provider } = getLLMConfig();
|
||||||
|
if (!provider) return null;
|
||||||
|
if (instructionMergeAssistantPromise) return instructionMergeAssistantPromise;
|
||||||
|
|
||||||
|
instructionMergeAssistantPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const { chatJSON } = await import('./llm.js');
|
||||||
|
return async ({ relPath, existingText, sourceText, deterministicText }) => {
|
||||||
|
const systemPrompt = [
|
||||||
|
'You merge repository instruction files without losing any skill, command, or rule.',
|
||||||
|
'Never delete unique content from either input.',
|
||||||
|
'You may only remove exact duplicates or improve ordering/formatting.',
|
||||||
|
'Return JSON with a single field: merged_text.',
|
||||||
|
].join(' ');
|
||||||
|
const userContent = JSON.stringify({
|
||||||
|
path: relPath,
|
||||||
|
existing_text: existingText,
|
||||||
|
source_text: sourceText,
|
||||||
|
deterministic_candidate: deterministicText,
|
||||||
|
});
|
||||||
|
const result = await chatJSON(systemPrompt, userContent);
|
||||||
|
if (typeof result === 'string') return result;
|
||||||
|
if (result && typeof result.merged_text === 'string') return result.merged_text;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
warn(`[merge] AI instruction merge unavailable: ${e.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return instructionMergeAssistantPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function mergeInstructionText(existingText, sourceText, relPath, aiMergeAssistant = null) {
|
||||||
|
const deterministic = mergeText(existingText, sourceText);
|
||||||
|
const requiredBlocks = uniqueBlocksFromTexts(existingText, sourceText);
|
||||||
|
if (!aiMergeAssistant || requiredBlocks.length === 0) return deterministic;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const aiMerged = await aiMergeAssistant({ relPath, existingText, sourceText, deterministicText: deterministic, requiredBlocks });
|
||||||
|
if (typeof aiMerged === 'string' && validateMergedInstructionText(aiMerged, requiredBlocks)) {
|
||||||
|
return normalizeText(aiMerged) === normalizeText(existingText) ? existingText : aiMerged;
|
||||||
|
}
|
||||||
|
abortInstructionMerge(`[merge] ${relPath} AI result rejected; refusing fallback`);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof InstructionMergeError) throw e;
|
||||||
|
abortInstructionMerge(`[merge] ${relPath} AI merge failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncInstructionFile(sourceRoot, repoDir, relPath, aiMergeAssistant = null) {
|
||||||
|
const src = path.join(sourceRoot, relPath);
|
||||||
|
if (!fs.existsSync(src)) return null;
|
||||||
|
|
||||||
|
const dest = path.join(repoDir, relPath);
|
||||||
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||||
|
|
||||||
|
if (!fs.existsSync(dest)) {
|
||||||
|
fs.copyFileSync(src, dest);
|
||||||
|
return relPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingText = fs.readFileSync(dest, 'utf8');
|
||||||
|
const sourceText = fs.readFileSync(src, 'utf8');
|
||||||
|
const merged = await mergeInstructionText(existingText, sourceText, relPath, aiMergeAssistant);
|
||||||
|
if (merged !== existingText) {
|
||||||
|
fs.writeFileSync(dest, merged, 'utf8');
|
||||||
|
}
|
||||||
|
return relPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncTree(sourceRoot, repoDir, relDir) {
|
||||||
const srcDir = path.join(sourceRoot, relDir);
|
const srcDir = path.join(sourceRoot, relDir);
|
||||||
if (!fs.existsSync(srcDir)) return [];
|
if (!fs.existsSync(srcDir)) return [];
|
||||||
|
|
||||||
const copied = [];
|
const copied = [];
|
||||||
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
||||||
const relPath = path.join(relDir, entry.name);
|
const relPath = path.join(relDir, entry.name);
|
||||||
const src = path.join(sourceRoot, relPath);
|
|
||||||
const dest = path.join(repoDir, relPath);
|
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
copied.push(...copyTree(sourceRoot, repoDir, relPath));
|
copied.push(...syncTree(sourceRoot, repoDir, relPath));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
const synced = syncFileOverwrite(sourceRoot, repoDir, relPath);
|
||||||
fs.copyFileSync(src, dest);
|
if (synced) copied.push(synced);
|
||||||
copied.push(relPath);
|
|
||||||
}
|
}
|
||||||
return copied;
|
return copied;
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyFileOverwrite(sourceRoot, repoDir, relPath) {
|
|
||||||
const src = path.join(sourceRoot, relPath);
|
|
||||||
if (!fs.existsSync(src)) return null;
|
|
||||||
const dest = path.join(repoDir, relPath);
|
|
||||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
||||||
fs.copyFileSync(src, dest);
|
|
||||||
return relPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRepoState(repoDir, _spawnSync = spawnSync) {
|
export function getRepoState(repoDir, _spawnSync = spawnSync) {
|
||||||
const run = makeRunner(_spawnSync);
|
const run = makeRunner(_spawnSync);
|
||||||
const headSha = readGitOutput(run, ['rev-parse', 'HEAD'], repoDir);
|
const headSha = readGitOutput(run, ['rev-parse', 'HEAD'], repoDir);
|
||||||
@@ -142,26 +291,30 @@ export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const existingSyncPaths = new Set();
|
const existingSyncPaths = new Set();
|
||||||
|
const aiMergeAssistant = await getInstructionMergeAssistant();
|
||||||
|
|
||||||
// Copy action skill trees into the target repo. Existing files are overwritten;
|
// Copy action skill trees into the target repo. Existing files are merged with
|
||||||
// missing source files are ignored so we do not delete target repo content.
|
// the action source; missing source files are ignored so we do not delete
|
||||||
|
// target repo content.
|
||||||
for (const relDir of SYNC_TREE_PATHS) {
|
for (const relDir of SYNC_TREE_PATHS) {
|
||||||
for (const relPath of copyTree(sourceRoot, repoDir, relDir)) {
|
for (const relPath of syncTree(sourceRoot, repoDir, relDir)) {
|
||||||
existingSyncPaths.add(relPath);
|
existingSyncPaths.add(relPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force overwrite the direct instruction files first so the target repo always
|
// Merge only the direct instruction files that must preserve repository-specific
|
||||||
// receives the action-owned versions even if the repo has drifted.
|
// skills, commands, and rules. Everything else keeps the source copy.
|
||||||
for (const relPath of FORCE_SYNC_FILE_PATHS) {
|
for (const relPath of FORCE_SYNC_FILE_PATHS) {
|
||||||
const copied = copyFileOverwrite(sourceRoot, repoDir, relPath);
|
const copied = MERGE_SYNC_FILE_PATHS.has(relPath)
|
||||||
|
? await syncInstructionFile(sourceRoot, repoDir, relPath, aiMergeAssistant)
|
||||||
|
: syncFileOverwrite(sourceRoot, repoDir, relPath);
|
||||||
if (copied) existingSyncPaths.add(copied);
|
if (copied) existingSyncPaths.add(copied);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy standalone action files into the target repo. Existing files are overwritten.
|
// Merge standalone action files into the target repo.
|
||||||
for (const relPath of SYNC_PATHS) {
|
for (const relPath of SYNC_PATHS) {
|
||||||
if (FORCE_SYNC_FILE_PATHS.includes(relPath)) continue;
|
if (FORCE_SYNC_FILE_PATHS.includes(relPath)) continue;
|
||||||
const copied = copyFileOverwrite(sourceRoot, repoDir, relPath);
|
const copied = syncFileOverwrite(sourceRoot, repoDir, relPath);
|
||||||
if (copied) existingSyncPaths.add(copied);
|
if (copied) existingSyncPaths.add(copied);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+66
-17
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { commitAndPush, cloneRepo, SYNC_PATHS, BOT_COMMIT_MARKER, getHeadCommitMessage, isBotAutoCommit } from './git.js';
|
import { commitAndPush, cloneRepo, SYNC_PATHS, BOT_COMMIT_MARKER, getHeadCommitMessage, isBotAutoCommit, mergeInstructionText } from './git.js';
|
||||||
|
|
||||||
// --- helpers ---
|
// --- helpers ---
|
||||||
function makeTmpWorkspace() {
|
function makeTmpWorkspace() {
|
||||||
@@ -130,10 +130,14 @@ describe('commitAndPush', () => {
|
|||||||
assert.ok(generatedAddCall, 'expected git add for generated review files');
|
assert.ok(generatedAddCall, 'expected git add for generated review files');
|
||||||
assert.ok(skillAddCall.args.includes('.codex/skills/triage-findings/SKILL.md'));
|
assert.ok(skillAddCall.args.includes('.codex/skills/triage-findings/SKILL.md'));
|
||||||
assert.ok(skillAddCall.args.includes('.codex/skills/triage-findings/agents/openai.yaml'));
|
assert.ok(skillAddCall.args.includes('.codex/skills/triage-findings/agents/openai.yaml'));
|
||||||
|
assert.ok(skillAddCall.args.includes('.agents/skills/triage-findings/SKILL.md'));
|
||||||
assert.ok(skillAddCall.args.includes('.claude/skills/triage-findings/SKILL.md'));
|
assert.ok(skillAddCall.args.includes('.claude/skills/triage-findings/SKILL.md'));
|
||||||
assert.ok(skillAddCall.args.includes('.gemini/skills/triage-findings/SKILL.md'));
|
assert.ok(skillAddCall.args.includes('.gemini/skills/triage-findings/SKILL.md'));
|
||||||
|
assert.ok(skillAddCall.args.includes('.antigravity/skills/triage-findings/SKILL.md'));
|
||||||
assert.ok(skillAddCall.args.includes('.github/copilot-instructions.md'));
|
assert.ok(skillAddCall.args.includes('.github/copilot-instructions.md'));
|
||||||
assert.ok(skillAddCall.args.includes('.amazonq/rules/triage-findings.md'));
|
assert.ok(skillAddCall.args.includes('.amazonq/rules/triage-findings.md'));
|
||||||
|
assert.ok(skillAddCall.args.includes('AGENTS.md'));
|
||||||
|
assert.ok(skillAddCall.args.includes('ANTIGRAVITY.md'));
|
||||||
assert.ok(skillAddCall.args.includes('CLAUDE.md'));
|
assert.ok(skillAddCall.args.includes('CLAUDE.md'));
|
||||||
assert.ok(skillAddCall.args.includes('GEMINI.md'));
|
assert.ok(skillAddCall.args.includes('GEMINI.md'));
|
||||||
assert.ok(!skillAddCall.args.includes('README.md'));
|
assert.ok(!skillAddCall.args.includes('README.md'));
|
||||||
@@ -155,34 +159,79 @@ describe('commitAndPush', () => {
|
|||||||
assert.equal(fs.readFileSync(repoPath, 'utf8'), 'stale');
|
assert.equal(fs.readFileSync(repoPath, 'utf8'), 'stale');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('overwrites existing repo copies with workspace files', async () => {
|
it('merges existing repo copies with workspace files', async () => {
|
||||||
const repoDir = path.join(workspace, 'repo');
|
const repoDir = path.join(workspace, 'repo');
|
||||||
fs.writeFileSync(path.join(repoDir, '.github/skills/triage-findings/SKILL.md'), 'stale');
|
fs.writeFileSync(path.join(repoDir, 'AGENTS.md'), 'repo agents doc');
|
||||||
fs.writeFileSync(path.join(repoDir, 'CLAUDE.md'), 'stale');
|
fs.writeFileSync(path.join(repoDir, 'ANTIGRAVITY.md'), 'repo antigravity doc');
|
||||||
fs.writeFileSync(path.join(repoDir, 'GEMINI.md'), 'stale');
|
fs.writeFileSync(path.join(repoDir, 'CLAUDE.md'), 'repo claude doc');
|
||||||
fs.writeFileSync(path.join(repoDir, '.github/copilot-instructions.md'), 'stale');
|
fs.writeFileSync(path.join(repoDir, 'GEMINI.md'), 'repo gemini doc');
|
||||||
|
|
||||||
await commitAndPush(workspace, repoDir, makeSpawn(), sourceRoot);
|
await commitAndPush(workspace, repoDir, makeSpawn(), sourceRoot);
|
||||||
|
|
||||||
assert.equal(fs.readFileSync(path.join(repoDir, '.github/skills/triage-findings/SKILL.md'), 'utf8'), '.github/skills/triage-findings/SKILL.md');
|
const agentsDoc = fs.readFileSync(path.join(repoDir, 'AGENTS.md'), 'utf8');
|
||||||
assert.equal(fs.readFileSync(path.join(repoDir, 'CLAUDE.md'), 'utf8'), 'CLAUDE.md');
|
const antigravityDoc = fs.readFileSync(path.join(repoDir, 'ANTIGRAVITY.md'), 'utf8');
|
||||||
assert.equal(fs.readFileSync(path.join(repoDir, 'GEMINI.md'), 'utf8'), 'GEMINI.md');
|
const claudeDoc = fs.readFileSync(path.join(repoDir, 'CLAUDE.md'), 'utf8');
|
||||||
assert.equal(fs.readFileSync(path.join(repoDir, '.github/copilot-instructions.md'), 'utf8'), '.github/copilot-instructions.md');
|
const geminiDoc = fs.readFileSync(path.join(repoDir, 'GEMINI.md'), 'utf8');
|
||||||
|
|
||||||
|
assert.ok(agentsDoc.includes('repo agents doc'));
|
||||||
|
assert.ok(agentsDoc.includes('AGENTS.md'));
|
||||||
|
assert.ok(antigravityDoc.includes('repo antigravity doc'));
|
||||||
|
assert.ok(antigravityDoc.includes('ANTIGRAVITY.md'));
|
||||||
|
assert.ok(claudeDoc.includes('repo claude doc'));
|
||||||
|
assert.ok(claudeDoc.includes('CLAUDE.md'));
|
||||||
|
assert.ok(geminiDoc.includes('repo gemini doc'));
|
||||||
|
assert.ok(geminiDoc.includes('GEMINI.md'));
|
||||||
|
assert.ok(agentsDoc.includes('repo agents doc'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('recursively overwrites skill tree files from the action source', async () => {
|
it('accepts AI merged instruction text when all unique blocks are preserved', async () => {
|
||||||
|
const calls = [];
|
||||||
|
const aiMergeAssistant = async payload => {
|
||||||
|
calls.push(payload);
|
||||||
|
return ['repo block', 'source block', 'extra block'].join('\n\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await mergeInstructionText('repo block', 'source block', 'AGENTS.md', aiMergeAssistant);
|
||||||
|
|
||||||
|
assert.equal(calls.length, 1);
|
||||||
|
assert.ok(result.includes('repo block'));
|
||||||
|
assert.ok(result.includes('source block'));
|
||||||
|
assert.ok(result.includes('extra block'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exits when AI output drops a block', async () => {
|
||||||
|
const originalExit = process.exit;
|
||||||
|
let exitCode = null;
|
||||||
|
process.exit = code => { exitCode = code; };
|
||||||
|
try {
|
||||||
|
const aiMergeAssistant = async () => 'source block only';
|
||||||
|
await assert.rejects(() => mergeInstructionText('repo block', 'source block', 'AGENTS.md', aiMergeAssistant));
|
||||||
|
assert.equal(exitCode, 1);
|
||||||
|
} finally {
|
||||||
|
process.exit = originalExit;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('overwrites non-merge sync files with workspace files', async () => {
|
||||||
const repoDir = path.join(workspace, 'repo');
|
const repoDir = path.join(workspace, 'repo');
|
||||||
const nestedRelPath = '.codex/skills/triage-findings/assets/example.txt';
|
const sourceSkillPath = path.join(sourceRoot, '.github/skills/triage-findings/SKILL.md');
|
||||||
const sourceNestedPath = path.join(sourceRoot, nestedRelPath);
|
const repoSkillPath = path.join(repoDir, '.github/skills/triage-findings/SKILL.md');
|
||||||
const repoNestedPath = path.join(repoDir, nestedRelPath);
|
const sourceNestedPath = path.join(sourceRoot, '.codex/skills/triage-findings/assets/example.txt');
|
||||||
|
const repoNestedPath = path.join(repoDir, '.codex/skills/triage-findings/assets/example.txt');
|
||||||
|
|
||||||
|
fs.writeFileSync(sourceSkillPath, 'fresh github skill');
|
||||||
|
fs.writeFileSync(repoSkillPath, 'stale github skill');
|
||||||
fs.mkdirSync(path.dirname(sourceNestedPath), { recursive: true });
|
fs.mkdirSync(path.dirname(sourceNestedPath), { recursive: true });
|
||||||
fs.writeFileSync(sourceNestedPath, 'fresh');
|
fs.writeFileSync(sourceNestedPath, 'fresh nested');
|
||||||
fs.mkdirSync(path.dirname(repoNestedPath), { recursive: true });
|
fs.mkdirSync(path.dirname(repoNestedPath), { recursive: true });
|
||||||
fs.writeFileSync(repoNestedPath, 'stale');
|
fs.writeFileSync(repoNestedPath, 'stale nested');
|
||||||
|
fs.writeFileSync(path.join(repoDir, '.github/copilot-instructions.md'), 'stale copilot');
|
||||||
|
|
||||||
await commitAndPush(workspace, repoDir, makeSpawn(), sourceRoot);
|
await commitAndPush(workspace, repoDir, makeSpawn(), sourceRoot);
|
||||||
|
|
||||||
assert.equal(fs.readFileSync(repoNestedPath, 'utf8'), 'fresh');
|
assert.equal(fs.readFileSync(repoSkillPath, 'utf8'), 'fresh github skill');
|
||||||
|
assert.equal(fs.readFileSync(repoNestedPath, 'utf8'), 'fresh nested');
|
||||||
|
assert.equal(fs.readFileSync(path.join(repoDir, '.github/copilot-instructions.md'), 'utf8'), '.github/copilot-instructions.md');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not throw when git command fails', async () => {
|
it('does not throw when git command fails', async () => {
|
||||||
|
|||||||
@@ -26,11 +26,15 @@ export async function getPRDiff() {
|
|||||||
const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}.diff`), { headers: headers(), timeout: 60000, httpsAgent });
|
const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}.diff`), { headers: headers(), timeout: 60000, httpsAgent });
|
||||||
return filterDiff(resp.data, [
|
return filterDiff(resp.data, [
|
||||||
'.amazonq/',
|
'.amazonq/',
|
||||||
|
'.agents/',
|
||||||
|
'.antigravity/',
|
||||||
'.claude/',
|
'.claude/',
|
||||||
'.codex/',
|
'.codex/',
|
||||||
'.gemini/',
|
'.gemini/',
|
||||||
'.gitea/',
|
'.gitea/',
|
||||||
'.github/',
|
'.github/',
|
||||||
|
'AGENTS.md',
|
||||||
|
'ANTIGRAVITY.md',
|
||||||
'CLAUDE.md',
|
'CLAUDE.md',
|
||||||
'GEMINI.md',
|
'GEMINI.md',
|
||||||
'README.md',
|
'README.md',
|
||||||
|
|||||||
+2
-2
@@ -119,8 +119,8 @@ describe('filterDiff', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns empty string when all blocks are excluded', () => {
|
it('returns empty string when all blocks are excluded', () => {
|
||||||
const diff = block('.gitea/workflows/review.yaml') + block('.gitea/ai-review/findings.json') + block('CLAUDE.md');
|
const diff = block('.gitea/workflows/review.yaml') + block('.gitea/ai-review/findings.json') + block('.agents/skills/triage-findings/SKILL.md');
|
||||||
const result = filterDiff(diff, ['.gitea/', 'CLAUDE.md']);
|
const result = filterDiff(diff, ['.gitea/', '.agents/']);
|
||||||
assert.equal(result, '');
|
assert.equal(result, '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user