Compare commits

...

15 Commits

Author SHA1 Message Date
jiantw83 93c602b86a Merge pull request 'feat: 新增skill處理問題' (#100) from feat/新增skill處理問題 into develop
Reviewed-on: #100
2026-05-14 02:39:57 +00:00
jiantw83 b397b76a7a chore: triage review findings 2026-05-14 02:37:45 +00:00
AI Review Bot c5c3f1d7e1 chore: update ai-review findings [skip ci] 2026-05-14 02:24:48 +00:00
jiantw83 12980d6ca4 fix: dedupe sync paths in git tests 2026-05-14 02:22:50 +00:00
AI Review Bot aa8b3ae89a chore: update ai-review findings [skip ci] 2026-05-14 02:20:01 +00:00
jiantw83 1ad87ac4a4 fix: address triaged review findings 2026-05-14 02:18:17 +00:00
AI Review Bot fb5c28114d chore: update ai-review findings [skip ci] 2026-05-14 02:14:49 +00:00
jiantw83 c871a27c9a docs: note skill sync overwrite behavior 2026-05-14 02:13:08 +00:00
jiantw83 4492fcbdd6 feat: sync triage skill files 2026-05-14 02:13:08 +00:00
jiantw83 3338a518fe docs: streamline triage skill triggers 2026-05-14 02:13:08 +00:00
jiantw83 098d4aea97 feat: expand diff exclusions 2026-05-14 02:13:08 +00:00
jiantw83 850b2d770e chore: add multi-tool triage skill 2026-05-14 02:13:08 +00:00
jiantw83 5478918e25 feat: add triage findings skill for managing review issues 2026-05-14 02:13:08 +00:00
jiantw83 fd49610838 Merge pull request 'feat: tighten json validation repair flow' (#99) from feat/驗證JSON檔案 into develop
Reviewed-on: #99
2026-05-14 01:26:07 +00:00
AI Review Bot 92d32766b9 chore: update ai-review findings [skip ci] 2026-05-14 01:25:08 +00:00
17 changed files with 360 additions and 33 deletions
+14
View File
@@ -0,0 +1,14 @@
# 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`.
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.
+28
View File
@@ -0,0 +1,28 @@
---
name: triage-findings
description: Triage findings, fix real issues, and exclude false positives.
---
# Triage Findings
## Use
直接輸入:`triage-findings 問題原始檔(文字或截圖)`
## Workflow
1. Merge all findings.
2. Sort by severity:
- critical
- warning
- info
3. Renumber from 1.
4. Fix real issues.
5. Put false positives into `.gitea/ai-review/exclusions.json`.
6. Add tests when behavior changes.
## Output Rules
- Keep the final list short.
- Keep numbering contiguous.
- Preserve file path, location, and fix.
+44
View File
@@ -0,0 +1,44 @@
---
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 using the existing schema in the repo or task context.
## 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.
- If the source already provides a severity or title, keep it unless it conflicts with the final ordering.
@@ -0,0 +1,4 @@
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 exclusions."
+28
View File
@@ -0,0 +1,28 @@
---
name: triage-findings
description: Triage findings, fix real issues, and exclude false positives.
---
# Triage Findings
## Use
直接輸入:`triage-findings 問題原始檔(文字或截圖)`
## Workflow
1. Merge all findings.
2. Sort by severity:
- critical
- warning
- info
3. Renumber from 1.
4. Fix real issues.
5. Put false positives into `.gitea/ai-review/exclusions.json`.
6. Add tests when behavior changes.
## Output Rules
- Keep the final list short.
- Keep numbering contiguous.
- Preserve file path, location, and fix.
+28
View File
@@ -279,5 +279,33 @@
{
"location": "app/json.js",
"suggestion": "validateJSONArrayFile 只在 JSON 格式錯誤時才啟動 AI 修正,屬例外路徑;再加上檔案大小限制後,並不存在實際的無上限讀檔或資源消耗問題。"
},
{
"location": "app/json.test.js",
"suggestion": "邊界值測試已存在,`MAX_JSON_BYTES` 等於上限時可正常讀取,這不是未解決問題。"
},
{
"location": "app/gitea.test.js:64",
"suggestion": "`describe` 已改為同步 callback`async` 不再出現在這個區塊。"
},
{
"location": "app/git.test.js:13",
"suggestion": "`makeTmpWorkspace` 已直接使用 `app/git.js` 匯出的 `SYNC_PATHS`,不再維護重複清單。"
},
{
"location": "app/gitea.js:32",
"suggestion": "`filterDiff` 內層縮排已符合專案的 2-space 風格,這是誤報。"
},
{
"location": "app/json.test.js:76",
"suggestion": "1MB 上限下的 JSON 讀取不需要改成串流解析;現有實作已先做大小檢查,這個建議屬過度設計。"
},
{
"location": "app/json.test.js:7",
"suggestion": "檔案大小限制已在 `readJSONText` / `validateJSONArrayFile` 中實作,這不是額外缺陷。"
},
{
"location": "app/json.test.js:10",
"suggestion": "`MAX_JSON_BYTES` 是 `json.js` 的內部限制常數,不需要匯出成公開 API。"
}
]
+16
View File
@@ -0,0 +1,16 @@
# 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`.
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`.
+28
View File
@@ -0,0 +1,28 @@
---
name: triage-findings
description: Triage findings, fix real issues, and exclude false positives.
---
# Triage Findings
## Use
`triage-findings 問題原始檔(文字或截圖)`
## Workflow
1. Merge all findings.
2. Sort by severity:
- critical
- warning
- info
3. Renumber from 1.
4. Fix real issues.
5. Put false positives into `.gitea/ai-review/exclusions.json`.
6. Add tests when behavior changes.
## Output Rules
- Keep the final list short.
- Keep numbering contiguous.
- Preserve file path, location, and fix.
+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`.
7. Add or update tests when behavior changes.
8. Re-check after each fix.
The full reusable skill lives in `.claude/skills/triage-findings/SKILL.md`.
+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`.
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`.
+30 -1
View File
@@ -22,7 +22,7 @@
4. 盡量將應用程式放在 ./app,修改 entrypoint.sh 與 Dockerfile 讓程式可以正常運行
5. 將提示詞放到 ./app/prompts 內供程式讀取
6. API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,全部失敗則 exit 1
7. 讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,避免 AI 分析 workflow 設定等非業務程式碼
7. 讀取 Git Diff 時排除 `.gitea/``.amazonq/``.claude/``.codex/``.gemini/``.github/` 資料夾,以及 `CLAUDE.md``GEMINI.md``TODO.md``README.md`,避免 AI 分析 workflow 設定、skill 入口與文件等非業務程式碼
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 用量
@@ -199,3 +199,32 @@ jobs:
pull-requests: write
issues: write
```
## SkillTriage 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 問題原始檔(文字或截圖)`
### 適用情境
`triage-findings 問題原始檔(文字或截圖)` 用在 review 問題整併、排序、修正、排除誤判。
### 版本包含
提交時一併包含 `triage-findings` skill 與各平台入口檔;已存在檔案一律覆蓋,同步到最新內容。
+4 -4
View File
@@ -6,8 +6,8 @@
- 已驗收:`code-review` job 的 log 已完整出現 `Step1``Step8`,並以 `Pipeline 完成` 結束。
## 階段二:Git Diff 排除 .gitea/ 資料夾
- 目標:讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,避免 AI 分析 workflow 設定等非業務程式碼。
- 驗收:PR 中有 `.gitea/` 路徑的變更時,diff 內容不包含該路徑的區塊,AI 分析結果不含 `.gitea/` 相關問題。
- 目標:讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,以及 `.amazonq/``.claude/``.codex/``.gemini/``.github/``CLAUDE.md``GEMINI.md``TODO.md``README.md`避免 AI 分析 workflow 設定、skill 入口與文件等非業務程式碼。
- 驗收:PR 中有上述路徑或檔案的變更時,diff 內容不包含該區塊,AI 分析結果不含這些路徑相關問題。
- 已驗收:`app/gitea.js` 已在取得 diff 時過濾 `.gitea/` 區塊,且相關單元測試已覆蓋。
## 階段三:Findings 產生與合併
@@ -38,8 +38,8 @@
- 已驗收:log 已明確顯示 `.gitea/ai-review/findings.json``.gitea/ai-review/exclusions.json` 都是 `JSON 格式正確`
## 階段八:記憶區 commit/push 與錯誤處理
- 目標:記憶區能成功 commit/push,錯誤時有明確 log,流程結束有總結訊息。
- 驗收:log 有「persisted findings」、「commit=...」、「push=...」等訊息,錯誤時有「Runner failed: ...」等明確錯誤說明。
- 目標:記憶區能成功 commit/push且一併包含 `triage-findings` skill 與各平台入口檔;skill 檔案已存在時一律以來源覆蓋,達到同步效果;錯誤時有明確 log,流程結束有總結訊息。
- 驗收:log 有「persisted findings」、「commit=...」、「push=...」等訊息,且能看出 skill 相關檔案已一併提交並被覆蓋同步;錯誤時有「Runner failed: ...」等明確錯誤說明。
- 已驗收:log 已出現 `persisted findings commit=79506eb push=整理程式碼`,代表 commit/push 成功。
## 階段九:阻擋嚴重問題 PR(第 8 點)
+20 -6
View File
@@ -4,6 +4,16 @@ import path from 'path';
import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js';
const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git`;
export const SYNC_PATHS = [
FINDINGS_PATH,
'.amazonq/rules/triage-findings.md',
'.claude/skills/triage-findings/SKILL.md',
'.gemini/skills/triage-findings/SKILL.md',
'.github/copilot-instructions.md',
'.github/skills/triage-findings/SKILL.md',
'CLAUDE.md',
'GEMINI.md',
];
function makeRunner(spawn) {
return function run(args, cwd, env) {
@@ -55,16 +65,20 @@ export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync)
run(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir);
run(['config', 'user.name', 'AI Review Bot'], repoDir);
const srcFindings = path.join(workspace, FINDINGS_PATH);
const destFindings = path.join(repoDir, FINDINGS_PATH);
fs.mkdirSync(path.dirname(destFindings), { recursive: true });
fs.copyFileSync(srcFindings, destFindings);
// Always copy source files over the repo copy so skill files stay in sync.
for (const relPath of SYNC_PATHS) {
const src = path.join(workspace, relPath);
const dest = path.join(repoDir, relPath);
if (!fs.existsSync(src)) continue;
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.copyFileSync(src, dest);
}
run(['add', FINDINGS_PATH], repoDir);
run(['add', ...SYNC_PATHS], repoDir);
const status = run(['status', '--porcelain'], repoDir);
if (!status) {
console.log(' findings.json 無變更,跳過 commit');
console.log(' sync files 無變更,跳過 commit');
return;
}
+32 -5
View File
@@ -3,17 +3,18 @@ import assert from 'node:assert/strict';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { commitAndPush, cloneRepo } from './git.js';
import { commitAndPush, cloneRepo, SYNC_PATHS } from './git.js';
// --- helpers ---
function makeTmpWorkspace() {
const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'git-test-'));
// Pre-create repo dir so clone branch is skipped
fs.mkdirSync(path.join(ws, 'repo'), { recursive: true });
// Create a findings.json to copy
const findingsDir = path.join(ws, '.gitea/ai-review');
fs.mkdirSync(findingsDir, { recursive: true });
fs.writeFileSync(path.join(findingsDir, 'findings.json'), '[]');
for (const relPath of SYNC_PATHS) {
const fullPath = path.join(ws, relPath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, relPath);
}
return ws;
}
@@ -85,6 +86,32 @@ 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 () => {
const spawn = makeSpawn();
await commitAndPush(workspace, path.join(workspace, 'repo'), spawn);
const addCall = spawn.calls.find(c => c.args[0] === 'add');
assert.ok(addCall, 'expected git add to run');
assert.ok(addCall.args.includes('.github/skills/triage-findings/SKILL.md'));
assert.ok(addCall.args.includes('.claude/skills/triage-findings/SKILL.md'));
assert.ok(addCall.args.includes('.gemini/skills/triage-findings/SKILL.md'));
assert.ok(addCall.args.includes('.github/copilot-instructions.md'));
assert.ok(addCall.args.includes('.amazonq/rules/triage-findings.md'));
assert.ok(addCall.args.includes('CLAUDE.md'));
assert.ok(addCall.args.includes('GEMINI.md'));
assert.ok(!addCall.args.includes('README.md'));
});
it('overwrites existing repo copies with workspace files', async () => {
const repoDir = path.join(workspace, 'repo');
fs.writeFileSync(path.join(repoDir, '.github/skills/triage-findings/SKILL.md'), 'stale');
fs.writeFileSync(path.join(repoDir, 'CLAUDE.md'), 'stale');
await commitAndPush(workspace, repoDir, makeSpawn());
assert.equal(fs.readFileSync(path.join(repoDir, '.github/skills/triage-findings/SKILL.md'), 'utf8'), '.github/skills/triage-findings/SKILL.md');
assert.equal(fs.readFileSync(path.join(repoDir, 'CLAUDE.md'), 'utf8'), 'CLAUDE.md');
});
it('does not throw when git command fails', async () => {
const failSpawn = () => ({ status: 1, stdout: '', stderr: 'fatal: error', error: null });
await assert.doesNotReject(() => commitAndPush(workspace, path.join(workspace, 'repo'), failSpawn));
+17 -2
View File
@@ -11,7 +11,18 @@ const api = (path) => `${GITEA_SERVER_URL.replace(/\/$/, '')}/api/v1${path}`;
*/
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, ['.gitea/']);
return filterDiff(resp.data, [
'.amazonq/',
'.claude/',
'.codex/',
'.gemini/',
'.gitea/',
'.github/',
'CLAUDE.md',
'GEMINI.md',
'README.md',
'TODO.md',
]);
}
/**
@@ -20,7 +31,11 @@ export async function getPRDiff() {
*/
export function filterDiff(diff, excludePrefixes) {
return diff.split(/(?=^diff --git )/m)
.filter(block => !excludePrefixes.some(p => block.startsWith(`diff --git a/${p}`)))
.filter(block => !excludePrefixes.some(p => {
const prefix = `diff --git a/${p}`;
const singleFile = `diff --git a/${p} b/${p}`;
return block.startsWith(prefix) || block.startsWith(singleFile);
}))
.join('');
}
+14 -15
View File
@@ -1,12 +1,11 @@
import { describe, it, afterEach, mock } from 'node:test';
import assert from 'node:assert/strict';
import axios from 'axios';
import { getPRDiff, filterDiff, postComment } from './gitea.js';
afterEach(() => mock.restoreAll());
describe('gitea', async () => {
const { getPRDiff, filterDiff, postComment } = await import('./gitea.js');
describe('gitea', () => {
it('getPRDiff calls Gitea diff API with Authorization header', async () => {
let capturedUrl, capturedOpts;
mock.method(axios, 'get', async (url, opts) => {
@@ -59,27 +58,27 @@ describe('gitea', async () => {
});
});
describe('filterDiff', async () => {
const { filterDiff } = await import('./gitea.js');
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 .gitea/ blocks', () => {
const diff = block('.gitea/workflows/review.yaml') + block('src/index.js');
const result = filterDiff(diff, ['.gitea/']);
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/']);
assert.ok(!result.includes('.gitea/'));
assert.ok(!result.includes('.amazonq/'));
assert.ok(result.includes('src/index.js'));
});
it('does not filter non-.gitea/ blocks', () => {
const diff = block('src/index.js') + block('README.md');
const result = filterDiff(diff, ['.gitea/']);
assert.equal(result, diff);
it('filters out configured top-level file blocks', () => {
const diff = block('README.md') + block('src/index.js');
const result = filterDiff(diff, ['README.md', 'TODO.md']);
assert.ok(!result.includes('README.md'));
assert.ok(result.includes('src/index.js'));
});
it('returns empty string when all blocks are excluded', () => {
const diff = block('.gitea/workflows/review.yaml') + block('.gitea/ai-review/findings.json');
const result = filterDiff(diff, ['.gitea/']);
const diff = block('.gitea/workflows/review.yaml') + block('.gitea/ai-review/findings.json') + block('CLAUDE.md');
const result = filterDiff(diff, ['.gitea/', 'CLAUDE.md']);
assert.equal(result, '');
});
+25
View File
@@ -6,6 +6,7 @@ import path from 'path';
import { stripCodeFence, repairJSONArrayWithAI, validateJSONArrayFile, ensureJSONArrayFileExists } from './json.js';
describe('json helpers', () => {
const MAX_JSON_BYTES = 1024 * 1024;
let workspace;
beforeEach(() => {
@@ -76,6 +77,16 @@ describe('json helpers', () => {
assert.equal(fs.readFileSync(fullPath, 'utf8'), '[]\n');
});
it('reads a valid JSON file whose size equals the maximum limit', async () => {
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, `[]${' '.repeat(MAX_JSON_BYTES - 2)}`, 'utf8');
const result = await validateJSONArrayFile(fullPath, '.gitea/ai-review/findings.json');
assert.deepEqual(result, { exists: true, valid: true, repaired: false });
});
it('repairs invalid JSON using AI output and rewrites the file', async () => {
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
@@ -90,6 +101,20 @@ describe('json helpers', () => {
assert.equal(fs.readFileSync(fullPath, 'utf8'), '[{"fixed":true}]\n');
});
it('preserves a trailing newline returned by AI repair', async () => {
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, '{broken', 'utf8');
const result = await validateJSONArrayFile(fullPath, '.gitea/ai-review/findings.json', async (_fullPath, _label, original) => {
assert.equal(original, '{broken');
return '[{"fixed":true}]\n';
});
assert.deepEqual(result, { exists: true, valid: true, repaired: true });
assert.equal(fs.readFileSync(fullPath, 'utf8'), '[{"fixed":true}]\n');
});
it('throws when AI repair fails', async () => {
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
fs.mkdirSync(path.dirname(fullPath), { recursive: true });