@khoavu882/doflow 2.0.0-beta.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/README.md +12 -11
  2. package/bin/doflow.js +21 -11
  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/permission-audit.sh +3 -0
  9. package/core/harnesses/claude/hooks/post-compact.sh +35 -32
  10. package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
  11. package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
  12. package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
  13. package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
  14. package/core/harnesses/claude/hooks/session-end.sh +23 -10
  15. package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
  16. package/core/harnesses/claude/settings/settings.json +18 -0
  17. package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
  18. package/core/harnesses/codex/agents/core-implementer.toml +1 -0
  19. package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
  20. package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
  21. package/core/harnesses/codex/agents/research-writer.toml +1 -1
  22. package/core/harnesses/codex/agents/reviewer.toml +10 -0
  23. package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
  24. package/core/harnesses/codex/agents/system-architect.toml +1 -1
  25. package/core/harnesses/codex/hooks/lib.sh +44 -25
  26. package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
  27. package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
  28. package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
  29. package/core/harnesses/codex/hooks/subagent-audit.sh +1 -0
  30. package/core/harnesses/copilot/hooks/doflow-lib.sh +93 -0
  31. package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
  32. package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +9 -0
  33. package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
  34. package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
  35. package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
  36. package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
  37. package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
  38. package/core/harnesses/copilot/hooks/doflow.json +57 -0
  39. package/core/harnesses/kiro/agents/doflow.json +1 -1
  40. package/core/harnesses/kiro/hooks/lib.sh +26 -6
  41. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +1 -0
  42. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +1 -0
  43. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +1 -0
  44. package/core/harnesses/kiro/hooks/session-start.sh +1 -0
  45. package/core/harnesses/kiro/hooks/stop-check.sh +1 -0
  46. package/core/harnesses/pi/extensions/doflow-guard.js +8 -0
  47. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +0 -1
  48. package/core/harnesses/shared/hooks/bridge/bridge.sh +102 -9
  49. package/core/harnesses/shared/hooks/bridge/envelope.jq +87 -18
  50. package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
  51. package/core/harnesses/shared/hooks/bridge/respond.sh +10 -2
  52. package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +28 -14
  53. package/core/harnesses/shared/hooks/policies/capture-failure.sh +48 -15
  54. package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
  55. package/core/harnesses/shared/hooks/policies/contract.sh +3 -0
  56. package/core/harnesses/shared/hooks/policies/deny-json.sh +2 -11
  57. package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
  58. package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
  59. package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
  60. package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
  61. package/core/harnesses/shared/hooks/policies/lib.sh +63 -40
  62. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +3 -2
  63. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +213 -12
  64. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +114 -38
  65. package/core/harnesses/shared/hooks/policies/session-context.sh +30 -1
  66. package/core/harnesses/shared/hooks/policies/stop-check.sh +20 -9
  67. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +5 -2
  68. package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
  69. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +60 -155
  70. package/core/harnesses/shared/hooks/stream-hook-runner.js +47 -8
  71. package/core/harnesses/shared/locator/doflow-run +2 -3
  72. package/core/registry/advice.json +3 -3
  73. package/core/registry/assets.json +55 -82
  74. package/core/registry/capabilities.json +12 -0
  75. package/core/registry/contracts.json +58 -31
  76. package/core/registry/contribution-points.json +32 -0
  77. package/core/registry/gates.json +32 -0
  78. package/core/registry/guidance-caps.json +40 -0
  79. package/core/registry/harnesses.json +1442 -363
  80. package/core/registry/lifecycle.json +485 -304
  81. package/core/registry/mcp.json +5 -1
  82. package/core/registry/models.json +7 -1
  83. package/core/registry/package-templates/README.md +19 -0
  84. package/core/registry/package-templates/agent/files/agent.md +9 -0
  85. package/core/registry/package-templates/agent/template.json +4 -0
  86. package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
  87. package/core/registry/package-templates/capability-provider/template.json +11 -0
  88. package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
  89. package/core/registry/package-templates/debate-preset/template.json +4 -0
  90. package/core/registry/package-templates/review-preset/files/preset.json +30 -0
  91. package/core/registry/package-templates/review-preset/template.json +4 -0
  92. package/core/registry/package-templates/skill/files/SKILL.md +8 -0
  93. package/core/registry/package-templates/skill/template.json +4 -0
  94. package/core/registry/package-templates/worker/files/worker.json +92 -0
  95. package/core/registry/package-templates/worker/template.json +11 -0
  96. package/core/registry/packages/doflow/doflow-package.json +15 -0
  97. package/core/registry/packages/doflow/presets/debate-3.json +26 -0
  98. package/core/registry/protocols.json +61 -0
  99. package/core/registry/routes.json +17 -0
  100. package/core/registry/rules.json +431 -0
  101. package/core/registry/settings.json +276 -0
  102. package/core/registry/storage.json +679 -31
  103. package/core/registry/telemetry.json +236 -0
  104. package/core/registry/transcripts.json +142 -0
  105. package/core/registry/verification.json +76 -1
  106. package/core/registry/workflows.json +55 -0
  107. package/core/shared/agent-specs/core-implementer.md +3 -0
  108. package/core/shared/agent-specs/delivery-lead.md +68 -0
  109. package/core/shared/agent-specs/quality-guardian.md +5 -1
  110. package/core/shared/agent-specs/research-writer.md +10 -3
  111. package/core/shared/agent-specs/reviewer.md +53 -0
  112. package/core/shared/agent-specs/spec-analyst.md +5 -6
  113. package/core/shared/agent-specs/system-architect.md +5 -1
  114. package/core/shared/guidance/DOFLOW_CORE.md +1 -5
  115. package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
  116. package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
  117. package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
  118. package/core/shared/guidance/pointers/codex.md +1 -4
  119. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
  120. package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
  121. package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
  122. package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
  123. package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
  124. package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
  125. package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
  126. package/core/shared/guidance/references/ORCHESTRATION.md +1 -2
  127. package/core/shared/guidance/rules/RULES.md +45 -0
  128. package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
  129. package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
  130. package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
  131. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
  132. package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
  133. package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
  134. package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
  135. package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
  136. package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
  137. package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
  138. package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
  139. package/core/shared/scripts/doflow/bash/validate-artifacts.sh +48 -4
  140. package/core/shared/scripts/doflow/bin/doflow-run +149 -34
  141. package/core/shared/skills/do/SKILL.md +12 -10
  142. package/core/shared/skills/do/references/advice_calls.md +26 -0
  143. package/core/shared/skills/do/references/pm_routing.md +1 -2
  144. package/core/shared/skills/do/references/program_mode.md +31 -0
  145. package/core/shared/skills/do-brainstorm/SKILL.md +20 -74
  146. package/core/shared/skills/do-brainstorm/references/handoff_branches.md +35 -0
  147. package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
  148. package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
  149. package/core/shared/skills/do-code-review/SKILL.md +41 -144
  150. package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
  151. package/core/shared/skills/do-code-review/references/tools.md +90 -0
  152. package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
  153. package/core/shared/skills/do-design/SKILL.md +18 -17
  154. package/core/shared/skills/do-diagnose/SKILL.md +14 -4
  155. package/core/shared/skills/do-document/SKILL.md +11 -1
  156. package/core/shared/skills/do-document/references/deep_research.md +1 -2
  157. package/core/shared/skills/do-document/references/embedded-research.md +18 -6
  158. package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
  159. package/core/shared/skills/do-execute-plan/SKILL.md +15 -58
  160. package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
  161. package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
  162. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +26 -1
  163. package/core/shared/skills/do-execute-plan/references/run_position.md +13 -1
  164. package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
  165. package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
  166. package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
  167. package/core/shared/skills/do-flow/SKILL.md +8 -9
  168. package/core/shared/skills/do-flow/references/task_classes.md +2 -2
  169. package/core/shared/skills/do-git/SKILL.md +4 -0
  170. package/core/shared/skills/do-implement/SKILL.md +4 -7
  171. package/core/shared/skills/do-plan/SKILL.md +26 -15
  172. package/core/shared/skills/do-test/SKILL.md +7 -4
  173. package/core/shared/templates/doflow/constitution-template.md +9 -3
  174. package/core/shared/templates/doflow/data-model-template.md +1 -1
  175. package/core/shared/templates/doflow/design-template.md +113 -16
  176. package/core/shared/templates/doflow/plan-template.md +11 -10
  177. package/core/shared/templates/doflow/question-log-template.md +14 -2
  178. package/core/shared/templates/doflow/requirement-template.md +1 -1
  179. package/core/worker-evidence/antigravity/checks.json +14 -0
  180. package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
  181. package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
  182. package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
  183. package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
  184. package/core/worker-evidence/codex/checks.json +38 -0
  185. package/package.json +10 -6
  186. package/src/adapters/antigravity/index.js +143 -66
  187. package/src/adapters/claude/index.js +24 -4
  188. package/src/adapters/codex/agents.js +25 -15
  189. package/src/adapters/codex/index.js +48 -13
  190. package/src/adapters/codex/native-projection.js +6 -2
  191. package/src/adapters/copilot/index.js +58 -23
  192. package/src/adapters/copy-tree.js +290 -44
  193. package/src/adapters/guidance-manifest.js +71 -0
  194. package/src/adapters/guidance-pointer.js +19 -0
  195. package/src/adapters/hook-commands.js +6 -10
  196. package/src/adapters/index.js +1 -0
  197. package/src/adapters/kiro/index.js +23 -12
  198. package/src/adapters/opencode/index.js +8 -8
  199. package/src/adapters/pi/hooks.js +2 -1
  200. package/src/adapters/pi/index.js +14 -7
  201. package/src/adapters/resolve-source.js +26 -2
  202. package/src/cli/commands/install.js +2 -1
  203. package/src/cli/commands/reconcile.js +53 -9
  204. package/src/cli/commands/remove.js +2 -2
  205. package/src/cli/commands/rollback.js +6 -3
  206. package/src/cli/commands/self-update.js +18 -9
  207. package/src/cli/commands/status.js +21 -2
  208. package/src/cli/commands/ui.js +99 -0
  209. package/src/cli/commands/update.js +2 -1
  210. package/src/cli/index.js +322 -29
  211. package/src/cli/runtime-commands.js +209 -57
  212. package/src/cli/shared.js +0 -2
  213. package/src/helper/git.js +1 -1
  214. package/src/helper/guardrails.js +1 -1
  215. package/src/helper/marker-merge.js +31 -13
  216. package/src/helper/package-ids.js +37 -0
  217. package/src/helper/semver.js +94 -0
  218. package/src/install/backup.js +105 -15
  219. package/src/install/context.js +1 -1
  220. package/src/install/targets.js +31 -16
  221. package/src/lifecycle/coverage.js +20 -3
  222. package/src/lifecycle/hook-tier.js +160 -0
  223. package/src/lifecycle/index.js +412 -78
  224. package/src/lifecycle/retired-harnesses.js +318 -0
  225. package/src/lifecycle/view.js +35 -15
  226. package/src/registry/index.js +551 -26
  227. package/src/registry/worker-ids.js +45 -0
  228. package/src/release/build-package.js +0 -3
  229. package/src/release/docs-blocks.js +492 -0
  230. package/src/release/preflight.js +425 -0
  231. package/src/release/rehearse.js +401 -0
  232. package/src/release/release-workflow.js +733 -0
  233. package/src/release/sync-plugin-versions.js +43 -7
  234. package/src/runtime/advice/bench-gate.js +25 -0
  235. package/src/runtime/advice/cli.js +59 -16
  236. package/src/runtime/advice/client.js +32 -9
  237. package/src/runtime/advice/config.js +98 -133
  238. package/src/runtime/advice/doctor.js +1 -1
  239. package/src/runtime/advice/elevation.js +97 -0
  240. package/src/runtime/advice/endpoint-guard.js +9 -3
  241. package/src/runtime/advice/mode.js +37 -0
  242. package/src/runtime/advice/record.js +18 -5
  243. package/src/runtime/advice/registry.js +4 -1
  244. package/src/runtime/advice/shadow-config.js +229 -0
  245. package/src/runtime/advice/shadow-dispatcher.js +276 -0
  246. package/src/runtime/brief-rules.js +81 -0
  247. package/src/runtime/brief-tools.js +111 -34
  248. package/src/runtime/candidate.js +20 -11
  249. package/src/runtime/capabilities/executor.js +450 -0
  250. package/src/runtime/capabilities/loopback.js +41 -0
  251. package/src/runtime/capabilities/sanitizer.js +88 -0
  252. package/src/runtime/capability-router.js +135 -3
  253. package/src/runtime/checkouts.js +73 -11
  254. package/src/runtime/claims.js +13 -7
  255. package/src/runtime/cli.js +196 -19
  256. package/src/runtime/config-home.js +42 -0
  257. package/src/runtime/context/arbitrate.js +767 -0
  258. package/src/runtime/context/delivery-store.js +4 -6
  259. package/src/runtime/context/emission-rules.js +118 -0
  260. package/src/runtime/context/identity.js +227 -0
  261. package/src/runtime/context/primary-command.js +117 -0
  262. package/src/runtime/context/primary.js +344 -0
  263. package/src/runtime/context/summary-store.js +655 -0
  264. package/src/runtime/context-pack.js +154 -78
  265. package/src/runtime/decision-register.js +13 -4
  266. package/src/runtime/dispatch/admission.js +392 -0
  267. package/src/runtime/dispatch/attempt-end.js +274 -0
  268. package/src/runtime/dispatch/command.js +628 -0
  269. package/src/runtime/dispatch/controls.js +904 -0
  270. package/src/runtime/dispatch/envelope.js +276 -0
  271. package/src/runtime/dispatch/grants.js +310 -0
  272. package/src/runtime/dispatch/integrate.js +617 -0
  273. package/src/runtime/dispatch/loop.js +1835 -0
  274. package/src/runtime/dispatch/store.js +352 -0
  275. package/src/runtime/dispatch/verify-run.js +201 -0
  276. package/src/runtime/evidence-ledger.js +34 -7
  277. package/src/runtime/failure/capture.js +3 -1
  278. package/src/runtime/failure/cli.js +25 -9
  279. package/src/runtime/failure/home.js +57 -30
  280. package/src/runtime/failure/store.js +7 -2
  281. package/src/runtime/feature-pointer.js +174 -0
  282. package/src/runtime/feature-resolve.js +13 -2
  283. package/src/runtime/fleet/command.js +77 -0
  284. package/src/runtime/fleet/projects.js +178 -0
  285. package/src/runtime/fleet/tree.js +383 -0
  286. package/src/runtime/freshness.js +33 -3
  287. package/src/runtime/git-memo.js +46 -0
  288. package/src/runtime/guidance/ablation.js +427 -0
  289. package/src/runtime/health.js +565 -17
  290. package/src/runtime/history-compactor.js +13 -3
  291. package/src/runtime/implementation-gate.js +277 -22
  292. package/src/runtime/inventory/coverage.js +1 -1
  293. package/src/runtime/inventory/index.js +1 -1
  294. package/src/runtime/inventory/precedence.js +2 -2
  295. package/src/runtime/inventory/siblings.js +6 -4
  296. package/src/runtime/knowledge/code-corpus.js +277 -0
  297. package/src/runtime/knowledge/code-lexical.js +130 -0
  298. package/src/runtime/knowledge/code-retrieval.js +307 -0
  299. package/src/runtime/knowledge/code-validation.js +106 -0
  300. package/src/runtime/knowledge/gp3-gate.js +74 -0
  301. package/src/runtime/knowledge/graph-adapter.js +288 -0
  302. package/src/runtime/knowledge/graph-fallback.js +394 -0
  303. package/src/runtime/knowledge/hybrid.js +136 -8
  304. package/src/runtime/knowledge/retrieval.js +34 -1
  305. package/src/runtime/knowledge/rtk-compress.js +77 -0
  306. package/src/runtime/knowledge/semble-bridge.py +106 -0
  307. package/src/runtime/knowledge/semble-lifecycle.js +114 -0
  308. package/src/runtime/knowledge/semble-provider.js +365 -0
  309. package/src/runtime/lifecycle/cli.js +16 -6
  310. package/src/runtime/lifecycle/event-store.js +8 -6
  311. package/src/runtime/lifecycle/followup.js +6 -1
  312. package/src/runtime/lifecycle/overview.js +24 -1
  313. package/src/runtime/lifecycle/report-store.js +8 -10
  314. package/src/runtime/lifecycle/retention.js +95 -31
  315. package/src/runtime/lifecycle/root.js +9 -1
  316. package/src/runtime/lifecycle/status.js +34 -17
  317. package/src/runtime/lifecycle/store-upkeep.js +56 -7
  318. package/src/runtime/mask.js +134 -7
  319. package/src/runtime/mcp-consultations.js +40 -6
  320. package/src/runtime/model-router.js +79 -7
  321. package/src/runtime/models/catalogue.js +191 -0
  322. package/src/runtime/models/command.js +153 -0
  323. package/src/runtime/off-words.js +12 -0
  324. package/src/runtime/outcome.js +2 -2
  325. package/src/runtime/packages/command.js +1009 -0
  326. package/src/runtime/packages/contributions.js +116 -0
  327. package/src/runtime/packages/digest.js +132 -0
  328. package/src/runtime/packages/fetch.js +182 -0
  329. package/src/runtime/packages/grants.js +192 -0
  330. package/src/runtime/packages/index-file.js +134 -0
  331. package/src/runtime/packages/instruction.js +90 -0
  332. package/src/runtime/packages/manifest.js +331 -0
  333. package/src/runtime/packages/points.js +477 -0
  334. package/src/runtime/packages/programs.js +216 -0
  335. package/src/runtime/packages/scaffold.js +277 -0
  336. package/src/runtime/packages/source.js +90 -0
  337. package/src/runtime/packages/state.js +164 -0
  338. package/src/runtime/packages/store.js +449 -0
  339. package/src/runtime/packages/tar.js +106 -0
  340. package/src/runtime/packages/view.js +406 -0
  341. package/src/runtime/plan-check.js +59 -0
  342. package/src/runtime/plan-content.js +324 -0
  343. package/src/runtime/policy-init.js +118 -0
  344. package/src/runtime/profiles/doctor.js +96 -0
  345. package/src/runtime/profiles/resolve.js +244 -0
  346. package/src/runtime/profiles/rollback.js +254 -0
  347. package/src/runtime/profiles/sampler.js +41 -0
  348. package/src/runtime/readiness-record.js +16 -11
  349. package/src/runtime/readiness.js +46 -5
  350. package/src/runtime/research-request.js +50 -25
  351. package/src/runtime/retrieval-plan.js +10 -30
  352. package/src/runtime/retrieval-record.js +75 -5
  353. package/src/runtime/review/command.js +246 -0
  354. package/src/runtime/review/dominance.js +68 -0
  355. package/src/runtime/review/findings.js +434 -0
  356. package/src/runtime/review/identity.js +255 -0
  357. package/src/runtime/review/protocol-command.js +109 -0
  358. package/src/runtime/review/protocols.js +405 -0
  359. package/src/runtime/review/resolve-protocol.js +302 -0
  360. package/src/runtime/review/start.js +530 -0
  361. package/src/runtime/roadmap/facts.js +479 -0
  362. package/src/runtime/roadmap/gates.js +270 -0
  363. package/src/runtime/roadmap/loop.js +1636 -0
  364. package/src/runtime/roadmap/merge.js +381 -0
  365. package/src/runtime/roadmap/rows.js +1113 -0
  366. package/src/runtime/roadmap/seed.js +951 -0
  367. package/src/runtime/rules-report.js +336 -0
  368. package/src/runtime/scaffold/artifacts.js +60 -9
  369. package/src/runtime/scaffold/generate.js +21 -9
  370. package/src/runtime/settings/autopilot.js +106 -0
  371. package/src/runtime/settings/cli.js +280 -0
  372. package/src/runtime/settings/config-write.js +314 -0
  373. package/src/runtime/settings/files.js +271 -0
  374. package/src/runtime/settings/legacy.js +154 -0
  375. package/src/runtime/settings/lexical.js +56 -0
  376. package/src/runtime/settings/prepared-run.js +269 -0
  377. package/src/runtime/settings/project-layers.js +118 -0
  378. package/src/runtime/settings/refs.js +288 -0
  379. package/src/runtime/settings/registry.js +553 -0
  380. package/src/runtime/settings/resolve.js +713 -0
  381. package/src/runtime/settings/shared.js +37 -0
  382. package/src/runtime/settings/worker-billing.js +107 -0
  383. package/src/runtime/settings/write.js +269 -0
  384. package/src/runtime/slot-command.js +78 -0
  385. package/src/runtime/slots.js +671 -0
  386. package/src/runtime/source-version.js +1 -0
  387. package/src/runtime/state-root.js +217 -0
  388. package/src/runtime/storage/cli.js +12 -5
  389. package/src/runtime/storage/engine.js +9 -2
  390. package/src/runtime/storage/registry.js +14 -1
  391. package/src/runtime/storage/roots.js +66 -16
  392. package/src/runtime/storage/rules.js +82 -10
  393. package/src/runtime/storage/upkeep.js +10 -2
  394. package/src/runtime/storage/work-status.js +245 -26
  395. package/src/runtime/task-attempts.js +589 -69
  396. package/src/runtime/task-classifier.js +47 -11
  397. package/src/runtime/task-graph.js +373 -0
  398. package/src/runtime/task-run.js +778 -0
  399. package/src/runtime/task-scope.js +176 -55
  400. package/src/runtime/task-sources.js +147 -0
  401. package/src/runtime/task-state.js +374 -64
  402. package/src/runtime/team/admission.js +301 -0
  403. package/src/runtime/team/envelope.js +324 -0
  404. package/src/runtime/team/review-guard.js +156 -0
  405. package/src/runtime/trace/analyze.js +427 -0
  406. package/src/runtime/trace/baseline.js +770 -0
  407. package/src/runtime/trace/ledger.js +63 -140
  408. package/src/runtime/trace/otlp-export.js +568 -0
  409. package/src/runtime/trace/otlp-map.js +481 -0
  410. package/src/runtime/trace/spans.js +745 -0
  411. package/src/runtime/trace/telemetry.js +225 -0
  412. package/src/runtime/trace/views.js +1 -0
  413. package/src/runtime/transcripts/command.js +98 -0
  414. package/src/runtime/transcripts/cursor.js +326 -0
  415. package/src/runtime/transcripts/read.js +126 -0
  416. package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
  417. package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
  418. package/src/runtime/transcripts/readers/copilot-events.js +90 -0
  419. package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
  420. package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
  421. package/src/runtime/transcripts/registry.js +161 -0
  422. package/src/runtime/transcripts/stats.js +161 -0
  423. package/src/runtime/verification/check-key.js +628 -0
  424. package/src/runtime/verification/contract-runner.js +546 -11
  425. package/src/runtime/verification/engine.js +499 -9
  426. package/src/runtime/verification/impact.js +617 -0
  427. package/src/runtime/verification/inflight.js +205 -0
  428. package/src/runtime/verification/registry.js +176 -0
  429. package/src/runtime/verification/reuse.js +184 -0
  430. package/src/runtime/verification/telemetry-emit.js +254 -0
  431. package/src/runtime/verification/test-selection.js +67 -7
  432. package/src/runtime/worker/active.js +90 -0
  433. package/src/runtime/worker/cancel.js +222 -0
  434. package/src/runtime/worker/catalogue.js +122 -0
  435. package/src/runtime/worker/command.js +677 -0
  436. package/src/runtime/worker/evidence.js +288 -0
  437. package/src/runtime/worker/launch.js +288 -0
  438. package/src/runtime/worker/models.js +137 -0
  439. package/src/runtime/worker/parsers.js +218 -0
  440. package/src/runtime/worker/probes/index.js +706 -0
  441. package/src/runtime/worker/process-tree.js +415 -0
  442. package/src/runtime/worker/quota.js +43 -0
  443. package/src/runtime/worker/readiness.js +713 -0
  444. package/src/runtime/worker/run-store.js +166 -0
  445. package/src/runtime/worker/supervisor.js +541 -0
  446. package/src/runtime/worker/worktree-check.js +252 -0
  447. package/src/runtime/workflow-engine.js +5 -1
  448. package/src/runtime/workflow-orchestrator.js +578 -63
  449. package/src/runtime/worktree.js +149 -22
  450. package/src/state/index.js +1 -1
  451. package/src/state/lockfile.js +64 -10
  452. package/src/state/run-lock.js +1 -1
  453. package/types/node-commonjs.d.ts +106 -7
  454. package/core/harnesses/gemini/hooks/hooks.json +0 -77
  455. package/core/harnesses/gemini/hooks/lib.sh +0 -302
  456. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -10
  457. package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
  458. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -10
  459. package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
  460. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -10
  461. package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
  462. package/core/harnesses/gemini/hooks/session-end.sh +0 -5
  463. package/core/harnesses/gemini/hooks/session-start.sh +0 -7
  464. package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
  465. package/core/shared/guidance/FLAGS.md +0 -25
  466. package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
  467. package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
  468. package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
  469. package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
  470. package/src/adapters/gemini/hooks.js +0 -251
  471. package/src/adapters/gemini/index.js +0 -429
