@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
@@ -0,0 +1,217 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const { acquireLock } = require('./task-state.js');
9
+ const { loadStorageRegistry } = require('./storage/registry.js');
10
+ const { REPO_ROOT } = require('../helper/repo-root.js');
11
+
12
+ // Where a task's records live (115 IC-004) and how records a linked worktree wrote before that rule
13
+ // reach the main checkout (IC-006): one locked sweep per redirected call.
14
+ //
15
+ // A linked, non-sandbox worktree reads and writes every per-task record under the main checkout's
16
+ // `.doflow/state`, whatever folders the worktree holds: a conditional rule flipped mid-run when
17
+ // do-brainstorm created the feature folder in the worktree and split one task's records across two
18
+ // stores. A DoFlow sandbox (`.doflow-worktree-base`) keeps its own store, and `--global` is $HOME.
19
+
20
+ /** This module's path as `core/registry/storage.json` names it in a store's `writers`. */
21
+ const ADOPTER = 'src/runtime/state-root.js';
22
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
23
+
24
+ /**
25
+ * IC-004: the root a task-store verb or `orchestrate` reads and writes its records in.
26
+ * @param {Object} o parsed CLI options (`global`, `positional`)
27
+ * @param {Object|null} checkouts a `listCheckouts` result, or null when none was listed
28
+ * @returns {{root: string, redirected: boolean}}
29
+ */
30
+ function taskStateRoot(o , checkouts ) {
31
+ if (!o.global && checkouts?.ok && checkouts.isLinked && !checkouts.sandbox && checkouts.main) {
32
+ return { root: checkouts.main, redirected: true };
33
+ }
34
+ return { root: o.global ? os.homedir() : path.resolve(o.positional?.[0] || '.'), redirected: false };
35
+ }
36
+
37
+ /**
38
+ * The project-state stores a worktree's records are adopted from: the ones that declare this module
39
+ * among their `writers` in `core/registry/storage.json`, so the list is the registry's, not a copy.
40
+ * @param {Object} [registry] a loaded storage registry (default: the shipped one)
41
+ * @returns {Array<{id: string, path: string, match: RegExp}>}
42
+ */
43
+ function adoptedStores(registry = loadStorageRegistry({ repoRoot: REPO_ROOT })) {
44
+ return registry.stores
45
+ .filter((store ) => store.root === 'project-state' && Array.isArray(store.writers) && store.writers.includes(ADOPTER))
46
+ .map((store ) => ({ id: store.id, path: store.path, match: new RegExp(store.match) }));
47
+ }
48
+
49
+ /** Whether `name` is a namespace directory of a store (a feature slug), not a `<file>.lock` directory. */
50
+ function isNamespaceDir(name ) {
51
+ return SAFE_ID.test(name) && !name.includes('..') && !name.endsWith('.lock');
52
+ }
53
+
54
+ /** The record files of one store directory of the worktree, by the store's exact `match`: flat files
55
+ * and the files of each `<slug>/` directory, as paths relative to the store. */
56
+ function storeFiles(dir , match , fsImpl ) {
57
+ const out = [];
58
+ let entries ;
59
+ try { entries = fsImpl.readdirSync(dir, { withFileTypes: true }); } catch { return out; }
60
+ for (const entry of entries) {
61
+ // A link is listed as it stands, file or directory, so the sweep refuses it instead of skipping it.
62
+ if (entry.isSymbolicLink() && (match.test(entry.name) || isNamespaceDir(entry.name))) out.push(entry.name);
63
+ else if (entry.isFile() && match.test(entry.name)) out.push(entry.name);
64
+ else if (entry.isDirectory() && isNamespaceDir(entry.name)) {
65
+ let inner ;
66
+ try { inner = fsImpl.readdirSync(path.join(dir, entry.name), { withFileTypes: true }); } catch { continue; }
67
+ for (const file of inner) if ((file.isFile() || file.isSymbolicLink()) && match.test(file.name)) out.push(path.join(entry.name, file.name));
68
+ }
69
+ }
70
+ return out.sort();
71
+ }
72
+
73
+ /** The first link among `root`'s own path components down to `file` (the root itself is the
74
+ * checkout and is not owned here), or null. A component that does not exist yet is not a link. */
75
+ function linkOnTheWay(root , file , fsImpl ) {
76
+ let at = root;
77
+ for (const part of path.relative(root, file).split(path.sep)) {
78
+ at = path.join(at, part);
79
+ let stat ;
80
+ try { stat = fsImpl.lstatSync(at); } catch { return null; }
81
+ if (stat.isSymbolicLink()) return at;
82
+ }
83
+ return null;
84
+ }
85
+
86
+ /** The task id a flat record file names: `<taskId>.json` or the claims file `<taskId>_claims.json`. */
87
+ function taskIdOf(name ) {
88
+ const base = name.slice(0, -'.json'.length);
89
+ return base.endsWith('_claims') ? base.slice(0, -'_claims'.length) : base;
90
+ }
91
+
92
+ /** The feature a flat record names (`featureSlug` on a run, `slug` on a readiness record), or null. */
93
+ function ownSlugOf(bytes ) {
94
+ try {
95
+ const data = JSON.parse(bytes.toString('utf8'));
96
+ const named = [data?.featureSlug, data?.slug].find((v) => typeof v === 'string' && SAFE_ID.test(v) && !v.includes('..'));
97
+ return named || null;
98
+ } catch { return null; }
99
+ }
100
+
101
+ /** Writes `bytes` to `to` through a temporary file beside it and a rename, so a reader never sees a
102
+ * partial record. The one write path for a record copied between stores or checkouts. */
103
+ function placeRecord(to , bytes , fsImpl = fs) {
104
+ const tmp = `${to}.${process.pid}.tmp`;
105
+ fsImpl.writeFileSync(tmp, bytes);
106
+ fsImpl.renameSync(tmp, to);
107
+ }
108
+
109
+ function realOrNull(p , fsImpl ) {
110
+ try { return fsImpl.realpathSync(p); } catch { return null; }
111
+ }
112
+
113
+ /**
114
+ * IC-006 (115 C-3, D-1, D-4): moves every record a linked worktree holds in its own `.doflow/state`
115
+ * to the main checkout's, before the verb reads any of them. One sweep per redirected call, over
116
+ * every adopted store, whatever task the call names.
117
+ *
118
+ * Nothing is swept when the two state directories are one directory (a worktree whose
119
+ * `.doflow/state` links to the main checkout's). Per file: a link on either side's own path, or a
120
+ * source and target that are one file (`dev:ino`), refuses that file; otherwise the target's lock and
121
+ * then the source's are taken, the source is re-read under them (gone means another sweep moved it),
122
+ * and it goes to the same relative path, except that a flat record of a task id other than its
123
+ * feature's slug goes under `<store>/<slug>/` (the slug it names, else the call's `recordSlug`, so it
124
+ * lands where every reader looks). Equal bytes at the target delete the source; no target means a
125
+ * copy to a temporary file, a rename into place, a re-read compared with the source, and only then
126
+ * the source's deletion; other bytes leave both and are reported. Files already moved stay moved.
127
+ * @param {Object} options
128
+ * @param {string} options.mainRoot the main checkout
129
+ * @param {string} options.worktreeRoot the linked worktree the verb runs in
130
+ * @param {string|null|(() => string|null)} [options.recordSlug] the call's feature slug, asked for only
131
+ * when a flat record names no feature of its own
132
+ * @param {Object} [options.registry] storage registry (default: the shipped one)
133
+ * @param {Object} [options.fsImpl]
134
+ * @returns {{ok: boolean, adopted: string[], removed: string[], error?: 'store-conflict',
135
+ * conflicts?: Array<{worktree: string, main: string, reason: 'differs'|'link'|'same-file'}>}}
136
+ */
137
+ function sweepWorktreeRecords({ mainRoot, worktreeRoot, recordSlug = null, registry, fsImpl = fs } ) {
138
+ const done = { ok: true, adopted: [] , removed: [] };
139
+ const conflicts = [];
140
+ const fromState = path.join(worktreeRoot, '.doflow', 'state');
141
+ const toState = path.join(mainRoot, '.doflow', 'state');
142
+ const fromReal = realOrNull(fromState, fsImpl);
143
+ if (fromReal === null || fromReal === realOrNull(toState, fsImpl)) return done;
144
+ let callSlug ;
145
+ const slugOfCall = () => {
146
+ if (callSlug === undefined) {
147
+ let value = null;
148
+ try { value = typeof recordSlug === 'function' ? recordSlug() : recordSlug; } catch { /* no feature */ }
149
+ callSlug = typeof value === 'string' && SAFE_ID.test(value) && !value.includes('..') ? value : null;
150
+ }
151
+ return callSlug;
152
+ };
153
+
154
+ for (const store of adoptedStores(registry)) {
155
+ const fromDir = path.join(fromState, store.path);
156
+ let dirStat ;
157
+ try { dirStat = fsImpl.lstatSync(fromDir); } catch { continue; }
158
+ if (dirStat.isSymbolicLink()) {
159
+ conflicts.push({ worktree: fromDir, main: path.join(toState, store.path), reason: 'link' });
160
+ continue;
161
+ }
162
+ if (!dirStat.isDirectory()) continue;
163
+ for (const rel of storeFiles(fromDir, store.match, fsImpl)) {
164
+ const from = path.join(fromDir, rel);
165
+ if (linkOnTheWay(worktreeRoot, from, fsImpl)) {
166
+ conflicts.push({ worktree: from, main: path.join(toState, store.path, rel), reason: 'link' });
167
+ continue;
168
+ }
169
+ let toRel = rel;
170
+ if (!rel.includes(path.sep)) {
171
+ let bytes = null;
172
+ try { bytes = fsImpl.readFileSync(from); } catch { continue; }
173
+ const slug = ownSlugOf(bytes ) || slugOfCall();
174
+ if (slug && slug !== taskIdOf(rel)) toRel = path.join(slug, rel);
175
+ }
176
+ const to = path.join(toState, store.path, toRel);
177
+ if (linkOnTheWay(mainRoot, to, fsImpl)) { conflicts.push({ worktree: from, main: to, reason: 'link' }); continue; }
178
+ try {
179
+ const a = fsImpl.lstatSync(from);
180
+ const b = fsImpl.lstatSync(to);
181
+ if (a.dev === b.dev && a.ino === b.ino) { conflicts.push({ worktree: from, main: to, reason: 'same-file' }); continue; }
182
+ } catch { /* one side is absent: not one file */ }
183
+
184
+ fsImpl.mkdirSync(path.dirname(to), { recursive: true });
185
+ const releaseTarget = acquireLock(fsImpl, to);
186
+ try {
187
+ const releaseSource = acquireLock(fsImpl, from);
188
+ try {
189
+ let bytes ;
190
+ try { bytes = fsImpl.readFileSync(from); } catch (error ) {
191
+ if (error.code === 'ENOENT') continue; // another sweep moved it
192
+ throw error;
193
+ }
194
+ let there = null;
195
+ try { there = fsImpl.readFileSync(to); } catch (error ) { if (error.code !== 'ENOENT') throw error; }
196
+ if (there !== null) {
197
+ if (!bytes.equals(there)) { conflicts.push({ worktree: from, main: to, reason: 'differs' }); continue; }
198
+ fsImpl.unlinkSync(from);
199
+ done.removed.push(from);
200
+ continue;
201
+ }
202
+ placeRecord(to, bytes, fsImpl);
203
+ if (!bytes.equals(fsImpl.readFileSync(to))) { conflicts.push({ worktree: from, main: to, reason: 'differs' }); continue; }
204
+ fsImpl.unlinkSync(from);
205
+ done.adopted.push(to);
206
+ } finally {
207
+ releaseSource();
208
+ }
209
+ } finally {
210
+ releaseTarget();
211
+ }
212
+ }
213
+ }
214
+ return conflicts.length ? { ...done, ok: false, error: 'store-conflict', conflicts } : done;
215
+ }
216
+
217
+ module.exports = { taskStateRoot, sweepWorktreeRecords, placeRecord, adoptedStores, ADOPTER };
@@ -5,7 +5,7 @@
5
5
  const path = require('node:path');
