@fitlab-ai/agent-infra 0.7.4 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/bin/cli.ts +13 -11
  2. package/dist/bin/cli.js +13 -11
  3. package/dist/lib/init.js +1 -1
  4. package/dist/lib/merge.js +1 -1
  5. package/dist/lib/sandbox/index.js +21 -21
  6. package/dist/lib/task/index.js +13 -13
  7. package/dist/lib/update.js +1 -1
  8. package/lib/init.ts +1 -1
  9. package/lib/merge.ts +1 -1
  10. package/lib/sandbox/index.ts +21 -21
  11. package/lib/task/index.ts +13 -13
  12. package/lib/update.ts +1 -1
  13. package/package.json +1 -1
  14. package/templates/.agents/rules/README.en.md +7 -3
  15. package/templates/.agents/rules/README.zh-CN.md +7 -3
  16. package/templates/.agents/rules/cli-help-format.en.md +49 -0
  17. package/templates/.agents/rules/cli-help-format.zh-CN.md +49 -0
  18. package/templates/.agents/rules/no-mid-flow-questions.en.md +14 -2
  19. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +14 -2
  20. package/templates/.agents/rules/pr-sync.github.en.md +8 -6
  21. package/templates/.agents/rules/pr-sync.github.zh-CN.md +8 -6
  22. package/templates/.agents/rules/review-handshake.en.md +83 -0
  23. package/templates/.agents/rules/review-handshake.zh-CN.md +83 -0
  24. package/templates/.agents/scripts/lib/post-review-commit.js +56 -0
  25. package/templates/.agents/scripts/lib/review-artifacts.js +117 -0
  26. package/templates/.agents/scripts/review-diff-fingerprint.js +99 -0
  27. package/templates/.agents/scripts/validate-artifact.js +240 -0
  28. package/templates/.agents/skills/analyze-task/SKILL.en.md +52 -6
  29. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +52 -6
  30. package/templates/.agents/skills/code-task/config/verify.en.json +3 -0
  31. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +3 -0
  32. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -3
  33. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -3
  34. package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -4
  35. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -4
  36. package/templates/.agents/skills/code-task/scripts/detect-mode.js +2 -107
  37. package/templates/.agents/skills/commit/SKILL.en.md +6 -0
  38. package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -0
  39. package/templates/.agents/skills/commit/reference/task-status-update.en.md +8 -0
  40. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +8 -0
  41. package/templates/.agents/skills/complete-task/SKILL.en.md +10 -0
  42. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -0
  43. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -0
  44. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -0
  45. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
  46. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
  47. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
  48. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
  49. package/templates/.agents/skills/plan-task/config/verify.en.json +3 -0
  50. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +3 -0
  51. package/templates/.agents/skills/review-analysis/config/verify.en.json +2 -1
  52. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +2 -1
  53. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -4
  54. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -4
  55. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -0
  56. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -0
  57. package/templates/.agents/skills/review-code/SKILL.en.md +4 -1
  58. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -1
  59. package/templates/.agents/skills/review-code/config/verify.en.json +5 -2
  60. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +5 -2
  61. package/templates/.agents/skills/review-code/reference/output-templates.en.md +5 -4
  62. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +5 -4
  63. package/templates/.agents/skills/review-code/reference/report-template.en.md +6 -0
  64. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +6 -0
  65. package/templates/.agents/skills/review-plan/config/verify.en.json +2 -1
  66. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +2 -1
  67. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -4
  68. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -4
  69. package/templates/.agents/skills/review-plan/reference/report-template.en.md +4 -0
  70. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +4 -0
  71. package/templates/.agents/templates/task.en.md +7 -0
  72. package/templates/.agents/templates/task.zh-CN.md +7 -0
  73. package/templates/.github/workflows/metadata-sync.yml +1 -1
  74. package/templates/.github/workflows/pr-label.yml +1 -1
  75. package/templates/.github/workflows/status-label.yml +1 -1
@@ -32,7 +32,11 @@ Aggregation rules:
32
32
  - build the review-history table from `review-code*` and `code*`
33
33
  - extract the test summary from `code*`
34
34
  - if one artifact class is missing, treat it as "no data for this stage" and continue