@@ -13,7 +13,10 @@
13
13
  // This module is the one seam both stores persist through:
14
14
  //
15
15
  // - a per-file lock (`<file>.lock` directory — mkdirSync is atomic on every platform) makes the
16
- // read-merge-write below a critical section across processes on this machine;
16
+ // read-merge-write below a critical section across processes on this machine; the holder
17
+ // marks the directory with its own `owner-*` entry, so release and the pre-write check act on
18
+ // that holder's lock only, and a stale lock is reclaimed in place by one process (see the
19
+ // protocol above `attemptLock`);
17
20
  // - inside the lock, the writer re-reads the file and MERGES what is on disk into its own view
18
21
  // before writing, so an overlapping writer's items survive;
19
22
  // - the payload carries a monotonically increasing `revision`, so an overlapped-and-merged
@@ -24,6 +27,7 @@
24
27
  // the version named, instead of guessing at its shape.
25
28
 
26
29
  const path = require('node:path');
30
+ const { randomBytes } = require('node:crypto');
27
31
  const { isDeepStrictEqual } = require('node:util');
28
32
 
29
33
  /** Three-way merge against the reader's snapshot. Unchanged local records never overwrite a
@@ -52,74 +56,340 @@ function sleep(ms ) {
52
56
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
53
57
  }
54
58
 
59
+ // ── The lock protocol (117 Phase C; FU-8acds5, FU-vgkrvd) ───────────────────────────────────────────
60
+ //
61
+ // The lock is the `<file>.lock` directory, created with mkdirSync. A holder marks it with its own
62
+ // `owner-<pid>-<random>` entry. Before this protocol any process that found the lock older than
63
+ // LOCK_STALE_MS removed it by name and every release removed it by name, so two waiters that judged
64
+ // one stale lock could both take it and each one's release could delete the other's. Three facts
65
+ // carry the protocol (the safety argument, one interleaving at a time, is in the 117 feature records:
66
+ // research/phase-c-protocol.md):
67
+ //
68
+ // - Pinned: no DoFlow runtime renames a lock directory and rmdir removes only an empty one, so a
69
+ // directory with an entry in it stays at `<file>.lock` until it is empty. A process whose entry
70
+ // is in the lock knows the directory at that path is the one holding its entry.
71
+ // - Released: this runtime removes a lock directory only in its release, after removing its own
72
+ // entry from the lock it held; it reclaims a stale lock in place. Older runtimes remove one by
73
+ // name in their release, or after judging a lock stale.
74
+ // - Alone: a process holds the lock when its entry is the only entry in it. Two processes whose
75
+ // entries are in one directory cannot both have been alone in it.
76
+ //
77
+ // - Acquire: note the time, mkdir `<file>.lock`, mkdir its entry inside, read the time again. Under
78
+ // PUBLISH_BOUND_MS, no process can have judged the new directory stale before the entry landed,
79
+ // so the entry is in this process's own directory. Over it, the entry may have landed in a
80
+ // directory another process made after reclaiming this one (an older runtime's empty lock looks
81
+ // exactly like a new one), so the process removes its entry and starts over. Then it lists the
82
+ // lock: alone, it holds.
83
+ // - Reclaim (the lock is older than LOCK_STALE_MS): stat, list, stat again (the same directory,
84
+ // unchanged, so every entry listed has been there that long); mkdir its own entry; stat again:
85
+ // not the same directory means the judged one was removed and the entry landed in a newer lock,
86
+ // so it removes its entry and starts over. Then it removes the judged entries by name (names are
87
+ // unique, so a later holder's entry is never among them) and lists the lock: alone, it holds. The
88
+ // directory never empties on the way, so an older runtime's rmdir of a stale lock fails instead of
89
+ // removing it.
90
+ // - Contention: two entries mean two processes reached the lock. The one whose entry name sorts
91
+ // first waits for the other to leave (it holds next); the other removes its entry and waits. A
92
+ // process that removes its entry and finds the lock emptied enters it again (once, under a fresh
93
+ // name, after checking it is the same directory) instead of leaving an empty lock everyone would
94
+ // wait 10 s on.
95
+ // - Release notes the lock's identity while its entry is in it, removes the entry, then removes the
96
+ // directory only if it is still that one (an older runtime may have made a new, empty lock there
97
+ // if the release stalled). A holder whose entry is gone (its lock was reclaimed) removes nothing;
98
+ // a waiting process's entry keeps the directory, and that process holds next.
99
+ // - `updateTaskState` checks the holder's entry is still in the lock (`release.held()`) after
100
+ // writing its tmp file and before the rename: a holder that was paused past LOCK_STALE_MS and
101
+ // reclaimed writes nothing.
102
+ //
103
+ // Runtimes from before this protocol see a directory they did not create: they wait on a fresh one,
104
+ // and on a stale one their rmdir fails (it is not empty), so they fail closed ("Could not lock",
105
+ // `store-locked`) instead of taking it; their release of a lock reclaimed from them fails the same
106
+ // way. Whatever a process leaves when it dies is reclaimed once LOCK_STALE_MS old. The bounds (what
107
+ // this does not cover) are listed once, in docs/architecture.md under "The task-state lock".
108
+ const OWNER_PREFIX = 'owner-';
109
+ // The longest a fresh acquire may take from its mkdir to its entry and still trust the directory is
110
+ // its own: half the stale age, so a process that judged it stale cannot have done so before the
111
+ // entry landed, with room for coarse file timestamps.
112
+ const PUBLISH_BOUND_MS = LOCK_STALE_MS / 2;
113
+ // How long tryAcquireLock waits for a process that reached the lock at the same moment to leave.
114
+ const CONTEND_MS = 40;
115
+ const CONTEND_WAIT_MS = 2;
116
+
117
+
118
+
55
119
  /**
56
- * Makes one non-waiting attempt to take the cross-process lock for a state file. A lock older than
57
- * LOCK_STALE_MS may be broken; a live holder is never waited on. Returns a release function or null.
58
- * @param {Object} fsImpl
59
- * @param {string} file
60
- * @returns {(() => void)|null}
120
+ * Optional lock settings (FU-31dyvs, for a store whose holders are identified by their entry): the
121
+ * defaults are the protocol above, unchanged.
122
+ * - `stale` replaces the age rule of a reclaim. It is asked after the listing re-check, with the
123
+ * entries listed and the lock's age, and the reclaim goes ahead only when it says true. It must
124
+ * treat an empty lock by age (an acquire in progress makes an empty lock first; see
125
+ * PUBLISH_BOUND_MS), and it may judge an entry by its owner: an entry whose owner is dead can be
126
+ * reclaimed at any age, because a dead process never acts on it again.
127
+ * - `entryName` makes this process's entry name (default `owner-<pid>-<random>`); every name it
128
+ * makes must be unique.
129
+ * - `removeEntry` removes one judged entry by name (default rmdir); it throws as rmdir would.
130
+ * - `retries` is acquireLock's attempt budget (default LOCK_RETRIES, each LOCK_WAIT_MS apart).
61
131
  */
