@khoavu882/doflow 2.0.0-beta.4 → 2.0.0

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 (497) hide show
  1. package/README.md +12 -11
  2. package/bin/doflow.js +21 -11
  3. package/core/.antigravity-plugin/plugin.json +1 -3
  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 +7 -17
  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-bash-guard.sh +7 -17
  15. package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
  16. package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
  17. package/core/harnesses/claude/hooks/session-end.sh +23 -10
  18. package/core/harnesses/claude/hooks/session-start.sh +5 -3
  19. package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
  20. package/core/harnesses/claude/hooks/stop-check.sh +5 -3
  21. package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
  22. package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
  23. package/core/harnesses/claude/settings/settings.json +18 -0
  24. package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
  25. package/core/harnesses/codex/agents/core-implementer.toml +1 -0
  26. package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
  27. package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
  28. package/core/harnesses/codex/agents/research-writer.toml +1 -1
  29. package/core/harnesses/codex/agents/reviewer.toml +10 -0
  30. package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
  31. package/core/harnesses/codex/agents/system-architect.toml +1 -1
  32. package/core/harnesses/codex/hooks/lib.sh +44 -25
  33. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
  34. package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
  35. package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
  36. package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
  37. package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
  38. package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
  39. package/core/harnesses/codex/hooks/session-start.sh +5 -3
  40. package/core/harnesses/codex/hooks/stop-check.sh +6 -5
  41. package/core/harnesses/codex/hooks/subagent-audit.sh +18 -6
  42. package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
  43. package/core/harnesses/copilot/hooks/doflow-lib.sh +93 -0
  44. package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
  45. package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +9 -0
  46. package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
  47. package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
  48. package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
  49. package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
  50. package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
  51. package/core/harnesses/copilot/hooks/doflow.json +57 -0
  52. package/core/harnesses/kiro/agents/doflow.json +1 -1
  53. package/core/harnesses/kiro/hooks/lib.sh +26 -6
  54. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +21 -5
  55. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +21 -5
  56. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +21 -5
  57. package/core/harnesses/kiro/hooks/session-start.sh +12 -5
  58. package/core/harnesses/kiro/hooks/stop-check.sh +22 -30
  59. package/core/harnesses/pi/extensions/doflow-guard.js +167 -0
  60. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -12
  61. package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
  62. package/core/harnesses/shared/hooks/bridge/bridge.sh +277 -0
  63. package/core/harnesses/shared/hooks/bridge/envelope.jq +129 -0
  64. package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
  65. package/core/harnesses/shared/hooks/bridge/respond.sh +70 -0
  66. package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +28 -14
  67. package/core/harnesses/shared/hooks/policies/capture-failure.sh +51 -16
  68. package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
  69. package/core/harnesses/shared/hooks/policies/contract.sh +78 -0
  70. package/core/harnesses/shared/hooks/policies/deny-json.sh +35 -9
  71. package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
  72. package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
  73. package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
  74. package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
  75. package/core/harnesses/shared/hooks/policies/lib.sh +65 -40
  76. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +48 -35
  77. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +284 -33
  78. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +180 -110
  79. package/core/harnesses/shared/hooks/policies/session-context.sh +66 -7
  80. package/core/harnesses/shared/hooks/policies/stop-check.sh +61 -23
  81. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +39 -6
  82. package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
  83. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +97 -159
  84. package/core/harnesses/shared/hooks/stream-hook-runner.js +114 -187
  85. package/core/harnesses/shared/locator/doflow-run +2 -3
  86. package/core/registry/advice.json +3 -3
  87. package/core/registry/assets.json +72 -80
  88. package/core/registry/capabilities.json +12 -0
  89. package/core/registry/contracts.json +60 -33
  90. package/core/registry/contribution-points.json +32 -0
  91. package/core/registry/gates.json +32 -0
  92. package/core/registry/guidance-caps.json +40 -0
  93. package/core/registry/harnesses.json +1542 -277
  94. package/core/registry/lifecycle.json +2837 -37
  95. package/core/registry/mcp.json +5 -1
  96. package/core/registry/models.json +7 -1
  97. package/core/registry/package-templates/README.md +19 -0
  98. package/core/registry/package-templates/agent/files/agent.md +9 -0
  99. package/core/registry/package-templates/agent/template.json +4 -0
  100. package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
  101. package/core/registry/package-templates/capability-provider/template.json +11 -0
  102. package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
  103. package/core/registry/package-templates/debate-preset/template.json +4 -0
  104. package/core/registry/package-templates/review-preset/files/preset.json +30 -0
  105. package/core/registry/package-templates/review-preset/template.json +4 -0
  106. package/core/registry/package-templates/skill/files/SKILL.md +8 -0
  107. package/core/registry/package-templates/skill/template.json +4 -0
  108. package/core/registry/package-templates/worker/files/worker.json +92 -0
  109. package/core/registry/package-templates/worker/template.json +11 -0
  110. package/core/registry/packages/doflow/doflow-package.json +15 -0
  111. package/core/registry/packages/doflow/presets/debate-3.json +26 -0
  112. package/core/registry/protocols.json +61 -0
  113. package/core/registry/routes.json +17 -0
  114. package/core/registry/rules.json +431 -0
  115. package/core/registry/settings.json +276 -0
  116. package/core/registry/storage.json +701 -30
  117. package/core/registry/telemetry.json +236 -0
  118. package/core/registry/transcripts.json +142 -0
  119. package/core/registry/verification.json +76 -1
  120. package/core/registry/workflows.json +55 -0
  121. package/core/shared/agent-specs/core-implementer.md +3 -0
  122. package/core/shared/agent-specs/delivery-lead.md +68 -0
  123. package/core/shared/agent-specs/quality-guardian.md +5 -1
  124. package/core/shared/agent-specs/research-writer.md +10 -3
  125. package/core/shared/agent-specs/reviewer.md +53 -0
  126. package/core/shared/agent-specs/spec-analyst.md +5 -6
  127. package/core/shared/agent-specs/system-architect.md +5 -1
  128. package/core/shared/guidance/DOFLOW_CORE.md +1 -5
  129. package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
  130. package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
  131. package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
  132. package/core/shared/guidance/pointers/codex.md +1 -4
  133. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
  134. package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
  135. package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
  136. package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
  137. package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
  138. package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
  139. package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
  140. package/core/shared/guidance/references/ORCHESTRATION.md +1 -2
  141. package/core/shared/guidance/rules/RULES.md +45 -0
  142. package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
  143. package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
  144. package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
  145. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
  146. package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
  147. package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
  148. package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
  149. package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
  150. package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
  151. package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
  152. package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
  153. package/core/shared/scripts/doflow/bash/validate-artifacts.sh +48 -4
  154. package/core/shared/scripts/doflow/bin/doflow-run +154 -35
  155. package/core/shared/skills/do/SKILL.md +12 -10
  156. package/core/shared/skills/do/references/advice_calls.md +26 -0
  157. package/core/shared/skills/do/references/pm_routing.md +1 -2
  158. package/core/shared/skills/do/references/program_mode.md +31 -0
  159. package/core/shared/skills/do-brainstorm/SKILL.md +20 -74
  160. package/core/shared/skills/do-brainstorm/references/handoff_branches.md +35 -0
  161. package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
  162. package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
  163. package/core/shared/skills/do-code-review/SKILL.md +41 -144
  164. package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
  165. package/core/shared/skills/do-code-review/references/tools.md +90 -0
  166. package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
  167. package/core/shared/skills/do-design/SKILL.md +24 -23
  168. package/core/shared/skills/do-diagnose/SKILL.md +14 -4
  169. package/core/shared/skills/do-document/SKILL.md +11 -1
  170. package/core/shared/skills/do-document/references/deep_research.md +1 -2
  171. package/core/shared/skills/do-document/references/embedded-research.md +18 -6
  172. package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
  173. package/core/shared/skills/do-execute-plan/SKILL.md +17 -59
  174. package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
  175. package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
  176. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +28 -1
  177. package/core/shared/skills/do-execute-plan/references/run_position.md +13 -1
  178. package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
  179. package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
  180. package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
  181. package/core/shared/skills/do-flow/SKILL.md +8 -9
  182. package/core/shared/skills/do-flow/references/task_classes.md +2 -2
  183. package/core/shared/skills/do-git/SKILL.md +4 -0
  184. package/core/shared/skills/do-implement/SKILL.md +7 -9
  185. package/core/shared/skills/do-plan/SKILL.md +29 -19
  186. package/core/shared/skills/do-test/SKILL.md +7 -4
  187. package/core/shared/templates/doflow/constitution-template.md +9 -3
  188. package/core/shared/templates/doflow/data-model-template.md +1 -1
  189. package/core/shared/templates/doflow/design-template.md +113 -16
  190. package/core/shared/templates/doflow/plan-template.md +11 -10
  191. package/core/shared/templates/doflow/question-log-template.md +14 -2
  192. package/core/shared/templates/doflow/requirement-template.md +1 -1
  193. package/core/worker-evidence/antigravity/checks.json +14 -0
  194. package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
  195. package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
  196. package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
  197. package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
  198. package/core/worker-evidence/codex/checks.json +38 -0
  199. package/package.json +10 -6
  200. package/src/adapters/antigravity/index.js +226 -113
  201. package/src/adapters/claude/index.js +39 -5
  202. package/src/adapters/codex/agents.js +25 -15
  203. package/src/adapters/codex/hooks.js +40 -5
  204. package/src/adapters/codex/index.js +100 -22
  205. package/src/adapters/codex/native-projection.js +6 -2
  206. package/src/adapters/copilot/index.js +58 -23
  207. package/src/adapters/copy-tree.js +294 -48
  208. package/src/adapters/guidance-manifest.js +71 -0
  209. package/src/adapters/guidance-pointer.js +19 -0
  210. package/src/adapters/hook-commands.js +6 -10
  211. package/src/adapters/index.js +1 -0
  212. package/src/adapters/kiro/index.js +38 -13
  213. package/src/adapters/opencode/index.js +8 -8
  214. package/src/adapters/pi/hooks.js +120 -0
  215. package/src/adapters/pi/index.js +48 -16
  216. package/src/adapters/resolve-source.js +26 -2
  217. package/src/cli/commands/install.js +3 -2
  218. package/src/cli/commands/reconcile.js +53 -9
  219. package/src/cli/commands/remove.js +2 -2
  220. package/src/cli/commands/rollback.js +6 -3
  221. package/src/cli/commands/self-update.js +18 -9
  222. package/src/cli/commands/status.js +21 -2
  223. package/src/cli/commands/ui.js +99 -0
  224. package/src/cli/commands/update.js +3 -2
  225. package/src/cli/index.js +338 -33
  226. package/src/cli/runtime-commands.js +243 -52
  227. package/src/cli/shared.js +0 -2
  228. package/src/helper/git.js +1 -1
  229. package/src/helper/guardrails.js +1 -1
  230. package/src/helper/marker-merge.js +31 -13
  231. package/src/helper/package-ids.js +37 -0
  232. package/src/helper/semver.js +94 -0
  233. package/src/install/backup.js +105 -15
  234. package/src/install/context.js +1 -1
  235. package/src/install/targets.js +31 -16
  236. package/src/lifecycle/coverage.js +74 -0
  237. package/src/lifecycle/hook-step.js +61 -0
  238. package/src/lifecycle/hook-tier.js +160 -0
  239. package/src/lifecycle/index.js +434 -86
  240. package/src/lifecycle/retired-harnesses.js +318 -0
  241. package/src/lifecycle/view.js +35 -15
  242. package/src/registry/index.js +686 -32
  243. package/src/registry/worker-ids.js +45 -0
  244. package/src/release/build-package.js +0 -3
  245. package/src/release/docs-blocks.js +492 -0
  246. package/src/release/preflight.js +425 -0
  247. package/src/release/rehearse.js +401 -0
  248. package/src/release/release-workflow.js +733 -0
  249. package/src/release/sync-plugin-versions.js +43 -7
  250. package/src/runtime/advice/bench-gate.js +25 -0
  251. package/src/runtime/advice/cli.js +59 -16
  252. package/src/runtime/advice/client.js +32 -9
  253. package/src/runtime/advice/config.js +98 -133
  254. package/src/runtime/advice/doctor.js +1 -1
  255. package/src/runtime/advice/elevation.js +97 -0
  256. package/src/runtime/advice/endpoint-guard.js +9 -3
  257. package/src/runtime/advice/mode.js +37 -0
  258. package/src/runtime/advice/record.js +18 -5
  259. package/src/runtime/advice/registry.js +4 -1
  260. package/src/runtime/advice/shadow-config.js +229 -0
  261. package/src/runtime/advice/shadow-dispatcher.js +276 -0
  262. package/src/runtime/brief-rules.js +81 -0
  263. package/src/runtime/brief-tools.js +111 -34
  264. package/src/runtime/candidate.js +514 -0
  265. package/src/runtime/capabilities/executor.js +450 -0
  266. package/src/runtime/capabilities/loopback.js +41 -0
  267. package/src/runtime/capabilities/sanitizer.js +88 -0
  268. package/src/runtime/capability-router.js +135 -3
  269. package/src/runtime/checkouts.js +73 -11
  270. package/src/runtime/claims.js +27 -12
  271. package/src/runtime/cli-result.js +11 -4
  272. package/src/runtime/cli.js +221 -277
  273. package/src/runtime/config-home.js +42 -0
  274. package/src/runtime/context/arbitrate.js +767 -0
  275. package/src/runtime/context/budget.js +203 -0
  276. package/src/runtime/context/compose.js +1367 -0
  277. package/src/runtime/context/delivery-store.js +100 -0
  278. package/src/runtime/context/emission-rules.js +118 -0
  279. package/src/runtime/context/identity.js +227 -0
  280. package/src/runtime/context/mandatory.js +594 -0
  281. package/src/runtime/context/pointer.js +142 -0
  282. package/src/runtime/context/primary-command.js +117 -0
  283. package/src/runtime/context/primary.js +344 -0
  284. package/src/runtime/context/summary-store.js +655 -0
  285. package/src/runtime/context-pack.js +1106 -245
  286. package/src/runtime/decision-register.js +13 -4
  287. package/src/runtime/dispatch/admission.js +392 -0
  288. package/src/runtime/dispatch/attempt-end.js +274 -0
  289. package/src/runtime/dispatch/command.js +628 -0
  290. package/src/runtime/dispatch/controls.js +904 -0
  291. package/src/runtime/dispatch/envelope.js +276 -0
  292. package/src/runtime/dispatch/grants.js +310 -0
  293. package/src/runtime/dispatch/integrate.js +617 -0
  294. package/src/runtime/dispatch/loop.js +1835 -0
  295. package/src/runtime/dispatch/store.js +352 -0
  296. package/src/runtime/dispatch/verify-run.js +201 -0
  297. package/src/runtime/evidence-fields.js +39 -0
  298. package/src/runtime/evidence-ledger.js +41 -7
  299. package/src/runtime/failure/capture.js +3 -1
  300. package/src/runtime/failure/cli.js +25 -9
  301. package/src/runtime/failure/home.js +57 -30
  302. package/src/runtime/failure/store.js +7 -2
  303. package/src/runtime/feature-pointer.js +174 -0
  304. package/src/runtime/feature-resolve.js +13 -2
  305. package/src/runtime/fleet/command.js +77 -0
  306. package/src/runtime/fleet/projects.js +178 -0
  307. package/src/runtime/fleet/tree.js +383 -0
  308. package/src/runtime/freshness.js +77 -10
  309. package/src/runtime/git-memo.js +46 -0
  310. package/src/runtime/guidance/ablation.js +427 -0
  311. package/src/runtime/health.js +565 -17
  312. package/src/runtime/history-compactor.js +13 -3
  313. package/src/runtime/implementation-gate.js +277 -22
  314. package/src/runtime/inventory/coverage.js +1 -1
  315. package/src/runtime/inventory/index.js +1 -1
  316. package/src/runtime/inventory/precedence.js +2 -2
  317. package/src/runtime/inventory/siblings.js +6 -4
  318. package/src/runtime/knowledge/code-corpus.js +277 -0
  319. package/src/runtime/knowledge/code-lexical.js +130 -0
  320. package/src/runtime/knowledge/code-retrieval.js +307 -0
  321. package/src/runtime/knowledge/code-validation.js +106 -0
  322. package/src/runtime/knowledge/gp3-gate.js +74 -0
  323. package/src/runtime/knowledge/graph-adapter.js +288 -0
  324. package/src/runtime/knowledge/graph-fallback.js +394 -0
  325. package/src/runtime/knowledge/hybrid.js +136 -8
  326. package/src/runtime/knowledge/retrieval.js +34 -1
  327. package/src/runtime/knowledge/rtk-compress.js +77 -0
  328. package/src/runtime/knowledge/semble-bridge.py +106 -0
  329. package/src/runtime/knowledge/semble-lifecycle.js +114 -0
  330. package/src/runtime/knowledge/semble-provider.js +365 -0
  331. package/src/runtime/lifecycle/cli.js +16 -6
  332. package/src/runtime/lifecycle/event-store.js +8 -6
  333. package/src/runtime/lifecycle/followup.js +6 -1
  334. package/src/runtime/lifecycle/overview.js +24 -1
  335. package/src/runtime/lifecycle/report-store.js +8 -10
  336. package/src/runtime/lifecycle/retention.js +95 -31
  337. package/src/runtime/lifecycle/root.js +9 -1
  338. package/src/runtime/lifecycle/status.js +34 -17
  339. package/src/runtime/lifecycle/store-upkeep.js +56 -7
  340. package/src/runtime/mask.js +134 -7
  341. package/src/runtime/mcp-consultations.js +40 -6
  342. package/src/runtime/model-router.js +79 -7
  343. package/src/runtime/models/catalogue.js +191 -0
  344. package/src/runtime/models/command.js +153 -0
  345. package/src/runtime/off-words.js +12 -0
  346. package/src/runtime/outcome.js +3 -3
  347. package/src/runtime/packages/command.js +1009 -0
  348. package/src/runtime/packages/contributions.js +116 -0
  349. package/src/runtime/packages/digest.js +132 -0
  350. package/src/runtime/packages/fetch.js +182 -0
  351. package/src/runtime/packages/grants.js +192 -0
  352. package/src/runtime/packages/index-file.js +134 -0
  353. package/src/runtime/packages/instruction.js +90 -0
  354. package/src/runtime/packages/manifest.js +331 -0
  355. package/src/runtime/packages/points.js +477 -0
  356. package/src/runtime/packages/programs.js +216 -0
  357. package/src/runtime/packages/scaffold.js +277 -0
  358. package/src/runtime/packages/source.js +90 -0
  359. package/src/runtime/packages/state.js +164 -0
  360. package/src/runtime/packages/store.js +449 -0
  361. package/src/runtime/packages/tar.js +106 -0
  362. package/src/runtime/packages/view.js +406 -0
  363. package/src/runtime/plan-check.js +59 -0
  364. package/src/runtime/plan-content.js +324 -0
  365. package/src/runtime/policy-init.js +118 -0
  366. package/src/runtime/profiles/doctor.js +96 -0
  367. package/src/runtime/profiles/resolve.js +244 -0
  368. package/src/runtime/profiles/rollback.js +254 -0
  369. package/src/runtime/profiles/sampler.js +41 -0
  370. package/src/runtime/readiness-record.js +16 -11
  371. package/src/runtime/readiness.js +127 -16
  372. package/src/runtime/research-request.js +50 -25
  373. package/src/runtime/retrieval-plan.js +402 -73
  374. package/src/runtime/retrieval-record.js +293 -0
  375. package/src/runtime/review/command.js +246 -0
  376. package/src/runtime/review/dominance.js +68 -0
  377. package/src/runtime/review/findings.js +434 -0
  378. package/src/runtime/review/identity.js +255 -0
  379. package/src/runtime/review/protocol-command.js +109 -0
  380. package/src/runtime/review/protocols.js +405 -0
  381. package/src/runtime/review/resolve-protocol.js +302 -0
  382. package/src/runtime/review/start.js +530 -0
  383. package/src/runtime/roadmap/facts.js +479 -0
  384. package/src/runtime/roadmap/gates.js +270 -0
  385. package/src/runtime/roadmap/loop.js +1636 -0
  386. package/src/runtime/roadmap/merge.js +381 -0
  387. package/src/runtime/roadmap/rows.js +1113 -0
  388. package/src/runtime/roadmap/seed.js +951 -0
  389. package/src/runtime/rules-report.js +336 -0
  390. package/src/runtime/scaffold/artifacts.js +63 -9
  391. package/src/runtime/scaffold/generate.js +21 -9
  392. package/src/runtime/settings/autopilot.js +106 -0
  393. package/src/runtime/settings/cli.js +280 -0
  394. package/src/runtime/settings/config-write.js +314 -0
  395. package/src/runtime/settings/files.js +271 -0
  396. package/src/runtime/settings/legacy.js +154 -0
  397. package/src/runtime/settings/lexical.js +56 -0
  398. package/src/runtime/settings/prepared-run.js +269 -0
  399. package/src/runtime/settings/project-layers.js +118 -0
  400. package/src/runtime/settings/refs.js +288 -0
  401. package/src/runtime/settings/registry.js +553 -0
  402. package/src/runtime/settings/resolve.js +713 -0
  403. package/src/runtime/settings/shared.js +37 -0
  404. package/src/runtime/settings/worker-billing.js +107 -0
  405. package/src/runtime/settings/write.js +269 -0
  406. package/src/runtime/slot-command.js +78 -0
  407. package/src/runtime/slots.js +671 -0
  408. package/src/runtime/source-version.js +290 -0
  409. package/src/runtime/state-root.js +217 -0
  410. package/src/runtime/storage/cli.js +12 -5
  411. package/src/runtime/storage/engine.js +9 -2
  412. package/src/runtime/storage/registry.js +14 -1
  413. package/src/runtime/storage/roots.js +66 -16
  414. package/src/runtime/storage/rules.js +82 -10
  415. package/src/runtime/storage/upkeep.js +10 -2
  416. package/src/runtime/storage/work-status.js +245 -26
  417. package/src/runtime/task-attempts.js +589 -69
  418. package/src/runtime/task-classifier.js +47 -11
  419. package/src/runtime/task-graph.js +373 -0
  420. package/src/runtime/task-run.js +778 -0
  421. package/src/runtime/task-scope.js +202 -56
  422. package/src/runtime/task-sources.js +147 -0
  423. package/src/runtime/task-state.js +440 -65
  424. package/src/runtime/team/admission.js +301 -0
  425. package/src/runtime/team/envelope.js +324 -0
  426. package/src/runtime/team/review-guard.js +156 -0
  427. package/src/runtime/trace/analyze.js +427 -0
  428. package/src/runtime/trace/baseline.js +770 -0
  429. package/src/runtime/trace/ledger.js +63 -140
  430. package/src/runtime/trace/otlp-export.js +568 -0
  431. package/src/runtime/trace/otlp-map.js +481 -0
  432. package/src/runtime/trace/spans.js +745 -0
  433. package/src/runtime/trace/telemetry.js +225 -0
  434. package/src/runtime/trace/views.js +1 -0
  435. package/src/runtime/transcripts/command.js +98 -0
  436. package/src/runtime/transcripts/cursor.js +326 -0
  437. package/src/runtime/transcripts/read.js +126 -0
  438. package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
  439. package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
  440. package/src/runtime/transcripts/readers/copilot-events.js +90 -0
  441. package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
  442. package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
  443. package/src/runtime/transcripts/registry.js +161 -0
  444. package/src/runtime/transcripts/stats.js +161 -0
  445. package/src/runtime/verification/check-key.js +628 -0
  446. package/src/runtime/verification/contract-runner.js +546 -11
  447. package/src/runtime/verification/engine.js +499 -9
  448. package/src/runtime/verification/impact.js +617 -0
  449. package/src/runtime/verification/inflight.js +205 -0
  450. package/src/runtime/verification/registry.js +176 -0
  451. package/src/runtime/verification/reuse.js +184 -0
  452. package/src/runtime/verification/telemetry-emit.js +254 -0
  453. package/src/runtime/verification/test-selection.js +67 -7
  454. package/src/runtime/worker/active.js +90 -0
  455. package/src/runtime/worker/cancel.js +222 -0
  456. package/src/runtime/worker/catalogue.js +122 -0
  457. package/src/runtime/worker/command.js +677 -0
  458. package/src/runtime/worker/evidence.js +288 -0
  459. package/src/runtime/worker/launch.js +288 -0
  460. package/src/runtime/worker/models.js +137 -0
  461. package/src/runtime/worker/parsers.js +218 -0
  462. package/src/runtime/worker/probes/index.js +706 -0
  463. package/src/runtime/worker/process-tree.js +415 -0
  464. package/src/runtime/worker/quota.js +43 -0
  465. package/src/runtime/worker/readiness.js +713 -0
  466. package/src/runtime/worker/run-store.js +166 -0
  467. package/src/runtime/worker/supervisor.js +541 -0
  468. package/src/runtime/worker/worktree-check.js +252 -0
  469. package/src/runtime/workflow-engine.js +5 -1
  470. package/src/runtime/workflow-orchestrator.js +578 -63
  471. package/src/runtime/worktree.js +149 -22
  472. package/src/state/index.js +1 -1
  473. package/src/state/lockfile.js +64 -10
  474. package/src/state/run-lock.js +1 -1
  475. package/types/node-commonjs.d.ts +106 -7
  476. package/core/.antigravity-plugin/hooks.json +0 -39
  477. package/core/harnesses/gemini/hooks/hooks.json +0 -77
  478. package/core/harnesses/gemini/hooks/lib.sh +0 -302
  479. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -21
  480. package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
  481. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -21
  482. package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
  483. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -22
  484. package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
  485. package/core/harnesses/gemini/hooks/session-end.sh +0 -5
  486. package/core/harnesses/gemini/hooks/session-start.sh +0 -5
  487. package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
  488. package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
  489. package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
  490. package/core/harnesses/shared/hooks/hooks.json +0 -47
  491. package/core/shared/guidance/FLAGS.md +0 -25
  492. package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
  493. package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
  494. package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
  495. package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
  496. package/src/adapters/gemini/hooks.js +0 -210
  497. package/src/adapters/gemini/index.js +0 -390