6
6
  const fs = require('node:fs');
7
7
  const { loadStorageRegistry } = require('./registry.js');
8
- const { resolveRoots, readPruneSetting } = require('./roots.js');
8
+ const { resolveRoots, createPruneSettingsResolver } = require('./roots.js');
9
9
  const { runPass } = require('./engine.js');
10
10
  const { upkeepAfterVerb } = require('./upkeep.js');
11
11
  const { REPO_ROOT } = require('../../helper/repo-root.js');
@@ -29,10 +29,15 @@ function handleStorageCommand({ action = 'report', json = false, global = false,
29
29
  return;
30
30
  }
31
31
  const roots = resolveRoots({ registry, projectRoot: global ? null : path.resolve(projectRoot), global, env });
32
- const projectSetting = readPruneSetting({ registry, roots, rootId: 'project-state', fsImpl });
33
- const userSetting = readPruneSetting({ registry, roots, rootId: 'user-state', fsImpl });
34
- const setting = Object.fromEntries(Object.keys(roots).map(rootId => [rootId,
35
- readPruneSetting({ registry, roots, rootId, fsImpl })]));
32
+ let settingsView = null;
33
+ const resolvePruneSetting = createPruneSettingsResolver({ roots, fsImpl, onView: (view ) => { settingsView = view; } });
34
+ const projectSetting = resolvePruneSetting('project-state');
35
+ const userSetting = resolvePruneSetting('user-state');
36
+ let deprecationNotes = [];
37
+ try {
38
+ if (settingsView) deprecationNotes = settingsView.notes.filter((note ) => note.code === 'deprecated' && note.key === 'storage.prune');
39
+ } catch { /* a settings fault does not change storage's fail-closed report */ }
40
+ const setting = Object.fromEntries(Object.keys(roots).map(rootId => [rootId, resolvePruneSetting(rootId)]));
36
41
  const mode = action === 'prune' ? 'prune' : 'report';
37
42
  const result = runPass({ registry, roots, mode, setting, now: Date.now(), env, fsImpl });
38
43
  const removed = result.stores.reduce((sum , row ) => sum + row.removed, 0);
@@ -42,6 +47,7 @@ function handleStorageCommand({ action = 'report', json = false, global = false,
42
47
  // IC-013: an explicitly set DOFLOW_RETENTION_HOURS still removes settled lifecycle events.
43
48
  console.log(removed === 0 ? `storage: pruning is off (${decision}); nothing removed`
44
49
  : `storage: pruning is off (${decision}); DOFLOW_RETENTION_HOURS still applied to lifecycle events: removed ${removed} ${entries}`);
50
+ for (const note of deprecationNotes) console.log(`note: ${note.text}`);
45
51
  return;
46
52
  }
47
53
  if (json) {
@@ -53,6 +59,7 @@ function handleStorageCommand({ action = 'report', json = false, global = false,
53
59
  return;
54
60
  }
55
61
  console.log(`Settings: project ${projectSetting.setting}${projectSetting.decidedBy ? ` (${projectSetting.decidedBy})` : ' (default)'}; user ${userSetting.setting}${userSetting.decidedBy ? ` (${userSetting.decidedBy})` : ' (default)'}`);
62
+ for (const note of deprecationNotes) console.log(`note: ${note.text}`);
56
63
  for (const row of result.stores) {
57
64
  const names = row.selected.slice(0, 20).map((entry ) => entry.name);
58
65
  const more = row.selected.length > 20 ? `\n ... and ${row.selected.length - 20} more` : '';
@@ -7,7 +7,7 @@ const path = require('node:path');
7
7
  const { randomBytes } = require('node:crypto');
8
8
  const { formatBound } = require('./registry.js');
9
9
  const { resolveStorePath } = require('./roots.js');
10
- const { selectCandidates, recordStillCandidate, protectedLogs, removableRules, ruleOrder, projectRoot } = require('./rules.js');
10
+ const { selectCandidates, recordStillCandidate, protectedLogs, unfinishedRuns, removableRules, ruleOrder, projectRoot } = require('./rules.js');
11
11
  const { retainLocked } = require('../lifecycle/store-upkeep.js');
12
12
  const { parseWindow, selectExpired } = require('../lifecycle/retention.js');
13
13
  const { readEvents } = require('../lifecycle/event-store.js');
@@ -192,7 +192,7 @@ function runPass({registry,roots,stores,mode,setting={},now=Date.now(),budgetMs,
192
192
  row.selected=selection.files.map((name )=>({name,reason:'settled',bytes:stat(fsImpl,path.join(location,name))?.size || 0}));
193
193
  } else {
194
194
  let output;
195
- try { output=retainLocked(project,{fsImpl,now:new Date(now),window,maxFiles:Math.max(0,cap-removals),shouldStop}); }
195
+ try { output=retainLocked(project,{fsImpl,now:new Date(now),window,maxFiles:Math.max(0,cap-removals),shouldStop,staleTmpMs:registry.settings.staleTmpHours*3600000}); }
196
196
  catch(error ) { row.failed.push({name:store.id,code:error.code || 'EIO'}); continue; }
197
197
  row.selected=output.selected.map(({name,bytes} )=>({name,reason:'settled',bytes}));
198
198
  row.removed=output.removed; removals+=row.removed;
@@ -222,6 +222,13 @@ function runPass({registry,roots,stores,mode,setting={},now=Date.now(),budgetMs,
222
222
  row.protected=candidates.filter((c )=>protectedSet.has(c.name)).length;
223
223
  candidates=candidates.filter((c )=>!protectedSet.has(c.name));
224
224
  }
225
+ if (store.protect==='unfinished-run' && candidates.length) {
226
+ // Fails closed: a file whose run cannot be shown terminal is kept, never reported as a failure.
227
+ const kept=unfinishedRuns({roots,fsImpl,names:candidates.map((c )=>c.name),shouldStop});
228
+ if (kept.incomplete) { result.partial=true; break; }
229
+ row.protected=candidates.filter((c )=>kept.names.has(c.name)).length;
230
+ candidates=candidates.filter((c )=>!kept.names.has(c.name));
231
+ }
225
232
  const orderedCandidates=store.entry==='record' ? removalOrder(candidates)
226
233
  : [...candidates].sort((a,b)=>a.mtimeMs-b.mtimeMs || a.name.localeCompare(b.name));
227
234
  if (shouldStop()) { result.partial=true; break; }
@@ -18,6 +18,9 @@ const RULES = new Map([
18
18
  ['existing', []],
19
19
  ['control', []],
20
20
  ]);
21
+ // open-work keeps logs an unfinished task's record names; unfinished-run keeps a worker-model file
22
+ // while its worker run or dispatch is not terminal or cannot be resolved (row 120 IC-005).
23
+ const PROTECTS = new Set(['open-work', 'unfinished-run']);
21
24
  const MOMENTS = new Set(['verb', 'session', 'verify', 'install', 'lifecycle-verb', 'uninstall']);
22
25
  const STORE_ID = /^[a-z][a-z0-9-]*$/;
23
26
 
@@ -70,8 +73,18 @@ function validateStorageRegistry(registry )
70
73
  for (const field of RULES.get(store.rule) || []) if (!positiveInteger(store.bound[field])) error(errors, `${where}.bound.${field}`, 'must be a positive integer');
71
74
  if (['existing', 'control'].includes(store.rule) && (typeof store.bound.text !== 'string' || !store.bound.text)) error(errors, `${where}.bound.text`, 'must be a non-empty string');
72
75
  if (store.rule === 'existing' && store.bound.value !== undefined && !positiveInteger(store.bound.value)) error(errors, `${where}.bound.value`, 'must be a positive integer when present');
76
+ if (store.rule === 'count-and-age' && store.bound.either !== undefined && typeof store.bound.either !== 'boolean') error(errors, `${where}.bound.either`, 'must be a boolean when present');
77
+ const allowed = new Set([
78
+ ...(RULES.get(store.rule) || []),
79
+ ...(['existing', 'control'].includes(store.rule) ? ['text'] : []),
80
+ ...(store.rule === 'existing' ? ['value'] : []),
81
+ ...(store.rule === 'count-and-age' ? ['either'] : []),
82
+ ]);
83
+ for (const key of Object.keys(store.bound)) {
84
+ if (!allowed.has(key)) error(errors, `${where}.bound`, `unknown key '${key}'`);
85
+ }
73
86
  }
74
- if (store.protect !== null && store.protect !== 'open-work') error(errors, `${where}.protect`, 'must be open-work or null');
87
+ if (store.protect !== null && !PROTECTS.has(store.protect)) error(errors, `${where}.protect`, 'must be open-work, unfinished-run or null');
75
88
  if (!Array.isArray(store.moments) || store.moments.some((moment ) => !MOMENTS.has(moment)) || new Set(store.moments).size !== store.moments.length) error(errors, `${where}.moments`, 'must be a unique subset of known moments');
76
89
  if (store.rule === 'existing' && store.moments?.length) error(errors, `${where}.moments`, 'must be empty for existing stores');
77
90
  if (!Array.isArray(store.writers) || store.writers.some((writer ) => typeof writer !== 'string' || !writer)) error(errors, `${where}.writers`, 'must be an array of repository-relative paths');
@@ -3,16 +3,16 @@
3
3
  // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
4
 
5
5
  const fs = require('node:fs');
6
- const os = require('node:os');
7
6
  const path = require('node:path');
7
+ const { configHome } = require('../config-home.js');
8
8
 
9
9
  function absolute(value ) { return typeof value === 'string' && path.isAbsolute(value) ? value : null; }
10
10
 
11
11
  /** Resolves IC-001's five physical roots; -g/global deliberately omits project roots. */
12
12
  function resolveRoots({ registry, projectRoot, global = false, env = process.env } = {}) {
13
- const home = absolute(env.HOME) || os.homedir();
14
- const xdgBase = env.XDG_CONFIG_HOME === undefined || env.XDG_CONFIG_HOME === ''
15
- ? path.join(home, '.config') : absolute(env.XDG_CONFIG_HOME);
13
+ // No home variable means no home base and no config home: nothing falls back to the account home (DD-7).
14
+ const home = absolute(env.HOME);
15
+ const xdgBase = configHome(env).base;
16
16
  const project = global ? null : absolute(projectRoot);
17
17
  const bases = { project, home, 'xdg-config': xdgBase };
18
18
  return Object.fromEntries(Object.entries(registry.roots).map(([id, root] ) => {
@@ -21,21 +21,71 @@ function resolveRoots({ registry, projectRoot, global = false, env = process.env
21
21
  }));
22
22
  }
23
23
 
24
- function readSetting(file , fsImpl ) {
25
- try {
26
- const first = fsImpl.readFileSync(file, 'utf8').split(/\r?\n/, 1)[0].trim().toLowerCase();
27
- return first === 'on' || first === 'off' ? first : 'off';
28
- } catch { return 'off'; }
24
+ /** Creates one IC-014 resolver for all roots in a command; a project view can also read global scope. */
25
+ function createPruneSettingsResolver({ roots, fsImpl = fs, onView } = {}) {
26
+ const resolverEnv = {};
27
+ if (roots['config-home']) resolverEnv.XDG_CONFIG_HOME = path.dirname(roots['config-home']);
28
+ let view = null;
29
+ let loadedView = false;
30
+ return (rootId ) => {
31
+ if (!loadedView) {
32
+ loadedView = true;
33
+ try {
34
+ let settingsRegistry;
35
+ if (fsImpl !== fs) {
36
+ const { loadSettingsRegistry } = require('../settings/registry.js');
37
+ const loaded = loadSettingsRegistry({ fsImpl });
38
+ if (!loaded.ok) throw new Error(loaded.detail);
39
+ settingsRegistry = loaded.registry;
40
+ }
41
+ const { loadSettings } = require('../settings/resolve.js');
42
+ const projectRoot = roots['project-doflow'] ? path.dirname(roots['project-doflow']) : null;
43
+ view = loadSettings({ projectRoot, env: resolverEnv, fsImpl, ...(settingsRegistry ? { registry: settingsRegistry } : {}) });
44
+ } catch {
45
+ // A settings registry fault fails closed for every root, as it did for each old call.
46
+ view = null;
47
+ }
48
+ if (typeof onView === 'function') onView(view);
49
+ }
50
+ if (!view) return { setting: 'off', decidedBy: null };
51
+ try {
52
+ const usesProject = rootId === 'project-doflow' || rootId === 'project-state';
53
+ const resolved = view.get('storage.prune', usesProject ? 'project' : 'global');
54
+ return {
55
+ setting: resolved.value === 'on' ? 'on' : 'off',
56
+ decidedBy: resolved.origin.layer === 'default' ? null : resolved.origin.source,
57
+ };
58
+ } catch {
59
+ return { setting: 'off', decidedBy: null };
60
+ }
61
+ };
29
62
  }
30
63
 
31
64
  /** Reads IC-014 and reports exactly which present file made the decision, if any. */
32
- function readPruneSetting({ registry, roots, rootId, fsImpl = fs } = {}) {
33
- const projectFile = roots['project-doflow'] ? path.join(roots['project-doflow'], registry.settings.optOutFile) : null;
34
- const userFile = roots['config-home'] ? path.join(roots['config-home'], registry.settings.optOutFile) : null;
65
+ function readPruneSetting({ roots, rootId, fsImpl = fs } = {}) {
35
66
  const usesProject = rootId === 'project-doflow' || rootId === 'project-state';
36
- if (usesProject && projectFile && fsImpl.existsSync(projectFile)) return { setting: readSetting(projectFile, fsImpl), decidedBy: projectFile };
37
- if (userFile && fsImpl.existsSync(userFile)) return { setting: readSetting(userFile, fsImpl), decidedBy: userFile };
38
- return { setting: 'on', decidedBy: null };
67
+ const global = !usesProject;
68
+ const projectRoot = !global && roots['project-doflow'] ? path.dirname(roots['project-doflow']) : null;
69
+ const resolverEnv = {};
70
+ if (roots['config-home']) resolverEnv.XDG_CONFIG_HOME = path.dirname(roots['config-home']);
71
+ try {
72
+ let settingsRegistry;
73
+ if (fsImpl !== fs) {
74
+ const { loadSettingsRegistry } = require('../settings/registry.js');
75
+ const loaded = loadSettingsRegistry({ fsImpl });
76
+ if (!loaded.ok) throw new Error(loaded.detail);
77
+ settingsRegistry = loaded.registry;
78
+ }
79
+ const { loadSettings } = require('../settings/resolve.js');
80
+ const view = loadSettings({ projectRoot, global, env: resolverEnv, fsImpl, ...(settingsRegistry ? { registry: settingsRegistry } : {}) });
81
+ const resolved = view.get('storage.prune', global ? 'global' : 'project');
82
+ return {
83
+ setting: resolved.value === 'on' ? 'on' : 'off',
84
+ decidedBy: resolved.origin.layer === 'default' ? null : resolved.origin.source,
85
+ };
86
+ } catch {
87
+ return { setting: 'off', decidedBy: null };
88
+ }
39
89
  }
40
90
 
41
91
  function linked(file , fsImpl ) {
@@ -60,4 +110,4 @@ function resolveStorePath({ store, roots, fsImpl = fs } = {})
60
110
  return current;
61
111
  }
62
112
 
63
- module.exports = { resolveRoots, readPruneSetting, resolveStorePath };
113
+ module.exports = { resolveRoots, readPruneSetting, createPruneSettingsResolver, resolveStorePath };
@@ -4,7 +4,7 @@
4
4
 
5
5
  const fs = require('node:fs');
6
6
  const path = require('node:path');
7
- const { workStatus, createWorkCache, refreshedWorkCache } = require('./work-status.js');
7
+ const { workStatus, recordExtension, createWorkCache, refreshedWorkCache } = require('./work-status.js');
8
8
  const { projectKey } = require('../lifecycle/report-store.js');
9
9
  const { readEvents } = require('../lifecycle/event-store.js');
10
10
 
@@ -50,7 +50,7 @@ function workCandidates(store , entries , context ) {
50
50
  const project = projectRoot(roots);
51
51
  if (!project) return [];
52
52
  const cache = context.cache || createWorkCache({ projectRoot: project, fsImpl });
53
- const recordMatch = new RegExp(store.match), selected = [];
53
+ const recordMatch = new RegExp(store.match), extensionLength = recordExtension(store.match).length, selected = [];
54
54
  const visit = (dir , namespace = null) => {
55
55
  let children;
56
56
  try { children = names(fsImpl, dir); } catch { return true; }
@@ -61,7 +61,7 @@ function workCandidates(store , entries , context ) {
61
61
  if (TMP.test(name) && now - stat.mtimeMs > settings.staleTmpHours * HOUR) {
62
62
  selected.push({ name: namespace ? `${namespace}/${name}` : name, reason: 'stale-tmp', bytes: stat.size, mtimeMs: stat.mtimeMs, recordStat: stat });
63
63
  } else if (recordMatch.test(name)) {
64
- const result = workStatus({ projectRoot: project, store: store.id, taskId: name.slice(0, -5), namespace, mtimeMs: stat.mtimeMs, cache, fsImpl, shouldStop });
64
+ const result = workStatus({ projectRoot: project, store: store.id, taskId: name.slice(0, -extensionLength), namespace, mtimeMs: stat.mtimeMs, cache, fsImpl, shouldStop });
65
65
  if (result.incomplete || shouldStop?.()) return false;
66
66
  if (result.state === 'finished' && now - result.since > store.bound.finishedDays * DAY)
67
67
  selected.push({ name: namespace ? `${namespace}/${name}` : name, reason: 'finished-work', bytes: stat.size, mtimeMs: result.since, recordStat: stat });
@@ -92,7 +92,7 @@ function recordStillCandidate(store , candidate , info , context
92
92
  if (!snapshot?.ok || JSON.stringify(current) !== JSON.stringify(snapshot.value)) return false;
93
93
  }
94
94
  const fresh = cache ? refreshedWorkCache(cache) : undefined;
95
- const status = workStatus({ projectRoot: projectRoot(roots), store: store.id, taskId: name.slice(0, -5), namespace, mtimeMs: info.mtimeMs, cache: fresh, fsImpl, shouldStop });
95
+ const status = workStatus({ projectRoot: projectRoot(roots), store: store.id, taskId: name.slice(0, -recordExtension(store.match).length), namespace, mtimeMs: info.mtimeMs, cache: fresh, fsImpl, shouldStop });
96
96
  return !status.incomplete && !shouldStop?.() && status.state === 'finished'
97
97
  && status.since === candidate.mtimeMs && now - status.since > store.bound.finishedDays * DAY;
98
98
  }
@@ -138,7 +138,7 @@ function protectedLogs({ registry, roots, fsImpl, cache, shouldStop } ) {
138
138
  continue;
139
139
  }
140
140
  if (!stat.isFile() || !recordMatch.test(name)) continue;
141
- const status = workStatus({ projectRoot: project, store: store.id, taskId: name.slice(0, -5), namespace, mtimeMs: stat.mtimeMs, cache: workCache, fsImpl, shouldStop });
141
+ const status = workStatus({ projectRoot: project, store: store.id, taskId: name.slice(0, -recordExtension(store.match).length), namespace, mtimeMs: stat.mtimeMs, cache: workCache, fsImpl, shouldStop });
142
142
  if (status.incomplete || shouldStop?.()) return false;
143
143
  if (status.state === 'finished') continue;
144
144
  try {
@@ -158,6 +158,73 @@ function protectedLogs({ registry, roots, fsImpl, cache, shouldStop } ) {
158
158
  }
159
159
  return { names: protectedNames, incomplete: false };
160
160
  }
161
+ const WORKER_UNIT = /^L\.([A-Za-z0-9][A-Za-z0-9_-]{0,55})\.([A-Za-z0-9][A-Za-z0-9_-]{0,55})\.([1-9][0-9]{0,3})\.json$/;
162
+ /** Every `<slug>.json` store file of `.doflow/state/<dir>` read by `read`; [] when the folder is
163
+ * absent, null when a link sits on the path or any file cannot be read (the caller fails closed). */
164
+ function stateStores(project , dir , read , fsImpl , shouldStop ) {
165
+ let current = project;
166
+ for (const segment of ['.doflow', 'state', dir]) {
167
+ current = path.join(current, segment);
168
+ let info;
169
+ try { info = fsImpl.lstatSync(current); }
170
+ catch (error ) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return []; return null; }
171
+ if (info.isSymbolicLink() || !info.isDirectory()) return null;
172
+ }
173
+ const stores = [];
174
+ let list;
175
+ try { list = names(fsImpl, current); } catch { return null; }
176
+ for (const name of list) {
177
+ if (shouldStop?.()) return null;
178
+ if (!name.endsWith('.json')) continue;
179
+ const info = safeStat(fsImpl, path.join(current, name));
180
+ if (!info || info.isSymbolicLink() || !info.isFile()) return null;
181
+ try { stores.push(read(path.join(current, name), fsImpl)); } catch { return null; }
182
+ }
183
+ return stores;
184
+ }
185
+ /**
186
+ * Protect kind `unfinished-run` (row 120 IC-005): of the candidate names `L.<run>.<lineage>.<attempt>.json`,
187
+ * the ones to keep. A name is released only when this project's worker run store holds at least one
188
+ * run whose `identity` is that unit and every such run is terminal, and no dispatch naming one of
189
+ * those runs (`workerRunId`) is still open. Terminal is the one end phase each store has: a worker
190
+ * run's `phase: 'ended'` (run-store.ts PHASES, which `transition` never leaves) and a dispatch's
191
+ * `phase: 'ended'` (dispatch/store.ts NEXT_PHASES, add-only once reached). Everything else is kept
192
+ * (fail closed): an unparsable name, no project, an unknown unit (another project's file, or a run
193
+ * record pruned first), a link on a store path or any store file that cannot be read.
194
+ */
195
+ function unfinishedRuns({ roots, fsImpl, names: candidates, shouldStop } ) {
196
+ const all = new Set (candidates);
197
+ const project = projectRoot(roots);
198
+ if (!project) return { names: all, incomplete: false };
199
+ const { readRuns } = require('../worker/run-store.js');
200
+ const { readDispatch } = require('../dispatch/store.js');
201
+ const runStores = stateStores(project, 'worker-runs', (file, impl) => ({ runs: readRuns(file, impl) }), fsImpl, shouldStop);
202
+ if (shouldStop?.()) return { names: all, incomplete: true };
203
+ const dispatchStores = runStores && stateStores(project, 'dispatch', readDispatch, fsImpl, shouldStop);
204
+ if (shouldStop?.()) return { names: all, incomplete: true };
205
+ if (!runStores || !dispatchStores) return { names: all, incomplete: false };
206
+ const units = new Map ();
207
+ for (const store of runStores) for (const [runId, run] of Object.entries(store.runs) ) {
208
+ const id = run?.identity;
209
+ if (!id || typeof id.run !== 'string' || typeof id.lineage !== 'string' || typeof id.attempt !== 'string') continue;
210
+ const key = `${id.run}.${id.lineage}.${id.attempt}`;
211
+ const unit = units.get(key) || { ended: true, runIds: [] };
212
+ unit.ended = unit.ended && run.phase === 'ended';
213
+ unit.runIds.push(runId);
214
+ units.set(key, unit);
215
+ }
216
+ const openRuns = new Set ();
217
+ for (const store of dispatchStores) for (const dispatch of Object.values(store.dispatches) ) {
218
+ if (dispatch.phase !== 'ended' && typeof dispatch.workerRunId === 'string') openRuns.add(dispatch.workerRunId);
219
+ }
220
+ const kept = new Set ();
221
+ for (const name of candidates) {
222
+ const match = WORKER_UNIT.exec(name);
223
+ const unit = match && units.get(`${match[1]}.${match[2]}.${match[3]}`);
224
+ if (!unit || !unit.ended || unit.runIds.some((runId) => openRuns.has(runId))) kept.add(name);
225
+ }
226
+ return { names: kept, incomplete: false };
227
+ }
161
228
  function lifecyclePathsSafe(project , fsImpl ) {
162
229
  for (const rel of [path.join('.doflow'), path.join('.doflow', 'state'), path.join('.doflow', 'state', 'lifecycle'), path.join('.doflow', 'state', 'lifecycle', 'events')]) {
163
230
  try { if (fsImpl.lstatSync(path.join(project, rel)).isSymbolicLink()) return false; }
@@ -236,14 +303,19 @@ function selectCandidates(store , entries , context ) {
236
303
  const { now, fsImpl, storeDir, roots, shouldStop } = context;
237
304
  switch (store.rule) {
238
305
  case 'count-and-age': {
239
- const ordered = [...entries].sort((a, b) => Number(b.name.slice(0, 13)) - Number(a.name.slice(0, 13)) || a.name.localeCompare(b.name));
306
+ const hasTimestamp = (entry ) => /^[0-9]{13}/.test(entry.name);
307
+ const entryTime = (entry ) => hasTimestamp(entry) ? Number(entry.name.slice(0, 13)) : entry.stat.mtimeMs;
308
+ const ordered = [...entries].sort((a, b) => entryTime(b) - entryTime(a) || a.name.localeCompare(b.name));
240
309
  if (stopped(context)) return null;
241
310
  const selected = [];
242
311
  for (let index = 0; index < ordered.length; index += 1) {
243
312
  if (shouldStop?.()) return null;
244
- const entry = ordered[index], time = Number(entry.name.slice(0, 13));
245
- if (index >= store.bound.keepNewest || now - time > store.bound.maxAgeDays * DAY)
246
- selected.push({ name: entry.name, reason: index >= store.bound.keepNewest ? 'count' : 'age', bytes: entry.stat.size, mtimeMs: time });
313
+ const entry = ordered[index], time = entryTime(entry);
314
+ const beyondCount = index >= store.bound.keepNewest, beyondAge = now - time > store.bound.maxAgeDays * DAY;
315
+ // Preserve timestamped stores' count-or-age policy; mtime entries must exceed both bounds unless bound.either is set.
316
+ const eitherBound = hasTimestamp(entry) || store.bound?.either === true;
317
+ if (eitherBound ? beyondCount || beyondAge : beyondCount && beyondAge)
318
+ selected.push({ name: entry.name, reason: beyondCount ? 'count' : 'age', bytes: entry.stat.size, mtimeMs: time });
247
319
  }
248
320
  return selected;
249
321
  }
@@ -279,4 +351,4 @@ function selectCandidates(store , entries , context ) {
279
351
  default: throw new Error(`Unknown storage rule '${store.rule}'`);
280
352
  }
281
353
  }
282
- module.exports={selectCandidates,recordStillCandidate,protectedLogs,removableRules,ruleOrder,projectRoot,BODY};
354
+ module.exports={selectCandidates,recordStillCandidate,protectedLogs,unfinishedRuns,removableRules,ruleOrder,projectRoot,BODY};
@@ -9,7 +9,7 @@ const { REPO_ROOT } = require('../../helper/repo-root.js');
9
9
  const { loadStorageRegistry } = require('./registry.js');
10
10
  const { resolveRoots, readPruneSetting } = require('./roots.js');
11
11
  const engine = require('./engine.js');
12
- const { RunLedger } = require('../trace/ledger.js');
12
+ const { RunLedger, outerProjectRoot } = require('../trace/ledger.js');
13
13
 
14
14
  // Load once: a not-due moment only reads the three small markers, never the catalogue again.
15
15
  let registry ;
@@ -147,7 +147,10 @@ function passForRoot({ roots, rootId, stores, now, env, fsImpl, throttled = fals
147
147
 
148
148
  /** IC-009: silent throttled upkeep after a verb, using that verb's resolved store root. */
149
149
  function upkeepAfterVerb({ projectRoot, now = Date.now(), env = process.env, fsImpl = fs } = {}) {
150
+ // IC-004: the read switch, exact `off` like DOFLOW_TELEMETRY; nothing at all is written.
151
+ if (env.DOFLOW_UPKEEP === 'off') return;
150
152
  return silently(() => {
153
+ projectRoot = projectRoot && outerProjectRoot(projectRoot);
151
154
  const roots = resolveRoots({ registry, projectRoot, env });
152
155
  for (const rootId of ['project-state', 'user-state']) {
153
156
  try { ensureIgnore({ roots, rootId, projectRoot, fsImpl }); } catch { /* continue other roots */ }
@@ -165,14 +168,19 @@ function upkeepAfterVerb({ projectRoot, now = Date.now(), env = process.env, fsI
165
168
 
166
169
  /** IC-010: the verify moment is project-only and unthrottled. Wiring belongs to D.3. */
167
170
  function upkeepAfterVerify({ projectRoot, now = Date.now(), env = process.env, fsImpl = fs } = {}) {
171
+ // IC-004: the switch covers every upkeep moment, so `off` prunes, locks and writes nothing here either.
172
+ if (env.DOFLOW_UPKEEP === 'off') return;
168
173
  return silently(() => {
174
+ projectRoot = projectRoot && outerProjectRoot(projectRoot);
169
175
  const roots = resolveRoots({ registry, projectRoot, env });
170
- return passForRoot({ roots, rootId: 'project-state', stores: ['verification-logs'], now, env, fsImpl });
176
+ return passForRoot({ roots, rootId: 'project-state', stores: ['verification-logs', 'verification-reuse'], now, env, fsImpl });
171
177
  });
172
178
  }
173
179
 
174
180
  /** IC-011: only this scope's recovery store; install/update call sites belong to D.4. */
175
181
  function storageAfterInstall({ scope, scopeRoot, now = Date.now(), env = process.env, fsImpl = fs } = {}) {
182
+ // IC-004: no ignore file, prune pass or lock at install time under the switch either.
183
+ if (env.DOFLOW_UPKEEP === 'off') return;
176
184
  return silently(() => {
177
185
  if (!['project', 'global'].includes(scope)) return;
178
186
  const global = scope === 'global';