@khoavu882/doflow 2.0.0-beta.5 → 2.0.1

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 (494) hide show
  1. package/README.md +16 -16
  2. package/bin/doflow.js +22 -12
  3. package/core/.antigravity-plugin/plugin.json +1 -2
  4. package/core/.claude-plugin/plugin.json +1 -1
  5. package/core/.codex-plugin/plugin.json +1 -1
  6. package/core/.plugin/plugin.json +1 -1
  7. package/core/harnesses/claude/hooks/lib.sh +43 -24
  8. package/core/harnesses/claude/hooks/mcp-tool-guard.sh +4 -1
  9. package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
  10. package/core/harnesses/claude/hooks/post-compact.sh +35 -32
  11. package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
  12. package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
  13. package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
  14. package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
  15. package/core/harnesses/claude/hooks/pre-implement-gate.sh +4 -1
  16. package/core/harnesses/claude/hooks/session-end.sh +23 -10
  17. package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
  18. package/core/harnesses/claude/settings/settings.json +26 -9
  19. package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
  20. package/core/harnesses/codex/agents/core-implementer.toml +1 -0
  21. package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
  22. package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
  23. package/core/harnesses/codex/agents/research-writer.toml +1 -1
  24. package/core/harnesses/codex/agents/reviewer.toml +10 -0
  25. package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
  26. package/core/harnesses/codex/agents/system-architect.toml +1 -1
  27. package/core/harnesses/codex/hooks/hooks.json +0 -10
  28. package/core/harnesses/codex/hooks/lib.sh +44 -25
  29. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +4 -1
  30. package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
  31. package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
  32. package/core/harnesses/codex/hooks/pre-implement-gate.sh +4 -1
  33. package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
  34. package/core/harnesses/codex/hooks/subagent-audit.sh +1 -0
  35. package/core/harnesses/copilot/hooks/doflow-lib.sh +94 -0
  36. package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
  37. package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
  38. package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
  39. package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
  40. package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
  41. package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
  42. package/core/harnesses/copilot/hooks/doflow.json +51 -0
  43. package/core/harnesses/kiro/agents/doflow.json +1 -11
  44. package/core/harnesses/kiro/hooks/doflow.json +0 -10
  45. package/core/harnesses/kiro/hooks/lib.sh +26 -6
  46. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +8 -3
  47. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +8 -3
  48. package/core/harnesses/kiro/hooks/session-start.sh +1 -0
  49. package/core/harnesses/kiro/hooks/stop-check.sh +1 -0
  50. package/core/harnesses/pi/extensions/doflow-guard.js +11 -2
  51. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +1 -2
  52. package/core/harnesses/shared/hooks/adapters/antigravity.js +1 -1
  53. package/core/harnesses/shared/hooks/bridge/bridge.sh +126 -23
  54. package/core/harnesses/shared/hooks/bridge/envelope.jq +87 -18
  55. package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
  56. package/core/harnesses/shared/hooks/bridge/respond.sh +16 -2
  57. package/core/harnesses/shared/hooks/policies/capture-failure.sh +49 -16
  58. package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
  59. package/core/harnesses/shared/hooks/policies/contract.sh +3 -0
  60. package/core/harnesses/shared/hooks/policies/deny-json.sh +20 -7
  61. package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
  62. package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
  63. package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
  64. package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
  65. package/core/harnesses/shared/hooks/policies/lib.sh +63 -40
  66. package/core/harnesses/shared/hooks/policies/mcp-policy.conf +1 -1
  67. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +7 -8
  68. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +132 -57
  69. package/core/harnesses/shared/hooks/policies/session-context.sh +30 -1
  70. package/core/harnesses/shared/hooks/policies/stop-check.sh +27 -15
  71. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +6 -3
  72. package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
  73. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +60 -155
  74. package/core/harnesses/shared/hooks/stream-hook-runner.js +58 -20
  75. package/core/harnesses/shared/locator/doflow-run +2 -3
  76. package/core/registry/advice.json +3 -3
  77. package/core/registry/assets.json +55 -82
  78. package/core/registry/capabilities.json +12 -0
  79. package/core/registry/contracts.json +58 -31
  80. package/core/registry/contribution-points.json +35 -0
  81. package/core/registry/gates.json +32 -0
  82. package/core/registry/guidance-caps.json +40 -0
  83. package/core/registry/harnesses.json +1445 -366
  84. package/core/registry/intake.json +53 -0
  85. package/core/registry/lifecycle.json +599 -996
  86. package/core/registry/mcp.json +5 -1
  87. package/core/registry/models.json +7 -1
  88. package/core/registry/package-templates/README.md +19 -0
  89. package/core/registry/package-templates/agent/files/agent.md +9 -0
  90. package/core/registry/package-templates/agent/template.json +4 -0
  91. package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
  92. package/core/registry/package-templates/capability-provider/template.json +11 -0
  93. package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
  94. package/core/registry/package-templates/debate-preset/template.json +4 -0
  95. package/core/registry/package-templates/intake-category/files/category.json +8 -0
  96. package/core/registry/package-templates/intake-category/template.json +4 -0
  97. package/core/registry/package-templates/review-preset/files/preset.json +30 -0
  98. package/core/registry/package-templates/review-preset/template.json +4 -0
  99. package/core/registry/package-templates/skill/files/SKILL.md +8 -0
  100. package/core/registry/package-templates/skill/template.json +4 -0
  101. package/core/registry/package-templates/worker/files/worker.json +92 -0
  102. package/core/registry/package-templates/worker/template.json +11 -0
  103. package/core/registry/packages/doflow/doflow-package.json +15 -0
  104. package/core/registry/packages/doflow/presets/debate-3.json +26 -0
  105. package/core/registry/protocols.json +61 -0
  106. package/core/registry/routes.json +17 -0
  107. package/core/registry/rules.json +431 -0
  108. package/core/registry/settings.json +272 -0
  109. package/core/registry/storage.json +687 -31
  110. package/core/registry/telemetry.json +236 -0
  111. package/core/registry/transcripts.json +142 -0
  112. package/core/registry/verification.json +76 -6
  113. package/core/registry/workflows.json +55 -0
  114. package/core/shared/agent-specs/core-implementer.md +3 -0
  115. package/core/shared/agent-specs/delivery-lead.md +68 -0
  116. package/core/shared/agent-specs/quality-guardian.md +5 -1
  117. package/core/shared/agent-specs/research-writer.md +10 -3
  118. package/core/shared/agent-specs/reviewer.md +53 -0
  119. package/core/shared/agent-specs/spec-analyst.md +5 -6
  120. package/core/shared/agent-specs/system-architect.md +5 -1
  121. package/core/shared/guidance/DOFLOW_CORE.md +1 -5
  122. package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
  123. package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
  124. package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
  125. package/core/shared/guidance/pointers/codex.md +1 -4
  126. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
  127. package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
  128. package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
  129. package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
  130. package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
  131. package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
  132. package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
  133. package/core/shared/guidance/references/ORCHESTRATION.md +3 -2
  134. package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +19 -1
  135. package/core/shared/guidance/rules/RULES.md +45 -0
  136. package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
  137. package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
  138. package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
  139. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
  140. package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
  141. package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
  142. package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
  143. package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
  144. package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
  145. package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
  146. package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
  147. package/core/shared/scripts/doflow/bash/validate-artifacts.sh +49 -5
  148. package/core/shared/scripts/doflow/bin/doflow-run +152 -34
  149. package/core/shared/skills/do/SKILL.md +14 -12
  150. package/core/shared/skills/do/references/advice_calls.md +26 -0
  151. package/core/shared/skills/do/references/pm_routing.md +1 -2
  152. package/core/shared/skills/do/references/program_mode.md +31 -0
  153. package/core/shared/skills/do-brainstorm/SKILL.md +45 -97
  154. package/core/shared/skills/do-brainstorm/references/handoff_branches.md +36 -0
  155. package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
  156. package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
  157. package/core/shared/skills/do-code-review/SKILL.md +48 -148
  158. package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
  159. package/core/shared/skills/do-code-review/references/tools.md +90 -0
  160. package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
  161. package/core/shared/skills/do-design/SKILL.md +23 -20
  162. package/core/shared/skills/do-diagnose/SKILL.md +18 -6
  163. package/core/shared/skills/do-document/SKILL.md +12 -1
  164. package/core/shared/skills/do-document/references/deep_research.md +1 -2
  165. package/core/shared/skills/do-document/references/embedded-research.md +19 -7
  166. package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
  167. package/core/shared/skills/do-execute-plan/SKILL.md +19 -60
  168. package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
  169. package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
  170. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +26 -1
  171. package/core/shared/skills/do-execute-plan/references/run_position.md +16 -2
  172. package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
  173. package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
  174. package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
  175. package/core/shared/skills/do-flow/SKILL.md +10 -11
  176. package/core/shared/skills/do-flow/references/task_classes.md +5 -5
  177. package/core/shared/skills/do-git/SKILL.md +4 -0
  178. package/core/shared/skills/do-implement/SKILL.md +13 -14
  179. package/core/shared/skills/do-plan/SKILL.md +34 -20
  180. package/core/shared/skills/do-test/SKILL.md +7 -4
  181. package/core/shared/templates/doflow/constitution-template.md +9 -3
  182. package/core/shared/templates/doflow/data-model-template.md +1 -1
  183. package/core/shared/templates/doflow/design-template.md +113 -16
  184. package/core/shared/templates/doflow/plan-template.md +11 -10
  185. package/core/shared/templates/doflow/question-log-template.md +14 -2
  186. package/core/shared/templates/doflow/requirement-template.md +1 -1
  187. package/core/shared/workflows/do-flow-chain.md +2 -0
  188. package/core/worker-evidence/antigravity/checks.json +14 -0
  189. package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
  190. package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
  191. package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
  192. package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
  193. package/core/worker-evidence/codex/checks.json +38 -0
  194. package/package.json +10 -6
  195. package/src/adapters/antigravity/index.js +143 -66
  196. package/src/adapters/claude/index.js +46 -5
  197. package/src/adapters/codex/agents.js +25 -15
  198. package/src/adapters/codex/hooks.js +39 -3
  199. package/src/adapters/codex/index.js +61 -13
  200. package/src/adapters/codex/native-projection.js +6 -2
  201. package/src/adapters/copilot/index.js +58 -23
  202. package/src/adapters/copy-tree.js +291 -46
  203. package/src/adapters/guidance-manifest.js +71 -0
  204. package/src/adapters/guidance-pointer.js +19 -0
  205. package/src/adapters/hook-commands.js +6 -10
  206. package/src/adapters/index.js +1 -0
  207. package/src/adapters/kiro/index.js +23 -12
  208. package/src/adapters/opencode/index.js +8 -8
  209. package/src/adapters/pi/hooks.js +2 -1
  210. package/src/adapters/pi/index.js +14 -7
  211. package/src/adapters/resolve-source.js +26 -2
  212. package/src/cli/commands/install.js +2 -1
  213. package/src/cli/commands/reconcile.js +53 -9
  214. package/src/cli/commands/remove.js +2 -2
  215. package/src/cli/commands/rollback.js +6 -3
  216. package/src/cli/commands/self-update.js +18 -9
  217. package/src/cli/commands/status.js +22 -3
  218. package/src/cli/commands/ui.js +99 -0
  219. package/src/cli/commands/update.js +2 -1
  220. package/src/cli/index.js +337 -32
  221. package/src/cli/runtime-commands.js +220 -60
  222. package/src/cli/shared.js +0 -2
  223. package/src/helper/git.js +1 -1
  224. package/src/helper/guardrails.js +1 -1
  225. package/src/helper/marker-merge.js +31 -13
  226. package/src/helper/package-ids.js +37 -0
  227. package/src/helper/semver.js +94 -0
  228. package/src/install/backup.js +105 -15
  229. package/src/install/context.js +1 -1
  230. package/src/install/targets.js +31 -16
  231. package/src/lifecycle/coverage.js +4 -16
  232. package/src/lifecycle/hook-tier.js +160 -0
  233. package/src/lifecycle/index.js +412 -78
  234. package/src/lifecycle/retired-harnesses.js +318 -0
  235. package/src/lifecycle/view.js +35 -15
  236. package/src/registry/index.js +551 -26
  237. package/src/registry/worker-ids.js +45 -0
  238. package/src/release/build-package.js +0 -3
  239. package/src/release/docs-blocks.js +472 -0
  240. package/src/release/preflight.js +425 -0
  241. package/src/release/rehearse.js +401 -0
  242. package/src/release/release-workflow.js +733 -0
  243. package/src/release/sync-plugin-versions.js +43 -7
  244. package/src/runtime/advice/bench-gate.js +25 -0
  245. package/src/runtime/advice/cli.js +59 -16
  246. package/src/runtime/advice/client.js +32 -9
  247. package/src/runtime/advice/config.js +98 -133
  248. package/src/runtime/advice/doctor.js +1 -1
  249. package/src/runtime/advice/elevation.js +97 -0
  250. package/src/runtime/advice/endpoint-guard.js +9 -3
  251. package/src/runtime/advice/mode.js +37 -0
  252. package/src/runtime/advice/record.js +18 -5
  253. package/src/runtime/advice/registry.js +4 -1
  254. package/src/runtime/advice/shadow-config.js +229 -0
  255. package/src/runtime/advice/shadow-dispatcher.js +276 -0
  256. package/src/runtime/brief-rules.js +81 -0
  257. package/src/runtime/brief-tools.js +111 -34
  258. package/src/runtime/candidate.js +20 -11
  259. package/src/runtime/capabilities/executor.js +450 -0
  260. package/src/runtime/capabilities/loopback.js +41 -0
  261. package/src/runtime/capabilities/sanitizer.js +88 -0
  262. package/src/runtime/capability-router.js +135 -3
  263. package/src/runtime/checkouts.js +73 -11
  264. package/src/runtime/claims.js +13 -7
  265. package/src/runtime/cli.js +196 -19
  266. package/src/runtime/config-home.js +42 -0
  267. package/src/runtime/context/arbitrate.js +767 -0
  268. package/src/runtime/context/delivery-store.js +4 -6
  269. package/src/runtime/context/emission-rules.js +118 -0
  270. package/src/runtime/context/identity.js +227 -0
  271. package/src/runtime/context/pointer.js +67 -12
  272. package/src/runtime/context/primary-command.js +117 -0
  273. package/src/runtime/context/primary.js +344 -0
  274. package/src/runtime/context/summary-store.js +655 -0
  275. package/src/runtime/context-pack.js +158 -79
  276. package/src/runtime/decision-register.js +13 -4
  277. package/src/runtime/dispatch/admission.js +392 -0
  278. package/src/runtime/dispatch/attempt-end.js +274 -0
  279. package/src/runtime/dispatch/command.js +628 -0
  280. package/src/runtime/dispatch/controls.js +904 -0
  281. package/src/runtime/dispatch/envelope.js +276 -0
  282. package/src/runtime/dispatch/grants.js +310 -0
  283. package/src/runtime/dispatch/integrate.js +617 -0
  284. package/src/runtime/dispatch/loop.js +1835 -0
  285. package/src/runtime/dispatch/store.js +352 -0
  286. package/src/runtime/dispatch/verify-run.js +201 -0
  287. package/src/runtime/evidence-ledger.js +52 -7
  288. package/src/runtime/failure/capture.js +3 -1
  289. package/src/runtime/failure/cli.js +25 -9
  290. package/src/runtime/failure/home.js +57 -30
  291. package/src/runtime/failure/store.js +7 -2
  292. package/src/runtime/feature-pointer.js +174 -0
  293. package/src/runtime/feature-resolve.js +13 -2
  294. package/src/runtime/fleet/command.js +77 -0
  295. package/src/runtime/fleet/projects.js +178 -0
  296. package/src/runtime/fleet/tree.js +383 -0
  297. package/src/runtime/freshness.js +33 -3
  298. package/src/runtime/git-memo.js +46 -0
  299. package/src/runtime/guidance/ablation.js +427 -0
  300. package/src/runtime/health.js +624 -18
  301. package/src/runtime/history-compactor.js +13 -3
  302. package/src/runtime/implementation-gate.js +281 -26
  303. package/src/runtime/intake/engine.js +677 -0
  304. package/src/runtime/intake/index.js +136 -0
  305. package/src/runtime/intake/manifest.js +140 -0
  306. package/src/runtime/intake/taxonomy.js +206 -0
  307. package/src/runtime/inventory/coverage.js +1 -1
  308. package/src/runtime/inventory/index.js +1 -1
  309. package/src/runtime/inventory/precedence.js +2 -2
  310. package/src/runtime/inventory/siblings.js +6 -4
  311. package/src/runtime/knowledge/code-corpus.js +277 -0
  312. package/src/runtime/knowledge/code-lexical.js +130 -0
  313. package/src/runtime/knowledge/code-retrieval.js +307 -0
  314. package/src/runtime/knowledge/code-validation.js +106 -0
  315. package/src/runtime/knowledge/gp3-gate.js +74 -0
  316. package/src/runtime/knowledge/graph-adapter.js +288 -0
  317. package/src/runtime/knowledge/graph-fallback.js +394 -0
  318. package/src/runtime/knowledge/hybrid.js +136 -8
  319. package/src/runtime/knowledge/retrieval.js +34 -1
  320. package/src/runtime/knowledge/rtk-compress.js +77 -0
  321. package/src/runtime/knowledge/semble-bridge.py +106 -0
  322. package/src/runtime/knowledge/semble-lifecycle.js +114 -0
  323. package/src/runtime/knowledge/semble-provider.js +365 -0
  324. package/src/runtime/lifecycle/cli.js +88 -10
  325. package/src/runtime/lifecycle/event-store.js +8 -6
  326. package/src/runtime/lifecycle/followup.js +38 -7
  327. package/src/runtime/lifecycle/overview.js +26 -2
  328. package/src/runtime/lifecycle/report-store.js +8 -10
  329. package/src/runtime/lifecycle/retention.js +95 -31
  330. package/src/runtime/lifecycle/root.js +9 -1
  331. package/src/runtime/lifecycle/status.js +34 -17
  332. package/src/runtime/lifecycle/store-upkeep.js +56 -7
  333. package/src/runtime/mask.js +134 -7
  334. package/src/runtime/mcp-consultations.js +40 -6
  335. package/src/runtime/model-router.js +79 -7
  336. package/src/runtime/models/catalogue.js +191 -0
  337. package/src/runtime/models/command.js +153 -0
  338. package/src/runtime/off-words.js +12 -0
  339. package/src/runtime/outcome.js +2 -2
  340. package/src/runtime/packages/command.js +1009 -0
  341. package/src/runtime/packages/contributions.js +116 -0
  342. package/src/runtime/packages/digest.js +132 -0
  343. package/src/runtime/packages/fetch.js +182 -0
  344. package/src/runtime/packages/grants.js +192 -0
  345. package/src/runtime/packages/index-file.js +134 -0
  346. package/src/runtime/packages/instruction.js +90 -0
  347. package/src/runtime/packages/manifest.js +331 -0
  348. package/src/runtime/packages/points.js +487 -0
  349. package/src/runtime/packages/programs.js +216 -0
  350. package/src/runtime/packages/scaffold.js +277 -0
  351. package/src/runtime/packages/source.js +90 -0
  352. package/src/runtime/packages/state.js +164 -0
  353. package/src/runtime/packages/store.js +449 -0
  354. package/src/runtime/packages/tar.js +106 -0
  355. package/src/runtime/packages/view.js +406 -0
  356. package/src/runtime/plan-check.js +59 -0
  357. package/src/runtime/plan-content.js +324 -0
  358. package/src/runtime/policy-init.js +118 -0
  359. package/src/runtime/profiles/doctor.js +96 -0
  360. package/src/runtime/profiles/resolve.js +244 -0
  361. package/src/runtime/profiles/rollback.js +254 -0
  362. package/src/runtime/profiles/sampler.js +41 -0
  363. package/src/runtime/readiness-record.js +16 -11
  364. package/src/runtime/readiness.js +46 -5
  365. package/src/runtime/research-request.js +50 -25
  366. package/src/runtime/retrieval-plan.js +10 -30
  367. package/src/runtime/retrieval-record.js +75 -5
  368. package/src/runtime/review/command.js +246 -0
  369. package/src/runtime/review/dominance.js +68 -0
  370. package/src/runtime/review/findings.js +434 -0
  371. package/src/runtime/review/identity.js +255 -0
  372. package/src/runtime/review/protocol-command.js +109 -0
  373. package/src/runtime/review/protocols.js +405 -0
  374. package/src/runtime/review/resolve-protocol.js +302 -0
  375. package/src/runtime/review/start.js +530 -0
  376. package/src/runtime/roadmap/attendance.js +137 -0
  377. package/src/runtime/roadmap/facts.js +479 -0
  378. package/src/runtime/roadmap/gates.js +273 -0
  379. package/src/runtime/roadmap/loop.js +1719 -0
  380. package/src/runtime/roadmap/merge.js +390 -0
  381. package/src/runtime/roadmap/rows.js +1122 -0
  382. package/src/runtime/roadmap/seed.js +1063 -0
  383. package/src/runtime/roadmap/worktree-cleanup.js +147 -0
  384. package/src/runtime/rules-report.js +336 -0
  385. package/src/runtime/scaffold/artifacts.js +60 -9
  386. package/src/runtime/scaffold/generate.js +21 -9
  387. package/src/runtime/settings/autopilot.js +106 -0
  388. package/src/runtime/settings/cli.js +280 -0
  389. package/src/runtime/settings/config-write.js +314 -0
  390. package/src/runtime/settings/files.js +271 -0
  391. package/src/runtime/settings/legacy.js +154 -0
  392. package/src/runtime/settings/lexical.js +56 -0
  393. package/src/runtime/settings/prepared-run.js +269 -0
  394. package/src/runtime/settings/project-layers.js +118 -0
  395. package/src/runtime/settings/refs.js +288 -0
  396. package/src/runtime/settings/registry.js +553 -0
  397. package/src/runtime/settings/resolve.js +713 -0
  398. package/src/runtime/settings/shared.js +37 -0
  399. package/src/runtime/settings/worker-billing.js +107 -0
  400. package/src/runtime/settings/write.js +269 -0
  401. package/src/runtime/slot-command.js +78 -0
  402. package/src/runtime/slots.js +671 -0
  403. package/src/runtime/source-version.js +1 -0
  404. package/src/runtime/state-root.js +217 -0
  405. package/src/runtime/storage/cli.js +12 -5
  406. package/src/runtime/storage/engine.js +9 -2
  407. package/src/runtime/storage/registry.js +14 -1
  408. package/src/runtime/storage/roots.js +66 -16
  409. package/src/runtime/storage/rules.js +82 -10
  410. package/src/runtime/storage/upkeep.js +10 -2
  411. package/src/runtime/storage/work-status.js +245 -26
  412. package/src/runtime/task-attempts.js +589 -69
  413. package/src/runtime/task-classifier.js +85 -14
  414. package/src/runtime/task-graph.js +373 -0
  415. package/src/runtime/task-run.js +778 -0
  416. package/src/runtime/task-scope.js +176 -55
  417. package/src/runtime/task-sources.js +147 -0
  418. package/src/runtime/task-state.js +373 -64
  419. package/src/runtime/team/admission.js +301 -0
  420. package/src/runtime/team/envelope.js +324 -0
  421. package/src/runtime/team/review-guard.js +156 -0
  422. package/src/runtime/trace/analyze.js +427 -0
  423. package/src/runtime/trace/baseline.js +770 -0
  424. package/src/runtime/trace/ledger.js +63 -140
  425. package/src/runtime/trace/otlp-export.js +568 -0
  426. package/src/runtime/trace/otlp-map.js +481 -0
  427. package/src/runtime/trace/spans.js +745 -0
  428. package/src/runtime/trace/telemetry.js +225 -0
  429. package/src/runtime/trace/views.js +1 -0
  430. package/src/runtime/transcripts/command.js +98 -0
  431. package/src/runtime/transcripts/cursor.js +326 -0
  432. package/src/runtime/transcripts/read.js +126 -0
  433. package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
  434. package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
  435. package/src/runtime/transcripts/readers/copilot-events.js +90 -0
  436. package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
  437. package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
  438. package/src/runtime/transcripts/registry.js +161 -0
  439. package/src/runtime/transcripts/stats.js +161 -0
  440. package/src/runtime/verb-view.js +267 -0
  441. package/src/runtime/verification/check-key.js +628 -0
  442. package/src/runtime/verification/contract-runner.js +546 -11
  443. package/src/runtime/verification/engine.js +499 -54
  444. package/src/runtime/verification/impact.js +617 -0
  445. package/src/runtime/verification/inflight.js +205 -0
  446. package/src/runtime/verification/registry.js +176 -0
  447. package/src/runtime/verification/reuse.js +184 -0
  448. package/src/runtime/verification/telemetry-emit.js +254 -0
  449. package/src/runtime/verification/test-selection.js +67 -7
  450. package/src/runtime/worker/active.js +90 -0
  451. package/src/runtime/worker/cancel.js +222 -0
  452. package/src/runtime/worker/catalogue.js +122 -0
  453. package/src/runtime/worker/command.js +677 -0
  454. package/src/runtime/worker/evidence.js +288 -0
  455. package/src/runtime/worker/launch.js +256 -0
  456. package/src/runtime/worker/models.js +137 -0
  457. package/src/runtime/worker/parsers.js +218 -0
  458. package/src/runtime/worker/probes/index.js +706 -0
  459. package/src/runtime/worker/process-tree.js +415 -0
  460. package/src/runtime/worker/quota.js +43 -0
  461. package/src/runtime/worker/readiness.js +713 -0
  462. package/src/runtime/worker/run-store.js +166 -0
  463. package/src/runtime/worker/supervisor.js +541 -0
  464. package/src/runtime/worker/worktree-check.js +252 -0
  465. package/src/runtime/workflow-engine.js +5 -1
  466. package/src/runtime/workflow-orchestrator.js +610 -67
  467. package/src/runtime/worktree.js +149 -22
  468. package/src/state/index.js +1 -1
  469. package/src/state/lockfile.js +64 -10
  470. package/src/state/run-lock.js +1 -1
  471. package/types/node-commonjs.d.ts +106 -7
  472. package/core/harnesses/claude/hooks/pre-bash-guard.sh +0 -10
  473. package/core/harnesses/codex/hooks/pre-bash-guard.sh +0 -10
  474. package/core/harnesses/gemini/hooks/hooks.json +0 -77
  475. package/core/harnesses/gemini/hooks/lib.sh +0 -302
  476. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -10
  477. package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
  478. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -10
  479. package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
  480. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -10
  481. package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
  482. package/core/harnesses/gemini/hooks/session-end.sh +0 -5
  483. package/core/harnesses/gemini/hooks/session-start.sh +0 -7
  484. package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
  485. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +0 -24
  486. package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +0 -91
  487. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +0 -378
  488. package/core/shared/guidance/FLAGS.md +0 -25
  489. package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
  490. package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
  491. package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
  492. package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
  493. package/src/adapters/gemini/hooks.js +0 -251
  494. package/src/adapters/gemini/index.js +0 -429
