@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
@@ -12,22 +12,27 @@
12
12
 
13
13
  const fs = require('node:fs');
14
14
  const path = require('node:path');
15
+ const { execFileSync } = require('node:child_process');
15
16
  const { WorkflowEngine } = require('./workflow-engine.js');
16
- const { updateTaskState, readTaskState } = require('./task-state.js');
17
+ const { updateTaskState, readTaskState, boundedAcquire } = require('./task-state.js');
18
+ const { recordIn, isSafeSlug } = require('./task-scope.js');
17
19
  const { ResearchRequestStore } = require('./research-request.js');
18
20
  const { REPO_ROOT } = require('../helper/repo-root.js');
19
21
  const { resolveActiveFeature } = require('./feature-resolve.js');
20
- const { compactDecisions } = require('./decision-register.js');
22
+ const { compactDecisions, addDecisions } = require('./decision-register.js');
21
23
  const { EvidenceLedger } = require('./evidence-ledger.js');
22
24
  const { resolveConsultations } = require('./mcp-consultations.js');
23
25
  const { mcpReach } = require('./mcp-reach.js');
24
26
  const { loadRegistry } = require('../registry/index.js');
25
27
  const { loadStorageRegistry } = require('./storage/registry.js');
26
28
  const { resolveRoots, resolveStorePath } = require('./storage/roots.js');
27
- const { checkCompletion: checkTaskAttemptCompletion } = require('./task-attempts.js');
29
+ const { checkCompletion: checkTaskAttemptCompletion, storeFile: taskAttemptStoreFile } = require('./task-attempts.js');
30
+ const { runKeyFor, refuseSolo } = require('./task-run.js');
31
+ const { projectRoot: lifecycleProjectRoot } = require('./lifecycle/root.js');
28
32
  const { appendOutcome, findPick, readWindow } = require('./advice/record.js');
29
33
  const {
30
- checkReadiness, harnessHookNote, featureSlugFor, runSlugOf, findRunFile,
34
+ checkReadiness, harnessHookNote, featureSlugFor, runSlugOf, findRunFile, orchestrationRuns, selectRunEntries,
35
+ RUN_STORE_REL, runStoreDir, readRunRecord,
31
36
  } = require('./implementation-gate.js');
32
37
 
33
38
  /** Appended to the note of an implementation stage completed with no task-attempt ledger (FR-006). */
@@ -56,10 +61,31 @@ function runState(state ) {
56
61
  return state;
57
62
  }
58
63
 
64
+ /** The stable status shape shared by the status action and the read-only list action. */
65
+ function snapshotRun(run ) {
66
+ const current = run.cursor < run.program.length ? run.program[run.cursor] : null;
67
+ const done = run.program.filter((n ) => n.status === 'completed' || n.status === 'skipped' || n.status === 'approved').length;
68
+ return {
69
+ taskId: run.taskId,
70
+ taskClass: run.taskClass,
71
+ workflowName: run.workflowName,
72
+ state: run.state,
73
+ current: current ? { type: current.type, id: current.id, kind: current.kind ?? null, agent: current.agent ?? null, mutatesSource: current.mutatesSource ?? null, readinessTemplate: current.readinessTemplate ?? null } : null,
74
+ awaitingGate: run.state === 'AWAITING_GATE' && current ? { gateId: current.id, name: current.name, prompt: current.prompt } : null,
75
+ progress: { done, total: run.program.length },
76
+ terminalStage: run.terminalStage,
77
+ revision: Number.isInteger(run.revision) ? run.revision : 0,
78
+ };
79
+ }
80
+
59
81
  const GATE_DECISIONS = Object.freeze(['approve', 'reject']);
82
+ /** IC-005: the register stage a policy approval of each gate is recorded under. */
83
+ const POLICY_GATE_STAGE = Object.freeze({ 'gate-0': 'discovery', 'gate-a': 'planning', 'gate-b': 'review' });
60
84
  /** What a completed stage established, separate from the fact it was walked past (review A1). */
61
85
  const STAGE_OUTCOMES = new Set(['passed', 'failed', 'unverified']);
62
86
  const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
87
+ /** A context-pack delivery digest, as the delivery store keys it (`packDigest`). */
88
+ const PACK_DIGEST = /^sha256:[0-9a-f]{64}$/;
63
89
  /** Why a handoff with no run and no class records nothing, as the result and the CLI line say. */
64
90
  const STANDALONE_REASON = 'no run exists for this task and no --task-class was given, so nothing was recorded and no readiness was required';
65
91
 
