@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,671 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ /**
6
+ * Named machine-wide slots (feature 115, IC-008 to IC-010): at most one `runSlot` holder per name,
7
+ * waiters served in arrival order. State is one file per slot under the user-state root
8
+ * (`~/.doflow/state/slots/<name>.json`), and EVERY change to it, the whole grant decision included,
9
+ * runs inside one `updateTaskState` critical section, so two waiters can never both be granted.
10
+ *
11
+ * A holder or waiter counts as dead only when `alive()` says `false`; `unknown` counts as alive.
12
+ * A holder is alive while the `slot run` process or its recorded child is. Liveness is probed before
13
+ * the lock, from an unlocked read (`probeSlot`), never under it; dead entries are dropped under the
14
+ * lock on every critical section that looks at the queue. Nothing here ever signals a process other
15
+ * than the caller's own child, and `releaseSlot` never signals at all.
16
+ *
17
+ * Library only: it returns exit codes and never calls process.exit; the CLI maps them.
18
+ */
19
+
20
+ const fs = require('node:fs');
21
+ const os = require('node:os');
22
+ const path = require('node:path');
23
+ const { spawn: spawnChild } = require('node:child_process');
24
+ const { randomBytes } = require('node:crypto');
25
+ const { updateTaskState, readTaskState, acquireLock, LOCK_STALE_MS, LOCK_WAIT_MS } = require('./task-state.js');
26
+ const { alive: processAlive, procStart: processStart } = require('./worker/process-tree.js');
27
+ const { loadStorageRegistry } = require('./storage/registry.js');
28
+ const { resolveRoots, resolveStorePath } = require('./storage/roots.js');
29
+ const { REPO_ROOT } = require('../helper/repo-root.js');
30
+
31
+ const SLOT_NAME = /^[a-z0-9][a-z0-9-]{0,63}$/;
32
+ const SLOT_FILE = /^[a-z0-9][a-z0-9-]{0,63}\.json$/;
33
+ const POLL_MS = 1000;
34
+ const LABEL_MAX = 80;
35
+ const BUSY_EXIT = 75;
36
+ const FORWARDED = ['SIGINT', 'SIGTERM', 'SIGHUP'];
37
+ const SLOT_LOCK_BUDGET_MS = 30_000;
38
+ const SLOT_LOST_RETRIES = 2;
39
+
40
+ function fail(code , message ) {
41
+ return Object.assign(new Error(message), { code });
42
+ }
43
+
44
+ function checkName(name ) {
45
+ if (typeof name !== 'string' || !SLOT_NAME.test(name)) {
46
+ throw fail('usage', `Slot name '${name}' is invalid: use 1 to 64 characters of a-z, 0-9 and '-', starting with a letter or digit.`);
47
+ }
48
+ return name;
49
+ }
50
+
51
+ /** The slots directory's path under the user-state root, not created; never falls back to a real home. */
52
+ function slotsPath(env , fsImpl ) {
53
+ const registry = loadStorageRegistry({ repoRoot: REPO_ROOT });
54
+ const roots = resolveRoots({ registry, env });
55
+ if (!roots['user-state']) throw fail('no-home', 'HOME is not set to an absolute path, so the slot state directory cannot be resolved.');
56
+ const dir = resolveStorePath({ store: { root: 'user-state', path: 'slots' }, roots, fsImpl });
57
+ if (!dir) throw fail('slots-unsafe', `Refusing to use '${roots['user-state']}/slots': a DoFlow-owned path component is a link.`);
58
+ return dir;
59
+ }
60
+
61
+ /** The slots directory under the user-state root, created when absent. */
62
+ function slotsDir(env , fsImpl ) {
63
+ const dir = slotsPath(env, fsImpl);
64
+ fsImpl.mkdirSync(dir, { recursive: true });
65
+ return dir;
66
+ }
67
+
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
85
+
86
+ /**
87
+ * How long a process judged alive is taken as alive without asking again. A look at the queue probes
88
+ * every entry, and the default check starts a `ps` per entry: with N waiters that is N `ps` runs per
89
+ * look per waiter. Only `true` is remembered, only for this runSlot call: a death is noticed within the window.
90
+ */
91
+ const ALIVE_CACHE_MS = 2000;
92
+
93
+ function cachedAlive(check , clock = () => performance.now()) {
94
+ const seen = new Map ();
95
+ return (proc ) => {
96
+ const key = `${proc?.pid}\0${proc?.start ?? ''}`;
97
+ // A monotonic clock, and a non-positive or odd age counts as expired: a wall-clock step back must not keep a dead process alive.
98
+ const now = clock();
99
+ const at = seen.get(key);
100
+ const age = at === undefined ? NaN : now - at;
101
+ if (age >= 0 && age < ALIVE_CACHE_MS) return true;
102
+ const state = check(proc);
103
+ if (state === true) seen.set(key, now); else seen.delete(key);
104
+ return state;
105
+ };
106
+ }
107
+
108
+ function resolveDeps(deps = {}) {
109
+ const kill = deps.kill;
110
+ const run = deps.run;
111
+ return {
112
+ fsImpl: deps.fsImpl || fs,
113
+ spawn: deps.spawn || spawnChild,
114
+ alive: deps.alive || cachedAlive((proc ) => processAlive(proc, { kill, run })),
115
+ procStart: deps.procStart || ((pid ) => processStart(pid, run ? { run } : {})),
116
+ now: deps.now || Date.now,
117
+ sleep: deps.sleep || ((ms ) => new Promise ((resolve) => setTimeout(resolve, ms))),
118
+ stderr: deps.stderr || ((line ) => { process.stderr.write(`${line}\n`); }),
119
+ pid: deps.pid ?? process.pid,
120
+ pollMs: deps.pollMs ?? POLL_MS,
121
+ platform: deps.platform ?? process.platform,
122
+ proc: deps.proc || process,
123
+ quietMs: deps.quietMs,
124
+ lockBudgetMs: deps.lockBudgetMs,
125
+ };
126
+ }
127
+
128
+ const iso = (ms ) => new Date(ms).toISOString();
129
+
130
+ /** What is on disk, made well-formed. */
131
+ function shape(disk , name ) {
132
+ return {
133
+ name,
134
+ holder: disk && disk.holder ? { ...disk.holder } : null,
135
+ waiters: Array.isArray(disk?.waiters) ? disk.waiters.map((waiter ) => ({ ...waiter })) : [],
136
+ lastRelease: disk && disk.lastRelease ? disk.lastRelease : null,
137
+ };
138
+ }
139
+
140
+ const LEGACY_OWNER = 'owner.json';
141
+ // An entry is `owner-<pid>-<hex of the owner's start time, or x>-<12 hex>`: the protocol needs a
142
+ // unique name, the slot lock also reads its owner from it.
143
+ const ENTRY = /^owner-(\d+)-([0-9a-f]+|x)-[0-9a-f]{12}$/;
144
+
145
+ /** The text of the 115 slot lock's owner token (`owner.json`, a regular file), or null. */
146
+ function readLegacyToken(fsImpl , lockDir ) {
147
+ const file = path.join(lockDir, LEGACY_OWNER);
148
+ try {
149
+ const stat = fsImpl.lstatSync(file);
150
+ if (!stat.isFile() || stat.isSymbolicLink()) return null;
151
+ return fsImpl.readFileSync(file, 'utf8');
152
+ } catch {
153
+ return null;
154
+ }
155
+ }
156
+
157
+ /** The owner a 115 token names, or null. */
158
+ function legacyOwner(text ) {
159
+ if (text == null) return null;
160
+ try {
161
+ const owner = JSON.parse(text);
162
+ if (!Number.isInteger(owner?.pid) || owner.pid <= 0) return null;
163
+ return { pid: owner.pid, start: owner.start ?? null };
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+
169
+ /** The owner an entry name records, or null for a name this lock does not make. */
170
+ function entryOwner(name ) {
171
+ const match = ENTRY.exec(name);
172
+ if (!match) return null;
173
+ const pid = Number(match[1]);
174
+ if (!Number.isSafeInteger(pid) || pid <= 0) return null;
175
+ return { pid, start: match[2] === 'x' ? null : Buffer.from(match[2], 'hex').toString('utf8') };
176
+ }
177
+
178
+ const refuse = (why ) => Object.assign(new Error(why), { code: 'EBUSY' });
179
+
180
+ /**
181
+ * The slot lock (FU-31dyvs) is the task-state lock (`src/runtime/task-state.js`, its protocol and
182
+ * `release.held()`), with the entry naming its owner and a stale rule that keeps 115's C-4: an entry
183
+ * whose owner is alive is never reclaimed, however old (a slow live critical section is not a crash);
184
+ * one whose owner is dead is reclaimable at once; one whose liveness is unknown, an empty lock and a
185
+ * directory name this lock does not make once the lock is 10 s old, as in task-state. An older
186
+ * runtime's lock (its `owner.json` token) is judged the same way. `reclaimed` records that this acquire
187
+ * judged a lock reclaimable, so it holds only after `quiet` (every reclaim quiets; user ruling, 117
188
+ * decisions: amends 115 C-4's "dead owner reclaimed at once").
189
+ */
190
+ function slotLock(fsImpl , deps ) {
191
+ const start = deps.procStart(deps.pid);
192
+ const startCode = typeof start === 'string' && start ? Buffer.from(start, 'utf8').toString('hex') : 'x';
193
+ const liveness = (owner ) => {
194
+ try { return deps.alive({ pid: owner.pid, start: owner.start }); } catch { return 'unknown'; }
195
+ };
196
+ const lock = { reclaimed: false, current: null, judgedToken: null };
197
+ lock.options = {
198
+ entryName: () => (lock.current = `owner-${deps.pid}-${startCode}-${randomBytes(6).toString('hex')}`),
199
+ stale: ({ lockDir, entries, ageMs } ) => {
200
+ const aged = ageMs > LOCK_STALE_MS;
201
+ const reclaimable = entries.length === 0 ? aged : entries.every((name ) => {
202
+ let owner ;
203
+ if (name === LEGACY_OWNER) {
204
+ lock.judgedToken = readLegacyToken(fsImpl, lockDir);
205
+ owner = legacyOwner(lock.judgedToken);
206
+ } else owner = entryOwner(name);
207
+ if (!owner) return aged;
208
+ const state = liveness(owner);
209
+ return state === false || (state !== true && aged);
210
+ });
211
+ // Any reclaim quiets (FU-31dyvs N-1): an older runtime may have judged the lock, or the one before
212
+ // it, stale, and a current process that died in its own quiet hold leaves a young lock with a dead
213
+ // entry, which only a quiet successor keeps safe from that older runtime's pending move.
214
+ if (reclaimable) lock.reclaimed = true;
215
+ return reclaimable;
216
+ },
217
+ removeEntry: (lockFs , lockDir , name ) => {
218
+ const entry = path.join(lockDir, name);
219
+ const stat = lockFs.lstatSync(entry);
220
+ if (name !== LEGACY_OWNER || stat.isDirectory()) return lockFs.rmdirSync(entry);
221
+ if (!stat.isFile()) throw refuse('not a token');
222
+ // C-1: unlink only the dead token judged, in the lock that holds this process's entry. An older
223
+ // runtime that moved the lock aside and made a new one in between has its own token there.
224
+ if (!lock.current || !lockFs.existsSync(path.join(lockDir, lock.current))) throw refuse('the lock was replaced');
225
+ if (lockFs.readFileSync(entry, 'utf8') !== lock.judgedToken) throw refuse('the token changed');
226
+ return lockFs.unlinkSync(entry);
227
+ },
228
+ };
229
+ return lock;
230
+ }
231
+
232
+ /**
233
+ * After reclaiming a lock (FU-31dyvs C-2, N-1): an older runtime that judged it (or an earlier lock at
234
+ * the path) stale moves it aside by name, at any later moment, so this process waits LOCK_STALE_MS
235
+ * with its entry in the lock, holding nothing, before it uses the lock. A move that lands meanwhile
236
+ * takes the lock with this process's entry (`held()` turns false and the acquire starts over); one
237
+ * that lands later came from a process paused 10 s or more between its judgement and its move. The
238
+ * lock's time is refreshed meanwhile so that no older runtime judges it stale by its age.
239
+ */
240
+ function quiet(fsImpl , lockDir , release , quietMs ) {
241
+ const until = Date.now() + quietMs;
242
+ let touched = -Infinity;
243
+ for (;;) {
244
+ if (!release.held()) return false;
245
+ const now = Date.now();
246
+ if (now >= until || now - touched >= LOCK_STALE_MS / 4) {
247
+ try { fsImpl.utimesSync(lockDir, now / 1000, now / 1000); } catch { /* held() below decides */ }
248
+ touched = now;
249
+ }
250
+ if (now >= until) return release.held();
251
+ blockingSleep(LOCK_WAIT_MS);
252
+ }
253
+ }
254
+
255
+ function blockingSleep(ms ) {
256
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
257
+ }
258
+
259
+ /** The slot lock's give-up error; it names a file in the lock, which keeps it locked (C-4). */
260
+ function lockFailure(fsImpl , file , budgetMs ) {
261
+ const lockDir = `${file}.lock`;
262
+ let stray = [];
263
+ try {
264
+ stray = fsImpl.readdirSync(lockDir, { withFileTypes: true })
265
+ .filter((entry ) => !entry.isDirectory() && entry.name !== LEGACY_OWNER).map((entry ) => entry.name);
266
+ } catch { /* no lock left to name anything in */ }
267
+ const named = stray.length === 0 ? ''
268
+ : ` It holds ${stray.map((name) => `'${name}'`).join(', ')}, which no DoFlow runtime makes and which keeps it locked: `
269
+ + `remove ${stray.length > 1 ? 'them' : 'it'} once no DoFlow process is running.`;
270
+ return new Error(`Could not lock '${file}' after ${budgetMs / 1000}s — another writer holds '${lockDir}'.${named}`);
271
+ }
272
+
273
+ /** Takes the slot lock for `file` within SLOT_LOCK_BUDGET_MS; the release function carries `held()`. */
274
+ function acquireSlotLock(fsImpl , file , deps ) {
275
+ const lockDir = `${file}.lock`;
276
+ const budgetMs = deps.lockBudgetMs ?? SLOT_LOCK_BUDGET_MS;
277
+ const deadline = Date.now() + budgetMs;
278
+ const lock = slotLock(fsImpl, deps);
279
+ for (;;) {
280
+ const left = deadline - Date.now();
281
+ if (left <= 0) throw lockFailure(fsImpl, file, budgetMs);
282
+ lock.reclaimed = false;
283
+ let release ;
284
+ try {
285
+ release = acquireLock(fsImpl, file, { ...lock.options, retries: Math.max(1, Math.ceil(left / LOCK_WAIT_MS)) });
286
+ } catch (error ) {
287
+ if (typeof error?.message !== 'string' || !error.message.startsWith('Could not lock')) throw error;
288
+ throw lockFailure(fsImpl, file, budgetMs);
289
+ }
290
+ if (!lock.reclaimed || quiet(fsImpl, lockDir, release, deps.quietMs ?? LOCK_STALE_MS)) return release;
291
+ release(); // an older runtime moved the lock with our entry in it: nothing of ours is at the path
292
+ }
293
+ }
294
+
295
+ /** Holder liveness for a run: dead only when the runner and its recorded child are both `false`. */
296
+ function holderState(holder , alive ) {
297
+ const self = alive({ pid: holder.pid, start: holder.start ?? null });
298
+ const child = Number.isInteger(holder.childPid) ? alive({ pid: holder.childPid, start: holder.childStart ?? null }) : false;
299
+ if (self === true || child === true) return true;
300
+ if (self === false && child === false) return false;
301
+ return 'unknown';
302
+ }
303
+
304
+ const identity = (pid , start ) => `${pid}\0${start ?? ''}`;
305
+
306
+ /**
307
+ * Probes, BEFORE the slot lock, every process an unlocked read of the slot file names (a read error
308
+ * or a missing file: nothing to probe); under the lock a probe (a `ps`, slow on a loaded host) made
309
+ * every critical section that slow. The death of a (pid, start) identity is permanent, so a `false`
310
+ * found here is still true under the lock; an identity not probed here is `unknown`, which counts as
311
+ * alive, and is judged on the next look. `seen` is that read: a hint for skipping a look, never a decision.
312
+ */
313
+ function probeSlot(fsImpl , file , name , alive ) {
314
+ let seen = null;
315
+ try {
316
+ const disk = readTaskState(fsImpl, file);
317
+ seen = disk ? shape(disk, name) : null;
318
+ } catch { /* nothing to probe; the critical section reads the file itself */ }
319
+ const verdicts = new Map ();
320
+ const ask = (pid , start ) => {
321
+ const key = identity(pid, start);
322
+ if (!verdicts.has(key)) verdicts.set(key, alive({ pid, start: start ?? null }));
323
+ };
324
+ if (seen) {
325
+ for (const waiter of seen.waiters) ask(waiter.pid, waiter.start);
326
+ if (seen.holder) {
327
+ ask(seen.holder.pid, seen.holder.start);
328
+ if (Number.isInteger(seen.holder.childPid)) ask(seen.holder.childPid, seen.holder.childStart);
329
+ }
330
+ }
331
+ return {
332
+ seen,
333
+ judged: (proc ) => verdicts.get(identity(proc.pid, proc.start)) ?? 'unknown',
334
+ probed: (proc ) => verdicts.has(identity(proc.pid, proc.start)),
335
+ };
336
+ }
337
+
338
+ /**
339
+ * True when the unlocked read shows `self` queued and its grant cannot change on this look: a holder
340
+ * not judged dead, or no holder and a waiter ahead of it, and no entry judged dead (a sweep is due).
341
+ * The grant is decided under the lock only; a stale read costs one poll.
342
+ */
343
+ function queuedBehindLive(seen , judged , self ) {
344
+ if (!seen) return false;
345
+ const at = seen.waiters.findIndex((waiter ) => waiter.pid === self.pid && waiter.since === self.since);
346
+ if (at < 0 || seen.waiters.some((waiter ) => judged(waiter) === false)) return false;
347
+ if (seen.holder) return holderState(seen.holder, judged) !== false;
348
+ return at > 0;
349
+ }
350
+
351
+ /** Drops dead waiters and a dead holder. Called only inside the lock, with `alive` from `probeSlot`. */
352
+ function sweep(state , alive ) {
353
+ state.waiters = state.waiters.filter((waiter ) => alive({ pid: waiter.pid, start: waiter.start ?? null }) !== false);
354
+ if (state.holder && holderState(state.holder, alive) === false) state.holder = null;
355
+ return state;
356
+ }
357
+
358
+ /**
359
+ * One critical section on a slot file. `change(state)` receives what is on disk (not yet swept) and
360
+ * returns the state to store, or `false` to leave the file untouched. A state with no holder and
361
+ * no waiters deletes the file inside the lock, so a concurrent waiter either already appended or
362
+ * starts from nothing.
363
+ */
364
+ function locked(file , name , deps , change ) {
365
+ // A lock reclaimed from this process before its write or delete (`lock-lost`: nothing was written)
366
+ // runs the change again on the fresh file; every `change` here only sets what it returns or reports.
367
+ for (let attempt = 0; ; attempt += 1) {
368
+ let release = null;
369
+ try {
370
+ updateTaskState({
371
+ fsImpl: deps.fsImpl,
372
+ file,
373
+ acquire: (fsImpl , target ) => (release = acquireSlotLock(fsImpl, target, deps)),
374
+ build: (disk ) => {
375
+ const next = change(shape(disk, name));
376
+ if (next === false) return null;
377
+ if (!next.holder && next.waiters.length === 0) {
378
+ // The delete is a write too: check the lock is still ours first, as updateTaskState does.
379
+ if (!release.held()) throw Object.assign(new Error(`Lost the lock on '${file}' to another writer before deleting it. Nothing was changed; run the command again.`), { code: 'lock-lost' });
380
+ try { deps.fsImpl.unlinkSync(file); } catch (error ) { if (error.code !== 'ENOENT') throw error; }
381
+ return null;
382
+ }
383
+ return next;
384
+ },
385
+ });
386
+ return;
387
+ } catch (error ) {
388
+ if (error?.code !== 'lock-lost' || attempt >= SLOT_LOST_RETRIES) throw error;
389
+ }
390
+ }
391
+ }
392
+
393
+ /** One look at the queue for `self`: append, and grant when first in line with the holder gone. */
394
+ function step(state , self , noWait , deps , nowMs ) {
395
+ let mine = state.waiters.find((waiter ) => waiter.pid === self.pid && waiter.since === self.since);
396
+ if (!mine) {
397
+ const ahead = state.holder || state.waiters[0];
398
+ if (noWait && ahead) return { kind: 'busy', ahead };
399
+ mine = { ...self };
400
+ state.waiters.push(mine);
401
+ }
402
+ if (!state.holder && state.waiters[0] === mine) {
403
+ state.waiters.shift();
404
+ state.holder = { pid: self.pid, start: self.start, childPid: null, childStart: null, label: self.label, since: iso(nowMs), cwd: self.cwd };
405
+ return { kind: 'granted', ahead: null };
406
+ }
407
+ return { kind: 'wait', ahead: state.holder || state.waiters.find((waiter ) => waiter !== mine) || null };
408
+ }
409
+
410
+ function describeAhead(ahead ) {
411
+ return ahead ? `${ahead.pid} (${ahead.label})` : 'another waiter';
412
+ }
413
+
414
+ /**
415
+ * Waits for the slot, runs `command` as its holder, and resolves with the exit code: the child's,
416
+ * 128 + signal when it died by one, 75 when refused or timed out, 127 when it could not start.
417
+ */
418
+ async function runSlot({
419
+ name, command, label, timeoutSeconds, noWait = false, cwd = process.cwd(), env = process.env, deps: injected,
420
+ } = {}) {
421
+ checkName(name);
422
+ if (!Array.isArray(command) || command.length === 0 || typeof command[0] !== 'string' || !command[0]) {
423
+ throw fail('usage', 'slot run needs a command after --.');
424
+ }
425
+ const deps = resolveDeps(injected);
426
+ const file = path.join(slotsDir(env, deps.fsImpl), `${name}.json`);
427
+ const startedAt = deps.now();
428
+ const self = {
429
+ pid: deps.pid,
430
+ start: deps.procStart(deps.pid),
431
+ label: (label || `${path.basename(command[0])}@${path.basename(cwd)}`).slice(0, LABEL_MAX),
432
+ since: iso(startedAt),
433
+ cwd,
434
+ };
435
+
436
+ let received = null;
437
+ let child = null;
438
+ const forward = (signal ) => () => {
439
+ received = received || signal;
440
+ if (child) { try { child.kill(signal); } catch { /* the child is already gone */ } }
441
+ };
442
+ const handlers = FORWARDED.map((signal) => [signal, forward(signal)] );
443
+ for (const [signal, handler] of handlers) deps.proc.on(signal, handler);
444
+
445
+ let holding = false;
446
+ const release = () => {
447
+ holding = false;
448
+ const { judged } = probeSlot(deps.fsImpl, file, name, deps.alive);
449
+ locked(file, name, deps, (state) => {
450
+ sweep(state, judged);
451
+ if (state.holder && state.holder.pid === self.pid) state.holder = null;
452
+ state.waiters = state.waiters.filter((waiter ) => !(waiter.pid === self.pid && waiter.since === self.since));
453
+ return state;
454
+ });
455
+ };
456
+ const signalExit = (signal ) => 128 + ((signal && (os.constants.signals )[signal]) || 0);
457
+
458
+ try {
459
+ let outcome ;
460
+ for (;;) {
461
+ const { seen, judged } = probeSlot(deps.fsImpl, file, name, deps.alive);
462
+ // A waiter whose grant cannot change leaves the lock to the holder and the waiter that can.
463
+ const due = timeoutSeconds && deps.now() - startedAt >= timeoutSeconds * 1000;
464
+ if (!received && !due && queuedBehindLive(seen, judged, self)) { await deps.sleep(deps.pollMs); continue; }
465
+ locked(file, name, deps, (state) => {
466
+ const clean = sweep(state, judged);
467
+ const waited = deps.now() - startedAt;
468
+ if (timeoutSeconds && waited >= timeoutSeconds * 1000) {
469
+ outcome = {
470
+ kind: 'timeout',
471
+ ahead: clean.holder || clean.waiters.find((waiter ) => !(waiter.pid === self.pid && waiter.since === self.since)) || null,
472
+ waited,
473
+ };
474
+ return clean;
475
+ }
476
+ outcome = step(clean, self, noWait, deps, deps.now());
477
+ return clean;
478
+ });
479
+ if (outcome.kind === 'granted') { holding = true; break; }
480
+ if (outcome.kind === 'busy') {
481
+ const since = outcome.ahead.since ? ` since ${outcome.ahead.since}` : '';
482
+ deps.stderr(`doflow slot: ${name} busy; held by ${describeAhead(outcome.ahead)}${since}`);
483
+ return BUSY_EXIT;
484
+ }
485
+ if (outcome.kind === 'timeout') {
486
+ release();
487
+ deps.stderr(`doflow slot: ${name} timed out after ${Math.round(outcome.waited / 1000)} s; held by ${describeAhead(outcome.ahead)}`);
488
+ return BUSY_EXIT;
489
+ }
490
+ if (received) { release(); return signalExit(received); }
491
+ await deps.sleep(deps.pollMs);
492
+ }
493
+
494
+ if (received) return signalExit(received);
495
+ deps.stderr(`doflow slot: ${name} granted after ${Math.round((deps.now() - startedAt) / 1000)} s (${self.label})`);
496
+
497
+ const gated = deps.platform !== 'win32';
498
+ const spawnCommand = gated ? 'sh' : command[0];
499
+ const spawnArgs = gated
500
+ ? ['-c', 'read _ <&3 || exit 125; exec 3<&-; exec "$@"', 'doflow-slot', ...command]
501
+ : command.slice(1);
502
+ child = deps.spawn(spawnCommand, spawnArgs, {
503
+ stdio: gated ? ['inherit', 'inherit', 'inherit', 'pipe'] : 'inherit',
504
+ env: { ...env, DOFLOW_SLOT: name },
505
+ cwd,
506
+ });
507
+ const finished = new Promise ((resolve) => {
508
+ child.on('error', (error ) => resolve({ code: null, signal: null, error }));
509
+ child.on('exit', (code , signal ) => resolve({ code, signal, error: null }));
510
+ });
511
+ const exited = new Promise ((resolve) => {
512
+ child.on('exit', (code , signal ) => resolve({ code, signal }));
513
+ });
514
+ const startGate = gated ? child.stdio?.[3] : null;
515
+ try {
516
+ let registered = false;
517
+ if (Number.isInteger(child.pid)) {
518
+ const childStart = deps.procStart(child.pid);
519
+ locked(file, name, deps, (state) => {
520
+ registered = false; // a lock-lost re-run decides again on the fresh file
521
+ if (state.holder && state.holder.pid === self.pid) {
522
+ state.holder = { ...state.holder, childPid: child.pid, childStart };
523
+ registered = true;
524
+ }
525
+ return state;
526
+ });
527
+ if (!registered) throw new Error(`Could not register child ${child.pid} for slot '${name}': this process is no longer the holder.`);
528
+ }
529
+ if (startGate) {
530
+ if (!registered) throw new Error(`Could not register the child for slot '${name}': it has no process id.`);
531
+ if (typeof startGate.end === 'function') startGate.end('go\n');
532
+ }
533
+ } catch (registrationError) {
534
+ if (startGate) {
535
+ try {
536
+ if (typeof startGate.end === 'function') startGate.end();
537
+ else startGate.destroy?.();
538
+ } catch { /* the child still observes a closed pipe when its runner exits */ }
539
+ }
540
+ if (Number.isInteger(child.pid)) await exited;
541
+ else await finished;
542
+ throw registrationError;
543
+ }
544
+ if (received) { try { child.kill(received); } catch { /* already gone */ } }
545
+ const result = await finished;
546
+ if (result.error) {
547
+ deps.stderr(`doflow slot: ${name}: cannot run ${command[0]}: ${result.error.message}`);
548
+ return 127;
549
+ }
550
+ return result.code != null ? result.code : signalExit(result.signal);
551
+ } finally {
552
+ for (const [signal, handler] of handlers) deps.proc.removeListener(signal, handler);
553
+ if (holding) release();
554
+ }
555
+ }
556
+
557
+ /** Reads every slot file under the lock (dead entries dropped) and returns them sorted by name. */
558
+ async function listSlots({ env = process.env, json = false, deps: injected } = {}) {
559
+ const deps = resolveDeps(injected);
560
+ const dir = slotsDir(env, deps.fsImpl);
561
+ const names = deps.fsImpl.readdirSync(dir).filter((entry ) => SLOT_FILE.test(entry)).map((entry ) => entry.slice(0, -5)).sort();
562
+ const slots = [];
563
+ for (const name of names) {
564
+ const file = path.join(dir, `${name}.json`);
565
+ let view = null;
566
+ const { judged } = probeSlot(deps.fsImpl, file, name, deps.alive);
567
+ locked(file, name, deps, (state) => {
568
+ const before = JSON.stringify([state.holder, state.waiters]);
569
+ sweep(state, judged);
570
+ view = state;
571
+ // Only a slot with dead entries is rewritten; an unchanged one is left alone.
572
+ return JSON.stringify([state.holder, state.waiters]) === before ? false : state;
573
+ });
574
+ if (view && (view.holder || view.waiters.length > 0)) slots.push({ name, holder: view.holder, waiters: view.waiters });
575
+ }
576
+ const output = json
577
+ ? JSON.stringify({ ok: true, slots })
578
+ : slots.length === 0 ? 'no slots held'
579
+ : slots.map((slot) => {
580
+ const head = slot.holder ? `held by ${slot.holder.pid} (${slot.holder.label}) since ${slot.holder.since}` : 'free';
581
+ return `${slot.name} ${head}; ${slot.waiters.length} waiting`;
582
+ }).join('\n');
583
+ return { ok: true, slots, output };
584
+ }
585
+
586
+ /**
587
+ * 093 IC-018 b: every slot file read as it is on disk, with no lock, no sweep and no mkdir; a missing
588
+ * directory is an empty list and a file that cannot be read or parsed is counted in `unreadable`.
589
+ * A holder's `alive` is `holderState` (computed, never written); its `worktreeKey` is the `cwdHash`
590
+ * of the checkout its `cwd` lies in when that is one of the project's checkouts seen from `cwd`,
591
+ * else null. `waiters` counts the waiters not known to be dead. `pid`, `start`, `cwd` and waiter
592
+ * details are never returned.
593
+ */
594
+ async function peekSlots({ env = process.env, cwd = process.cwd(), exec, deps: injected } = {}) {
595
+ const deps = resolveDeps(injected);
596
+ const dir = slotsPath(env, deps.fsImpl);
597
+ let names ;
598
+ try {
599
+ names = deps.fsImpl.readdirSync(dir).filter((entry ) => SLOT_FILE.test(entry)).map((entry ) => entry.slice(0, -5)).sort();
600
+ } catch (error ) {
601
+ if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') throw error;
602
+ names = [];
603
+ }
604
+ let entries = null;
605
+ const checkoutKey = (holderCwd ) => {
606
+ const { listCheckouts, checkoutContaining } = require('./checkouts.js');
607
+ if (entries === null) {
608
+ const listed = listCheckouts({ cwd, ...(exec ? { exec } : {}) });
609
+ entries = listed.ok ? listed.entries : [];
610
+ }
611
+ const at = checkoutContaining(entries , holderCwd, deps.fsImpl);
612
+ if (!at || at.sandbox) return null;
613
+ return require('./context/identity.js').cwdHash(at.path);
614
+ };
615
+ const slots = [];
616
+ let unreadable = 0;
617
+ for (const name of names) {
618
+ let state ;
619
+ try {
620
+ const file = path.join(dir, `${name}.json`);
621
+ if (!deps.fsImpl.lstatSync(file).isFile()) { unreadable += 1; continue; }
622
+ const disk = JSON.parse(deps.fsImpl.readFileSync(file, 'utf8'));
623
+ if (disk === null || typeof disk !== 'object' || Array.isArray(disk)) { unreadable += 1; continue; }
624
+ state = shape(disk, name);
625
+ } catch {
626
+ unreadable += 1;
627
+ continue;
628
+ }
629
+ const waiters = state.waiters.filter((waiter ) => deps.alive({ pid: waiter.pid, start: waiter.start ?? null }) !== false).length;
630
+ if (!state.holder && waiters === 0) continue;
631
+ const holder = state.holder ? {
632
+ label: typeof state.holder.label === 'string' ? state.holder.label : null,
633
+ since: typeof state.holder.since === 'string' ? state.holder.since : null,
634
+ alive: holderState(state.holder, deps.alive),
635
+ worktreeKey: checkoutKey(state.holder.cwd),
636
+ } : null;
637
+ slots.push({ name, holder, waiters });
638
+ }
639
+ return { v: 1, slots, unreadable };
640
+ }
641
+
642
+ /**
643
+ * Removes the holder of `name` unless it is alive (`true`); never signals anything. Resolves with
644
+ * `{ok, disposition, holder?}` as IC-008 spells it.
645
+ */
646
+ async function releaseSlot({ name, by, reason, env = process.env, deps: injected } = {}) {
647
+ checkName(name);
648
+ if (typeof by !== 'string' || !by.trim()) throw fail('usage', 'slot release needs --by.');
649
+ if (typeof reason !== 'string' || !reason.trim()) throw fail('usage', 'slot release needs --reason.');
650
+ const deps = resolveDeps(injected);
651
+ const file = path.join(slotsDir(env, deps.fsImpl), `${name}.json`);
652
+ let result = { ok: true, disposition: 'free' };
653
+ const { judged, probed } = probeSlot(deps.fsImpl, file, name, deps.alive);
654
+ locked(file, name, deps, (state) => {
655
+ state.waiters = state.waiters.filter((waiter ) => judged(waiter) !== false);
656
+ const holder = state.holder;
657
+ if (!holder) return state;
658
+ const child = Number.isInteger(holder.childPid) ? { pid: holder.childPid, start: holder.childStart } : null;
659
+ // A holder or child that appeared after the probe was not judged: it is left in place, as a live one is.
660
+ const live = !probed(holder) || (child !== null && !probed(child))
661
+ || judged(holder) === true || (child !== null && judged(child) === true);
662
+ if (live) { result = { ok: false, disposition: 'holder-alive', holder }; return false; }
663
+ state.holder = null;
664
+ state.lastRelease = { by, reason, at: iso(deps.now()), holder };
665
+ result = { ok: true, disposition: 'released', holder };
666
+ return state;
667
+ });
668
+ return result;
669
+ }
670
+
671
+ module.exports = { runSlot, listSlots, peekSlots, releaseSlot, acquireSlotLock, cachedAlive, ALIVE_CACHE_MS, SLOT_NAME, POLL_MS };
@@ -283,6 +283,7 @@ function sourceVersionFor(root , locator , state )
283
283
  }
284
284
 
285
285
  module.exports = {
286
+ findWorkTreeTop,
286
287
  measureTreeState,
287
288
  sourceVersionFor,
288
289
  clearTreeStateCache,