@@ -24,20 +24,31 @@ const path = require('node:path');
24
24
  const crypto = require('node:crypto');
25
25
  const { updateTaskState, readTaskState } = require('./task-state.js');
26
26
  const { parsePlan } = require('./scaffold/artifacts.js');
27
+ const { buildGraph, planVerdict, deriveTasks, isGraphPlan, doneOf, normalizeOwner } = require('./task-graph.js');
28
+ const {
29
+ runKeyFor, refuseWorker, computeTotals, aggregateRefusal, controlRefusal, epochRefusal,
30
+ refreshRun, ensureRun, identityRefusal, startRun, pauseRun, cancelRun, cancelAttempt,
31
+ clearedCancels, resumeRun, inspectRun,
32
+ } = require('./task-run.js');
33
+ const { measureSources } = require('./task-sources.js');
34
+ const { findWorkTreeTop } = require('./source-version.js');
27
35
  const { EvidenceLedger } = require('./evidence-ledger.js');
28
36
  const { projectRoot: lifecycleProjectRoot } = require('./lifecycle/root.js');
29
37
  const { isSafeSlug } = require('./task-scope.js');
30
38
 
31
39
  const SCHEMA_VERSION = 1;
32
40
  const MODES = Object.freeze(['manual', 'sequential', 'subagent']);