@@ -70,6 +96,77 @@ function assertSafeId(value , label ) {
70
96
 
71
97
  function iso(now ) { return (now ?? new Date()).toISOString(); }
72
98
 
99
+ /** The well-formed `{ taskId, digest }` items of a node's `packs`, in delivery order. */
100
+ function stagePacks(packs ) {
101
+ return (Array.isArray(packs) ? packs : [])
102
+ .filter((p ) => p && typeof p.taskId === 'string' && typeof p.digest === 'string' && ID_PATTERN.test(p.taskId) && PACK_DIGEST.test(p.digest))
103
+ .map((p ) => ({ taskId: p.taskId, digest: p.digest }));
104
+ }
105
+
106
+ /**
107
+ * Issue #9 stage 3 (FU-7j3k84): record, on the run's own record, that a context pack was handed to
108
+ * the stage active at this moment. Called by the context-pack verb after it records a delivery.
109
+ *
110
+ * The run is the pack task's own run, else its feature's run: the feature named by `slug`, else by
111
+ * this checkout's feature pointer (row 135) while the checkout is still on the branch it recorded. Only runs
112
+ * under `stateRoot` are read; the branch name alone never picks one. The first run that exists
113
+ * decides: when it is RUNNING with a stage under its cursor, that stage's node gets
114
+ * `packs: [{ taskId, digest }]`, one item per pack task (its latest delivery, moved last). Anything
115
+ * else (no run, a gate, a finished run, an unreadable or locked record) records nothing: unknown,
116
+ * never a guess. A delivery the node already ends with writes nothing, so a repeat leaves the
117
+ * run's revision alone. Never throws.
118
+ * `checkout` (`{ repoRoot, branch }`) is the caller's already-resolved checkout, so the pointer lookup
119
+ * spawns no git child of its own.
120
+ * @returns {{ runTaskId: string, stage: string }|null} where the pack was recorded
121
+ */
122
+ function recordStagePack({ stateRoot, sourceRoot = stateRoot, taskId, slug = null, digest, checkout = null, fsImpl = fs } ) {
123
+ try {
124
+ if (!ID_PATTERN.test(String(taskId ?? '')) || !PACK_DIGEST.test(String(digest ?? ''))) return null;
125
+ const storeDir = runStoreDir(stateRoot);
126
+ const locate = (runTaskId , feature ) => {
127
+ const { file, read } = recordIn({ base: storeDir, namespace: feature && feature !== runTaskId ? feature : null, name: `${runTaskId}.json`, fsImpl });
128
+ // `read` is the namespaced file when it exists, else a legacy flat file that is this feature's.
129
+ return fsImpl.existsSync(read) ? { runTaskId, file: read } : null;
130
+ };
131
+ let found = locate(taskId, isSafeSlug(slug) ? slug : null);
132
+ if (!found) {
133
+ let feature = isSafeSlug(slug) ? slug : null;
134
+ let featureRun = feature;
135
+ if (!feature) {
136
+ const active = require('./feature-pointer.js').activeEntry({ storeRoot: stateRoot, cwd: sourceRoot, checkout });
137
+ if (active && isSafeSlug(active.slug)) {
138
+ feature = active.slug;
139
+ featureRun = ID_PATTERN.test(String(active.taskId ?? '')) ? active.taskId : active.slug;
140
+ found = locate(taskId, feature);
141
+ }
142
+ }
143
+ if (!found && featureRun) found = locate(featureRun, feature);
144
+ }
145
+ if (!found) return null;
146
+ readRunRecord(fsImpl, found.file, storeDir); // the run store's guarded read: a link or FIFO is refused here
147
+ let stage = null;
148
+ updateTaskState({
149
+ fsImpl, file: found.file, acquire: (impl , target ) => boundedAcquire(impl, target, { retries: 3, waitMs: 5 }),
150
+ // The guarded check again INSIDE the lock, before the unguarded read and the write: a namespace
151
+ // folder swapped for a link since the check above is refused here (null result). A swap after
152
+ // this line and before the rename is the accepted, much narrower window.
153
+ prepare: () => { readRunRecord(fsImpl, found.file, storeDir); },
154
+ build(disk ) {
155
+ const program = Array.isArray(disk?.program) ? disk.program : [];
156
+ const node = disk?.state === 'RUNNING' && Number.isInteger(disk.cursor) ? program[disk.cursor] : null;
157
+ if (!node || node.type !== 'stage') return null;
158
+ const prior = stagePacks(node.packs);
159
+ const last = prior[prior.length - 1];
160
+ if (last && last.taskId === taskId && last.digest === digest) return null;
161
+ node.packs = [...prior.filter((p) => p.taskId !== taskId), { taskId, digest }];
162
+ stage = node.id;
163
+ return disk;
164
+ },
165
+ });
166
+ return stage ? { runTaskId: found.runTaskId, stage } : null;
167
+ } catch { return null; }
168
+ }
169
+
73
170
  /** Interleave stages with their gates: each gate sits immediately after the stage named by its
74
171
  * `afterStage`. A gate anchored to an unknown stage is a compile error here, not a silent orphan. */
75
172
  function compileProgram(workflow ) {
@@ -78,6 +175,7 @@ function compileProgram(workflow ) {
78
175
  for (const stage of workflow.stages || []) {
79
176
  nodes.push({
80
177
  type: 'stage', id: stage.id, skill: stage.skill, kind: stage.kind,
178
+ agent: stage.agent,
81
179
  mutatesSource: Boolean(stage.mutatesSource),
82
180
  readinessTemplate: stage.readinessTemplate ?? null,
83
181
  optional: Boolean(stage.optional),
@@ -98,6 +196,29 @@ function compileProgram(workflow ) {
98
196
  return nodes;
99
197
  }
100
198
 
199
+ /** The stage a loop program adds to a class that has no review stage of its own (trivial-edit). */
200
+ const LOOP_REVIEW_STAGE = Object.freeze({
201
+ id: 'review', skill: 'do-code-review', agent: 'reviewer', kind: 'review', readinessTemplate: null, optional: false,
202
+ purpose: 'Review the delivered change and report an approval status.',
203
+ });
204
+
205
+ /**
206
+ * The program the roadmap loop runs for a task class (AU-P1). The loop is unattended, so it takes
207
+ * no optional stage out of the walk: every stage is required, a class without a review stage gets
208
+ * one, and a `gate-b` follows the last review so the class's gate-b policy (core/registry/gates.json)
209
+ * can approve the merge. The feature class already has all of that. workflows.json is not changed:
210
+ * an attended `/do-implement` run keeps its gate-free program.
211
+ */
212
+ function loopWorkflow(workflow ) {
213
+ if ((workflow.gates || []).some((gate ) => gate.id === 'gate-b')) return workflow;
214
+ const stages = (workflow.stages || []).map((stage ) => ({ ...stage, optional: false }));
215
+ if (!stages.some((stage ) => stage.kind === 'review')) stages.push({ ...LOOP_REVIEW_STAGE });
216
+ const review = stages.filter((stage ) => stage.kind === 'review').at(-1);
217
+ const gate = { id: 'gate-b', name: 'Before commit or merge', afterStage: review.id, kind: 'approval', trigger: 'always',
218
+ prompt: 'Review is complete. Proceed to commit and merge?' };
219
+ return { ...workflow, stages, gates: [...(workflow.gates || []), gate] };
220
+ }
221
+
101
222
  class WorkflowOrchestrator {
102
223
 
103
224
 
@@ -106,8 +227,12 @@ class WorkflowOrchestrator {
106
227
 
107
228
 
108
229
 
230
+
109
231
 
110
232
 
233
+
234
+
235
+
111
236
  /**
112
237
  * @param {Object} [options]
113
238
  * @param {string} [options.repoRoot] install root owning core/registry + default state dir.
@@ -123,39 +248,56 @@ class WorkflowOrchestrator {
123
248
  * `none` (no ledger) records the stage as `unverified` whatever outcome was supplied. Unwired
124
249
  * means the stage is not checked, which only a unit test that builds the orchestrator by hand
125
250
  * relies on: the CLI always wires the real one.
251
+ * @param {(node: object, run: object) => {refusals: Array<{path: string, line: number, message: string}>, advisories: Array<object>}} [options.planExitCheck]
252
+ * row 134 gate for the do-plan planning stage: every refusal (a constitution MUST marked FAIL or
253
+ * without a row) refuses completion. Unwired means the stage is not checked, as for taskAttemptCheck.
126
254
  * @param {object} [options.fsImpl]
127
255
  * @param {string|null} [options.featureSlug] the feature a run started here belongs to
256
+ * @param {object} [options.env] the environment `start` reads the caller's identity and the telemetry switch from
257
+ * @param {string} [options.cwd] the directory `start` resolves the caller's run key and the 080 store from
128
258
  * @param {() => object} [options.buildAdapters] builds the adapter registry `mcpReach` reads installs
129
259
  * through; without it install facts are unknown and no outcome is inferred from them
130
260
  */
131
- constructor({ repoRoot = REPO_ROOT, projectRoot, stateDir, engine, readinessEvaluate, taskAttemptCheck, fsImpl, featureSlug = null, buildAdapters = null } = {}) {
261
+ constructor({ repoRoot = REPO_ROOT, projectRoot, sourceRoot, stateDir, engine, readinessEvaluate, taskAttemptCheck, planExitCheck, fsImpl, featureSlug = null, buildAdapters = null, env = process.env, cwd = process.cwd() } = {}) {
132
262
  this.fsImpl = fsImpl || fs;
133
263
  this.repoRoot = repoRoot;
134
- this.projectRoot = projectRoot || (stateDir?.endsWith(path.join('.doflow', 'state', 'orchestration'))
264
+ this.projectRoot = projectRoot || (stateDir?.endsWith(RUN_STORE_REL)
135
265
  ? path.resolve(stateDir, '../../..') : repoRoot);
266
+ this.sourceRoot = sourceRoot || this.projectRoot;
136
267
  this.engine = engine || new WorkflowEngine({ repoRoot });
137
- this.stateDir = stateDir || path.join(this.projectRoot, '.doflow', 'state', 'orchestration');
268
+ this.stateDir = stateDir || runStoreDir(this.projectRoot);
138
269
  this.readinessEvaluate = readinessEvaluate || null;
139
270
  this.taskAttemptCheck = taskAttemptCheck || null;
271
+ this.planExitCheck = planExitCheck || null;
140
272
  this.featureSlug = featureSlug;
141
273
  this.buildAdapters = buildAdapters;
274
+ this.env = env;
275
+ this.cwd = cwd;
276
+ }
277
+
278
+ /** The task's run (115 C-1): `<stateDir>/<featureSlug>/<taskId>.json` for a task id other than its
279
+ * feature's slug, else `<stateDir>/<taskId>.json`; a flat run of a namespaced id is read, and copied
280
+ * into the namespace on its next write, only when it is this feature's or names none (D-3). */
281
+ runRecord(taskId ) {
282
+ const id = assertSafeId(taskId, 'taskId');
283
+ const namespace = isSafeSlug(this.featureSlug) && this.featureSlug !== id ? this.featureSlug : null;
284
+ return recordIn({ base: this.stateDir, namespace, name: `${id}.json`, fsImpl: this.fsImpl, refuseUnsafeFlat: true });
142
285
  }
143
286
 
144
287
  runFile(taskId ) {
145
- return path.join(this.stateDir, `${assertSafeId(taskId, 'taskId')}.json`);
288
+ return this.runRecord(taskId).file;
146
289
  }
147
290
 
148
291
  readRun(taskId ) {
149
- const file = this.runFile(taskId);
150
- if (!this.fsImpl.existsSync(file)) return null;
151
- return readTaskState(this.fsImpl, file);
292
+ return readRunRecord(this.fsImpl, this.runRecord(taskId).read, this.stateDir);
152
293
  }
153
294
 
154
295
  writeRun(run , now , beforeWrite ) {
155
296
  run.updatedAt = iso(now);
156
- this.fsImpl.mkdirSync(this.stateDir, { recursive: true });
297
+ const { file, seed } = this.runRecord(run.taskId);
298
+ this.fsImpl.mkdirSync(path.dirname(file), { recursive: true });
157
299
  const expectedRevision = run.revision || 0;
158
- updateTaskState({ file: this.runFile(run.taskId), fsImpl: this.fsImpl, build: (disk ) => {
300
+ updateTaskState({ file, fsImpl: this.fsImpl, prepare: seed, build: (disk ) => {
159
301
  if ((disk?.revision || 0) !== expectedRevision) {
160
302
  throw new Error(`Concurrent workflow update for '${run.taskId}'; reload the run before retrying. Nothing was written.`);
161
303
  }
@@ -166,11 +308,15 @@ class WorkflowOrchestrator {
166
308
  return run;
167
309
  }
168
310
 
169
- /** Begin a new run. An existing run for the same task id is refused — resume instead. */
170
- start({ taskId, taskClass, now } = {}) {
311
+ /** Begin a new run. An existing run for the same task id is refused — resume instead.
312
+ * `loopProgram` is the roadmap loop's option only (loopWorkflow): the run journal records it, and no
313
+ * verb passes it. */
314
+ start({ taskId, taskClass, now, loopProgram = false } = {}) {
171
315
  assertSafeId(taskId, 'taskId');
172
316
  if (this.readRun(taskId)) throw new Error(`Workflow run '${taskId}' already exists; resume it instead of restarting`);
173
- const workflow = this.engine.resolveWorkflow(taskClass); // unknown classes are rejected loudly
317
+ const resolved = this.engine.resolveWorkflow(taskClass); // unknown classes are rejected loudly
318
+ const workflow = loopProgram ? loopWorkflow(resolved) : resolved;
319
+ const looped = workflow !== resolved; // the feature class already is a loop program: its journal is unchanged
174
320
  const program = compileProgram(workflow);
175
321
  const run = {
176
322
  version: 1,
@@ -181,28 +327,40 @@ class WorkflowOrchestrator {
181
327
  state: runState('RUNNING'),
182
328
  cursor: 0,
183
329
  program,
184
- history: [{ at: iso(now), action: 'start', detail: `task-class=${taskClass}` }],
330
+ history: [{ at: iso(now), action: 'start', detail: `task-class=${taskClass}${looped ? ' program=roadmap-loop' : ''}` }],
185
331
  startedAt: iso(now),
332
+ ...(looped ? { programKind: 'roadmap-loop' } : {}),
186
333
  // The feature the run belongs to, so another checkout can tell it from a run with the same id.
187
334
  ...(this.featureSlug ? { featureSlug: this.featureSlug } : {}),
335
+ ...this.traceFields(),
188
336
  };
189
337
  this.writeRun(run, now);
190
338
  return this.snapshot(run);
191
339
  }
192
340
 
341
+ /**
342
+ * 088 IC-010 / DD-5: the caller's 080 run key and, when the 080 store already holds that run with
343
+ * a trace id, a copy of the id. Empty with telemetry off, for a solo lineage, or on any fault:
344
+ * nothing here can fail or change a start. No id is minted here and no action is added.
345
+ */
346
+ traceFields() {
347
+ try {
348
+ const key = runKeyFor(this.env, this.cwd);
349
+ if (!key.traceIds || refuseSolo(key.identity)) return {};
350
+ const out = { runKey: key.runKey };
351
+ if (this.featureSlug) {
352
+ try {
353
+ const store = readTaskState(this.fsImpl, taskAttemptStoreFile(lifecycleProjectRoot(this.cwd), this.featureSlug));
354
+ const record = store && store.runs && Object.hasOwn(store.runs, key.runKey) ? store.runs[key.runKey] : null;
355
+ if (record && typeof record.traceId === 'string' && /^[0-9a-f]{32}$/.test(record.traceId)) out.traceId = record.traceId;
356
+ } catch { /* an unreadable 080 store means no copy */ }
357
+ }
358
+ return out;
359
+ } catch { return {}; }
360
+ }
361
+
193
362
  snapshot(run ) {
194
- const current = run.cursor < run.program.length ? run.program[run.cursor] : null;
195
- const done = run.program.filter((n ) => n.status === 'completed' || n.status === 'skipped' || n.status === 'approved').length;
196
- return {
197
- taskId: run.taskId,
198
- taskClass: run.taskClass,
199
- workflowName: run.workflowName,
200
- state: run.state,
201
- current: current ? { type: current.type, id: current.id, kind: current.kind ?? null, mutatesSource: current.mutatesSource ?? null, readinessTemplate: current.readinessTemplate ?? null } : null,
202
- awaitingGate: run.state === 'AWAITING_GATE' && current ? { gateId: current.id, name: current.name, prompt: current.prompt } : null,
203
- progress: { done, total: run.program.length },
204
- terminalStage: run.terminalStage,
205
- };
363
+ return snapshotRun(run);
206
364
  }
207
365
 
208
366
  status(taskId ) {
@@ -244,7 +402,10 @@ class WorkflowOrchestrator {
244
402
  }
245
403
  // The ledger is checked here too, before catch-up writes anything, so a refused implementation
246
404
  // handoff leaves no backfilled stage behind.
247
- if (preview.reason === 'reached-candidate') this.assertTaskAttempts(preview.node, existing || { taskId, taskClass: resolvedClass });
405
+ if (preview.reason === 'reached-candidate') {
406
+ this.assertTaskAttempts(preview.node, existing || { taskId, taskClass: resolvedClass });
407
+ this.assertPlanExit(preview.node, existing || { taskId, taskClass: resolvedClass });
408
+ }
248
409
  const positioned = this.catchUp({ taskId, taskClass: resolvedClass, candidateStageIds: candidates, note, now });
249
410
  if (positioned.reason === 'reached-candidate') {
250
411
  // A run this handoff started has no earlier stage: every evidence item counts.
@@ -295,8 +456,9 @@ class WorkflowOrchestrator {
295
456
  const harness = (registry.harnesses || []).find((item ) => item.id === id);
296
457
  return harness ? harness.displayName || harness.id : id;
297
458
  };
298
- const { briefRecords, repoRoot } = stageId ? this.readBriefRecords(taskId) : { briefRecords: [], repoRoot: this.projectRoot };
299
- if (stageId) this.recordBriefTierAdvice({ briefRecords, taskId, stageId, repoRoot });
459
+ const { briefRecords, repoRoot } = stageId ? this.readBriefRecords(taskId) : { briefRecords: [], repoRoot: this.sourceRoot };
460
+ // Advice picks are project state, kept with the run's records, not in the feature folder's checkout.
461
+ if (stageId) this.recordBriefTierAdvice({ briefRecords, taskId, stageId, repoRoot: this.projectRoot });
300
462
  try {
301
463
  const result = resolveConsultations({
302
464
  taskId, stageId, skill, flags, registry, ledger: {
@@ -305,7 +467,7 @@ class WorkflowOrchestrator {
305
467
  return ledger.queryEvidence({ taskId: id });
306
468
  },
307
469
  },
308
- requests: new ResearchRequestStore({ projectRoot: this.projectRoot, fsImpl: this.fsImpl, slug: this.featureSlug }),
470
+ requests: new ResearchRequestStore({ projectRoot: this.projectRoot, sourceRoot: this.sourceRoot, fsImpl: this.fsImpl, slug: this.featureSlug }),
309
471
  briefRecords,
310
472
  readReport: (file ) => {
311
473
  const target = path.resolve(repoRoot, file);
@@ -389,28 +551,28 @@ class WorkflowOrchestrator {
389
551
  }
390
552
 
391
553
  /** The brief records (`*-brief.json`) of the feature this run belongs to, parsed; unreadable ones
392
- * are left out. Resolving the feature spawns the resolver, so it is only done where a feature
393
- * folder exists above the project. */
554
+ * are left out. They are feature-folder files, so they are resolved from the checkout the verb runs
555
+ * in with the resolver's `feature_root` (that checkout's folder, else the main checkout's), never
556
+ * from the state root (115 C-10). Resolving the feature spawns the resolver, so it is only done
557
+ * where a feature folder exists above one of the two. */
394
558
  readBriefRecords(taskId ) {
395
- const none = { briefRecords: [], repoRoot: this.projectRoot };
559
+ const none = { briefRecords: [], repoRoot: this.sourceRoot };
396
560
  try {
397
- let dir = path.resolve(this.projectRoot);
398
- let hasFolder = false;
399
- for (;;) {
400
- if (this.fsImpl.existsSync(path.join(dir, 'agent-docs', 'doflow'))) { hasFolder = true; break; }
401
- const parent = path.dirname(dir);
402
- if (parent === dir) break;
403
- dir = parent;
404
- }
561
+ const hasFolder = [this.sourceRoot, this.projectRoot].some((root) => {
562
+ for (let dir = path.resolve(root); ; dir = path.dirname(dir)) {
563
+ if (this.fsImpl.existsSync(path.join(dir, 'agent-docs', 'doflow'))) return true;
564
+ if (path.dirname(dir) === dir) return false;
565
+ }
566
+ });
405
567
  if (!hasFolder) return none;
406
- const feature = resolveActiveFeature({ projectRoot: this.projectRoot, slug: taskId });
568
+ const feature = resolveActiveFeature({ projectRoot: this.sourceRoot, slug: taskId });
407
569
  if (feature.error) return none;
408
- const exec = path.join(feature.featureDir, 'exec');
570
+ const exec = path.join(feature.featureFolder, 'exec');
409
571
  const briefRecords = [];
410
572
  for (const name of this.fsImpl.readdirSync(exec).filter((entry ) => entry.endsWith('-brief.json')).sort()) {
411
573
  try { briefRecords.push(JSON.parse(this.fsImpl.readFileSync(path.join(exec, name), 'utf8'))); } catch { /* an unreadable record contributes nothing */ }
412
574
  }
413
- return { briefRecords, repoRoot: feature.repoRoot };
575
+ return { briefRecords, repoRoot: feature.featureRoot };
414
576
  } catch { return none; }
415
577
  }
416
578
 
@@ -463,15 +625,21 @@ class WorkflowOrchestrator {
463
625
  * stage is always imported+unverified, whatever the caller says: catch-up importing history
464
626
  * cannot import a verification. A 'failed' outcome still advances — recording the failure
465
627
  * honestly beats refusing to record it, and a bug reproduction legitimately completes by
466
- * observing the expected failure — but the record no longer reads as success. */
628
+ * observing the expected failure — but the record no longer reads as success.
629
+ *
630
+ * Issue #9 stage 3: the entry names the context packs the stage was handed (`packs`), copied from
631
+ * what `recordStagePack` wrote on this node while it was the run's active stage. A stage handed
632
+ * none gets no `packs`; a malformed item is left out, never an error. */
467
633
  completeStage({ taskId, stageId, note, outcome, backfilled = false, consultations, now } = {}) {
468
634
  const run = this.requireRun(taskId);
469
635
  const node = this.expectOpenStage(run, stageId);
636
+ const packs = stagePacks(node.packs);
470
637
  if (outcome !== undefined && !STAGE_OUTCOMES.has(outcome)) {
471
638
  throw new Error(`Unknown stage outcome '${outcome}'. Valid: ${[...STAGE_OUTCOMES].join(', ')} — omit the flag to record 'unverified'.`);
472
639
  }
473
640
  if (node.mutatesSource && node.readinessTemplate) this.assertReady(node, run);
474
641
  const attempts = backfilled ? null : this.assertTaskAttempts(node, run);
642
+ this.assertPlanExit(node, run);
475
643
  // No ledger for the feature: the stage is recorded, but nothing verified its tasks, and the
476
644
  // record says so whatever outcome the caller supplied (FR-006).
477
645
  const unledgered = attempts?.state === 'none';
@@ -483,11 +651,12 @@ class WorkflowOrchestrator {
483
651
  note: unledgered ? [note, NO_LEDGER_NOTE].filter(Boolean).join(' ') : (note ?? null),
484
652
  backfilled: Boolean(backfilled), executionStatus: node.executionStatus, outcome: node.outcome,
485
653
  ...(consultations ? { consultations } : {}),
654
+ ...(packs.length ? { packs } : {}),
486
655
  });
487
656
  this.advance(run, now);
488
657
  this.writeRun(run, now, () => {
489
658
  if (run.taskClass !== 'feature') return;
490
- const request = new ResearchRequestStore({ projectRoot: this.projectRoot, fsImpl: this.fsImpl })
659
+ const request = new ResearchRequestStore({ projectRoot: this.projectRoot, sourceRoot: this.sourceRoot, fsImpl: this.fsImpl })
491
660
  .blockingGap(taskId, stageId);
492
661
  if (request) throw new Error(`Research request '${request.id}' blocks stage '${stageId}': ${request.failureReason || request.gap || request.status}`);
493
662
  });
@@ -640,15 +809,22 @@ class WorkflowOrchestrator {
640
809
  * `forced` decision (e.g. approving despite unresolved clarification markers) requires an
641
810
  * explicit `note` reason — never a silent override — and is flagged in history for
642
811
  * `render-audit.sh` to distinguish from a routine handoff. */
643
- decideGate({ taskId, gateId, decision, note, forced = false, now } = {}) {
812
+ decideGate({ taskId, gateId, decision, policy, by, note, forced = false, now, bindHead } = {}) {
644
813
  assertSafeId(gateId, 'gateId');
645
- if (!GATE_DECISIONS.includes(decision)) throw new Error(`Invalid gate decision '${decision}'; valid: ${GATE_DECISIONS.join(', ')}`);
814
+ if (bindHead !== undefined && (policy === undefined || !/^[0-9a-f]{40}$/.test(String(bindHead)))) throw new Error('bindHead is a 40-hex feature head and needs --policy');
815
+ // IC-005: the decider is derived, never named by a caller; `autopilot` is recorded only by --policy.
816
+ if (by !== undefined) throw new Error('decide-gate takes no decider (by); only --policy records autopilot');
817
+ if (policy !== undefined) {
818
+ if (policy !== gateId) throw new Error('--policy must name the gate being decided');
819
+ if (decision !== undefined || forced) throw new Error('--policy refuses --decision and --forced');
820
+ } else if (!GATE_DECISIONS.includes(decision)) throw new Error(`Invalid gate decision '${decision}'; valid: ${GATE_DECISIONS.join(', ')}`);
646
821
  if (forced && (!note || !String(note).trim())) throw new Error('A forced gate decision requires a --note reason');
647
822
  const run = this.requireRun(taskId);
648
823
  if (run.state !== 'AWAITING_GATE') throw new Error(`Run '${run.taskId}' is not awaiting a gate (state=${run.state})`);
649
824
  const node = this.currentNode(run);
650
825
  if (!node || node.type !== 'gate') throw new Error('Run state says AWAITING_GATE but no gate is at the cursor');
651
826
  if (node.id !== gateId) throw new Error(`Expected gate '${node.id}' next, got '${gateId}'`);
827
+ if (policy !== undefined) return this.decideGateByPolicy(run, node, policy, now, bindHead);
652
828
  node.status = decision === 'approve' ? 'approved' : 'rejected';
653
829
  node.decision = decision;
654
830
  run.history.push({ at: iso(now), action: 'decide-gate', node: node.id, detail: decision, note: note ?? null, forced: Boolean(forced) });
@@ -663,6 +839,94 @@ class WorkflowOrchestrator {
663
839
  return this.snapshot(run);
664
840
  }
665
841
 
842
+ /** IC-005: the feature checkout cannot grant itself a weaker gate policy. Denies write nothing. */
843
+ decideGateByPolicy(run , node , policyId , now , bindHead ) {
844
+ // Decision C9: the autopilot kill switch binds every policy decision, not only the loop's. Off on
845
+ // any layer (env, sentinel, settings; an unreadable setting fails safe to off) denies before any
846
+ // fact is read, and records nothing.
847
+ const { readKillSwitch } = require('./settings/autopilot.js');
848
+ const killSwitch = readKillSwitch(this.env);
849
+ if (!killSwitch.enabled) {
850
+ return { ...this.snapshot(run), decision: 'deny', reason: 'autopilot-disabled', reasons: ['autopilot-disabled'],
851
+ rule: `${node.id}/autopilot`, policy: { id: policyId, digest: null }, inputs: [], annotations: { switch: killSwitch.switch } };
852
+ }
853
+ // Loaded here, not at module top: every other orchestrator path (and a dry-run install) must not import them.
854
+ const { projectLayerSource } = require('./settings/project-layers.js');
855
+ const { parseSeed } = require('./roadmap/rows.js');
856
+ const { loadPolicy, evaluate } = require('./roadmap/gates.js');
857
+ const { gatherGateFacts } = require('./roadmap/facts.js');
858
+ const root = this.sourceRoot;
859
+ const mainRoot = mainCheckoutOf(root);
860
+ const execOptions = { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] };
861
+ let developHead ;
862
+ let baseBytes = null;
863
+ // The base policy is develop's committed blob, never the worktree or the feature branch: a
864
+ // branch that weakens `never` to `approve` must not approve itself. No textconv filter applies.
865
+ try {
866
+ const head = execFileSync('git', ['rev-parse', '--verify', 'refs/heads/develop'], execOptions).trim();
867
+ developHead = head;
868
+ if (/^[0-9a-f]{40}$/.test(head)) {
869
+ baseBytes = execFileSync('git', ['show', '--no-textconv', `${head}:core/registry/gates.json`], execOptions);
870
+ }
871
+ } catch { /* loadPolicy returns no-policy; never fall back to the feature copy. */ }
872
+ const layers = projectLayerSource(mainRoot, this.fsImpl);
873
+ let overrideBytes = null;
874
+ if (layers.layersFrom) {
875
+ try { overrideBytes = this.fsImpl.readFileSync(path.join(layers.layersFrom, 'gates.override.json'), 'utf8'); }
876
+ catch (error ) { if (error.code !== 'ENOENT') overrideBytes = ''; }
877
+ }
878
+ const loaded = loadPolicy(baseBytes, overrideBytes);
879
+ const policy = { id: policyId, digest: loaded.digest };
880
+ if (!loaded.ok) return { ...this.snapshot(run), decision: 'deny', reasons: loaded.reasons, reason: loaded.reason, policy, inputs: [], ...(loaded.annotations ? { annotations: loaded.annotations } : {}) };
881
+
882
+ const slug = runSlugOf(run, root) || this.featureSlug || run.taskId;
883
+ let seed = null;
884
+ let row = null;
885
+ try {
886
+ seed = parseSeed(this.fsImpl.readFileSync(path.join(mainRoot, 'agent-docs/roadmap/rows.json'), 'utf8'));
887
+ row = seed.rows.find((entry ) => entry.slug === slug && entry.class === run.taskClass) || null;
888
+ } catch (error ) {
889
+ if (error.code === 'seed-typed-status') return { ...this.snapshot(run), decision: 'deny', reason: error.code, reasons: [error.code], policy, inputs: [] };
890
+ // Invalid or unreadable seed means no trustworthy row; evaluate returns row-unknown.
891
+ }
892
+ // The feature folder follows the feature-store rule (resolveFeatureInStore): a linked worktree
893
+ // without the folder would otherwise read no artifacts (a false `unresolved-clarifications`) and
894
+ // register into a missing folder.
895
+ const feature = resolveFeatureInStore({ root, slug });
896
+ if (feature.error) throw new Error(`Cannot resolve policy gate artifacts: ${feature.message}`);
897
+ const facts = gatherGateFacts({ root, mainRoot, registryRoot: this.repoRoot, artifactRoot: feature.featureDir,
898
+ gateId: node.id, policy: loaded.policy, row, rows: seed?.rows, developHead, featureBranch: `feat/${slug}`, fsImpl: this.fsImpl, env: this.env });
899
+ const evaluation = evaluate(loaded.policy, run.taskClass, node.id, facts);
900
+ // A deny changes nothing: the run stays AWAITING_GATE and autopilot never rejects.
901
+ if (evaluation.decision === 'deny') return { ...this.snapshot(run), ...evaluation, policy };
902
+
903
+ const audit = `approved by policy ${policyId}@${loaded.digest} (${evaluation.inputs.length} inputs)`;
904
+ node.status = 'approved';
905
+ node.decision = 'approve';
906
+ // `policy.digest` and every fact's {locator, sha256} let a later reader re-derive this decision.
907
+ // C15: the roadmap loop names the feature head it read before deciding (bindHead). The entry keeps
908
+ // it as `approvedHead` only when the gate's own heads fact evaluated that same head, so the merge
909
+ // binds to it across loop runs; an approval without it falls back to the both-heads digest.
910
+ const heads = facts.find((fact ) => fact.name === 'heads' && !fact.unreadable)?.value;
911
+ const approvedHead = bindHead && heads?.featureHead === bindHead ? { approvedHead: bindHead } : {};
912
+ run.history.push({ at: iso(now), action: 'decide-gate', node: node.id, detail: 'approve', by: 'autopilot',
913
+ policy, inputs: evaluation.inputs, ...approvedHead, note: audit, forced: false });
914
+ run.state = runState('RUNNING');
915
+ run.cursor += 1;
916
+ this.settleCursor(run);
917
+ // The optimistic revision check runs before registering a decision, so a stale run cannot
918
+ // leave a forged approval. A register refusal likewise prevents the journal from advancing.
919
+ this.writeRun(run, now, () => {
920
+ const registered = addDecisions({ featureDir: feature.featureDir, slug, now: now ?? new Date(), fsImpl: this.fsImpl, items: [{
921
+ topic: node.id, statement: `${node.id} ${audit}.`, channel: 'policy', stage: POLICY_GATE_STAGE[node.id],
922
+ rationale: `Every predicate of ${node.id} passed on the inputs digest ${evaluation.inputsDigest}.`,
923
+ source: `git:${developHead}:core/registry/gates.json`,
924
+ }] });
925
+ if (registered.finding) throw new Error(`Cannot register policy approval: ${registered.finding}: ${registered.message}`);
926
+ });
927
+ return { ...this.snapshot(run), ...evaluation, policy };
928
+ }
929
+
666
930
  /** Append a pure audit-trail entry — a stage re-run after its own handoff, or an artifact
667
931
  * hand-edited after that stage's own handoff — without touching program/cursor/state. Not a
668
932
  * stage completion or a gate decision, so it never collides with the state machine's ordering
@@ -731,6 +995,20 @@ class WorkflowOrchestrator {
731
995
  return answer;
732
996
  }
733
997
 
998
+ /** Row 134: the planning stage (the `do-plan` stage) cannot complete while the plan's
999
+ * Constitution Check leaves a MUST principle marked FAIL or without a row. There is no waiver:
1000
+ * the plan is revised, or a person amends the constitution. A stage catch-up backfills is checked
1001
+ * too, so a refused plan is never imported as complete. */
1002
+ assertPlanExit(node , run ) {
1003
+ if (!this.planExitCheck || node.skill !== 'do-plan') return null;
1004
+ const answer = this.planExitCheck(node, run);
1005
+ if (!answer || !Array.isArray(answer.refusals)) throw new Error(`Plan exit check returned no usable answer for stage '${node.id}'`);
1006
+ if (answer.refusals.length) {
1007
+ throw new Error(`Stage '${node.id}' cannot be completed: ${answer.refusals.map((f ) => `${f.path}:${f.line} ${f.message}`).join('; ')}`);
1008
+ }
1009
+ return answer;
1010
+ }
1011
+
734
1012
  /** Where `catchUp` would stop for these candidates, worked out on a copy of the run's program (or
735
1013
  * of a fresh run's) without writing anything. Same stop rules and reason vocabulary as `catchUp`;
736
1014
  * `stageId` is what `catchUp` reports as `caughtUpTo`, and `node` the program node it stops on.
@@ -768,6 +1046,26 @@ class WorkflowOrchestrator {
768
1046
  }
769
1047
  }
770
1048
 
1049
+ /** The project's main checkout, or `root` itself when the checkouts cannot be listed. Loaded here, not at
1050
+ * module top: every other orchestrator path (and a dry-run install) must not import the resolver. */
1051
+ function mainCheckoutOf(root ) {
1052
+ const { listCheckouts } = require('./checkouts.js');
1053
+ const checkouts = listCheckouts({ cwd: root });
1054
+ return checkouts.ok && checkouts.main ? checkouts.main : root;
1055
+ }
1056
+
1057
+ /**
1058
+ * The feature folder follows the feature-store rule, which the feature resolver already applies: the
1059
+ * calling checkout's own folder, else the main checkout's when a linked worktree holds none
1060
+ * (`featureFolder` under `featureRoot`). `featureDir` and `repoRoot` of the result name that folder and
1061
+ * checkout, as `decision --action compact` reads them, so a caller never looks in the wrong checkout.
1062
+ */
1063
+ function resolveFeatureInStore({ root, slug } ) {
1064
+ const feature = resolveActiveFeature({ projectRoot: root, slug });
1065
+ if (feature.error) return feature;
1066
+ return { ...feature, featureDir: feature.featureFolder, repoRoot: feature.featureRoot };
1067
+ }
1068
+
771
1069
  /**
772
1070
  * IC-013: fold the finished stage's History into the feature's archive. The task id is the feature
773
1071
  * slug. This is housekeeping after a recorded handoff, so it reports and never throws: the
@@ -777,7 +1075,7 @@ class WorkflowOrchestrator {
777
1075
  */
778
1076
  function compactAfterHandoff({ taskId, projectRoot } ) {
779
1077
  try {
780
- const feature = resolveActiveFeature({ projectRoot, slug: taskId });
1078
+ const feature = resolveFeatureInStore({ root: projectRoot, slug: taskId });
781
1079
  if (feature.error) return { status: 'skipped', reason: feature.message };
782
1080
  // A task id such as a plan task (`B.1`) resolves on a feature branch to a folder that was
783
1081
  // never created; say that, rather than blaming a missing register.
@@ -797,41 +1095,242 @@ function compactAfterHandoff({ taskId, projectRoot } ) {
797
1095
  }
798
1096
  }
799
1097
 
1098
+ /**
1099
+ * IC-001: `orchestrate --action list`, every run the resolver can see as a prompt-free row plus the
1100
+ * records it could not read, by reason code only. Reads and prints; writes nothing.
1101
+ */
1102
+ /**
1103
+ * The first flag given that a read action does not take, or undefined. Every flag another action
1104
+ * reads is refused before any read: a filter the read does not apply must not look applied, and an
1105
+ * outcome nothing records must not look recorded. `takes` names the flags the action does read.
1106
+ */
1107
+ function noEffectFlag({ taskId, taskClass, gate, decision, policy, forced, note, reason, stage, node, verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, slug, dryRun } , takes = []) {
1108
+ const supplied = (value ) => value !== undefined && value !== null;
1109
+ const given = [
1110
+ ['--task-id', supplied(taskId)], ['--task-class', supplied(taskClass)], ['--gate', supplied(gate)],
1111
+ ['--decision', supplied(decision)], ['--policy', supplied(policy)],
1112
+ ['--forced', forced], ['--note', supplied(note)], ['--reason', supplied(reason)], ['--stage', supplied(stage)],
1113
+ ['--node', supplied(node)], ['--verification-plan', supplied(verificationPlan)], ['--scope', supplied(scope)],
1114
+ ['--invariants', supplied(invariants)], ['--result', supplied(result)], ['--mcp-outcome', supplied(mcpOutcome)],
1115
+ ['--calling-skill', supplied(callingSkill)], ['--slug', supplied(slug)], ['--dry-run', Boolean(dryRun)],
1116
+ ].find(([flag, on]) => on && !takes.includes(flag ));
1117
+ return given ? given[0] : undefined;
1118
+ }
1119
+
1120
+ /** A journal time as an ISO instant, else null, so no other journal text reaches a read's output. */
1121
+ const journalTime = (value ) => typeof value === 'string'
1122
+ && /^\d{4}-\d{2}-\d{2}T/.test(value) && !Number.isNaN(Date.parse(value)) ? value : null;
1123
+
1124
+ /** The journal actions a timeline shows (093 IC-012); any other is counted in `otherEvents`. */
1125
+ const TIMELINE_ACTIONS = new Set(['start', 'complete-stage', 'skip-stage', 'skip-gate', 'decide-gate', 'annotate', 'complete-run']);
1126
+ const ISO_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
1127
+
1128
+ /**
1129
+ * 093 IC-012: one run's history as closed values only. Each event copies the action, an id-shaped
1130
+ * node, the stage outcome (complete-stage), the gate decision (decide-gate's `detail`), `forced`, and
1131
+ * whether the entry records no work done (the indicators rule, trace/indicators.ts isSynthetic).
1132
+ * Notes, detail text, deciders, policy inputs, consultations and trace ids are never read.
1133
+ */
1134
+ function timelineOf(run , taskId , featureSlug ) {
1135
+ const at = (value ) => journalTime(value) !== null && ISO_INSTANT.test(value) ? value : null;
1136
+ const events = [];
1137
+ let otherEvents = 0;
1138
+ for (const entry of Array.isArray(run.history) ? run.history : []) {
1139
+ if (!entry || typeof entry !== 'object' || !TIMELINE_ACTIONS.has(entry.action)) {
1140
+ otherEvents += 1;
1141
+ continue;
1142
+ }
1143
+ events.push({
1144
+ at: at(entry.at),
1145
+ action: entry.action,
1146
+ node: typeof entry.node === 'string' && ID_PATTERN.test(entry.node) ? entry.node : null,
1147
+ outcome: entry.action === 'complete-stage' && STAGE_OUTCOMES.has(entry.outcome) ? entry.outcome : null,
1148
+ decision: entry.action === 'decide-gate' && GATE_DECISIONS.includes(entry.detail) ? entry.detail : null,
1149
+ forced: entry.forced === true,
1150
+ synthetic: Boolean(entry.backfilled) || Boolean(entry.executionStatus && entry.executionStatus !== 'completed'),
1151
+ });
1152
+ }
1153
+ return {
1154
+ v: 1,
1155
+ taskId,
1156
+ featureSlug: typeof featureSlug === 'string' && ID_PATTERN.test(featureSlug) ? featureSlug : null,
1157
+ revision: Number.isInteger(run.revision) && run.revision >= 0 ? run.revision : 0,
1158
+ startedAt: at(run.startedAt),
1159
+ events,
1160
+ otherEvents,
1161
+ };
1162
+ }
1163
+
1164
+ function listRuns({ json, stateRoot, ...flags } ) {
1165
+ const { finishRuntime, usageError } = require('./cli-result.js');
1166
+ // list takes no filter, so a `--slug` must not look like one.
1167
+ const noEffect = noEffectFlag(flags);
1168
+ if (noEffect) return usageError('orchestrate', `'${noEffect}' has no effect on action 'list'`, json);
1169
+
1170
+ const resolverFailure = () => {
1171
+ if (json) console.log(JSON.stringify({ v: 1, error: 'cannot-list-checkouts' }, null, 2));
1172
+ else console.error('[ERROR] orchestrate: cannot list checkouts');
1173
+ return finishRuntime(1);
1174
+ };
1175
+ let resolved ;
1176
+ try { resolved = orchestrationRuns({ stateRoot: stateRoot || process.cwd() }); } catch { return resolverFailure(); }
1177
+ if (!resolved.ok) return resolverFailure();
1178
+
1179
+ const runs = [];
1180
+ const unreadable = [];
1181
+ const textCompare = (a , b ) => a < b ? -1 : a > b ? 1 : 0;
1182
+ const slugCompare = (a , b ) => a === null ? (b === null ? 0 : -1)
1183
+ : b === null ? 1 : textCompare(a, b);
1184
+ const addUnreadable = (entry , reason ) => {
1185
+ const relative = path.relative(runStoreDir(entry.root), entry.file);
1186
+ const parts = relative.split(path.sep);
1187
+ unreadable.push({
1188
+ taskId: path.basename(entry.file, '.json'),
1189
+ featureSlug: parts.length > 1 ? parts[0] : null,
1190
+ origin: entry.origin,
1191
+ reason,
1192
+ });
1193
+ };
1194
+ // One row per run, the copy status reads (C-2): a shadowed flat or another checkout's copy is dropped.
1195
+ for (const entry of selectRunEntries(resolved.entries)) {
1196
+ if (entry.error) {
1197
+ addUnreadable(entry, entry.error);
1198
+ continue;
1199
+ }
1200
+ const record = entry.record;
1201
+ if (!record || typeof record !== 'object' || Array.isArray(record)) {
1202
+ addUnreadable(entry, 'shape');
1203
+ continue;
1204
+ }
1205
+ if (record.version !== 1) {
1206
+ addUnreadable(entry, 'schema-version');
1207
+ continue;
1208
+ }
1209
+ const relative = path.relative(runStoreDir(entry.root), entry.file);
1210
+ const parts = relative.split(path.sep);
1211
+ const folderSlug = parts.length > 1 ? parts[0] : null;
1212
+ const stem = path.basename(entry.file, '.json');
1213
+ // Every id a row prints is checked, so no newline or escape byte from a record reaches the text form.
1214
+ if (typeof record.taskId !== 'string' || !ID_PATTERN.test(record.taskId) || record.taskId !== stem
1215
+ || (folderSlug && typeof record.featureSlug === 'string' && record.featureSlug !== folderSlug)
1216
+ || (entry.slug !== null && entry.slug !== undefined && !(typeof entry.slug === 'string' && ID_PATTERN.test(entry.slug)))) {
1217
+ addUnreadable(entry, 'id-mismatch');
1218
+ continue;
1219
+ }
1220
+ if (!RUN_STATES.includes(record.state)) {
1221
+ addUnreadable(entry, 'shape');
1222
+ continue;
1223
+ }
1224
+ let snapshot ;
1225
+ try { snapshot = snapshotRun(record); } catch {
1226
+ addUnreadable(entry, 'shape');
1227
+ continue;
1228
+ }
1229
+ if (snapshot.current && !(typeof snapshot.current.id === 'string' && ID_PATTERN.test(snapshot.current.id))) {
1230
+ addUnreadable(entry, 'shape');
1231
+ continue;
1232
+ }
1233
+ const terminal = snapshot.terminalStage;
1234
+ runs.push({
1235
+ taskId: snapshot.taskId,
1236
+ featureSlug: entry.slug,
1237
+ taskClass: snapshot.taskClass,
1238
+ workflowName: snapshot.workflowName,
1239
+ state: snapshot.state,
1240
+ current: snapshot.current,
1241
+ awaitingGate: snapshot.awaitingGate ? { gateId: snapshot.awaitingGate.gateId, name: snapshot.awaitingGate.name } : null,
1242
+ progress: snapshot.progress,
1243
+ // IC-001: the stage's identity only; its gate list (and the prompts in it) stays in status.
1244
+ terminalStage: terminal && typeof terminal === 'object'
1245
+ ? { id: terminal.id ?? null, skill: terminal.skill ?? null, kind: terminal.kind ?? null } : null,
1246
+ revision: snapshot.revision,
1247
+ startedAt: journalTime(record.startedAt),
1248
+ updatedAt: journalTime(record.updatedAt),
1249
+ origin: entry.origin,
1250
+ });
1251
+ }
1252
+
1253
+ runs.sort((a, b) => {
1254
+ if (a.updatedAt === null && b.updatedAt !== null) return 1;
1255
+ if (a.updatedAt !== null && b.updatedAt === null) return -1;
1256
+ if (a.updatedAt !== null && b.updatedAt !== null) {
1257
+ const byTime = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
1258
+ if (byTime !== 0) return byTime;
1259
+ }
1260
+ return slugCompare(a.featureSlug, b.featureSlug) || textCompare(a.taskId, b.taskId);
1261
+ });
1262
+ unreadable.sort((a, b) => textCompare(a.origin, b.origin)
1263
+ || slugCompare(a.featureSlug, b.featureSlug) || textCompare(a.taskId, b.taskId));
1264
+
1265
+ if (json) {
1266
+ console.log(JSON.stringify({ v: 1, runs, unreadable }, null, 2));
1267
+ } else {
1268
+ for (const row of runs) console.log(`${row.state} ${row.featureSlug || '-'} ${row.taskId} ${row.current ? row.current.id : '-'} rev ${row.revision}`);
1269
+ if (unreadable.length > 0) console.log(`unreadable: ${unreadable.length}`);
1270
+ }
1271
+ return finishRuntime(0);
1272
+ }
1273
+
800
1274
  /** CLI handler for `doflow orchestrate`. The run journal lives in the CALLER's project state
801
1275
  * (like evidence), while templates come from this install — same two-roots split readiness uses.
802
1276
  * Completing a source-mutating gated stage needs a READY readiness record of the stage's template,
803
1277
  * made by `doflow readiness` before this call; nothing is evaluated here. */
804
1278
  function handleOrchestrateCommand({
805
- action = 'status', taskId, taskClass, stage, gate, node, decision, note, reason, forced = false,
806
- verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, json = false, repoRoot, stateRoot, slug = null, buildAdapters = null,
1279
+ action = 'status', taskId, taskClass, stage, gate, node, decision, policy, note, reason, forced = false,
1280
+ verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, json = false, repoRoot, stateRoot, sourceRoot, slug = null, recordSlug, buildAdapters = null, dryRun = false,
807
1281
  } = {}) {
808
1282
  const { finishRuntime, usageError } = require('./cli-result.js');
809
1283
 
1284
+ if (action === 'list') {
1285
+ return listRuns({ taskId, taskClass, gate, decision, policy, forced, note, reason, stage, node, verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, slug, dryRun, json, stateRoot });
1286
+ }
1287
+ // 093 IC-012: timeline reads one run, so it takes the two flags that name it and refuses the rest.
1288
+ if (action === 'timeline') {
1289
+ const noEffect = noEffectFlag({ taskId, taskClass, gate, decision, policy, forced, note, reason, stage, node, verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, slug, dryRun }, ['--task-id', '--slug']);
1290
+ if (noEffect) return usageError('orchestrate', `'${noEffect}' has no effect on action 'timeline'`, json);
1291
+ if (!taskId) return usageError('orchestrate', 'timeline requires --task-id', json);
1292
+ }
1293
+
1294
+ // `start` writes the run; --dry-run would otherwise be ignored and a real run started.
1295
+ if (action === 'start' && dryRun) return usageError('orchestrate', "'--dry-run' has no effect on action 'start'", json);
1296
+
810
1297
  const root = repoRoot || REPO_ROOT;
811
1298
  // Same scope rule every other stateful command uses (evidenceRoot(o) in runtime-commands.js):
812
1299
  // a bare `stateRoot: stateRoot || process.cwd()` silently ignored `--global`, so `orchestrate`
813
1300
  // wrote its journal to $PWD while a `--global` `evidence`/`readiness` call on the same task read
814
1301
  // and wrote $HOME — two roots disagreeing about where one task's state lives.
815
1302
  const state = stateRoot || process.cwd();
1303
+ // The checkout the verb runs in: its branch and feature folder name the feature; `state` (the main
1304
+ // checkout for a linked worktree, 115 IC-004) holds the records.
1305
+ const source = sourceRoot || state;
816
1306
  const safeId = ID_PATTERN.test(String(taskId ?? ''));
817
- // The feature this task belongs to: recorded on a run this command starts, matched against a run
818
- // found in another checkout, and the namespace its readiness record is read from.
819
- const featureSlug = safeId ? featureSlugFor({ projectRoot: state, taskId, slug }) : slug;
1307
+ // The feature this task belongs to: recorded on a run this command starts, the namespace its run
1308
+ // and readiness record live under (115 C-1), and matched against a run found in another checkout.
1309
+ // The dispatcher passes the call's record slug, computed once for every store the call touches.
1310
+ let featureSlug = recordSlug !== undefined ? recordSlug : safeId ? featureSlugFor({ projectRoot: source, taskId, slug }) : slug;
820
1311
  // A run started in another checkout of this repository for the same feature is read and updated
821
1312
  // where it is. `start` is always local: an unrelated run with the same id elsewhere neither
822
1313
  // refuses nor satisfies it.
823
1314
  let runRoot = state;
824
1315
  if (safeId && action !== 'start') {
825
- const found = findRunFile({ stateRoot: state, taskId, slug: featureSlug });
1316
+ // `status` (and `timeline`, which reads the same run) with no --slug opens the one run the list
1317
+ // action shows for the id, whichever feature namespace holds it (FU-wrnnaq); more than one is
1318
+ // refused until --slug names the feature.
1319
+ const found = findRunFile({ stateRoot: state, taskId, slug: featureSlug, anyNamespace: (action === 'status' || action === 'timeline') && !slug });
826
1320
  if (found.status === 'ambiguous') {
827
- console.error(`[ERROR] orchestrate: run '${taskId}' exists in more than one other checkout (${found.candidates.join(', ')}); run the command from the checkout that holds the run you mean. Nothing was changed.`);
1321
+ console.error(found.byFeature
1322
+ ? `[ERROR] orchestrate: run '${taskId}' exists under more than one feature (${found.candidates.join(', ')}); pass --slug <feature> to name the one you mean. Nothing was changed.`
1323
+ : `[ERROR] orchestrate: run '${taskId}' exists in more than one other checkout (${found.candidates.join(', ')}); run the command from the checkout that holds the run you mean. Nothing was changed.`);
828
1324
  return finishRuntime(1);
829
1325
  }
830
- if (found.status === 'found') runRoot = found.root;
1326
+ if (found.status === 'found') {
1327
+ runRoot = found.root;
1328
+ if (found.slug !== undefined) featureSlug = found.slug;
1329
+ }
831
1330
  }
832
1331
  const orchestrator = new WorkflowOrchestrator({
833
- repoRoot: root, projectRoot: runRoot,
834
- stateDir: path.join(runRoot, '.doflow', 'state', 'orchestration'),
1332
+ repoRoot: root, projectRoot: runRoot, sourceRoot: source,
1333
+ stateDir: runStoreDir(runRoot),
835
1334
  featureSlug, buildAdapters,
836
1335
  });
837
1336
  orchestrator.readinessEvaluate = (node , run ) => {
@@ -865,12 +1364,28 @@ function handleOrchestrateCommand({
865
1364
  slug: runSlugOf(run, runRoot) || featureSlug, cwd: state,
866
1365
  });
867
1366
 
1367
+ // Row 134. The plan is read in the checkout the verb runs in first, then where the run lives; a
1368
+ // SHOULD principle marked FAIL is one note, printed once although the check runs twice.
1369
+ let planAdvised = false;
1370
+ orchestrator.planExitCheck = (node , run ) => {
1371
+ const { planStageExit } = require('./plan-content.js');
1372
+ const answer = planStageExit({ roots: [...new Set([source, runRoot])], slug: runSlugOf(run, runRoot) || featureSlug });
1373
+ if (!planAdvised) {
1374
+ planAdvised = true;
1375
+ for (const f of answer.advisories) console.error(`orchestrate: note: ${f.path}:${f.line} ${f.message}`);
1376
+ }
1377
+ return answer;
1378
+ };
1379
+
868
1380
  if ((action === 'handoff' || action === 'complete-stage')
869
1381
  && [verificationPlan, scope, invariants].some((v) => typeof v === 'string' && v.trim() !== '')) {
870
1382
  console.error(`orchestrate: note: --verification-plan, --scope and --invariants are inputs to doflow-run readiness; this ${action} did not read them.`);
871
1383
  }
872
1384
 
873
1385
  try {
1386
+ if (policy !== undefined && (action !== 'decide-gate' || decision !== undefined || forced || policy !== gate)) {
1387
+ return usageError('orchestrate', '--policy requires decide-gate with the matching --gate and refuses --decision and --forced', json);
1388
+ }
874
1389
  // `--forced` is only read by the two actions that can override a decision. Accepting it
875
1390
  // silently on any other action lets an operator believe a stage completion or skip was
876
1391
  // recorded as forced when nothing recorded it — refuse instead of dropping the flag.
@@ -886,6 +1401,8 @@ function handleOrchestrateCommand({
886
1401
  if (mcpOutcome !== undefined && action !== 'handoff') {
887
1402
  return usageError('orchestrate', `'--mcp-outcome' has no effect on action '${action}' — only handoff records MCP outcomes`, json);
888
1403
  }
1404
+ // A catch-up on an absent run starts it implicitly, which counts as a run start for the pointer.
1405
+ const absentBefore = action === 'catch-up' && taskId ? !orchestrator.readRun(taskId) : false;
889
1406
  let snapshot;
890
1407
  switch (action) {
891
1408
  case 'handoff':
@@ -910,8 +1427,8 @@ function handleOrchestrateCommand({
910
1427
  snapshot = orchestrator.skipStage({ taskId, stageId: stage, reason: reason ?? note });
911
1428
  break;
912
1429
  case 'decide-gate':
913
- if (!taskId || !gate || !decision) return usageError('orchestrate', 'decide-gate requires --task-id, --gate and --decision approve|reject', json);
914
- snapshot = orchestrator.decideGate({ taskId, gateId: gate, decision, note, forced });
1430
+ if (!taskId || !gate || (policy === undefined && !decision)) return usageError('orchestrate', 'decide-gate requires --task-id, --gate and --decision approve|reject or --policy <gateId>', json);
1431
+ snapshot = orchestrator.decideGate({ taskId, gateId: gate, decision, policy, note, forced });
915
1432
  break;
916
1433
  case 'annotate':
917
1434
  if (!taskId || !node || !note) return usageError('orchestrate', 'annotate requires --task-id, --node and --note', json);
@@ -921,18 +1438,44 @@ function handleOrchestrateCommand({
921
1438
  if (!taskId) return usageError('orchestrate', 'status requires --task-id', json);
922
1439
  snapshot = orchestrator.status(taskId);
923
1440
  break;
1441
+ case 'timeline': {
1442
+ const run = orchestrator.requireRun(taskId);
1443
+ const body = timelineOf(run, taskId, runSlugOf(run, runRoot) || featureSlug);
1444
+ if (json) console.log(JSON.stringify(body, null, 2));
1445
+ else {
1446
+ console.log(`Timeline ${body.taskId} ${body.featureSlug || '-'} rev ${body.revision}`);
1447
+ for (const e of body.events) {
1448
+ console.log(`${e.at || '-'} ${e.action} ${e.node || '-'} ${e.outcome || e.decision || '-'}${e.forced ? ' forced' : ''}${e.synthetic ? ' synthetic' : ''}`);
1449
+ }
1450
+ if (body.otherEvents > 0) console.log(`other events: ${body.otherEvents}`);
1451
+ }
1452
+ return finishRuntime(0);
1453
+ }
924
1454
  default:
925
- return usageError('orchestrate', `unknown action '${action}'; valid: start | status | handoff | catch-up | complete-stage | skip-stage | decide-gate | annotate`, json);
1455
+ return usageError('orchestrate', `unknown action '${action}'; valid: start | status | list | timeline | handoff | catch-up | complete-stage | skip-stage | decide-gate | annotate`, json);
1456
+ }
1457
+ // Row 135: the feature pointer follows a `feature` run: set when it starts (`start`, or the `catch-up` that
1458
+ // starts it), dropped once it completes.
1459
+ if ((action === 'start' || absentBefore) && snapshot.taskClass === 'feature' && snapshot.state !== 'COMPLETED') {
1460
+ require('./feature-pointer.js').recordFeature({ storeRoot: state, cwd: source, slug: featureSlug, taskId });
1461
+ } else if (action !== 'status' && snapshot.taskClass === 'feature' && snapshot.state === 'COMPLETED') {
1462
+ require('./feature-pointer.js').clearCheckout({ storeRoot: state, cwd: source, slug: featureSlug });
926
1463
  }
927
1464
  // Only a handoff that recorded work compacts; `deferred` and `standalone` recorded nothing.
928
1465
  if (action === 'handoff' && (snapshot.disposition === 'completed' || snapshot.disposition === 'annotated')) {
929
- snapshot = { ...snapshot, compaction: compactAfterHandoff({ taskId, projectRoot: state }) };
1466
+ snapshot = { ...snapshot, compaction: compactAfterHandoff({ taskId, projectRoot: source }) };
930
1467
  }
931
1468
  if (json) { console.log(JSON.stringify(snapshot, null, 2)); return finishRuntime(0); }
932
1469
  console.log(`Workflow ${snapshot.taskId} [${snapshot.taskClass}] — ${snapshot.state}`);
1470
+ // `status` with no --slug opens the one run of the id under any feature (FU-wrnnaq): say whose it is.
1471
+ if (action === 'status' && featureSlug) console.log(`Feature: ${featureSlug}`);
933
1472
  if (snapshot.disposition === 'standalone') console.log(`Handoff: standalone (${snapshot.reason})`);
934
1473
  else if (snapshot.disposition) console.log(`Handoff: ${snapshot.disposition}`);
935
1474
  if (snapshot.compaction) console.log(`Compaction: ${snapshot.compaction.status}${snapshot.compaction.reason ? ` — ${snapshot.compaction.reason}` : ''}`);
1475
+ if (snapshot.policy) console.log(snapshot.decision === 'pass'
1476
+ ? `${gate} ${snapshot.taskId} approved by policy ${snapshot.policy.id}@${snapshot.policy.digest} (${snapshot.inputs.length} inputs)`
1477
+ : `${gate} ${snapshot.taskId} denied by policy: ${snapshot.reasons.join(', ')}`);
1478
+ if (snapshot.annotations?.hint) console.log(` ${snapshot.annotations.hint}`);
936
1479
  if (snapshot.progress) console.log(`Progress: ${snapshot.progress.done}/${snapshot.progress.total}`);
937
1480
  if (snapshot.current) {
938
1481
  console.log(snapshot.awaitingGate
@@ -950,4 +1493,4 @@ function handleOrchestrateCommand({
950
1493
  }
951
1494
  }
952
1495
 
953
- module.exports = { WorkflowOrchestrator, RUN_STATES, GATE_DECISIONS, STAGE_OUTCOMES, handleOrchestrateCommand };
1496
+ module.exports = { WorkflowOrchestrator, loopWorkflow, RUN_STATES, GATE_DECISIONS, STAGE_OUTCOMES, handleOrchestrateCommand, recordStagePack };