35
- - Manual verification section: extract items requiring human confirmation/fallback from the "Assumptions"/"Open Questions" of the latest `plan*` and the "Environment-Blocked Findings"/"Self-Doubt" sections (i.e. env-blocked items) of the latest `review-code*`; when there are none, write the explicit placeholder `- None no items require manual verification`, never leave it empty
35
+ - Manual verification section: include only post-code-stage checks that still require a human to execute or judge and that the AI cannot close on its own.
36
+ - **Admission boundary**: the verification result depends on a real environment, permissions, account, external system, or human judgment, and cannot be closed by an agent rerunning tests, adding checks, or continuing the fix loop.
37
+ - **Sources**: `review-code*` "Environment-Blocked Findings", plus `code*` items that satisfy the boundary above.
38
+ - **Wording**: each retained item must state at least "what to verify + location (file/change/scope) + why only a human can verify it".
39
+ - **Empty rendering**: when there are no retained items, do NOT use the ⚠️ alarm style (it falsely implies a problem). Render the whole block as: heading `### ✅ No Manual Verification Needed` and a single line `No items in this change require manual confirmation.`, with no item list. Only use the `### ⚠️ Manual Verification Required` heading + item list when retained items exist.
36
40
 
37
41
  ## Comment Body Template
38
42
 
@@ -47,11 +51,7 @@ Use this canonical comment body template:
47
51
 
48
52
  **Updated At**: {current-time}
49
53
 
50
- ### ⚠️ Manual Verification Required
51
-
52
- > Items in this change that need human confirmation/fallback; reviewers can reply under this comment once verified.
53
-
54
- - {manual-verify-item}
54
+ {manual-verify-section}
55
55
 
56
56
  ### Key Technical Decisions
57
57
 
@@ -72,6 +72,8 @@ Use this canonical comment body template:
72
72
  *Generated by {agent} · Internal tracking: {task-id}*
73
73
  ```
74
74
 
75
+ > Render `{manual-verify-section}` per the "manual verification section" aggregation rule above: with retained items → `### ⚠️ Manual Verification Required` heading + quote + item list; with none → `### ✅ No Manual Verification Needed` heading + a single line `No items in this change require manual confirmation.` (no ⚠️, no list).
76
+
75
77
  ## Comment Lookup And Update
76
78
 
77
79
  Fetch existing comments through the Issues comments API, not the dedicated PR comments API.
@@ -32,7 +32,11 @@
32
32
  - 用 `review-code*` 与 `code*` 构建审查历程表
33
33
  - 从 `code*` 提取测试结果摘要
34
34
  - 某一类产物缺失时,按“无该阶段数据”处理并继续生成
35
- - 需人工校验段落:从最新 `plan*` 的「假设」「未决问题」与最新 `review-code*` 的「环境性遗留」「自我质疑」提取需人工确认/兜底事项;无任何事项时写显式占位 `- 无需人工校验事项`,不得留空
35
+ - 需人工校验段落:只收进入 code 阶段后仍需人实际执行或判断、AI 无法自行关闭的校验点。
36
+ - **准入边界**:校验结论依赖真实环境、权限、账号、外部系统或人工判断,且无法通过 agent 重跑测试、补充检查或继续修复自行关闭。
37
+ - **来源**:`review-code*` 的「环境性遗留」,以及 `code*` 中满足上述边界的校验点。
38
+ - **写法**:每条保留项至少写明「校验什么 + 定位(文件/改动/范围)+ 为什么只能由人校验」。
39
+ - **空集渲染**:无保留项时,不要使用 ⚠️ 告警样式(会让人误以为有问题)。整段降级渲染为:标题 `### ✅ 无需人工校验`,正文一行 `本次改动无需人工确认事项。`,不带条目列表。有保留项时才用 `### ⚠️ 需人工校验` 标题 + 条目列表。
36
40
 
37
41
  ## 评论体模板
38
42
 
@@ -47,11 +51,7 @@
47
51
 
48
52
  **更新时间**:{当前时间}
49
53
 
50
- ### ⚠️ 需人工校验
51
-
52
- > 本次改动中需人工确认/兜底的事项;reviewer 校验后可在本评论下回复收尾。
53
-
54
- - {manual-verify-item}
54
+ {manual-verify-section}
55
55
 
56
56
  ### 关键技术决策
57
57
 
