@fitlab-ai/agent-infra 0.8.4 → 0.8.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 (51) hide show
  1. package/dist/lib/defaults.json +8 -0
  2. package/dist/lib/init.js +5 -0
  3. package/dist/lib/sandbox/commands/create.js +30 -4
  4. package/dist/lib/sandbox/engine.js +33 -3
  5. package/dist/lib/sandbox/engines/colima.js +2 -2
  6. package/dist/lib/sandbox/runtimes/base.dockerfile +4 -0
  7. package/dist/lib/server/daemon.js +9 -0
  8. package/dist/lib/server/process-control.js +66 -73
  9. package/dist/lib/server/process-state.js +168 -0
  10. package/dist/lib/update.js +2 -0
  11. package/lib/defaults.json +8 -0
  12. package/lib/init.ts +9 -0
  13. package/lib/sandbox/commands/create.ts +32 -3
  14. package/lib/sandbox/engine.ts +37 -3
  15. package/lib/sandbox/engines/colima.ts +2 -2
  16. package/lib/sandbox/runtimes/base.dockerfile +4 -0
  17. package/lib/server/daemon.ts +9 -0
  18. package/lib/server/process-control.ts +70 -71
  19. package/lib/server/process-state.ts +183 -0
  20. package/lib/update.ts +6 -0
  21. package/package.json +2 -1
  22. package/runtime/platform-adapters/platform-sync.github.js +1501 -0
  23. package/templates/.agents/README.en.md +2 -0
  24. package/templates/.agents/README.zh-CN.md +2 -0
  25. package/templates/.agents/rules/review-handshake.en.md +5 -4
  26. package/templates/.agents/rules/review-handshake.zh-CN.md +5 -4
  27. package/templates/.agents/scripts/lib/agent-infra-package.js +149 -0
  28. package/templates/.agents/scripts/lib/post-review-commit.js +5 -0
  29. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +25 -1459
  30. package/templates/.agents/scripts/review-diff-fingerprint.js +95 -24
  31. package/templates/.agents/scripts/validate-artifact.js +68 -55
  32. package/templates/.agents/skills/commit/SKILL.en.md +5 -3
  33. package/templates/.agents/skills/commit/SKILL.zh-CN.md +5 -3
  34. package/templates/.agents/skills/commit/reference/task-status-update.en.md +29 -2
  35. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +29 -2
  36. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  37. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  38. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  39. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  40. package/templates/.agents/skills/init-milestones/SKILL.en.md +2 -2
  41. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +2 -2
  42. package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +5 -4
  43. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  44. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  45. package/templates/.agents/skills/review-code/config/verify.en.json +2 -1
  46. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +2 -1
  47. package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -1
  48. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -1
  49. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -1
  50. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +5 -1
  51. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +174 -136
@@ -13,7 +13,8 @@ const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
13
13
  }).trim();
14
14
 
15
15
  function usage() {
16
- console.error("Usage: node .agents/scripts/review-diff-fingerprint.js <worktree|staged> <baseline>");
16
+ console.error("Usage: node .agents/scripts/review-diff-fingerprint.js <worktree|staged> <baseline> [--format json]");
17
+ console.error(" or: node .agents/scripts/review-diff-fingerprint.js compare <expected-tree> <actual-tree> [--format json]");
17
18
  process.exit(2);
18
19
  }
19
20
 
@@ -42,6 +43,15 @@ function splitNull(output) {
42
43
  return output.split("\0").filter((value) => value !== "");
43
44
  }
44
45
 
