@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
@@ -12,6 +12,8 @@ import {
12
12
  } from './short-id.ts';
13
13
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
14
14
  import type { TaskFileSystem, TaskMutation, TaskOperationSummary, TaskWriteMetadata } from './write.ts';
15
+ import { validateCurrentTaskContract } from './current-contract.ts';
16
+ import { isValidAgentInfraVersion } from '../version.ts';
15
17
 
16
18
  const lifecycleIntentCatalog = [
17
19
  'block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore'
@@ -126,6 +128,14 @@ function failed(request: Pick<TaskLifecycleRequest, 'taskRef' | 'intent'>, error
126
128
  };
127
129
  }
128
130
 
131
+ export function taskLifecycleFailure(
132
+ request: Pick<TaskLifecycleRequest, 'taskRef' | 'intent'>,
133
+ error: LifecycleError,
134
+ taskId: string | null = null
135
+ ): TaskLifecycleResult {
136
+ return failed(request, error, { taskId });
137
+ }
138
+
129
139
  function normalizedRequest(request: TaskLifecycleRequest): TaskLifecycleRequest | LifecycleError {
130
140
  if (!request || typeof request !== 'object' || !lifecycleIntentCatalog.includes(request.intent as TaskLifecycleIntent)) {
131
141
  return { code: 'LIFECYCLE_PAYLOAD_INVALID', message: 'intent is not in the lifecycle catalog' };
@@ -385,6 +395,10 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
385
395
  catch (error) { return failed(request, { code: 'LIFECYCLE_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath }); }
386
396
  if (frontmatter.id !== taskId) 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 });
387
397
  if (!locateActivityLog(content) && !allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING')) return failed(request, { code: 'LIFECYCLE_LOG_MISSING', message: 'task has no unique Activity Log section' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
398
+ if (spec.target === 'active') {
399
+ const contract = validateCurrentTaskContract(content);
400
+ if (!contract.ok) return failed(request, { code: 'LIFECYCLE_DOCUMENT_INVALID', message: contract.message }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
401
+ }
388
402
  const registryRequest = executeShortIdCommand({
389
403
  operation: 'list', activeDir: path.join(repoRoot, '.agents', 'workspace', 'active'),
390
404
  shortIdLength: configuredShortIdLength(repoRoot)
@@ -408,6 +422,9 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
408
422
  try { journal = parseJournal(io.readFileSync(initialJournalPath)); }
409
423
  catch (error) { return failed(request, { code: 'LIFECYCLE_JOURNAL_INVALID', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath }); }
410
424
  if (journal.taskId !== taskId || journal.intent !== request.intent) 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 });
425
+ if (spec.target === 'active' && !isValidAgentInfraVersion(journal.metadata.agentInfraVersion)) {
426
+ 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 });
427
+ }
411
428
  if (request.dryRun) {
412
429
  if (!journal.failure) 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 });
413
430
  return failed(request, journal.failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath, completedSteps: journal.completedSteps, pendingSteps: STEPS.filter((step) => !journal.completedSteps.includes(step)) });
@@ -418,6 +435,9 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
418
435
  let metadata: TaskWriteMetadata;
419
436
  try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
420
437
  catch (error) { return failed(request, { code: 'LIFECYCLE_METADATA_FAILED', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath }); }
438
+ if (spec.target === 'active' && !isValidAgentInfraVersion(metadata.agentInfraVersion)) {
439
+ 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 });
440
+ }
421
441
  journal = { version: 1, taskId, intent: request.intent, intentDigest: digest, sourceState, targetState: spec.target, metadata, completedSteps: [] };