@@ -72,6 +72,8 @@
72
72
  *由 {agent} 自动生成 · 内部追踪:{task-id}*
73
73
  ```
74
74
 
75
+ > `{manual-verify-section}` 按上文「需人工校验段落」聚合规则渲染:有保留项 → `### ⚠️ 需人工校验` 标题 + 引用说明 + 条目列表;无保留项 → `### ✅ 无需人工校验` 标题 + 一行 `本次改动无需人工确认事项。`(不带 ⚠️、不带列表)。
76
+
75
77
  ## 评论查找与更新
76
78
 
77
79
  已有评论必须通过 Issues comments API 获取,而不是单独的 PR comments API。
@@ -0,0 +1,83 @@
1
+ # Bidirectional Review Handshake Protocol
2
+
3
+ > Shared by executor and reviewer across all three stages (analysis / plan / code) when running the `review-*` and `*-task` skills.
4
+ > This file is the **single source of truth** for the protocol; each SKILL only `Read`s it and never re-copies the vocabulary.
5
+
6
+ ## Core principles
7
+
8
+ - **A review finding is input to be verified, not a command to execute.** The executor must verify each finding before disposing of it — neither rubber-stamping nor blindly refuting.
9
+ - **Symmetric evidence burden**: every disposition, whether accept or refute, must carry **commensurate evidence**. "Accept" is not a zero-cost default path.
10
+ - **Converge before advancing**: while any unclosed disagreement, alternative fix, cannot-judge, or post-review commit exists, do not silently advance to the next stage, archive, or merge.
11
+
12
+ ## Executor four-state disposition (`*-task` skills, when responding to the prior review round in Round ≥ 2)
13
+
14
+ For each finding in the latest `review-*`, first Read/Grep the cited `file:line` / command, then assign one status:
15
+
16
+ | Status | Meaning | Required evidence |
17
+ |--------|---------|-------------------|
18
+ | `accepted` | Valid; will fix as suggested | `file:line` of the fix, or the change to be applied this round |
19
+ | `adjusted` | Valid, but an alternative fix is used | the alternative + why it is better; awaits reviewer confirmation |
20
+ | `refuted` | After verification, judged invalid / hallucinated / based on a wrong `file:line` | counter-evidence (`file:line` or raw command output); awaits reviewer confirmation |
21
+ | `cannot-judge` | Insufficient evidence to decide | the verification path attempted; handed to reviewer/human |
22
+
23
+ ## Reviewer hand-back duty (`review-*` skills, when re-reviewing the executor response)
24
+
25
+ After the executor gives `adjusted` / `refuted` / `cannot-judge`, the reviewer must respond per item — never re-reading the original finding nor ignoring the hand-back:
26
+
27
+ - **Withdraw the finding** → set the ledger row to `confirmed` (accepts the refutation).
28
+ - **Accept the alternative fix** → set to `confirmed`.
29
+ - **Hold with new evidence** → set back to `open` (with new evidence, returned to the executor).
30
+ - **Escalate to human** → set to `needs-human-decision`.
31
+
32
+ ## Convergence termination (loop guard)
33
+
34
+ - The per-finding handshake round limit is `MAX_HANDSHAKE_ROUNDS`, default **3**, overridable via `review.maxHandshakeRounds` in `.agents/.airc.json`.
35
+ - When a finding's `round` reaches the limit without entering a terminal state, it must be forced to `needs-human-decision`; the gate rejects rows that hit the limit without escalating.
36
+ - `needs-human-decision` keeps blocking completion until a human records a ruling in the task.md `## 人工裁决` section and flips the row to `human-decided`.
37
+
38
+ ## Same-model convergence-bias mitigation (documentation-level discipline)
39
+
40
+ The executor and reviewer are often the same/similar model and are naturally inclined to agree. When reviewing:
41
+
42
+ 1. **Read the evidence before the conclusion**: read the `git diff` / artifact itself and form findings independently **before** reading the executor's conclusions and responses, to avoid being anchored.
43
+ 2. **Default-skeptical framing**: treat "looks fine" as unverified; every clearance needs reproducible evidence (see the `Evidence` hard gate in each `review-*`).
44
+
45
+ > The only mechanical lever is the **symmetric-evidence gate** (non-`open` ledger rows must carry evidence); model homogeneity itself is not mechanically checkable, so this section is discipline rather than a gate.
46
+
47
+ ## Mechanical ledger (task.md `## 审查分歧账本`)
48
+
49
+ The single source of truth for disagreement state is the fixed `## 审查分歧账本` section in task.md — one parseable Markdown table. The phase-advance and `complete-task` gates read this section.
50
+
51
+ ```markdown
52
+ ## 审查分歧账本
53
+
54
+ <!-- One row per review finding; state machine / evidence rules in .agents/rules/review-handshake.md. The phase-advance and complete-task gates read this section. -->
55
+
56
+ | id | stage | round | severity | status | evidence |
57
+ |----|-------|-------|----------|--------|----------|
58
+ | CD-1 | code | 1 | blocker | open | review-code.md#1 |
59
+ ```
60
+
61
+ - `id`: stage prefix + ordinal — analysis→`AN-`, plan→`PL-`, code→`CD-`.
62
+ - `stage` ∈ `{analysis, plan, code}` (plus the reserved value `post-review-commit`, used only for post-review exemption rows).
63
+ - `status` legal enum: `open` / `accepted` / `adjusted` / `refuted` / `cannot-judge` / `confirmed` / `needs-human-decision` / `closed` / `human-decided`.
64
+ - **Terminal set (gate passes)**: `{confirmed, closed, human-decided}`; everything else is blocking.
65
+ - **Write responsibility**: `review-*` raises a finding → upsert an `open` row; `*-task` responds → set four-state and fill `evidence`, `round` +1; next `review-*` → `confirmed` / back to `open` / `needs-human-decision`; an executor fix verified by the next review → `closed`; a human ruling → `human-decided`.
66
+ - **Backward compatible**: when task.md has no such section the gate treats it as no open disagreements and passes.
67
+
68
+ ## post-review commit gate (code stage only)
69
+
70
+ - The highest-round `review-code` report records `Review Baseline Commit` (R, `git rev-parse HEAD`) and `Reviewed Diff Fingerprint` (F, full worktree diff fingerprint).
71
+ - `commit` reads only the highest-round `review-code` artifact. When that artifact is Approved, the pre-commit HEAD equals R, and the staged diff fingerprint equals F, task.md receives `last_reviewed_commit` (B, the new commit SHA).
72
+ - The `complete-task` `post-review-commit` gate prefers B; when B is absent or invalid, it falls back to R from the highest-round `review-code` artifact.
73
+ - If new commits touch code / rule paths after B / R, the gate blocks and requires a fresh `review-code`.
74
+ - **Exemption**: append a ledger row `| PRC-1 | post-review-commit | - | - | human-decided | <ruling note> |` recording that a human explicitly allowed those commits without re-review.
75
+
76
+ ## Gate behavior cheat sheet
77
+
78
+ | Caller | `review-ledger` scope | `post-review-commit` |
79
+ |--------|-----------------------|----------------------|
80
+ | `plan-task` | only `analysis`-stage rows must be terminal | not attached |
81
+ | `code-task` | `analysis` + `plan`-stage rows must be terminal | not attached |
82
+ | `complete-task` | all stage rows must be terminal | attached (see above) |
83
+ | `analyze-task` | not attached (first stage) | not attached |
@@ -0,0 +1,83 @@
1
+ # 双向审查握手协议
2
+
3
+ > 三阶段(analysis / plan / code)的执行方与检视方在执行 `review-*` 与 `*-task` 技能时共用本协议。
4
+ > 这是协议的**单一事实源**;各 SKILL 只 `Read` 本文件,不重复抄写词表。
5
+
6
+ ## 核心原则
7
+
8
+ - **检视意见是待验证输入,不是执行命令**。执行方必须逐条核实后再处置,不默认认账、不盲目反驳。
9
+ - **对称证据负担**:无论接受还是反驳,每条处置都要附**相称证据**。"接受"不是零成本默认路径。
10
+ - **达成一致再推进**:存在未关闭分歧、替代修法、无法判断或 review 后新增提交时,不得静默进入下一阶段、归档或合并。
11
+
12
+ ## 执行方四态处置(`*-task` 技能,Round ≥ 2 响应上一轮审查时)
13
+
14
+ 对上一轮 `review-*` 的每条 finding,先 Read/Grep 核实其引用的 `file:line` / 命令,再落一个状态:
15
+
16
+ | 状态 | 含义 | 必附证据 |
17
+ |------|------|----------|
18
+ | `accepted` | 成立,将按建议修复 | 指向修复点的 `file:line` 或本轮将施加的改动说明 |
19
+ | `adjusted` | 成立,但采用替代修法 | 替代修法说明 + 为何更优;待检视方确认 |
20
+ | `refuted` | 核实后判定不成立 / 幻觉 / 基于错误 `file:line` | 反证(`file:line` 或命令原文);待检视方确认 |
21
+ | `cannot-judge` | 证据不足,无法判断 | 已尝试的核实路径;交检视方/人工 |
22
+
23
+ ## 检视方回交义务(`review-*` 技能,对执行方响应复核时)
24
+
25
+ 执行方给出 `adjusted` / `refuted` / `cannot-judge` 后,检视方必须逐条回应,不得复读原意见或无视:
26
+
27
+ - **撤回 finding** → 账本置 `confirmed`(接受反驳)。
28
+ - **接受替代修法** → 账本置 `confirmed`。
29
+ - **补充新证据后坚持** → 账本置回 `open`(带新证据,回到执行方)。
30
+ - **升级人工裁决** → 账本置 `needs-human-decision`。
31
+
32
+ ## 收敛终止语义(防死循环)
33
+
34
+ - 单条 finding 的握手轮次上限 `MAX_HANDSHAKE_ROUNDS`,默认 **3**,可在 `.agents/.airc.json` 的 `review.maxHandshakeRounds` 覆盖。
35
+ - 某条 finding 的 `round` 达到上限仍未进入终态,必须强制置 `needs-human-decision`;gate 会拦截"达限却未升级"的行。
36
+ - `needs-human-decision` 持续阻塞完成,直到人工在 task.md `## 人工裁决` 段记录裁定并把该行翻为 `human-decided`。
37
+
38
+ ## 同源模型收敛偏差缓解(文档级纪律)
39
+
40
+ 执行方与检视方常由相近模型承担,天然容易互相同意。检视时遵守:
41
+
42
+ 1. **先看证据再看结论**:先读 `git diff` / 产物本体并独立形成 findings,**再**读执行方的结论与响应,避免被其结论锚定。
43
+ 2. **默认怀疑框架**:把"看起来没问题"视为未验证;每条放行都要有可复现证据支撑(见各 `review-*` 的 `证据原文` 段硬门禁)。
44
+
45
+ > 唯一的机械杠杆是**对称证据 gate**(账本非 `open` 行必须有证据);模型同源性本身不可机械校验,故本节为纪律而非门禁。
46
+
47
+ ## 机械账本(task.md `## 审查分歧账本`)
48
+
49
+ 分歧状态的**单一事实源**是 task.md 的固定段 `## 审查分歧账本`,单张可解析表。阶段推进与 `complete-task` 的 gate 读取本段。
50
+
51
+ ```markdown
52
+ ## 审查分歧账本
53
+
54
+ <!-- 每条 review finding 一行;状态机/证据规则见 .agents/rules/review-handshake.md。阶段推进与 complete-task gate 读取本段。 -->
55
+
56
+ | id | stage | round | severity | status | evidence |
57
+ |----|-------|-------|----------|--------|----------|
58
+ | CD-1 | code | 1 | blocker | open | review-code.md#1 |
59
+ ```
60
+
61
+ - `id`:阶段前缀 + 序号——analysis→`AN-`、plan→`PL-`、code→`CD-`。
62
+ - `stage` ∈ `{analysis, plan, code}`(外加保留值 `post-review-commit`,仅用于 post-review 豁免行)。
63
+ - `status` 合法枚举:`open` / `accepted` / `adjusted` / `refuted` / `cannot-judge` / `confirmed` / `needs-human-decision` / `closed` / `human-decided`。
64
+ - **终态集合(gate 放行)**:`{confirmed, closed, human-decided}`;其余为阻塞态。
65
+ - **写入责任**:`review-*` 提 finding → upsert `open` 行;`*-task` 响应 → 改四态并填 `evidence`、`round` +1;下一轮 `review-*` → `confirmed` / 置回 `open` / `needs-human-decision`;执行方修复经下一轮 review 验证通过 → `closed`;人工裁决 → `human-decided`。
66
+ - **向后兼容**:task.md 无此段时,gate 视为无未决分歧而放行。
67
+
68
+ ## post-review commit 门禁(仅 code 阶段)
69
+
70
+ - `review-code` 在最高轮报告中记录 `审查基线提交`(R,`git rev-parse HEAD`)和 `审查差异指纹`(F,完整工作区 diff fingerprint)。
71
+ - `commit` 只读取最高轮 `review-code` 产物;当该产物 Approved、提交前 HEAD 等于 R、且 staged diff fingerprint 等于 F 时,在 task.md 写入 `last_reviewed_commit`(B,新提交 SHA)。
72
+ - `complete-task` 的 `post-review-commit` gate 优先使用 B;B 缺失或非法时回退最高轮 `review-code` 的 R。
73
+ - 若 B / R 之后代码 / 规则路径出现新提交,gate 会拦截,要求重新 `review-code`。
74
+ - **豁免**:在账本追加一行 `| PRC-1 | post-review-commit | - | - | human-decided | <裁定说明> |`,记录人工明确允许该批提交免复审。
75
+
76
+ ## gate 行为速查
77
+
78
+ | 调用方 | `review-ledger` 作用域 | `post-review-commit` |
79
+ |--------|------------------------|----------------------|
80
+ | `plan-task` | 仅 `analysis` 阶段行须终态 | 不挂 |
81
+ | `code-task` | `analysis` + `plan` 阶段行须终态 | 不挂 |
82
+ | `complete-task` | 全部阶段行须终态 | 挂(见上) |
83
+ | `analyze-task` | 不挂(首阶段) | 不挂 |
@@ -0,0 +1,56 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { artifactName, maxRound } from "./review-artifacts.js";
5
+
6
+ export const DEFAULT_POST_REVIEW_GLOBS = [
7
+ ".agents/skills",
8
+ ".agents/scripts",
9
+ ".agents/rules",
10
+ ".agents/workflows",
11
+ "bin",
12
+ "lib",
13
+ "src",
14
+ "templates"
15
+ ];
16
+
17
+ export function resolvePostReviewGlobs(config = {}, reviewConfig = {}) {
18
+ if (Array.isArray(config.post_review_globs)) {
19
+ return config.post_review_globs;
20
+ }
21
+ if (Array.isArray(reviewConfig.post_review_globs)) {
22
+ return reviewConfig.post_review_globs;
23
+ }
24
+ return DEFAULT_POST_REVIEW_GLOBS;
25
+ }
26
+
27
+ export function findAuthoritativeReviewCodeArtifact(taskDir) {
28
+ const entries = fs.existsSync(taskDir) ? fs.readdirSync(taskDir) : [];
29
+ const round = maxRound(entries, "review-code");
30
+ if (round === 0) {
31
+ return { ok: false, round: 0, fileName: null, path: null };
32
+ }
33
+
34
+ const fileName = artifactName("review-code", round);
35
+ return {
36
+ ok: true,
37
+ round,
38
+ fileName,
39
+ path: path.join(taskDir, fileName)
40
+ };
41
+ }
42
+
43
+ export function extractReviewBaseline(content) {
44
+ const match = String(content).match(/^[-*]?\s*\*\*(?:审查基线提交|Review Baseline Commit)\*\*[::]\s*(.*?)\s*$/m);
45
+ return match ? match[1].trim().replace(/`/g, "") : "";
46
+ }
47
+
48
+ export function extractReviewDiffFingerprint(content) {
49
+ const match = String(content).match(/^[-*]?\s*\*\*(?:审查差异指纹|Reviewed Diff Fingerprint)\*\*[::]\s*(.*?)\s*$/m);
50
+ return match ? match[1].trim().replace(/`/g, "") : "";
51
+ }
52
+
53
+ export function parseReviewVerdict(content) {
54
+ const match = String(content).match(/^[-*]?\s*\*\*(?:总体结论|Overall Verdict)\*\*[::]\s*(.*?)\s*$/m);
55
+ return match ? match[1].trim() : "";
56
+ }
@@ -0,0 +1,117 @@
1
+ // Shared helpers for review-artifact parsing.
2
+ // Imported by both .agents/skills/code-task/scripts/detect-mode.js and
3
+ // .agents/scripts/validate-artifact.js so the round/verdict vocabulary stays
4
+ // in a single source of truth (prevents the cross-file drift this lifecycle
5
+ // is designed to eliminate).
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+
9
+ export function escapeRegExp(value) {
10
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11
+ }
12
+
13
+ export function maxRound(entries, stem) {
14
+ let max = 0;
15
+ for (const entry of entries) {
16
+ if (entry === `${stem}.md`) {
17
+ max = Math.max(max, 1);
18
+ continue;
19
+ }
20
+
21
+ const match = entry.match(new RegExp(`^${escapeRegExp(stem)}-r(\\d+)\\.md$`));
22
+ if (match) {
23
+ max = Math.max(max, Number(match[1]));
24
+ }
25
+ }
26
+ return max;
27
+ }
28
+
29
+ export function artifactName(stem, round) {
30
+ return round === 1 ? `${stem}.md` : `${stem}-r${round}.md`;
31
+ }
32
+
33
+ export function normalizeVerdict(raw) {
34
+ const value = String(raw).trim().toLowerCase();
35
+ if (value === "通过" || value === "approved") {
36
+ return "Approved";
37
+ }
38
+ if (value === "需要修改" || value === "changes requested") {
39
+ return "Changes Requested";
40
+ }
41
+ if (value === "拒绝" || value === "rejected") {
42
+ return "Rejected";
43
+ }
44
+ return "";
45
+ }
46
+
47
+ export function extractSection(content, names) {
48
+ const lines = content.split(/\r?\n/);
49
+ const nameSet = new Set(names);
50
+ const start = lines.findIndex((line) => {
51
+ const match = line.trim().match(/^##\s+(.+?)\s*$/);
52
+ return match ? nameSet.has(match[1]) : false;
53
+ });
54
+
55
+ if (start === -1) {
56
+ return "";
57
+ }
58
+
59
+ const sectionLines = [];
60
+ for (let index = start + 1; index < lines.length; index += 1) {
61
+ if (/^##\s+/.test(lines[index])) {
62
+ break;
63
+ }
64
+ sectionLines.push(lines[index]);
65
+ }
66
+ return sectionLines.join("\n");
67
+ }
68
+
69
+ // Parse the canonical verdict out of a review-* artifact.
70
+ // Returns { ok, verdict, message }. Verdict collapses Approved into
71
+ // "Approved-with-issues" when the findings counts are non-zero.
72
+ export function parseVerdict(reviewPath) {
73
+ if (!fs.existsSync(reviewPath)) {
74
+ return { ok: false, verdict: null, message: `Review artifact not found: ${path.basename(reviewPath)}` };
75
+ }
76
+
77
+ const content = fs.readFileSync(reviewPath, "utf8");
78
+ const summary = extractSection(content, ["审查摘要", "Review Summary"]);
79
+ const fileName = path.basename(reviewPath);
80
+ if (!summary) {
81
+ return { ok: false, verdict: null, message: `cannot locate review summary section in ${fileName}` };
82
+ }
83
+
84
+ const verdictMatch = summary.match(/^[-*]?\s*\*\*(?:总体结论|Overall Verdict)\*\*[::]\s*(.+?)\s*$/im);
85
+ if (!verdictMatch) {
86
+ return { ok: false, verdict: null, message: `cannot parse verdict in ${fileName}` };
87
+ }
88
+
89
+ const verdict = normalizeVerdict(verdictMatch[1]);
90
+ if (!verdict) {
91
+ return {
92
+ ok: false,
93
+ verdict: null,
94
+ message: `unrecognized verdict '${verdictMatch[1].trim()}' in ${fileName}`
95
+ };
96
+ }
97
+
98
+ if (verdict !== "Approved") {
99
+ return { ok: true, verdict };
100
+ }
101
+
102
+ const findingsMatch = summary.match(/^[-*]?\s*\*\*(?:发现(AI 可处理)|Findings \(AI-actionable\))\*\*[::]\s*(.+?)\s*$/im);
103
+ if (!findingsMatch) {
104
+ return { ok: false, verdict, message: `cannot parse findings count in ${fileName}` };
105
+ }
106
+
107
+ const counts = findingsMatch[1].match(/(\d+)\s*(?:阻塞项|blockers?).*?(\d+)\s*(?:主要|majors?).*?(\d+)\s*(?:次要|minors?)/i);
108
+ if (!counts) {
109
+ return { ok: false, verdict, message: `cannot parse findings count in ${fileName}` };
110
+ }
111
+
112
+ const [, blockers, majors, minors] = counts.map(Number);
113
+ return {
114
+ ok: true,
115
+ verdict: blockers === 0 && majors === 0 && minors === 0 ? "Approved" : "Approved-with-issues"
116
+ };
117
+ }
@@ -0,0 +1,99 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import { execFileSync } from "node:child_process";
7
+
8
+ import { resolvePostReviewGlobs } from "./lib/post-review-commit.js";
9
+
10
+ const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
11
+ cwd: process.cwd(),
12
+ encoding: "utf8"
13
+ }).trim();
14
+
15
+ function usage() {
16
+ console.error("Usage: node .agents/scripts/review-diff-fingerprint.js <worktree|staged> <baseline>");
17
+ process.exit(2);
18
+ }
19
+
20
+ function git(args, options = {}) {
21
+ return execFileSync("git", args, {
22
+ cwd: repoRoot,
23
+ encoding: options.encoding || "utf8",
24
+ env: options.env || process.env
25
+ });
26
+ }
27
+
28
+ function loadReviewConfig() {
29
+ const configPath = path.join(repoRoot, ".agents", ".airc.json");
30
+ if (!fs.existsSync(configPath)) {
31
+ return {};
32
+ }
33
+
34
+ try {
35
+ return JSON.parse(fs.readFileSync(configPath, "utf8")).review || {};
36
+ } catch {
37
+ return {};
38
+ }
39
+ }
40
+
41
+ function splitNull(output) {
42
+ return output.split("\0").filter((value) => value !== "");
43
+ }
44
+
45
+ function hashDiff(args, env = process.env) {
46
+ const diff = execFileSync("git", args, {
47
+ cwd: repoRoot,
48
+ encoding: "buffer",
49
+ env
50
+ });
51
+ return `sha256:${crypto.createHash("sha256").update(diff).digest("hex")}`;
52
+ }
53
+
54
+ function worktreeFingerprint(baseline, globs) {
55
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-infra-review-index-"));
56
+ const tempIndex = path.join(tempDir, "index");
57
+ const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
58
+
59
+ try {
60
+ execFileSync("git", ["read-tree", baseline], { cwd: repoRoot, env });
61
+
62
+ const tracked = splitNull(git(["diff", "--name-only", "-z", baseline, "--", ...globs]));
63
+ const untracked = splitNull(git(["ls-files", "-o", "--exclude-standard", "-z", "--", ...globs]));
64
+ const paths = [...new Set([...tracked, ...untracked])];
65
+
66
+ for (const filePath of paths) {
67
+ const absolutePath = path.join(repoRoot, filePath);
68
+ if (fs.existsSync(absolutePath)) {
69
+ execFileSync("git", ["update-index", "--add", "--", filePath], { cwd: repoRoot, env });
70
+ } else {
71
+ execFileSync("git", ["update-index", "--remove", "--", filePath], { cwd: repoRoot, env });
72
+ }
73
+ }
74
+
75
+ return hashDiff(["diff", "--cached", "--binary", baseline, "--", ...globs], env);
76
+ } finally {
77
+ fs.rmSync(tempDir, { recursive: true, force: true });
78
+ }
79
+ }
80
+
81
+ function stagedFingerprint(baseline, globs) {
82
+ return hashDiff(["diff", "--cached", "--binary", baseline, "--", ...globs]);
83
+ }
84
+
85
+ function main(argv) {
86
+ const [mode, baseline] = argv;
87
+ if (!["worktree", "staged"].includes(mode) || !baseline) {
88
+ usage();
89
+ }
90
+
91
+ git(["rev-parse", "--verify", `${baseline}^{commit}`]);
92
+ const globs = resolvePostReviewGlobs({}, loadReviewConfig());
93
+ const fingerprint = mode === "worktree"
94
+ ? worktreeFingerprint(baseline, globs)
95
+ : stagedFingerprint(baseline, globs);
96
+ process.stdout.write(`${fingerprint}\n`);
97
+ }
98
+
99
+ main(process.argv.slice(2));