Compare commits

...

36 Commits

Author SHA1 Message Date
jiantw83 49f190e944 Merge pull request 'feat: implement Git integration for automated repository instruction syncing and commit management' (#131) from develop into master
Reviewed-on: #131
2026-05-21 04:00:28 +00:00
jiantw83 9af09de0d3 Merge pull request 'feat: implement Git integration for automated repository instruction syncing and commit management' (#130) from feat/ai_merge into develop
Reviewed-on: #130
2026-05-21 03:56:41 +00:00
Jeffery fbff9b3a86 chore: initialize ai-review exclusion and findings configuration files 2026-05-21 11:52:18 +08:00
jiantw83 7a01b7e3f4 Merge pull request 'feat: 加入 Codex 的 Triage Findings 技能' (#129) from feat/codex into develop
Reviewed-on: #129
2026-05-21 03:36:41 +00:00
Jeffery 097b6fb721 feat: implement Git integration for automated repository instruction syncing and commit management 2026-05-21 11:36:11 +08:00
AI Review Bot adf37520cb chore: update ai-review findings [ai-review-bot][success] 2026-05-21 03:35:13 +00:00
Jeffery e99236b893 feat: implement git repository synchronization and automated commit functionality for AI review findings 2026-05-21 10:17:01 +08:00
Jeffery 43ebc81f1d feat: add triage-findings agent skill and documentation for issue resolution workflow 2026-05-21 09:34:47 +08:00
jiantw83 72701dee0a Merge pull request 'feat: add SKILL.md for triage-findings documentation' (#128) from develop into master
Reviewed-on: #128
2026-05-20 09:11:03 +00:00
jiantw83 f55264bb18 Merge pull request 'feat: add SKILL.md for triage-findings documentation' (#127) from feat/amazon_q into develop
Reviewed-on: #127
2026-05-20 09:10:16 +00:00
Jeffery 0d4776888f feat: add SKILL.md for triage-findings documentation 2026-05-20 17:09:11 +08:00
jiantw83 503e50a2d0 Merge pull request 'feat: 將 ANTIGRAVITY 加入程式與技能' (#126) from develop into master
Reviewed-on: #126
2026-05-20 02:56:21 +00:00
jiantw83 e3ae1bc10e Merge pull request 'feat: 將 ANTIGRAVITY 加入程式與技能' (#125) from feat/ANTIGRAVITY into develop
Reviewed-on: #125
2026-05-20 02:55:48 +00:00
Jeffery e80a462d96 feat: 將 ANTIGRAVITY 加入程式 2026-05-20 10:49:34 +08:00
Jeffery d818baffa7 feat: 複製 triage-findings 給 ANTIGRAVITY 使用 2026-05-20 10:33:59 +08:00
Jeffery c24f2e00e2 feat: 同步所有平台的技能 2026-05-20 10:31:59 +08:00
jiantw83 dddcc9031b Merge pull request 'develop' (#124) from develop into master
Reviewed-on: #124
2026-05-18 03:32:00 +00:00
jiantw83 fc02cda577 Merge pull request 'docs: align README and TODO with current flow' (#123) from feat/優化AI排除問題與過濾 into develop
Reviewed-on: #123
2026-05-18 03:31:26 +00:00
jiantw83 ace50037ba Merge pull request 'feat: 優化AI排除問題與過濾' (#122) from develop into master
Reviewed-on: #122
2026-05-18 02:59:46 +00:00
jiantw83 5afe8a2119 Merge pull request 'feat: 優化AI排除問題與過濾' (#121) from feat/優化AI排除問題與過濾 into develop
Reviewed-on: #121
2026-05-18 02:58:02 +00:00
jiantw83 76eaff7788 Merge pull request '版本 0.1.6' (#120) from develop into master
Reviewed-on: #120
2026-05-15 15:57:20 +00:00
jiantw83 6ac8512dbc Merge pull request 'fix: remove GITEA_TOKEN from AI Code Review step and ensure master branch is ignored in pull requestsfix: remove GITEA_TOKEN from AI Code Review step and ensure master branch is ignored in pull requests' (#116) from develop into master
Reviewed-on: #116
2026-05-15 09:56:51 +00:00
jiantw83 3b8e942e7f Merge pull request 'feat: enhance findings and exclusions handling with repo state logging' (#114) from develop into master
Reviewed-on: #114
2026-05-15 09:52:26 +00:00
jiantw83 051457b11b Merge pull request 'fix: clarify stage seven push failures' (#112) from develop into master
Reviewed-on: #112
2026-05-15 06:55:50 +00:00
jiantw83 92f1c6fe82 Merge pull request 'fix: support wrapped exclusions schema' (#111) from develop into master
Reviewed-on: #111
2026-05-15 06:46:28 +00:00
jiantw83 27df6894a4 Merge pull request 'fix: write findings to review dir' (#110) from develop into master
Reviewed-on: #110
2026-05-15 06:25:29 +00:00
jiantw83 1afd978059 Merge pull request 'fix: stage generated review files' (#109) from develop into master
Reviewed-on: #109
2026-05-15 05:53:55 +00:00
jiantw83 146faca7cb Merge pull request 'docs: preserve original text in exclusions' (#108) from develop into master
Reviewed-on: #108
2026-05-15 04:51:23 +00:00
jiantw83 4c99247566 Merge pull request 'fix: sync codex skill assets' (#107) from develop into master
Reviewed-on: #107
2026-05-15 04:24:32 +00:00
jiantw83 81cbb83340 Merge pull request 'fix: package triage skills into the action image' (#106) from develop into master
Reviewed-on: #106
2026-05-15 04:00:55 +00:00
jiantw83 3f65b72cf0 Merge pull request 'fix: restore triage skill files and keep sync non-destructive' (#104) from develop into master
Reviewed-on: #104
2026-05-15 03:34:26 +00:00
jiantw83 2eb94c8f74 Merge pull request 'feat: 解決階段七commit失敗的問題' (#102) from develop into master
Reviewed-on: #102
2026-05-15 03:18:55 +00:00
jiantw83 6354c0987c Merge pull request 'chore: refine stage 7 json validation' (#98) from develop into master
Reviewed-on: #98
2026-05-14 02:42:13 +00:00
jiantw83 7df34eb1d0 Merge pull request '版本 0.0.4' (#97) from develop into master
Reviewed-on: #97
2026-05-13 06:31:30 +00:00
jiantw83 ca5d54882f Merge pull request '版本 0.0.2' (#94) from develop into master
Reviewed-on: #94
2026-05-13 02:43:10 +00:00
jiantw83 ca4664e0cc Merge pull request '發布 0.0.1' (#86) from develop into master
Reviewed-on: #86
2026-05-12 10:09:32 +00:00
18 changed files with 859 additions and 435 deletions
+46
View File
@@ -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.
+41
View File
@@ -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.
+29 -13
View File
@@ -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.
+29 -13
View File
@@ -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.
+8 -1
View File
@@ -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
} }
] ]
+1 -1
View File
@@ -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`.
+37 -10
View File
@@ -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`.
+14
View File
@@ -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`.
+1 -1
View File
@@ -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`.
+2
View File
@@ -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/
+3 -2
View File
@@ -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 與目標專案內容脫節。
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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 () => {
+4
View File
@@ -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
View File
@@ -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, '');
}); });