@@ -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),
@@ -106,8 +204,12 @@ class WorkflowOrchestrator {
106
204
 
107
205
 
108
206
 
207
+
109
208
 
110
209
 
210
+
211
+
212
+
111
213
  /**
112
214
  * @param {Object} [options]
113
215
  * @param {string} [options.repoRoot] install root owning core/registry + default state dir.
@@ -123,39 +225,56 @@ class WorkflowOrchestrator {
123
225
  * `none` (no ledger) records the stage as `unverified` whatever outcome was supplied. Unwired
124
226
  * means the stage is not checked, which only a unit test that builds the orchestrator by hand
125
227
  * relies on: the CLI always wires the real one.
228
+ * @param {(node: object, run: object) => {refusals: Array<{path: string, line: number, message: string}>, advisories: Array<object>}} [options.planExitCheck]
229
+ * row 134 gate for the do-plan planning stage: every refusal (a constitution MUST marked FAIL or
230
+ * without a row) refuses completion. Unwired means the stage is not checked, as for taskAttemptCheck.
126
231
  * @param {object} [options.fsImpl]
127
232
  * @param {string|null} [options.featureSlug] the feature a run started here belongs to
233
+ * @param {object} [options.env] the environment `start` reads the caller's identity and the telemetry switch from
234
+ * @param {string} [options.cwd] the directory `start` resolves the caller's run key and the 080 store from
128
235
  * @param {() => object} [options.buildAdapters] builds the adapter registry `mcpReach` reads installs
129
236
  * through; without it install facts are unknown and no outcome is inferred from them
130
237
  */
131
- constructor({ repoRoot = REPO_ROOT, projectRoot, stateDir, engine, readinessEvaluate, taskAttemptCheck, fsImpl, featureSlug = null, buildAdapters = null } = {}) {
238
+ constructor({ repoRoot = REPO_ROOT, projectRoot, sourceRoot, stateDir, engine, readinessEvaluate, taskAttemptCheck, planExitCheck, fsImpl, featureSlug = null, buildAdapters = null, env = process.env, cwd = process.cwd() } = {}) {
132
239
  this.fsImpl = fsImpl || fs;
133
240
  this.repoRoot = repoRoot;
134
- this.projectRoot = projectRoot || (stateDir?.endsWith(path.join('.doflow', 'state', 'orchestration'))
241
+ this.projectRoot = projectRoot || (stateDir?.endsWith(RUN_STORE_REL)
135
242
  ? path.resolve(stateDir, '../../..') : repoRoot);
243
+ this.sourceRoot = sourceRoot || this.projectRoot;
136
244
  this.engine = engine || new WorkflowEngine({ repoRoot });
137
- this.stateDir = stateDir || path.join(this.projectRoot, '.doflow', 'state', 'orchestration');
245
+ this.stateDir = stateDir || runStoreDir(this.projectRoot);
138
246
  this.readinessEvaluate = readinessEvaluate || null;
139
247
  this.taskAttemptCheck = taskAttemptCheck || null;
248
+ this.planExitCheck = planExitCheck || null;
140
249
  this.featureSlug = featureSlug;
141
250
  this.buildAdapters = buildAdapters;
251
+ this.env = env;
252
+ this.cwd = cwd;
253
+ }
254
+
255
+ /** The task's run (115 C-1): `<stateDir>/<featureSlug>/<taskId>.json` for a task id other than its
256
+ * feature's slug, else `<stateDir>/<taskId>.json`; a flat run of a namespaced id is read, and copied
257
+ * into the namespace on its next write, only when it is this feature's or names none (D-3). */
258
+ runRecord(taskId ) {
259
+ const id = assertSafeId(taskId, 'taskId');
260
+ const namespace = isSafeSlug(this.featureSlug) && this.featureSlug !== id ? this.featureSlug : null;
261
+ return recordIn({ base: this.stateDir, namespace, name: `${id}.json`, fsImpl: this.fsImpl, refuseUnsafeFlat: true });
142
262
  }
143
263
 
144
264
  runFile(taskId ) {
145
- return path.join(this.stateDir, `${assertSafeId(taskId, 'taskId')}.json`);
265
+ return this.runRecord(taskId).file;
146
266
  }
147
267
 
148
268
  readRun(taskId ) {
149
- const file = this.runFile(taskId);
150
- if (!this.fsImpl.existsSync(file)) return null;
151
- return readTaskState(this.fsImpl, file);
269
+ return readRunRecord(this.fsImpl, this.runRecord(taskId).read, this.stateDir);
152
270
  }
153
271
 
154
272
  writeRun(run , now , beforeWrite ) {
155
273
  run.updatedAt = iso(now);
156
- this.fsImpl.mkdirSync(this.stateDir, { recursive: true });
274
+ const { file, seed } = this.runRecord(run.taskId);
275
+ this.fsImpl.mkdirSync(path.dirname(file), { recursive: true });
157
276
  const expectedRevision = run.revision || 0;
158
- updateTaskState({ file: this.runFile(run.taskId), fsImpl: this.fsImpl, build: (disk ) => {
277
+ updateTaskState({ file, fsImpl: this.fsImpl, prepare: seed, build: (disk ) => {
159
278
  if ((disk?.revision || 0) !== expectedRevision) {
160
279
  throw new Error(`Concurrent workflow update for '${run.taskId}'; reload the run before retrying. Nothing was written.`);
161
280
  }
@@ -185,24 +304,35 @@ class WorkflowOrchestrator {
185
304
  startedAt: iso(now),
186
305
  // The feature the run belongs to, so another checkout can tell it from a run with the same id.
187
306
  ...(this.featureSlug ? { featureSlug: this.featureSlug } : {}),
307
+ ...this.traceFields(),
188
308
  };
189
309
  this.writeRun(run, now);
190
310
  return this.snapshot(run);
191
311
  }
192
312
 
313
+ /**
314
+ * 088 IC-010 / DD-5: the caller's 080 run key and, when the 080 store already holds that run with
315
+ * a trace id, a copy of the id. Empty with telemetry off, for a solo lineage, or on any fault:
316
+ * nothing here can fail or change a start. No id is minted here and no action is added.
317
+ */
318
+ traceFields() {
319
+ try {
320
+ const key = runKeyFor(this.env, this.cwd);
321
+ if (!key.traceIds || refuseSolo(key.identity)) return {};
322
+ const out = { runKey: key.runKey };
323
+ if (this.featureSlug) {
324
+ try {
325
+ const store = readTaskState(this.fsImpl, taskAttemptStoreFile(lifecycleProjectRoot(this.cwd), this.featureSlug));
326
+ const record = store && store.runs && Object.hasOwn(store.runs, key.runKey) ? store.runs[key.runKey] : null;
327
+ if (record && typeof record.traceId === 'string' && /^[0-9a-f]{32}$/.test(record.traceId)) out.traceId = record.traceId;
328
+ } catch { /* an unreadable 080 store means no copy */ }
329
+ }
330
+ return out;
331
+ } catch { return {}; }
332
+ }
333
+
193
334
  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
- };
335
+ return snapshotRun(run);
206
336
  }
207
337
 
208
338
  status(taskId ) {
@@ -244,7 +374,10 @@ class WorkflowOrchestrator {
244
374
  }
245
375
  // The ledger is checked here too, before catch-up writes anything, so a refused implementation
246
376
  // handoff leaves no backfilled stage behind.
247
- if (preview.reason === 'reached-candidate') this.assertTaskAttempts(preview.node, existing || { taskId, taskClass: resolvedClass });
377
+ if (preview.reason === 'reached-candidate') {
378
+ this.assertTaskAttempts(preview.node, existing || { taskId, taskClass: resolvedClass });
379
+ this.assertPlanExit(preview.node, existing || { taskId, taskClass: resolvedClass });
380
+ }
248
381
  const positioned = this.catchUp({ taskId, taskClass: resolvedClass, candidateStageIds: candidates, note, now });
249
382
  if (positioned.reason === 'reached-candidate') {
250
383
  // A run this handoff started has no earlier stage: every evidence item counts.
@@ -295,8 +428,9 @@ class WorkflowOrchestrator {
295
428
  const harness = (registry.harnesses || []).find((item ) => item.id === id);
296
429
  return harness ? harness.displayName || harness.id : id;
297
430
  };
298
- const { briefRecords, repoRoot } = stageId ? this.readBriefRecords(taskId) : { briefRecords: [], repoRoot: this.projectRoot };
299
- if (stageId) this.recordBriefTierAdvice({ briefRecords, taskId, stageId, repoRoot });
431
+ const { briefRecords, repoRoot } = stageId ? this.readBriefRecords(taskId) : { briefRecords: [], repoRoot: this.sourceRoot };
432
+ // Advice picks are project state, kept with the run's records, not in the feature folder's checkout.
433
+ if (stageId) this.recordBriefTierAdvice({ briefRecords, taskId, stageId, repoRoot: this.projectRoot });
300
434
  try {
301
435
  const result = resolveConsultations({
302
436
  taskId, stageId, skill, flags, registry, ledger: {
@@ -305,7 +439,7 @@ class WorkflowOrchestrator {
305
439
  return ledger.queryEvidence({ taskId: id });
306
440
  },
307
441
  },
308
- requests: new ResearchRequestStore({ projectRoot: this.projectRoot, fsImpl: this.fsImpl, slug: this.featureSlug }),
442
+ requests: new ResearchRequestStore({ projectRoot: this.projectRoot, sourceRoot: this.sourceRoot, fsImpl: this.fsImpl, slug: this.featureSlug }),
309
443
  briefRecords,
310
444
  readReport: (file ) => {
311
445
  const target = path.resolve(repoRoot, file);
@@ -389,28 +523,28 @@ class WorkflowOrchestrator {
389
523
  }
390
524
 
391
525
  /** 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. */
526
+ * are left out. They are feature-folder files, so they are resolved from the checkout the verb runs
527
+ * in with the resolver's `feature_root` (that checkout's folder, else the main checkout's), never
528
+ * from the state root (115 C-10). Resolving the feature spawns the resolver, so it is only done
529
+ * where a feature folder exists above one of the two. */
394
530
  readBriefRecords(taskId ) {
395
- const none = { briefRecords: [], repoRoot: this.projectRoot };
531
+ const none = { briefRecords: [], repoRoot: this.sourceRoot };
396
532
  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
- }
533
+ const hasFolder = [this.sourceRoot, this.projectRoot].some((root) => {
534
+ for (let dir = path.resolve(root); ; dir = path.dirname(dir)) {
535
+ if (this.fsImpl.existsSync(path.join(dir, 'agent-docs', 'doflow'))) return true;
536
+ if (path.dirname(dir) === dir) return false;
537
+ }
538
+ });
405
539
  if (!hasFolder) return none;
406
- const feature = resolveActiveFeature({ projectRoot: this.projectRoot, slug: taskId });
540
+ const feature = resolveActiveFeature({ projectRoot: this.sourceRoot, slug: taskId });
407
541
  if (feature.error) return none;
408
- const exec = path.join(feature.featureDir, 'exec');
542
+ const exec = path.join(feature.featureFolder, 'exec');
409
543
  const briefRecords = [];
410
544
  for (const name of this.fsImpl.readdirSync(exec).filter((entry ) => entry.endsWith('-brief.json')).sort()) {
411
545
  try { briefRecords.push(JSON.parse(this.fsImpl.readFileSync(path.join(exec, name), 'utf8'))); } catch { /* an unreadable record contributes nothing */ }
412
546
  }
413
- return { briefRecords, repoRoot: feature.repoRoot };
547
+ return { briefRecords, repoRoot: feature.featureRoot };
414
548
  } catch { return none; }
415
549
  }
416
550
 
@@ -463,15 +597,21 @@ class WorkflowOrchestrator {
463
597
  * stage is always imported+unverified, whatever the caller says: catch-up importing history
464
598
  * cannot import a verification. A 'failed' outcome still advances — recording the failure
465
599
  * 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. */
600
+ * observing the expected failure — but the record no longer reads as success.
601
+ *
602
+ * Issue #9 stage 3: the entry names the context packs the stage was handed (`packs`), copied from
603
+ * what `recordStagePack` wrote on this node while it was the run's active stage. A stage handed
604
+ * none gets no `packs`; a malformed item is left out, never an error. */
467
605
  completeStage({ taskId, stageId, note, outcome, backfilled = false, consultations, now } = {}) {
468
606
  const run = this.requireRun(taskId);
469
607
  const node = this.expectOpenStage(run, stageId);
608
+ const packs = stagePacks(node.packs);
470
609
  if (outcome !== undefined && !STAGE_OUTCOMES.has(outcome)) {
471
610
  throw new Error(`Unknown stage outcome '${outcome}'. Valid: ${[...STAGE_OUTCOMES].join(', ')} — omit the flag to record 'unverified'.`);
472
611
  }
473
612
  if (node.mutatesSource && node.readinessTemplate) this.assertReady(node, run);
474
613
  const attempts = backfilled ? null : this.assertTaskAttempts(node, run);
614
+ this.assertPlanExit(node, run);
475
615
  // No ledger for the feature: the stage is recorded, but nothing verified its tasks, and the
476
616
  // record says so whatever outcome the caller supplied (FR-006).
477
617
  const unledgered = attempts?.state === 'none';
@@ -483,11 +623,12 @@ class WorkflowOrchestrator {
483
623
  note: unledgered ? [note, NO_LEDGER_NOTE].filter(Boolean).join(' ') : (note ?? null),
484
624
  backfilled: Boolean(backfilled), executionStatus: node.executionStatus, outcome: node.outcome,
485
625
  ...(consultations ? { consultations } : {}),
626
+ ...(packs.length ? { packs } : {}),
486
627
  });
487
628
  this.advance(run, now);
488
629
  this.writeRun(run, now, () => {
489
630
  if (run.taskClass !== 'feature') return;
490
- const request = new ResearchRequestStore({ projectRoot: this.projectRoot, fsImpl: this.fsImpl })
631
+ const request = new ResearchRequestStore({ projectRoot: this.projectRoot, sourceRoot: this.sourceRoot, fsImpl: this.fsImpl })
491
632
  .blockingGap(taskId, stageId);
492
633
  if (request) throw new Error(`Research request '${request.id}' blocks stage '${stageId}': ${request.failureReason || request.gap || request.status}`);
493
634
  });
@@ -640,15 +781,22 @@ class WorkflowOrchestrator {
640
781
  * `forced` decision (e.g. approving despite unresolved clarification markers) requires an
641
782
  * explicit `note` reason — never a silent override — and is flagged in history for
642
783
  * `render-audit.sh` to distinguish from a routine handoff. */
643
- decideGate({ taskId, gateId, decision, note, forced = false, now } = {}) {
784
+ decideGate({ taskId, gateId, decision, policy, by, note, forced = false, now, bindHead } = {}) {
644
785
  assertSafeId(gateId, 'gateId');
645
- if (!GATE_DECISIONS.includes(decision)) throw new Error(`Invalid gate decision '${decision}'; valid: ${GATE_DECISIONS.join(', ')}`);
786
+ 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');
787
+ // IC-005: the decider is derived, never named by a caller; `autopilot` is recorded only by --policy.
788
+ if (by !== undefined) throw new Error('decide-gate takes no decider (by); only --policy records autopilot');
789
+ if (policy !== undefined) {
790
+ if (policy !== gateId) throw new Error('--policy must name the gate being decided');
791
+ if (decision !== undefined || forced) throw new Error('--policy refuses --decision and --forced');
792
+ } else if (!GATE_DECISIONS.includes(decision)) throw new Error(`Invalid gate decision '${decision}'; valid: ${GATE_DECISIONS.join(', ')}`);
646
793
  if (forced && (!note || !String(note).trim())) throw new Error('A forced gate decision requires a --note reason');
647
794
  const run = this.requireRun(taskId);
648
795
  if (run.state !== 'AWAITING_GATE') throw new Error(`Run '${run.taskId}' is not awaiting a gate (state=${run.state})`);
649
796
  const node = this.currentNode(run);
650
797
  if (!node || node.type !== 'gate') throw new Error('Run state says AWAITING_GATE but no gate is at the cursor');
651
798
  if (node.id !== gateId) throw new Error(`Expected gate '${node.id}' next, got '${gateId}'`);
799
+ if (policy !== undefined) return this.decideGateByPolicy(run, node, policy, now, bindHead);
652
800
  node.status = decision === 'approve' ? 'approved' : 'rejected';
653
801
  node.decision = decision;
654
802
  run.history.push({ at: iso(now), action: 'decide-gate', node: node.id, detail: decision, note: note ?? null, forced: Boolean(forced) });
@@ -663,6 +811,94 @@ class WorkflowOrchestrator {
663
811
  return this.snapshot(run);
664
812
  }
665
813
 
814
+ /** IC-005: the feature checkout cannot grant itself a weaker gate policy. Denies write nothing. */
815
+ decideGateByPolicy(run , node , policyId , now , bindHead ) {
816
+ // Decision C9: the autopilot kill switch binds every policy decision, not only the loop's. Off on
817
+ // any layer (env, sentinel, settings; an unreadable setting fails safe to off) denies before any
818
+ // fact is read, and records nothing.
819
+ const { readKillSwitch } = require('./settings/autopilot.js');
820
+ const killSwitch = readKillSwitch(this.env);
821
+ if (!killSwitch.enabled) {
822
+ return { ...this.snapshot(run), decision: 'deny', reason: 'autopilot-disabled', reasons: ['autopilot-disabled'],
823
+ rule: `${node.id}/autopilot`, policy: { id: policyId, digest: null }, inputs: [], annotations: { switch: killSwitch.switch } };
824
+ }
825
+ // Loaded here, not at module top: every other orchestrator path (and a dry-run install) must not import them.
826
+ const { projectLayerSource } = require('./settings/project-layers.js');
827
+ const { parseSeed } = require('./roadmap/rows.js');
828
+ const { loadPolicy, evaluate } = require('./roadmap/gates.js');
829
+ const { gatherGateFacts } = require('./roadmap/facts.js');
830
+ const root = this.sourceRoot;
831
+ const mainRoot = mainCheckoutOf(root);
832
+ const execOptions = { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] };
833
+ let developHead ;
834
+ let baseBytes = null;
835
+ // The base policy is develop's committed blob, never the worktree or the feature branch: a
836
+ // branch that weakens `never` to `approve` must not approve itself. No textconv filter applies.
837
+ try {
838
+ const head = execFileSync('git', ['rev-parse', '--verify', 'refs/heads/develop'], execOptions).trim();
839
+ developHead = head;
840
+ if (/^[0-9a-f]{40}$/.test(head)) {
841
+ baseBytes = execFileSync('git', ['show', '--no-textconv', `${head}:core/registry/gates.json`], execOptions);
842
+ }
843
+ } catch { /* loadPolicy returns no-policy; never fall back to the feature copy. */ }
844
+ const layers = projectLayerSource(mainRoot, this.fsImpl);
845
+ let overrideBytes = null;
846
+ if (layers.layersFrom) {
847
+ try { overrideBytes = this.fsImpl.readFileSync(path.join(layers.layersFrom, 'gates.override.json'), 'utf8'); }
848
+ catch (error ) { if (error.code !== 'ENOENT') overrideBytes = ''; }
849
+ }
850
+ const loaded = loadPolicy(baseBytes, overrideBytes);
851
+ const policy = { id: policyId, digest: loaded.digest };
852
+ if (!loaded.ok) return { ...this.snapshot(run), decision: 'deny', reasons: loaded.reasons, reason: loaded.reason, policy, inputs: [], ...(loaded.annotations ? { annotations: loaded.annotations } : {}) };
853
+
854
+ const slug = runSlugOf(run, root) || this.featureSlug || run.taskId;
855
+ let seed = null;
856
+ let row = null;
857
+ try {
858
+ seed = parseSeed(this.fsImpl.readFileSync(path.join(mainRoot, 'agent-docs/roadmap/rows.json'), 'utf8'));
859
+ row = seed.rows.find((entry ) => entry.slug === slug && entry.class === run.taskClass) || null;
860
+ } catch (error ) {
861
+ if (error.code === 'seed-typed-status') return { ...this.snapshot(run), decision: 'deny', reason: error.code, reasons: [error.code], policy, inputs: [] };
862
+ // Invalid or unreadable seed means no trustworthy row; evaluate returns row-unknown.
863
+ }
864
+ // The feature folder follows the feature-store rule (resolveFeatureInStore): a linked worktree
865
+ // without the folder would otherwise read no artifacts (a false `unresolved-clarifications`) and
866
+ // register into a missing folder.
867
+ const feature = resolveFeatureInStore({ root, slug });
868
+ if (feature.error) throw new Error(`Cannot resolve policy gate artifacts: ${feature.message}`);
869
+ const facts = gatherGateFacts({ root, mainRoot, registryRoot: this.repoRoot, artifactRoot: feature.featureDir,
870
+ gateId: node.id, policy: loaded.policy, row, rows: seed?.rows, developHead, featureBranch: `feat/${slug}`, fsImpl: this.fsImpl, env: this.env });
871
+ const evaluation = evaluate(loaded.policy, run.taskClass, node.id, facts);
872
+ // A deny changes nothing: the run stays AWAITING_GATE and autopilot never rejects.
873
+ if (evaluation.decision === 'deny') return { ...this.snapshot(run), ...evaluation, policy };
874
+
875
+ const audit = `approved by policy ${policyId}@${loaded.digest} (${evaluation.inputs.length} inputs)`;
876
+ node.status = 'approved';
877
+ node.decision = 'approve';
878
+ // `policy.digest` and every fact's {locator, sha256} let a later reader re-derive this decision.
879
+ // C15: the roadmap loop names the feature head it read before deciding (bindHead). The entry keeps
880
+ // it as `approvedHead` only when the gate's own heads fact evaluated that same head, so the merge
881
+ // binds to it across loop runs; an approval without it falls back to the both-heads digest.
882
+ const heads = facts.find((fact ) => fact.name === 'heads' && !fact.unreadable)?.value;
883
+ const approvedHead = bindHead && heads?.featureHead === bindHead ? { approvedHead: bindHead } : {};
884
+ run.history.push({ at: iso(now), action: 'decide-gate', node: node.id, detail: 'approve', by: 'autopilot',
885
+ policy, inputs: evaluation.inputs, ...approvedHead, note: audit, forced: false });
886
+ run.state = runState('RUNNING');
887
+ run.cursor += 1;
888
+ this.settleCursor(run);
889
+ // The optimistic revision check runs before registering a decision, so a stale run cannot
890
+ // leave a forged approval. A register refusal likewise prevents the journal from advancing.
891
+ this.writeRun(run, now, () => {
892
+ const registered = addDecisions({ featureDir: feature.featureDir, slug, now: now ?? new Date(), fsImpl: this.fsImpl, items: [{
893
+ topic: node.id, statement: `${node.id} ${audit}.`, channel: 'policy', stage: POLICY_GATE_STAGE[node.id],
894
+ rationale: `Every predicate of ${node.id} passed on the inputs digest ${evaluation.inputsDigest}.`,
895
+ source: `git:${developHead}:core/registry/gates.json`,
896
+ }] });
897
+ if (registered.finding) throw new Error(`Cannot register policy approval: ${registered.finding}: ${registered.message}`);
898
+ });
899
+ return { ...this.snapshot(run), ...evaluation, policy };
900
+ }
901
+
666
902
  /** Append a pure audit-trail entry — a stage re-run after its own handoff, or an artifact
667
903
  * hand-edited after that stage's own handoff — without touching program/cursor/state. Not a
668
904
  * stage completion or a gate decision, so it never collides with the state machine's ordering
@@ -731,6 +967,20 @@ class WorkflowOrchestrator {
731
967
  return answer;
732
968
  }
733
969
 
970
+ /** Row 134: the planning stage (the `do-plan` stage) cannot complete while the plan's
971
+ * Constitution Check leaves a MUST principle marked FAIL or without a row. There is no waiver:
972
+ * the plan is revised, or a person amends the constitution. A stage catch-up backfills is checked
973
+ * too, so a refused plan is never imported as complete. */
974
+ assertPlanExit(node , run ) {
975
+ if (!this.planExitCheck || node.skill !== 'do-plan') return null;
976
+ const answer = this.planExitCheck(node, run);
977
+ if (!answer || !Array.isArray(answer.refusals)) throw new Error(`Plan exit check returned no usable answer for stage '${node.id}'`);
978
+ if (answer.refusals.length) {
979
+ throw new Error(`Stage '${node.id}' cannot be completed: ${answer.refusals.map((f ) => `${f.path}:${f.line} ${f.message}`).join('; ')}`);
980
+ }
981
+ return answer;
982
+ }
983
+
734
984
  /** Where `catchUp` would stop for these candidates, worked out on a copy of the run's program (or
735
985
  * of a fresh run's) without writing anything. Same stop rules and reason vocabulary as `catchUp`;
736
986
  * `stageId` is what `catchUp` reports as `caughtUpTo`, and `node` the program node it stops on.
@@ -768,6 +1018,26 @@ class WorkflowOrchestrator {
768
1018
  }
769
1019
  }
770
1020
 
1021
+ /** The project's main checkout, or `root` itself when the checkouts cannot be listed. Loaded here, not at
1022
+ * module top: every other orchestrator path (and a dry-run install) must not import the resolver. */
1023
+ function mainCheckoutOf(root ) {
1024
+ const { listCheckouts } = require('./checkouts.js');
1025
+ const checkouts = listCheckouts({ cwd: root });
1026
+ return checkouts.ok && checkouts.main ? checkouts.main : root;
1027
+ }
1028
+
1029
+ /**
1030
+ * The feature folder follows the feature-store rule, which the feature resolver already applies: the
1031
+ * calling checkout's own folder, else the main checkout's when a linked worktree holds none
1032
+ * (`featureFolder` under `featureRoot`). `featureDir` and `repoRoot` of the result name that folder and
1033
+ * checkout, as `decision --action compact` reads them, so a caller never looks in the wrong checkout.
1034
+ */
1035
+ function resolveFeatureInStore({ root, slug } ) {
1036
+ const feature = resolveActiveFeature({ projectRoot: root, slug });
1037
+ if (feature.error) return feature;
1038
+ return { ...feature, featureDir: feature.featureFolder, repoRoot: feature.featureRoot };
1039
+ }
1040
+
771
1041
  /**
772
1042
  * IC-013: fold the finished stage's History into the feature's archive. The task id is the feature
773
1043
  * slug. This is housekeeping after a recorded handoff, so it reports and never throws: the
@@ -777,7 +1047,7 @@ class WorkflowOrchestrator {
777
1047
  */
778
1048
  function compactAfterHandoff({ taskId, projectRoot } ) {
779
1049
  try {
780
- const feature = resolveActiveFeature({ projectRoot, slug: taskId });
1050
+ const feature = resolveFeatureInStore({ root: projectRoot, slug: taskId });
781
1051
  if (feature.error) return { status: 'skipped', reason: feature.message };
782
1052
  // A task id such as a plan task (`B.1`) resolves on a feature branch to a folder that was
783
1053
  // never created; say that, rather than blaming a missing register.
@@ -797,41 +1067,242 @@ function compactAfterHandoff({ taskId, projectRoot } ) {
797
1067
  }
798
1068
  }
799
1069
 
1070
+ /**
1071
+ * IC-001: `orchestrate --action list`, every run the resolver can see as a prompt-free row plus the
1072
+ * records it could not read, by reason code only. Reads and prints; writes nothing.
1073
+ */
1074
+ /**
1075
+ * The first flag given that a read action does not take, or undefined. Every flag another action
1076
+ * reads is refused before any read: a filter the read does not apply must not look applied, and an
1077
+ * outcome nothing records must not look recorded. `takes` names the flags the action does read.
1078
+ */
1079
+ function noEffectFlag({ taskId, taskClass, gate, decision, policy, forced, note, reason, stage, node, verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, slug, dryRun } , takes = []) {
1080
+ const supplied = (value ) => value !== undefined && value !== null;
1081
+ const given = [
1082
+ ['--task-id', supplied(taskId)], ['--task-class', supplied(taskClass)], ['--gate', supplied(gate)],
1083
+ ['--decision', supplied(decision)], ['--policy', supplied(policy)],
1084
+ ['--forced', forced], ['--note', supplied(note)], ['--reason', supplied(reason)], ['--stage', supplied(stage)],
1085
+ ['--node', supplied(node)], ['--verification-plan', supplied(verificationPlan)], ['--scope', supplied(scope)],
1086
+ ['--invariants', supplied(invariants)], ['--result', supplied(result)], ['--mcp-outcome', supplied(mcpOutcome)],
1087
+ ['--calling-skill', supplied(callingSkill)], ['--slug', supplied(slug)], ['--dry-run', Boolean(dryRun)],
1088
+ ].find(([flag, on]) => on && !takes.includes(flag ));
1089
+ return given ? given[0] : undefined;
1090
+ }
1091
+
1092
+ /** A journal time as an ISO instant, else null, so no other journal text reaches a read's output. */
1093
+ const journalTime = (value ) => typeof value === 'string'
1094
+ && /^\d{4}-\d{2}-\d{2}T/.test(value) && !Number.isNaN(Date.parse(value)) ? value : null;
1095
+
1096
+ /** The journal actions a timeline shows (093 IC-012); any other is counted in `otherEvents`. */
1097
+ const TIMELINE_ACTIONS = new Set(['start', 'complete-stage', 'skip-stage', 'skip-gate', 'decide-gate', 'annotate', 'complete-run']);
1098
+ const ISO_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
1099
+
1100
+ /**
1101
+ * 093 IC-012: one run's history as closed values only. Each event copies the action, an id-shaped
1102
+ * node, the stage outcome (complete-stage), the gate decision (decide-gate's `detail`), `forced`, and
1103
+ * whether the entry records no work done (the indicators rule, trace/indicators.ts isSynthetic).
1104
+ * Notes, detail text, deciders, policy inputs, consultations and trace ids are never read.
1105
+ */
1106
+ function timelineOf(run , taskId , featureSlug ) {
1107
+ const at = (value ) => journalTime(value) !== null && ISO_INSTANT.test(value) ? value : null;
1108
+ const events = [];
1109
+ let otherEvents = 0;
1110
+ for (const entry of Array.isArray(run.history) ? run.history : []) {
1111
+ if (!entry || typeof entry !== 'object' || !TIMELINE_ACTIONS.has(entry.action)) {
1112
+ otherEvents += 1;
1113
+ continue;
1114
+ }
1115
+ events.push({
1116
+ at: at(entry.at),
1117
+ action: entry.action,
1118
+ node: typeof entry.node === 'string' && ID_PATTERN.test(entry.node) ? entry.node : null,
1119
+ outcome: entry.action === 'complete-stage' && STAGE_OUTCOMES.has(entry.outcome) ? entry.outcome : null,
1120
+ decision: entry.action === 'decide-gate' && GATE_DECISIONS.includes(entry.detail) ? entry.detail : null,
1121
+ forced: entry.forced === true,
1122
+ synthetic: Boolean(entry.backfilled) || Boolean(entry.executionStatus && entry.executionStatus !== 'completed'),
1123
+ });
1124
+ }
1125
+ return {
1126
+ v: 1,
1127
+ taskId,
1128
+ featureSlug: typeof featureSlug === 'string' && ID_PATTERN.test(featureSlug) ? featureSlug : null,
1129
+ revision: Number.isInteger(run.revision) && run.revision >= 0 ? run.revision : 0,
1130
+ startedAt: at(run.startedAt),
1131
+ events,
1132
+ otherEvents,
1133
+ };
1134
+ }
1135
+
1136
+ function listRuns({ json, stateRoot, ...flags } ) {
1137
+ const { finishRuntime, usageError } = require('./cli-result.js');
1138
+ // list takes no filter, so a `--slug` must not look like one.
1139
+ const noEffect = noEffectFlag(flags);
1140
+ if (noEffect) return usageError('orchestrate', `'${noEffect}' has no effect on action 'list'`, json);
1141
+
1142
+ const resolverFailure = () => {
1143
+ if (json) console.log(JSON.stringify({ v: 1, error: 'cannot-list-checkouts' }, null, 2));
1144
+ else console.error('[ERROR] orchestrate: cannot list checkouts');
1145
+ return finishRuntime(1);
1146
+ };
1147
+ let resolved ;
1148
+ try { resolved = orchestrationRuns({ stateRoot: stateRoot || process.cwd() }); } catch { return resolverFailure(); }
1149
+ if (!resolved.ok) return resolverFailure();
1150
+
1151
+ const runs = [];
1152
+ const unreadable = [];
1153
+ const textCompare = (a , b ) => a < b ? -1 : a > b ? 1 : 0;
1154
+ const slugCompare = (a , b ) => a === null ? (b === null ? 0 : -1)
1155
+ : b === null ? 1 : textCompare(a, b);
1156
+ const addUnreadable = (entry , reason ) => {
1157
+ const relative = path.relative(runStoreDir(entry.root), entry.file);
1158
+ const parts = relative.split(path.sep);
1159
+ unreadable.push({
1160
+ taskId: path.basename(entry.file, '.json'),
1161
+ featureSlug: parts.length > 1 ? parts[0] : null,
1162
+ origin: entry.origin,
1163
+ reason,
1164
+ });
1165
+ };
1166
+ // One row per run, the copy status reads (C-2): a shadowed flat or another checkout's copy is dropped.
1167
+ for (const entry of selectRunEntries(resolved.entries)) {
1168
+ if (entry.error) {
1169
+ addUnreadable(entry, entry.error);
1170
+ continue;
1171
+ }
1172
+ const record = entry.record;
1173
+ if (!record || typeof record !== 'object' || Array.isArray(record)) {
1174
+ addUnreadable(entry, 'shape');
1175
+ continue;
1176
+ }
1177
+ if (record.version !== 1) {
1178
+ addUnreadable(entry, 'schema-version');
1179
+ continue;
1180
+ }
1181
+ const relative = path.relative(runStoreDir(entry.root), entry.file);
1182
+ const parts = relative.split(path.sep);
1183
+ const folderSlug = parts.length > 1 ? parts[0] : null;
1184
+ const stem = path.basename(entry.file, '.json');
1185
+ // Every id a row prints is checked, so no newline or escape byte from a record reaches the text form.
1186
+ if (typeof record.taskId !== 'string' || !ID_PATTERN.test(record.taskId) || record.taskId !== stem
1187
+ || (folderSlug && typeof record.featureSlug === 'string' && record.featureSlug !== folderSlug)
1188
+ || (entry.slug !== null && entry.slug !== undefined && !(typeof entry.slug === 'string' && ID_PATTERN.test(entry.slug)))) {
1189
+ addUnreadable(entry, 'id-mismatch');
1190
+ continue;
1191
+ }
1192
+ if (!RUN_STATES.includes(record.state)) {
1193
+ addUnreadable(entry, 'shape');
1194
+ continue;
1195
+ }
1196
+ let snapshot ;
1197
+ try { snapshot = snapshotRun(record); } catch {
1198
+ addUnreadable(entry, 'shape');
1199
+ continue;
1200
+ }
1201
+ if (snapshot.current && !(typeof snapshot.current.id === 'string' && ID_PATTERN.test(snapshot.current.id))) {
1202
+ addUnreadable(entry, 'shape');
1203
+ continue;
1204
+ }
1205
+ const terminal = snapshot.terminalStage;
1206
+ runs.push({
1207
+ taskId: snapshot.taskId,
1208
+ featureSlug: entry.slug,
1209
+ taskClass: snapshot.taskClass,
1210
+ workflowName: snapshot.workflowName,
1211
+ state: snapshot.state,
1212
+ current: snapshot.current,
1213
+ awaitingGate: snapshot.awaitingGate ? { gateId: snapshot.awaitingGate.gateId, name: snapshot.awaitingGate.name } : null,
1214
+ progress: snapshot.progress,
1215
+ // IC-001: the stage's identity only; its gate list (and the prompts in it) stays in status.
1216
+ terminalStage: terminal && typeof terminal === 'object'
1217
+ ? { id: terminal.id ?? null, skill: terminal.skill ?? null, kind: terminal.kind ?? null } : null,
1218
+ revision: snapshot.revision,
1219
+ startedAt: journalTime(record.startedAt),
1220
+ updatedAt: journalTime(record.updatedAt),
1221
+ origin: entry.origin,
1222
+ });
1223
+ }
1224
+
1225
+ runs.sort((a, b) => {
1226
+ if (a.updatedAt === null && b.updatedAt !== null) return 1;
1227
+ if (a.updatedAt !== null && b.updatedAt === null) return -1;
1228
+ if (a.updatedAt !== null && b.updatedAt !== null) {
1229
+ const byTime = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
1230
+ if (byTime !== 0) return byTime;
1231
+ }
1232
+ return slugCompare(a.featureSlug, b.featureSlug) || textCompare(a.taskId, b.taskId);
1233
+ });
1234
+ unreadable.sort((a, b) => textCompare(a.origin, b.origin)
1235
+ || slugCompare(a.featureSlug, b.featureSlug) || textCompare(a.taskId, b.taskId));
1236
+
1237
+ if (json) {
1238
+ console.log(JSON.stringify({ v: 1, runs, unreadable }, null, 2));
1239
+ } else {
1240
+ for (const row of runs) console.log(`${row.state} ${row.featureSlug || '-'} ${row.taskId} ${row.current ? row.current.id : '-'} rev ${row.revision}`);
1241
+ if (unreadable.length > 0) console.log(`unreadable: ${unreadable.length}`);
1242
+ }
1243
+ return finishRuntime(0);
1244
+ }
1245
+
800
1246
  /** CLI handler for `doflow orchestrate`. The run journal lives in the CALLER's project state
801
1247
  * (like evidence), while templates come from this install — same two-roots split readiness uses.
802
1248
  * Completing a source-mutating gated stage needs a READY readiness record of the stage's template,
803
1249
  * made by `doflow readiness` before this call; nothing is evaluated here. */
804
1250
  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,
1251
+ action = 'status', taskId, taskClass, stage, gate, node, decision, policy, note, reason, forced = false,
1252
+ verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, json = false, repoRoot, stateRoot, sourceRoot, slug = null, recordSlug, buildAdapters = null, dryRun = false,
807
1253
  } = {}) {
808
1254
  const { finishRuntime, usageError } = require('./cli-result.js');
809
1255
 
1256
+ if (action === 'list') {
1257
+ return listRuns({ taskId, taskClass, gate, decision, policy, forced, note, reason, stage, node, verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, slug, dryRun, json, stateRoot });
1258
+ }
1259
+ // 093 IC-012: timeline reads one run, so it takes the two flags that name it and refuses the rest.
1260
+ if (action === 'timeline') {
1261
+ const noEffect = noEffectFlag({ taskId, taskClass, gate, decision, policy, forced, note, reason, stage, node, verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, slug, dryRun }, ['--task-id', '--slug']);
1262
+ if (noEffect) return usageError('orchestrate', `'${noEffect}' has no effect on action 'timeline'`, json);
1263
+ if (!taskId) return usageError('orchestrate', 'timeline requires --task-id', json);
1264
+ }
1265
+
1266
+ // `start` writes the run; --dry-run would otherwise be ignored and a real run started.
1267
+ if (action === 'start' && dryRun) return usageError('orchestrate', "'--dry-run' has no effect on action 'start'", json);
1268
+
810
1269
  const root = repoRoot || REPO_ROOT;
811
1270
  // Same scope rule every other stateful command uses (evidenceRoot(o) in runtime-commands.js):
812
1271
  // a bare `stateRoot: stateRoot || process.cwd()` silently ignored `--global`, so `orchestrate`
813
1272
  // wrote its journal to $PWD while a `--global` `evidence`/`readiness` call on the same task read
814
1273
  // and wrote $HOME — two roots disagreeing about where one task's state lives.
815
1274
  const state = stateRoot || process.cwd();
1275
+ // The checkout the verb runs in: its branch and feature folder name the feature; `state` (the main
1276
+ // checkout for a linked worktree, 115 IC-004) holds the records.
1277
+ const source = sourceRoot || state;
816
1278
  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;
1279
+ // The feature this task belongs to: recorded on a run this command starts, the namespace its run
1280
+ // and readiness record live under (115 C-1), and matched against a run found in another checkout.
1281
+ // The dispatcher passes the call's record slug, computed once for every store the call touches.
1282
+ let featureSlug = recordSlug !== undefined ? recordSlug : safeId ? featureSlugFor({ projectRoot: source, taskId, slug }) : slug;
820
1283
  // A run started in another checkout of this repository for the same feature is read and updated
821
1284
  // where it is. `start` is always local: an unrelated run with the same id elsewhere neither
822
1285
  // refuses nor satisfies it.
823
1286
  let runRoot = state;
824
1287
  if (safeId && action !== 'start') {
825
- const found = findRunFile({ stateRoot: state, taskId, slug: featureSlug });
1288
+ // `status` (and `timeline`, which reads the same run) with no --slug opens the one run the list
1289
+ // action shows for the id, whichever feature namespace holds it (FU-wrnnaq); more than one is
1290
+ // refused until --slug names the feature.
1291
+ const found = findRunFile({ stateRoot: state, taskId, slug: featureSlug, anyNamespace: (action === 'status' || action === 'timeline') && !slug });
826
1292
  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.`);
1293
+ console.error(found.byFeature
1294
+ ? `[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.`
1295
+ : `[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
1296
  return finishRuntime(1);
829
1297
  }
830
- if (found.status === 'found') runRoot = found.root;
1298
+ if (found.status === 'found') {
1299
+ runRoot = found.root;
1300
+ if (found.slug !== undefined) featureSlug = found.slug;
1301
+ }
831
1302
  }
832
1303
  const orchestrator = new WorkflowOrchestrator({
833
- repoRoot: root, projectRoot: runRoot,
834
- stateDir: path.join(runRoot, '.doflow', 'state', 'orchestration'),
1304
+ repoRoot: root, projectRoot: runRoot, sourceRoot: source,
1305
+ stateDir: runStoreDir(runRoot),
835
1306
  featureSlug, buildAdapters,
836
1307
  });
837
1308
  orchestrator.readinessEvaluate = (node , run ) => {
@@ -865,12 +1336,28 @@ function handleOrchestrateCommand({
865
1336
  slug: runSlugOf(run, runRoot) || featureSlug, cwd: state,
866
1337
  });
867
1338
 
1339
+ // Row 134. The plan is read in the checkout the verb runs in first, then where the run lives; a
1340
+ // SHOULD principle marked FAIL is one note, printed once although the check runs twice.
1341
+ let planAdvised = false;
1342
+ orchestrator.planExitCheck = (node , run ) => {
1343
+ const { planStageExit } = require('./plan-content.js');
1344
+ const answer = planStageExit({ roots: [...new Set([source, runRoot])], slug: runSlugOf(run, runRoot) || featureSlug });
1345
+ if (!planAdvised) {
1346
+ planAdvised = true;
1347
+ for (const f of answer.advisories) console.error(`orchestrate: note: ${f.path}:${f.line} ${f.message}`);
1348
+ }
1349
+ return answer;
1350
+ };
1351
+
868
1352
  if ((action === 'handoff' || action === 'complete-stage')
869
1353
  && [verificationPlan, scope, invariants].some((v) => typeof v === 'string' && v.trim() !== '')) {
870
1354
  console.error(`orchestrate: note: --verification-plan, --scope and --invariants are inputs to doflow-run readiness; this ${action} did not read them.`);
871
1355
  }
872
1356
 
873
1357
  try {
1358
+ if (policy !== undefined && (action !== 'decide-gate' || decision !== undefined || forced || policy !== gate)) {
1359
+ return usageError('orchestrate', '--policy requires decide-gate with the matching --gate and refuses --decision and --forced', json);
1360
+ }
874
1361
  // `--forced` is only read by the two actions that can override a decision. Accepting it
875
1362
  // silently on any other action lets an operator believe a stage completion or skip was
876
1363
  // recorded as forced when nothing recorded it — refuse instead of dropping the flag.
@@ -886,6 +1373,8 @@ function handleOrchestrateCommand({
886
1373
  if (mcpOutcome !== undefined && action !== 'handoff') {
887
1374
  return usageError('orchestrate', `'--mcp-outcome' has no effect on action '${action}' — only handoff records MCP outcomes`, json);
888
1375
  }
1376
+ // A catch-up on an absent run starts it implicitly, which counts as a run start for the pointer.
1377
+ const absentBefore = action === 'catch-up' && taskId ? !orchestrator.readRun(taskId) : false;
889
1378
  let snapshot;
890
1379
  switch (action) {
891
1380
  case 'handoff':
@@ -910,8 +1399,8 @@ function handleOrchestrateCommand({
910
1399
  snapshot = orchestrator.skipStage({ taskId, stageId: stage, reason: reason ?? note });
911
1400
  break;
912
1401
  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 });
1402
+ if (!taskId || !gate || (policy === undefined && !decision)) return usageError('orchestrate', 'decide-gate requires --task-id, --gate and --decision approve|reject or --policy <gateId>', json);
1403
+ snapshot = orchestrator.decideGate({ taskId, gateId: gate, decision, policy, note, forced });
915
1404
  break;
916
1405
  case 'annotate':
917
1406
  if (!taskId || !node || !note) return usageError('orchestrate', 'annotate requires --task-id, --node and --note', json);
@@ -921,18 +1410,44 @@ function handleOrchestrateCommand({
921
1410
  if (!taskId) return usageError('orchestrate', 'status requires --task-id', json);
922
1411
  snapshot = orchestrator.status(taskId);
923
1412
  break;
1413
+ case 'timeline': {
1414
+ const run = orchestrator.requireRun(taskId);
1415
+ const body = timelineOf(run, taskId, runSlugOf(run, runRoot) || featureSlug);
1416
+ if (json) console.log(JSON.stringify(body, null, 2));
1417
+ else {
1418
+ console.log(`Timeline ${body.taskId} ${body.featureSlug || '-'} rev ${body.revision}`);
1419
+ for (const e of body.events) {
1420
+ console.log(`${e.at || '-'} ${e.action} ${e.node || '-'} ${e.outcome || e.decision || '-'}${e.forced ? ' forced' : ''}${e.synthetic ? ' synthetic' : ''}`);
1421
+ }
1422
+ if (body.otherEvents > 0) console.log(`other events: ${body.otherEvents}`);
1423
+ }
1424
+ return finishRuntime(0);
1425
+ }
924
1426
  default:
925
- return usageError('orchestrate', `unknown action '${action}'; valid: start | status | handoff | catch-up | complete-stage | skip-stage | decide-gate | annotate`, json);
1427
+ return usageError('orchestrate', `unknown action '${action}'; valid: start | status | list | timeline | handoff | catch-up | complete-stage | skip-stage | decide-gate | annotate`, json);
1428
+ }
1429
+ // Row 135: the feature pointer follows a `feature` run: set when it starts (`start`, or the `catch-up` that
1430
+ // starts it), dropped once it completes.
1431
+ if ((action === 'start' || absentBefore) && snapshot.taskClass === 'feature' && snapshot.state !== 'COMPLETED') {
1432
+ require('./feature-pointer.js').recordFeature({ storeRoot: state, cwd: source, slug: featureSlug, taskId });
1433
+ } else if (action !== 'status' && snapshot.taskClass === 'feature' && snapshot.state === 'COMPLETED') {
1434
+ require('./feature-pointer.js').clearCheckout({ storeRoot: state, cwd: source, slug: featureSlug });
926
1435
  }
927
1436
  // Only a handoff that recorded work compacts; `deferred` and `standalone` recorded nothing.
928
1437
  if (action === 'handoff' && (snapshot.disposition === 'completed' || snapshot.disposition === 'annotated')) {
929
- snapshot = { ...snapshot, compaction: compactAfterHandoff({ taskId, projectRoot: state }) };
1438
+ snapshot = { ...snapshot, compaction: compactAfterHandoff({ taskId, projectRoot: source }) };
930
1439
  }
931
1440
  if (json) { console.log(JSON.stringify(snapshot, null, 2)); return finishRuntime(0); }
932
1441
  console.log(`Workflow ${snapshot.taskId} [${snapshot.taskClass}] — ${snapshot.state}`);
1442
+ // `status` with no --slug opens the one run of the id under any feature (FU-wrnnaq): say whose it is.
1443
+ if (action === 'status' && featureSlug) console.log(`Feature: ${featureSlug}`);
933
1444
  if (snapshot.disposition === 'standalone') console.log(`Handoff: standalone (${snapshot.reason})`);
934
1445
  else if (snapshot.disposition) console.log(`Handoff: ${snapshot.disposition}`);
935
1446
  if (snapshot.compaction) console.log(`Compaction: ${snapshot.compaction.status}${snapshot.compaction.reason ? ` — ${snapshot.compaction.reason}` : ''}`);
1447
+ if (snapshot.policy) console.log(snapshot.decision === 'pass'
1448
+ ? `${gate} ${snapshot.taskId} approved by policy ${snapshot.policy.id}@${snapshot.policy.digest} (${snapshot.inputs.length} inputs)`
1449
+ : `${gate} ${snapshot.taskId} denied by policy: ${snapshot.reasons.join(', ')}`);
1450
+ if (snapshot.annotations?.hint) console.log(` ${snapshot.annotations.hint}`);
936
1451
  if (snapshot.progress) console.log(`Progress: ${snapshot.progress.done}/${snapshot.progress.total}`);
937
1452
  if (snapshot.current) {
938
1453
  console.log(snapshot.awaitingGate
@@ -950,4 +1465,4 @@ function handleOrchestrateCommand({
950
1465
  }
951
1466
  }
952
1467
 
953
- module.exports = { WorkflowOrchestrator, RUN_STATES, GATE_DECISIONS, STAGE_OUTCOMES, handleOrchestrateCommand };
1468
+ module.exports = { WorkflowOrchestrator, RUN_STATES, GATE_DECISIONS, STAGE_OUTCOMES, handleOrchestrateCommand, recordStagePack };