@fitlab-ai/agent-infra 0.9.10 → 0.9.11

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 (269) hide show
  1. package/README.md +13 -3
  2. package/README.zh-CN.md +13 -3
  3. package/bin/cli.ts +17 -4
  4. package/dist/bin/cli.js +19 -4
  5. package/dist/bin/internal-cli.js +50 -4
  6. package/dist/lib/agent-client.js +0 -1
  7. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +0 -4
  8. package/dist/lib/agent-clients/adapters/traecli.js +7 -8
  9. package/dist/lib/agent-clients/config.js +14 -99
  10. package/dist/lib/agent-clients/reconcile.js +1 -11
  11. package/dist/lib/agent-clients/schema.js +5 -4
  12. package/dist/lib/agent-clients/tokens.js +5 -15
  13. package/dist/lib/decide.js +14 -30
  14. package/dist/lib/defaults.json +4 -0
  15. package/dist/lib/init.js +2 -2
  16. package/dist/lib/internal/release-workflow.js +0 -1
  17. package/dist/lib/internal/task-finalization.js +19 -9
  18. package/dist/lib/internal/task-ledger.js +8 -2
  19. package/dist/lib/internal/task-lifecycle.js +17 -93
  20. package/dist/lib/internal/task-orchestration.js +30 -216
  21. package/dist/lib/internal/task-validate.js +2 -1
  22. package/dist/lib/merge.js +205 -103
  23. package/dist/lib/process-data/normalize.js +2 -9
  24. package/dist/lib/run/host.js +46 -16
  25. package/dist/lib/sandbox/agent-client-reconciler.js +4 -12
  26. package/dist/lib/sandbox/commands/create.js +3 -3
  27. package/dist/lib/sandbox/commands/enter.js +18 -4
  28. package/dist/lib/sandbox/config.js +3 -12
  29. package/dist/lib/sandbox/control/executor.js +131 -39
  30. package/dist/lib/sandbox/control/lifecycle.js +29 -20
  31. package/dist/lib/sandbox/control/server.js +18 -16
  32. package/dist/lib/sandbox/mounts.js +3 -2
  33. package/dist/lib/sandbox/recovery.js +123 -22
  34. package/dist/lib/sandbox/workspace-identity.js +14 -0
  35. package/dist/lib/sandbox/workspace-view.js +8 -9
  36. package/dist/lib/self-update.js +278 -0
  37. package/dist/lib/server/protocol.js +5 -5
  38. package/dist/lib/task/activity-log.js +4 -1
  39. package/dist/lib/task/artifact-lifecycle.js +4 -2
  40. package/dist/lib/task/codex-orchestration.js +1 -1
  41. package/dist/lib/task/command-options.js +1 -3
  42. package/dist/lib/task/commands/cat.js +0 -5
  43. package/dist/lib/task/commands/decisions.js +76 -92
  44. package/dist/lib/task/commands/files.js +6 -6
  45. package/dist/lib/task/commands/grep.js +25 -61
  46. package/dist/lib/task/commands/issue-body.js +6 -6
  47. package/dist/lib/task/commands/log.js +25 -10
  48. package/dist/lib/task/commands/show.js +5 -6
  49. package/dist/lib/task/commands/status.js +8 -8
  50. package/dist/lib/task/commit-operation.js +21 -25
  51. package/dist/lib/task/commit-policy.js +1 -1
  52. package/dist/lib/task/control-authority.js +431 -0
  53. package/dist/lib/task/create.js +6 -1
  54. package/dist/lib/task/current-contract.js +35 -0
  55. package/dist/lib/task/decision-details.js +263 -0
  56. package/dist/lib/task/decision-intents.js +5 -2
  57. package/dist/lib/task/events.js +11 -5
  58. package/dist/lib/task/index.js +19 -20
  59. package/dist/lib/task/ledger-intents.js +8 -9
  60. package/dist/lib/task/ledger.js +13 -6
  61. package/dist/lib/task/lifecycle.js +16 -0
  62. package/dist/lib/task/orchestration.js +50 -13
  63. package/dist/lib/task/review-artifacts.js +31 -12
  64. package/dist/lib/task/review-finalization.js +19 -6
  65. package/dist/lib/task/verification-engine.js +38 -15
  66. package/dist/lib/task/verification-review.js +1 -1
  67. package/dist/lib/task/workspace-snapshot.js +5 -11
  68. package/dist/lib/task/write.js +11 -1
  69. package/dist/lib/update.js +6 -37
  70. package/dist/lib/version.js +5 -1
  71. package/dist/lifecycle-build-manifest.json +3 -3
  72. package/lib/agent-client.ts +0 -1
  73. package/lib/agent-clients/adapters/opencode-sandbox.ts +0 -5
  74. package/lib/agent-clients/adapters/traecli.ts +7 -8
  75. package/lib/agent-clients/config.ts +14 -144
  76. package/lib/agent-clients/reconcile.ts +1 -12
  77. package/lib/agent-clients/schema.ts +5 -4
  78. package/lib/agent-clients/tokens.ts +5 -18
  79. package/lib/decide.ts +12 -22
  80. package/lib/defaults.json +4 -0
  81. package/lib/init.ts +2 -2
  82. package/lib/internal/release-workflow.ts +0 -1
  83. package/lib/internal/task-finalization.ts +27 -13
  84. package/lib/internal/task-ledger.ts +8 -2
  85. package/lib/internal/task-lifecycle.ts +33 -76
  86. package/lib/internal/task-orchestration.ts +33 -190
  87. package/lib/internal/task-validate.ts +3 -2
  88. package/lib/merge.ts +236 -120
  89. package/lib/process-data/normalize.ts +2 -8
  90. package/lib/run/host.ts +63 -14
  91. package/lib/sandbox/agent-client-reconciler.ts +5 -15
  92. package/lib/sandbox/commands/create.ts +4 -4
  93. package/lib/sandbox/commands/enter.ts +24 -4
  94. package/lib/sandbox/config.ts +2 -14
  95. package/lib/sandbox/control/container-identity.ts +2 -2
  96. package/lib/sandbox/control/executor.ts +148 -51
  97. package/lib/sandbox/control/lifecycle.ts +34 -28
  98. package/lib/sandbox/control/protocol.ts +1 -3
  99. package/lib/sandbox/control/server.ts +15 -12
  100. package/lib/sandbox/control/state.ts +7 -7
  101. package/lib/sandbox/mounts.ts +4 -3
  102. package/lib/sandbox/recovery.ts +162 -34
  103. package/lib/sandbox/tools.ts +1 -1
  104. package/lib/sandbox/workspace-identity.ts +28 -0
  105. package/lib/sandbox/workspace-view.ts +14 -10
  106. package/lib/self-update.ts +341 -0
  107. package/lib/server/protocol.ts +5 -5
  108. package/lib/task/activity-log.ts +6 -1
  109. package/lib/task/artifact-lifecycle.ts +5 -3
  110. package/lib/task/codex-orchestration.ts +2 -1
  111. package/lib/task/command-options.ts +1 -2
  112. package/lib/task/commands/cat.ts +0 -4
  113. package/lib/task/commands/decisions.ts +80 -75
  114. package/lib/task/commands/files.ts +6 -6
  115. package/lib/task/commands/grep.ts +23 -51
  116. package/lib/task/commands/issue-body.ts +6 -6
  117. package/lib/task/commands/log.ts +22 -10
  118. package/lib/task/commands/show.ts +5 -6
  119. package/lib/task/commands/status.ts +8 -8
  120. package/lib/task/commit-operation.ts +25 -30
  121. package/lib/task/commit-policy.ts +1 -2
  122. package/lib/task/control-authority.ts +563 -0
  123. package/lib/task/create.ts +6 -1
  124. package/lib/task/current-contract.ts +49 -0
  125. package/lib/task/decision-details.ts +340 -0
  126. package/lib/task/decision-intents.ts +7 -2
  127. package/lib/task/events.ts +10 -5
  128. package/lib/task/index.ts +19 -20
  129. package/lib/task/ledger-intents.ts +6 -8
  130. package/lib/task/ledger.ts +17 -7
  131. package/lib/task/lifecycle.ts +20 -0
  132. package/lib/task/orchestration.ts +40 -15
  133. package/lib/task/review-artifacts.ts +53 -15
  134. package/lib/task/review-finalization.ts +33 -7
  135. package/lib/task/verification-engine.ts +33 -18
  136. package/lib/task/verification-review.ts +1 -1
  137. package/lib/task/workspace-snapshot.ts +4 -13
  138. package/lib/task/write.ts +20 -1
  139. package/lib/update.ts +6 -40
  140. package/lib/version.ts +6 -1
  141. package/package.json +1 -1
  142. package/templates/.agents/README.en.md +5 -3
  143. package/templates/.agents/README.zh-CN.md +5 -3
  144. package/templates/.agents/rules/compatibility-policy.en.md +5 -4
  145. package/templates/.agents/rules/compatibility-policy.zh-CN.md +5 -4
  146. package/templates/.agents/rules/create-issue.github.en.md +1 -1
  147. package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
  148. package/templates/.agents/rules/human-decision-context.en.md +9 -7
  149. package/templates/.agents/rules/human-decision-context.zh-CN.md +9 -7
  150. package/templates/.agents/rules/issue-fields.github.en.md +1 -1
  151. package/templates/.agents/rules/issue-fields.github.zh-CN.md +1 -1
  152. package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
  153. package/templates/.agents/rules/issue-pr-commands.github.en.md +1 -1
  154. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +1 -1
  155. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
  156. package/templates/.agents/rules/issue-sync.github.en.md +1 -1
  157. package/templates/.agents/rules/issue-sync.github.zh-CN.md +2 -2
  158. package/templates/.agents/rules/milestone-inference.github.en.md +1 -1
  159. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +1 -1
  160. package/templates/.agents/rules/next-step-output.en.md +3 -3
  161. package/templates/.agents/rules/next-step-output.zh-CN.md +3 -3
  162. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -1
  163. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -1
  164. package/templates/.agents/rules/pr-sync.en.md +1 -1
  165. package/templates/.agents/rules/pr-sync.github.en.md +1 -1
  166. package/templates/.agents/rules/pr-sync.github.zh-CN.md +1 -1
  167. package/templates/.agents/rules/pr-sync.zh-CN.md +1 -1
  168. package/templates/.agents/rules/release-commands.github.en.md +6 -0
  169. package/templates/.agents/rules/release-commands.github.zh-CN.md +6 -0
  170. package/templates/.agents/rules/review-handshake.en.md +1 -1
  171. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  172. package/templates/.agents/rules/task-management.en.md +1 -2
  173. package/templates/.agents/rules/task-management.zh-CN.md +1 -2
  174. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  175. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  176. package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
  177. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  178. package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -2
  179. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
  180. package/templates/.agents/skills/check-task/SKILL.en.md +2 -2
  181. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +2 -2
  182. package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
  183. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +1 -1
  184. package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
  185. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +1 -1
  186. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  187. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  188. package/templates/.agents/skills/commit/SKILL.en.md +27 -7
  189. package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -4
  190. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -5
  191. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -5
  192. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  193. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  194. package/templates/.agents/skills/create-pr/SKILL.en.md +5 -7
  195. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +5 -7
  196. package/templates/.agents/skills/create-pr/reference/change-report.en.md +9 -4
  197. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +9 -4
  198. package/templates/.agents/skills/create-pr/scripts/change-report.mjs +203 -0
  199. package/templates/.agents/skills/create-release-note/SKILL.en.md +5 -6
  200. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +5 -6
  201. package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
  202. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  203. package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
  204. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  205. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  206. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  207. package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
  208. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
  209. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -3
  210. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -3
  211. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -0
  212. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -0
  213. package/templates/.agents/skills/review-code/SKILL.en.md +5 -3
  214. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -3
  215. package/templates/.agents/skills/review-code/config/verify.en.json +1 -0
  216. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -0
  217. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +2 -1
  218. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +2 -1
  219. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -3
  220. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -3
  221. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -0
  222. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -0
  223. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -0
  224. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -0
  225. package/templates/.agents/skills/run-task/SKILL.en.md +4 -0
  226. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +4 -0
  227. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +2 -2
  228. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +2 -2
  229. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +140 -109
  230. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  231. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  232. package/templates/.agents/templates/task.en.md +1 -1
  233. package/templates/.agents/templates/task.zh-CN.md +1 -1
  234. package/templates/.claude/commands/analyze-task.en.md +1 -1
  235. package/templates/.claude/commands/analyze-task.zh-CN.md +1 -1
  236. package/templates/.claude/commands/block-task.en.md +1 -1
  237. package/templates/.claude/commands/block-task.zh-CN.md +1 -1
  238. package/templates/.claude/commands/cancel-task.en.md +1 -1
  239. package/templates/.claude/commands/cancel-task.zh-CN.md +1 -1
  240. package/templates/.claude/commands/check-task.en.md +1 -1
  241. package/templates/.claude/commands/check-task.zh-CN.md +1 -1
  242. package/templates/.claude/commands/code-task.en.md +1 -1
  243. package/templates/.claude/commands/code-task.zh-CN.md +1 -1
  244. package/templates/.claude/commands/commit.en.md +1 -1
  245. package/templates/.claude/commands/commit.zh-CN.md +1 -1
  246. package/templates/.claude/commands/complete-manual-validation.en.md +1 -1
  247. package/templates/.claude/commands/complete-manual-validation.zh-CN.md +1 -1
  248. package/templates/.claude/commands/complete-task.en.md +1 -1
  249. package/templates/.claude/commands/complete-task.zh-CN.md +1 -1
  250. package/templates/.claude/commands/create-pr.en.md +1 -1
  251. package/templates/.claude/commands/create-pr.zh-CN.md +1 -1
  252. package/templates/.claude/commands/plan-task.en.md +1 -1
  253. package/templates/.claude/commands/plan-task.zh-CN.md +1 -1
  254. package/templates/.claude/commands/review-analysis.en.md +1 -1
  255. package/templates/.claude/commands/review-analysis.zh-CN.md +1 -1
  256. package/templates/.claude/commands/review-code.en.md +1 -1
  257. package/templates/.claude/commands/review-code.zh-CN.md +1 -1
  258. package/templates/.claude/commands/review-plan.en.md +1 -1
  259. package/templates/.claude/commands/review-plan.zh-CN.md +1 -1
  260. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  261. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  262. package/templates/.claude/commands/run-task.en.md +1 -1
  263. package/templates/.claude/commands/run-task.zh-CN.md +1 -1
  264. package/templates/.claude/commands/watch-pr.en.md +1 -1
  265. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  266. package/templates/{.trae/skills → .traecli/commands}/update-agent-infra.en.md +0 -1
  267. package/templates/{.trae/skills → .traecli/commands}/update-agent-infra.zh-CN.md +0 -1
  268. package/dist/lib/builtin-tuis.js +0 -40
  269. package/lib/builtin-tuis.ts +0 -56