33
- const RESULTS = Object.freeze(['COMPLETED', 'BLOCKED', 'ABANDONED', 'INCONCLUSIVE']);
34
- const ACTIONS = Object.freeze(['claim', 'finish', 'release', 'status']);
41
+ const RESULTS = Object.freeze(['COMPLETED', 'BLOCKED', 'ABANDONED', 'INCONCLUSIVE', 'FAILED']);
42
+ const USAGE_BASES = Object.freeze(['measured', 'estimated']);
43
+ const RUN_CONTROLS = Object.freeze(['running', 'paused', 'cancelled']);
44
+ const ACTIONS = Object.freeze(['claim', 'finish', 'release', 'status', 'start', 'pause', 'cancel', 'resume', 'inspect', 'reopen', 'report-usage']);
35
45
  const TASK_ID = /^[A-Za-z]+\.\d+$/;
36
46
  const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._ -]*$/;
37
47
 
38
48
  /** A failure that is the caller's or the environment's, not a finding: exit 2. */
39
49
  class TaskAttemptError extends Error {
40
50
 
51
+
41
52
  constructor(code , message ) {
42
53
  super(message);
43
54
  this.name = 'TaskAttemptError';
@@ -62,11 +73,12 @@ function assertSlug(slug ) {
62
73
  // ── plan.md ────────────────────────────────────────────────────────────────────────────────────
63
74
 
64
75
  /** The hash a claim records and `finish` compares: everything about the task's plan line except
65
- * its checkbox, so ticking the box after a finish is not an edit of the task. */
76
+ * its checkbox, so ticking the box after a finish is not an edit of the task. A line with neither
77
+ * `acceptance:` nor `budget:` hashes exactly as before IC-002; `parallel` is never hashed. */
66
78
  function taskLineHash(task ) {
67
- const material = JSON.stringify([
68
- task.id, task.description, task.stories, task.owner, task.files, task.dependsOn, task.externalContract,
69
- ]);
79
+ const legacy = [task.id, task.description, task.stories, task.owner, task.files, task.dependsOn, task.externalContract];
80
+ const extended = (task.acceptance && task.acceptance.length) || task.budget ? [...legacy, task.acceptance, task.budget] : legacy;
81
+ const material = JSON.stringify(extended);
70
82
  return `sha256:${crypto.createHash('sha256').update(material).digest('hex')}`;
71
83
  }
72
84
 
@@ -82,24 +94,42 @@ function loadPlan({ roots, slug, fsImpl = fs } ) {
82
94
  try { text = fsImpl.readFileSync(found, 'utf8'); } catch (error ) {
83
95
  throw new TaskAttemptError('plan-unreadable', `cannot read ${found}: ${error.message}`);
84
96
  }
97
+ const verdict = planVerdict(text);
85
98
  const parsed = parsePlan(text);
86
- if (parsed.tasks.length === 0) {
87
- throw new TaskAttemptError('plan-malformed', `${found} holds no \`- [ ] <phase>.<n>\` tasks: ${parsed.gaps.map((g ) => g.why).join('; ') || 'nothing to track'}`);
88
- }
89
- const seen = new Set();
90
- for (const task of parsed.tasks) {
91
- if (seen.has(task.id)) throw new TaskAttemptError('plan-malformed', `plan task id '${task.id}' appears more than once in ${found}`);
92
- seen.add(task.id);
99
+ if (verdict.tasks === 0) {
100
+ const error = new TaskAttemptError('plan-malformed', `${found} holds no \`- [ ] <phase>.<n>\` tasks: ${parsed.gaps.map((g ) => g.why).join('; ') || 'nothing to track'}`);
101
+ error.problems = verdict.problems;
102
+ throw error;
93
103
  }
104
+ const built = buildGraph(parsed);
105
+ if (!verdict.ok) throw malformedGraph(found, verdict.problems);
94
106
  const phases = uniq(parsed.tasks.map((t ) => t.phase));
95
107
  return {
96
108
  planPath: relative.split(path.sep).join('/'),
97
109
  tasks: parsed.tasks.map((t ) => ({
98
110
  id: t.id, phase: t.phase, phaseIndex: phases.indexOf(t.phase), checked: t.done, hash: taskLineHash(t),
99
111
  })),
112
+ graph: built.graph,
113
+ warnings: built.warnings,
100
114
  };
101
115
  }
102
116
 
117
+ /** IC-003: `plan-malformed` carrying every problem. A lone `duplicate-id` keeps the pre-080 text. */
118
+ function malformedGraph(found , problems ) {
119
+ const only = problems.length === 1 ? problems[0] : null;
120
+ const message = only && only.kind === 'duplicate-id'
121
+ ? `plan task id '${only.ids[0]}' appears more than once in ${found}`
122
+ : `${found} has ${problems.length} task-graph problem${problems.length === 1 ? '' : 's'}: ${problems.map(describeProblem).join('; ')}`;
123
+ const error = new TaskAttemptError('plan-malformed', message);
124
+ error.problems = problems;
125
+ return error;
126
+ }
127
+
128
+ function describeProblem(problem ) {
129
+ const detail = problem.via ? ` via ${problem.via.join(', ')}` : problem.value !== undefined ? ` '${problem.value}'` : '';
130
+ return `${problem.kind}${problem.ids.length ? ` ${problem.ids.join(' ')}` : ''}${detail}`;
131
+ }
132
+
103
133
  // ── store ──────────────────────────────────────────────────────────────────────────────────────
104
134
 
105
135
  function unreadable(file , why ) {
@@ -119,6 +149,17 @@ function normalizeStore(data , file ) {
119
149
  throw unreadable(file, `Task '${id}' has no readable attempt list.`);
120
150
  }
121
151
  }
152
+ // IC-010: `runs` is optional and additive; the store stays version 1.
153
+ if (data.runs !== undefined) {
154
+ if (typeof data.runs !== 'object' || data.runs === null || Array.isArray(data.runs)) {
155
+ throw unreadable(file, "Its 'runs' is not an object.");
156
+ }
157
+ for (const [key, run] of (Object.entries(data.runs) )) {
158
+ if (!run || typeof run !== 'object' || Array.isArray(run)) throw unreadable(file, `Run '${key}' is not an object.`);
159
+ if (!RUN_CONTROLS.includes(run.control)) throw unreadable(file, `Run '${key}' has control '${run.control}', not one of ${RUN_CONTROLS.join(', ')}.`);
160
+ if (!Number.isInteger(run.epoch) || run.epoch < 0) throw unreadable(file, `Run '${key}' has epoch '${run.epoch}', not a non-negative integer.`);
161
+ }
162
+ }
122
163
  return data;
123
164
  }
124
165
 
@@ -138,8 +179,12 @@ function mutate({ file, fsImpl, apply } ) {
138
179
  try {
139
180
  updateTaskState({
140
181
  fsImpl, file,
182
+ // `apply` decides from the store alone, so a reclaimed writer re-runs it on the fresh store.
183
+ lostLockRetries: 2,
141
184
  build: (disk ) => {
142
- const store = normalizeStore(disk, file);
185
+ // A copy of the top level: `updateTaskState` stamps `disk.revision + 1` on what this returns, so
186
+ // bumping the disk object itself would make every write advance the revision by two.
187
+ const store = { ...normalizeStore(disk, file) };
143
188
  store.revision = (Number.isInteger(store.revision) ? store.revision : 0) + 1;
144
189
  outcome = apply(store);
145
190
  if (!outcome.write) throw new NoWrite();
@@ -149,6 +194,8 @@ function mutate({ file, fsImpl, apply } ) {
149
194
  } catch (error ) {
150
195
  if (error instanceof NoWrite) return outcome.result;
151
196
  if (error instanceof TaskAttemptError) throw error;
197
+ // Contention, not damage: the store is intact and a later run decides afresh.
198
+ if (error?.code === 'lock-lost') throw new TaskAttemptError('lock-lost', error.message);
152
199
  throw unreadable(file, error.message);
153
200
  }
154
201
  return outcome.result;
@@ -160,11 +207,18 @@ function entryFor(store , taskId ) {
160
207
  }
161
208
 
162
209
  const activeAttempt = (entry ) => (entry ? entry.attempts.find((a ) => a.state === 'active') || null : null);
163
- const doneAttempt = (entry ) => (entry ? entry.attempts.find((a ) => a.state === 'finished' && a.result === 'COMPLETED') || null : null);
210
+ // DEC-016 done-attempt-selection: the highest-token finished COMPLETED attempt, as deriveTasks reads it.
211
+ const doneAttempt = (entry ) => (entry ? doneOf(entry.attempts) : null);
164
212
 
165
- function latestFinished(entry ) {
166
- const finished = entry ? entry.attempts.filter((a ) => a.state === 'finished') : [];
167
- return finished.length ? finished.reduce((a , b ) => (b.token > a.token ? b : a)) : null;
213
+ function planHashes(plan ) {
214
+ return Object.fromEntries(plan.tasks.map((t ) => [t.id, t.hash]));
215
+ }
216
+
217
+ /** IC-005 over the store, with each task's current line hash from `loadPlan` (done-stale, IC-024)
218
+ * and the cancels an acknowledged resume cleared (row 4, IC-015). Pure, so it may run inside the
219
+ * lock. A store with no cancelled attempt (every `8ce5ad7` store) clears nothing. */
220
+ function derivePlan(plan , store ) {
221
+ return deriveTasks(plan.graph, store, { hashes: planHashes(plan), clearedCancels: clearedCancels(store) });
168
222
  }
169
223
 
170
224
  // ── evidence ───────────────────────────────────────────────────────────────────────────────────
@@ -220,31 +274,164 @@ function rootsFor({ cwd, lifecycleRoot } ) {
220
274
  return uniq([path.resolve(cwd), lifecycleRoot]);
221
275
  }
222
276
 
223
- function claim({ file, fsImpl, roots, slug, task, owner, mode, now = new Date() } ) {
277
+ /** The caller's run record, or null. Own keys only, so a run key never reaches the prototype. */
278
+ function runOf(store , runKey ) {
279
+ return store.runs && Object.prototype.hasOwnProperty.call(store.runs, runKey) ? store.runs[runKey] : null;
280
+ }
281
+
282
+ /** IC-026: `--exec-worktree` made absolute against `cwd`, then lifted to its worktree top. A path
283
+ * that is not an existing directory is exit 2 `usage` (DEC-023 exec-worktree-directory). Runs
284
+ * before the lock. */
285
+ function resolveExecWorktree(execWorktree , cwd , fsImpl ) {
286
+ if (!given(execWorktree)) return null;
287
+ const absolute = path.resolve(cwd, String(execWorktree));
288
+ if (String(execWorktree).trim() === '' || !fsImpl.existsSync(absolute)) {
289
+ throw new TaskAttemptError('usage', `--exec-worktree '${execWorktree}' does not exist`);
290
+ }
291
+ if (!fsImpl.statSync(absolute).isDirectory()) {
292
+ throw new TaskAttemptError('usage', `--exec-worktree '${execWorktree}' is not a directory`);
293
+ }
294
+ return findWorkTreeTop(absolute).top ?? absolute;
295
+ }
296
+
297
+ function wholeNumber(value , flag , min = 0) {
298
+ if (!given(value)) return null;
299
+ if (!Number.isInteger(value) || value < min) throw new TaskAttemptError('usage', `${flag} expects a whole number of at least ${min}, got '${value}'`);
300
+ return value;
301
+ }
302
+
303
+ /** Synchronous wait for the IC-021 test-only seam; never reached from the CLI. */
304
+ function sleepSync(ms ) {
305
+ if (ms > 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
306
+ }
307
+
308
+ /**
309
+ * IC-006 steps 3 to 12 against `store`, in the contract's order. Reads only: returns a refusal
310
+ * result, or `{ ok }` carrying what the write needs. For a plan without the header (DEC-008) steps
311
+ * 6, 8 and 9 do not apply and steps 3, 4, 5 and 12 answer exactly as before 080.
312
+ */
313
+ function claimChecks(store , c ) {
314
+ const { plan, graphPlan, task, owner, override, key, epoch, reserveTokens } = c;
315
+ const refuse = (reason , extra = {}) => ({ refused: { disposition: 'refused', taskId: task, reason, ...extra } });
316
+ const target = plan.tasks.find((t ) => t.id === task);
317
+ if (!target) return refuse('unknown-task');
318
+ const record = derivePlan(plan, store).find((r ) => r.id === task);
319
+ if (record.state === 'SUCCEEDED') return refuse('already-done');
320
+ const active = activeAttempt(store.tasks[task]);
321
+ if (active) return refuse('active-attempt', { activeToken: active.token, owner: active.owner, startedAt: active.startedAt });
322
+ if (graphPlan) {
323
+ const worker = refuseWorker(key.identity);
324
+ if (worker) return refuse(worker.reason);
325
+ }
326
+ const run = runOf(store, key.runKey);
327
+ const control = controlRefusal(run, key.runKey);
328
+ if (control) { const { reason, ...extra } = control; return refuse(reason, extra); }
329
+ if (graphPlan) {
330
+ const stale = epochRefusal(run, key.runKey, epoch);
331
+ if (stale) { const { reason, ...extra } = stale; return refuse(reason, extra); }
332
+ }
333
+ const declared = plan.graph.tasks.find((t ) => t.id === task).owner;
334
+ let ownerOverrideRecord = null;
335
+ if (graphPlan && declared && normalizeOwner(owner) !== normalizeOwner(declared)) {
336
+ if (!override) return refuse('owner-mismatch', { declared });
337
+ ownerOverrideRecord = { declared, reason: override };
338
+ }
339
+ const { limit, used } = record.budget;
340
+ if (used >= limit) return refuse('budget-exhausted', { scope: 'task', limit, used });
341
+ if (record.state === 'CANCELLED') {
342
+ const cancelled = store.tasks[task].attempts.filter((a ) => a.state === 'cancelled')
343
+ .reduce((a , b ) => (b.token > a.token ? b : a));
344
+ return refuse('cancelled', { cancelledToken: cancelled.token, clear: 'resume' });
345
+ }
346
+ const readiness = record.readiness;
347
+ if (readiness.reason === 'dependency-held') return refuse('dependency-held', { held: readiness.held });
348
+ if (readiness.reason === 'unmet-dependency') return refuse('unmet-dependency', { unmet: readiness.unmet });
349
+ if (readiness.reason === 'unmet-phase') return refuse('unmet-phase', graphPlan ? { unmet: readiness.unmet, held: readiness.held } : { unmet: readiness.unmet });
350
+ const aggregate = aggregateRefusal(run, computeTotals(store, key.runKey), plan.graph, reserveTokens);
351
+ if (aggregate) { const { reason, ...extra } = aggregate; return refuse(reason, extra); }
352
+ return { ok: { target, ownerOverrideRecord } };
353
+ }
354
+
355
+ /**
356
+ * The claim's write (IC-006 "on success"): the attempt with IC-010's run fields, a legacy-plan run
357
+ * record created if absent (a graph plan's run exists, step 8), and the run's recorded graph
358
+ * (IC-004) and totals (IC-009) refreshed from the attempt list. A legacy-created run gets epoch 0:
359
+ * only start and an acknowledged resume issue an epoch (DEC-007).
360
+ */
361
+ function claimWrite(store , c , ok ) {
362
+ const { plan, graphPlan, task, owner, mode, key, execWorktree, reserveTokens, now } = c;
363
+ const run = ensureRun(store, key, now);
364
+ const entry = entryFor(store, task);
365
+ const token = entry.nextToken;
366
+ entry.nextToken += 1;
367
+ const attempt = {
368
+ token, owner, mode, startedAt: now.toISOString(), taskLineHash: ok.target.hash,
369
+ state: 'active', result: null, evidenceId: null, reason: null,
370
+ ...(ok.ownerOverrideRecord ? { ownerOverride: ok.ownerOverrideRecord } : {}),
371
+ runKey: key.runKey, epoch: run.epoch, execWorktree: execWorktree ?? run.worktree,
372
+ };
373
+ if (reserveTokens !== null) attempt.reserve = { tokens: reserveTokens };
374
+ attempt.sources = c.measured.sources;
375
+ attempt.sourcesTruncated = c.measured.sourcesTruncated;
376
+ entry.attempts.push(attempt);
377
+ store.planPath = plan.planPath;
378
+ refreshRun(store, run, plan.graph);
379
+ const claimed = { disposition: 'claimed', taskId: task, token, revision: store.revision };
380
+ // A legacy plan's result keeps the 8ce5ad7 shape (FR-015); the run fields are on the attempt.
381
+ // IC-013: the unhashable or unverifiable entries are listed once, only when there are any, so a
382
+ // claim whose files all hash keeps C.2's result shape.
383
+ if (graphPlan) {
384
+ Object.assign(claimed, { runKey: key.runKey, epoch: attempt.epoch, execWorktree: attempt.execWorktree });
385
+ if (c.measured.sourcesUnverifiable.length) claimed.sourcesUnverifiable = c.measured.sourcesUnverifiable;
386
+ }
387
+ return claimed;
388
+ }
389
+
390
+ /**
391
+ * IC-006. Steps 0 to 2 before the lock (flags, graph, run key, execution worktree); steps 3 to 12
392
+ * and the write inside one `mutate()` against the store on disk, so every refusal writes nothing.
393
+ *
394
+ * `checkOutsideLock` and `delayAfterCheckMs` are the IC-021 test-only seam: the checks run against
395
+ * a plain read before the lock, then the claim waits, then writes without re-checking. It exists
396
+ * only so the race suite can prove it detects a missing lock; `handleTaskAttemptCommand` never
397
+ * passes it.
398
+ */
399
+ function claim({
400
+ file, fsImpl, roots, slug, task, owner, mode, ownerOverride, epoch, reserveTokens, execWorktree,
401
+ env = process.env, cwd = process.cwd(), now = new Date(), checkOutsideLock = false, delayAfterCheckMs = 0,
402
+ } ) {
224
403
  if (!TASK_ID.test(task || '')) throw new TaskAttemptError('usage', '--task is required: a plan task id such as A.1');
225
404
  if (!owner || !SAFE_NAME.test(owner)) throw new TaskAttemptError('usage', '--owner is required: the name of the agent or person taking the task');
226
405
  if (!MODES.includes(mode)) throw new TaskAttemptError('usage', `--mode is required: one of ${MODES.join(', ')}`);
406
+ // DEC-019: --epoch is at least 1 in-process, as on the CLI; no run ever holds epoch 0 to match.
407
+ const epochGiven = wholeNumber(epoch, '--epoch', 1);
408
+ const reserve = wholeNumber(reserveTokens, '--reserve-tokens');
227
409
  const plan = loadPlan({ roots, slug, fsImpl });
410
+ const graphPlan = isGraphPlan(plan.graph);
411
+ const override = graphPlan && given(ownerOverride) ? String(ownerOverride).trim() : null;
412
+ if (override === '') throw new TaskAttemptError('usage', '--owner-override needs a reason: why the declared owner is not taking the task');
413
+ const key = runKeyFor(env, cwd);
414
+ const exec = resolveExecWorktree(execWorktree, cwd, fsImpl);
415
+ // IC-013 / IC-006 step 2: hash the declared files before the lock, under the execution worktree
416
+ // the attempt will record (the flag, else the run's worktree, which is the key's top).
417
+ const declared = plan.graph.tasks.find((t ) => t.id === task);
418
+ const measured = measureSources({ files: declared ? declared.files : [] }, exec ?? key.top, roots);
419
+ const c = {
420
+ plan, graphPlan, task, owner, mode, override, key, epoch: epochGiven, reserveTokens: reserve,
421
+ execWorktree: exec, measured, now,
422
+ };
423
+ if (checkOutsideLock) {
424
+ const checked = claimChecks(readStore(file, fsImpl), c);
425
+ if (checked.refused) return checked.refused;
426
+ sleepSync(delayAfterCheckMs);
427
+ return mutate({ file, fsImpl, apply: (store ) => ({ write: true, result: claimWrite(store, c, checked.ok) }) });
428
+ }
228
429
  return mutate({
229
430
  file, fsImpl,
230
431
  apply: (store ) => {
231
- const refuse = (reason , extra = {}) => ({ write: false, result: { disposition: 'refused', taskId: task, reason, ...extra } });
232
- const target = plan.tasks.find((t ) => t.id === task);
233
- if (!target) return refuse('unknown-task');
234
- if (doneAttempt(store.tasks[task])) return refuse('already-done');
235
- const active = activeAttempt(store.tasks[task]);
236
- if (active) return refuse('active-attempt', { activeToken: active.token, owner: active.owner, startedAt: active.startedAt });
237
- const unmet = plan.tasks.filter((t ) => t.phaseIndex < target.phaseIndex && !doneAttempt(store.tasks[t.id])).map((t ) => t.id);
238
- if (unmet.length) return refuse('unmet-phase', { unmet });
239
- const entry = entryFor(store, task);
240
- const token = entry.nextToken;
241
- entry.nextToken += 1;
242
- entry.attempts.push({
243
- token, owner, mode, startedAt: now.toISOString(), taskLineHash: target.hash,
244
- state: 'active', result: null, evidenceId: null, reason: null,
245
- });
246
- store.planPath = plan.planPath;
247
- return { write: true, result: { disposition: 'claimed', taskId: task, token, revision: store.revision } };
432
+ const checked = claimChecks(store, c);
433
+ if (checked.refused) return { write: false, result: checked.refused };
434
+ return { write: true, result: claimWrite(store, c, checked.ok) };
248
435
  },
249
436
  });
250
437
  }
@@ -253,13 +440,40 @@ function stale(taskId , token , reason ) {
253
440
  return { write: false, result: { disposition: 'stale', taskId, token, reason } };
254
441
  }
255
442
 
256
- function finish({ file, fsImpl, roots, slug, task, token, result, evidenceId } ) {
443
+ const given = (value ) => value !== undefined && value !== null;
444
+
445
+ /** IC-007: `tokens` and `basis` together or not at all (DEC-003: caller-reported, never metered). */
446
+ function readUsage(tokens , basis ) {
447
+ if (!given(tokens) && !given(basis)) return null;
448
+ if (given(tokens) !== given(basis)) throw new TaskAttemptError('usage', '--tokens and --tokens-basis are given together or not at all');
449
+ if (!Number.isInteger(tokens) || tokens < 0) throw new TaskAttemptError('usage', `--tokens expects a whole number of at least 0, got '${tokens}'`);
450
+ if (!USAGE_BASES.includes(basis)) throw new TaskAttemptError('usage', `--tokens-basis expects one of ${USAGE_BASES.join(', ')}, got '${basis}'`);
451
+ return { tokens, basis };
452
+ }
453
+
454
+ /** The finisher's identity, read from the environment before the lock (IC-026). */
455
+ function callerIdentity(env , cwd ) {
456
+ const { resolveIdentity } = require('./context/identity.js');
457
+ return resolveIdentity(env, { cwd, sessionId: '' });
458
+ }
459
+
460
+ /** IC-007 step 1: the attempt holding `token` was cancelled. Runs before every other check. */
461
+ function cancelledAttempt(entry , token ) {
462
+ return entry ? entry.attempts.find((a ) => a.token === token && a.state === 'cancelled') || null : null;
463
+ }
464
+
465
+ function finish({ file, fsImpl, roots, slug, task, token, result, evidenceId, reason, tokens, basis, env = process.env, cwd = process.cwd(), now = new Date() } ) {
257
466
  if (!TASK_ID.test(task || '')) throw new TaskAttemptError('usage', '--task is required: a plan task id such as A.1');
258
467
  if (!Number.isInteger(token) || token < 1) throw new TaskAttemptError('usage', '--token is required: the token claim returned');
259
468
  if (!RESULTS.includes(result)) throw new TaskAttemptError('usage', `--result is required: one of ${RESULTS.join(', ')}`);
469
+ const why = given(reason) ? String(reason).trim() : '';
470
+ if (result === 'FAILED' && !why) throw new TaskAttemptError('usage', '--reason is required with --result FAILED: what failed');
471
+ const reported = readUsage(tokens, basis);
472
+ const identity = callerIdentity(env, cwd);
260
473
  return mutate({
261
474
  file, fsImpl,
262
475
  apply: (store ) => {
476
+ if (cancelledAttempt(store.tasks[task], token)) return stale(task, token, 'attempt-cancelled');
263
477
  const active = activeAttempt(store.tasks[task]);
264
478
  if (!active) return stale(task, token, 'no-active-attempt');
265
479
  if (active.token !== token) return stale(task, token, 'token-mismatch');
@@ -269,55 +483,270 @@ function finish({ file, fsImpl, roots, slug, task, token, result, evidenceId }
269
483
  if (!line) return refuse('unknown-task');
270
484
  if (line.hash !== active.taskLineHash) return refuse('plan-changed');
271
485
  if (result === 'COMPLETED') {
272
- const why = evidenceRefusal({ evidenceId, slug, taskId: task, startedAt: active.startedAt, roots, fsImpl });
273
- if (why) return refuse(why);
486
+ const refusal = evidenceRefusal({ evidenceId, slug, taskId: task, startedAt: active.startedAt, roots, fsImpl });
487
+ if (refusal) return refuse(refusal);
274
488
  }
275
489
  active.state = 'finished';
276
490
  active.result = result;
277
491
  active.evidenceId = evidenceId || null;
492
+ if (why) active.reason = why;
493
+ active.finishedAt = now.toISOString();
494
+ active.finishedBy = { role: identity.role, lineage: identity.lineage, worktree: identity.worktree };
495
+ active.usage = reported ? { ...reported, reporter: `${identity.role}:${identity.lineage}` } : null;
496
+ // IC-009: every locked write stores totals recomputed from the attempt list.
497
+ const run = active.runKey ? runOf(store, active.runKey) : null;
498
+ if (run) refreshRun(store, run, plan.graph);
278
499
  return { write: true, result: { disposition: 'applied', taskId: task, token, result, revision: store.revision } };
279
500
  },
280
501
  });
281
502
  }
282
503
 
283
- function release({ file, fsImpl, task, token, reason } ) {
504
+ function release({ file, fsImpl, roots, slug, task, token, reason, tokens, basis, env = process.env, cwd = process.cwd(), now = new Date() } ) {
284
505
  if (!TASK_ID.test(task || '')) throw new TaskAttemptError('usage', '--task is required: a plan task id such as A.1');
285
506
  if (!Number.isInteger(token) || token < 1) throw new TaskAttemptError('usage', '--token is required: the token claim returned');
286
507
  if (!reason || !String(reason).trim()) throw new TaskAttemptError('usage', '--reason is required: why the attempt is being released');
508
+ const reported = readUsage(tokens, basis);
509
+ const identity = reported ? callerIdentity(env, cwd) : null;
510
+ // DEC-020 release-without-plan: never a precondition; read before the lock, null when unloadable.
511
+ const plan = planOrNull({ roots, slug, fsImpl });
287
512
  return mutate({
288
513
  file, fsImpl,
289
514
  apply: (store ) => {
515
+ if (cancelledAttempt(store.tasks[task], token)) return stale(task, token, 'attempt-cancelled');
290
516
  const active = activeAttempt(store.tasks[task]);
291
517
  if (!active) return stale(task, token, 'no-active-attempt');
292
518
  if (active.token !== token) return stale(task, token, 'token-mismatch');
293
519
  active.state = 'released';
294
520
  active.reason = String(reason).trim();
521
+ active.endedAt = now.toISOString();
522
+ active.usage = reported ? { ...reported, reporter: `${identity.role}:${identity.lineage}` } : null;
523
+ // IC-009: every locked write stores totals recomputed from the attempt list. DEC-020: with an
524
+ // unloadable plan only the totals are refreshed; the recorded graph is kept.
525
+ const run = active.runKey ? runOf(store, active.runKey) : null;
526
+ if (run) refreshRunOrTotals(store, run, plan);
295
527
  return { write: true, result: { disposition: 'released', taskId: task, token, revision: store.revision } };
296
528
  },
297
529
  });
298
530
  }
299
531
 
300
- /** Completion read from the ledger and the plan, never from checkboxes (FR-005). */
532
+ /** DEC-020 release-without-plan: the plan when it loads, else null for a malformed or unreadable
533
+ * one. Other failures (a store problem, a bug) still throw. */
534
+ function planOrNull(args ) {
535
+ try { return loadPlan(args); } catch (error ) {
536
+ if (error instanceof TaskAttemptError && (error.code === 'plan-malformed' || error.code === 'plan-unreadable')) return null;
537
+ throw error;
538
+ }
539
+ }
540
+
541
+ /** IC-004 / IC-009 refresh with a loaded plan; with none (DEC-020), the totals only. */
542
+ function refreshRunOrTotals(store , run , plan ) {
543
+ if (plan) refreshRun(store, run, plan.graph);
544
+ else run.totals = computeTotals(store, run.runKey);
545
+ }
546
+
547
+ /**
548
+ * DEC-018 unreported-usage-recovery: `report-usage` writes caller-reported usage onto an ended
549
+ * attempt that has none, so a run under a token limit that DEC-011 refuses as
550
+ * `aggregate-unaccounted` can recover. Any plan; fenced by the token alone (like finish, IC-026),
551
+ * so a worker may report. Never overwrites. The owning run's totals are refreshed in the same
552
+ * locked write; like release (DEC-020) an unloadable plan refreshes totals only.
553
+ */
554
+ function reportUsage({ file, fsImpl, roots, slug, task, token, tokens, basis, env = process.env, cwd = process.cwd() } ) {
555
+ if (!TASK_ID.test(task || '')) throw new TaskAttemptError('usage', '--task is required: a plan task id such as A.1');
556
+ if (!Number.isInteger(token) || token < 1) throw new TaskAttemptError('usage', '--token is required: the token of the ended attempt');
557
+ if (!given(tokens) || !given(basis)) throw new TaskAttemptError('usage', '--tokens and --tokens-basis are required: the usage to record');
558
+ const reported = readUsage(tokens, basis);
559
+ const identity = callerIdentity(env, cwd);
560
+ const plan = planOrNull({ roots, slug, fsImpl });
561
+ return mutate({
562
+ file, fsImpl,
563
+ apply: (store ) => {
564
+ const refuse = (reasonCode ) => ({ write: false, result: { disposition: 'refused', taskId: task, token, reason: reasonCode } });
565
+ const entry = Object.prototype.hasOwnProperty.call(store.tasks, task) ? store.tasks[task] : null;
566
+ // A task the plan no longer lists may still hold an unreported attempt, so the ledger counts too.
567
+ const known = plan ? plan.tasks.some((t ) => t.id === task) : false;
568
+ if (!known && !entry) return refuse('unknown-task');
569
+ const attempt = entry ? entry.attempts.find((a ) => a.token === token) || null : null;
570
+ if (!attempt) return stale(task, token, 'no-attempt');
571
+ if (attempt.state === 'active') return refuse('active-attempt');
572
+ if (attempt.state === 'cancelled') return stale(task, token, 'attempt-cancelled');
573
+ if (attempt.usage) return refuse('usage-present');
574
+ attempt.usage = { ...reported, reporter: `${identity.role}:${identity.lineage}` };
575
+ const run = attempt.runKey ? runOf(store, attempt.runKey) : null;
576
+ if (run) refreshRunOrTotals(store, run, plan);
577
+ return { write: true, result: { disposition: 'usage-recorded', taskId: task, token, revision: store.revision } };
578
+ },
579
+ });
580
+ }
581
+
582
+ // ── run controls and reopen (IC-012, IC-024, IC-026; DEC-005, DEC-009) ──────────────────────────
583
+ //
584
+ // Flags, the plan and the run key are resolved before the lock; the decision and the write are one
585
+ // `mutate()`. Each identity, control and token refusal returns `write: false`.
586
+
587
+ function requireBy(by ) {
588
+ if (!by || !SAFE_NAME.test(String(by))) throw new TaskAttemptError('usage', '--by is required: the name of the agent or person acting on the run');
589
+ return String(by);
590
+ }
591
+
592
+ function requireReason(reason , what ) {
593
+ const why = given(reason) ? String(reason).trim() : '';
594
+ if (!why) throw new TaskAttemptError('usage', `--reason is required: ${what}`);
595
+ return why;
596
+ }
597
+
598
+ function requireTaskToken(task , token ) {
599
+ if (!TASK_ID.test(task || '')) throw new TaskAttemptError('usage', '--task is required: a plan task id such as A.1');
600
+ if (!Number.isInteger(token) || token < 1) throw new TaskAttemptError('usage', '--token is required: the attempt token');
601
+ }
602
+
603
+ function budgetLimit(value , flag ) {
604
+ if (!given(value)) return null;
605
+ if (!Number.isInteger(value) || value < 1) throw new TaskAttemptError('usage', `${flag} expects a whole number of at least 1, got '${value}'`);
606
+ return value;
607
+ }
608
+
609
+ /** IC-012 `start`: the caller's run with `epoch: 1`, or `run-exists`. */
610
+ function start({ file, fsImpl, roots, slug, by, budgetAttempts, budgetTokens, env = process.env, cwd = process.cwd(), now = new Date() } ) {
611
+ const who = requireBy(by);
612
+ const budget = { attempts: budgetLimit(budgetAttempts, '--budget-attempts'), tokens: budgetLimit(budgetTokens, '--budget-tokens') };
613
+ const plan = loadPlan({ roots, slug, fsImpl });
614
+ const key = runKeyFor(env, cwd);
615
+ return mutate({ file, fsImpl, apply: (store ) => startRun(store, { key, graph: plan.graph, by: who, budget, now }) });
616
+ }
617
+
618
+ /** IC-012 `pause`: claims refuse until an acknowledged resume; active attempts still finish. */
619
+ function pause({ file, fsImpl, roots, slug, by, reason, env = process.env, cwd = process.cwd(), now = new Date() } ) {
620
+ const who = requireBy(by);
621
+ const why = requireReason(reason, 'why the run is being paused');
622
+ const plan = loadPlan({ roots, slug, fsImpl });
623
+ const key = runKeyFor(env, cwd);
624
+ return mutate({ file, fsImpl, apply: (store ) => pauseRun(store, { key, graph: plan.graph, by: who, reason: why, now }) });
625
+ }
626
+
627
+ /** IC-012 `cancel`: one attempt when `task` or `token` is given, else the whole run. Marks
628
+ * attempts only; ending a worker process is 081's. */
629
+ function cancel({ file, fsImpl, roots, slug, task, token, by, reason, adoptUnowned = false, env = process.env, cwd = process.cwd(), now = new Date() } ) {
630
+ const who = requireBy(by);
631
+ const why = requireReason(reason, 'why the run or attempt is being cancelled');
632
+ const single = given(task) || given(token);
633
+ if (single) {
634
+ requireTaskToken(task, token);
635
+ if (adoptUnowned) throw new TaskAttemptError('usage', '--adopt-unowned applies to a run cancel, not to one attempt');
636
+ }
637
+ const plan = loadPlan({ roots, slug, fsImpl });
638
+ const key = runKeyFor(env, cwd);
639
+ return mutate({
640
+ file, fsImpl,
641
+ apply: (store ) => (single
642
+ ? cancelAttempt(store, { key, graph: plan.graph, task, token, by: who, reason: why, now })
643
+ : cancelRun(store, { key, graph: plan.graph, by: who, reason: why, adoptUnowned: Boolean(adoptUnowned), now })),
644
+ });
645
+ }
646
+
647
+ /** Task ids that depend on `taskId`, directly or through another dependent: an explicit
648
+ * `depends-on:` naming it, or no `depends-on:` and a later phase. */
649
+ function dependentsOf(graph , taskId ) {
650
+ const found = new Set ();
651
+ const queue = [taskId];
652
+ while (queue.length) {
653
+ const id = queue.shift() ;
654
+ const of = graph.tasks.find((t ) => t.id === id);
655
+ for (const t of graph.tasks) {
656
+ if (found.has(t.id) || t.id === taskId) continue;
657
+ const waits = t.taskDeps.length ? t.taskDeps.includes(id) : t.phaseIndex > of.phaseIndex;
658
+ if (waits) { found.add(t.id); queue.push(t.id); }
659
+ }
660
+ }
661
+ return graph.tasks.filter((t ) => found.has(t.id)).map((t ) => t.id);
662
+ }
663
+
664
+ /**
665
+ * IC-024 / DEC-009: the task's done attempt (stale or not) becomes `superseded`, so the task is no
666
+ * longer done and the attempt counts toward the task budget (IC-008). Available in every plan.
667
+ * `dependentsActive` names active attempts of dependent tasks; the caller decides about them.
668
+ */
669
+ function reopen({ file, fsImpl, roots, slug, task, token, by, reason, env = process.env, cwd = process.cwd(), now = new Date() } ) {
670
+ requireTaskToken(task, token);
671
+ const who = requireBy(by);
672
+ const why = requireReason(reason, 'why the done task is being reopened');
673
+ const plan = loadPlan({ roots, slug, fsImpl });
674
+ const key = runKeyFor(env, cwd);
675
+ return mutate({
676
+ file, fsImpl,
677
+ apply: (store ) => {
678
+ const refuse = (reasonCode , extra = {}) => ({ write: false, result: { disposition: 'refused', taskId: task, token, reason: reasonCode, ...extra } });
679
+ const identity = identityRefusal(key.identity);
680
+ if (identity) return refuse(identity.reason);
681
+ if (!plan.tasks.some((t ) => t.id === task)) return refuse('unknown-task');
682
+ const done = doneAttempt(store.tasks[task]);
683
+ if (!done) return refuse('not-done');
684
+ if (done.token !== token) return refuse('token-mismatch', { doneToken: done.token });
685
+ const existing = runOf(store, key.runKey);
686
+ if (existing && existing.control === 'cancelled') return refuse('run-cancelled', { runKey: key.runKey });
687
+ const run = ensureRun(store, key, now);
688
+ const at = now.toISOString();
689
+ done.state = 'superseded';
690
+ done.reopen = { at, by: who, reason: why, runKey: key.runKey };
691
+ run.history.push({ at, action: 'reopen', by: who, reason: why, taskId: task, token });
692
+ refreshRun(store, run, plan.graph);
693
+ const dependentsActive = dependentsOf(plan.graph, task)
694
+ .map((id ) => ({ id, active: activeAttempt(store.tasks[id]) }))
695
+ .filter((d ) => d.active)
696
+ .map((d ) => ({ taskId: d.id, token: d.active.token }));
697
+ return { write: true, result: { disposition: 'reopened', taskId: task, token, dependentsActive } };
698
+ },
699
+ });
700
+ }
701
+
702
+ const ACK = /^sha256:[0-9a-f]{64}$/;
703
+
704
+ /**
705
+ * IC-015 `resume`: the report and its digest, and with `ack` the acknowledged resume. Flags and
706
+ * the plan are checked first; `resumeRun` refuses on identity, runs the probes before any lock,
707
+ * and with `ack` makes one `mutate()`. Without `ack` the store is read with `readTaskState` and
708
+ * nothing is written (DEC-004). `by` is required with `ack`; `reason` is optional, non-empty when
709
+ * given. `gitRunner(args, cwd) => stdout | null` replaces the git process (tests).
710
+ */
711
+ function resume({ file, fsImpl, roots, slug, ack, by, reason, env = process.env, cwd = process.cwd(), now = new Date(), gitRunner = null } ) {
712
+ const acknowledged = given(ack) ? String(ack) : null;
713
+ if (acknowledged !== null && !ACK.test(acknowledged)) throw new TaskAttemptError('usage', `--ack expects the digest resume reported (sha256:<64 hex>), got '${ack}'`);
714
+ const who = acknowledged !== null ? requireBy(by) : (given(by) ? requireBy(by) : null);
715
+ const why = given(reason) ? requireReason(reason, 'why the run is being resumed') : null;
716
+ const plan = loadPlan({ roots, slug, fsImpl });
717
+ const key = runKeyFor(env, cwd);
718
+ return resumeRun(
719
+ { read: () => readStore(file, fsImpl), mutate: (apply ) => mutate({ file, fsImpl, apply }) },
720
+ { key, graph: plan.graph, hashes: planHashes(plan), roots, ack: acknowledged, by: who, reason: why, now, env, gitRunner },
721
+ );
722
+ }
723
+
724
+ /**
725
+ * IC-016 `inspect`: the plan's graph, every task's derived state, budgets with their source, and
726
+ * the caller's run with recomputed totals and `totalsMismatch`. Read-only: the store is read with
727
+ * `readStore` (`readTaskState`), no lock is taken, nothing is written and no git process runs.
728
+ */
729
+ function inspect({ file, fsImpl, roots, slug, env = process.env, cwd = process.cwd() } ) {
730
+ const plan = loadPlan({ roots, slug, fsImpl });
731
+ const key = runKeyFor(env, cwd);
732
+ const store = readStore(file, fsImpl);
733
+ return inspectRun(store, { slug, key, graph: plan.graph, hashes: planHashes(plan), warnings: plan.warnings });
734
+ }
735
+
736
+ /** Completion read from the ledger and the plan, never from checkboxes (FR-005). Each task keeps
737
+ * the pre-080 fields and adds IC-005's `state`, `reason`, `claimable`, `budget` and, where
738
+ * present, `held`, `unmet` and `doneStale`. */
301
739
  function status({ file, fsImpl, roots, slug } ) {
302
740
  const plan = loadPlan({ roots, slug, fsImpl });
303
741
  const store = readStore(file, fsImpl);
304
- const tasks = plan.tasks.map((t ) => {
305
- const entry = store.tasks[t.id];
306
- const active = activeAttempt(entry);
307
- const done = doneAttempt(entry);
308
- const last = latestFinished(entry);
309
- let derived;
310
- if (done) derived = 'done';
311
- else if (active) derived = 'running';
312
- else if (last && last.result === 'BLOCKED') derived = 'blocked';
313
- else if (plan.tasks.every((o ) => o.phaseIndex >= t.phaseIndex || doneAttempt(store.tasks[o.id]))) derived = 'ready';
314
- else derived = 'pending';
315
- return {
316
- id: t.id, phase: t.phase, derived,
317
- activeToken: active ? active.token : null,
318
- doneToken: done ? done.token : null,
319
- active: active ? { owner: active.owner, mode: active.mode, startedAt: active.startedAt } : null,
742
+ const tasks = derivePlan(plan, store).map((r ) => {
743
+ const out = {
744
+ id: r.id, phase: r.phase, derived: r.derived,
745
+ activeToken: r.activeToken, doneToken: r.doneToken, active: r.active,
746
+ state: r.state, reason: r.reason, claimable: r.claimable, budget: r.budget,
320
747
  };
748
+ for (const key of ['held', 'unmet', 'doneStale']) if (r[key] !== undefined) out[key] = r[key];
749
+ return out;
321
750
  });
322
751
  return {
323
752
  complete: tasks.length > 0 && tasks.every((t ) => t.derived === 'done'),
@@ -340,14 +769,20 @@ function checkCompletion({ slug, cwd, fsImpl = fs } ) {
340
769
  if (!fsImpl.existsSync(file)) return { state: 'none', slug, incomplete: [] };
341
770
  const answer = status({ file, fsImpl, roots: rootsFor({ cwd, lifecycleRoot }), slug });
342
771
  const incomplete = answer.tasks.filter((t ) => t.derived !== 'done').map((t ) => t.id);
772
+ // IC-024: only a graph plan has done-stale tasks (DEC-009); a legacy answer keeps its shape.
773
+ const stale = answer.tasks.filter((t ) => t.doneStale);
774
+ if (stale.length) {
775
+ const reasons = Object.fromEntries(stale.map((t ) => [t.id, 'plan-changed-after-done']));
776
+ return { state: 'incomplete', slug, incomplete, reasons };
777
+ }
343
778
  return { state: answer.complete ? 'complete' : 'incomplete', slug, incomplete };
344
779
  }
345
780
 
346
781
  // ── CLI ────────────────────────────────────────────────────────────────────────────────────────
347
782
 
348
- function failure(code , message , json ) {
783
+ function failure(code , message , json , problems ) {
349
784
  const { finishRuntime } = require('./cli-result.js');
350
- if (json) console.log(JSON.stringify({ ok: false, error: code, message }, null, 2));
785
+ if (json) console.log(JSON.stringify(problems ? { ok: false, error: code, message, problems } : { ok: false, error: code, message }, null, 2));
351
786
  else console.error(`doflow task-attempt: ${message}`);
352
787
  return finishRuntime(2);
353
788
  }
@@ -357,23 +792,89 @@ function describe(result ) {
357
792
  if (result.taskId) parts.push(`task ${result.taskId}`);
358
793
  if (result.token !== undefined) parts.push(`token ${result.token}`);
359
794
  if (result.reason) parts.push(`(${result.reason})`);
795
+ if (result.disposition === 'ack-required' || result.disposition === 'resumed' || result.reason === 'report-changed') {
796
+ if (result.digest) parts.push(`digest ${result.digest}`);
797
+ }
798
+ if (['started', 'resumed'].includes(result.disposition) && given(result.epoch)) parts.push(`epoch ${result.epoch}`);
360
799
  return parts.join(' ');
361
800
  }
362
801
 
802
+ /** IC-017: the dispositions that exit 0. `status` and `inspect` are reads and always exit 0. */
803
+ const APPLIED = Object.freeze(['claimed', 'applied', 'released', 'started', 'paused', 'cancelled', 'resumed', 'reopened', 'usage-recorded']);
804
+
805
+ /**
806
+ * IC-017: the actions each flag may be given to. The 080 flags are refused on every other action;
807
+ * the pre-080 flags are refused only on the 080 actions, so every existing invocation keeps its
808
+ * exit code (NFR-001).
809
+ */
810
+ const FLAG_ACTIONS = {
811
+ ownerOverride: { flag: '--owner-override', actions: ['claim'] },
812
+ epoch: { flag: '--epoch', actions: ['claim'] },
813
+ execWorktree: { flag: '--exec-worktree', actions: ['claim'] },
814
+ reserveTokens: { flag: '--reserve-tokens', actions: ['claim'] },
815
+ tokens: { flag: '--tokens', actions: ['finish', 'release', 'report-usage'] },
816
+ tokensBasis: { flag: '--tokens-basis', actions: ['finish', 'release', 'report-usage'] },
817
+ by: { flag: '--by', actions: ['start', 'pause', 'cancel', 'reopen', 'resume'] },
818
+ budgetAttempts: { flag: '--budget-attempts', actions: ['start'] },
819
+ budgetTokens: { flag: '--budget-tokens', actions: ['start'] },
820
+ ack: { flag: '--ack', actions: ['resume'] },
821
+ adoptUnowned: { flag: '--adopt-unowned', actions: ['cancel'] },
822
+ };
823
+ const PRE_080_ACTIONS = Object.freeze(['claim', 'finish', 'release', 'status']);
824
+ const PRE_080_FLAG_ACTIONS = {
825
+ task: { flag: '--task', actions: ['cancel', 'reopen', 'report-usage'] },
826
+ token: { flag: '--token', actions: ['cancel', 'reopen', 'report-usage'] },
827
+ reason: { flag: '--reason', actions: ['pause', 'cancel', 'reopen', 'resume'] },
828
+ owner: { flag: '--owner', actions: [] },
829
+ mode: { flag: '--mode', actions: [] },
830
+ result: { flag: '--result', actions: [] },
831
+ evidenceId: { flag: '--evidence-id', actions: [] },
832
+ };
833
+
834
+ function refuseMisplacedFlags(action , flags ) {
835
+ const check = (table ) => {
836
+ for (const [key, { flag, actions }] of Object.entries(table)) {
837
+ if (given(flags[key]) && flags[key] !== false && !actions.includes(action)) {
838
+ throw new TaskAttemptError('usage', `${flag} does not apply to --action ${action}`);
839
+ }
840
+ }
841
+ };
842
+ check(FLAG_ACTIONS);
843
+ if (!PRE_080_ACTIONS.includes(action)) check(PRE_080_FLAG_ACTIONS);
844
+ }
845
+
846
+ /** IC-017: a CLI integer is digits only, so `5abc`, `1.5` and `-1` are usage rather than whatever
847
+ * `parseInt` would make of them. A number passed in-process is checked as its decimal form. */
848
+ function digits(value , flag , min ) {
849
+ if (!given(value)) return undefined;
850
+ const text = String(value);
851
+ if (!/^[0-9]+$/.test(text) || Number(text) > Number.MAX_SAFE_INTEGER || Number(text) < min) {
852
+ throw new TaskAttemptError('usage', `${flag} expects a whole number of at least ${min}, got '${value}'`);
853
+ }
854
+ return Number(text);
855
+ }
856
+
363
857
  /**
364
- * `doflow task-attempt --action claim|finish|release|status --slug <slug>`. Exit 0 applied, 1 refused
365
- * or stale, 2 malformed input or an unreadable store.
858
+ * `doflow task-attempt --action <action> --slug <slug>` (IC-017). Exit 0 for the applied
859
+ * dispositions and every `status` or `inspect` read; 1 refused, stale or ack-required, with the
860
+ * disposition in the JSON; 2 malformed input or an unreadable store or plan.
366
861
  * @param {Object} options
367
862
  * @param {string} options.cwd the invoking checkout
368
863
  * @param {(cwd: string) => string|null} [options.resolveSlug] fallback when `slug` is not given
369
864
  */
370
865
  function handleTaskAttemptCommand({
371
866
  action = 'status', slug = null, task, owner, mode, token, result, evidenceId, reason, json = false,
867
+ ownerOverride, epoch, reserveTokens, execWorktree, tokens, tokensBasis, budgetAttempts, budgetTokens, by, ack,
868
+ adoptUnowned = false, env = process.env,
372
869
  cwd = process.cwd(), resolveSlug = null, fsImpl = fs, now = new Date(),
373
870
  } = {}) {
374
871
  const { finishRuntime } = require('./cli-result.js');
375
872
  try {
376
873
  if (!ACTIONS.includes(action)) throw new TaskAttemptError('usage', `unknown action '${action}'; valid: ${ACTIONS.join(' | ')}`);
874
+ refuseMisplacedFlags(action, {
875
+ ownerOverride, epoch, reserveTokens, execWorktree, tokens, tokensBasis, budgetAttempts, budgetTokens, by, ack, adoptUnowned,
876
+ task, token, reason, owner, mode, result, evidenceId,
877
+ });
377
878
  const feature = slug || (resolveSlug ? resolveSlug(cwd) : null);
378
879
  if (!feature) throw new TaskAttemptError('usage', '--slug is required: no feature could be resolved from the current branch');
379
880
  assertSlug(feature);
@@ -382,34 +883,53 @@ function handleTaskAttemptCommand({
382
883
  if (!/^[0-9]+$/.test(String(token))) throw new TaskAttemptError('usage', `--token expects the whole number claim returned, got '${token}'`);
383
884
  tokenNumber = Number(token);
384
885
  }
886
+ const epochNumber = digits(epoch, '--epoch', 1);
887
+ const reserveNumber = digits(reserveTokens, '--reserve-tokens', 0);
888
+ const tokensNumber = digits(tokens, '--tokens', 0);
889
+ const attemptsLimit = digits(budgetAttempts, '--budget-attempts', 1);
890
+ const tokensLimit = digits(budgetTokens, '--budget-tokens', 1);
891
+ if (given(ownerOverride) && !String(ownerOverride).trim()) throw new TaskAttemptError('usage', '--owner-override needs a reason: why the declared owner is not taking the task');
385
892
  const lifecycleRoot = lifecycleProjectRoot(cwd);
386
893
  const file = storeFile(lifecycleRoot, feature);
387
894
  const roots = rootsFor({ cwd, lifecycleRoot });
388
- const common = { file, fsImpl, roots, slug: feature };
895
+ const common = { file, fsImpl, roots, slug: feature, env, cwd, now };
896
+ const usage = { tokens: tokensNumber, basis: tokensBasis };
389
897
 
390
898
  let answer;
391
899
  switch (action) {
392
- case 'claim': answer = claim({ ...common, task, owner, mode, now }); break;
393
- case 'finish': answer = finish({ ...common, task, token: tokenNumber, result, evidenceId }); break;
394
- case 'release': answer = release({ ...common, task, token: tokenNumber, reason }); break;
900
+ case 'claim': answer = claim({ ...common, task, owner, mode, ownerOverride, epoch: epochNumber, reserveTokens: reserveNumber, execWorktree }); break;
901
+ case 'finish': answer = finish({ ...common, task, token: tokenNumber, result, evidenceId, reason, ...usage }); break;
902
+ case 'release': answer = release({ ...common, task, token: tokenNumber, reason, ...usage }); break;
903
+ case 'start': answer = start({ ...common, by, budgetAttempts: attemptsLimit, budgetTokens: tokensLimit }); break;
904
+ case 'pause': answer = pause({ ...common, by, reason }); break;
905
+ case 'cancel': answer = cancel({ ...common, task, token: tokenNumber, by, reason, adoptUnowned: Boolean(adoptUnowned) }); break;
906
+ case 'reopen': answer = reopen({ ...common, task, token: tokenNumber, by, reason }); break;
907
+ case 'report-usage': answer = reportUsage({ ...common, task, token: tokenNumber, ...usage }); break;
908
+ case 'resume': answer = resume({ ...common, ack, by, reason }); break;
909
+ case 'inspect': answer = inspect(common); break;
395
910
  default: answer = status(common);
396
911
  }
397
912
 
398
- const ok = action === 'status' || ['claimed', 'applied', 'released'].includes(answer.disposition);
913
+ const read = action === 'status' || action === 'inspect';
914
+ const ok = read || APPLIED.includes(answer.disposition);
399
915
  if (json) console.log(JSON.stringify(answer, null, 2));
400
916
  else if (action === 'status') {
401
917
  console.log(`Task attempts for ${feature}: ${answer.complete ? 'complete' : 'incomplete'}`);
402
918
  for (const t of answer.tasks) console.log(` ${t.id} ${t.derived}${t.activeToken ? ` (token ${t.activeToken}, ${t.active.owner} since ${t.active.startedAt})` : ''}`);
403
919
  if (answer.unclaimedChecked.length) console.log(`Checked in plan.md with no COMPLETED attempt: ${answer.unclaimedChecked.join(', ')}`);
920
+ } else if (action === 'inspect') {
921
+ const { formatInspect } = require('./task-run.js');
922
+ for (const line of formatInspect(answer)) console.log(line);
404
923
  } else (ok ? console.log : console.error)(describe(answer));
405
924
  return finishRuntime(ok ? 0 : 1);
406
925
  } catch (error ) {
407
- if (error instanceof TaskAttemptError) return failure(error.code, error.message, json);
926
+ if (error instanceof TaskAttemptError) return failure(error.code, error.message, json, error.problems);
408
927
  throw error;
409
928
  }
410
929
  }
411
930
 
412
931
  module.exports = {
413
- handleTaskAttemptCommand, checkCompletion, claim, finish, release, status, loadPlan, taskLineHash,
414
- storeFile, TaskAttemptError, MODES, RESULTS, ACTIONS, activeAttempt,
932
+ handleTaskAttemptCommand, checkCompletion, claim, finish, release, status, start, pause, cancel, reopen, resume, inspect, reportUsage,
933
+ loadPlan, taskLineHash,
934
+ storeFile, TaskAttemptError, MODES, RESULTS, ACTIONS, USAGE_BASES, RUN_CONTROLS, activeAttempt,
415
935
  };