46
+ function pathExists(filePath) {
47
+ try {
48
+ fs.lstatSync(filePath);
49
+ return true;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
45
55
  function hashDiff(args, env = process.env) {
46
56
  const diff = execFileSync("git", args, {
47
57
  cwd: repoRoot,
@@ -51,49 +61,110 @@ function hashDiff(args, env = process.env) {
51
61
  return `sha256:${crypto.createHash("sha256").update(diff).digest("hex")}`;
52
62
  }
53
63
 
54
- function worktreeFingerprint(baseline, globs) {
64
+ function withTemporaryIndex(baseline, callback) {
55
65
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-infra-review-index-"));
56
66
  const tempIndex = path.join(tempDir, "index");
57
67
  const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
58
68
 
59
69
  try {
60
70
  execFileSync("git", ["read-tree", baseline], { cwd: repoRoot, env });
71
+ return callback(env);
72
+ } finally {
73
+ fs.rmSync(tempDir, { recursive: true, force: true });
74
+ }
75
+ }
61
76
 
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
- }
77
+ function projectWorktree(baseline, globs, env) {
78
+ const tracked = splitNull(git(["diff", "--name-only", "-z", baseline, "--", ...globs]));
79
+ const untracked = splitNull(git(["ls-files", "-o", "--exclude-standard", "-z", "--", ...globs]));
80
+ const paths = [...new Set([...tracked, ...untracked])];
81
+
82
+ for (const filePath of paths) {
83
+ const absolutePath = path.join(repoRoot, filePath);
84
+ if (pathExists(absolutePath)) {
85
+ execFileSync("git", ["update-index", "--add", "--", filePath], { cwd: repoRoot, env });
86
+ } else {
87
+ execFileSync("git", ["update-index", "--remove", "--", filePath], { cwd: repoRoot, env });
73
88
  }
89
+ }
90
+ }
74
91
 
75
- return hashDiff(["diff", "--cached", "--binary", baseline, "--", ...globs], env);
76
- } finally {
77
- fs.rmSync(tempDir, { recursive: true, force: true });
92
+ function projectStaged(baseline, globs, env) {
93
+ const paths = splitNull(git(["diff", "--cached", "--name-only", "-z", baseline, "--", ...globs]));
94
+ for (const filePath of paths) {
95
+ const entry = execFileSync(
96
+ "git",
97
+ ["--literal-pathspecs", "ls-files", "--stage", "-z", "--", filePath],
98
+ { cwd: repoRoot, encoding: "buffer" }
99
+ );
100
+ const indexInfo = entry.length > 0
101
+ ? entry
102
+ : Buffer.from(`0 ${"0".repeat(40)}\t${filePath}\0`);
103
+ execFileSync("git", ["update-index", "-z", "--index-info"], {
104
+ cwd: repoRoot,
105
+ env,
106
+ input: indexInfo
107
+ });
78
108
  }
79
109
  }
80
110
 
81
- function stagedFingerprint(baseline, globs) {
82
- return hashDiff(["diff", "--cached", "--binary", baseline, "--", ...globs]);
111
+ function snapshot(mode, baseline, globs) {
112
+ return withTemporaryIndex(baseline, (env) => {
113
+ if (mode === "worktree") {
114
+ projectWorktree(baseline, globs, env);
115
+ } else {
116
+ projectStaged(baseline, globs, env);
117
+ }
118
+ return {
119
+ baseline,
120
+ fingerprint: hashDiff(["diff", "--cached", "--binary", baseline, "--", ...globs], env),
121
+ tree: git(["write-tree"], { env }).trim()
122
+ };
123
+ });
124
+ }
125
+
126
+ function compareTrees(expected, actual) {
127
+ git(["rev-parse", "--verify", `${expected}^{tree}`]);
128
+ git(["rev-parse", "--verify", `${actual}^{tree}`]);
129
+ const fields = splitNull(git(["diff", "--name-status", "-z", "--no-renames", expected, actual]));
130
+ const result = { equal: fields.length === 0, added: [], missing: [], different: [] };
131
+ for (let index = 0; index < fields.length; index += 2) {
132
+ const status = fields[index];
133
+ const filePath = fields[index + 1];
134
+ if (status === "A") {
135
+ result.added.push(filePath);
136
+ } else if (status === "D") {
137
+ result.missing.push(filePath);
138
+ } else {
139
+ result.different.push(filePath);
140
+ }
141
+ }
142
+ return result;
83
143
  }
84
144
 
85
145
  function main(argv) {
86
- const [mode, baseline] = argv;
146
+ const [mode, first, second] = argv;
147
+ const json = argv.includes("--format") && argv[argv.indexOf("--format") + 1] === "json";
148
+
149
+ if (mode === "compare") {
150
+ if (!first || !second) {
151
+ usage();
152
+ }
153
+ const result = compareTrees(first, second);
154
+ process.stdout.write(json ? `${JSON.stringify(result)}\n` : `${result.equal ? "equal" : "different"}\n`);
155
+ process.exitCode = result.equal ? 0 : 1;
156
+ return;
157
+ }
158
+
159
+ const baseline = first;
87
160
  if (!["worktree", "staged"].includes(mode) || !baseline) {
88
161
  usage();
89
162
  }
90
163
 
91
- git(["rev-parse", "--verify", `${baseline}^{commit}`]);
164
+ const resolvedBaseline = git(["rev-parse", "--verify", `${baseline}^{commit}`]).trim();
92
165
  const globs = resolvePostReviewGlobs({}, loadReviewConfig());
93
- const fingerprint = mode === "worktree"
94
- ? worktreeFingerprint(baseline, globs)
95
- : stagedFingerprint(baseline, globs);
96
- process.stdout.write(`${fingerprint}\n`);
166
+ const result = snapshot(mode, resolvedBaseline, globs);
167
+ process.stdout.write(json ? `${JSON.stringify(result)}\n` : `${result.fingerprint}\n`);
97
168
  }
98
169
 
99
170
  main(process.argv.slice(2));
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
7
7
  import {
8
8
  extractReviewBaseline,
9
9
  extractReviewDiffFingerprint,
10
+ extractReviewedSnapshotTree,
10
11
  findAuthoritativeReviewCodeArtifact,
11
12
  parseReviewVerdict,
12
13
  resolvePostReviewGlobs
@@ -779,12 +780,10 @@ function checkPostReviewCommit({ taskDir, config }) {
779
780
 
780
781
  const task = loadTask(taskDir);
781
782
  const content = fs.readFileSync(reviewArtifact.path, "utf8");
782
- const reviewBaseline = extractReviewBaseline(content);
783
783
  const lastReviewedCommit = task.ok ? (task.metadata.last_reviewed_commit || "").trim() : "";
784
784
  const baselineSource = resolvePostReviewBaseline({
785
785
  gitRoot,
786
786
  lastReviewedCommit,
787
- reviewBaseline,
788
787
  reviewArtifact: reviewArtifact.fileName
789
788
  });
790
789
  if (!baselineSource.ok) {
@@ -818,7 +817,7 @@ function checkPostReviewCommit({ taskDir, config }) {
818
817
 
819
818
  return failResult(
820
819
  "post-review-commit",
821
- `${commits.length} commit(s) to code/rule paths after review baseline ${sha.slice(0, 8)}; re-run review-code or record a human-decided exemption`
820
+ `${commits.length} commit(s) to code/rule paths after reviewed commit ${sha.slice(0, 8)}; re-run review-code or record a human-decided exemption`
822
821
  );
823
822
  }
824
823
 
@@ -841,6 +840,7 @@ function checkReviewFact({ taskDir, artifactFile }) {
841
840
  const verdict = parseReviewVerdict(content);
842
841
  const reviewBaseline = extractReviewBaseline(content);
843
842
  const reviewedFingerprint = extractReviewDiffFingerprint(content);
843
+ const reviewedTree = extractReviewedSnapshotTree(content);
844
844
 
845
845
  if (!["通过", "需要修改", "拒绝", "Approved", "Changes Requested", "Rejected"].includes(verdict)) {
846
846
  return failResult("review-fact", `Unsupported review verdict '${verdict}'`);
@@ -867,13 +867,13 @@ function checkReviewFact({ taskDir, artifactFile }) {
867
867
  );
868
868
  }
869
869
 
870
- let actualFingerprint;
870
+ let actualSnapshot;
871
871
  try {
872
- actualFingerprint = execFileSync(
872
+ actualSnapshot = JSON.parse(execFileSync(
873
873
  process.execPath,
874
- [path.join(repoRoot, ".agents", "scripts", "review-diff-fingerprint.js"), "worktree", baseline],
874
+ [path.join(repoRoot, ".agents", "scripts", "review-diff-fingerprint.js"), "worktree", baseline, "--format", "json"],
875
875
  { cwd: gitRoot, encoding: "utf8" }
876
- ).trim();
876
+ ));
877
877
  } catch {
878
878
  return blockedResult(
879
879
  "review-fact",
@@ -881,26 +881,37 @@ function checkReviewFact({ taskDir, artifactFile }) {
881
881
  );
882
882
  }
883
883
 
884
- if (actualFingerprint !== reviewedFingerprint) {
884
+ if (actualSnapshot.fingerprint !== reviewedFingerprint) {
885
885
  return failResult(
886
886
  "review-fact",
887
887
  `Reviewed diff fingerprint does not match the current worktree for baseline ${baseline.slice(0, 8)}; re-run review-code`
888
888
  );
889
889
  }
890
890
 
891
+ if (!reviewedTree || actualSnapshot.tree !== reviewedTree) {
892
+ return failResult(
893
+ "review-fact",
894
+ `Reviewed snapshot tree does not match the current worktree for baseline ${baseline.slice(0, 8)}; re-run review-code`
895
+ );
896
+ }
897
+
891
898
  if (["通过", "Approved"].includes(verdict)) {
892
899
  const lastReviewedCommit = String(task.metadata.last_reviewed_commit || "").trim();
893
- let reviewedCommit = "";
894
- try {
895
- reviewedCommit = execFileSync("git", ["-C", gitRoot, "rev-parse", `${lastReviewedCommit}^{commit}`], { encoding: "utf8" }).trim();
896
- } catch {
897
- // The failure below names the missing or invalid task review fact.
900
+ const cleanSnapshot = actualSnapshot.tree === execFileSync(
901
+ "git",
902
+ ["-C", gitRoot, "rev-parse", `${baseline}^{tree}`],
903
+ { encoding: "utf8" }
904
+ ).trim();
905
+ if (cleanSnapshot && lastReviewedCommit !== baseline) {
906
+ return failResult(
907
+ "review-fact",
908
+ `Approved clean review must set task last_reviewed_commit to baseline ${baseline.slice(0, 8)}`
909
+ );
898
910
  }
899
-
900
- if (reviewedCommit !== baseline) {
911
+ if (!cleanSnapshot && lastReviewedCommit) {
901
912
  return failResult(
902
913
  "review-fact",
903
- `Approved review must set task last_reviewed_commit to baseline ${baseline.slice(0, 8)}`
914
+ "Approved review with uncommitted changes must remain unanchored until commit"
904
915
  );
905
916
  }
906
917
  }
@@ -911,35 +922,18 @@ function checkReviewFact({ taskDir, artifactFile }) {
911
922
  );
912
923
  }
913
924
 
914
- function resolvePostReviewBaseline({ gitRoot, lastReviewedCommit, reviewBaseline, reviewArtifact }) {
915
- if (lastReviewedCommit) {
916
- if (SHA_PATTERN.test(lastReviewedCommit) && gitCommitExists(gitRoot, lastReviewedCommit)) {
917
- return { ok: true, sha: lastReviewedCommit };
918
- }
919
- }
920
-
921
- if (!reviewBaseline) {
922
- return {
923
- ok: false,
924
- result: passResult(
925
- "post-review-commit",
926
- `${reviewArtifact} predates baseline-commit anchoring; skipped (legacy artifact)`,
927
- [`${reviewArtifact} has no 审查基线提交 / Review Baseline Commit field`]
928
- )
929
- };
930
- }
931
-
932
- if (!SHA_PATTERN.test(reviewBaseline)) {
933
- return {
934
- ok: false,
935
- result: blockedResult(
936
- "post-review-commit",
937
- `${reviewArtifact} has an empty or malformed 审查基线提交 SHA ('${reviewBaseline}'); manual remediation required`
938
- )
939
- };
925
+ function resolvePostReviewBaseline({ gitRoot, lastReviewedCommit, reviewArtifact }) {
926
+ if (SHA_PATTERN.test(lastReviewedCommit) && gitCommitExists(gitRoot, lastReviewedCommit)) {
927
+ return { ok: true, sha: lastReviewedCommit };
940
928
  }
941
929
 
942
- return { ok: true, sha: reviewBaseline };
930
+ return {
931
+ ok: false,
932
+ result: blockedResult(
933
+ "post-review-commit",
934
+ `${reviewArtifact}: reviewed snapshot was not anchored to a valid commit; re-run commit or review-code`
935
+ )
936
+ };
943
937
  }
944
938
 
945
939
  function gitCommitExists(gitRoot, sha) {
@@ -1035,22 +1029,41 @@ function parseFrontmatter(content) {
1035
1029
  function getSectionContent(content, names) {
1036
1030
  const lines = content.split(/\r?\n/);
1037
1031
 
1032
+ function visibleHeadings() {
1033
+ const headings = [];
1034
+ let fence = null;
1035
+ for (let index = 0; index < lines.length; index += 1) {
1036
+ const line = lines[index];
1037
+ if (fence) {
1038
+ const closer = line.match(/^ {0,3}(`+|~+)\s*$/);
1039
+ if (closer && closer[1][0] === fence.character && closer[1].length >= fence.length) {
1040
+ fence = null;
1041
+ }
1042
+ continue;
1043
+ }
1044
+ const opener = line.match(/^ {0,3}(`{3,}|~{3,})(?:[^`~].*)?$/);
1045
+ if (opener) {
1046
+ fence = { character: opener[1][0], length: opener[1].length };
1047
+ continue;
1048
+ }
1049
+ if (line.startsWith("## ")) {
1050
+ headings.push({ index, text: line.trim() });
1051
+ }
1052
+ }
1053
+ return headings;
1054
+ }
1055
+
1056
+ const headings = visibleHeadings();
1057
+
1038
1058
  for (const name of names) {
1039
1059
  const heading = `## ${name}`;
1040
- const startIndex = lines.findIndex((line) => line.trim() === heading);
1041
- if (startIndex === -1) {
1060
+ const position = headings.findIndex((item) => item.text === heading);
1061
+ if (position === -1) {
1042
1062
  continue;
1043
1063
  }
1044
-
1045
- const sectionLines = [];
1046
- for (let index = startIndex + 1; index < lines.length; index += 1) {
1047
- if (lines[index].startsWith("## ")) {
1048
- break;
1049
- }
1050
- sectionLines.push(lines[index]);
1051
- }
1052
-
1053
- return sectionLines.join("\n").trim();
1064
+ const startIndex = headings[position].index;
1065
+ const endIndex = headings[position + 1]?.index ?? lines.length;
1066
+ return lines.slice(startIndex + 1, endIndex).join("\n").trim();
1054
1067
  }
1055
1068
 
1056
1069
  return "";
@@ -61,9 +61,11 @@ Review status, diff, and recent history, then prepare a Conventional Commit with
61
61
  Stage specific files only and run `git commit` with the prepared message.
62
62
 
63
63
  If this commit is associated with a task and a `review-code` artifact exists, read the highest-round `review-code` artifact before committing:
64
- - If that artifact's `Overall Verdict` / `总体结论` is Approved, parse `Review Baseline Commit` / `审查基线提交` as `R`, and `Reviewed Diff Fingerprint` / `审查差异指纹` as `F`
65
- - After staging the explicit files, run `node .agents/scripts/review-diff-fingerprint.js staged <R>` to get `S`, and record `pre_head=$(git rev-parse HEAD)`
66
- - After committing, write `last_reviewed_commit: <new_head>` to task.md frontmatter only when `pre_head == R` and `S == F`; otherwise do not advance that field
64
+ - If that artifact's `Overall Verdict` / `总体结论` is Approved, parse `R`, `F`, and `Reviewed Snapshot Tree` / `审查快照树` (`T`)
65
+ - After staging the explicit files, record `pre_head=$(git rev-parse HEAD)` and use the helper's JSON mode to generate the complete worktree tree `W` and normalized staged tree `S`
66
+ - Before `git commit`, require `pre_head == R && W == T && S == T`; use the helper's `compare` mode to produce added, missing, and different diagnostics for both worktree and staged snapshots
67
+ - If any condition fails, enter Scenario 4 in `reference/task-status-update.md`; do not run `git commit`, push, successful state updates, PR summary sync, or the completion gate
68
+ - After all comparisons match and the commit succeeds, write `last_reviewed_commit: <new_head>` to task.md frontmatter
67
69
  - Do not scan backward to earlier Approved artifacts; the highest-round `review-code` artifact is the only authoritative source
68
70
 
69
71
  ## 5. Push to the Existing PR When Applicable
@@ -61,9 +61,11 @@ git diff
61
61
  只暂存明确列出的文件,然后执行 `git commit`。
62
62
 
63
63
  如果本次提交关联任务且存在 `review-code` 产物,在提交前读取最高轮 `review-code` 产物:
64
- - 若该产物 `总体结论` / `Overall Verdict` 为 Approved,解析 `审查基线提交` / `Review Baseline Commit` 为 `R`,解析 `审查差异指纹` / `Reviewed Diff Fingerprint` 为 `F`
65
- - 暂存明确文件后运行 `node .agents/scripts/review-diff-fingerprint.js staged <R>` 得到 `S`,并记录 `pre_head=$(git rev-parse HEAD)`
66
- - 提交后仅当 `pre_head == R` 且 `S == F` 时,在 task.md frontmatter 写入 `last_reviewed_commit: <new_head>`;否则不推进该字段
64
+ - 若该产物 `总体结论` / `Overall Verdict` 为 Approved,解析 `R`、`F` 与 `审查快照树` / `Reviewed Snapshot Tree`(`T`)
65
+ - 暂存明确文件后记录 `pre_head=$(git rev-parse HEAD)`,并以 helper 的 JSON 模式生成当前完整工作区树 `W` 与规范化暂存树 `S`
66
+ - 在 `git commit` 前要求 `pre_head == R && W == T && S == T`;分别运行 helper 的 `compare` 模式生成 worktree/staged 的 added、missing、different 路径诊断
67
+ - 任一条件不满足时进入 `reference/task-status-update.md` 的“场景 4:提交前快照阻断”,不得执行 `git commit`、push、成功状态更新、PR 摘要同步或完成 gate
68
+ - 全部相等并成功提交后,在 task.md frontmatter 写入 `last_reviewed_commit: <new_head>`
67
69
  - 不向后扫描更早的 Approved 产物;最高轮 `review-code` 产物是唯一权威来源
68
70
 
69
71
  ## 5. 推送到已有 PR(按需)
@@ -18,13 +18,40 @@ For every task-related commit, append this Activity Log entry in `task.md`:
18
18
  - {YYYY-MM-DD HH:mm:ss±HH:MM} — **Commit** by {agent} — {commit hash short} {commit subject}
19
19
  ```
20
20
 
21
- If the commit stage confirmed that the highest-round `review-code` artifact is Approved, `pre_head` equals its review baseline commit `R`, and the staged diff fingerprint `S` equals its reviewed diff fingerprint `F`, also write or refresh:
21
+ If the commit stage confirmed that the highest-round `review-code` artifact is Approved, `pre_head == R`, complete worktree tree `W == T`, and normalized staged tree `S == T`, also write or refresh after a successful commit:
22
22
 
23
23
  ```yaml
24
24
  last_reviewed_commit: {new_head}
25
25
  ```
26
26
 
27
- This field is the preferred baseline for the `complete-task` `post-review-commit` gate. When any condition is not met, do not write or advance it.
27
+ This field is the only baseline for the `complete-task` `post-review-commit` gate. When any condition is not met, do not write or advance it.
28
+
29
+ ### Scenario 4: pre-commit snapshot block
30
+
31
+ This scenario ends the run before `git commit` and does not enter the successful post-commit scenario selection below. When any of `pre_head != R`, `W != T`, or `S != T` applies:
32
+
33
+ - Do not run `git commit`, push, successful state updates, Issue/PR success sync, or the commit completion gate; preserve the current worktree and index.
34
+ - Refresh task `updated_at`, `assigned_to`, and `agent_infra_version`, then append a done log with action `Commit`: `Blocked before git commit: reviewed snapshot mismatch (worktree added={a}, missing={m}, different={d}; staged added={a}, missing={m}, different={d})`.
35
+ - User output must include `No commit was created.` and show Added/Missing/Different under both `Current worktree vs reviewed snapshot` and `Staged snapshot vs reviewed snapshot`; render an empty set as `- (none)` and list paths only in user output.
36
+ - When `pre_head != R` or `W != T`, the only next step is a fresh `review-code`; when only `W == T && S != T`, tell the user to fix staging and rerun `commit` without re-review.
37
+
38
+ Full re-review commands:
39
+
40
+ ```text
41
+ Next step - re-run code review:
42
+ - Claude Code / OpenCode: /review-code {task-ref}
43
+ - Gemini CLI: /{{project}}:review-code {task-ref}
44
+ - Codex CLI: $review-code {task-ref}
45
+ ```
46
+
47
+ Full staging-only retry commands:
48
+
49
+ ```text
50
+ Next step - fix staging and retry commit:
51
+ - Claude Code / OpenCode: /commit {task-ref}
52
+ - Gemini CLI: /{{project}}:commit {task-ref}
53
+ - Codex CLI: $commit {task-ref}
54
+ ```
28
55
 
29
56
  Before selecting the next step, verify:
30
57
  - `current_step` and the latest workflow progress in `task.md`
@@ -18,13 +18,40 @@ date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\([+-][0-9][0-9]\)\([0-9][0-9]\)$/\1:\2/'
18
18
  - {YYYY-MM-DD HH:mm:ss±HH:MM} — **Commit** by {agent} — {commit hash short} {commit subject}
19
19
  ```
20
20
 
21
- 如果提交阶段已确认最高轮 `review-code` 产物 Approved、`pre_head` 等于其审查基线提交 `R`、且 staged 差异指纹 `S` 等于其审查差异指纹 `F`,同时写入或刷新:
21
+ 如果提交阶段已确认最高轮 `review-code` 产物 Approved、`pre_head == R`、完整工作区树 `W == T` 且规范化暂存树 `S == T`,成功提交后同时写入或刷新:
22
22
 
23
23
  ```yaml
24
24
  last_reviewed_commit: {new_head}
25
25
  ```
26
26
 
27
- 该字段是 `complete-task` 的 `post-review-commit` gate 优先 baseline。条件不满足时不要写入或推进该字段。
27
+ 该字段是 `complete-task` 的 `post-review-commit` gate 唯一 baseline。条件不满足时不要写入或推进该字段。
28
+
29
+ ### 场景 4:提交前快照阻断
30
+
31
+ 此场景在 `git commit` 前终止本轮,不进入下方提交成功后的场景选择。任一 `pre_head != R`、`W != T` 或 `S != T` 命中时:
32
+
33
+ - 不执行 `git commit`、push、成功状态更新、Issue/PR 成功同步或 commit 完成 gate;保留当前工作区与暂存区。
34
+ - 刷新任务的 `updated_at`、`assigned_to`、`agent_infra_version`,并以 action `Commit` 追加 done 日志:`Blocked before git commit: reviewed snapshot mismatch (worktree added={a}, missing={m}, different={d}; staged added={a}, missing={m}, different={d})`。
35
+ - 用户输出必须包含 `No commit was created.`,并分别按 `Current worktree vs reviewed snapshot`、`Staged snapshot vs reviewed snapshot` 展示 Added/Missing/Different;空集合显示 `- (none)`,路径只在用户输出中逐行展示。
36
+ - `pre_head != R` 或 `W != T` 时,下一步只指向重新 `review-code`;仅 `W == T && S != T` 时,提示修正暂存后重跑 `commit`,无需重审。
37
+
38
+ 重新审查的完整命令:
39
+
40
+ ```text
41
+ 下一步 - 重新代码审查:
42
+ - Claude Code / OpenCode: /review-code {task-ref}
43
+ - Gemini CLI: /{{project}}:review-code {task-ref}
44
+ - Codex CLI: $review-code {task-ref}
45
+ ```
46
+
47
+ 仅修正暂存的完整命令:
48
+
49
+ ```text
50
+ 下一步 - 修正暂存后重新提交:
51
+ - Claude Code / OpenCode: /commit {task-ref}
52
+ - Gemini CLI: /{{project}}:commit {task-ref}
53
+ - Codex CLI: $commit {task-ref}
54
+ ```
28
55
 
29
56
  在决定下一步之前,先确认:
30
57
  - `task.md` 中的 `current_step` 和最新工作流进度
@@ -98,7 +98,7 @@ node .agents/scripts/validate-artifact.js check review-ledger .agents/workspace/
98
98
  node .agents/scripts/validate-artifact.js check post-review-commit .agents/workspace/active/{task-id} --skill complete-task --format text
99
99
  ```
100
100
 
101
- A non-zero exit from either (fail/blocked) -> treat as an unmet prerequisite and **stop**, do not run Steps 3-7. `--force` does **NOT** lift this hard gate: unclosed disagreements must first be closed in the ledger (`confirmed`/`closed`/`human-decided`), and un-re-reviewed post-review commits must be re-reviewed via `review-code` or covered by a `post-review-commit` / `human-decided` exemption row in the ledger.
101
+ A non-zero exit from either (fail/blocked) -> treat as an unmet prerequisite and **stop**, do not run Steps 3-7. If the output contains `reviewed snapshot was not anchored`, rerun `commit` or `review-code`; never fall back to the review baseline. `--force` does **NOT** lift this hard gate: unclosed disagreements must first be closed in the ledger (`confirmed`/`closed`/`human-decided`), and un-re-reviewed commits after an anchor must be re-reviewed via `review-code` or covered by a `post-review-commit` / `human-decided` exemption row in the ledger.
102
102
 
103
103
  > **⚠️ Prerequisite Branch Check — you must decide whether to continue or stop before proceeding:**
104
104
  >
@@ -166,7 +166,7 @@ Check whether `task.md` includes a valid `issue_number`. If not, skip this step
166
166
  If a valid `issue_number` exists:
167
167
  - First scan and backfill unpublished `task.md`, `analysis*.md`, `review-analysis*.md`, `plan*.md`, `review-plan*.md`, `code*.md`, and `review-code*.md` comments using the backfill rules in `.agents/rules/issue-sync.md` (`task.md` uses the idempotent update path)
168
168
  - Backfill checked `## Requirements` items to the Issue body by following the requirements-checkbox sync steps in issue-sync.md
169
- - Do not set any `status:` label — status labels are automatically cleared when the Issue is closed
169
+ - Do not set any `status:` label — platform automation should clear status labels after the Issue closes; the pre-completion platform-sync gate verifies that a CLOSED Issue has no `status:` labels and fails until the workflow finishes or is repaired
170
170
  - Finally create or update the summary comment marked with the summary marker defined in `.agents/rules/issue-sync.md`
171
171
  - Read `.agents/rules/issue-fields.md` and follow Flow A to sync every non-empty Issue field (`priority`/`effort`/`start_date`/`target_date`) from `task.md` to the Issue (idempotent; skip without blocking when `has_push=false` or the fetch/write fails)
172
172
 
@@ -97,7 +97,7 @@ node .agents/scripts/validate-artifact.js check review-ledger .agents/workspace/
97
97
  node .agents/scripts/validate-artifact.js check post-review-commit .agents/workspace/active/{task-id} --skill complete-task --format text
98
98
  ```
99
99
 
100
- 任一退出码非 0(fail/blocked)→ 按前置条件未满足处理,**停止**,不执行步骤 3-7。`--force` **不解除**本硬门禁:未关闭分歧必须先在账本闭合(`confirmed`/`closed`/`human-decided`),未复审 post-review 提交必须重新 `review-code` 或在账本追加 `post-review-commit` / `human-decided` 豁免行。
100
+ 任一退出码非 0(fail/blocked)→ 按前置条件未满足处理,**停止**,不执行步骤 3-7。若输出包含 `reviewed snapshot was not anchored`,必须先重新 `commit` 或 `review-code`;不得回退审查基线。`--force` **不解除**本硬门禁:未关闭分歧必须先在账本闭合(`confirmed`/`closed`/`human-decided`),已锚点后的未复审提交必须重新 `review-code` 或在账本追加 `post-review-commit` / `human-decided` 豁免行。
101
101
 
102
102
  > **⚠️ 前置条件分支判断 — 你必须先判断“继续”还是“停止”:**
103
103
  >
@@ -165,7 +165,7 @@ ls .agents/workspace/completed/{task-id}/task.md
165
165
  如果存在有效的 `issue_number`:
166
166
  - 先按 `.agents/rules/issue-sync.md` 的补发规则扫描并补发未发布的 `task.md`、`analysis*.md`、`review-analysis*.md`、`plan*.md`、`review-plan*.md`、`code*.md`、`review-code*.md` 评论(`task.md` 走幂等更新路径)
167
167
  - 按 issue-sync.md 的需求复选框同步步骤,兜底同步 `## 需求` 中已勾选的条目到 Issue body
168
- - 不要设置 `status:` label — Issue 关闭后 status label 会被自动清除
168
+ - 不要设置 `status:` label — 平台自动化应在 Issue 关闭后清理状态标签;预完成 platform-sync gate 会验证 CLOSED Issue 不含任何 `status:` 标签,残留时失败并要求等待或修复 workflow 后重跑
169
169
  - 最后创建或更新 `.agents/rules/issue-sync.md` 中定义的 summary 评论标记对应的 summary 评论
170
170
  - 读取 `.agents/rules/issue-fields.md`,按流程 A 把 `task.md` 中所有非空的 Issue 字段(`priority`/`effort`/`start_date`/`target_date`)同步到 Issue(幂等;`has_push=false` 或取数/写入失败时跳过,不阻断)
171
171
 
@@ -35,6 +35,7 @@
35
35
  "verify_issue_type": true,
36
36
  "verify_issue_fields": false,
37
37
  "verify_milestone": true,
38
+ "verify_closed_issue_has_no_status_labels": true,
38
39
  "expected_comment_marker_key": "summary"
39
40
  },
40
41
  "artifact": {
@@ -35,6 +35,7 @@
35
35
  "verify_issue_type": true,
36
36
  "verify_issue_fields": false,
37
37
  "verify_milestone": true,
38
+ "verify_closed_issue_has_no_status_labels": true,
38
39
  "expected_comment_marker_key": "summary"
39
40
  },
40
41
  "artifact": {
@@ -40,7 +40,7 @@ The script and `.agents/rules/label-milestone-setup.md` are responsible for:
40
40
  Create the following milestones with fixed descriptions:
41
41
  - `General Backlog`: `All unsorted backlogged tasks may be completed in a future version.` (state=`open`)
42
42
  - `{major}.{minor}.x`: `Issues that we want to resolve in {major}.{minor} line.` (state=`open`)
43
- - `{major}.{minor}.{patch+1}`: `Issues that we want to release in v{major}.{minor}.{patch+1}.` (state=`open`)
43
+ - Release version: use compatibility baseline `0.1.0` for the default source, or `{major}.{minor}.{patch+1}` for a valid tag source. The description is `Issues that we want to release in v{version}.` (state=`open`)
44
44
 
45
45
  When `--history` is present, each historical `vX.Y.Z` tag additionally contributes:
46
46
  - `X.Y.x` as an open line milestone
@@ -59,7 +59,7 @@ The summary must include:
59
59
  Operational notes:
60
60
  - Milestone titles are treated as the idempotency key.
61
61
  - General Backlog is the fallback milestone for unsorted work.
62
- - Without `--history`, version milestones are created only for the next patch release.
62
+ - Without `--history`, create one standard release milestone: compatibility baseline `0.1.0` for the default source, or the next patch version for a valid tag source.
63
63
  - Historical `X.Y.Z` tags create `X.Y.x` milestones as open and `X.Y.Z` milestones as closed.
64
64
  - Repositories with many tags may hit the platform API rate limit.
65
65
 
@@ -40,7 +40,7 @@ bash .agents/skills/init-milestones/scripts/init-milestones.sh "$ARGUMENTS"
40
40
  按固定描述创建以下里程碑:
41
41
  - `General Backlog`:`All unsorted backlogged tasks may be completed in a future version.`(state=`open`)
42
42
  - `{major}.{minor}.x`:`Issues that we want to resolve in {major}.{minor} line.`(state=`open`)
43
- - `{major}.{minor}.{patch+1}`:`Issues that we want to release in v{major}.{minor}.{patch+1}.`(state=`open`)
43
+ - 具体版本:兼容默认来源使用基线 `0.1.0`;合法 tag 来源使用 `{major}.{minor}.{patch+1}`。描述为 `Issues that we want to release in v{version}.`(state=`open`)
44
44
 
45
45
  当传入 `--history` 时,每个历史 `vX.Y.Z` tag 还会额外贡献:
46
46
  - `X.Y.x` 作为开启状态的线里程碑
@@ -59,7 +59,7 @@ bash .agents/skills/init-milestones/scripts/init-milestones.sh "$ARGUMENTS"
59
59
  执行说明:
60
60
  - Milestone titles are treated as the idempotency key.
61
61
  - General Backlog 是未分类工作的兜底里程碑。
62
- - 不带 `--history` 时,只为下一次 patch 发布创建版本里程碑。
62
+ - 不带 `--history` 时,只创建一个标准版本里程碑:兼容默认来源使用基线 `0.1.0`,合法 tag 来源使用下一次 patch 版本。
63
63
  - 历史 `X.Y.Z` tag 会生成开启状态的 `X.Y.x` 和关闭状态的 `X.Y.Z`。
64
64
  - 标签较多的仓库可能触发平台 API rate limit。
65
65
 
@@ -189,17 +189,18 @@ IFS="$previous_ifs"
189
189
  current_version="$major.$minor.$patch"
190
190
  if [ "$selected_tag" = "-" ]; then
191
191
  version_source="compatibility default"
192
+ version_milestone="$current_version"
192
193
  else
193
194
  version_source="git tag $selected_tag"
195
+ version_milestone="$major.$minor.$next_patch"
194
196
  fi
195
197
 
196
198
  line_milestone="$major.$minor.x"
197
- next_version="$major.$minor.$next_patch"
198
199
 
199
200
  echo "Detected version baseline: $current_version"
200
201
  echo "Version baseline source: $version_source"
201
202
  echo "Line milestone: $line_milestone"
202
- echo "Next version milestone: $next_version"
203
+ echo "Next version milestone: $version_milestone"
203
204
 
204
205
  repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')"
205
206
 
@@ -213,7 +214,7 @@ cat "$tmpdir/existing.tsv"
213
214
  cat <<EOF > "$tmpdir/desired.tsv"
214
215
  General Backlog All unsorted backlogged tasks may be completed in a future version. open
215
216
  $line_milestone Issues that we want to resolve in $major.$minor line. open
216
- $next_version Issues that we want to release in v$next_version. open
217
+ $version_milestone Issues that we want to release in v$version_milestone. open
217
218
  EOF
218
219
 
219
220
  if [ "$history_mode" = "true" ]; then
@@ -285,7 +286,7 @@ echo
285
286
  echo "Notes:"
286
287
  echo "- Milestone titles are treated as the idempotency key."
287
288
  echo "- General Backlog is the fallback milestone for unsorted work."
288
- echo "- Without --history, version milestones are created only for the next patch release."
289
+ echo "- Without --history, a single version milestone is created based on the detected baseline."
289
290
 
290
291
  if [ "$history_mode" = "true" ]; then
291
292
  echo "- Historical X.Y.Z tags create X.Y.x milestones as open and X.Y.Z milestones as closed."
@@ -77,7 +77,7 @@ Follow `.agents/workflows/feature-development.yaml` and inspect the full change
77
77
  - Capture `R=$(git rev-parse HEAD)` exactly once; reuse that R for this round's report, fingerprint, and task review fact, and do not re-read HEAD later as a substitute
78
78
  - `git diff --binary "$R" -- <post-review-globs>` for tracked changes
79
79
  - `git ls-files -o --exclude-standard -z -- <post-review-globs>` for untracked new files
80
- - `node .agents/scripts/review-diff-fingerprint.js worktree "$R"` for the reviewed diff fingerprint; write it into the report
80
+ - `node .agents/scripts/review-diff-fingerprint.js worktree "$R" --format json` to generate the reviewed diff fingerprint `F` and reviewed snapshot tree `T` together; write both into the report
81
81
 
82
82
  > Detailed review criteria, severity rules, and reviewer expectations live in `reference/review-criteria.md`. Read `reference/review-criteria.md` before reviewing.
83
83
  > Test review gate: when `git diff` touches test files, read `.agents/rules/testing-discipline.md` first and check it item by item, especially "do not add negative assertions when a positive assertion already covers the behavior".
@@ -97,7 +97,7 @@ date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\([+-][0-9][0-9]\)\([0-9][0-9]\)$/\1:\2/'
97
97
  ```
98
98
 
99
99
  Update task.md:
100
- - When this round's `Overall Verdict` / `总体结论` is `Approved` / `通过`, write `last_reviewed_commit: {R}`; the presence of a worktree diff does not affect this write, and repeating the same R is idempotent
100
+ - When this round's `Overall Verdict` / `总体结论` is `Approved` / `通过` and `T == R^{tree}`, write `last_reviewed_commit: {R}`; when the Approved snapshot contains uncommitted changes, clear any existing `last_reviewed_commit` until `commit` anchors it
101
101
  - When this round is not Approved, preserve the existing `last_reviewed_commit`; do not advance or clear it
102
102
  - Append:
103
103
  `- {YYYY-MM-DD HH:mm:ss±HH:MM} — **Review Code (Round {N})** by {agent} — Verdict: {Approved/Changes Requested/Rejected}, blockers: {n}, major: {n}, minor: {n}, Manual-validation: {n} → {artifact-filename}`