@@ -0,0 +1,263 @@
1
+ const DECISION_ID = /^(AN|PL|CD|HD)-[1-9]\d*(?=$|[\s::])/i;
2
+ const HEADING = /^ {0,3}(#{1,6})[ \t]+(.+?)\s*$/;
3
+ const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})(.*)$/;
4
+ const FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})\s*$/;
5
+ const ANCHOR = /^\s*<a\s+id=(['"])([^'"]+)\1\s*><\/a>\s*$/;
6
+ const FIELD = /^\s*[-*]\s+\*\*([^*]+)\*\*\s*[::]\s*/;
7
+ const OPTION_HEADING = /^(?:方案|Option)\s+[A-Z](?:\b|\s*[::])/i;
8
+ function sourceLines(content) {
9
+ const lines = [];
10
+ let start = 0;
11
+ while (start < content.length) {
12
+ const newline = content.indexOf('\n', start);
13
+ const end = newline === -1 ? content.length : newline;
14
+ lines.push({ start, end, text: content.slice(start, end).replace(/\r$/, '') });
15
+ start = newline === -1 ? content.length : newline + 1;
16
+ }
17
+ return lines;
18
+ }
19
+ function scanVisibleMarkdown(content) {
20
+ const lines = sourceLines(content);
21
+ const visible = [];
22
+ const headings = [];
23
+ const anchors = [];
24
+ let fence = null;
25
+ for (const line of lines) {
26
+ if (fence) {
27
+ const close = line.text.match(FENCE_CLOSE);
28
+ if (close && close[1][0] === fence.character && close[1].length >= fence.length)
29
+ fence = null;
30
+ continue;
31
+ }
32
+ const open = line.text.match(FENCE_OPEN);
33
+ const marker = open?.[1];
34
+ const info = open?.[2] ?? '';
35
+ if (marker && !(marker[0] === '`' && info.includes('`'))) {
36
+ fence = { character: marker[0], length: marker.length };
37
+ continue;
38
+ }
39
+ visible.push(line);
40
+ const heading = line.text.match(HEADING);
41
+ if (heading)
42
+ headings.push({ ...line, level: heading[1].length, text: heading[2].trim() });
43
+ const anchor = line.text.match(ANCHOR);
44
+ if (anchor)
45
+ anchors.push({ ...line, id: anchor[2] });
46
+ }
47
+ return { lines: visible, headings, anchors };
48
+ }
49
+ function parseDecisionDetailBlocks(content) {
50
+ const scanned = scanVisibleMarkdown(content);
51
+ const details = scanned.headings.filter((heading) => heading.level === 3 && DECISION_ID.test(heading.text));
52
+ return details.map((heading) => {
53
+ const next = scanned.headings.find((candidate) => (candidate.start > heading.start && candidate.level <= 3));
54
+ const end = next?.start ?? content.length;
55
+ return {
56
+ id: heading.text.match(DECISION_ID)[0],
57
+ heading: heading.text,
58
+ canonical: /\[needs-human-decision\]/i.test(heading.text),
59
+ start: heading.start,
60
+ end,
61
+ content: content.slice(heading.start, end).trim()
62
+ };
63
+ });
64
+ }
65
+ function parseDecisionEvidence(evidence) {
66
+ const separator = evidence.indexOf('#');
67
+ if (separator <= 0 || evidence.indexOf('#', separator + 1) !== -1)
68
+ return { ok: false };
69
+ const artifact = evidence.slice(0, separator);
70
+ const anchor = evidence.slice(separator + 1);
71
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*\.md$/.test(artifact))
72
+ return { ok: false };
73
+ if (!anchor || /[\s\u0000-\u001f#]/.test(anchor))
74
+ return { ok: false };
75
+ return { ok: true, artifact, anchor };
76
+ }
77
+ function sameId(left, right) {
78
+ return left.toUpperCase() === right.toUpperCase();
79
+ }
80
+ function resolveDecisionDetail(content, rowId, anchor) {
81
+ const scanned = scanVisibleMarkdown(content);
82
+ const blocks = parseDecisionDetailBlocks(content);
83
+ const explicit = scanned.anchors.filter((candidate) => candidate.id === anchor);
84
+ if (explicit.length > 1) {
85
+ return { status: 'ambiguous', reason: `evidence anchor '${anchor}' occurs more than once`, candidates: [] };
86
+ }
87
+ if (explicit.length === 1) {
88
+ const marker = explicit[0];
89
+ const followingLine = scanned.lines.find((line) => line.start > marker.end && line.text.trim());
90
+ const followingHeading = followingLine
91
+ ? scanned.headings.find((heading) => heading.start === followingLine.start)
92
+ : undefined;
93
+ const candidate = followingHeading?.level === 3
94
+ ? blocks.find((block) => block.start === followingHeading.start)
95
+ : undefined;
96
+ if (!candidate || !sameId(candidate.id, rowId) || !candidate.canonical) {
97
+ return { status: 'missing', reason: `evidence anchor '${anchor}' does not identify a canonical ${rowId} detail` };
98
+ }
99
+ return { status: 'found', block: candidate };
100
+ }
101
+ if (!sameId(anchor, rowId)) {
102
+ return { status: 'missing', reason: `evidence anchor '${anchor}' is not the ledger id '${rowId}'` };
103
+ }
104
+ const matches = blocks.filter((block) => sameId(block.id, rowId) && block.canonical);
105
+ if (matches.length === 0)
106
+ return { status: 'missing', reason: `no canonical ${rowId} detail was found` };
107
+ if (matches.length > 1) {
108
+ return { status: 'ambiguous', reason: `more than one canonical ${rowId} detail was found`, candidates: matches };
109
+ }
110
+ return { status: 'found', block: matches[0] };
111
+ }
112
+ const INFORMAL_SHORT_HEADING = /^(?:AN|PL|CD|HD)-[1-9]\d*[::]\s*(?:上一轮)?简短复核$/i;
113
+ const INFORMAL_SHORT_FRAGMENT = /^简短(?:结论|说明)$/;
114
+ function isInformalShortBlock(block) {
115
+ if (block.canonical || block.content.length > 600)
116
+ return false;
117
+ if (!INFORMAL_SHORT_HEADING.test(block.heading))
118
+ return false;
119
+ const scanned = scanVisibleMarkdown(block.content);
120
+ const bodyLines = scanned.lines
121
+ .filter((line) => !scanned.headings.some((heading) => heading.start === line.start) && line.text.trim())
122
+ .map((line) => line.text);
123
+ if (bodyLines.length === 0 || bodyLines.length > 2)
124
+ return false;
125
+ const fragments = bodyLines.map((line) => line.match(/^\s*[-*]\s+([^\s].*?)\s*$/)?.[1] ?? null);
126
+ if (fragments.some((fragment) => fragment === null))
127
+ return false;
128
+ return fragments.every((fragment) => INFORMAL_SHORT_FRAGMENT.test(fragment));
129
+ }
130
+ function hasDirectPrecedingAnchor(block, scanned) {
131
+ return scanned.anchors.some((anchor) => (anchor.end < block.start
132
+ && !scanned.lines.some((line) => line.start > anchor.end && line.start < block.start && line.text.trim())));
133
+ }
134
+ function inspectDecisionDetailDuplicates(content) {
135
+ const blocks = parseDecisionDetailBlocks(content);
136
+ const scanned = scanVisibleMarkdown(content);
137
+ const grouped = new Map();
138
+ for (const block of blocks) {
139
+ const key = block.id.toUpperCase();
140
+ const group = grouped.get(key) ?? [];
141
+ group.push(block);
142
+ grouped.set(key, group);
143
+ }
144
+ const duplicates = [];
145
+ for (const group of grouped.values()) {
146
+ if (group.length < 2)
147
+ continue;
148
+ const canonical = group.filter((block) => block.canonical);
149
+ const repairable = canonical.length === 1 && group.every((block) => (block.canonical || (!hasDirectPrecedingAnchor(block, scanned) && isInformalShortBlock(block))));
150
+ duplicates.push({ id: canonical[0]?.id ?? group[0].id, blocks: group, repairable });
151
+ }
152
+ if (duplicates.length === 0)
153
+ return { ok: true, blocks };
154
+ const summary = duplicates.map((duplicate) => {
155
+ const types = duplicate.blocks.map((block) => block.canonical ? 'canonical' : 'informal').join(', ');
156
+ return `${duplicate.id} (${duplicate.blocks.length} blocks: ${types}${duplicate.repairable ? '; safe repair available' : ''})`;
157
+ }).join(', ');
158
+ return {
159
+ ok: false,
160
+ code: 'DECISION_DETAIL_DUPLICATE',
161
+ message: `Duplicate decision detail ids: ${summary}`,
162
+ blocks,
163
+ duplicates
164
+ };
165
+ }
166
+ function repairDecisionDetailDuplicates(content) {
167
+ const inspection = inspectDecisionDetailDuplicates(content);
168
+ if (inspection.ok)
169
+ return { ok: true, changed: false, content };
170
+ const unsafe = inspection.duplicates.filter((duplicate) => !duplicate.repairable);
171
+ if (unsafe.length > 0) {
172
+ return {
173
+ ok: false,
174
+ code: 'DECISION_DETAIL_AMBIGUOUS',
175
+ message: `Cannot safely repair duplicate decision detail ids: ${unsafe.map((duplicate) => duplicate.id).join(', ')}`,
176
+ content
177
+ };
178
+ }
179
+ const removals = inspection.duplicates.flatMap((duplicate) => duplicate.blocks.filter((block) => !block.canonical));
180
+ let repaired = content;
181
+ for (const block of removals.sort((left, right) => right.start - left.start)) {
182
+ repaired = repaired.slice(0, block.start) + repaired.slice(block.end);
183
+ }
184
+ return { ok: true, changed: repaired !== content, content: repaired };
185
+ }
186
+ const FIELD_KEYS = {
187
+ '为什么现在要决定': 'why', 'why decide now': 'why', 'why a decision is needed now': 'why',
188
+ '要决定什么': 'decision', 'what to decide': 'decision', 'what needs a decision': 'decision',
189
+ '会影响什么': 'impact', 'what it affects': 'impact', 'what this affects': 'impact',
190
+ '如果选错或暂不处理': 'risk', 'if wrong or postponed': 'risk',
191
+ 'what could go wrong': 'risk',
192
+ '打个比方': 'analogy', '例子': 'analogy', 'example': 'analogy',
193
+ '建议选哪个': 'recommend', 'recommended option': 'recommend', 'recommended choice': 'recommend',
194
+ '为什么这样建议': 'reason', 'why this recommendation': 'reason', 'why this choice': 'reason',
195
+ '是否需要实现': 'implementation', 'implementation required': 'implementation'
196
+ };
197
+ const TERMINAL_FIELD_KEYS = new Set(['recommend', 'reason', 'implementation']);
198
+ function normalizeDecisionDetailForDisplay(block) {
199
+ const body = block.content;
200
+ const scanned = scanVisibleMarkdown(body);
201
+ const heading = scanned.headings.find((candidate) => candidate.level === 3);
202
+ if (!heading)
203
+ return body.trim();
204
+ const options = scanned.headings.filter((candidate) => candidate.level === 4 && OPTION_HEADING.test(candidate.text));
205
+ const optionRanges = options.map((option, index) => ({
206
+ start: option.start,
207
+ end: options[index + 1]?.start
208
+ ?? scanned.headings.find((candidate) => candidate.start > option.start && candidate.level <= 3)?.start
209
+ ?? scanned.lines.find((line) => (line.start > option.start
210
+ && FIELD.test(line.text)
211
+ && TERMINAL_FIELD_KEYS.has(FIELD_KEYS[line.text.match(FIELD)[1].trim().toLowerCase()])))?.start
212
+ ?? body.length
213
+ }));
214
+ const topLines = scanned.lines.filter((line) => (line.start > heading.end && !optionRanges.some((range) => line.start >= range.start && line.start < range.end)));
215
+ const fields = [];
216
+ for (const line of topLines) {
217
+ const match = line.text.match(FIELD);
218
+ if (!match)
219
+ continue;
220
+ const key = FIELD_KEYS[match[1].trim().toLowerCase()];
221
+ fields.push({ key: key ?? null, line });
222
+ }
223
+ if (fields.some((field) => field.key === null))
224
+ return body.trim();
225
+ const byKey = new Map();
226
+ const selectedRanges = [];
227
+ for (let index = 0; index < fields.length; index += 1) {
228
+ const field = fields[index];
229
+ if (!field.key)
230
+ continue;
231
+ const nextOption = optionRanges.find((range) => range.start > field.line.start)?.start ?? body.length;
232
+ const end = Math.min(fields[index + 1]?.line.start ?? body.length, nextOption);
233
+ const value = body.slice(field.line.start, end).trim();
234
+ byKey.set(field.key, value);
235
+ selectedRanges.push({ start: field.line.start, end });
236
+ }
237
+ selectedRanges.push(...optionRanges);
238
+ const required = ['decision', 'why', 'impact', 'recommend', 'reason'];
239
+ if (required.some((key) => !byKey.has(key)) || options.length < 2)
240
+ return body.trim();
241
+ const sortedRanges = selectedRanges.sort((left, right) => left.start - right.start);
242
+ let cursor = heading.end;
243
+ for (const range of sortedRanges) {
244
+ if (/\S/.test(body.slice(cursor, range.start)))
245
+ return body.trim();
246
+ cursor = Math.max(cursor, range.end);
247
+ }
248
+ if (/\S/.test(body.slice(cursor)))
249
+ return body.trim();
250
+ const ordered = [
251
+ byKey.get('decision'), byKey.get('why'), byKey.get('impact')
252
+ ];
253
+ for (const optional of ['risk', 'analogy'])
254
+ if (byKey.has(optional))
255
+ ordered.push(byKey.get(optional));
256
+ ordered.push(...optionRanges.map((range) => body.slice(range.start, range.end).trim()));
257
+ ordered.push(byKey.get('recommend'), byKey.get('reason'));
258
+ if (byKey.has('implementation'))
259
+ ordered.push(byKey.get('implementation'));
260
+ return [body.slice(heading.start, heading.end).trim(), ...ordered].join('\n\n').trim();
261
+ }
262
+ export { inspectDecisionDetailDuplicates, normalizeDecisionDetailForDisplay, parseDecisionDetailBlocks, parseDecisionEvidence, repairDecisionDetailDuplicates, resolveDecisionDetail };
263
+ //# sourceMappingURL=decision-details.js.map
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import { appendActivityEntry, locateActivityLog } from './activity-log.js';
3
3
  import { isDecisionItem, listDecisionItems, selectDecisionItem } from './decision-items.js';
4
4
  import { createImplementationInput, finalizeImplementationInput, IMPLEMENTATION_INPUT_ALIASES, parseImplementationInputs, renderImplementationInputs, selectDeclaredImplementationInput } from './implementation-inputs.js';
5
- import { LEDGER_COLUMNS, LEDGER_HEADINGS, parseLedger } from './ledger.js';
5
+ import { LEDGER_COLUMNS, LEDGER_HEADINGS, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument } from './ledger.js';
6
6
  import { resolveTaskContext } from './resolve-ref.js';
7
7
  import { extractSection, extractSubSection, findSectionHeading } from './sections.js';
8
8
  import { captureTaskWriteMetadata, writeTask } from './write.js';
@@ -64,7 +64,10 @@ function applyHumanDecision(request, options = {}) {
64
64
  let rows;
65
65
  try {
66
66
  content = fs.readFileSync(resolved.taskMdPath, 'utf8');
67
- rows = parseLedger(content);
67
+ const ledger = parseLedgerDocument(content);
68
+ if (!ledger.present)
69
+ return failed(LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, resolved.taskId);
70
+ rows = ledger.rows;
68
71
  }
69
72
  catch (error) {
70
73
  return failed('DECISION_DOCUMENT_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
@@ -1,11 +1,11 @@
1
1
  import fs from 'node:fs';
2
- import { appendActivityEntry, locateActivityLog, pairEntries } from './activity-log.js';
2
+ import { appendActivityEntry, locateActivityLog, pairEntries, startedBackedRows } from './activity-log.js';
3
3
  import { buildArtifactLinkSection, artifactName, inspectArtifactDirectory, parseReviewedInputReference, parseCodePlanInputReference, parseArtifactName, resolveArtifactContext, validateCompletedArtifact } from './artifact-lifecycle.js';
4
4
  import { ArtifactReceiptError, sha256File, upsertArtifactReceipt } from './artifact-receipts.js';
5
5
  import { parseTypedTaskFrontmatter } from './frontmatter.js';
6
6
  import { consumeImplementationInput, IMPLEMENTATION_INPUT_ALIASES, parseImplementationInputs, renderImplementationInputs } from './implementation-inputs.js';
7
- import { parseLedger, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
8
- import { parseReviewSummary } from './review-artifacts.js';
7
+ import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
8
+ import { parseReviewSummary, resolveCanonicalVerdict } from './review-artifacts.js';
9
9
  import { resolveTaskRef } from './resolve-ref.js';
10
10
  import { findSectionHeading } from './sections.js';
11
11
  import { validateLifecycleExecution } from './lifecycle-execution.js';
@@ -101,7 +101,10 @@ function validateReviewFindingCounts(request, content, family, artifactPath) {
101
101
  const stage = REVIEW_LEDGER_STAGES[family];
102
102
  if (!stage || !artifactPath)
103
103
  return null;
104
- const rows = parseLedger(content);
104
+ const ledger = parseLedgerDocument(content);
105
+ if (!ledger.present)
106
+ return { code: 'EVENT_FINDING_COUNT_MISMATCH', message: `${LEDGER_SECTION_MISSING_CODE}: ${LEDGER_SECTION_MISSING_MESSAGE}` };
107
+ const rows = ledger.rows;
105
108
  const invalid = validateLedgerRows(rows);
106
109
  if (invalid)
107
110
  return { code: 'TASK_DOCUMENT_INVALID', message: `${invalid.code}: ${invalid.message}` };
@@ -125,6 +128,9 @@ function validateReviewFindingCounts(request, content, family, artifactPath) {
125
128
  message: parsed.ok ? 'review summary finding counts are not finalized' : parsed.message
126
129
  };
127
130
  }
131
+ const canonical = resolveCanonicalVerdict(parsed.summary);
132
+ if (!canonical.ok)
133
+ return { code: 'EVENT_VERDICT_INVALID', message: `${canonical.code}: ${canonical.message}` };
128
134
  const report = parsed.summary.counts;
129
135
  const reportVerdict = parsed.summary.verdict === 'Approved'
130
136
  ? 'approved'
@@ -352,7 +358,7 @@ function applyTaskEventUnlocked(request, options = {}) {
352
358
  const section = locateActivityLog(content);
353
359
  if (!section)
354
360
  return failed(request, { code: 'EVENT_LOG_MISSING', message: 'task has no unique Activity Log section' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
355
- const rows = pairEntries(section.entries);
361
+ const rows = startedBackedRows(pairEntries(section.entries));
356
362
  let normalized = request;
357
363
  let artifactContext = null;
358
364
  const initialParts = eventParts(request.event);
@@ -1,34 +1,33 @@
1
1
  const USAGE = `Usage: ai task <command> [options]
2
2
 
3
3
  Commands:
4
- cat [--task <ref>] <artifact | N> Print a task artifact (by name or number)
5
- decisions, d [--task <ref>] [-i item] List review decisions, or show one item's detail
6
- files [--task <ref>] List artifacts in a task dir (numbered)
7
- grep <pattern> [--current|--task <ref>] Literal search across task artifacts (global by default)
8
- issue-body [--task <ref>] [--template] Print a deterministic Issue body from task.md
9
- log [--task <ref>] Render a task's activity log as a timeline
4
+ cat [--task <ref> | -t <ref>] <artifact | N> Print a task artifact (by name or number)
5
+ decisions, d [--task <ref> | -t <ref>] [--item <selector> | -i <selector>] List review decisions, or show one item's detail
6
+ files [--task <ref> | -t <ref>] List artifacts in a task dir (numbered)
7
+ grep [--current | --task <ref> | -t <ref>] <pattern> Literal search in the current or selected task
8
+ issue-body [--task <ref> | -t <ref>] [--template <path>] Print a deterministic Issue body from task.md
9
+ log [--task <ref> | -t <ref>] Render a task's activity log as a timeline
10
10
  ls [--all | --blocked | --completed] List tasks (default: active)
11
- show [--task <ref>] Print a task.md
12
- status [--task <ref>] Aggregated status view (metadata / artifacts / git / platform)
11
+ show [--task <ref> | -t <ref>] Print a task.md
12
+ status [--task <ref> | -t <ref>] Aggregated status view (metadata / artifacts / git / platform)
13
13
 
14
14
  Examples:
15
- ai task cat 11 analysis
16
15
  ai task cat analysis
17
- ai task cat 11 3
18
- ai task decisions 11
16
+ ai task cat --task 11 analysis
17
+ ai task decisions --task 11
19
18
  ai task decisions --item 1
20
- ai task d 11 PL-3 --format markdown
21
- ai task files 11
19
+ ai task d --task 11 --item PL-3 --format markdown
20
+ ai task files --task 11
22
21
  ai task grep resolveArtifact
23
22
  ai task grep resolveArtifact --current
24
- ai task grep resolveArtifact 11
25
- ai task issue-body 11
26
- ai task issue-body 11 --template .github/ISSUE_TEMPLATE/05_other.yml
27
- ai task log 11
23
+ ai task grep resolveArtifact --task 11
24
+ ai task issue-body --task 11
25
+ ai task issue-body --task 11 --template .github/ISSUE_TEMPLATE/05_other.yml
26
+ ai task log --task 11
28
27
  ai task ls
29
- ai task show 11
30
- ai task show TASK-20260612-162737
31
- ai task status 11
28
+ ai task show --task 11
29
+ ai task show --task TASK-20260612-162737
30
+ ai task status --task 11
32
31
 
33
32
  Run 'ai task <command> --help' for details.`;
34
33
  export async function runTask(args) {
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { declareImplementationInput, IMPLEMENTATION_INPUT_ALIASES, parseImplementationInputs, renderImplementationInputs } from './implementation-inputs.js';
4
- import { LEDGER_COLUMNS, LEDGER_HEADINGS, nextHdId, parseLedger, validateLedgerRows } from './ledger.js';
4
+ import { LEDGER_COLUMNS, LEDGER_HEADINGS, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, nextHdId, parseLedgerDocument, validateLedgerRows } from './ledger.js';
5
5
  import { resolveTaskRef } from './resolve-ref.js';
6
6
  import { writeTask } from './write.js';
7
7
  import { allowsManualOverride } from './guard-override.js';
@@ -26,6 +26,8 @@ function normalizedEvidence(value) {
26
26
  }
27
27
  function ledgerReadErrorCode(error) {
28
28
  const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
29
+ if (code === LEDGER_SECTION_MISSING_CODE)
30
+ return code;
29
31
  return code === 'TABLE_DUPLICATE_KEY' ? 'LEDGER_DUPLICATE_ID' : 'LEDGER_DOCUMENT_INVALID';
30
32
  }
31
33
  function maxHandshakeRounds(repoRoot) {
@@ -60,13 +62,7 @@ function rowMutation(row) {
60
62
  };
61
63
  }
62
64
  function ledgerSectionMutation(content) {
63
- if (/^##\s+(审查分歧账本|Review Disagreement Ledger)\s*$/m.test(content))
64
- return [];
65
- const english = /^##\s+Activity Log\s*$/m.test(content);
66
- return [{
67
- kind: 'section', aliases: LEDGER_HEADINGS, heading: english ? 'Review Disagreement Ledger' : '审查分歧账本',
68
- body: `| ${LEDGER_COLUMNS.join(' | ')} |\n|----|-------|-------|----------|--------|----------|`
69
- }];
65
+ return [];
70
66
  }
71
67
  function implementationInputMutation(content, rows) {
72
68
  const english = /^##\s+Activity Log\s*$/m.test(content);
@@ -92,7 +88,10 @@ function applyLedgerIntent(intent, options = {}) {
92
88
  let rows;
93
89
  try {
94
90
  content = fs.readFileSync(resolved.taskMdPath, 'utf8');
95
- rows = parseLedger(content);
91
+ const ledger = parseLedgerDocument(content);
92
+ if (!ledger.present)
93
+ return failed(intent, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, resolved.taskId);
94
+ rows = ledger.rows;
96
95
  }
97
96
  catch (error) {
98
97
  return failed(intent, ledgerReadErrorCode(error), error instanceof Error ? error.message : String(error), resolved.taskId);
@@ -12,18 +12,25 @@ const LEDGER_TERMINAL = new Set(['confirmed', 'closed', 'human-decided']);
12
12
  // Statuses that represent a decision row awaiting or carrying a human ruling.
13
13
  const HUMAN_DECISION_STATUSES = new Set(['needs-human-decision', 'human-decided']);
14
14
  const FINDING_STATUSES = new Set(['open', 'accepted', 'adjusted', 'refuted', 'cannot-judge', 'confirmed', 'needs-human-decision', 'closed', 'human-decided']);
15
+ const LEDGER_SECTION_MISSING_CODE = 'LEDGER_SECTION_MISSING';
16
+ const LEDGER_SECTION_MISSING_MESSAGE = 'review disagreement ledger section is missing';
15
17
  function isReviewStage(stage) {
16
18
  return stage === 'analysis' || stage === 'plan' || stage === 'code';
17
19
  }
18
20
  // Parse all rows of the disagreement ledger table. Skips the heading, the
19
21
  // header row (`| id | ... |`) and the `|---|` separator; ignores non-`|` lines.
20
22
  // Rows with fewer than 6 columns are skipped (mirrors the JS gate parser).
21
- function parseLedger(content) {
23
+ function parseLedgerDocument(content) {
22
24
  const table = parseTable(content, { sectionAliases: LEDGER_HEADINGS, columns: LEDGER_COLUMNS });
23
- return table?.rows.map(({ values, sourceLine }) => ({
24
- id: values.id, stage: values.stage, round: values.round, severity: values.severity,
25
- status: values.status, evidence: values.evidence, sourceLine
26
- })) ?? [];
25
+ if (!table)
26
+ return { present: false, rows: [] };
27
+ return {
28
+ present: true,
29
+ rows: table.rows.map(({ values, sourceLine }) => ({
30
+ id: values.id, stage: values.stage, round: values.round, severity: values.severity,
31
+ status: values.status, evidence: values.evidence, sourceLine
32
+ }))
33
+ };
27
34
  }
28
35
  function validateLedgerRows(rows) {
29
36
  const seen = new Set();
@@ -104,5 +111,5 @@ function nextHdId(rows) {
104
111
  }
105
112
  return `HD-${max + 1}`;
106
113
  }
107
- export { LEDGER_HEADINGS, LEDGER_COLUMNS, parseLedger, nextHdId, isReviewStage, validateLedgerRows, summarizeLedgerStage, LEDGER_TERMINAL, HUMAN_DECISION_STATUSES };
114
+ export { LEDGER_HEADINGS, LEDGER_COLUMNS, parseLedgerDocument, nextHdId, isReviewStage, validateLedgerRows, summarizeLedgerStage, LEDGER_TERMINAL, HUMAN_DECISION_STATUSES, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE };
108
115
  //# sourceMappingURL=ledger.js.map
@@ -7,6 +7,8 @@ import { parseTypedTaskFrontmatter } from './frontmatter.js';
7
7
  import { locateHotTaskDirs, resolveTaskRef, TASK_ID_RE } from './resolve-ref.js';
8
8
  import { configuredShortIdLength, executeShortIdCommand, loadShortIdByTaskId, mutateShortIdRegistry } from './short-id.js';
9
9
  import { captureTaskWriteMetadata, writeTask } from './write.js';
10
+ import { validateCurrentTaskContract } from './current-contract.js';
11
+ import { isValidAgentInfraVersion } from '../version.js';
10
12
  const lifecycleIntentCatalog = [
11
13
  'block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore'
12
14
  ];
@@ -57,6 +59,9 @@ function failed(request, error, extra = {}) {
57
59
  error, ...extra
58
60
  };
59
61
  }
62
+ export function taskLifecycleFailure(request, error, taskId = null) {
63
+ return failed(request, error, { taskId });
64
+ }
60
65
  function normalizedRequest(request) {
61
66
  if (!request || typeof request !== 'object' || !lifecycleIntentCatalog.includes(request.intent)) {
62
67
  return { code: 'LIFECYCLE_PAYLOAD_INVALID', message: 'intent is not in the lifecycle catalog' };
@@ -366,6 +371,11 @@ function applyTaskLifecycle(requestInput, options = {}) {
366
371
  return failed(request, { code: 'LIFECYCLE_IDENTITY_INVALID', message: 'task.md id does not match its lifecycle task id' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
367
372
  if (!locateActivityLog(content) && !allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING'))
368
373
  return failed(request, { code: 'LIFECYCLE_LOG_MISSING', message: 'task has no unique Activity Log section' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
374
+ if (spec.target === 'active') {
375
+ const contract = validateCurrentTaskContract(content);
376
+ if (!contract.ok)
377
+ return failed(request, { code: 'LIFECYCLE_DOCUMENT_INVALID', message: contract.message }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
378
+ }
369
379
  const registryRequest = executeShortIdCommand({
370
380
  operation: 'list', activeDir: path.join(repoRoot, '.agents', 'workspace', 'active'),
371
381
  shortIdLength: configuredShortIdLength(repoRoot)
@@ -394,6 +404,9 @@ function applyTaskLifecycle(requestInput, options = {}) {
394
404
  }
395
405
  if (journal.taskId !== taskId || journal.intent !== request.intent)
396
406
  return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: 'an in-progress lifecycle journal belongs to a different request' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
407
+ if (spec.target === 'active' && !isValidAgentInfraVersion(journal.metadata.agentInfraVersion)) {
408
+ return failed(request, { code: 'LIFECYCLE_DOCUMENT_INVALID', message: `journal metadata agentInfraVersion must be a valid v-prefixed semver (received ${journal.metadata.agentInfraVersion})` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
409
+ }
397
410
  if (request.dryRun) {
398
411
  if (!journal.failure)
399
412
  return failed(request, { code: 'LIFECYCLE_JOURNAL_INVALID', message: 'existing lifecycle journal has no durable failure evidence for a read-only probe' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
@@ -411,6 +424,9 @@ function applyTaskLifecycle(requestInput, options = {}) {
411
424
  catch (error) {
412
425
  return failed(request, { code: 'LIFECYCLE_METADATA_FAILED', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
413
426
  }
427
+ if (spec.target === 'active' && !isValidAgentInfraVersion(metadata.agentInfraVersion)) {
428
+ return failed(request, { code: 'LIFECYCLE_DOCUMENT_INVALID', message: `metadata agentInfraVersion must be a valid v-prefixed semver (received ${metadata.agentInfraVersion})` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
429
+ }
414
430
  journal = { version: 1, taskId, intent: request.intent, intentDigest: digest, sourceState, targetState: spec.target, metadata, completedSteps: [] };
415
431
  if (request.dryRun) {
416
432
  const plannedMutations = mutationsFor(request, content, metadata, restoredFiles, allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING'));
@@ -2,9 +2,10 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { parseTypedTaskFrontmatter } from './frontmatter.js';
5
- import { parseReviewSummary } from './review-artifacts.js';
5
+ import { validateCurrentTaskContract } from './current-contract.js';
6
+ import { parseReviewSummary, resolveCanonicalVerdict } from './review-artifacts.js';
6
7
  import { inspectArtifactDirectory } from './artifact-lifecycle.js';
7
- import { parseLedger, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
8
+ import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
8
9
  import { resolveTaskRef } from './resolve-ref.js';
9
10
  import { activateDelegation, abortPreparedDelegation, completeDelegationStage, consumeDelegation, dispatchDelegation, foldBlankToNull, isDelegationReceipt, managedDelegationRole, prepareDelegation, sealDelegation } from './delegation-receipts.js';
10
11
  import { normalizeAgentClients } from '../agent-clients/config.js';
@@ -350,9 +351,21 @@ function hasReviewAfterArtifact(taskDir, family, reviewFamily) {
350
351
  function latestReviewApproved(taskDir, family) {
351
352
  const round = highestRound(taskDir, family);
352
353
  if (round === 0)
353
- return false;
354
- const parsed = parseReviewSummary(fs.readFileSync(path.join(taskDir, artifactName(family, round)), 'utf8'));
355
- return parsed.ok && parsed.summary.verdict === 'Approved' && parsed.summary.counts !== null;
354
+ return { approved: false, error: null };
355
+ let content;
356
+ try {
357
+ content = fs.readFileSync(path.join(taskDir, artifactName(family, round)), 'utf8');
358
+ }
359
+ catch (error) {
360
+ return { approved: false, error: String(error) };
361
+ }
362
+ const parsed = parseReviewSummary(content);
363
+ if (!parsed.ok)
364
+ return { approved: false, error: `${parsed.code}: ${parsed.message}` };
365
+ const verdict = resolveCanonicalVerdict(parsed.summary);
366
+ if (!verdict.ok)
367
+ return { approved: false, error: `${verdict.code}: ${verdict.message}` };
368
+ return { approved: verdict.verdict === 'Approved', error: null };
356
369
  }
357
370
  function routeFromFacts(taskDir) {
358
371
  const analysisRound = highestRound(taskDir, 'analysis');
@@ -369,7 +382,10 @@ function routeFromFacts(taskDir) {
369
382
  const round = analysisReviewRound + 1;
370
383
  return { action: 'review-analysis', role: 'reviewer', stage: 'review-analysis', round, artifact: artifactName('review-analysis', round) };
371
384
  }
372
- if (!latestReviewApproved(taskDir, 'review-analysis')) {
385
+ const analysisVerdict = latestReviewApproved(taskDir, 'review-analysis');
386
+ if (analysisVerdict.error)
387
+ return { error: analysisVerdict.error };
388
+ if (!analysisVerdict.approved) {
373
389
  const round = analysisRound + 1;
374
390
  return { action: 'analyze-task', role: 'executor', stage: 'analysis', round, artifact: artifactName('analysis', round) };
375
391
  }
@@ -381,7 +397,10 @@ function routeFromFacts(taskDir) {
381
397
  const round = planReviewRound + 1;
382
398
  return { action: 'review-plan', role: 'reviewer', stage: 'review-plan', round, artifact: artifactName('review-plan', round) };
383
399
  }
384
- if (!latestReviewApproved(taskDir, 'review-plan')) {
400
+ const planVerdict = latestReviewApproved(taskDir, 'review-plan');
401
+ if (planVerdict.error)
402
+ return { error: planVerdict.error };
403
+ if (!planVerdict.approved) {
385
404
  const round = planRound + 1;
386
405
  return { action: 'plan-task', role: 'executor', stage: 'plan', round, artifact: artifactName('plan', round) };
387
406
  }
@@ -393,7 +412,10 @@ function routeFromFacts(taskDir) {
393
412
  const round = codeReviewRound + 1;
394
413
  return { action: 'review-code', role: 'reviewer', stage: 'review-code', round, artifact: artifactName('review-code', round) };
395
414
  }
396
- if (!latestReviewApproved(taskDir, 'review-code')) {
415
+ const codeVerdict = latestReviewApproved(taskDir, 'review-code');
416
+ if (codeVerdict.error)
417
+ return { error: codeVerdict.error };
418
+ if (!codeVerdict.approved) {
397
419
  const round = codeRound + 1;
398
420
  return { action: 'code-task', role: 'executor', stage: 'code', round, artifact: artifactName('code', round) };
399
421
  }
@@ -418,13 +440,25 @@ function routeOrchestration(taskRef, options = {}) {
418
440
  return failed('ORCHESTRATION_TASK_READ_FAILED', String(error), resolved.taskId);
419
441
  }
420
442
  let metadata;
421
- try {
422
- metadata = parseTypedTaskFrontmatter(content);
443
+ let currentLedger = null;
444
+ if (resolved.state === 'active') {
445
+ const contract = validateCurrentTaskContract(content);
446
+ if (!contract.ok)
447
+ return failed('ORCHESTRATION_CURRENT_CONTRACT_INVALID', contract.message, resolved.taskId);
448
+ metadata = contract.metadata;
449
+ currentLedger = contract.ledger;
423
450
  }
424
- catch (error) {
425
- return failed('ORCHESTRATION_TASK_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
451
+ else {
452
+ try {
453
+ metadata = parseTypedTaskFrontmatter(content);
454
+ }
455
+ catch (error) {
456
+ return failed('ORCHESTRATION_TASK_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
457
+ }
426
458
  }
427
459
  const routed = routeFromFacts(resolved.taskDir);
460
+ if (routed && 'error' in routed)
461
+ return failed('ORCHESTRATION_REVIEW_INVALID', routed.error, resolved.taskId);
428
462
  if (!routed)
429
463
  return failed('ORCHESTRATION_ROUTE_UNKNOWN', 'cannot determine a unique lifecycle action', resolved.taskId);
430
464
  const run = readRun(resolved.taskDir);
@@ -440,7 +474,10 @@ function routeOrchestration(taskRef, options = {}) {
440
474
  if (!review.ok || review.summary.manualValidation === null) {
441
475
  return failed('ORCHESTRATION_REVIEW_INVALID', 'latest code review has no numeric manual-validation count', resolved.taskId);
442
476
  }
443
- const rows = parseLedger(content);
477
+ const ledger = currentLedger ?? parseLedgerDocument(content);
478
+ if (!ledger.present)
479
+ return failed('ORCHESTRATION_CURRENT_CONTRACT_INVALID', `${LEDGER_SECTION_MISSING_CODE}: ${LEDGER_SECTION_MISSING_MESSAGE}`, resolved.taskId);
480
+ const rows = ledger.rows;
444
481
  const ledgerError = validateLedgerRows(rows);
445
482
  if (ledgerError)
446
483
  return failed('ORCHESTRATION_LEDGER_INVALID', ledgerError.message, resolved.taskId);