diff --git a/.agents/skills/triage-findings/SKILL.md b/.agents/skills/triage-findings/SKILL.md deleted file mode 100644 index 2422e52..0000000 --- a/.agents/skills/triage-findings/SKILL.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -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. diff --git a/.amazonq/skills/triage-findings/SKILL.md b/.amazonq/skills/triage-findings/SKILL.md deleted file mode 100644 index d747008..0000000 --- a/.amazonq/skills/triage-findings/SKILL.md +++ /dev/null @@ -1,41 +0,0 @@ -# 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. diff --git a/.antigravity/skills/triage-findings/SKILL.md b/.antigravity/skills/triage-findings/SKILL.md deleted file mode 100644 index 2422e52..0000000 --- a/.antigravity/skills/triage-findings/SKILL.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -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. diff --git a/.claude/skills/triage-findings/SKILL.md b/.claude/skills/triage-findings/SKILL.md deleted file mode 100644 index 2422e52..0000000 --- a/.claude/skills/triage-findings/SKILL.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -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. diff --git a/.codex/skills/triage-findings/SKILL.md b/.codex/skills/triage-findings/SKILL.md deleted file mode 100644 index 2422e52..0000000 --- a/.codex/skills/triage-findings/SKILL.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -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. diff --git a/.codex/skills/triage-findings/agents/openai.yaml b/.codex/skills/triage-findings/agents/openai.yaml deleted file mode 100644 index 9b2c3ea..0000000 --- a/.codex/skills/triage-findings/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Triage Findings" - short_description: "Triage, sort, fix, and exclude review findings" - default_prompt: "Use $triage-findings to merge review findings, sort and renumber them by severity, resolve real issues one by one, and add false positives to `.gitea/ai-review/exclusions.json` as a top-level JSON array." diff --git a/.gemini/skills/triage-findings/SKILL.md b/.gemini/skills/triage-findings/SKILL.md deleted file mode 100644 index 2422e52..0000000 --- a/.gemini/skills/triage-findings/SKILL.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -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. diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index b6dc98d..3c5766e 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -169,18 +169,6 @@ "location": "app/llm.js", "suggestion": "此 action 為 CLI 工具,process.exit(1) 是設計意圖讓 CI/CD workflow 失敗。改拋錯會被 chatJSON 的 catch 吞掉回傳 [],破壞現有行為" }, - { - "location": "Dockerfile, app/git.js, app/git.test.js", - "suggestion": "`SYNC_PATHS` 已包含 `.claude/skills/triage-findings/SKILL.md` 與 `.gemini/skills/triage-findings/SKILL.md`,Docker image 也已打包這些 skill 資產;現有測試已覆蓋複製與覆寫行為,並不存在同步不一致問題。" - }, - { - "location": "Dockerfile", - "suggestion": "此目錄中的檔案是 triage skill 與入口文件,不含敏感資料;若未來加入秘密資訊,應另外從 build context 排除,而不是把目前的 skill 資產視為風險。" - }, - { - "location": "Dockerfile", - "suggestion": "多個 COPY 指令是刻意設計,用來區分 app 與 skill 資產並維持 layer cache 可讀性,不是維護問題。" - }, { "role": "Bard", "location": "Dockerfile", @@ -345,13 +333,6 @@ "location": "app/log.js", "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 - }, { "role": "Assassin", "location": "app/preflight.js:12", @@ -400,4 +381,4 @@ "location": "app/preflight.test.js:14", "suggestion": "函數名稱 clearLLMEnv 雖然可理解,但可以更具描述性,例如 clearLlmEnvironmentVariables 或 resetLlmEnv。" } -] \ No newline at end of file +] diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index ca2d3a9..0000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,14 +0,0 @@ -# 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 full reusable skill lives in `.github/skills/triage-findings/SKILL.md`. diff --git a/.github/skills/triage-findings/SKILL.md b/.github/skills/triage-findings/SKILL.md deleted file mode 100644 index d747008..0000000 --- a/.github/skills/triage-findings/SKILL.md +++ /dev/null @@ -1,41 +0,0 @@ -# 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. diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index fa2403d..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,16 +0,0 @@ -# Triage Findings - -When the task is to triage review findings, follow this workflow: - -1. Merge all findings into one list. -2. Remove duplicates. -3. Sort by severity: `critical` -> `warning` -> `info`. -4. Renumber from 1 after sorting. -5. Fix real issues with the smallest safe change. -6. Add false positives to `.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 the issue after each fix. - -Use the repo-local `triage-findings` skill for the same workflow when running in Codex. - -Trigger it with `/triage-findings`. diff --git a/ANTIGRAVITY.md b/ANTIGRAVITY.md deleted file mode 100644 index 70b8f1d..0000000 --- a/ANTIGRAVITY.md +++ /dev/null @@ -1,14 +0,0 @@ -# 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`. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index e1c3938..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,16 +0,0 @@ -# Triage Findings - -When the task is to triage review findings, follow this workflow: - -1. Merge all findings into one list. -2. Remove duplicates. -3. Sort by severity: `critical` -> `warning` -> `info`. -4. Renumber from 1 after sorting. -5. Fix real issues with the smallest safe change. -6. Add false positives to `.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 the issue after each fix. - -Use the repo-local `triage-findings` skill for the same workflow when running in Claude. - -Trigger it with `/triage-findings`. diff --git a/Dockerfile b/Dockerfile index b3c58fe..ee322d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,16 +10,6 @@ WORKDIR /action COPY app/package.json /action/app/ RUN cd /action/app && npm install -COPY .amazonq/ /action/.amazonq/ -COPY .codex/ /action/.codex/ -COPY .agents/ /action/.agents/ -COPY .claude/ /action/.claude/ -COPY .gemini/ /action/.gemini/ -COPY .github/ /action/.github/ -COPY AGENTS.md /action/ -COPY CLAUDE.md /action/ -COPY GEMINI.md /action/ - COPY app/ /action/app/ COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/GEMINI.md b/GEMINI.md deleted file mode 100644 index 8ca4117..0000000 --- a/GEMINI.md +++ /dev/null @@ -1,14 +0,0 @@ -# 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 `.gemini/skills/triage-findings/SKILL.md`. diff --git a/README.md b/README.md index 0e48726..e956074 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ 5. 從 PR 問題表格中取出所有舊問題,依照等級排序後 Comment 到 Pull Request 6. 從 PR 問題表格中取出所有新問題,排除嚴重等級的問題後 Comment 到 Pull Request 7. 從 PR 問題表格中取出所有新問題,將每個嚴重等級的問題以 Gitea 行內 review comment 標註在問題所在的檔案與行數上,留言內容為等級/審查員/建議;若問題位置無法解析出行號(例如未標行號或一次列出多個檔案),或該行不在本次 diff 範圍內導致行內留言失敗,則降級為一般 PR Comment -8. Commit 問題檔案,將 workspace 中實際存在的同步檔覆蓋到記憶區;workspace 沒有的同步檔就略過,不會刪除記憶區既有內容。自動提交的 commit message 會帶上 `[ai-review-bot]`,供 workflow 判斷是否要跳過重跑 +8. Commit 問題檔案,只將 workspace 中實際存在的 `.gitea/ai-review/findings.json` 與 `.gitea/ai-review/exclusions.json` 覆蓋到記憶區;workspace 沒有的問題檔就略過。自動提交的 commit message 會帶上 `[ai-review-bot]`,供 workflow 判斷是否要跳過重跑 9. 如果 PR 問題表格中有嚴重問題,則不要讓 workflow 執行成功(exit 1) # 設計 @@ -28,7 +28,7 @@ 4. 盡量將應用程式放在 ./app,修改 entrypoint.sh 與 Dockerfile 讓程式可以正常運行 5. 將提示詞放到 ./app/prompts 內供程式讀取 6. API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,全部失敗則 exit 1 -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 入口與文件等非業務程式碼 +7. 讀取 Git Diff 時排除 `.gitea/`、`.github/` 資料夾,以及 `TODO.md`、`README.md`,避免 AI 分析 workflow 設定與文件等非業務程式碼 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 用量 10. 執行時會額外記錄來源分支狀態、`findings.json` / `exclusions.json` 的檔案路徑、大小、mtime 與 raw/normalized 筆數,方便追查讀檔與分支內容不一致的問題 @@ -276,33 +276,3 @@ jobs: pull-requests: write issues: write ``` - -## Skill:Triage Findings - -這份 skill 用來處理 review 問題清單。 - -### 規則 - -1. 合併問題。 -2. 依嚴重度排序:`critical` -> `warning` -> `info`。 -3. 重新編號。 -4. 真問題就修。 -5. 誤判就加到 `.gitea/ai-review/exclusions.json`。 -6. 有變更就補測試。 - -### 使用方式 - -Codex:`$triage-findings 問題原始檔(文字或截圖)` -Copilot:`/triage-findings 問題原始檔(文字或截圖)` -Claude:直接輸入 `triage-findings 問題原始檔(文字或截圖)` -Gemini:直接輸入 `triage-findings 問題原始檔(文字或截圖)` -Amazon Q:直接輸入 `triage-findings 問題原始檔(文字或截圖)` -Antigravity:直接輸入 `triage-findings 問題原始檔(文字或截圖)` - -### 適用情境 - -`triage-findings 問題原始檔(文字或截圖)` 用在 review 問題整併、排序、修正、排除誤判。 - -### 版本包含 - -提交時一併包含 `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 與目標專案內容脫節。 diff --git a/TODO.md b/TODO.md index 17e570a..4703e54 100644 --- a/TODO.md +++ b/TODO.md @@ -6,7 +6,7 @@ - 已驗收:`code-review` job 的 log 已完整出現 `Step1` 到 `Step8`,並以 `Pipeline 完成` 結束。 ## 階段二:Git Diff 排除 .gitea/ 資料夾 -- 目標:讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,以及 `.amazonq/`、`.antigravity/`、`.claude/`、`.codex/`、`.gemini/`、`.github/`、`ANTIGRAVITY.md`、`CLAUDE.md`、`GEMINI.md`、`TODO.md`、`README.md`,避免 AI 分析 workflow 設定、skill 入口與文件等非業務程式碼。 +- 目標:讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,以及 `.github/`、`TODO.md`、`README.md`,避免 AI 分析 workflow 設定與文件等非業務程式碼。 - 驗收:PR 中有上述路徑或檔案的變更時,diff 內容不包含該區塊,AI 分析結果不含這些路徑相關問題。 - 已驗收:`app/gitea.js` 已在取得 diff 時過濾 `.gitea/` 區塊,且相關單元測試已覆蓋。 @@ -38,9 +38,9 @@ - 已驗收:log 已明確顯示 `.gitea/ai-review/findings.json` 與 `.gitea/ai-review/exclusions.json` 都是 `JSON 格式正確`。 ## 階段八:記憶區 commit/push 與錯誤處理 -- 目標:記憶區能成功 commit/push,且一併包含 `triage-findings` skill 與各平台入口檔;`AGENTS.md`、`ANTIGRAVITY.md`、`CLAUDE.md`、`GEMINI.md` 在目標專案已存在時會先做規則化合併,並在可用 LLM 時再做 AI 輔助檢查以避免遺失 skill、command 或規則;其餘同步檔則以來源覆蓋;workspace 沒有的同步檔則保留記憶區既有內容,不做刪除;錯誤時有明確 log,流程結束有總結訊息。 -- 驗收:log 有「persisted findings」、「commit=...」、「push=...」等訊息,且能看出四個入口檔會先規則合併、再由 AI 輔助檢查,其他同步檔會被來源覆蓋;當 workspace 缺少某個同步檔時,記憶區中的對應檔案不會被刪除;錯誤時有「Runner failed: ...」等明確錯誤說明。 -- 已驗收:commit/push 成功時會出現 `persisted findings commit=... push=... review_outcome=...`,且同步規則與缺檔不刪除的行為都有單元測試覆蓋。 +- 目標:記憶區能成功 commit/push,且只提交 workspace 中實際存在的 `.gitea/ai-review/findings.json` 與 `.gitea/ai-review/exclusions.json`;workspace 沒有的問題檔就略過;錯誤時有明確 log,流程結束有總結訊息。 +- 驗收:log 有「persisted findings」、「commit=...」、「push=...」等訊息;git add 只包含新的問題檔;錯誤時有「Runner failed: ...」等明確錯誤說明。 +- 已驗收:commit/push 成功時會出現 `persisted findings commit=... push=... review_outcome=...`,且只提交問題檔的行為已有單元測試覆蓋。 ## 階段九:阻擋嚴重問題 PR(第 8 點) - 目標:如果 PR 問題表格中有嚴重(critical)問題,workflow 需直接 exit 1,不讓流程成功。 diff --git a/app/git.js b/app/git.js index 39468fe..9be810c 100644 --- a/app/git.js +++ b/app/git.js @@ -1,51 +1,12 @@ import { spawnSync } from 'child_process'; import fs from 'fs'; import path from 'path'; -import { fileURLToPath } from 'url'; -import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH, getLLMConfig } from './config.js'; -import { line, ok, warn, error } from './log.js'; +import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; +import { line, ok, warn } from './log.js'; -const ACTION_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); -const GENERATED_SYNC_PATHS = [FINDINGS_PATH, '.gitea/ai-review/exclusions.json']; +const REVIEW_FILE_PATHS = [FINDINGS_PATH, '.gitea/ai-review/exclusions.json']; const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git`; export const BOT_COMMIT_MARKER = '[ai-review-bot]'; -export const SYNC_PATHS = [ - '.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/agents/openai.yaml', - '.claude/skills/triage-findings/SKILL.md', - '.gemini/skills/triage-findings/SKILL.md', - '.github/copilot-instructions.md', - '.github/skills/triage-findings/SKILL.md', - 'AGENTS.md', - 'ANTIGRAVITY.md', - 'CLAUDE.md', - 'GEMINI.md', -]; -const FORCE_SYNC_FILE_PATHS = [ - '.github/copilot-instructions.md', - 'AGENTS.md', - 'ANTIGRAVITY.md', - 'CLAUDE.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 = [ - '.agents/skills/triage-findings', - '.antigravity/skills/triage-findings', - '.codex/skills/triage-findings', - '.claude/skills/triage-findings', - '.gemini/skills/triage-findings', - '.github/skills/triage-findings', -]; function makeRunner(spawn) { return function run(args, cwd, env) { @@ -88,173 +49,6 @@ function readGitOutput(run, args, cwd, env) { } } -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 (aiMerged == null) { - warn(`[merge] ${relPath} AI result unavailable; using deterministic merge`); - return deterministic; - } - 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); - if (!fs.existsSync(srcDir)) return []; - - const copied = []; - for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) { - const relPath = path.join(relDir, entry.name); - if (entry.isDirectory()) { - copied.push(...syncTree(sourceRoot, repoDir, relPath)); - continue; - } - const synced = syncFileOverwrite(sourceRoot, repoDir, relPath); - if (synced) copied.push(synced); - } - return copied; -} - export function getRepoState(repoDir, _spawnSync = spawnSync) { const run = makeRunner(_spawnSync); const headSha = readGitOutput(run, ['rev-parse', 'HEAD'], repoDir); @@ -311,7 +105,7 @@ export function cloneRepo(workspace, _spawnSync = spawnSync) { }); } -export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync, sourceRoot = ACTION_ROOT, reviewOutcome = 'success') { +export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync, _sourceRoot = null, reviewOutcome = 'success') { const run = makeRunner(_spawnSync); try { @@ -323,51 +117,20 @@ export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync, run(['reset', '--hard', `origin/${PR_HEAD_BRANCH}`], repoDir); } - const existingSyncPaths = new Set(); - const aiMergeAssistant = await getInstructionMergeAssistant(); - - // Copy action skill trees into the target repo. Existing files are merged with - // 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 relPath of syncTree(sourceRoot, repoDir, relDir)) { - existingSyncPaths.add(relPath); - } - } - - // Merge only the direct instruction files that must preserve repository-specific - // skills, commands, and rules. Everything else keeps the source copy. - for (const relPath of FORCE_SYNC_FILE_PATHS) { - const copied = MERGE_SYNC_FILE_PATHS.has(relPath) - ? await syncInstructionFile(sourceRoot, repoDir, relPath, aiMergeAssistant) - : syncFileOverwrite(sourceRoot, repoDir, relPath); - if (copied) existingSyncPaths.add(copied); - } - - // Merge standalone action files into the target repo. - for (const relPath of SYNC_PATHS) { - if (FORCE_SYNC_FILE_PATHS.includes(relPath)) continue; - const copied = syncFileOverwrite(sourceRoot, repoDir, relPath); - if (copied) existingSyncPaths.add(copied); - } - - if (existingSyncPaths.size > 0) { - run(['add', ...existingSyncPaths], repoDir); - } - const generatedSyncPaths = GENERATED_SYNC_PATHS.filter(relPath => fs.existsSync(path.join(workspace, relPath))); - if (generatedSyncPaths.length > 0) { - for (const relPath of generatedSyncPaths) { + const reviewFilePaths = REVIEW_FILE_PATHS.filter(relPath => fs.existsSync(path.join(workspace, relPath))); + if (reviewFilePaths.length > 0) { + for (const relPath of reviewFilePaths) { const src = path.join(workspace, relPath); const dest = path.join(repoDir, relPath); fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.copyFileSync(src, dest); } - run(['add', ...generatedSyncPaths], repoDir); + run(['add', ...reviewFilePaths], repoDir); } const status = run(['status', '--porcelain'], repoDir); if (!status) { - line('sync files 無變更,跳過 commit'); + line('review files 無變更,跳過 commit'); return; } diff --git a/app/git.test.js b/app/git.test.js index e766a0d..173bca6 100644 --- a/app/git.test.js +++ b/app/git.test.js @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import fs from 'fs'; import os from 'os'; import path from 'path'; -import { commitAndPush, cloneRepo, verifyRemoteAccess, SYNC_PATHS, BOT_COMMIT_MARKER, getHeadCommitMessage, isBotAutoCommit, mergeInstructionText } from './git.js'; +import { commitAndPush, cloneRepo, verifyRemoteAccess, BOT_COMMIT_MARKER, getHeadCommitMessage, isBotAutoCommit } from './git.js'; // --- helpers --- function makeTmpWorkspace() { @@ -13,13 +13,7 @@ function makeTmpWorkspace() { } function makeActionSource() { - const sourceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'git-source-')); - for (const relPath of SYNC_PATHS) { - const fullPath = path.join(sourceRoot, relPath); - fs.mkdirSync(path.dirname(fullPath), { recursive: true }); - fs.writeFileSync(fullPath, relPath); - } - return sourceRoot; + return fs.mkdtempSync(path.join(os.tmpdir(), 'git-source-')); } // Default stub: all commands succeed, status returns changes @@ -125,7 +119,7 @@ describe('commitAndPush', () => { assert.equal(commitCalled, false, 'commit should not run when there are no changes'); }); - it('adds skill and entry files together with findings', async () => { + it('adds only generated review files', async () => { const repoDir = path.join(workspace, 'repo'); fs.mkdirSync(path.join(workspace, '.gitea/ai-review'), { recursive: true }); fs.writeFileSync(path.join(workspace, '.gitea/ai-review/findings.json'), '[]\n'); @@ -136,123 +130,36 @@ describe('commitAndPush', () => { const spawn = makeSpawn(); await commitAndPush(workspace, repoDir, spawn, sourceRoot); const addCalls = spawn.calls.filter(c => c.args[0] === 'add'); - const skillAddCall = addCalls.find(c => c.args.includes('.github/skills/triage-findings/SKILL.md')); const generatedAddCall = addCalls.find(c => c.args.includes('.gitea/ai-review/exclusions.json')); - assert.ok(skillAddCall, 'expected git add for synced skill 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/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('.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('.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('GEMINI.md')); - assert.ok(!skillAddCall.args.includes('README.md')); assert.ok(generatedAddCall.args.includes('.gitea/ai-review/findings.json')); assert.ok(generatedAddCall.args.includes('.gitea/ai-review/exclusions.json')); + assert.equal(addCalls.length, 1, 'expected only generated review files to be staged'); }); - it('keeps repo copies when the source sync file is missing', async () => { - const missingPath = path.join(sourceRoot, '.amazonq/rules/triage-findings.md'); - fs.rmSync(missingPath, { force: true }); - const repoPath = path.join(workspace, 'repo', '.amazonq/rules/triage-findings.md'); - fs.writeFileSync(repoPath, 'stale'); + it('does not overwrite or add action source files', async () => { + const repoDir = path.join(workspace, 'repo'); + const sourceDocPath = path.join(sourceRoot, 'docs/source-only.md'); + const repoDocPath = path.join(repoDir, 'docs/source-only.md'); + const repoConfigPath = path.join(repoDir, 'project-notes.md'); + fs.mkdirSync(path.join(workspace, '.gitea/ai-review'), { recursive: true }); + fs.writeFileSync(path.join(workspace, '.gitea/ai-review/findings.json'), '[]\n'); + fs.writeFileSync(path.join(workspace, '.gitea/ai-review/exclusions.json'), '[]\n'); + + fs.mkdirSync(path.dirname(sourceDocPath), { recursive: true }); + fs.mkdirSync(path.dirname(repoDocPath), { recursive: true }); + fs.writeFileSync(sourceDocPath, 'fresh action source doc'); + fs.writeFileSync(repoDocPath, 'existing repo doc'); + fs.writeFileSync(repoConfigPath, 'existing repo notes'); + const spawn = makeSpawn(); + await commitAndPush(workspace, repoDir, spawn, sourceRoot); + const addedArgs = spawn.calls.filter(c => c.args[0] === 'add').flatMap(c => c.args); - await commitAndPush(workspace, path.join(workspace, 'repo'), spawn, sourceRoot); - - const rmCall = spawn.calls.find(c => c.args[0] === 'rm'); - assert.equal(rmCall, undefined, 'git rm should not run for missing source files'); - assert.equal(fs.readFileSync(repoPath, 'utf8'), 'stale'); - }); - - it('merges existing repo copies with workspace files', async () => { - const repoDir = path.join(workspace, 'repo'); - fs.writeFileSync(path.join(repoDir, 'AGENTS.md'), 'repo agents doc'); - fs.writeFileSync(path.join(repoDir, 'ANTIGRAVITY.md'), 'repo antigravity doc'); - fs.writeFileSync(path.join(repoDir, 'CLAUDE.md'), 'repo claude doc'); - fs.writeFileSync(path.join(repoDir, 'GEMINI.md'), 'repo gemini doc'); - - await commitAndPush(workspace, repoDir, makeSpawn(), sourceRoot); - - const agentsDoc = fs.readFileSync(path.join(repoDir, 'AGENTS.md'), 'utf8'); - const antigravityDoc = fs.readFileSync(path.join(repoDir, 'ANTIGRAVITY.md'), 'utf8'); - const claudeDoc = fs.readFileSync(path.join(repoDir, 'CLAUDE.md'), 'utf8'); - 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('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('uses deterministic instruction merge when AI returns no usable result', async () => { - const aiMergeAssistant = async () => null; - - const result = await mergeInstructionText('repo block', 'source block', 'AGENTS.md', aiMergeAssistant); - - assert.ok(result.includes('repo block')); - assert.ok(result.includes('source 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 sourceSkillPath = path.join(sourceRoot, '.github/skills/triage-findings/SKILL.md'); - const repoSkillPath = path.join(repoDir, '.github/skills/triage-findings/SKILL.md'); - 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.writeFileSync(sourceNestedPath, 'fresh nested'); - fs.mkdirSync(path.dirname(repoNestedPath), { recursive: true }); - fs.writeFileSync(repoNestedPath, 'stale nested'); - fs.writeFileSync(path.join(repoDir, '.github/copilot-instructions.md'), 'stale copilot'); - - await commitAndPush(workspace, repoDir, makeSpawn(), sourceRoot); - - 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'); + assert.equal(fs.readFileSync(repoDocPath, 'utf8'), 'existing repo doc'); + assert.equal(fs.readFileSync(repoConfigPath, 'utf8'), 'existing repo notes'); + assert.ok(!addedArgs.includes('docs/source-only.md')); + assert.ok(!addedArgs.includes('project-notes.md')); }); it('does not throw when git command fails', async () => { diff --git a/app/gitea.js b/app/gitea.js index 2442ff7..7aeffed 100644 --- a/app/gitea.js +++ b/app/gitea.js @@ -25,18 +25,8 @@ export function getBotReviewOutcome(message) { export async function getPRDiff() { const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}.diff`), { headers: headers(), timeout: 60000, httpsAgent }); return filterDiff(resp.data, [ - '.amazonq/', - '.agents/', - '.antigravity/', - '.claude/', - '.codex/', - '.gemini/', '.gitea/', '.github/', - 'AGENTS.md', - 'ANTIGRAVITY.md', - 'CLAUDE.md', - 'GEMINI.md', 'README.md', 'TODO.md', ]); diff --git a/app/gitea.test.js b/app/gitea.test.js index 8dc7749..89c5a3a 100644 --- a/app/gitea.test.js +++ b/app/gitea.test.js @@ -129,10 +129,10 @@ describe('filterDiff', () => { const block = (file) => `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-old\n+new\n`; it('filters out configured folder blocks', () => { - const diff = block('.gitea/workflows/review.yaml') + block('.amazonq/rules/triage-findings.md') + block('src/index.js'); - const result = filterDiff(diff, ['.gitea/', '.amazonq/']); + const diff = block('.gitea/workflows/review.yaml') + block('.github/workflows/review.yaml') + block('src/index.js'); + const result = filterDiff(diff, ['.gitea/', '.github/']); assert.ok(!result.includes('.gitea/')); - assert.ok(!result.includes('.amazonq/')); + assert.ok(!result.includes('.github/')); assert.ok(result.includes('src/index.js')); }); @@ -144,8 +144,8 @@ describe('filterDiff', () => { }); it('returns empty string when all blocks are excluded', () => { - 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/', '.agents/']); + const diff = block('.gitea/workflows/review.yaml') + block('.gitea/ai-review/findings.json'); + const result = filterDiff(diff, ['.gitea/']); assert.equal(result, ''); });