@khoavu882/doflow 0.0.0-stage → 2.0.0-beta.4

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 (409) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +132 -2
  3. package/bin/doflow.js +44 -0
  4. package/core/.antigravity-plugin/hooks.json +39 -0
  5. package/core/.antigravity-plugin/mcp_config.json +18 -0
  6. package/core/.antigravity-plugin/plugin.json +31 -0
  7. package/core/.claude-plugin/marketplace.json +20 -0
  8. package/core/.claude-plugin/plugin.json +21 -0
  9. package/core/.codex-plugin/marketplace.json +20 -0
  10. package/core/.codex-plugin/plugin.json +35 -0
  11. package/core/.plugin/plugin.json +19 -0
  12. package/core/harnesses/antigravity/hooks/pre-implementation-gate.sh +9 -0
  13. package/core/harnesses/antigravity/hooks/stop-check.sh +10 -0
  14. package/core/harnesses/claude/hooks/lib.sh +304 -0
  15. package/core/harnesses/claude/hooks/mcp-tool-guard.sh +20 -0
  16. package/core/harnesses/claude/hooks/permission-audit.sh +47 -0
  17. package/core/harnesses/claude/hooks/post-compact.sh +74 -0
  18. package/core/harnesses/claude/hooks/post-edit-lint.sh +36 -0
  19. package/core/harnesses/claude/hooks/post-tool-failure.sh +47 -0
  20. package/core/harnesses/claude/hooks/pre-bash-guard.sh +20 -0
  21. package/core/harnesses/claude/hooks/pre-compact.sh +43 -0
  22. package/core/harnesses/claude/hooks/pre-implement-gate.sh +21 -0
  23. package/core/harnesses/claude/hooks/session-end.sh +89 -0
  24. package/core/harnesses/claude/hooks/session-start.sh +5 -0
  25. package/core/harnesses/claude/hooks/skill-config-audit.sh +39 -0
  26. package/core/harnesses/claude/hooks/stop-check.sh +5 -0
  27. package/core/harnesses/claude/hooks/subagent-audit.sh +7 -0
  28. package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -0
  29. package/core/harnesses/claude/settings/keybindings.json +20 -0
  30. package/core/harnesses/claude/settings/settings.json +201 -0
  31. package/core/harnesses/claude/statusline/doflow-statusline.sh +49 -0
  32. package/core/harnesses/codex/agents/core-implementer.toml +7 -0
  33. package/core/harnesses/codex/agents/quality-guardian.toml +8 -0
  34. package/core/harnesses/codex/agents/research-writer.toml +8 -0
  35. package/core/harnesses/codex/agents/spec-analyst.toml +8 -0
  36. package/core/harnesses/codex/agents/system-architect.toml +9 -0
  37. package/core/harnesses/codex/config/config.toml +10 -0
  38. package/core/harnesses/codex/hooks/hooks.json +122 -0
  39. package/core/harnesses/codex/hooks/lib.sh +335 -0
  40. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +18 -0
  41. package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +52 -0
  42. package/core/harnesses/codex/hooks/post-edit-lint.sh +6 -0
  43. package/core/harnesses/codex/hooks/pre-bash-guard.sh +18 -0
  44. package/core/harnesses/codex/hooks/pre-compact.impl.sh +46 -0
  45. package/core/harnesses/codex/hooks/pre-compact.sh +9 -0
  46. package/core/harnesses/codex/hooks/pre-implement-gate.sh +19 -0
  47. package/core/harnesses/codex/hooks/session-end.impl.sh +92 -0
  48. package/core/harnesses/codex/hooks/session-end.sh +5 -0
  49. package/core/harnesses/codex/hooks/session-start.sh +5 -0
  50. package/core/harnesses/codex/hooks/stop-check.sh +8 -0
  51. package/core/harnesses/codex/hooks/subagent-audit.sh +8 -0
  52. package/core/harnesses/codex/hooks/user-prompt-submit.sh +6 -0
  53. package/core/harnesses/gemini/hooks/hooks.json +77 -0
  54. package/core/harnesses/gemini/hooks/lib.sh +302 -0
  55. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +21 -0
  56. package/core/harnesses/gemini/hooks/post-edit-lint.sh +39 -0
  57. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +21 -0
  58. package/core/harnesses/gemini/hooks/pre-compact.sh +38 -0
  59. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +22 -0
  60. package/core/harnesses/gemini/hooks/session-end.impl.sh +89 -0
  61. package/core/harnesses/gemini/hooks/session-end.sh +5 -0
  62. package/core/harnesses/gemini/hooks/session-start.sh +5 -0
  63. package/core/harnesses/gemini/settings/adapter-defaults.json +4 -0
  64. package/core/harnesses/kiro/agents/doflow.json +58 -0
  65. package/core/harnesses/kiro/hooks/doflow.json +53 -0
  66. package/core/harnesses/kiro/hooks/lib.sh +324 -0
  67. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +9 -0
  68. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +9 -0
  69. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +9 -0
  70. package/core/harnesses/kiro/hooks/session-start.sh +9 -0
  71. package/core/harnesses/kiro/hooks/stop-check.sh +43 -0
  72. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +39 -0
  73. package/core/harnesses/shared/hooks/adapters/antigravity.js +111 -0
  74. package/core/harnesses/shared/hooks/adapters/arg-fields.js +19 -0
  75. package/core/harnesses/shared/hooks/adapters/gemini.js +114 -0
  76. package/core/harnesses/shared/hooks/hooks.json +47 -0
  77. package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +91 -0
  78. package/core/harnesses/shared/hooks/policies/capture-failure.sh +97 -0
  79. package/core/harnesses/shared/hooks/policies/deny-json.sh +29 -0
  80. package/core/harnesses/shared/hooks/policies/lib.sh +501 -0
  81. package/core/harnesses/shared/hooks/policies/mcp-policy.conf +21 -0
  82. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +91 -0
  83. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +328 -0
  84. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +496 -0
  85. package/core/harnesses/shared/hooks/policies/prompt-nudge.jq +131 -0
  86. package/core/harnesses/shared/hooks/policies/session-context.sh +141 -0
  87. package/core/harnesses/shared/hooks/policies/stop-check.sh +264 -0
  88. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +46 -0
  89. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +235 -0
  90. package/core/harnesses/shared/hooks/stream-hook-runner.js +590 -0
  91. package/core/harnesses/shared/locator/doflow-run +87 -0
  92. package/core/registry/advice.json +101 -0
  93. package/core/registry/assets.json +792 -0
  94. package/core/registry/capabilities.json +120 -0
  95. package/core/registry/contracts.json +189 -0
  96. package/core/registry/external-tools.json +191 -0
  97. package/core/registry/harnesses.json +2062 -0
  98. package/core/registry/lifecycle.json +231 -0
  99. package/core/registry/mcp.json +18 -0
  100. package/core/registry/models.json +79 -0
  101. package/core/registry/readiness-templates.json +118 -0
  102. package/core/registry/routes.json +47 -0
  103. package/core/registry/storage.json +1061 -0
  104. package/core/registry/verification.json +258 -0
  105. package/core/registry/workflows.json +790 -0
  106. package/core/shared/agent-specs/core-implementer.md +73 -0
  107. package/core/shared/agent-specs/quality-guardian.md +28 -0
  108. package/core/shared/agent-specs/research-writer.md +31 -0
  109. package/core/shared/agent-specs/spec-analyst.md +33 -0
  110. package/core/shared/agent-specs/system-architect.md +29 -0
  111. package/core/shared/guidance/DOFLOW_CORE.md +8 -0
  112. package/core/shared/guidance/FLAGS.md +25 -0
  113. package/core/shared/guidance/PRINCIPLES.md +61 -0
  114. package/core/shared/guidance/VERSION +1 -0
  115. package/core/shared/guidance/mcp/MCP_Context7.md +18 -0
  116. package/core/shared/guidance/mcp/MCP_Sequential.md +17 -0
  117. package/core/shared/guidance/modes/MODE_Brainstorming.md +46 -0
  118. package/core/shared/guidance/modes/MODE_DeepResearch.md +69 -0
  119. package/core/shared/guidance/modes/MODE_Introspection.md +33 -0
  120. package/core/shared/guidance/modes/MODE_Orchestration.md +57 -0
  121. package/core/shared/guidance/modes/MODE_Task_Management.md +74 -0
  122. package/core/shared/guidance/pointers/claude-gemini.md +2 -0
  123. package/core/shared/guidance/pointers/codex.md +6 -0
  124. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +373 -0
  125. package/core/shared/guidance/references/ARTIFACT_VOICE.md +104 -0
  126. package/core/shared/guidance/references/CONSTITUTION_BASE.md +47 -0
  127. package/core/shared/guidance/references/DIAGRAM_GUIDE.md +161 -0
  128. package/core/shared/guidance/references/DOFLOW_CHAIN.md +87 -0
  129. package/core/shared/guidance/references/EVIDENCE_LEDGER.md +93 -0
  130. package/core/shared/guidance/references/MODEL_SELECTION.md +74 -0
  131. package/core/shared/guidance/references/ORCHESTRATION.md +36 -0
  132. package/core/shared/guidance/references/RESEARCH_CONFIG.md +21 -0
  133. package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +81 -0
  134. package/core/shared/guidance/rules/RULE_01_SAFETY.md +25 -0
  135. package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +23 -0
  136. package/core/shared/guidance/rules/RULE_03_QUALITY.md +14 -0
  137. package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +46 -0
  138. package/core/shared/scripts/doflow/assets/mermaid-theme.json +46 -0
  139. package/core/shared/scripts/doflow/bash/do-exec-paths.sh +167 -0
  140. package/core/shared/scripts/doflow/bash/do-git-state.sh +546 -0
  141. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +208 -0
  142. package/core/shared/scripts/doflow/bash/do-paths.sh +347 -0
  143. package/core/shared/scripts/doflow/bash/do-prereqs.sh +66 -0
  144. package/core/shared/scripts/doflow/bash/do-review-package.sh +127 -0
  145. package/core/shared/scripts/doflow/bash/do-task-brief.sh +698 -0
  146. package/core/shared/scripts/doflow/bash/render-audit.sh +224 -0
  147. package/core/shared/scripts/doflow/bash/render-diagrams.sh +177 -0
  148. package/core/shared/scripts/doflow/bash/render-puml.sh +223 -0
  149. package/core/shared/scripts/doflow/bash/sync-context.sh +66 -0
  150. package/core/shared/scripts/doflow/bash/validate-artifacts.sh +526 -0
  151. package/core/shared/scripts/doflow/bin/doflow-run +592 -0
  152. package/core/shared/skills/do/SKILL.md +128 -0
  153. package/core/shared/skills/do/references/estimation.md +13 -0
  154. package/core/shared/skills/do/references/pm_routing.md +25 -0
  155. package/core/shared/skills/do/references/tool_matrix.md +52 -0
  156. package/core/shared/skills/do-brainstorm/SKILL.md +252 -0
  157. package/core/shared/skills/do-code-review/README.md +49 -0
  158. package/core/shared/skills/do-code-review/SKILL.md +333 -0
  159. package/core/shared/skills/do-code-review/assets/sample_c_clean.c +71 -0
  160. package/core/shared/skills/do-code-review/assets/sample_c_smells.c +66 -0
  161. package/core/shared/skills/do-code-review/assets/sample_cpp_shift.cpp +19 -0
  162. package/core/shared/skills/do-code-review/assets/sample_csharp_clean.cs +100 -0
  163. package/core/shared/skills/do-code-review/assets/sample_csharp_smells.cs +78 -0
  164. package/core/shared/skills/do-code-review/assets/sample_java_clean.java +55 -0
  165. package/core/shared/skills/do-code-review/assets/sample_java_smells.java +56 -0
  166. package/core/shared/skills/do-code-review/assets/sample_json_clean.json +7 -0
  167. package/core/shared/skills/do-code-review/assets/sample_markdown_clean.md +30 -0
  168. package/core/shared/skills/do-code-review/assets/sample_markdown_smells.md +29 -0
  169. package/core/shared/skills/do-code-review/assets/sample_shell_heredoc.sh +38 -0
  170. package/core/shared/skills/do-code-review/assets/sample_shell_smells.sh +15 -0
  171. package/core/shared/skills/do-code-review/assets/sample_typescript_strings.ts +36 -0
  172. package/core/shared/skills/do-code-review/assets/sample_yaml_smells.yaml +6 -0
  173. package/core/shared/skills/do-code-review/content-types/config.md +83 -0
  174. package/core/shared/skills/do-code-review/content-types/markdown.md +132 -0
  175. package/core/shared/skills/do-code-review/expected_outputs/sample_c_clean_quality.json +79 -0
  176. package/core/shared/skills/do-code-review/expected_outputs/sample_c_smells_quality.json +169 -0
  177. package/core/shared/skills/do-code-review/expected_outputs/sample_cpp_shift_quality.json +49 -0
  178. package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_clean_quality.json +149 -0
  179. package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_smells_quality.json +157 -0
  180. package/core/shared/skills/do-code-review/expected_outputs/sample_java_clean_quality.json +67 -0
  181. package/core/shared/skills/do-code-review/expected_outputs/sample_java_smells_quality.json +97 -0
  182. package/core/shared/skills/do-code-review/expected_outputs/sample_json_clean_quality.json +33 -0
  183. package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_clean_quality.json +1 -0
  184. package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_smells_quality.json +16 -0
  185. package/core/shared/skills/do-code-review/expected_outputs/sample_shell_heredoc_quality.json +54 -0
  186. package/core/shared/skills/do-code-review/expected_outputs/sample_shell_smells_quality.json +91 -0
  187. package/core/shared/skills/do-code-review/expected_outputs/sample_typescript_strings_quality.json +60 -0
  188. package/core/shared/skills/do-code-review/expected_outputs/sample_yaml_smells_quality.json +46 -0
  189. package/core/shared/skills/do-code-review/languages/c.md +103 -0
  190. package/core/shared/skills/do-code-review/languages/cpp.md +103 -0
  191. package/core/shared/skills/do-code-review/languages/csharp.md +97 -0
  192. package/core/shared/skills/do-code-review/languages/dart.md +108 -0
  193. package/core/shared/skills/do-code-review/languages/go.md +92 -0
  194. package/core/shared/skills/do-code-review/languages/java.md +103 -0
  195. package/core/shared/skills/do-code-review/languages/kotlin.md +88 -0
  196. package/core/shared/skills/do-code-review/languages/php.md +109 -0
  197. package/core/shared/skills/do-code-review/languages/python.md +94 -0
  198. package/core/shared/skills/do-code-review/languages/ruby.md +105 -0
  199. package/core/shared/skills/do-code-review/languages/rust.md +99 -0
  200. package/core/shared/skills/do-code-review/languages/shell.md +90 -0
  201. package/core/shared/skills/do-code-review/languages/swift.md +88 -0
  202. package/core/shared/skills/do-code-review/languages/typescript.md +97 -0
  203. package/core/shared/skills/do-code-review/review-policy.json +47 -0
  204. package/core/shared/skills/do-code-review/rules/universal.md +49 -0
  205. package/core/shared/skills/do-code-review/scripts/code_quality_checker.py +1696 -0
  206. package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +532 -0
  207. package/core/shared/skills/do-code-review/scripts/pr_analyzer.py +620 -0
  208. package/core/shared/skills/do-code-review/scripts/review_report_generator.py +505 -0
  209. package/core/shared/skills/do-constitution/SKILL.md +72 -0
  210. package/core/shared/skills/do-design/SKILL.md +212 -0
  211. package/core/shared/skills/do-diagnose/SKILL.md +156 -0
  212. package/core/shared/skills/do-diagnose/references/code_audit.md +15 -0
  213. package/core/shared/skills/do-diagnose/references/refactoring.md +12 -0
  214. package/core/shared/skills/do-diagnose/references/root_cause.md +17 -0
  215. package/core/shared/skills/do-document/SKILL.md +81 -0
  216. package/core/shared/skills/do-document/references/api-reference.md +42 -0
  217. package/core/shared/skills/do-document/references/deep_research.md +18 -0
  218. package/core/shared/skills/do-document/references/embedded-research.md +11 -0
  219. package/core/shared/skills/do-document/references/implementation-flow.md +105 -0
  220. package/core/shared/skills/do-document/references/index_generator.md +9 -0
  221. package/core/shared/skills/do-document/references/user-guide.md +22 -0
  222. package/core/shared/skills/do-execute-plan/SKILL.md +261 -0
  223. package/core/shared/skills/do-execute-plan/references/handoff_record.md +57 -0
  224. package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +92 -0
  225. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +155 -0
  226. package/core/shared/skills/do-execute-plan/references/run_position.md +63 -0
  227. package/core/shared/skills/do-execute-plan/references/scaffold.md +441 -0
  228. package/core/shared/skills/do-execute-plan/references/task_attempts.md +41 -0
  229. package/core/shared/skills/do-flow/SKILL.md +186 -0
  230. package/core/shared/skills/do-flow/references/task_classes.md +134 -0
  231. package/core/shared/skills/do-git/SKILL.md +115 -0
  232. package/core/shared/skills/do-git/references/hotfix-ritual.md +44 -0
  233. package/core/shared/skills/do-git/references/lifecycle-policy.md +54 -0
  234. package/core/shared/skills/do-git/references/release-ritual.md +64 -0
  235. package/core/shared/skills/do-implement/SKILL.md +222 -0
  236. package/core/shared/skills/do-plan/SKILL.md +219 -0
  237. package/core/shared/skills/do-test/SKILL.md +113 -0
  238. package/core/shared/templates/doflow/constitution-template.md +28 -0
  239. package/core/shared/templates/doflow/data-model-template.md +169 -0
  240. package/core/shared/templates/doflow/design-template.md +193 -0
  241. package/core/shared/templates/doflow/external-contract-template.md +35 -0
  242. package/core/shared/templates/doflow/intent-template.md +45 -0
  243. package/core/shared/templates/doflow/plan-template.md +151 -0
  244. package/core/shared/templates/doflow/question-log-template.md +51 -0
  245. package/core/shared/templates/doflow/requirement-template.md +126 -0
  246. package/core/shared/templates/doflow/specs-template.md +55 -0
  247. package/core/shared/templates/doflow/state-template.md +59 -0
  248. package/core/shared/workflows/do-flow-chain.md +17 -0
  249. package/package.json +48 -4
  250. package/src/adapters/antigravity/index.js +631 -0
  251. package/src/adapters/claude/index.js +548 -0
  252. package/src/adapters/claude/mcp.js +94 -0
  253. package/src/adapters/codex/agents.js +208 -0
  254. package/src/adapters/codex/config.js +272 -0
  255. package/src/adapters/codex/hooks.js +203 -0
  256. package/src/adapters/codex/index.js +707 -0
  257. package/src/adapters/codex/mcp.js +271 -0
  258. package/src/adapters/codex/native-projection.js +34 -0
  259. package/src/adapters/copilot/index.js +413 -0
  260. package/src/adapters/copy-tree.js +620 -0
  261. package/src/adapters/gemini/hooks.js +210 -0
  262. package/src/adapters/gemini/index.js +390 -0
  263. package/src/adapters/hook-commands.js +60 -0
  264. package/src/adapters/index.js +106 -0
  265. package/src/adapters/kiro/index.js +310 -0
  266. package/src/adapters/mcp-entries.js +331 -0
  267. package/src/adapters/opencode/index.js +466 -0
  268. package/src/adapters/pi/index.js +279 -0
  269. package/src/adapters/pi/json-members.js +185 -0
  270. package/src/adapters/pi/mcp.js +373 -0
  271. package/src/adapters/resolve-source.js +19 -0
  272. package/src/cli/commands/install.js +218 -0
  273. package/src/cli/commands/list-backups.js +21 -0
  274. package/src/cli/commands/reconcile.js +120 -0
  275. package/src/cli/commands/remove.js +82 -0
  276. package/src/cli/commands/rollback.js +172 -0
  277. package/src/cli/commands/self-update.js +33 -0
  278. package/src/cli/commands/status.js +138 -0
  279. package/src/cli/commands/tools.js +92 -0
  280. package/src/cli/commands/update.js +162 -0
  281. package/src/cli/index.js +568 -0
  282. package/src/cli/runtime-commands.js +281 -0
  283. package/src/cli/shared.js +218 -0
  284. package/src/helper/bash-helper.js +34 -0
  285. package/src/helper/git.js +24 -0
  286. package/src/helper/guardrails.js +49 -0
  287. package/src/helper/harness-paths.js +140 -0
  288. package/src/helper/marker-merge.js +116 -0
  289. package/src/helper/prompt.js +61 -0
  290. package/src/helper/repo-root.js +29 -0
  291. package/src/helper/settings-merge.js +151 -0
  292. package/src/helper/settings-scope.js +33 -0
  293. package/src/helper/toml.js +244 -0
  294. package/src/install/backup-set.js +149 -0
  295. package/src/install/backup.js +724 -0
  296. package/src/install/context.js +58 -0
  297. package/src/install/manifest.js +145 -0
  298. package/src/install/mcp.js +254 -0
  299. package/src/install/paths.js +84 -0
  300. package/src/install/targets.js +90 -0
  301. package/src/install/tool-lifecycle.js +151 -0
  302. package/src/lifecycle/bash-availability.js +25 -0
  303. package/src/lifecycle/index.js +812 -0
  304. package/src/lifecycle/mcp-index.js +28 -0
  305. package/src/lifecycle/policies.js +73 -0
  306. package/src/lifecycle/view.js +175 -0
  307. package/src/registry/index.js +611 -0
  308. package/src/release/build-package.js +91 -0
  309. package/src/release/changelog-section.js +70 -0
  310. package/src/release/sync-plugin-versions.js +66 -0
  311. package/src/runtime/advice/cli.js +283 -0
  312. package/src/runtime/advice/client.js +324 -0
  313. package/src/runtime/advice/config.js +189 -0
  314. package/src/runtime/advice/doctor.js +169 -0
  315. package/src/runtime/advice/endpoint-guard.js +134 -0
  316. package/src/runtime/advice/kinds.js +154 -0
  317. package/src/runtime/advice/providers/ollama-systemone.js +209 -0
  318. package/src/runtime/advice/record.js +166 -0
  319. package/src/runtime/advice/registry.js +170 -0
  320. package/src/runtime/advice/strength.js +66 -0
  321. package/src/runtime/brief-tools.js +342 -0
  322. package/src/runtime/c4-project.js +371 -0
  323. package/src/runtime/capability-router.js +484 -0
  324. package/src/runtime/checkouts.js +170 -0
  325. package/src/runtime/claims.js +468 -0
  326. package/src/runtime/cli-result.js +55 -0
  327. package/src/runtime/cli.js +678 -0
  328. package/src/runtime/command-detect.js +516 -0
  329. package/src/runtime/context-pack.js +492 -0
  330. package/src/runtime/decision-register.js +493 -0
  331. package/src/runtime/evidence-ledger.js +338 -0
  332. package/src/runtime/failure/capture.js +272 -0
  333. package/src/runtime/failure/classifier.js +68 -0
  334. package/src/runtime/failure/cli.js +178 -0
  335. package/src/runtime/failure/home.js +64 -0
  336. package/src/runtime/failure/store.js +226 -0
  337. package/src/runtime/feature-resolve.js +79 -0
  338. package/src/runtime/freshness.js +177 -0
  339. package/src/runtime/health.js +863 -0
  340. package/src/runtime/history-compactor.js +237 -0
  341. package/src/runtime/implementation-gate.js +321 -0
  342. package/src/runtime/inventory/coverage.js +218 -0
  343. package/src/runtime/inventory/identity.js +65 -0
  344. package/src/runtime/inventory/index.js +705 -0
  345. package/src/runtime/inventory/precedence.js +134 -0
  346. package/src/runtime/inventory/read-scopes.js +131 -0
  347. package/src/runtime/inventory/siblings.js +192 -0
  348. package/src/runtime/knowledge/chunker.js +124 -0
  349. package/src/runtime/knowledge/hybrid.js +149 -0
  350. package/src/runtime/knowledge/index-store.js +145 -0
  351. package/src/runtime/knowledge/retrieval.js +194 -0
  352. package/src/runtime/leak-scan.js +185 -0
  353. package/src/runtime/lifecycle/cli.js +276 -0
  354. package/src/runtime/lifecycle/event-store.js +340 -0
  355. package/src/runtime/lifecycle/fold.js +353 -0
  356. package/src/runtime/lifecycle/followup.js +432 -0
  357. package/src/runtime/lifecycle/goal.js +249 -0
  358. package/src/runtime/lifecycle/intent-writer.js +86 -0
  359. package/src/runtime/lifecycle/overview.js +292 -0
  360. package/src/runtime/lifecycle/release.js +241 -0
  361. package/src/runtime/lifecycle/report-store.js +221 -0
  362. package/src/runtime/lifecycle/retention.js +212 -0
  363. package/src/runtime/lifecycle/root.js +71 -0
  364. package/src/runtime/lifecycle/status.js +353 -0
  365. package/src/runtime/lifecycle/store-upkeep.js +393 -0
  366. package/src/runtime/locator-resolve.js +158 -0
  367. package/src/runtime/mask.js +210 -0
  368. package/src/runtime/mcp-consultations.js +412 -0
  369. package/src/runtime/mcp-reach.js +157 -0
  370. package/src/runtime/model-router.js +153 -0
  371. package/src/runtime/outcome.js +467 -0
  372. package/src/runtime/reach.js +148 -0
  373. package/src/runtime/readiness-record.js +150 -0
  374. package/src/runtime/readiness.js +435 -0
  375. package/src/runtime/recovery.js +282 -0
  376. package/src/runtime/research-request.js +231 -0
  377. package/src/runtime/retrieval-plan.js +578 -0
  378. package/src/runtime/scaffold/artifacts.js +217 -0
  379. package/src/runtime/scaffold/fingerprint.js +56 -0
  380. package/src/runtime/scaffold/generate.js +911 -0
  381. package/src/runtime/scaffold/languages.js +245 -0
  382. package/src/runtime/storage/cli.js +64 -0
  383. package/src/runtime/storage/engine.js +249 -0
  384. package/src/runtime/storage/registry.js +130 -0
  385. package/src/runtime/storage/roots.js +63 -0
  386. package/src/runtime/storage/rules.js +282 -0
  387. package/src/runtime/storage/upkeep.js +243 -0
  388. package/src/runtime/storage/work-status.js +326 -0
  389. package/src/runtime/task-attempts.js +415 -0
  390. package/src/runtime/task-classifier.js +550 -0
  391. package/src/runtime/task-scope.js +130 -0
  392. package/src/runtime/task-state.js +176 -0
  393. package/src/runtime/trace/indicators.js +235 -0
  394. package/src/runtime/trace/ledger.js +626 -0
  395. package/src/runtime/trace/render.js +76 -0
  396. package/src/runtime/trace/views.js +459 -0
  397. package/src/runtime/verification/contract-runner.js +341 -0
  398. package/src/runtime/verification/engine.js +1461 -0
  399. package/src/runtime/verification/registry.js +130 -0
  400. package/src/runtime/verification/scope-bound.js +418 -0
  401. package/src/runtime/verification/test-selection.js +172 -0
  402. package/src/runtime/workflow-engine.js +588 -0
  403. package/src/runtime/workflow-orchestrator.js +953 -0
  404. package/src/runtime/worktree.js +340 -0
  405. package/src/state/index.js +193 -0
  406. package/src/state/lockfile.js +136 -0
  407. package/src/state/run-lock.js +408 -0
  408. package/types/node-commonjs.d.ts +68 -0
  409. package/types/runtime.d.ts +91 -0