62
- function tryAcquireLock(fsImpl , file ) {
63
- const lockDir = `${file}.lock`;
64
- const release = () => { try { fsImpl.rmdirSync(lockDir); } catch { /* released is released */ } };
132
+
133
+
134
+
135
+
136
+
137
+
138
+
139
+ /** Same directory: same device and inode, and the same birth time where the platform reports one. */
140
+ function sameDir(a , b ) {
141
+ return a.dev === b.dev && a.ino === b.ino && (!a.birthtimeMs || !b.birthtimeMs || a.birthtimeMs === b.birthtimeMs);
142
+ }
143
+
144
+ /** This attempt's entry name; `leave` gives it a fresh one when it re-enters an emptied lock. */
145
+
146
+
147
+ /** Removes this process's entry; gone already is fine (a reclaimer removed it). */
148
+ function withdraw(fsImpl , lockDir , me ) {
149
+ try { fsImpl.rmdirSync(path.join(lockDir, me.name)); } catch { /* not there: nothing of ours is left */ }
150
+ }
151
+
152
+ /**
153
+ * With this process's entry in the lock: `held` when it is the only entry; `queued` when another
154
+ * entry is there and ours sorts first (we hold once it leaves); otherwise we leave (`busy`, or `held`
155
+ * when leaving found everyone else gone). `again` when our entry is gone (the lock was reclaimed from
156
+ * us). With `rejoin` false (an entry that came back to an emptied lock) it never queues or comes back.
157
+ */
158
+ function elect(fsImpl , lockDir , me , rejoin = true, options = {}) {
159
+ let entries ;
160
+ let pinned ;
65
161
  try {
66
- fsImpl.mkdirSync(lockDir);
67
- return release;
68
- } catch (error ) {
69
- if (error.code !== 'EEXIST') throw error;
162
+ entries = fsImpl.readdirSync(lockDir);
163
+ if (!entries.includes(me.name)) return 'again';
164
+ if (entries.length === 1) return 'held';
165
+ if (rejoin && entries.every((name ) => name === me.name || me.name < name)) return 'queued';
166
+ pinned = pinnedDir(fsImpl, lockDir, me);
167
+ } catch { pinned = null; }
168
+ if (!pinned) { withdraw(fsImpl, lockDir, me); return 'again'; }
169
+ if (!rejoin) { withdraw(fsImpl, lockDir, me); return 'busy'; }
170
+ return leave(fsImpl, lockDir, me, pinned, options);
171
+ }
172
+
173
+ /** The stat of the lock while our entry is in it (so it is the directory holding our entry), or null. */
174
+ function pinnedDir(fsImpl , lockDir , me ) {
175
+ try {
176
+ const stat = fsImpl.statSync(lockDir);
177
+ return fsImpl.existsSync(path.join(lockDir, me.name)) ? stat : null;
178
+ } catch { return null; }
179
+ }
180
+
181
+ /**
182
+ * Removes our entry from the lock `pinned` (its stat taken while our entry was in it). If that left
183
+ * it empty, everyone else left after we looked, and an empty lock would block every process until it
184
+ * is stale: we enter it again, once, under a fresh name (a reclaimer that listed the old name removes
185
+ * names it listed), and hold it if we are alone. Never removes the directory.
186
+ */
187
+ function leave(fsImpl , lockDir , me , pinned , options = {}) {
188
+ withdraw(fsImpl, lockDir, me);
189
+ try {
190
+ if (fsImpl.readdirSync(lockDir).length > 0) return 'busy';
191
+ me.name = (options.entryName || newToken)();
192
+ fsImpl.mkdirSync(path.join(lockDir, me.name));
193
+ } catch (error ) { return error.code === 'ENOENT' ? 'again' : 'busy'; }
194
+ let same = false;
195
+ try { same = sameDir(pinned, fsImpl.statSync(lockDir)); } catch { /* treat it as changed */ }
196
+ if (!same) { withdraw(fsImpl, lockDir, me); return 'again'; }
197
+ return elect(fsImpl, lockDir, me, false, options);
198
+ }
199
+
200
+ /** A queued attempt that stops waiting: leave the lock (or hold it, when the other left meanwhile). */
201
+ function giveUp(fsImpl , lockDir , me , options = {}) {
202
+ const step = elect(fsImpl, lockDir, me, true, options);
203
+ if (step !== 'queued') return step;
204
+ const pinned = pinnedDir(fsImpl, lockDir, me);
205
+ if (!pinned) { withdraw(fsImpl, lockDir, me); return 'again'; }
206
+ return leave(fsImpl, lockDir, me, pinned, options);
207
+ }
208
+
209
+ /** Reclaims the lock at `lockDir` in place when it is stale. `again` means look again now. */
210
+ function reclaimIfStale(fsImpl , lockDir , me , options = {}) {
211
+ const missing = (error ) => (error.code === 'ENOENT' ? 'again' : 'busy');
212
+ let judged ;
213
+ let entries ;
214
+ try {
215
+ judged = fsImpl.statSync(lockDir);
216
+ if (!options.stale && Date.now() - judged.mtimeMs <= LOCK_STALE_MS) return 'busy';
217
+ entries = fsImpl.readdirSync(lockDir);
218
+ const listed = fsImpl.statSync(lockDir);
219
+ if (!sameDir(judged, listed) || listed.mtimeMs !== judged.mtimeMs) return 'again';
220
+ } catch (error ) { return missing(error); }
221
+ if (options.stale && !options.stale({ lockDir, entries, ageMs: Date.now() - judged.mtimeMs })) return 'busy';
222
+ try { fsImpl.mkdirSync(path.join(lockDir, me.name)); } catch (error ) { return missing(error); }
223
+ // Our entry pins the directory it landed in; it must be the one judged stale.
224
+ let same = false;
225
+ try { same = sameDir(judged, fsImpl.statSync(lockDir)); } catch { /* treat it as changed */ }
226
+ if (!same) {
227
+ withdraw(fsImpl, lockDir, me);
228
+ return 'again';
70
229
  }
71
- let lock ;
72
- try { lock = fsImpl.statSync(lockDir); } catch (error ) {
73
- if (error.code !== 'ENOENT') return null;
74
- try { fsImpl.mkdirSync(lockDir); return release; } catch (retryError ) {
75
- if (retryError.code === 'EEXIST') return null;
76
- throw retryError;
230
+ for (const name of entries) {
231
+ try {
232
+ if (options.removeEntry) options.removeEntry(fsImpl, lockDir, name);
233
+ else fsImpl.rmdirSync(path.join(lockDir, name));
234
+ } catch (error ) {
235
+ if (error.code === 'ENOENT') continue; // another reclaimer, or its released holder, removed it
236
+ withdraw(fsImpl, lockDir, me); // not an entry this protocol makes: leave the lock alone
237
+ return 'busy';
77
238
  }
78
239
  }
79
- if (Date.now() - lock.mtimeMs <= LOCK_STALE_MS) return null;
80
- try { fsImpl.rmdirSync(lockDir); } catch (error ) {
81
- if (error.code !== 'ENOENT') return null;
240
+ return elect(fsImpl, lockDir, me, true, options);
241
+ }
242
+
243
+ /**
244
+ * A queued attempt's next look, for a lock with a `stale` rule (FU-31dyvs C-5): the other entries are
245
+ * judged as a reclaim judges them (listing re-check, `stale`, the directory still the one holding our
246
+ * entry), and when they are stale they are removed by name, so a queued process does not wait out its
247
+ * budget behind a holder that died. Without a `stale` rule this is `elect`, unchanged.
248
+ */
249
+ function queuedStep(fsImpl , lockDir , me , options ) {
250
+ if (!options.stale) return elect(fsImpl, lockDir, me, true, options);
251
+ let judged ;
252
+ let others ;
253
+ try {
254
+ judged = fsImpl.statSync(lockDir);
255
+ const entries = fsImpl.readdirSync(lockDir);
256
+ const listed = fsImpl.statSync(lockDir);
257
+ if (!sameDir(judged, listed) || listed.mtimeMs !== judged.mtimeMs || !entries.includes(me.name)) return elect(fsImpl, lockDir, me, true, options);
258
+ others = entries.filter((name ) => name !== me.name);
259
+ } catch { return elect(fsImpl, lockDir, me, true, options); }
260
+ if (others.length === 0 || !options.stale({ lockDir, entries: others, ageMs: Date.now() - judged.mtimeMs })) return elect(fsImpl, lockDir, me, true, options);
261
+ const pinned = pinnedDir(fsImpl, lockDir, me);
262
+ if (!pinned || !sameDir(pinned, judged)) return elect(fsImpl, lockDir, me, true, options);
263
+ for (const name of others) {
264
+ try {
265
+ if (options.removeEntry) options.removeEntry(fsImpl, lockDir, name);
266
+ else fsImpl.rmdirSync(path.join(lockDir, name));
267
+ } catch (error ) {
268
+ if (error.code !== 'ENOENT') break; // not removable now: elect decides as before
269
+ }
270
+ }
271
+ return elect(fsImpl, lockDir, me, true, options);
272
+ }
273
+
274
+ /** One step towards holding the lock under `me`: held, queued (our entry waits in it), busy, or again. */
275
+ function attemptLock(fsImpl , lockDir , me , options = {}) {
276
+ const started = Date.now();
277
+ try {
278
+ fsImpl.mkdirSync(lockDir);
279
+ } catch (error ) {
280
+ // Windows reports EPERM/EACCES for a lock folder another process is deleting at this moment.
281
+ if (error.code !== 'EEXIST' && !(process.platform === 'win32' && (error.code === 'EPERM' || error.code === 'EACCES'))) throw error;
282
+ return reclaimIfStale(fsImpl, lockDir, me, options);
82
283
  }
83
- try { fsImpl.mkdirSync(lockDir); return release; } catch (error ) {
84
- if (error.code === 'EEXIST') return null;
284
+ try {
285
+ fsImpl.mkdirSync(path.join(lockDir, me.name));
286
+ } catch (error ) {
287
+ if (error.code === 'ENOENT') return 'again'; // removed before it was marked: an older runtime's release or stale break
85
288
  throw error;
86
289
  }
290
+ if (Date.now() - started >= PUBLISH_BOUND_MS) {
291
+ // Paused long enough for the new directory to be judged stale and replaced: the entry may sit in
292
+ // another process's lock. Leave it; the directory (ours or not) is left as found.
293
+ withdraw(fsImpl, lockDir, me);
294
+ return 'again';
295
+ }
296
+ return elect(fsImpl, lockDir, me, true, options);
87
297
  }
88
298
 
299
+ /** The release function of a held lock; `release.held()` re-checks ownership before a write. */
300
+ function ownedRelease(fsImpl , lockDir , me ) {
301
+ const entry = path.join(lockDir, me.name);
302
+ let released = false;
303
+ const release = () => {
304
+ if (released) return;
305
+ released = true;
306
+ const mine = pinnedDir(fsImpl, lockDir, me); // the directory we hold, while our entry pins it
307
+ try { fsImpl.rmdirSync(entry); } catch { return; } // reclaimed: the lock is not ours to remove
308
+ // Removed only if it is still that directory: a release stalled here past LOCK_STALE_MS may find
309
+ // an older runtime's new (empty) lock at the path. What stays is the gap between this stat and the
310
+ // rmdir (docs/architecture.md, "The task-state lock", bound 3).
311
+ try {
312
+ if (mine && sameDir(mine, fsImpl.statSync(lockDir))) fsImpl.rmdirSync(lockDir);
313
+ } catch { /* a waiting process's entry is in it; that process holds next */ }
314
+ };
315
+ release.held = () => !released && fsImpl.existsSync(entry);
316
+ return release;
317
+ }
318
+
319
+ const newToken = () => `${OWNER_PREFIX}${process.pid}-${randomBytes(6).toString('hex')}`;
320
+
89
321
  /**
90
- * Takes the cross-process lock for one state file, retrying for the existing bounded wait interval.
91
- * A stale lock is broken because its holder died mid-write; the rename-based write keeps the file whole.
322
+ * Makes one attempt to take the cross-process lock for a state file, without waiting on a holder:
323
+ * a lock older than LOCK_STALE_MS is reclaimed, a live holder is never waited on. It waits only when
324
+ * another process reached the lock at the same moment, for at most CONTEND_MS (40 ms) for that
325
+ * process to leave. Returns a release function or null.
92
326
  * @param {Object} fsImpl
93
327
  * @param {string} file
328
+ * @param {LockOptions} [options] see LockOptions; the defaults are the protocol above
329
+ * @returns {(() => void)|null}
330
+ */
331
+ function tryAcquireLock(fsImpl , file , options = {}) {
332
+ const lockDir = `${file}.lock`;
333
+ const deadline = Date.now() + CONTEND_MS;
334
+ for (let attempt = 0; attempt < 3; attempt += 1) {
335
+ const me = { name: (options.entryName || newToken)() };
336
+ let step = attemptLock(fsImpl, lockDir, me, options);
337
+ while (step === 'queued' && Date.now() < deadline) {
338
+ sleep(CONTEND_WAIT_MS);
339
+ step = queuedStep(fsImpl, lockDir, me, options);
340
+ }
341
+ if (step === 'queued') step = giveUp(fsImpl, lockDir, me, options);
342
+ if (step === 'held') return ownedRelease(fsImpl, lockDir, me);
343
+ if (step === 'busy') return null;
344
+ }
345
+ return null;
346
+ }
347
+
348
+ /**
349
+ * Takes the cross-process lock for one state file, waiting up to LOCK_RETRIES * LOCK_WAIT_MS (5 s).
350
+ * A lock older than LOCK_STALE_MS is reclaimed: its holder died or stalled, and the rename-based write
351
+ * keeps the file whole.
352
+ * @param {Object} fsImpl
353
+ * @param {string} file
354
+ * @param {LockOptions} [options] see LockOptions; the defaults are the protocol above
94
355
  * @returns {() => void}
95
356
  */
96
- function acquireLock(fsImpl , file ) {
357
+ function acquireLock(fsImpl , file , options = {}) {
97
358
  const lockDir = `${file}.lock`;
359
+ const retries = options.retries ?? LOCK_RETRIES;
98
360
  // The stale rule below is wall-clock, so the wait must be too: on a host where each attempt costs
99
- // real time, counting attempts alone would outlive a live holder's staleness window and then
100
- // break (and later release) its lock.
101
- const deadline = Date.now() + LOCK_RETRIES * LOCK_WAIT_MS;
102
- for (let attempt = 0; attempt < LOCK_RETRIES && (attempt === 0 || Date.now() < deadline); attempt += 1) {
103
- try {
104
- fsImpl.mkdirSync(lockDir);
105
- return () => { try { fsImpl.rmdirSync(lockDir); } catch { /* released is released */ } };
106
- } catch {
107
- try {
108
- if (Date.now() - fsImpl.statSync(lockDir).mtimeMs > LOCK_STALE_MS) {
109
- fsImpl.rmdirSync(lockDir);
110
- continue;
111
- }
112
- } catch { /* the holder released between our mkdir and stat — retry immediately */ }
113
- sleep(LOCK_WAIT_MS);
361
+ // real time, counting attempts alone would outlive a live holder's staleness window.
362
+ const deadline = Date.now() + retries * LOCK_WAIT_MS;
363
+ let step = 'again';
364
+ let me = { name: '' };
365
+ for (let attempt = 0; attempt < retries && (attempt === 0 || Date.now() < deadline); attempt += 1) {
366
+ if (step === 'queued') step = queuedStep(fsImpl, lockDir, me, options);
367
+ else {
368
+ me = { name: (options.entryName || newToken)() }; // a fresh entry name per attempt: an earlier one's name never comes back
369
+ step = attemptLock(fsImpl, lockDir, me, options);
114
370
  }
371
+ if (step === 'held') return ownedRelease(fsImpl, lockDir, me);
372
+ if (step === 'busy' || step === 'queued') sleep(LOCK_WAIT_MS);
115
373
  }
116
- throw new Error(`Could not lock '${file}' after ${(LOCK_RETRIES * LOCK_WAIT_MS) / 1000}s — another writer holds '${file}.lock'. `
117
- + 'If no other session is running, the lock is leftover from a crash younger than its stale timeout; retry shortly.');
374
+ if (step === 'queued' && giveUp(fsImpl, lockDir, me, options) === 'held') return ownedRelease(fsImpl, lockDir, me);
375
+ throw new Error(`Could not lock '${file}' after ${(retries * LOCK_WAIT_MS) / 1000}s — another writer holds '${lockDir}'. `
376
+ + `If no other DoFlow process is running, the lock is left from a crash and is reclaimed once it is ${LOCK_STALE_MS / 1000}s old; retry then.`
377
+ + strayEntries(fsImpl, lockDir));
378
+ }
379
+
380
+ /** Names a non-directory entry in the lock (no DoFlow runtime makes one; it blocks the lock until removed). */
381
+ function strayEntries(fsImpl , lockDir ) {
382
+ let stray = [];
383
+ try { stray = fsImpl.readdirSync(lockDir, { withFileTypes: true }).filter((d ) => !d.isDirectory()).map((d ) => d.name); } catch { return ''; }
384
+ if (stray.length === 0) return '';
385
+ return ` It holds ${stray.map((name) => `'${name}'`).join(', ')}, which no DoFlow runtime makes and which keeps it locked: `
386
+ + `remove ${stray.length > 1 ? 'them' : 'it'} once no DoFlow process is running.`;
118
387
  }
119
388
 
120
389
  /**
121
390
  * Takes the cross-process lock for one state file with a bounded wait.
122
- * Makes 1 try plus `retries` retries (default 3) separated by `waitMs` (default 5).
391
+ * Makes 1 try plus `retries` retries (default 3) separated by `waitMs` (default 5); each try is one
392
+ * tryAcquireLock, which waits at most 40 ms, and only on a process that reached the lock at the same moment.
123
393
  * Throws an Error('store-locked') after one try plus retries.
124
394
  * @param {Object} fsImpl
125
395
  * @param {string} file
@@ -166,7 +436,9 @@ function readTaskState(fsImpl , file ) {
166
436
  try {
167
437
  data = JSON.parse(fsImpl.readFileSync(file, 'utf8'));
168
438
  } catch (error ) {
169
- const failure = new Error(`Failed to load state file '${file}': ${error.message}. The write path is atomic `
439
+ // The parser's own message quotes the first bytes of the file; a state file can hold text the
440
+ // reader should not echo, so only the error's class is named.
441
+ const failure = new Error(`Failed to load state file '${file}': its content is not valid JSON (${error?.name || 'Error'}). The write path is atomic `
170
442
  + '(tmp + rename), so a torn file means outside interference; recover it from version control '
171
443
  + 'or move it aside and re-record the task\'s items.');
172
444
  failure.code = 'store-unreadable';
@@ -181,6 +453,27 @@ function readTaskState(fsImpl , file ) {
181
453
  return data;
182
454
  }
183
455
 
456
+ /** How many times the folder is made again when it vanishes before the lock can be made in it. */
457
+ const FOLDER_RACE_RETRIES = 5;
458
+
459
+ /**
460
+ * Makes the state file's folder and takes the lock in it. The lock is a folder inside the state
461
+ * file's folder and is made without `recursive`, so a sweep that removes an empty namespace folder
462
+ * between the two steps makes the lock fail with ENOENT. The folder is then made again, a bounded
463
+ * number of times; the sweep never removes a folder that holds a lock, so a taken lock keeps it.
464
+ */
465
+ function acquireInFolder(fsImpl , file , acquire ) {
466
+ for (let tries = 0; ; tries += 1) {
467
+ fsImpl.mkdirSync(path.dirname(file), { recursive: true });
468
+ try {
469
+ return acquire(fsImpl, file);
470
+ } catch (error ) {
471
+ if (error && error.code === 'ENOENT' && tries < FOLDER_RACE_RETRIES) continue;
472
+ throw error;
473
+ }
474
+ }
475
+ }
476
+
184
477
  /**
185
478
  * The one durable-update path: lock, re-read, merge, bump revision, atomic write.
186
479
  *
@@ -194,6 +487,9 @@ function readTaskState(fsImpl , file ) {
194
487
  * that must inspect or set aside what is there under the same exclusion as the write
195
488
  * @param {(fsImpl: Object, file: string) => () => void} [options.acquire] optional lock acquirer
196
489
  * defaulting to acquireLock
490
+ * @param {number} [options.lostLockRetries=0] how many times to run the whole locked update again
491
+ * when the lock was reclaimed from this writer before its rename (nothing was written); only for a
492
+ * caller whose `prepare` and `build` have no effect outside the payload they return
197
493
  * @returns {string|null} the file path written, or null when build declines a write
198
494
  */
199
495
  function updateTaskState({
@@ -202,37 +498,51 @@ function updateTaskState({
202
498
  build,
203
499
  prepare,
204
500
  acquire = acquireLock,
501
+ lostLockRetries = 0,
205
502
  }
206
503
 
207
504
 
208
505
 
209
506
 
210
507
 
508
+
211
509
  ) {
212
- fsImpl.mkdirSync(path.dirname(file), { recursive: true });
213
- const release = acquire(fsImpl, file);
214
- try {
215
- if (prepare) prepare();
216
- const disk = readTaskState(fsImpl, file);
217
- const built = build(disk);
218
- if (built === null) return null;
219
- const payload = {
220
- ...built,
221
- version: SCHEMA_VERSION,
222
- revision: (disk && Number.isInteger(disk.revision) ? disk.revision : 0) + 1,
223
- };
224
- const tmp = `${file}.${process.pid}.tmp`;
225
- fsImpl.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
226
- fsImpl.renameSync(tmp, file);
227
- return file;
228
- } finally {
229
- release();
510
+ for (let attempt = 0; ; attempt += 1) {
511
+ const release = acquireInFolder(fsImpl, file, acquire);
512
+ try {
513
+ if (prepare) prepare();
514
+ const disk = readTaskState(fsImpl, file);
515
+ const built = build(disk);
516
+ if (built === null) return null;
517
+ const payload = {
518
+ ...built,
519
+ version: SCHEMA_VERSION,
520
+ revision: (disk && Number.isInteger(disk.revision) ? disk.revision : 0) + 1,
521
+ };
522
+ const tmp = `${file}.${process.pid}.tmp`;
523
+ fsImpl.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
524
+ // A writer paused past LOCK_STALE_MS may have been reclaimed; its read is then stale.
525
+ if (typeof release.held === 'function' && !release.held()) {
526
+ try { fsImpl.rmSync(tmp, { force: true }); } catch { /* stale-temp pruning removes it later */ }
527
+ if (attempt < lostLockRetries) continue;
528
+ const lost = new Error(`Lost the lock on '${file}' to another writer before writing (this writer stalled past `
529
+ + `${LOCK_STALE_MS / 1000}s${lostLockRetries ? `, ${lostLockRetries + 1} times` : ''}). Nothing was written; run the command again.`);
530
+ lost.code = 'lock-lost';
531
+ throw lost;
532
+ }
533
+ fsImpl.renameSync(tmp, file);
534
+ return file;
535
+ } finally {
536
+ release();
537
+ }
230
538
  }
231
539
  }
232
540
 
233
541
  module.exports = {
234
542
  acquireLock,
235
543
  tryAcquireLock,
544
+ LOCK_STALE_MS,
545
+ LOCK_WAIT_MS,
236
546
  boundedAcquire,
237
547
  updateTaskState,
238
548
  readTaskState,