422
442
  if (request.dryRun) {
423
443
  const plannedMutations = mutationsFor(request, content, metadata, restoredFiles, allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING'));
@@ -3,9 +3,11 @@ import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
4
 
5
5
  import { parseTypedTaskFrontmatter } from './frontmatter.ts';
6
- import { parseReviewSummary } from './review-artifacts.ts';
6
+ import { validateCurrentTaskContract } from './current-contract.ts';
7
+ import { parseReviewSummary, resolveCanonicalVerdict } from './review-artifacts.ts';
7
8
  import { inspectArtifactDirectory } from './artifact-lifecycle.ts';
8
- import { parseLedger, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
9
+ import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
10
+ import type { LedgerDocument } from './ledger.ts';
9
11
  import { resolveTaskRef } from './resolve-ref.ts';
10
12
  import {
11
13
  activateDelegation,
@@ -530,14 +532,20 @@ function hasReviewAfterArtifact(taskDir: string, family: string, reviewFamily: s
530
532
  && review.reviewedInput?.name === artifact.latest.name;
531
533
  }
532
534
 
533
- function latestReviewApproved(taskDir: string, family: 'review-analysis' | 'review-plan' | 'review-code'): boolean {
535
+ function latestReviewApproved(taskDir: string, family: 'review-analysis' | 'review-plan' | 'review-code'): { approved: boolean; error: string | null } {
534
536
  const round = highestRound(taskDir, family);
535
- if (round === 0) return false;
536
- const parsed = parseReviewSummary(fs.readFileSync(path.join(taskDir, artifactName(family, round)), 'utf8'));
537
- return parsed.ok && parsed.summary.verdict === 'Approved' && parsed.summary.counts !== null;
537
+ if (round === 0) return { approved: false, error: null };
538
+ let content: string;
539
+ try { content = fs.readFileSync(path.join(taskDir, artifactName(family, round)), 'utf8'); }
540
+ catch (error) { return { approved: false, error: String(error) }; }
541
+ const parsed = parseReviewSummary(content);
542
+ if (!parsed.ok) return { approved: false, error: `${parsed.code}: ${parsed.message}` };
543
+ const verdict = resolveCanonicalVerdict(parsed.summary);
544
+ if (!verdict.ok) return { approved: false, error: `${verdict.code}: ${verdict.message}` };
545
+ return { approved: verdict.verdict === 'Approved', error: null };
538
546
  }
539
547
 
540
- function routeFromFacts(taskDir: string): Omit<OrchestrationNext, 'requestedModel' | 'requestedReasoningEffort'> | null {
548
+ function routeFromFacts(taskDir: string): Omit<OrchestrationNext, 'requestedModel' | 'requestedReasoningEffort'> | { error: string } | null {
541
549
  const analysisRound = highestRound(taskDir, 'analysis');
542
550
  const analysisReviewRound = highestRound(taskDir, 'review-analysis');
543
551
  const planRound = highestRound(taskDir, 'plan');
@@ -552,7 +560,9 @@ function routeFromFacts(taskDir: string): Omit<OrchestrationNext, 'requestedMode
552
560
  const round = analysisReviewRound + 1;
553
561
  return { action: 'review-analysis', role: 'reviewer', stage: 'review-analysis', round, artifact: artifactName('review-analysis', round) };
554
562
  }
555
- if (!latestReviewApproved(taskDir, 'review-analysis')) {
563
+ const analysisVerdict = latestReviewApproved(taskDir, 'review-analysis');
564
+ if (analysisVerdict.error) return { error: analysisVerdict.error };
565
+ if (!analysisVerdict.approved) {
556
566
  const round = analysisRound + 1;
557
567
  return { action: 'analyze-task', role: 'executor', stage: 'analysis', round, artifact: artifactName('analysis', round) };
558
568
  }
@@ -564,7 +574,9 @@ function routeFromFacts(taskDir: string): Omit<OrchestrationNext, 'requestedMode
564
574
  const round = planReviewRound + 1;
565
575
  return { action: 'review-plan', role: 'reviewer', stage: 'review-plan', round, artifact: artifactName('review-plan', round) };
566
576
  }
567
- if (!latestReviewApproved(taskDir, 'review-plan')) {
577
+ const planVerdict = latestReviewApproved(taskDir, 'review-plan');
578
+ if (planVerdict.error) return { error: planVerdict.error };
579
+ if (!planVerdict.approved) {
568
580
  const round = planRound + 1;
569
581
  return { action: 'plan-task', role: 'executor', stage: 'plan', round, artifact: artifactName('plan', round) };
570
582
  }
@@ -576,7 +588,9 @@ function routeFromFacts(taskDir: string): Omit<OrchestrationNext, 'requestedMode
576
588
  const round = codeReviewRound + 1;
577
589
  return { action: 'review-code', role: 'reviewer', stage: 'review-code', round, artifact: artifactName('review-code', round) };
578
590
  }
579
- if (!latestReviewApproved(taskDir, 'review-code')) {
591
+ const codeVerdict = latestReviewApproved(taskDir, 'review-code');
592
+ if (codeVerdict.error) return { error: codeVerdict.error };
593
+ if (!codeVerdict.approved) {
580
594
  const round = codeRound + 1;
581
595
  return { action: 'code-task', role: 'executor', stage: 'code', round, artifact: artifactName('code', round) };
582
596
  }
@@ -599,12 +613,21 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
599
613
  return failed('ORCHESTRATION_TASK_READ_FAILED', String(error), resolved.taskId);
600
614
  }
601
615
  let metadata: ReturnType<typeof parseTypedTaskFrontmatter>;
602
- try {
603
- metadata = parseTypedTaskFrontmatter(content);
604
- } catch (error) {
605
- return failed('ORCHESTRATION_TASK_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
616
+ let currentLedger: LedgerDocument | null = null;
617
+ if (resolved.state === 'active') {
618
+ const contract = validateCurrentTaskContract(content);
619
+ if (!contract.ok) return failed('ORCHESTRATION_CURRENT_CONTRACT_INVALID', contract.message, resolved.taskId);
620
+ metadata = contract.metadata;
621
+ currentLedger = contract.ledger;
622
+ } else {
623
+ try {
624
+ metadata = parseTypedTaskFrontmatter(content);
625
+ } catch (error) {
626
+ return failed('ORCHESTRATION_TASK_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
627
+ }
606
628
  }
607
629
  const routed = routeFromFacts(resolved.taskDir);
630
+ if (routed && 'error' in routed) return failed('ORCHESTRATION_REVIEW_INVALID', routed.error, resolved.taskId);
608
631
  if (!routed) return failed('ORCHESTRATION_ROUTE_UNKNOWN', 'cannot determine a unique lifecycle action', resolved.taskId);
609
632
  const run = readRun(resolved.taskDir);
610
633
  const policy = run ? rolePolicy(run, routed.role) : null;
@@ -622,7 +645,9 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
622
645
  if (!review.ok || review.summary.manualValidation === null) {
623
646
  return failed('ORCHESTRATION_REVIEW_INVALID', 'latest code review has no numeric manual-validation count', resolved.taskId);
624
647
  }
625
- const rows = parseLedger(content);
648
+ const ledger = currentLedger ?? parseLedgerDocument(content);
649
+ if (!ledger.present) return failed('ORCHESTRATION_CURRENT_CONTRACT_INVALID', `${LEDGER_SECTION_MISSING_CODE}: ${LEDGER_SECTION_MISSING_MESSAGE}`, resolved.taskId);
650
+ const rows = ledger.rows;
626
651
  const ledgerError = validateLedgerRows(rows);
627
652
  if (ledgerError) return failed('ORCHESTRATION_LEDGER_INVALID', ledgerError.message, resolved.taskId);
628
653
  if (!summarizeLedgerStage(rows, 'code').canAdvance) {
@@ -18,6 +18,21 @@ type ReviewSummaryErrorCode =
18
18
  | 'REVIEW_SUMMARY_NOT_FOUND'
19
19
  | 'REVIEW_SUMMARY_PLACEHOLDER_INVALID'
20
20
  | 'REVIEW_SUMMARY_COUNT_MISMATCH';
21
+ type VerdictSemanticErrorCode =
22
+ | 'REVIEW_VERDICT_FINDING_MISMATCH'
23
+ | 'REVIEW_FINDING_COUNTS_NOT_FINALIZED';
24
+ type CanonicalVerdictResult =
25
+ | { ok: true; verdict: ReviewVerdict }
26
+ | { ok: false; verdict: null; code: VerdictSemanticErrorCode; message: string };
27
+ type PathVerdictErrorCode =
28
+ | 'REVIEW_ARTIFACT_NOT_FOUND'
29
+ | 'REVIEW_ARTIFACT_READ_FAILED'
30
+ | 'REVIEW_SUMMARY_NOT_FOUND'
31
+ | 'REVIEW_SUMMARY_PLACEHOLDER_INVALID'
32
+ | VerdictSemanticErrorCode;
33
+ type PathVerdictResult =
34
+ | { ok: true; verdict: ReviewVerdict }
35
+ | { ok: false; verdict: null; code: PathVerdictErrorCode; message: string };
21
36
  type ReviewSummaryParseResult =
22
37
  | { ok: true; summary: ReviewSummary; findingsStart: number; findingsEnd: number }
23
38
  | { ok: false; code: ReviewSummaryErrorCode; message: string };
@@ -203,28 +218,46 @@ function finalizeReviewSummaryContent(
203
218
  };
204
219
  }
205
220
 
206
- function parseVerdict(reviewPath: string):
207
- | { ok: true; verdict: 'Approved' | 'Approved-with-issues' | 'Changes Requested' | 'Rejected' }
208
- | { ok: false; verdict: null; message: string } {
221
+ function resolveCanonicalVerdict(summary: ReviewSummary): CanonicalVerdictResult {
222
+ if (summary.counts === null) {
223
+ return {
224
+ ok: false,
225
+ verdict: null,
226
+ code: 'REVIEW_FINDING_COUNTS_NOT_FINALIZED',
227
+ message: 'review summary finding counts are not finalized'
228
+ };
229
+ }
230
+ if (summary.verdict === 'Approved' && !equalCounts(summary.counts, { blocker: 0, major: 0, minor: 0 })) {
231
+ return {
232
+ ok: false,
233
+ verdict: null,
234
+ code: 'REVIEW_VERDICT_FINDING_MISMATCH',
235
+ message: 'Approved verdict requires zero finalized findings'
236
+ };
237
+ }
238
+ return { ok: true, verdict: summary.verdict };
239
+ }
240
+
241
+ function parseVerdict(reviewPath: string): PathVerdictResult {
209
242
  if (!fs.existsSync(reviewPath)) {
210
- return { ok: false, verdict: null, message: `Review artifact not found: ${path.basename(reviewPath)}` };
243
+ return { ok: false, verdict: null, code: 'REVIEW_ARTIFACT_NOT_FOUND', message: `Review artifact not found: ${path.basename(reviewPath)}` };
244
+ }
245
+ let content: string;
246
+ try {
247
+ content = fs.readFileSync(reviewPath, 'utf8');
248
+ } catch (error) {
249
+ return { ok: false, verdict: null, code: 'REVIEW_ARTIFACT_READ_FAILED', message: `Review artifact could not be read: ${String(error)}` };
211
250
  }
212
- const parsed = parseReviewSummary(fs.readFileSync(reviewPath, 'utf8'));
251
+ const parsed = parseReviewSummary(content);
213
252
  if (!parsed.ok) {
214
253
  return {
215
254
  ok: false,
216
255
  verdict: null,
256
+ code: parsed.code === 'REVIEW_SUMMARY_PLACEHOLDER_INVALID' ? parsed.code : 'REVIEW_SUMMARY_NOT_FOUND',
217
257
  message: `cannot parse review summary in ${path.basename(reviewPath)}: ${parsed.message}`
218
258
  };
219
259
  }
220
- const { verdict, counts } = parsed.summary;
221
- if (verdict !== 'Approved') return { ok: true, verdict };
222
- return {
223
- ok: true,
224
- verdict: counts && equalCounts(counts, { blocker: 0, major: 0, minor: 0 })
225
- ? 'Approved'
226
- : 'Approved-with-issues'
227
- };
260
+ return resolveCanonicalVerdict(parsed.summary);
228
261
  }
229
262
 
230
263
  export {
@@ -236,13 +269,18 @@ export {
236
269
  maxRound,
237
270
  normalizeVerdict,
238
271
  parseReviewSummary,
239
- parseVerdict
272
+ parseVerdict,
273
+ resolveCanonicalVerdict
240
274
  };
241
275
  export type {
276
+ CanonicalVerdictResult,
277
+ PathVerdictErrorCode,
278
+ PathVerdictResult,
242
279
  ReviewFindingCounts,
243
280
  ReviewSummary,
244
281
  ReviewSummaryErrorCode,
245
282
  ReviewSummaryFinalizeResult,
246
283
  ReviewSummaryParseResult,
247
- ReviewVerdict
284
+ ReviewVerdict,
285
+ VerdictSemanticErrorCode
248
286
  };
@@ -2,11 +2,12 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
4
 
5
- import { locateActivityLog, pairEntries } from './activity-log.ts';
5
+ import { locateActivityLog, pairEntries, startedBackedRows } from './activity-log.ts';
6
6
  import { parseArtifactName, validateCompletedArtifact } from './artifact-lifecycle.ts';
7
- import { parseLedger, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
7
+ import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
8
8
  import type { LedgerStageStatus, ReviewStage } from './ledger.ts';
9
9
  import { finalizeReviewSummaryContent } from './review-artifacts.ts';
10
+ import { inspectDecisionDetailDuplicates, repairDecisionDetailDuplicates } from './decision-details.ts';
10
11
  import { resolveTaskRef } from './resolve-ref.ts';
11
12
  import { validateLifecycleExecution } from './lifecycle-execution.ts';
12
13
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
@@ -25,6 +26,7 @@ type ReviewFinalizationErrorCode =
25
26
  | 'REVIEW_SUMMARY_NOT_FOUND'
26
27
  | 'REVIEW_SUMMARY_PLACEHOLDER_INVALID'
27
28
  | 'REVIEW_SUMMARY_COUNT_MISMATCH'
29
+ | 'REVIEW_DECISION_DETAIL_INVALID'
28
30
  | 'REVIEW_ARTIFACT_CONFLICT'
29
31
  | 'REVIEW_PROVENANCE_INVALID'
30
32
  | 'REVIEW_TEMP_WRITE_FAILED'
@@ -106,8 +108,8 @@ function openReviewRound(content: string, action: string, round: number): boolea
106
108
  const section = locateActivityLog(content);
107
109
  if (!section) return false;
108
110
  const expected = `${action} (Round ${round})`;
109
- return pairEntries(section.entries).filter((row) => (
110
- row.step === expected && Boolean(row.started) && !row.done
111
+ return startedBackedRows(pairEntries(section.entries)).filter((row) => (
112
+ row.step === expected && !row.done
111
113
  )).length === 1;
112
114
  }
113
115
 
@@ -194,7 +196,11 @@ function finalizeReviewSummaryUnlocked(
194
196
 
195
197
  let rows;
196
198
  try {
197
- rows = parseLedger(taskContent);
199
+ const ledger = parseLedgerDocument(taskContent);
200
+ if (!ledger.present) {
201
+ return failed(request, 'REVIEW_LEDGER_INVALID', `${LEDGER_SECTION_MISSING_CODE}: ${LEDGER_SECTION_MISSING_MESSAGE}`, resolved.taskId);
202
+ }
203
+ rows = ledger.rows;
198
204
  } catch (error) {
199
205
  return failed(request, 'REVIEW_LEDGER_INVALID', String(error), resolved.taskId);
200
206
  }
@@ -208,14 +214,34 @@ function finalizeReviewSummaryUnlocked(
208
214
  );
209
215
  }
210
216
  const stageStatus = summarizeLedgerStage(rows, stage);
217
+ const repaired = repairDecisionDetailDuplicates(artifactContent);
218
+ if (!repaired.ok) {
219
+ return failed(
220
+ request,
221
+ 'REVIEW_DECISION_DETAIL_INVALID',
222
+ `${repaired.code}: ${repaired.message}`,
223
+ resolved.taskId,
224
+ stageStatus
225
+ );
226
+ }
227
+ const detailInspection = inspectDecisionDetailDuplicates(repaired.content);
228
+ if (!detailInspection.ok) {
229
+ return failed(
230
+ request,
231
+ 'REVIEW_DECISION_DETAIL_INVALID',
232
+ `${detailInspection.code}: ${detailInspection.message}`,
233
+ resolved.taskId,
234
+ stageStatus
235
+ );
236
+ }
211
237
  const transformed = finalizeReviewSummaryContent(
212
- artifactContent,
238
+ repaired.content,
213
239
  stageStatus.unresolvedFindingCounts
214
240
  );
215
241
  if (!transformed.ok) {
216
242
  return failed(request, transformed.code, transformed.message, resolved.taskId, stageStatus);
217
243
  }
218
- if (!transformed.changed) {
244
+ if (!repaired.changed && !transformed.changed) {
219
245
  return {
220
246
  ...failed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, stageStatus),
221
247
  status: 'no-op',
@@ -22,9 +22,11 @@ import { resolveMaterializedReviewedHeadRelation } from "../platform/change-requ
22
22
  import { resolveReviewedHeadRelation } from "../platform/merged-pr-equivalence.ts";
23
23
  import { resolveLocalReviewedCommitRelation } from "../git/reviewed-commit-equivalence.ts";
24
24
  import { parseTypedTaskFrontmatter } from "./frontmatter.ts";
25
- import { parseLedger, summarizeLedgerStage, validateLedgerRows } from "./ledger.ts";
25
+ import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from "./ledger.ts";
26
26
  import type { LedgerRow } from "./ledger.ts";
27
+ import { isValidAgentInfraVersion } from "../version.ts";
27
28
  import { equalCounts, parseReviewSummary } from "./review-artifacts.ts";
29
+ import { inspectDecisionDetailDuplicates } from "./decision-details.ts";
28
30
  import { loadVerificationConfig } from "./verification-config.ts";
29
31
  import { snapshotReview } from "../git/review-snapshot.ts";
30
32
  import { OrchestrationStateError, readRun } from "./orchestration.ts";
@@ -48,7 +50,6 @@ const DEFAULT_REQUIRED_FIELDS = [
48
50
  ];
49
51
 
50
52
  const DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[+-]\d{2}:\d{2})?$/;
51
- const AGENT_INFRA_VERSION_PATTERN = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
52
53
  const ACTIVITY_LOG_PATTERN = /^- (\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[+-]\d{2}:\d{2})?) — \*\*(.+?)\*\* by (.+?) — (.+)$/;
53
54
  // Start markers (action suffixed with ` [started]`) are excluded from the
54
55
  // "latest action" / freshness computation so a step's in-flight marker never
@@ -108,6 +109,8 @@ function runCheck(type: any, context: any, shared: any): any {
108
109
  return checkTaskMeta(context);
109
110
  case "artifact":
110
111
  return checkArtifact(context);
112
+ case "decision-details":
113
+ return checkDecisionDetails(context);
111
114
  case "implementation-input":
112
115
  return checkImplementationInput(context);
113
116
  case "activity-log":
@@ -392,23 +395,20 @@ function checkTaskMeta({ taskDir, config }: any): any {
392
395
  const metadata = task.metadata;
393
396
  const requiredFields = config.required_fields || DEFAULT_REQUIRED_FIELDS;
394
397
  const missingFields = requiredFields.filter((field: any) => isBlank(metadata[field]));
395
- const blockingMissingFields = missingFields.filter((field: any) => field !== "agent_infra_version");
398
+ const currentTask = metadata.status === 'active';
399
+ const blockingMissingFields = missingFields.filter((field: any) => field !== "agent_infra_version" || currentTask);
396
400
  const warnings = [];
397
- if (missingFields.includes("agent_infra_version")) {
401
+ if (missingFields.includes("agent_infra_version") && !currentTask) {
398
402
  warnings.push("field 'agent_infra_version' missing — historical task or skipped version stamp");
399
403
  }
400
404
  if (blockingMissingFields.length > 0) {
401
405
  return failResult("task-meta", `Missing required fields: ${blockingMissingFields.join(", ")}`);
402
406
  }
403
407
 
404
- if (
405
- !isBlank(metadata.agent_infra_version) &&
406
- metadata.agent_infra_version !== "unknown" &&
407
- !AGENT_INFRA_VERSION_PATTERN.test(metadata.agent_infra_version)
408
- ) {
408
+ if (currentTask && !isValidAgentInfraVersion(metadata.agent_infra_version)) {
409
409
  return failResult(
410
410
  "task-meta",
411
- `Invalid agent_infra_version: ${metadata.agent_infra_version}`
411
+ `Invalid agent_infra_version: ${String(metadata.agent_infra_version ?? 'missing')}`
412
412
  );
413
413
  }
414
414
 
@@ -622,6 +622,18 @@ function checkArtifact({ taskDir, config, artifactFile }: any): any {
622
622
  );
623
623
  }
624
624
 
625
+ function checkDecisionDetails({ taskDir, artifactFile }: any): any {
626
+ const resolvedArtifact = resolveArtifactPath(taskDir, "", artifactFile);
627
+ if (!resolvedArtifact.ok) return failResult("decision-details", resolvedArtifact.message);
628
+ const artifactPath = resolvedArtifact.path;
629
+ const stat = safeStat(artifactPath);
630
+ if (!stat?.isFile()) return failResult("decision-details", `Artifact not found: ${path.basename(artifactPath)}`);
631
+ if (stat.size === 0) return passResult("decision-details", `${path.basename(artifactPath)} contains no decision details`);
632
+ const inspection = inspectDecisionDetailDuplicates(fs.readFileSync(artifactPath, "utf8"));
633
+ if (!inspection.ok) return failResult("decision-details", `${path.basename(artifactPath)}: ${inspection.message}`);
634
+ return passResult("decision-details", `${path.basename(artifactPath)} has no duplicate decision detail ids`);
635
+ }
636
+
625
637
  function checkReviewSummary({ taskDir, config, artifactFile }: any): any {
626
638
  const stage = String(config.stage || "");
627
639
  if (!["analysis", "plan", "code"].includes(stage)) {
@@ -640,7 +652,9 @@ function checkReviewSummary({ taskDir, config, artifactFile }: any): any {
640
652
  if (!task.ok) return failResult("review-summary", task.message);
641
653
  let rows;
642
654
  try {
643
- rows = parseLedger(task.content);
655
+ const ledger = parseLedgerDocument(task.content);
656
+ if (!ledger.present) return failResult("review-summary", `${LEDGER_SECTION_MISSING_CODE}: ${LEDGER_SECTION_MISSING_MESSAGE}`);
657
+ rows = ledger.rows;
644
658
  } catch (error) {
645
659
  return failResult("review-summary", `Invalid disagreement ledger: ${String(error)}`);
646
660
  }
@@ -946,14 +960,11 @@ function checkReviewLedger({ taskDir, config }: any): any {
946
960
  return failResult("review-ledger", task.message);
947
961
  }
948
962
 
949
- const section = getSectionContent(task.content, LEDGER_SECTION_NAMES);
950
- if (!section.trim()) {
951
- return passResult("review-ledger", "No disagreement ledger section; treated as no open disagreements");
952
- }
953
-
954
963
  let rows;
955
964
  try {
956
- rows = parseLedger(task.content).map((row) => [
965
+ const ledger = parseLedgerDocument(task.content);
966
+ if (!ledger.present) return failResult("review-ledger", `${LEDGER_SECTION_MISSING_CODE}: ${LEDGER_SECTION_MISSING_MESSAGE}`);
967
+ rows = ledger.rows.map((row) => [
957
968
  row.id, row.stage, row.round, row.severity, row.status, row.evidence
958
969
  ]);
959
970
  } catch (error) {
@@ -1248,7 +1259,11 @@ function resolvePostReviewExemption(content: string):
1248
1259
  | { ok: true; exemptions: readonly LedgerRow[] }
1249
1260
  | { ok: false; code: "POST_REVIEW_EXEMPTION_INVALID"; message: string } {
1250
1261
  try {
1251
- const rows = parseLedger(content);
1262
+ const ledger = parseLedgerDocument(content);
1263
+ if (!ledger.present) {
1264
+ return { ok: false, code: "POST_REVIEW_EXEMPTION_INVALID", message: `${LEDGER_SECTION_MISSING_CODE}: ${LEDGER_SECTION_MISSING_MESSAGE}` };
1265
+ }
1266
+ const rows = ledger.rows;
1252
1267
  const invalid = validateLedgerRows(rows);
1253
1268
  if (invalid) {
1254
1269
  return {
@@ -8,4 +8,4 @@ export {
8
8
  parseReviewVerdict,
9
9
  resolvePostReviewGlobs
10
10
  } from './review-fingerprint.ts';
11
- export { parseLedger, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
11
+ export { parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
@@ -53,13 +53,6 @@ function captureWorktreeTree(repoRoot: string, forcedPath: string | null): strin
53
53
  }
54
54
  }
55
55
 
56
- function captureLegacyWorkspaceSnapshot(repoRoot: string, taskId: string | null): string {
57
- const workspaceRelative = taskId
58
- ? `.agents/workspace/active/${taskId}`
59
- : '.agents/workspace/active';
60
- return captureWorktreeTree(repoRoot, workspaceRelative);
61
- }
62
-
63
56
  function taskFileFingerprints(stateRoot: string, taskId: string | null): TaskFileFingerprint[] {
64
57
  const activeRoot = path.join(stateRoot, '.agents', 'workspace', 'active');
65
58
  const roots = taskId ? [path.join(activeRoot, taskId)] : [activeRoot];
@@ -73,6 +66,7 @@ function taskFileFingerprints(stateRoot: string, taskId: string | null): TaskFil
73
66
  return;
74
67
  }
75
68
  if (!stat.isFile() && !stat.isSymbolicLink()) return;
69
+ if (relative.startsWith('.agents/workspace/active/') && path.posix.basename(relative) === 'orchestration.json') return;
76
70
  const content = stat.isSymbolicLink()
77
71
  ? Buffer.from(fs.readlinkSync(absolute))
78
72
  : fs.readFileSync(absolute);
@@ -108,14 +102,11 @@ function decodeWorkspaceFingerprint(value: string): OrchestrationWorkspaceFinger
108
102
  }
109
103
  }
110
104
 
111
- function captureWorkspaceSnapshot(context: WorkspaceSnapshotContext): string;
112
- function captureWorkspaceSnapshot(repoRoot: string, taskId: string | null): string;
113
- function captureWorkspaceSnapshot(contextOrRoot: WorkspaceSnapshotContext | string, taskId?: string | null): string {
114
- if (typeof contextOrRoot === 'string') return captureLegacyWorkspaceSnapshot(contextOrRoot, taskId ?? null);
105
+ function captureWorkspaceSnapshot(context: WorkspaceSnapshotContext): string {
115
106
  return encodeWorkspaceFingerprint({
116
107
  version: 2,
117
- gitTree: captureWorktreeTree(contextOrRoot.gitRoot, null),
118
- taskFiles: taskFileFingerprints(contextOrRoot.stateRoot, contextOrRoot.taskId)
108
+ gitTree: captureWorktreeTree(context.gitRoot, null),
109
+ taskFiles: taskFileFingerprints(context.stateRoot, context.taskId)
119
110
  });
120
111
  }
121
112
 
package/lib/task/write.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
- import { VERSION } from '../version.ts';
4
+ import { isValidAgentInfraVersion, VERSION } from '../version.ts';
5
5
  import {
6
6
  parseTypedTaskFrontmatter,
7
7
  updateTaskFrontmatter
@@ -15,6 +15,7 @@ import type {
15
15
  import { allowsManualOverride } from './guard-override.ts';
16
16
  import type { ManualOverrideCapability } from './guard-override.ts';
17
17
  import { mutateTableRow, upsertSection } from './sections.ts';
18
+ import { validateCurrentTaskContract } from './current-contract.ts';
18
19
  import type {
19
20
  TableRowDeleteMutation,
20
21
  TableRowUpsertMutation
@@ -84,6 +85,7 @@ type TaskWriteMetadata = {
84
85
  type TaskWriteErrorCode =
85
86
  | ResolveTaskRefErrorCode
86
87
  | 'TASK_STATE_MISMATCH'
88
+ | 'TASK_CURRENT_CONTRACT_INVALID'
87
89
  | 'TASK_READ_FAILED'
88
90
  | 'TASK_DOCUMENT_INVALID'
89
91
  | 'MUTATION_INVALID'
@@ -268,6 +270,13 @@ function writeTask(request: TaskWriteRequest, options: TaskWriteOptions = {}): T
268
270
  );
269
271
  }
270
272
 
273
+ if (resolved.state === 'active') {
274
+ const contract = validateCurrentTaskContract(original);
275
+ if (!contract.ok) {
276
+ return failure(request, identity, contract.code, contract.message);
277
+ }
278
+ }
279
+
271
280
  let candidate = original;
272
281
  const operations: TaskOperationSummary[] = [];
273
282
  try {
@@ -342,6 +351,16 @@ function writeTask(request: TaskWriteRequest, options: TaskWriteOptions = {}): T
342
351
  operations
343
352
  );
344
353
  }
354
+ if (resolved.state === 'active' && !isValidAgentInfraVersion(metadata.agentInfraVersion)) {
355
+ return failure(
356
+ request,
357
+ identity,
358
+ 'TASK_CURRENT_CONTRACT_INVALID',
359
+ `metadata agentInfraVersion must be a valid v-prefixed semver (received ${metadata.agentInfraVersion})`,
360
+ operations,
361
+ metadata
362
+ );
363
+ }
345
364
 
346
365
  if (candidate === original) {
347
366
  return {