@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
@@ -22,6 +22,7 @@ const { STAGES: CHAIN_STAGES } = require('../decision-register.js');
22
22
  const { isSafeSlug, invalidSlugRefusal } = require('../task-scope.js');
23
23
  const { resolveActiveFeature } = require('../feature-resolve.js');
24
24
  const { SETTLE_AS, withDerivedDone } = require('./fold.js');
25
+ const { pageItems } = require('../verb-view.js');
25
26
  const { appendEvents, readFold, byFromChannel, randomChars } = require('./event-store.js');
26
27
  const { deriveStatuses } = require('./status.js');
27
28
  const { writeIntent, kebabTitle } = require('./intent-writer.js');
@@ -73,7 +74,7 @@ function newFollowupId(taken ) {
73
74
 
74
75
  function channelBy(channel ) {
75
76
  const by = byFromChannel(channel || 'default');
76
- if (!by) throw new FollowupUsageError(`unknown --channel '${channel}'. Valid: question, gate, prompt, default`);
77
+ if (!by) throw new FollowupUsageError(`unknown --channel '${channel}'. Valid: question, gate, prompt, resolution, default`);
77
78
  return by;
78
79
  }
79
80
 
@@ -334,12 +335,37 @@ function reportFollowup({ root, statement, input, release, feature, channel, now
334
335
 
335
336
  // ── list ───────────────────────────────────────────────────────────────────────────────────────
336
337
 
337
- /** IC-006 `list`: `state` is open (default), taken, done, dismissed or all. */
338
- function listFollowups({ root, state = 'open', hasBody, fsImpl = nodeFs, statuses, now = new Date() } ) {
339
- if (state !== 'all' && !STATES.includes(state)) throw new FollowupUsageError(`--state must be one of ${[...STATES, 'all'].join(', ')} (got '${state}')`);
338
+ /** The most items one list answer holds, however large a `--limit` asks for (IC-004). */
339
+ const LIST_MAX_LIMIT = 100;
340
+
341
+ /** The key the fold orders on and a list cursor names: the creating event's time in ms, then its event id (IC-008). */
342
+ function followupOrderKey(item ) {
343
+ const created = item.history[0];
344
+ return { atMs: Date.parse(created.at), event: created.event };
345
+ }
346
+
347
+ /**
348
+ * IC-006 `list`: `state` is open (default), taken, done, dismissed or all. `ids` keeps the named items (and, with no
349
+ * `state`, looks in every state); an id that is no item at all is the `unknown-id` refusal. With `cursor`, `limit` or
350
+ * `defaultLimit` the answer holds one page of `items` plus `shown` and `nextCursor`; with none it is the whole list.
351
+ */
352
+ function listFollowups({ root, state, ids, cursor, limit, defaultLimit = null, hasBody, fsImpl = nodeFs, statuses, now = new Date() } ) {
353
+ const wanted = ids === undefined ? null : parseIds(ids);
354
+ const wantedState = state === undefined ? (wanted ? 'all' : 'open') : state;
355
+ if (wantedState !== 'all' && !STATES.includes(wantedState)) throw new FollowupUsageError(`--state must be one of ${[...STATES, 'all'].join(', ')} (got '${wantedState}')`);
340
356
  const { fold, followups } = loadFollowups(root, { hasBody, fsImpl, statuses, now });
341
- const items = state === 'all' ? followups : followups.filter((item ) => item.state === state);
342
- return { ok: true, action: 'list', state, count: items.length, items, conflicts: fold.conflicts, unreadable: fold.unreadable, unreadableReasons: fold.unreadableReasons, next: [] };
357
+ let matched = followups;
358
+ if (wanted) {
359
+ const known = new Set(followups.map((item ) => item.id));
360
+ const unknown = wanted.filter((id) => !known.has(id));
361
+ if (unknown.length) return refusal('list', 'unknown-id', `${unknown.join(', ')} ${unknown.length === 1 ? 'is' : 'are'} not a follow-up in this store. Nothing was listed.`);
362
+ matched = followups.filter((item ) => wanted.includes(item.id));
363
+ }
364
+ if (wantedState !== 'all') matched = matched.filter((item ) => item.state === wantedState);
365
+ const answer = { ok: true, action: 'list', state: wantedState, count: matched.length, items: matched, conflicts: fold.conflicts, unreadable: fold.unreadable, unreadableReasons: fold.unreadableReasons, next: [] };
366
+ if (cursor === undefined && limit === undefined && defaultLimit === null) return answer;
367
+ const page = pageItems(matched, { cursor, limit, defaultLimit, maxLimit: LIST_MAX_LIMIT, key: followupOrderKey });
368
+ return { ...answer, items: page.items, shown: page.shown, nextCursor: page.nextCursor };
343
369
  }
344
370
 
345
371
  // ── take, settle, promote ──────────────────────────────────────────────────────────────────────
@@ -350,6 +376,11 @@ function takeFollowups({ root, ids, slug, channel, now = new Date(), fsImpl = no
350
376
  if (!slug) throw new FollowupUsageError('--slug is required for --action take');
351
377
  const bad = invalidSlugRefusal(slug);
352
378
  if (bad) throw new FollowupUsageError(bad.message);
379
+ // `list` shows a taken item whose feature finished as `done`, but the fold still holds it `taken`: say what the user was shown.
380
+ const shown = loadFollowups(root, { fsImpl, now }).followups.find((item ) => item.derived && list.includes(item.id));
381
+ if (shown) {
382
+ return refusal('take', 'illegal-transition', `${shown.id} is done because feature ${shown.takenBy} finished (derived from git and the release records, not recorded on the item); only an open item can be taken. Nothing was written.`);
383
+ }
353
384
  const out = appendEvents(root, [{ type: 'followup.taken', by: channelBy(channel), data: { ids: list, feature: slug } }], { now, fsImpl });
354
385
  if (!out.ok) return refusalFrom('take', out);
355
386
  return { ok: true, action: 'take', ids: list, feature: slug, events: out.written.map((w ) => w.file), next: [] };
@@ -426,7 +457,7 @@ function promoteFollowups({ root, ids, title, channel, now = new Date(), fsImpl
426
457
  }
427
458
 
428
459
  module.exports = {
429
- addFollowups, reportFollowup, listFollowups, takeFollowups, settleFollowups, promoteFollowups,
460
+ addFollowups, reportFollowup, listFollowups, followupOrderKey, takeFollowups, settleFollowups, promoteFollowups,
430
461
  loadFollowups, readBatchFile, resolveFeatureSlug, parseIds, oneLine, channelBy,
431
462
  FollowupUsageError, STAGES, STATES, SOURCE_KINDS, STATEMENT_MAX,
432
463
  };
@@ -16,6 +16,8 @@ const { withDerivedDone } = require('./fold.js');
16
16
  const { appendEvents, readFold, readEvents } = require('./event-store.js');
17
17
  const { deriveStatuses, bucketize, behindNote } = require('./status.js');
18
18
  const { FollowupUsageError, parseIds } = require('./followup.js');
19
+ const { describeActive } = require('../feature-pointer.js');
20
+ const { commandLine } = require('../verb-view.js');
19
21
 
20
22
  /** The failure store, or null when its module cannot be loaded: the overview then reports no failures. */
21
23
  function failureStoreOrNull() {
@@ -129,7 +131,7 @@ function goalView(goal , features , statuses , conflicts = [])
129
131
 
130
132
  function nextLines({ shown, open, items, intents, goals, maintain, pendingItems, failures } ) {
131
133
  const next = [];
132
- if (shown < open) next.push('List the rest: doflow-run followup --action list');
134
+ if (shown < open) next.push(`Page through all ${open} open follow-ups: ${commandLine('followup', 'list', {})}`);
133
135
  if (items.length) next.push(`Take items into the new feature when its folder exists: doflow-run lifecycle --action init --slug <slug> --take ${items[0].id}`);
134
136
  if (intents.length) {
135
137
  next.push(`Start from a promoted intent: /do-brainstorm --intent ${intents[0].path}, then doflow-run lifecycle --action init --slug <slug> --intent ${intents[0].path}`);
@@ -289,4 +291,26 @@ function featureStatus({ root, slug, now = new Date(), fsImpl = nodeFs } ) {
289
291
  return result;
290
292
  }
291
293
 
292
- module.exports = { buildOverview, initFeature, featureStatus, goalView, goalConflicts, goalConflictNext, DISCOVERY_SHOWN, MAINTAIN_SHOWN, FEATURES_SHOWN };
294
+ /**
295
+ * `status` with no `--slug` (row 135): the feature the runtime recorded for this checkout, against the
296
+ * branch's. A mismatch or an unreadable pointer is a refusal (exit 1) that names both sides and picks
297
+ * neither; an agreeing pointer also carries the tracked feature's derived status, as `--slug` does.
298
+ */
299
+ function activeFeature({ root, cwd, now = new Date(), fsImpl = nodeFs } ) {
300
+ const active = describeActive({ storeRoot: root, cwd });
301
+ const here = `branch ${active.current.branch || 'unknown'}${active.current.slug ? `, branch feature ${active.current.slug}` : ''}`;
302
+ if (active.state === 'unreadable') {
303
+ return { ok: false, action: 'status', finding: 'feature-pointer-unreadable', message: `${active.file} cannot be read (${active.reason}); delete it to let the next feature start write it again.`, active };
304
+ }
305
+ if (active.state === 'mismatch') {
306
+ const p = active.pointer;
307
+ return { ok: false, action: 'status', finding: 'feature-mismatch', message: `the pointer names ${p.slug} on branch ${p.branch || 'unknown'}, but this checkout is on ${here}; neither is trusted, so check which feature you are on.`, active };
308
+ }
309
+ const result = { ok: true, action: 'status', active };
310
+ if (active.state === 'none') return { ...result, message: `no active feature is recorded for this checkout (${here}); pass --slug <slug> to ask about one.` };
311
+ // An untracked feature is refused by featureStatus; the pointer answer stands, named, without status fields.
312
+ const derived = featureStatus({ root, slug: active.slug, now, fsImpl });
313
+ return derived.ok ? { ...derived, active } : { ...result, slug: active.slug, message: `${active.slug} is tracked by its run only; \`lifecycle --action init\` to track it.` };
314
+ }
315
+
316
+ module.exports = { buildOverview, initFeature, featureStatus, activeFeature, goalView, goalConflicts, goalConflictNext, DISCOVERY_SHOWN, MAINTAIN_SHOWN, FEATURES_SHOWN };
@@ -6,7 +6,7 @@
6
6
  * The machine-local report body (IC-005, DEC-014). A report's text can hold a crash trace, a log or
7
7
  * pasted output, so it never enters the repository: the body is masked with the body profile (IC-012),
8
8
  * cut to at most 1 MiB and written once under the same per-machine home the failure store uses,
9
- * `<XDG_CONFIG_HOME or $HOME/.config>/doflow/reports/<projectKey>/<FU-id>.txt`, folder 0700 and file
9
+ * `<config home>/doflow/reports/<projectKey>/<FU-id>.txt`, folder 0700 and file
10
10
  * 0600. The project event carries only a reference, the size and a masked 2 KB excerpt.
11
11
  *
12
12
  * `projectKey` is the first 12 hex characters of the SHA-256 of the IC-001 root's real path, so two
@@ -20,6 +20,7 @@ const nodeFs = require('node:fs');
20
20
  const path = require('node:path');
21
21
  const crypto = require('node:crypto');
22
22
  const mask = require('../mask.js');
23
+ const { configHome } = require('../config-home.js');
23
24
 
24
25
  /** A `--file` or `--stdin` source is read for at most this many bytes; the rest could not be kept anyway. */
25
26
  const MAX_READ_BYTES = 16 * 1024 * 1024;
@@ -38,17 +39,14 @@ const EXCERPT_BYTES = 2048;
38
39
  class ReportInputError extends Error {}
39
40
 
40
41
  /**
41
- * The folder holding every project's report bodies, or null when no home resolves. It resolves the
42
- * base the way IC-011 resolves the failure home (`XDG_CONFIG_HOME` when set, non-empty and absolute,
43
- * else `$HOME/.config` when `HOME` is absolute) and sits beside it, `doflow/reports`. The rule is
44
- * repeated here rather than required from the failure module, so the follow-up service still loads in
45
- * a build or an install where the failure modules cannot be loaded (IC-016).
42
+ * The folder holding every project's report bodies, or null when no config home resolves. It sits in
43
+ * the same config home as the failure home (`../config-home.js`), `doflow/reports`, and takes that
44
+ * leaf helper rather than the failure module, so the follow-up service still loads in a build or an
45
+ * install where the failure modules cannot be loaded (IC-016).
46
46
  */
47
47
  function reportsHome(env = process.env) {
48
- const xdg = env.XDG_CONFIG_HOME;
49
- if (typeof xdg === 'string' && xdg !== '') return path.isAbsolute(xdg) ? path.join(xdg, 'doflow', 'reports') : null;
50
- const home = env.HOME;
51
- return typeof home === 'string' && path.isAbsolute(home) ? path.join(home, '.config', 'doflow', 'reports') : null;
48
+ const { dir } = configHome(env);
49
+ return dir === null ? null : path.join(dir, 'reports');
52
50
  }
53
51
 
54
52
  function realProjectRoot(root , fsImpl = nodeFs) {
@@ -9,18 +9,17 @@
9
9
  * joined by any event that names more than one, and a unit goes only as a whole: when every item in
10
10
  * it is settled in the fold (a follow-up done or dismissed, a goal done) and its newest event is
11
11
  * older than the window. A unit whose removal would change anything else the fold shows (another
12
- * item, a feature, a release, a conflict) is kept.
12
+ * item, a feature, a release, a conflict) is kept, and so is a unit that an event of a type this
13
+ * version does not know (written by a newer DoFlow) names by id.
13
14
  *
14
- * `parseWindow` remains pure. `retentionWindow` composes its unset case with the declared storage
15
- * default and the project's prune setting; explicit environment values retain their existing meaning.
15
+ * `parseWindow` remains pure. `retentionWindow` composes the resolver-owned default with the project's
16
+ * prune setting; explicit values retain their existing meaning while pruning is off.
16
17
  */
17
18
 
18
19
  const nodeFs = require('node:fs');
19
20
  const { isDeepStrictEqual } = require('node:util');
20
- const { REPO_ROOT } = require('../../helper/repo-root.js');
21
- const { loadStorageRegistry } = require('../storage/registry.js');
22
- const { resolveRoots, readPruneSetting } = require('../storage/roots.js');
23
- const { foldInto, finalize } = require('./fold.js');
21
+ const { loadSettings } = require('../settings/resolve.js');
22
+ const { foldInto, finalize, EVENT_TYPES } = require('./fold.js');
24
23
 
25
24
  const HOUR_MS = 3600000;
26
25
 
@@ -35,23 +34,46 @@ function parseWindow(env = process.env)
35
34
  return { state: 'invalid', raw: String(raw) };
36
35
  }
37
36
 
38
- /** Composes IC-013's explicit environment override with the declared lifecycle default. */
39
- function retentionWindow({ env = process.env, projectRoot, registry, fsImpl = nodeFs } = {}) {
37
+ function invalidRaw(resolved , env ) {
38
+ for (let i = resolved.candidates.length - 1; i >= 0; i -= 1) {
39
+ const candidate = resolved.candidates[i];
40
+ if (candidate.status !== 'invalid') continue;
41
+ if (candidate.layer === 'env') return String(env.DOFLOW_RETENTION_HOURS ?? '');
42
+ try {
43
+ const raw = JSON.parse(candidate.display);
44
+ if (typeof raw === 'string' || typeof raw === 'number') return String(raw);
45
+ } catch { /* a malformed whole-file candidate has no display value */ }
46
+ return candidate.display || candidate.reason || candidate.code || '';
47
+ }
48
+ return '';
49
+ }
50
+
51
+ /** Composes the retention setting and prune gate through the shared settings resolver. */
52
+ function retentionWindow({ env = process.env, projectRoot, fsImpl = nodeFs } = {}) {
40
53
  const parsed = parseWindow(env);
41
54
  if (parsed.state === 'invalid') return parsed;
42
- if (parsed.state === 'on') return { ...parsed, source: 'env' };
43
- let activeRegistry ;
44
55
  try {
45
- activeRegistry = registry || loadStorageRegistry({ repoRoot: REPO_ROOT, fsImpl });
56
+ let settingsRegistry;
57
+ if (fsImpl !== nodeFs) {
58
+ const { loadSettingsRegistry } = require('../settings/registry.js');
59
+ const loaded = loadSettingsRegistry({ fsImpl });
60
+ if (!loaded.ok) throw new Error(loaded.detail);
61
+ settingsRegistry = loaded.registry;
62
+ }
63
+ const view = loadSettings({ projectRoot, env, fsImpl, ...(settingsRegistry ? { registry: settingsRegistry } : {}) });
64
+ const retention = view.get('storage.retentionHours', 'project');
65
+ if (retention.state === 'invalid') return { state: 'invalid', raw: invalidRaw(retention, env) };
66
+ if (retention.explicit) {
67
+ const hours = retention.value;
68
+ return { state: 'on', hours, ms: hours * HOUR_MS, source: retention.origin.layer };
69
+ }
70
+ if (view.get('storage.prune', 'project').value !== 'on') return { state: 'off' };
71
+ const hours = retention.value;
72
+ return { state: 'on', hours, ms: hours * HOUR_MS, source: 'default' };
46
73
  } catch {
47
- return { state: 'off' };
74
+ // Preserve an explicit environment window when the resolver registry is unavailable.
75
+ return parsed.state === 'on' ? { ...parsed, source: 'env' } : { state: 'off' };
48
76
  }
49
- const roots = resolveRoots({ registry: activeRegistry, projectRoot, env });
50
- if (readPruneSetting({ registry: activeRegistry, roots, rootId: 'project-state', fsImpl }).setting !== 'on') return { state: 'off' };
51
- const lifecycleStore = activeRegistry.stores.find((store ) => store.id === 'lifecycle-events');
52
- if (!lifecycleStore) throw new Error('Storage registry has no lifecycle-events store');
53
- const hours = lifecycleStore.bound.defaultHours;
54
- return { state: 'on', hours, ms: hours * HOUR_MS, source: 'default' };
55
77
  }
56
78
 
57
79
  function isText(value ) { return typeof value === 'string' && value.trim() !== ''; }
@@ -113,6 +135,34 @@ function unitsOf(events , shouldStop ) {
113
135
  return [...units.values()];
114
136
  }
115
137
 
138
+ /**
139
+ * Every string value and every object key in the data of the events whose type this version's fold
140
+ * does not know (a newer DoFlow wrote them). The fold ignores them, so retention cannot tell what they change; a unit that
141
+ * one of them names by id is kept whole rather than deleted from under it. Exact matches only:
142
+ * keeping too little is a lost record, keeping too much only a file that stays.
143
+ * @returns {Set<string>|null} null when `shouldStop` fired
144
+ */
145
+ function unknownTypeStrings(events , shouldStop ) {
146
+ const known = new Set (EVENT_TYPES);
147
+ const found = new Set ();
148
+ for (const event of events) {
149
+ if (shouldStop?.()) return null;
150
+ if (known.has(event.type)) continue;
151
+ const pending = [event.data];
152
+ while (pending.length > 0) {
153
+ if (shouldStop?.()) return null;
154
+ const value = pending.pop();
155
+ if (typeof value === 'string') found.add(value);
156
+ else if (Array.isArray(value)) pending.push(...value);
157
+ else if (value && typeof value === 'object') {
158
+ for (const key of Object.keys(value)) found.add(key);
159
+ pending.push(...Object.values(value));
160
+ }
161
+ }
162
+ }
163
+ return found;
164
+ }
165
+
116
166
  function isSettled(state , key ) {
117
167
  const [kind, id] = [key.slice(0, key.indexOf(':')), key.slice(key.indexOf(':') + 1)];
118
168
  if (kind === 'followup') {
@@ -173,6 +223,12 @@ function selectExpired(events , { now, windowMs, shouldStop } = {})
173
223
  }
174
224
  const named = unitsOf(events, shouldStop);
175
225
  if (!named || shouldStop?.()) return incomplete();
226
+ const mentioned = unknownTypeStrings(events, shouldStop);
227
+ if (!mentioned || shouldStop?.()) return incomplete();
228
+ // Selected exactly as 5bc9407b did (every settled, expired, unserved unit is a candidate), and only
229
+ // then are the units an unknown-type event names dropped. Protection removes units from that
230
+ // selection; it never changes which of the others are picked, so what is deleted is always a subset
231
+ // of what base would delete on the same events.
176
232
  const candidates = [];
177
233
  for (const unit of named) {
178
234
  if (shouldStop?.()) return incomplete();
@@ -186,20 +242,28 @@ function selectExpired(events , { now, windowMs, shouldStop } = {})
186
242
  if (eligible) candidates.push({ ...unit, newestAt: newest });
187
243
  }
188
244
  const full = finalize(state);
189
- let selected = candidates;
190
- const allSafe = refoldEqual(events, full, candidates, now, shouldStop);
191
- if (allSafe === null || shouldStop?.()) return incomplete();
192
- if (!allSafe) {
193
- selected = [];
194
- const ordered = [...candidates].sort((a , b ) => Date.parse(a.newestAt) - Date.parse(b.newestAt));
195
- if (shouldStop?.()) return incomplete();
245
+ /** The refold-safe subset of `list`: all of it when that holds, else the oldest-first greedy pass. */
246
+ const safeSubset = (list ) => {
247
+ const allSafe = refoldEqual(events, full, list, now, shouldStop);
248
+ if (allSafe === null || shouldStop?.()) return null;
249
+ if (allSafe) return list;
250
+ const chosen = [];
251
+ const ordered = [...list].sort((a , b ) => Date.parse(a.newestAt) - Date.parse(b.newestAt));
252
+ if (shouldStop?.()) return null;
196
253
  for (const unit of ordered) {
197
- if (shouldStop?.()) return incomplete();
198
- const safe = refoldEqual(events, full, [...selected, unit], now, shouldStop);
199
- if (safe === null) return incomplete();
200
- if (safe) selected.push(unit);
254
+ if (shouldStop?.()) return null;
255
+ const safe = refoldEqual(events, full, [...chosen, unit], now, shouldStop);
256
+ if (safe === null) return null;
257
+ if (safe) chosen.push(unit);
201
258
  }
202
- }
259
+ return chosen;
260
+ };
261
+ const baseSelected = safeSubset(candidates);
262
+ if (!baseSelected) return incomplete();
263
+ const unprotected = baseSelected.filter((unit ) => ![...unit.items].some((key ) => mentioned.has(key.slice(key.indexOf(':') + 1))));
264
+ // Dropping units can leave a set the refold no longer accepts; check it again, inside the subset.
265
+ const selected = unprotected.length === baseSelected.length ? baseSelected : safeSubset(unprotected);
266
+ if (!selected) return incomplete();
203
267
  const units = [];
204
268
  for (const unit of selected) {
205
269
  if (shouldStop?.()) return incomplete();
@@ -10,6 +10,7 @@
10
10
  const fs = require('node:fs');
11
11
  const path = require('node:path');
12
12
  const { execFileSync } = require('node:child_process');
13
+ const { memoGit } = require('../git-memo.js');
13
14
 
14
15
 
15
16
 
@@ -61,11 +62,18 @@ function showToplevel(cwd ) {
61
62
  * @returns {string} an absolute, real path
62
63
  */
63
64
  function projectRoot(cwd = process.cwd()) {
65
+ // Inside a call scope (git-memo.ts) the answer git gave for this path is kept for the call. The
66
+ // fallback `real(cwd)` is not git's answer, so it is never kept: the path may become a repository.
67
+ return memoGit(`projectRoot:${path.resolve(cwd)}`, () => gitProjectRoot(cwd)) ?? real(cwd);
68
+ }
69
+
70
+ /** The root git names for `cwd`, or undefined when git names none (outside a repository). */
71
+ function gitProjectRoot(cwd ) {
64
72
  let entries = [];
65
73
  try { entries = worktreeEntries(git(cwd, ['worktree', 'list', '--porcelain'])); } catch { /* not a repository */ }
66
74
  const first = entries[0];
67
75
  if (first && !first.bare && isRealWorkTree(first)) return real(first.path);
68
- return showToplevel(cwd) || real(cwd);
76
+ return showToplevel(cwd) || undefined;
69
77
  }
70
78
 
71
79
  module.exports = { projectRoot, worktreeEntries };
@@ -11,19 +11,20 @@
11
11
  * the release tags and the base tag) come from `do-git-state.sh --lifecycle`, so bash and Node never
12
12
  * derive them twice. The rest is a fixed number of plain `git` calls, however many features are
13
13
  * tracked: one `rev-parse` pins the ref, one `for-each-ref` lists the branches, one
14
- * `git log --first-parent --merges` lists the merges, one `git tag --list` (tagged mode) gives the
15
- * release tags' commits, and one `git rev-list --parents` reads the commit graph of the pinned ref
16
- * and those tags. Every ancestry question (which merge brought a branch tip in, which tag contains a
17
- * merge) is then answered from that graph in memory, not by a git call per feature.
14
+ * `git log --first-parent --merges` lists the merges and their `DoFlow-Row` trailers, one
15
+ * `git tag --list` (tagged mode) gives the release tags' commits, and one `git rev-list --parents`
16
+ * reads the commit graph of the pinned ref and those tags. Every ancestry question (which merge
17
+ * brought a branch tip in, which tag contains a merge) is then answered from that graph in memory,
18
+ * not by a git call per feature.
18
19
  *
19
20
  * Nothing here writes, fetches or reaches a network.
20
21
  *
21
22
  * Accepted ceilings, stated rather than hidden: a feature's merge evidence is only as good as the
22
- * merge commit's subject and the branch name (a squash, a rebase and a fast-forward leave none,
23
- * so they reach the user through `notDetected` and `lifecycle --action merged`); the merge log and
24
- * the graph are read in full, without `--since` or a lower cut (a skewed clock must not hide a
25
- * merge), so their cost grows with the history of the integration ref, once per call; git dates
26
- * have one-second resolution, so the lower bound is floored to the second.
23
+ * merge commit's `DoFlow-Row` trailer or subject and the branch name (a squash, a rebase and a
24
+ * fast-forward leave none, so they reach the user through `notDetected` and `lifecycle --action
25
+ * merged`); the merge log and the graph are read in full, without `--since` or a lower cut (a skewed
26
+ * clock must not hide a merge), so their cost grows with the history of the integration ref, once
27
+ * per call; git dates have one-second resolution, so the lower bound is floored to the second.
27
28
  */
28
29
 
29
30
  const fs = require('node:fs');
@@ -69,13 +70,18 @@ function subjectNames(subject , slug ) {
69
70
  return new RegExp(`(?:^|[^a-z0-9-])${escapeRegExp(slug)}(?:$|[^a-z0-9-])`).test(subject);
70
71
  }
71
72
 
72
- /** First-parent merges of `ref`, oldest first. */
73
- function readMerges(root , ref ) {
74
- const out = git(root, ['log', '--first-parent', '--merges', '--format=%H%x09%P%x09%ct%x09%s', ref]);
75
- return out.split('\n').filter(Boolean).map((line) => {
76
- const [sha, parents, ct, ...subject] = line.split('\t');
77
- return { sha, parents: parents.split(' ').filter(Boolean), ct: Number(ct), subject: subject.join('\t') };
78
- }).reverse();
73
+ /** First-parent merges of `ref`, oldest first, with any `DoFlow-Row` trailer. */
74
+ function readMerges(root , ref ) {
75
+ const out = git(root, ['log', '--first-parent', '--merges', '--format=%H%x00%P%x00%ct%x00%s%x00%(trailers:key=DoFlow-Row,separator=%x1f)%x00', ref]);
76
+ const fields = out.split('\0');
77
+ const merges = [];
78
+ for (let i = 0; i + 4 < fields.length; i += 5) {
79
+ const [rawSha, parents, ct, subject, rowTrailer] = fields.slice(i, i + 5);
80
+ const sha = rawSha.trim();
81
+ if (!sha) continue;
82
+ merges.push({ sha, parents: parents.split(' ').filter(Boolean), ct: Number(ct), subject, rowTrailer: rowTrailer.trim() });
83
+ }
84
+ return merges.reverse();
79
85
  }
80
86
 
81
87
  /** Branch refs by short name (`feat/x`, `origin/feat/x`) with their tips; local branches first. */
@@ -221,7 +227,18 @@ function findEvidence({ slug, lowerBound, merges, mergeBySha, newestCt, history,
221
227
  if (merge) return { kind: 'branch', ref: tip.short, commit: merge.sha };
222
228
  }
223
229
  for (let i = merges.length - 1; i >= 0; i -= 1) {
224
- if (merges[i].ct >= lowerBound && subjectNames(merges[i].subject, slug)) return { kind: 'merge-subject', ref: merges[i].sha.slice(0, 7), commit: merges[i].sha };
230
+ const merge = merges[i];
231
+ if (merge.ct < lowerBound) continue;
232
+ if (merge.rowTrailer) {
233
+ // A present DoFlow-Row is authoritative: it can match only on an exact two-parent merge,
234
+ // and a mismatch must not fall through to a subject that names a different row.
235
+ const match = /^DoFlow-Row:\s*(.*?)\s*$/i.exec(merge.rowTrailer);
236
+ if (merge.parents.length === 2 && match && match[1] === slug) {
237
+ return { kind: 'trailer', ref: merge.sha.slice(0, 7), commit: merge.sha };
238
+ }
239
+ continue;
240
+ }
241
+ if (subjectNames(merge.subject, slug)) return { kind: 'merge-subject', ref: merge.sha.slice(0, 7), commit: merge.sha };
225
242
  }
226
243
  if (confirmed) return { kind: 'confirmed', ref: null, commit: null };
227
244
  return null;
@@ -36,8 +36,18 @@ const LEGACY_EVENTS_REL = path.join(LEGACY_LIFECYCLE_REL, 'events');
36
36
  const MARKER_REL = path.join(LIFECYCLE_REL, 'migrated.json');
37
37
  const MIGRATING_PREFIX = 'events.migrating-';
38
38
  const NOTICE = 'note: the lifecycle store is now .doflow/state/lifecycle/events; agent-docs/lifecycle/ is no longer read and can be deleted';
39
- const removedLine = (count , hours ) => `retention: removed ${count} event files older than ${hours} h`;
40
- const invalidLine = (raw ) => `warning: DOFLOW_RETENTION_HOURS='${raw}' is not a positive whole number of hours; ignored, nothing removed`;
39
+ const removedLine = (count , hours ) => `retention: removed ${count} event ${count === 1 ? 'file' : 'files'} older than ${hours} h`;
40
+ // A leading zero is rejected on purpose (`024` could be read as octal); the warning names it.
41
+ const invalidLine = (raw ) => `warning: DOFLOW_RETENTION_HOURS='${raw}' is not a positive whole number of hours${/^0[0-9]+$/.test(String(raw)) ? ' (no leading zeros)' : ''}; ignored, nothing removed`;
42
+ /**
43
+ * What `writeAtomic` leaves behind when a verb dies between creating its temp file and renaming it:
44
+ * `.retention-<pid>-<chars>.tmp` (the journal) and `.migrated-<pid>-<chars>.tmp` (the marker), in the
45
+ * lifecycle folder. The storage engine's work-store sweep matches `<name>.json.<pid>.tmp` inside
46
+ * record stores only, so nothing else ever removes them.
47
+ */
48
+ const WRITE_TMP = /^\.(?:retention|migrated)-[0-9]+-[A-Za-z0-9]+\.tmp$/;
49
+ /** The age past which such a file is abandoned; the storage engine passes `settings.staleTmpHours`, and a test pins this to it. */
50
+ const STALE_TMP_MS = 24 * 3600000;
41
51
  /** Bounds one pass, so its journal stays far below the reader's cap and its lock is held briefly. */
42
52
  const MAX_PASS_FILES = 20000;
43
53
  /** Unlinks between two refreshes of the lock's age. */
@@ -100,7 +110,8 @@ function fsyncCopy(fsImpl , file ) {
100
110
  function fsyncFolder(fsImpl , dir ) {
101
111
  let fd;
102
112
  try { fd = fsImpl.openSync(dir, 'r'); } catch { return; }
103
- try { fsImpl.fsyncSync(fd); } finally { fsImpl.closeSync(fd); }
113
+ // Windows opens a folder but refuses to flush it (EPERM); there is nothing to flush there.
114
+ try { fsImpl.fsyncSync(fd); } catch (error ) { if (!['EPERM', 'EINVAL', 'EISDIR', 'ENOTSUP'].includes(error?.code)) throw error; } finally { fsImpl.closeSync(fd); }
104
115
  }
105
116
 
106
117
  const migrationFailed = (code ) => ({
@@ -208,6 +219,30 @@ function refreshLock(fsImpl , root ) {
208
219
  try { fsImpl.utimesSync(`${lockTarget(root)}.lock`, now, now); } catch { /* the lock folder is ours; a failed touch only shortens its life */ }
209
220
  }
210
221
 
222
+ /**
223
+ * Removes temp files a crashed writer left in the lifecycle folder, once they are older than
224
+ * `staleMs`. Runs under the store lock, which every writer of these files holds, so none of them
225
+ * belongs to a live pass unless its lock was taken over as stale. Best effort: a file that cannot
226
+ * be removed is left for the next pass. `shouldStop` is checked before each unlink and each unlink
227
+ * counts against `maxFiles`, so a bounded caller's budget and stop hold here too.
228
+ * @returns {{removed: number, partial: boolean}} partial: stopped or out of budget with files left
229
+ */
230
+ function sweepStaleTmp(fsImpl , root , now , staleMs = STALE_TMP_MS, { maxFiles = Infinity, shouldStop } = {}) {
231
+ const dir = path.join(root, LIFECYCLE_REL);
232
+ let list ;
233
+ try { list = namesIn(fsImpl, dir); } catch { return { removed: 0, partial: false }; }
234
+ let removed = 0;
235
+ for (const name of list) {
236
+ if (!WRITE_TMP.test(name)) continue;
237
+ const file = path.join(dir, name);
238
+ const st = lstatOrNull(fsImpl, file);
239
+ if (!st || !st.isFile() || now.getTime() - st.mtimeMs <= staleMs) continue;
240
+ if (shouldStop?.() || removed >= maxFiles) return { removed, partial: true };
241
+ try { fsImpl.unlinkSync(file); removed += 1; } catch { /* left for the next pass */ }
242
+ }
243
+ return { removed, partial: false };
244
+ }
245
+
211
246
  /** Unlinks pending event files without clearing the journal until every hidden name is finished. */
212
247
  function removePending(fsImpl , root , names , onRemoved = () => {}, { maxFiles = Infinity, shouldStop } = {}) {
213
248
  const dir = path.join(root, EVENTS_REL);
@@ -294,7 +329,7 @@ function retain(fsImpl , root , window , now , { maxFiles = MAX_
294
329
 
295
330
  /** Runs a bounded lifecycle retention pass under a non-waiting store lock for the storage engine. */
296
331
  function retainLocked(root , options = {}) {
297
- const { window, now = new Date(), fsImpl = nodeFs, maxFiles = MAX_PASS_FILES, shouldStop } = options;
332
+ const { window, now = new Date(), fsImpl = nodeFs, maxFiles = MAX_PASS_FILES, shouldStop, staleTmpMs = STALE_TMP_MS } = options;
298
333
  const bounded = Object.hasOwn(options, 'maxFiles') || Object.hasOwn(options, 'shouldStop');
299
334
  const outcome = (removed , warning , partial = false, selected = [], code ) => bounded
300
335
  ? { removed, warning, partial, selected, ...(code ? { code } : {}) }
@@ -306,7 +341,9 @@ function retainLocked(root , options = {}) {
306
341
  const release = tryAcquireLock(fsImpl, lockTarget(root));
307
342
  if (!release) return outcome(0, [], true);
308
343
  try {
309
- const result = retain(fsImpl, root, window, now, { maxFiles, shouldStop });
344
+ const swept = sweepStaleTmp(fsImpl, root, now, staleTmpMs, { maxFiles, shouldStop });
345
+ if (swept.partial) return outcome(0, [], true);
346
+ const result = retain(fsImpl, root, window, now, { maxFiles: Math.max(0, maxFiles - swept.removed), shouldStop });
310
347
  return outcome(result.removedCount, result.warning, result.partial, result.selected, result.code);
311
348
  } finally {
312
349
  release();
@@ -326,7 +363,7 @@ function needsPrune(fsImpl , root , window , now ) {
326
363
  * @param {string} root projectRoot() result
327
364
  * @param {{env?: Object, now?: Date, fsImpl?: Object}} [options] defaults process.env, new Date(), node:fs
328
365
  * @returns {{ok: true, lines: string[]}
329
- * | {ok: false, finding: 'store-migration-failed'|'store-locked', message: string, lines: string[]}}
366
+ * | {ok: false, finding: 'store-migration-pending'|'store-migration-failed'|'store-locked', message: string, lines: string[]}}
330
367
  * `lines` are for stderr, without the verb prefix.
331
368
  * @throws {StoreUnsafeError} a symlinked new store folder, or a symlinked old folder when a copy must read it
332
369
  */
@@ -345,6 +382,17 @@ function prepareStore(root , { env = process.env, now = new Date(), fsImp
345
382
  try { migrate = legacyEventFiles(fsImpl, root).length > 0; } catch (error ) { return legacyUnreadable(error.code || error.message); }
346
383
  }
347
384
  }
385
+ if (env.DOFLOW_UPKEEP === 'off') {
386
+ if (migrate) {
387
+ return {
388
+ ok: false,
389
+ finding: 'store-migration-pending',
390
+ message: 'the lifecycle store has not been copied yet; run any followup, lifecycle or goal command once without DOFLOW_UPKEEP=off. Nothing was written.',
391
+ lines: [],
392
+ };
393
+ }
394
+ return { ok: true, lines: [...notice, ...invalid] };
395
+ }
348
396
  const retentionDue = readJournal(root, fsImpl).pending.length > 0 || needsPrune(fsImpl, root, window, now);
349
397
  if (!migrate && !markerDue && !retentionDue) return { ok: true, lines: [...notice, ...invalid] };
350
398
  if (migrate) refuseLinks(fsImpl, root, [LEGACY_LIFECYCLE_REL, LEGACY_EVENTS_REL]);
@@ -367,6 +415,7 @@ function prepareStore(root , { env = process.env, now = new Date(), fsImp
367
415
  throw error;
368
416
  }
369
417
  try {
418
+ sweepStaleTmp(fsImpl, root, now);
370
419
  // Decided again under the lock: another verb may have finished the copy while this one waited.
371
420
  let marked = hasMarker(fsImpl, root);
372
421
  if (!marked && storeHasEvents(fsImpl, root)) {
@@ -390,4 +439,4 @@ function prepareStore(root , { env = process.env, now = new Date(), fsImp
390
439
  }
391
440
  }
392
441
 
393
- module.exports = { prepareStore, retainLocked, LEGACY_LIFECYCLE_REL, LEGACY_EVENTS_REL, MARKER_REL };
442
+ module.exports = { prepareStore, retainLocked, removedLine, STALE_TMP_MS, LEGACY_LIFECYCLE_REL, LEGACY_EVENTS_REL, MARKER_REL };