@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,1063 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Roadmap seed: validator, judgeDraft and handleSeedCommand (feature 113, IC-001..IC-008).
5
+ * Pure judgement of drafts against store rows; person-only refusals; lease-held writes.
6
+ */
7
+
8
+ const fs = require('node:fs');
9
+ const path = require('node:path');
10
+ const os = require('node:os');
11
+ const { sha256Digest, gatherGitFacts } = require('./facts.js');
12
+ const {
13
+ seedFindings,
14
+ safeText,
15
+ validWriteSet,
16
+ SEED_RULES,
17
+ deriveStatus,
18
+ render,
19
+ renderRoadmapFile,
20
+ pathsOverlap,
21
+ writeSetsOverlap,
22
+ } = require('./rows.js');
23
+ const {
24
+ acquireLease,
25
+ releaseLease,
26
+ resolveMainRoot,
27
+ noPsAlive,
28
+ } = require('./loop.js');
29
+ const { readSeedSwitch } = require('../settings/autopilot.js');
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+
54
+
55
+
56
+
57
+
58
+
59
+
60
+
61
+
62
+
63
+
64
+ function judgeDraft({
65
+ draftRaw,
66
+ seedRaw,
67
+ gitFacts = {},
68
+ op = 'check',
69
+ classes,
70
+ } ) {
71
+ let draftDoc ;
72
+ if (typeof draftRaw === 'string') {
73
+ try {
74
+ draftDoc = JSON.parse(draftRaw);
75
+ } catch (err ) {
76
+ return {
77
+ schema: 1,
78
+ action: 'seed',
79
+ op,
80
+ ok: false,
81
+ rows: [],
82
+ findings: [
83
+ {
84
+ rule: 'seed-invalid-json',
85
+ message: `Draft is not valid JSON: ${safeText(err.message)}`,
86
+ },
87
+ ],
88
+ overlaps: [],
89
+ alone: [],
90
+ };
91
+ }
92
+ } else {
93
+ draftDoc = draftRaw;
94
+ }
95
+
96
+ let seedDoc ;
97
+ if (typeof seedRaw === 'string') {
98
+ try {
99
+ seedDoc = JSON.parse(seedRaw);
100
+ } catch {
101
+ const storeFindings = seedFindings(seedRaw, { classes });
102
+ return {
103
+ schema: 1,
104
+ action: 'seed',
105
+ op,
106
+ ok: false,
107
+ rows: [],
108
+ findings: storeFindings,
109
+ overlaps: [],
110
+ alone: [],
111
+ };
112
+ }
113
+ } else {
114
+ seedDoc = seedRaw;
115
+ }
116
+
117
+ if (!seedDoc || typeof seedDoc !== 'object' || Array.isArray(seedDoc)) {
118
+ const storeFindings = seedFindings(seedDoc, { classes });
119
+ return {
120
+ schema: 1,
121
+ action: 'seed',
122
+ op,
123
+ ok: false,
124
+ rows: [],
125
+ findings: storeFindings,
126
+ overlaps: [],
127
+ alone: [],
128
+ };
129
+ }
130
+
131
+ // 2. Draft findings (draft mode checks shape only: keys, field shapes, slug/id, duplicate id, class)
132
+ const draftFindings = seedFindings(draftDoc, { draft: true, classes });
133
+ const storeDocumentFindings = seedFindings(seedDoc, { classes })
134
+ .filter((finding ) => finding.rowId === undefined && finding.rowIds === undefined);
135
+ const findingKey = (finding ) => `${finding.rule}|${finding.rowId ?? ''}|${(finding.rowIds || []).join(',')}|${finding.message}`;
136
+ const mergeUnique = (...lists ) => {
137
+ const seen = new Set ();
138
+ return lists.flat().filter((finding) => {
139
+ const key = findingKey(finding);
140
+ if (seen.has(key)) return false;
141
+ seen.add(key);
142
+ return true;
143
+ });
144
+ };
145
+
146
+ if (!Array.isArray(seedDoc.rows)) {
147
+ const findings = mergeUnique(draftFindings, storeDocumentFindings)
148
+ .sort((a , b ) => SEED_RULES.indexOf(a.rule) - SEED_RULES.indexOf(b.rule));
149
+ return {
150
+ schema: 1,
151
+ action: 'seed',
152
+ op,
153
+ ok: false,
154
+ rows: [],
155
+ findings: findings.map((finding ) => ({ rule: finding.rule, message: finding.message })),
156
+ overlaps: [],
157
+ alone: [],
158
+ };
159
+ }
160
+
161
+ // 3. Candidate document = store doc with draft amends replaced in place and draft adds appended
162
+ const storeRows = seedDoc.rows;
163
+ const candidateRows = [...storeRows];
164
+ const storeIndexMap = new Map ();
165
+ for (let i = 0; i < storeRows.length; i++) {
166
+ const r = storeRows[i];
167
+ if (r && typeof r === 'object' && typeof r.id === 'string') {
168
+ storeIndexMap.set(r.id, i);
169
+ }
170
+ }
171
+
172
+ const draftRows = (draftDoc && typeof draftDoc === 'object' && Array.isArray(draftDoc.rows))
173
+ ? draftDoc.rows
174
+ : [];
175
+
176
+ for (const dRow of draftRows) {
177
+ if (dRow && typeof dRow === 'object' && typeof dRow.id === 'string') {
178
+ if (storeIndexMap.has(dRow.id)) {
179
+ candidateRows[storeIndexMap.get(dRow.id) ] = dRow;
180
+ } else {
181
+ candidateRows.push(dRow);
182
+ }
183
+ }
184
+ }
185
+
186
+ const candidate = {
187
+ ...seedDoc,
188
+ rows: candidateRows,
189
+ };
190
+ const candidateFindings = seedFindings(candidate, { classes });
191
+
192
+ // Merge draft and candidate findings, dropping a candidate finding whose key equals one already present
193
+ const mergedFindings = mergeUnique(draftFindings, storeDocumentFindings, candidateFindings);
194
+
195
+ // 4. Per draft row (object with a string id), in draft order:
196
+ // change = amend when its id is a store row id, else add
197
+ // status for an amend row = status of STORE row from deriveStatus(storeRows, gitFacts).rowStatus[id]
198
+ const statusSafeRow = (r ) => r && typeof r === 'object' && !Array.isArray(r) &&
199
+ typeof r.id === 'string' && typeof r.slug === 'string' &&
200
+ (r.mergeCommit === undefined || typeof r.mergeCommit === 'string') &&
201
+ (r.hold === undefined || typeof r.hold === 'string');
202
+ const storeValidRows = storeRows.filter(statusSafeRow);
203
+ const { rowStatus: storeRowStatus } = deriveStatus(storeValidRows, gitFacts);
204
+
205
+ const resultRows = [];
206
+ const opFindings = [];
207
+
208
+ for (const dRow of draftRows) {
209
+ if (!dRow || typeof dRow !== 'object' || typeof dRow.id !== 'string') {
210
+ continue;
211
+ }
212
+ const id = dRow.id;
213
+ const isAmend = storeIndexMap.has(id);
214
+ const change = isAmend ? 'amend' : 'add';
215
+
216
+ if (isAmend) {
217
+ const status = storeRowStatus[id] || 'not-started';
218
+ resultRows.push({ id, change, status });
219
+
220
+ if (op === 'apply') {
221
+ opFindings.push({
222
+ rule: 'id-exists',
223
+ rowId: id,
224
+ message: `Row '${safeText(id)}' already exists in store; use --action amend to update`,
225
+ });
226
+ } else if (op === 'check' || op === 'amend') {
227
+ if (status === 'merged' || status === 'in-flight') {
228
+ opFindings.push({
229
+ rule: 'row-started',
230
+ rowId: id,
231
+ message: `Row '${safeText(id)}' has status '${safeText(status)}' and cannot be amended`,
232
+ });
233
+ }
234
+ }
235
+ } else {
236
+ resultRows.push({ id, change });
237
+
238
+ if (op === 'amend') {
239
+ opFindings.push({
240
+ rule: 'row-not-found',
241
+ rowId: id,
242
+ message: `Row '${safeText(id)}' not found in store; use --action apply to add`,
243
+ });
244
+ }
245
+ }
246
+ }
247
+
248
+ // 5. Sort ALL findings:
249
+ // document-level first; then by candidate row position of rowId (or first rowIds);
250
+ // then by SEED_RULES index; stable.
251
+ const candidateIdPos = new Map ();
252
+ for (let i = 0; i < candidateRows.length; i++) {
253
+ const r = candidateRows[i];
254
+ if (r && typeof r === 'object' && typeof r.id === 'string') {
255
+ if (!candidateIdPos.has(r.id)) {
256
+ candidateIdPos.set(r.id, i);
257
+ }
258
+ }
259
+ }
260
+
261
+ const allFindings = [...mergedFindings, ...opFindings].map((f, seq) => {
262
+ const targetId = f.rowId ?? (Array.isArray(f.rowIds) && f.rowIds.length > 0 ? f.rowIds[0] : undefined);
263
+ const isDocLevel = targetId === undefined;
264
+ const pos = isDocLevel ? -1 : (candidateIdPos.has(targetId) ? candidateIdPos.get(targetId) : Infinity);
265
+ const ruleIdx = (SEED_RULES ).indexOf(f.rule);
266
+ return { f, isDocLevel, pos, ruleIdx: ruleIdx >= 0 ? ruleIdx : Infinity, seq };
267
+ });
268
+
269
+ allFindings.sort((a, b) => {
270
+ if (a.isDocLevel !== b.isDocLevel) return a.isDocLevel ? -1 : 1;
271
+ if (a.pos !== b.pos) return a.pos - b.pos;
272
+ if (a.ruleIdx !== b.ruleIdx) return a.ruleIdx - b.ruleIdx;
273
+ return a.seq - b.seq;
274
+ });
275
+
276
+ const finalFindings = allFindings.map(({ f }) => {
277
+ const out = { rule: f.rule };
278
+ if (f.rowId !== undefined) out.rowId = f.rowId;
279
+ if (f.rowIds !== undefined) out.rowIds = f.rowIds;
280
+ out.message = f.message;
281
+ return out;
282
+ });
283
+
284
+ // 7. Overlaps: statuses = deriveStatus(candidateValidRows, gitFacts)
285
+ // Pairs (i<j) of rows not merged, with at least one in draft id set, whose writeSets overlap
286
+ const candidateValidRows = candidateRows.filter(statusSafeRow);
287
+ const { rowStatus: candRowStatus } = deriveStatus(candidateValidRows, gitFacts);
288
+
289
+ const draftIdSet = new Set ();
290
+ for (const dRow of draftRows) {
291
+ if (dRow && typeof dRow === 'object' && typeof dRow.id === 'string') {
292
+ draftIdSet.add(dRow.id);
293
+ }
294
+ }
295
+
296
+ const overlaps = [];
297
+ for (let i = 0; i < candidateValidRows.length; i++) {
298
+ const rI = candidateValidRows[i];
299
+ if (candRowStatus[rI.id] === 'merged') continue;
300
+ for (let j = i + 1; j < candidateValidRows.length; j++) {
301
+ const rJ = candidateValidRows[j];
302
+ if (candRowStatus[rJ.id] === 'merged') continue;
303
+ if (!draftIdSet.has(rI.id) && !draftIdSet.has(rJ.id)) continue;
304
+ const writeSetI = validWriteSet(rI.writeSet);
305
+ const writeSetJ = validWriteSet(rJ.writeSet);
306
+ if (!writeSetI || !writeSetJ || !writeSetsOverlap(writeSetI, writeSetJ)) continue;
307
+
308
+ const paths = [];
309
+ for (const pI of writeSetI) {
310
+ for (const pJ of writeSetJ) {
311
+ if (pathsOverlap(pI, pJ)) {
312
+ paths.push([pI, pJ]);
313
+ }
314
+ }
315
+ }
316
+ paths.sort(([aI, aJ], [bI, bJ]) => {
317
+ if (aI < bI) return -1;
318
+ if (aI > bI) return 1;
319
+ if (aJ < bJ) return -1;
320
+ if (aJ > bJ) return 1;
321
+ return 0;
322
+ });
323
+ overlaps.push({
324
+ rows: [rI.id, rJ.id],
325
+ paths,
326
+ });
327
+ }
328
+ }
329
+
330
+ // 8. Alone: draft rows not merged that declare no writeSet or empty one
331
+ const alone = [];
332
+ const seenAlone = new Set ();
333
+ for (const dRow of draftRows) {
334
+ if (dRow && typeof dRow === 'object' && typeof dRow.id === 'string') {
335
+ const id = dRow.id;
336
+ if (seenAlone.has(id)) continue;
337
+ if (candRowStatus[id] !== 'merged') {
338
+ if (validWriteSet(dRow.writeSet) === null) {
339
+ seenAlone.add(id);
340
+ alone.push(id);
341
+ }
342
+ }
343
+ }
344
+ }
345
+
346
+ return {
347
+ schema: 1,
348
+ action: 'seed',
349
+ op,
350
+ ok: finalFindings.length === 0,
351
+ rows: resultRows,
352
+ findings: finalFindings,
353
+ overlaps,
354
+ alone,
355
+ };
356
+ }
357
+
358
+ function printTextResult(
359
+ result ,
360
+ out ,
361
+ err ,
362
+ opts
363
+ ) {
364
+ const op = safeText(result.op);
365
+ let statusPart ;
366
+ if (result.reason) {
367
+ statusPart = `refused: ${safeText(result.reason)}`;
368
+ } else if (result.findings.length === 0) {
369
+ statusPart = 'ok';
370
+ } else {
371
+ statusPart = `${result.findings.length} findings`;
372
+ }
373
+
374
+ let addCount = 0;
375
+ let amendCount = 0;
376
+ for (const r of result.rows) {
377
+ if (r.change === 'add') addCount += 1;
378
+ if (r.change === 'amend') amendCount += 1;
379
+ }
380
+ const overlapCount = result.overlaps.length;
381
+ const aloneCount = result.alone.length;
382
+
383
+ let line1 = `roadmap seed ${op}: ${statusPart}, ${addCount} add, ${amendCount} amend, ${overlapCount} overlaps, ${aloneCount} alone`;
384
+ if ((op === 'apply' || op === 'amend') && result.ok) {
385
+ line1 += `, ${result.writes} files written`;
386
+ if (Array.isArray(result.proposed) && result.proposed.length > 0) {
387
+ line1 += `, ${result.proposed.length} proposed (no person verified, or autopilot is not verifiably off; a person runs in their terminal: doflow roadmap promote ${result.proposed.map(safeText).join(' ')})`;
388
+ }
389
+ }
390
+ if (result.seed && result.seed.sha256 === null) {
391
+ line1 += ', store absent';
392
+ }
393
+
394
+ const detailLines = [];
395
+
396
+ // Findings
397
+ for (const f of result.findings) {
398
+ let idPart = '';
399
+ if (Array.isArray(f.rowIds) && f.rowIds.length > 0) {
400
+ idPart = f.rowIds.map(safeText).join(' ');
401
+ } else if (f.rowId) {
402
+ idPart = safeText(f.rowId);
403
+ }
404
+ const rule = safeText(f.rule);
405
+ // seedFindings escapes each raw value as it builds this message; this guards any other
406
+ // finding producer while preserving the full, untruncated prose of valid messages.
407
+ const message = typeof f.message === 'string'
408
+ ? f.message.replace(/\r/g, '\\r').replace(/\n/g, '\\n').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029')
409
+ : safeText(f.message);
410
+ if (idPart) {
411
+ detailLines.push(` [${rule}] ${idPart}: ${message}`);
412
+ } else {
413
+ detailLines.push(` [${rule}] ${message}`);
414
+ }
415
+ }
416
+
417
+ // Overlaps
418
+ for (const o of result.overlaps) {
419
+ const pair = o.paths && o.paths.length > 0 ? o.paths[0] : null;
420
+ let pathDesc = '';
421
+ if (pair) {
422
+ const [pA, pB] = pair;
423
+ pathDesc = pA === pB ? safeText(pA) : `${safeText(pA)} ~ ${safeText(pB)}`;
424
+ }
425
+ detailLines.push(` overlap ${safeText(o.rows[0])} ${safeText(o.rows[1])}: ${pathDesc}`);
426
+ }
427
+
428
+ // Refusal single reason line
429
+ if (result.reason && opts?.refusalDetail) {
430
+ detailLines.push(` ${opts.refusalDetail}`);
431
+ }
432
+
433
+ const lines = [line1];
434
+ let maxDetails = 10;
435
+ if (typeof result.land === 'string') {
436
+ lines.push(` land (person only): ${result.land}`);
437
+ maxDetails = 9;
438
+ }
439
+ if (detailLines.length <= maxDetails) {
440
+ lines.push(...detailLines);
441
+ } else {
442
+ lines.push(...detailLines.slice(0, maxDetails));
443
+ const cut = detailLines.length - maxDetails;
444
+ lines.push(` ... ${cut} more (--json)`);
445
+ }
446
+
447
+ const printFn = result.ok ? out : err;
448
+ for (const line of lines) {
449
+ printFn(line);
450
+ }
451
+ }
452
+
453
+ function quoteLandArgument(value ) {
454
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
455
+ return `'${value.replace(/'/g, "'\\''")}'`;
456
+ }
457
+
458
+ const LAND_CONTROL_CHARS = /[\u0000-\u001f\u007f\u2028\u2029]/;
459
+
460
+ function makeLandCommand(op , ok , rows , root , draftPath ) {
461
+ if (op !== 'check' || !ok || !Array.isArray(rows) || rows.length === 0) return null;
462
+ const change = rows[0]?.change;
463
+ if ((change !== 'add' && change !== 'amend') || !rows.every((row) => row?.change === change)) return null;
464
+ const resolvedRoot = path.resolve(root);
465
+ if (LAND_CONTROL_CHARS.test(resolvedRoot) || LAND_CONTROL_CHARS.test(draftPath)) return null;
466
+ const action = change === 'add' ? 'apply' : 'amend';
467
+ return `doflow roadmap seed ${quoteLandArgument(resolvedRoot)} --action ${action} --draft ${quoteLandArgument(draftPath)}`;
468
+ }
469
+
470
+ function handleSeedCommand(options = {}) {
471
+ const {
472
+ action,
473
+ draft,
474
+ json = false,
475
+ cwd = process.cwd(),
476
+ root = resolveMainRoot(cwd),
477
+ fsImpl = fs,
478
+ gitFacts,
479
+ readSwitch,
480
+ alive,
481
+ env = process.env,
482
+ pid,
483
+ host,
484
+ now,
485
+ out = (s ) => console.log(s),
486
+ err = (s ) => console.error(s),
487
+ classes,
488
+ isTTY = process.stdin.isTTY === true,
489
+ confirm,
490
+ ttyClock,
491
+ ttySleep,
492
+ } = options;
493
+
494
+ const usage = (message ) => {
495
+ if (json) {
496
+ out(JSON.stringify({ error: 'usage', detail: message }));
497
+ } else {
498
+ err(`doflow roadmap: error: ${message}`);
499
+ }
500
+ return 2;
501
+ };
502
+
503
+ // 1. Usage check
504
+ if (!action || !['check', 'apply', 'amend'].includes(action)) {
505
+ return usage('--action must be one of check, apply, amend');
506
+ }
507
+ if (!draft) {
508
+ return usage('--draft <path> is required');
509
+ }
510
+
511
+ const absDraftPath = path.resolve(cwd, draft);
512
+ let draftRaw ;
513
+ try {
514
+ draftRaw = fsImpl.readFileSync(absDraftPath, 'utf8');
515
+ } catch (error ) {
516
+ return usage(`cannot read draft file '${absDraftPath}': ${error.message}`);
517
+ }
518
+
519
+ const storePath = path.join(root, 'agent-docs', 'roadmap', 'rows.json');
520
+ const tablePath = path.join(root, 'agent-docs', 'roadmap', 'ROADMAP.md');
521
+ const emptySeedRaw = JSON.stringify({ schema: 1, rows: [] });
522
+
523
+ let seedRaw ;
524
+ let storeAbsent = false;
525
+ try {
526
+ seedRaw = fsImpl.readFileSync(storePath, 'utf8');
527
+ } catch (error ) {
528
+ if (error?.code === 'ENOENT') {
529
+ seedRaw = emptySeedRaw;
530
+ storeAbsent = true;
531
+ } else {
532
+ return usage(`cannot read store file '${storePath}': ${error.message}`);
533
+ }
534
+ }
535
+
536
+ // 2. File digests
537
+ const draftSha = sha256Digest(draftRaw);
538
+ const seedSha = storeAbsent ? null : sha256Digest(seedRaw);
539
+
540
+ // 3. Check action
541
+ if (action === 'check') {
542
+ const facts = gitFacts || gatherGitFacts({ root });
543
+ if (facts.unreadable && facts.unreadable.length > 0) {
544
+ if (json) {
545
+ out(
546
+ JSON.stringify({
547
+ schema: 1,
548
+ action: 'seed',
549
+ op: action,
550
+ error: 'cannot-read-facts',
551
+ sources: facts.unreadable,
552
+ })
553
+ );
554
+ } else {
555
+ err(`doflow roadmap seed: cannot read facts: ${facts.unreadable.join(', ')}`);
556
+ }
557
+ return 1;
558
+ }
559
+
560
+ const judgement = judgeDraft({
561
+ draftRaw,
562
+ seedRaw,
563
+ gitFacts: facts,
564
+ op: action,
565
+ classes,
566
+ });
567
+
568
+ const result = {
569
+ schema: 1,
570
+ action: 'seed',
571
+ op: action,
572
+ ok: judgement.ok,
573
+ draft: { path: absDraftPath, sha256: draftSha },
574
+ seed: { path: storePath, sha256: seedSha },
575
+ rows: judgement.rows,
576
+ findings: judgement.findings,
577
+ overlaps: judgement.overlaps,
578
+ alone: judgement.alone,
579
+ reason: null,
580
+ switch: null,
581
+ writes: 0,
582
+ land: makeLandCommand(action, judgement.ok, judgement.rows, root, absDraftPath),
583
+ };
584
+
585
+ if (json) {
586
+ out(JSON.stringify(result));
587
+ } else {
588
+ printTextResult(result, out, err);
589
+ }
590
+ return result.ok ? 0 : 1;
591
+ }
592
+
593
+ // 4. Apply / Amend action
594
+ // (a) maint-thin / AU C05: a seed write is allowed whatever the switch says. Rows land as drafted (ready) only when
595
+ // stdin is an interactive terminal AND either autopilot is verifiably off (the fail-closed seed reader) or a person
596
+ // answers yes to the same `/dev/tty` [y/N] that `roadmap promote` uses. A model with a shell controls the
597
+ // environment (an empty config home reads as off), so the switch alone cannot tell a person; and without the prompt
598
+ // a model with a pty could land ready rows while autopilot is on, which widens the queue. No terminal, or no/other
599
+ // answer, lands every row it adds or amends `proposed`, which autopilot never picks or advances until a person runs
600
+ // `roadmap promote`. The prompt comes before the roadmap lease (Ctrl-C would otherwise leave the lease behind) and
601
+ // only for a draft that judges clean against the store as read now. No flag or environment variable skips it.
602
+ const switchReader = readSwitch || readSeedSwitch;
603
+ let landProposed = isTTY !== true || switchReader(env, fsImpl).enabled !== false;
604
+ if (landProposed && isTTY === true) {
605
+ const preFacts = gitFacts || gatherGitFacts({ root });
606
+ const pre = preFacts.unreadable && preFacts.unreadable.length > 0 ? null
607
+ : judgeDraft({ draftRaw, seedRaw, gitFacts: preFacts, op: action, classes });
608
+ if (pre && pre.ok) {
609
+ const rowsList = (JSON.parse(draftRaw).rows || []).filter((r ) => r && typeof r === 'object');
610
+ const ids = rowsList.map((r ) => safeText(r.id));
611
+ const titles = rowsList.map((r ) => ` ${safeText(r.id)} ${safeText(r.title)}`);
612
+ const prompt = `${titles.join('\n')}\nLand ${ids.length} row(s) ready for autopilot (autopilot is not verifiably off): ${ids.join(' ')}? [y/N] `;
613
+ let answer = null;
614
+ try {
615
+ if (confirm) answer = confirm(prompt);
616
+ else {
617
+ const read = readTtyLine(prompt, { timeoutMs: SEED_PROMPT_TIMEOUT_MS, now: ttyClock, sleep: ttySleep });
618
+ answer = read.answer;
619
+ if (read.timedOut) err(`doflow roadmap seed: the [y/N] prompt timed out after ${Math.round(SEED_PROMPT_TIMEOUT_MS / 1000)} s with no answer; rows land proposed (run: doflow roadmap promote <id...>)`);
620
+ }
621
+ } catch { answer = null; }
622
+ if (typeof answer === 'string' && /^(y|yes)$/i.test(answer.trim())) landProposed = false;
623
+ }
624
+ }
625
+
626
+ // (b) Acquire lease
627
+ const leaseDir = path.join(root, '.doflow', 'state', 'roadmap');
628
+ const nowFn = now ? now : () => new Date();
629
+ const nowDate = nowFn();
630
+ const me = {
631
+ schema: 1,
632
+ pid: pid ?? process.pid,
633
+ start: new Date(Date.now() - (process ).uptime() * 1000).toISOString(),
634
+ host: host ?? os.hostname(),
635
+ runId: null,
636
+ acquiredAt: nowDate.toISOString(),
637
+ };
638
+ const leaseAlive = alive || noPsAlive;
639
+ const lease = acquireLease(leaseDir, me, leaseAlive, fsImpl);
640
+ if (!lease.ok) {
641
+ const holder = lease.holder && typeof lease.holder === 'object' ? lease.holder : null;
642
+ const result = {
643
+ schema: 1,
644
+ action: 'seed',
645
+ op: action,
646
+ ok: false,
647
+ draft: { path: absDraftPath, sha256: draftSha },
648
+ seed: { path: storePath, sha256: seedSha },
649
+ rows: [],
650
+ findings: [],
651
+ overlaps: [],
652
+ alone: [],
653
+ reason: 'lease-held',
654
+ switch: null,
655
+ writes: 0,
656
+ land: null,
657
+ };
658
+ const holderDesc = holder
659
+ ? `pid ${safeText(holder.pid ?? 'unknown')} on ${safeText(holder.host ?? 'unknown')}`
660
+ : 'unknown holder';
661
+
662
+ if (json) {
663
+ out(JSON.stringify(result));
664
+ } else {
665
+ printTextResult(result, out, err, {
666
+ refusalDetail: `lease held by ${holderDesc}`,
667
+ });
668
+ }
669
+ return 1;
670
+ }
671
+
672
+ // (c) Under lease
673
+ try {
674
+ let freshSeedRaw ;
675
+ let freshStoreAbsent = false;
676
+ try {
677
+ freshSeedRaw = fsImpl.readFileSync(storePath, 'utf8');
678
+ } catch (error ) {
679
+ if (error?.code === 'ENOENT') {
680
+ freshSeedRaw = emptySeedRaw;
681
+ freshStoreAbsent = true;
682
+ } else {
683
+ return usage(`cannot read store file '${storePath}': ${error.message}`);
684
+ }
685
+ }
686
+ const freshSeedSha = freshStoreAbsent ? null : sha256Digest(freshSeedRaw);
687
+
688
+ const facts = gitFacts || gatherGitFacts({ root });
689
+ if (facts.unreadable && facts.unreadable.length > 0) {
690
+ if (json) {
691
+ out(
692
+ JSON.stringify({
693
+ schema: 1,
694
+ action: 'seed',
695
+ op: action,
696
+ error: 'cannot-read-facts',
697
+ sources: facts.unreadable,
698
+ })
699
+ );
700
+ } else {
701
+ err(`doflow roadmap seed: cannot read facts: ${facts.unreadable.join(', ')}`);
702
+ }
703
+ return 1;
704
+ }
705
+
706
+ const judgement = judgeDraft({
707
+ draftRaw,
708
+ seedRaw: freshSeedRaw,
709
+ gitFacts: facts,
710
+ op: action,
711
+ classes,
712
+ });
713
+
714
+ if (!judgement.ok) {
715
+ const result = {
716
+ schema: 1,
717
+ action: 'seed',
718
+ op: action,
719
+ ok: false,
720
+ draft: { path: absDraftPath, sha256: draftSha },
721
+ seed: { path: storePath, sha256: freshSeedSha },
722
+ rows: judgement.rows,
723
+ findings: judgement.findings,
724
+ overlaps: judgement.overlaps,
725
+ alone: judgement.alone,
726
+ reason: null,
727
+ switch: null,
728
+ writes: 0,
729
+ land: null,
730
+ };
731
+
732
+ if (json) {
733
+ out(JSON.stringify(result));
734
+ } else {
735
+ printTextResult(result, out, err);
736
+ }
737
+ return 1;
738
+ }
739
+
740
+ // (d) No finding: build new store document and write
741
+ let parsedStore ;
742
+ try {
743
+ parsedStore = JSON.parse(freshSeedRaw);
744
+ } catch {
745
+ throw new Error('Seed invariant failed after clean judgement: store JSON could not be parsed');
746
+ }
747
+ if (!parsedStore || typeof parsedStore !== 'object' || Array.isArray(parsedStore) || !Array.isArray(parsedStore.rows)) {
748
+ throw new Error('Seed invariant failed after clean judgement: store rows must be an array');
749
+ }
750
+ const storeRows = parsedStore.rows;
751
+ const proposedIds = [];
752
+ const proposedIf = (row ) => {
753
+ if (!landProposed) return row;
754
+ proposedIds.push(row.id);
755
+ return { ...row, proposed: true };
756
+ };
757
+ let newRows ;
758
+ if (action === 'apply') {
759
+ const draftAddRows = [];
760
+ const draftDoc = JSON.parse(draftRaw);
761
+ for (const r of draftDoc.rows || []) {
762
+ if (r && typeof r === 'object' && typeof r.id === 'string') {
763
+ draftAddRows.push(r);
764
+ }
765
+ }
766
+ newRows = [...storeRows, ...draftAddRows.map(proposedIf)];
767
+ } else {
768
+ // amend
769
+ const draftMap = new Map ();
770
+ const draftDoc = JSON.parse(draftRaw);
771
+ for (const r of draftDoc.rows || []) {
772
+ if (r && typeof r === 'object' && typeof r.id === 'string') {
773
+ draftMap.set(r.id, r);
774
+ }
775
+ }
776
+ newRows = storeRows.map((r ) => {
777
+ if (r && typeof r === 'object' && draftMap.has(r.id)) {
778
+ // An amend never promotes: a live proposed row stays proposed whatever this call would otherwise land.
779
+ if (r.proposed === true) {
780
+ proposedIds.push(r.id);
781
+ return { ...draftMap.get(r.id), proposed: true };
782
+ }
783
+ return proposedIf(draftMap.get(r.id));
784
+ }
785
+ return r;
786
+ });
787
+ }
788
+
789
+ const newStoreDoc = { ...parsedStore, rows: newRows };
790
+ const newStoreRaw = JSON.stringify(newStoreDoc, null, 2) + '\n';
791
+
792
+ let writes = 0;
793
+ if (newStoreRaw !== freshSeedRaw) {
794
+ const storeDir = path.dirname(storePath);
795
+ fsImpl.mkdirSync(storeDir, { recursive: true });
796
+ const tmpStorePath = path.join(storeDir, `.tmp-rows-${pid ?? process.pid}-${Date.now()}`);
797
+ try {
798
+ fsImpl.writeFileSync(tmpStorePath, newStoreRaw, 'utf8');
799
+ fsImpl.renameSync(tmpStorePath, storePath);
800
+ } catch (error) {
801
+ try { fsImpl.unlinkSync(tmpStorePath); } catch { /* ignore temp cleanup failure */ }
802
+ throw error;
803
+ }
804
+ writes += 1;
805
+ }
806
+
807
+ const { statuses } = deriveStatus(newRows, facts);
808
+ const newTableContent = render(newRows, statuses);
809
+ const tableResult = renderRoadmapFile({
810
+ roadmapPath: tablePath,
811
+ content: newTableContent,
812
+ fsImpl,
813
+ });
814
+ if (tableResult.written) {
815
+ writes += 1;
816
+ }
817
+
818
+ const result = {
819
+ schema: 1,
820
+ action: 'seed',
821
+ op: action,
822
+ ok: true,
823
+ draft: { path: absDraftPath, sha256: draftSha },
824
+ seed: { path: storePath, sha256: freshSeedSha },
825
+ rows: judgement.rows,
826
+ findings: [],
827
+ overlaps: judgement.overlaps,
828
+ alone: judgement.alone,
829
+ reason: null,
830
+ switch: null,
831
+ writes,
832
+ land: null,
833
+ proposed: proposedIds,
834
+ };
835
+
836
+ if (json) {
837
+ out(JSON.stringify(result));
838
+ } else {
839
+ printTextResult(result, out, err);
840
+ }
841
+ return 0;
842
+ } finally {
843
+ releaseLease(leaseDir, me, fsImpl);
844
+ }
845
+ }
846
+
847
+
848
+
849
+
850
+
851
+
852
+
853
+
854
+
855
+
856
+
857
+
858
+
859
+
860
+
861
+
862
+
863
+
864
+ /** How long the seed prompt waits for a person before it counts as "no" (promote waits without limit). */
865
+ const SEED_PROMPT_TIMEOUT_MS = 60000;
866
+ const TTY_POLL_MS = 25;
867
+
868
+ function sleepMs(ms ) {
869
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
870
+ }
871
+
872
+
873
+
874
+
875
+
876
+
877
+
878
+
879
+ /**
880
+ * Reads one answer line from the controlling terminal (`/dev/tty`), never from stdin, an argument or the environment.
881
+ * `answer` is null when the terminal cannot be opened or read, the line is too long, or the optional timeout passes
882
+ * (`timedOut`). A timed read opens the terminal non-blocking and polls, so a pty that stays open without an answer
883
+ * cannot hold the caller for ever.
884
+ */
885
+ function readTtyLine(prompt , options = {}) {
886
+ const { timeoutMs = 0, now = Date.now, sleep = sleepMs } = options;
887
+ const timed = timeoutMs > 0;
888
+ let fd ;
889
+ try { fd = timed ? fs.openSync('/dev/tty', fs.constants.O_RDWR | fs.constants.O_NONBLOCK) : fs.openSync('/dev/tty', 'r+'); } catch { return { answer: null, timedOut: false }; }
890
+ const deadline = timed ? now() + timeoutMs : Infinity;
891
+ let timedOut = false;
892
+ // one byte; 0 at end of input, -1 when the timeout passed
893
+ const readByte = (buf ) => {
894
+ for (;;) {
895
+ try { return fs.readSync(fd, buf, 0, 1, null); }
896
+ catch (error ) {
897
+ if (!timed || (error?.code !== 'EAGAIN' && error?.code !== 'EWOULDBLOCK')) throw error;
898
+ if (now() >= deadline) { timedOut = true; return -1; }
899
+ sleep(TTY_POLL_MS);
900
+ }
901
+ }
902
+ };
903
+ try {
904
+ fs.writeSync(fd, prompt);
905
+ const buf = Buffer.alloc(1);
906
+ let line = '';
907
+ let ended = false;
908
+ for (let i = 0; i < 64 && !ended; i += 1) {
909
+ const n = readByte(buf);
910
+ if (n < 0) return { answer: null, timedOut: true };
911
+ if (n === 0 || buf[0] === 10) ended = true;
912
+ else line += String.fromCharCode(buf[0]);
913
+ }
914
+ // drain what is left of the typed line (bounded), so it is not left in the terminal queue
915
+ const tooLong = !ended;
916
+ for (let i = 0; i < 4096 && !ended; i += 1) {
917
+ const n = readByte(buf);
918
+ if (n < 0) return { answer: null, timedOut: true };
919
+ if (n === 0 || buf[0] === 10) ended = true;
920
+ }
921
+ return { answer: tooLong ? null : line, timedOut: false };
922
+ } catch {
923
+ return { answer: null, timedOut };
924
+ } finally {
925
+ try { fs.closeSync(fd); } catch { /* ignore close failure */ }
926
+ }
927
+ }
928
+
929
+ /** The untimed reader `roadmap promote` uses: a person runs that verb and may take as long as they need. */
930
+ function readTtyAnswer(prompt ) {
931
+ return readTtyLine(prompt).answer;
932
+ }
933
+
934
+ /**
935
+ * maint-thin: `roadmap promote <id...>`, person only. A model with a shell controls the environment, so neither an
936
+ * environment check nor the autopilot switch can tell a person. The verb requires an interactive terminal on stdin
937
+ * (refused `needs-person-tty`, nothing written), validates the named rows, then prints their ids and titles and asks
938
+ * `[y/N]` on the controlling terminal (`/dev/tty`): no `/dev/tty`, or an answer other than y/yes, is refused
939
+ * `needs-person-confirm` and nothing is written. No flag or environment variable skips the prompt, and the autopilot
940
+ * switch is not read. The prompt comes before the roadmap lease (Ctrl-C would otherwise leave the lease behind); under
941
+ * the lease the store is read again and must equal what was shown, then the verb drops the `proposed` mark of every
942
+ * named row, so autopilot may pick it. All or nothing: an unknown id, a row that is not proposed, or a duplicate id is a finding (reported before any
943
+ * prompt) and nothing is written. Exit 0 promoted, 1 refused or findings, 2 usage.
944
+ */
945
+ function handlePromoteCommand(options = {}) {
946
+ const {
947
+ ids = [],
948
+ json = false,
949
+ root = resolveMainRoot(process.cwd()),
950
+ fsImpl = fs,
951
+ gitFacts,
952
+ alive,
953
+ pid,
954
+ host,
955
+ now,
956
+ out = (s ) => console.log(s),
957
+ err = (s ) => console.error(s),
958
+ isTTY = process.stdin.isTTY === true,
959
+ confirm = readTtyAnswer,
960
+ } = options;
961
+ const emit = (result , code , detail ) => {
962
+ if (json) out(JSON.stringify(result));
963
+ else {
964
+ const head = result.ok ? `roadmap promote: ok, ${result.promoted.length} promoted${result.promoted.length ? `: ${result.promoted.map(safeText).join(' ')}` : ''}`
965
+ : result.reason ? `roadmap promote: refused: ${safeText(result.reason)}` : `roadmap promote: ${result.findings.length} findings`;
966
+ (result.ok ? out : err)(head);
967
+ for (const f of result.findings || []) err(` [${safeText(f.rule)}] ${safeText(f.rowId)}: ${f.message}`);
968
+ if (detail) err(` ${detail}`);
969
+ }
970
+ return code;
971
+ };
972
+ if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => typeof id === 'string' && /^[0-9]{3}([a-z]|-[A-Z])?$/.test(id))) {
973
+ if (json) out(JSON.stringify({ error: 'usage', detail: 'roadmap promote takes one or more row ids' }));
974
+ else err('doflow roadmap: error: roadmap promote takes one or more row ids (e.g. doflow roadmap promote 130 131)');
975
+ return 2;
976
+ }
977
+ const storePath = path.join(root, 'agent-docs', 'roadmap', 'rows.json');
978
+ const tablePath = path.join(root, 'agent-docs', 'roadmap', 'ROADMAP.md');
979
+ const base = { schema: 1, action: 'promote', ok: false, ids, promoted: [] , findings: [] , reason: null , writes: 0 };
980
+
981
+ if (isTTY !== true) {
982
+ return emit({ ...base, reason: 'needs-person-tty' }, 1,
983
+ 'promote runs only from an interactive terminal: a person runs doflow roadmap promote in their own shell');
984
+ }
985
+
986
+ // Read, validate and ask BEFORE taking the lease: Ctrl-C kills the process inside the terminal read, so a lease held
987
+ // across the prompt would be left behind. The lease is taken after the answer, and the store is read again under it.
988
+ let raw ;
989
+ try { raw = fsImpl.readFileSync(storePath, 'utf8'); }
990
+ catch (error ) { return emit({ ...base, findings: ids.map((id) => ({ rule: 'row-not-found', rowId: id, message: `no store at ${storePath}: ${safeText(error.message)}` })) }, 1); }
991
+ const storeFindings = seedFindings(raw);
992
+ if (storeFindings.length > 0) return emit({ ...base, findings: storeFindings }, 1);
993
+ const doc = JSON.parse(raw);
994
+ const findings = [];
995
+ const seen = new Set ();
996
+ for (const id of ids) {
997
+ const row = doc.rows.find((r ) => r.id === id);
998
+ if (seen.has(id)) findings.push({ rule: 'duplicate-id', rowId: id, message: `Row '${safeText(id)}' is named twice` });
999
+ else if (!row) findings.push({ rule: 'row-not-found', rowId: id, message: `Row '${safeText(id)}' not found in store` });
1000
+ else if (row.proposed !== true) findings.push({ rule: 'row-not-proposed', rowId: id, message: `Row '${safeText(id)}' is not proposed` });
1001
+ seen.add(id);
1002
+ }
1003
+ if (findings.length > 0) return emit({ ...base, findings }, 1);
1004
+ const titles = ids.map((id) => ` ${safeText(id)} ${safeText(doc.rows.find((r ) => r.id === id).title)}`);
1005
+ const prompt = `${titles.join('\n')}\nPromote ${ids.length} row(s): ${ids.map(safeText).join(' ')}? [y/N] `;
1006
+ let answer = null;
1007
+ try { answer = confirm(prompt); } catch { answer = null; }
1008
+ if (typeof answer !== 'string' || !/^(y|yes)$/i.test(answer.trim())) {
1009
+ return emit({ ...base, reason: 'needs-person-confirm' }, 1,
1010
+ 'promote needs a person to answer y on the controlling terminal (/dev/tty); there is no flag or variable that skips the prompt');
1011
+ }
1012
+
1013
+ const leaseDir = path.join(root, '.doflow', 'state', 'roadmap');
1014
+ const nowDate = (now ? now : () => new Date())();
1015
+ const me = {
1016
+ schema: 1,
1017
+ pid: pid ?? process.pid,
1018
+ start: new Date(Date.now() - (process ).uptime() * 1000).toISOString(),
1019
+ host: host ?? os.hostname(),
1020
+ runId: null,
1021
+ acquiredAt: nowDate.toISOString(),
1022
+ };
1023
+ const lease = acquireLease(leaseDir, me, alive || noPsAlive, fsImpl);
1024
+ if (!lease.ok) return emit({ ...base, reason: 'lease-held' }, 1);
1025
+ try {
1026
+ let current = null;
1027
+ try { current = fsImpl.readFileSync(storePath, 'utf8'); } catch { current = null; }
1028
+ if (current !== raw) {
1029
+ return emit({ ...base, reason: 'needs-person-confirm' }, 1,
1030
+ 'the roadmap store changed after the rows were shown; nothing was written, run doflow roadmap promote again');
1031
+ }
1032
+ const rows = doc.rows.map((r ) => {
1033
+ if (!ids.includes(r.id)) return r;
1034
+ const { proposed, ...ready } = r;
1035
+ return ready;
1036
+ });
1037
+ const newRaw = JSON.stringify({ ...doc, rows }, null, 2) + '\n';
1038
+ let writes = 0;
1039
+ const tmp = path.join(path.dirname(storePath), `.tmp-rows-${pid ?? process.pid}-${Date.now()}`);
1040
+ try {
1041
+ fsImpl.writeFileSync(tmp, newRaw, 'utf8');
1042
+ fsImpl.renameSync(tmp, storePath);
1043
+ } catch (error) {
1044
+ try { fsImpl.unlinkSync(tmp); } catch { /* ignore temp cleanup failure */ }
1045
+ throw error;
1046
+ }
1047
+ writes += 1;
1048
+ const facts = gitFacts || gatherGitFacts({ root });
1049
+ if (!facts.unreadable || facts.unreadable.length === 0) {
1050
+ const { statuses } = deriveStatus(rows, facts);
1051
+ if (renderRoadmapFile({ roadmapPath: tablePath, content: render(rows, statuses), fsImpl }).written) writes += 1;
1052
+ }
1053
+ return emit({ ...base, ok: true, promoted: [...ids], writes }, 0);
1054
+ } finally {
1055
+ releaseLease(leaseDir, me, fsImpl);
1056
+ }
1057
+ }
1058
+
1059
+ module.exports = {
1060
+ judgeDraft,
1061
+ handleSeedCommand,
1062
+ handlePromoteCommand,
1063
+ };