@@ -0,0 +1,493 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ /**
6
+ * Decision register (IC-001 to IC-009). One `decisions/register.json` per feature folder holds
7
+ * every decision taken during the feature's chain; `decisions.md` (live view, one row per live
8
+ * topic) and `decisions/archive.md` (every decision with its rationale) are rendered from it and
9
+ * never edited by hand. The register is owned by this module: no caller sets a decision's
10
+ * `decidedBy`, `date`, `at`, `id` or `status`.
11
+ *
12
+ * Concurrency: every write holds the lock `task-state.js` already provides for its own files, so
13
+ * two handoffs writing at once serialize rather than allocate the same id. Files are written with
14
+ * a temp file and a rename; the register is written before the views, so a crash leaves a register
15
+ * that the next write re-renders into agreement.
16
+ */
17
+
18
+ const nodeFs = require('node:fs');
19
+ const path = require('node:path');
20
+ const { acquireLock } = require('./task-state.js');
21
+ const { resolveActiveFeature } = require('./feature-resolve.js');
22
+ const { compactHistory } = require('./history-compactor.js');
23
+ const { finishRuntime, usageError } = require('./cli-result.js');
24
+
25
+ const REGISTER_VERSION = 1;
26
+ const TOPIC_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
27
+ const TOPIC_MAX = 48;
28
+ const STATEMENT_MAX = 280;
29
+ const REF_RE = /^[A-Za-z]+-?[0-9]+$/;
30
+ const DEC_RE = /^DEC-[0-9]{3,}$/;
31
+ const STAGES = ['discovery', 'design', 'planning', 'implementation', 'verification', 'review'];
32
+ /** Channel -> who decided. The caller names the channel; `decidedBy` follows from it. */
33
+ const CHANNEL_DECIDED_BY = { question: 'user', gate: 'user', prompt: 'user', default: 'agent', resolution: 'agent' };
34
+ const ITEM_KEYS = ['topic', 'statement', 'channel', 'stage', 'rationale', 'supersedes', 'refs', 'source'];
35
+ const REQUIRED_KEYS = ['topic', 'statement', 'channel', 'stage', 'rationale'];
36
+
37
+ /** Line breaks Markdown renderers and editors treat as a break, not just \r and \n. */
38
+ const LINE_BREAK = /[\r\n\u2028\u2029\u0085]/;
39
+
40
+ const GENERATED_NOTE = 'Generated from `decisions/register.json` by `doflow-run decision`. Do not edit this file.';
41
+
42
+ /** A caller mistake (exit 2). */
43
+ class DecisionUsageError extends Error {}
44
+
45
+ /** A finding the caller must act on (exit 1). */
46
+ class DecisionFinding extends Error {
47
+ finding ;
48
+
49
+ constructor(finding , message ) {
50
+ super(message);
51
+ this.finding = finding;
52
+ }
53
+ }
54
+
55
+ // ── paths ──────────────────────────────────────────────────────────────────────────────────────
56
+
57
+ function registerFile(featureDir ) { return path.join(featureDir, 'decisions', 'register.json'); }
58
+ function liveViewFile(featureDir ) { return path.join(featureDir, 'decisions.md'); }
59
+ function archiveFile(featureDir ) { return path.join(featureDir, 'decisions', 'archive.md'); }
60
+
61
+ // ── register io ────────────────────────────────────────────────────────────────────────────────
62
+
63
+ function readRegister(featureDir , fsImpl = nodeFs) {
64
+ const file = registerFile(featureDir);
65
+ if (!fsImpl.existsSync(file)) return null;
66
+ let data ;
67
+ try {
68
+ data = JSON.parse(fsImpl.readFileSync(file, 'utf8'));
69
+ } catch (error ) {
70
+ throw new DecisionUsageError(`${file} is not valid JSON (${error.message}). The register is written atomically, so a torn file means outside interference; restore it from version control.`);
71
+ }
72
+ if (!data || data.version !== REGISTER_VERSION || !Array.isArray(data.decisions) || !Number.isInteger(data.nextId)) {
73
+ throw new DecisionUsageError(`${file} is not a version ${REGISTER_VERSION} decision register; refusing to guess at its shape.`);
74
+ }
75
+ return data;
76
+ }
77
+
78
+ function writeAtomic(fsImpl , file , content ) {
79
+ fsImpl.mkdirSync(path.dirname(file), { recursive: true });
80
+ const tmp = `${file}.${process.pid}.tmp`;
81
+ try {
82
+ fsImpl.writeFileSync(tmp, content, 'utf8');
83
+ fsImpl.renameSync(tmp, file);
84
+ } catch (error ) {
85
+ try { fsImpl.rmSync(tmp, { force: true }); } catch { /* nothing to clean */ }
86
+ throw error;
87
+ }
88
+ }
89
+
90
+ /** Writes only when the content differs, so re-rendering an unchanged view is a no-op. */
91
+ function writeIfChanged(fsImpl , file , content ) {
92
+ if (fsImpl.existsSync(file) && fsImpl.readFileSync(file, 'utf8') === content) return;
93
+ writeAtomic(fsImpl, file, content);
94
+ }
95
+
96
+ function writeRegister(fsImpl , featureDir , register ) {
97
+ writeAtomic(fsImpl, registerFile(featureDir), `${JSON.stringify(register, null, 2)}\n`);
98
+ }
99
+
100
+ function renderViews(fsImpl , featureDir , register ) {
101
+ writeIfChanged(fsImpl, liveViewFile(featureDir), renderLiveView(register));
102
+ writeIfChanged(fsImpl, archiveFile(featureDir), renderArchive(register));
103
+ }
104
+
105
+ // ── ordering and ids ───────────────────────────────────────────────────────────────────────────
106
+
107
+ function idNumber(id ) { return Number(id.slice(4)); }
108
+ function formatId(n ) { return `DEC-${String(n).padStart(3, '0')}`; }
109
+ function byId(a , b ) { return idNumber(a.id) - idNumber(b.id); }
110
+ function byTopic(a , b ) { return a.topic < b.topic ? -1 : a.topic > b.topic ? 1 : idNumber(a.id) - idNumber(b.id); }
111
+ function liveOf(register ) { return register.decisions.filter((d ) => d.status === 'live'); }
112
+
113
+ // ── renderers ──────────────────────────────────────────────────────────────────────────────────
114
+
115
+ /** `\` is escaped first, so a statement ending in one cannot escape the pipe that closes the cell
116
+ * (IC-007); `|` would end the cell; `<` could open an HTML comment that hides every later row. */
117
+ function cell(text ) { return String(text).replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/</g, '\\<'); }
118
+
119
+ /** Archive text is read by people and agents, never as HTML: `<` is escaped so `<!--` cannot hide later entries. */
120
+ function plain(text ) { return String(text).replace(/</g, '\\<'); }
121
+
122
+ /**
123
+ * A one-line rationale reads inline. A longer one goes in a blockquote under the bullet, every line
124
+ * prefixed, so no line of it can start a heading or pose as one of the entry's own bullets.
125
+ */
126
+ function rationaleLines(rationale ) {
127
+ const parts = plain(rationale).split(/\r\n|[\n\r\u2028\u2029\u0085]/);
128
+ if (parts.length === 1) return [`- **Rationale:** ${parts[0]}`];
129
+ return ['- **Rationale:**', ...parts.map((l ) => (l === '' ? ' >' : ` > ${l}`))];
130
+ }
131
+
132
+ /** IC-007. Size depends on the number of live topics, never on how many decisions were made. */
133
+ function renderLiveView(register ) {
134
+ const live = liveOf(register).sort(byTopic);
135
+ const out = [
136
+ `# Decisions: ${register.slug}`,
137
+ '',
138
+ `> ${GENERATED_NOTE} Superseded decisions and rationale are in \`decisions/archive.md\`.`,
139
+ '',
140
+ ];
141
+ if (live.length === 0) {
142
+ out.push('No live decisions yet.');
143
+ } else {
144
+ out.push('| ID | Topic | Decision | By | Stage | Date |', '|---|---|---|---|---|---|');
145
+ for (const d of live) {
146
+ out.push(`| ${d.id} | ${cell(d.topic)} | ${cell(d.statement)} | ${d.decidedBy} | ${d.stage} | ${d.date} |`);
147
+ }
148
+ }
149
+ return `${out.join('\n')}\n`;
150
+ }
151
+
152
+ /** IC-008. One entry per decision, by id; superseded entries stay with their rationale. */
153
+ function renderArchive(register ) {
154
+ const out = [`# Decision archive: ${register.slug}`, '', `> ${GENERATED_NOTE}`];
155
+ for (const d of [...register.decisions].sort(byId)) {
156
+ out.push('', `### ${d.id}: ${d.topic}`, '');
157
+ out.push(`- **Status:** ${d.status === 'live' ? 'Live' : `Superseded → ${d.supersededBy}`}`);
158
+ out.push(`- **Decision:** ${plain(d.statement)}`);
159
+ out.push(`- **By:** ${d.decidedBy} (${d.channel}) · **Stage:** ${d.stage} · **Date:** ${d.date}`);
160
+ if (d.supersedes.length) out.push(`- **Supersedes:** ${d.supersedes.join(', ')}`);
161
+ if (d.refs.length) out.push(`- **Refs:** ${d.refs.join(', ')}`);
162
+ if (d.source) out.push(`- **Source:** ${plain(d.source)}`);
163
+ out.push(...rationaleLines(d.rationale));
164
+ }
165
+ return `${out.join('\n')}\n`;
166
+ }
167
+
168
+ // ── item validation ────────────────────────────────────────────────────────────────────────────
169
+
170
+ function toList(value , what , label ) {
171
+ if (value === undefined || value === null || value === '') return [];
172
+ let list ;
173
+ if (Array.isArray(value)) list = value;
174
+ else if (typeof value === 'string') list = value.split(',');
175
+ else throw new DecisionUsageError(`${label}: ${what} must be an array of strings`);
176
+ const out = [];
177
+ for (const entry of list) {
178
+ if (typeof entry !== 'string') throw new DecisionUsageError(`${label}: ${what} must be an array of strings`);
179
+ const trimmed = entry.trim();
180
+ if (trimmed) out.push(trimmed);
181
+ }
182
+ return out;
183
+ }
184
+
185
+ /** Validates one add item against IC-003's field rules. Throws DecisionUsageError. */
186
+ function normalizeItem(raw , label ) {
187
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new DecisionUsageError(`${label}: must be an object`);
188
+ const extra = Object.keys(raw).filter((k ) => !ITEM_KEYS.includes(k));
189
+ if (extra.length) {
190
+ throw new DecisionUsageError(`${label}: unknown key${extra.length > 1 ? 's' : ''} ${extra.map((k ) => `'${k}'`).join(', ')}. Allowed: ${ITEM_KEYS.join(', ')} (decidedBy is set by the runtime from the channel)`);
191
+ }
192
+ for (const key of REQUIRED_KEYS) {
193
+ if (typeof raw[key] !== 'string' || raw[key].trim() === '') throw new DecisionUsageError(`${label}: '${key}' is required`);
194
+ }
195
+ const topic = raw.topic.trim();
196
+ if (LINE_BREAK.test(topic)) throw new DecisionUsageError(`${label}: topic must be one line`);
197
+ if (topic.length > TOPIC_MAX || !TOPIC_RE.test(topic)) {
198
+ throw new DecisionUsageError(`${label}: topic '${topic}' must be lowercase words joined by hyphens, at most ${TOPIC_MAX} characters`);
199
+ }
200
+ const statement = raw.statement.trim();
201
+ if (LINE_BREAK.test(statement)) throw new DecisionUsageError(`${label}: statement must be one line`);
202
+ if (statement.length > STATEMENT_MAX) {
203
+ throw new DecisionUsageError(`${label}: statement is ${statement.length} characters; the limit is ${STATEMENT_MAX}. Put the reasoning in --rationale`);
204
+ }
205
+ const channel = raw.channel.trim();
206
+ if (!Object.prototype.hasOwnProperty.call(CHANNEL_DECIDED_BY, channel)) {
207
+ throw new DecisionUsageError(`${label}: channel '${channel}' must be one of ${Object.keys(CHANNEL_DECIDED_BY).join(', ')}`);
208
+ }
209
+ const stage = raw.stage.trim();
210
+ if (!STAGES.includes(stage)) throw new DecisionUsageError(`${label}: stage '${stage}' must be one of ${STAGES.join(', ')}`);
211
+ const supersedes = toList(raw.supersedes, 'supersedes', label);
212
+ for (const id of supersedes) {
213
+ if (!DEC_RE.test(id)) throw new DecisionUsageError(`${label}: supersedes entry '${id}' is not a DEC-### id`);
214
+ }
215
+ if (new Set(supersedes).size !== supersedes.length) throw new DecisionUsageError(`${label}: supersedes names an id twice`);
216
+ const refs = toList(raw.refs, 'refs', label);
217
+ for (const ref of refs) {
218
+ if (!REF_RE.test(ref)) throw new DecisionUsageError(`${label}: refs entry '${ref}' does not look like an id (for example FR-001)`);
219
+ }
220
+ let source = '';
221
+ if (raw.source !== undefined && raw.source !== null) {
222
+ if (typeof raw.source !== 'string' || LINE_BREAK.test(raw.source)) throw new DecisionUsageError(`${label}: source must be a one-line string`);
223
+ source = raw.source.trim();
224
+ }
225
+ return { topic, statement, channel, stage, rationale: raw.rationale.trim(), supersedes, refs, source };
226
+ }
227
+
228
+ /** Normalizes the batch (`--batch` contents) or the single item, refusing everything on one fault. */
229
+ function normalizeItems(items ) {
230
+ if (!Array.isArray(items)) throw new DecisionUsageError('a batch must be a JSON array of decision objects');
231
+ const normalized = items.map((raw , i ) => normalizeItem(raw, `item ${i + 1}`));
232
+ const seen = new Set();
233
+ for (const item of normalized) {
234
+ if (seen.has(item.topic)) throw new DecisionUsageError(`topic '${item.topic}' appears more than once in the batch; a topic takes one decision per batch`);
235
+ seen.add(item.topic);
236
+ }
237
+ return normalized;
238
+ }
239
+
240
+ // ── actions ────────────────────────────────────────────────────────────────────────────────────
241
+
242
+ function summary({ action, slug, featureDir } ) { return { action, slug, featureDir }; }
243
+
244
+ const FOLDER_ARTIFACTS = ['intention/requirement.md', 'requirement.md', 'design/design.md', 'design.md', 'plan.md'];
245
+
246
+ /** IC-002. */
247
+ function initRegister({ featureDir, slug, fsImpl = nodeFs } ) {
248
+ const base = summary({ action: 'init', slug, featureDir });
249
+ if (fsImpl.existsSync(registerFile(featureDir))) return { ...base, created: false };
250
+ const present = FOLDER_ARTIFACTS.filter((rel ) => fsImpl.existsSync(path.join(featureDir, rel)));
251
+ if (present.length) {
252
+ return {
253
+ ...base,
254
+ created: false,
255
+ finding: 'predates-register',
256
+ message: `${featureDir} already holds ${present.join(', ')} and no decision register; only a folder do-brainstorm is creating gets one, so it was left unchanged`,
257
+ };
258
+ }
259
+ fsImpl.mkdirSync(path.join(featureDir, 'decisions'), { recursive: true });
260
+ const release = acquireLock(fsImpl, registerFile(featureDir));
261
+ try {
262
+ if (fsImpl.existsSync(registerFile(featureDir))) return { ...base, created: false };
263
+ const register = { version: REGISTER_VERSION, slug, nextId: 1, decisions: [] };
264
+ writeRegister(fsImpl, featureDir, register);
265
+ renderViews(fsImpl, featureDir, register);
266
+ } finally {
267
+ release();
268
+ }
269
+ return { ...base, created: true };
270
+ }
271
+
272
+ /**
273
+ * IC-003. All-or-nothing: the whole batch is validated and simulated against the register first,
274
+ * and nothing is written unless every item passes.
275
+ */
276
+ function addDecisions({ featureDir, slug, items, now = new Date(), fsImpl = nodeFs } ) {
277
+ const normalized = normalizeItems(items);
278
+ const base = summary({ action: 'add', slug, featureDir });
279
+ if (!fsImpl.existsSync(registerFile(featureDir))) {
280
+ return { ...base, finding: 'no-register', message: `${featureDir} has no decisions/register.json; run \`decision --action init\` when the feature is created` };
281
+ }
282
+ // Nothing to register: answer without taking the lock or rewriting a byte.
283
+ if (normalized.length === 0) return { ...base, added: [], superseded: [] };
284
+ const release = acquireLock(fsImpl, registerFile(featureDir));
285
+ try {
286
+ const register = readRegister(featureDir, fsImpl);
287
+ const iso = now.toISOString();
288
+ const added = [];
289
+ const superseded = [];
290
+ let conflict = null;
291
+ let nextId = register.nextId;
292
+ for (const item of normalized) {
293
+ for (const id of item.supersedes) {
294
+ const target = register.decisions.find((d ) => d.id === id);
295
+ if (!target) throw new DecisionUsageError(`topic '${item.topic}': supersedes ${id}, which is not a decision in this register`);
296
+ if (target.status !== 'live') throw new DecisionUsageError(`topic '${item.topic}': supersedes ${id}, which is not live (it was superseded by ${target.supersededBy})`);
297
+ }
298
+ const unnamed = register.decisions.find((d ) => d.status === 'live' && d.topic === item.topic && !item.supersedes.includes(d.id));
299
+ if (unnamed && !conflict) {
300
+ conflict = { finding: 'topic-conflict', message: `topic '${item.topic}' already has live decision ${unnamed.id}; name it in supersedes to replace it. Nothing was written` };
301
+ }
302
+ const id = formatId(nextId);
303
+ nextId += 1;
304
+ for (const old of item.supersedes) {
305
+ const target = register.decisions.find((d ) => d.id === old);
306
+ target.status = 'superseded';
307
+ target.supersededBy = id;
308
+ superseded.push({ id: old, by: id });
309
+ }
310
+ register.decisions.push({
311
+ id,
312
+ topic: item.topic,
313
+ statement: item.statement,
314
+ decidedBy: CHANNEL_DECIDED_BY[item.channel],
315
+ channel: item.channel,
316
+ stage: item.stage,
317
+ date: iso.slice(0, 10),
318
+ at: iso,
319
+ status: 'live',
320
+ supersedes: item.supersedes,
321
+ supersededBy: null,
322
+ refs: item.refs,
323
+ source: item.source,
324
+ rationale: item.rationale,
325
+ });
326
+ added.push({ id, topic: item.topic });
327
+ }
328
+ if (conflict) return { ...base, ...conflict };
329
+ register.nextId = nextId;
330
+ writeRegister(fsImpl, featureDir, register);
331
+ renderViews(fsImpl, featureDir, register);
332
+ return { ...base, added, superseded };
333
+ } finally {
334
+ release();
335
+ }
336
+ }
337
+
338
+ /** IC-004. */
339
+ function listDecisions({ featureDir, slug, all = false, fsImpl = nodeFs } ) {
340
+ const base = summary({ action: 'list', slug, featureDir });
341
+ const register = readRegister(featureDir, fsImpl);
342
+ if (!register) return { ...base, finding: 'no-register', message: `${featureDir} has no decisions/register.json` };
343
+ const decisions = all ? [...register.decisions].sort(byId) : liveOf(register).sort(byTopic);
344
+ return { ...base, decisions };
345
+ }
346
+
347
+ /** IC-005: compaction plus the view re-render, under the register lock `compactHistory` itself lacks. */
348
+ function compactDecisions({ featureDir, slug, repoRoot, paths, now = new Date(), fsImpl = nodeFs } ) {
349
+ const base = summary({ action: 'compact', slug, featureDir });
350
+ if (!fsImpl.existsSync(registerFile(featureDir))) {
351
+ return { ...base, finding: 'no-register', message: `${featureDir} has no decisions/register.json; nothing was touched` };
352
+ }
353
+ const release = acquireLock(fsImpl, registerFile(featureDir));
354
+ try {
355
+ const register = readRegister(featureDir, fsImpl);
356
+ const result = compactHistory({ repoRoot, featureDir, paths, date: now.toISOString().slice(0, 10), fsImpl });
357
+ // The views are re-rendered when anything moved, including in a partial run: the artifacts
358
+ // that did compact are changed whatever happened to the others.
359
+ if (result.moved.length) renderViews(fsImpl, featureDir, register);
360
+ if (result.failed.length) {
361
+ return {
362
+ ...base, status: result.status, finding: 'compaction-failed',
363
+ message: result.failed.map((f ) => f.message).join('; '),
364
+ artifact: result.failed[0].path, moved: result.moved, failed: result.failed,
365
+ };
366
+ }
367
+ return { ...base, status: result.status, moved: result.moved, failed: [] };
368
+ } finally {
369
+ release();
370
+ }
371
+ }
372
+
373
+ // ── verb handler ───────────────────────────────────────────────────────────────────────────────
374
+
375
+ const VALID_ACTIONS = ['init', 'add', 'list', 'compact'];
376
+
377
+ function readBatchFile(batchPath , fsImpl ) {
378
+ let text ;
379
+ try {
380
+ // `-` is stdin, as for the evidence verb.
381
+ text = batchPath === '-' ? fsImpl.readFileSync(0, 'utf8') : fsImpl.readFileSync(path.resolve(batchPath), 'utf8');
382
+ } catch (error ) {
383
+ throw new DecisionUsageError(`cannot read --batch ${batchPath}: ${error.message}`);
384
+ }
385
+ try {
386
+ return JSON.parse(text);
387
+ } catch (error ) {
388
+ throw new DecisionUsageError(`--batch ${batchPath} is not valid JSON: ${error.message}`);
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Runs one action and returns its result without printing, so other verbs (the handoff's
394
+ * compaction) can call it directly.
395
+ *
396
+ * @param {Object} options
397
+ * @param {string} options.action init | add | list | status | compact
398
+ * @param {string} options.projectRoot directory the feature is resolved from
399
+ * @param {string|null} [options.slug] explicit feature override
400
+ * @param {Object} [options.flags] add: topic, statement, channel, stage, rationale, supersedes,
401
+ * refs, source, batch (path); list: all
402
+ * @returns {{exitCode:number, result?:Object, usage?:string}}
403
+ */
404
+ function runDecision({ action = 'list', projectRoot, slug = null, flags = {}, now = new Date(), fsImpl = nodeFs } ) {
405
+ const act = action === 'status' ? 'list' : action;
406
+ let resolved = null;
407
+ try {
408
+ if (!VALID_ACTIONS.includes(act)) throw new DecisionUsageError(`unknown --action '${action}'. Valid: ${VALID_ACTIONS.join(', ')}`);
409
+ const feature = resolveActiveFeature({ projectRoot, slug });
410
+ if (feature.error) throw new DecisionUsageError(feature.message);
411
+ resolved = { featureDir: feature.featureDir, slug: feature.paths.feature_slug };
412
+ let result ;
413
+ if (act === 'init') {
414
+ result = initRegister({ ...resolved, fsImpl });
415
+ } else if (act === 'list') {
416
+ result = listDecisions({ ...resolved, all: Boolean(flags.all), fsImpl });
417
+ } else if (act === 'compact') {
418
+ result = compactDecisions({ ...resolved, repoRoot: feature.repoRoot, paths: feature.paths, now, fsImpl });
419
+ } else {
420
+ let items ;
421
+ if (flags.batch) {
422
+ const named = ITEM_KEYS.filter((k) => flags[k] !== undefined);
423
+ if (named.length) throw new DecisionUsageError(`--batch cannot be combined with --${named[0]}`);
424
+ items = readBatchFile(flags.batch, fsImpl);
425
+ } else {
426
+ const single = {};
427
+ for (const key of ITEM_KEYS) if (flags[key] !== undefined) single[key] = flags[key];
428
+ for (const key of REQUIRED_KEYS) {
429
+ if (single[key] === undefined) throw new DecisionUsageError(`--${key} is required for --action add (or pass --batch <file.json>)`);
430
+ }
431
+ items = [single];
432
+ }
433
+ result = addDecisions({ ...resolved, items, now, fsImpl });
434
+ }
435
+ return { exitCode: result.finding ? 1 : 0, result };
436
+ } catch (error ) {
437
+ if (error instanceof DecisionUsageError) return { exitCode: 2, usage: error.message };
438
+ // Another writer holds the register lock: a condition to retry, not a mistake in the call.
439
+ if (resolved && error && /^Could not lock/.test(error.message || '')) {
440
+ return { exitCode: 1, result: { action: act, slug: resolved.slug, featureDir: resolved.featureDir, finding: 'register-locked', message: error.message } };
441
+ }
442
+ throw error;
443
+ }
444
+ }
445
+
446
+ function humanLines(result ) {
447
+ const lines = [];
448
+ if (result.finding) lines.push(`${result.finding}: ${result.message}`);
449
+ if (result.action === 'init' && !result.finding) {
450
+ lines.push(result.created ? `created ${path.join(result.featureDir, 'decisions', 'register.json')}` : 'register already exists; nothing written');
451
+ }
452
+ for (const a of (result.added || []) ) lines.push(`added ${a.id} (${a.topic})`);
453
+ for (const s of (result.superseded || []) ) lines.push(`superseded ${s.id} by ${s.by}`);
454
+ if (result.action === 'list' && !result.finding) {
455
+ if (result.decisions.length === 0) lines.push('No decisions.');
456
+ for (const d of result.decisions ) lines.push(`${d.id} ${d.topic} ${d.statement} [${d.decidedBy}, ${d.stage}${d.status === 'live' ? '' : `, superseded by ${d.supersededBy}`}]`);
457
+ }
458
+ if (result.action === 'compact') {
459
+ if (result.status === 'unchanged') lines.push('nothing to compact');
460
+ for (const m of (result.moved || []) ) lines.push(`moved ${m.lines} line${m.lines === 1 ? '' : 's'} from ${m.path} to ${m.archive}`);
461
+ }
462
+ return lines;
463
+ }
464
+
465
+ /**
466
+ * `decision` verb. Exit 0 = answered, 1 = a finding the caller must act on, 2 = usage.
467
+ * `--json` prints the result object unmodified.
468
+ */
469
+ function handleDecisionCommand({ action = 'list', projectRoot, slug = null, flags = {}, json = false } = {}) {
470
+ const run = runDecision({ action, projectRoot: projectRoot || process.cwd(), slug, flags });
471
+ if (run.usage !== undefined) return usageError('decision', run.usage, json);
472
+ if (json) console.log(JSON.stringify(run.result, null, 2));
473
+ else for (const line of humanLines(run.result)) console.log(line);
474
+ return finishRuntime(run.exitCode);
475
+ }
476
+
477
+ module.exports = {
478
+ handleDecisionCommand,
479
+ runDecision,
480
+ initRegister,
481
+ addDecisions,
482
+ listDecisions,
483
+ compactDecisions,
484
+ readRegister,
485
+ renderLiveView,
486
+ renderArchive,
487
+ registerFile,
488
+ liveViewFile,
489
+ archiveFile,
490
+ STAGES,
491
+ CHANNEL_DECIDED_BY,
492
+ STATEMENT_MAX,
493
+ };