@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,237 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ /**
6
+ * History compactor (IC-005, IC-009). Moves the body of an artifact's `## <n>. History` section into
7
+ * `decisions/history/<artifact>.md` and leaves a comment-and-pointer stub behind, so the live
8
+ * artifact stops growing with the feature's age while every History line stays reachable
9
+ * (NFR-004).
10
+ *
11
+ * Mechanical by design (NFR-001): no model is asked to decide what to move. A line moves unless it
12
+ * is blank, part of an HTML comment, the pointer line, or the initial-version literal.
13
+ *
14
+ * Write order per artifact is the safety property (FR-014): append to the archive, read it back and
15
+ * confirm the block is there, and only then rewrite the artifact. A failure at any step leaves the
16
+ * artifact as it was. A crash between the two writes leaves the block in the archive and the
17
+ * artifact unchanged; the next run recognises the identical chunk and does not append it twice.
18
+ */
19
+
20
+ const nodeFs = require('node:fs');
21
+ const path = require('node:path');
22
+
23
+ const HISTORY_HEADING = /^## ([0-9]+)\. History\s*$/;
24
+ const ANY_H2 = /^## /;
25
+ const POINTER = /^Earlier entries: \[decisions\/history\/[a-z-]+\.md\]/;
26
+ const INITIAL_VERSION = 'None — initial version.';
27
+ const TARGET_KEYS = ['requirement', 'design', 'specs', 'data_model', 'plan'];
28
+
29
+ const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/;
30
+
31
+ /**
32
+ * Classifies every line as `text`, `fence` (a fence marker or a line inside a fenced block),
33
+ * `comment` (the first line of an HTML comment) or `comment-cont`. A `## ` line inside a fence or a
34
+ * comment is not a heading, and fence markers inside a comment (or the reverse) do not open
35
+ * anything, so a History example quoted in a code block cannot be mistaken for the real section.
36
+ * @returns {{kinds:string[], openFence:boolean, openComment:boolean, openAt:number}} the middle two
37
+ * are true when a fence or an HTML comment is never closed, so everything after it was swallowed;
38
+ * `openAt` is the line index where that construct opened (-1 when none is left open)
39
+ */
40
+ function scan(lines ) {
41
+ const kinds = new Array(lines.length);
42
+ let fence = null;
43
+ let comment = false;
44
+ let openedAt = -1;
45
+ for (let i = 0; i < lines.length; i += 1) {
46
+ const line = lines[i];
47
+ if (fence) {
48
+ kinds[i] = 'fence';
49
+ const close = /^ {0,3}(`+|~+)\s*$/.exec(line);
50
+ if (close && close[1][0] === fence.ch && close[1].length >= fence.len) fence = null;
51
+ continue;
52
+ }
53
+ if (comment) {
54
+ kinds[i] = 'comment-cont';
55
+ if (line.includes('-->')) comment = false;
56
+ continue;
57
+ }
58
+ const open = FENCE_OPEN.exec(line);
59
+ if (open) { kinds[i] = 'fence'; fence = { ch: open[1][0], len: open[1].length }; openedAt = i; continue; }
60
+ const trimmed = line.trim();
61
+ if (trimmed.startsWith('<!--')) {
62
+ kinds[i] = 'comment';
63
+ if (!trimmed.includes('-->', 4)) { comment = true; openedAt = i; }
64
+ continue;
65
+ }
66
+ kinds[i] = 'text';
67
+ }
68
+ return { kinds, openFence: fence !== null, openComment: comment, openAt: fence !== null || comment ? openedAt : -1 };
69
+ }
70
+
71
+ /**
72
+ * Splits History body lines into what stays and what moves. Fenced content moves intact. Only the
73
+ * exact canonical pointer for this artifact is the pointer; any other line, including one that
74
+ * merely looks like a pointer, moves, so no text is dropped.
75
+ */
76
+ function classify(bodyLines , kinds , canonicalPointer ) {
77
+ const comments = [];
78
+ const moved = [];
79
+ let pointer = null;
80
+ let block = null;
81
+ bodyLines.forEach((line, i) => {
82
+ const kind = kinds[i];
83
+ if (kind === 'comment-cont') { (block ).push(line); return; }
84
+ block = null;
85
+ if (kind === 'comment') { block = [line]; comments.push(block); return; }
86
+ if (kind === 'fence') { moved.push(line); return; }
87
+ const trimmed = line.trim();
88
+ if (trimmed === '') return;
89
+ if (trimmed === canonicalPointer) { if (pointer === null) pointer = line; return; }
90
+ if (trimmed === INITIAL_VERSION) return;
91
+ moved.push(line);
92
+ });
93
+ return { comments, moved, pointer };
94
+ }
95
+
96
+ function writeAtomic(fsImpl , file , content ) {
97
+ fsImpl.mkdirSync(path.dirname(file), { recursive: true });
98
+ const tmp = `${file}.${process.pid}.tmp`;
99
+ try {
100
+ fsImpl.writeFileSync(tmp, content, 'utf8');
101
+ fsImpl.renameSync(tmp, file);
102
+ } catch (error ) {
103
+ try { fsImpl.rmSync(tmp, { force: true }); } catch { /* nothing to clean */ }
104
+ throw error;
105
+ }
106
+ }
107
+
108
+ function toPosix(p ) {
109
+ return p.split(path.sep).join('/');
110
+ }
111
+
112
+ /**
113
+ * Whether the archive's most recent chunk is exactly this block. Only the last chunk counts: the
114
+ * crash this guards against (archive written, artifact not) always leaves the interrupted chunk
115
+ * last. The dated header is ignored, so a recovery on a later day does not append a second copy,
116
+ * and a block that merely prefixes an earlier chunk does not match.
117
+ *
118
+ * A chunk header is a `## Compacted` line `scan()` classifies as text: a moved block may quote such
119
+ * a line inside a code fence, and that must not be read as the start of a chunk. The dedupe and the
120
+ * post-write confirmation both use this one predicate.
121
+ */
122
+ function lastChunkIs(archiveText , blockLF ) {
123
+ const lines = archiveText.replace(/\r\n/g, '\n').split('\n');
124
+ const { kinds } = scan(lines);
125
+ let header = -1;
126
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
127
+ if (kinds[i] === 'text' && lines[i].startsWith('## Compacted ')) { header = i; break; }
128
+ }
129
+ if (header === -1 || lines[header + 1] !== '') return false;
130
+ return lines.slice(header + 2).join('\n').replace(/\n+$/, '') === blockLF;
131
+ }
132
+
133
+ /**
134
+ * Compacts one artifact.
135
+ * @returns {{artifact:string, path:string, archive:string, lines:number}|null} null when there was
136
+ * nothing to move
137
+ */
138
+ function compactArtifact({ fsImpl, featureDir, file, date } ) {
139
+ const text = fsImpl.readFileSync(file, 'utf8');
140
+ const crlf = text.includes('\r\n');
141
+ const lines = text.split('\n');
142
+ const { kinds, openFence, openComment, openAt } = scan(lines);
143
+ const unclosed = openFence ? 'a code fence' : openComment ? 'an HTML comment' : null;
144
+ const refuse = () => {
145
+ throw new Error(`${unclosed} is never closed, so the end of the History section cannot be told`);
146
+ };
147
+ const start = lines.findIndex((l , i ) => kinds[i] === 'text' && HISTORY_HEADING.test(l));
148
+ // An unclosed construct swallows everything after it. It matters only when it opens before the
149
+ // History section ends (FR-001): then the section's end cannot be told. A History heading it
150
+ // swallowed is such a case; one that simply is not there, or that closed before the construct
151
+ // opened, is not, and the construct is treated as absent.
152
+ if (start === -1) {
153
+ if (unclosed && lines.some((l , i ) => i > openAt && HISTORY_HEADING.test(l))) refuse();
154
+ return null;
155
+ }
156
+ const section = Number((HISTORY_HEADING.exec(lines[start]) )[1]);
157
+ let end = lines.length;
158
+ for (let i = start + 1; i < lines.length; i += 1) {
159
+ if (kinds[i] === 'text' && ANY_H2.test(lines[i])) { end = i; break; }
160
+ }
161
+ if (unclosed && openAt < end) refuse();
162
+ const name = path.basename(file);
163
+ const archiveFile = path.join(featureDir, 'decisions', 'history', name);
164
+ const link = toPosix(path.relative(path.dirname(file), archiveFile));
165
+ const canonicalPointer = `Earlier entries: [decisions/history/${name}](${link}).`;
166
+ const { comments, moved, pointer } = classify(lines.slice(start + 1, end), kinds.slice(start + 1, end), canonicalPointer);
167
+ if (moved.length === 0) return null;
168
+
169
+ const rel = toPosix(path.relative(featureDir, file));
170
+ const archiveRel = toPosix(path.relative(featureDir, archiveFile));
171
+
172
+ // Chunks are built with LF and converted to the artifact's own line ending on write, so a CRLF
173
+ // artifact gets a CRLF archive and no file ends up mixed.
174
+ const eol = crlf ? '\r\n' : '\n';
175
+ const toEol = (t ) => (crlf ? t.replace(/\n/g, '\r\n') : t);
176
+ const blockLF = moved.map((l) => l.replace(/\r$/, '')).join('\n');
177
+ const chunk = `## Compacted ${date} from ${rel} §${section}\n\n${blockLF}\n`;
178
+ const existing = fsImpl.existsSync(archiveFile) ? fsImpl.readFileSync(archiveFile, 'utf8') : null;
179
+ if (existing === null || !lastChunkIs(existing, blockLF)) {
180
+ const base = existing === null ? toEol(`# History archive: ${rel}\n`) : (existing.endsWith('\n') ? existing : `${existing}${eol}`);
181
+ writeAtomic(fsImpl, archiveFile, `${base}${eol}${toEol(chunk)}`);
182
+ }
183
+ if (!lastChunkIs(fsImpl.readFileSync(archiveFile, 'utf8'), blockLF)) {
184
+ throw new Error(`the moved block is not present in ${archiveRel} after the write`);
185
+ }
186
+
187
+ const blank = crlf ? '\r' : '';
188
+ const pointerLine = pointer !== null ? pointer : `${canonicalPointer}${blank}`;
189
+ const body = [blank];
190
+ for (const c of comments) body.push(...c, blank);
191
+ body.push(pointerLine, end === lines.length ? '' : blank);
192
+ writeAtomic(fsImpl, file, [...lines.slice(0, start + 1), ...body, ...lines.slice(end)].join('\n'));
193
+ return { artifact: name, path: rel, archive: archiveRel, lines: moved.length };
194
+ }
195
+
196
+ /**
197
+ * Compacts every existing artifact among requirement, design, specs, data model and plan.
198
+ *
199
+ * @param {Object} options
200
+ * @param {string} options.repoRoot repository root the resolver's paths are relative to
201
+ * @param {string} options.featureDir absolute feature folder
202
+ * @param {Object} options.paths the resolver's JSON (`requirement`, `design`, `specs`,
203
+ * `data_model`, `plan`, repo-root-relative or null)
204
+ * @param {string} [options.date] YYYY-MM-DD stamped on the archive chunk; defaults to today (UTC)
205
+ * @param {Object} [options.fsImpl] fs implementation (tests substitute one)
206
+ *
207
+ * Not locked: concurrent callers can interleave archive appends. Call it through
208
+ * `compactDecisions` / `runDecision` in decision-register.js, which hold the register lock.
209
+ *
210
+ * Every artifact is attempted. One that cannot be compacted is left as it was and reported in
211
+ * `failed`; it does not stop the others (IC-005).
212
+ *
213
+ * @returns {{status:'compacted'|'unchanged'|'partial', moved:Object[], failed:Array<{artifact:string, path:string, message:string}>}}
214
+ * `partial` means at least one artifact failed
215
+ */
216
+ function compactHistory({ repoRoot, featureDir, paths, date, fsImpl = nodeFs } ) {
217
+ const stamp = date || new Date().toISOString().slice(0, 10);
218
+ const moved = [];
219
+ const failed = [];
220
+ for (const key of TARGET_KEYS) {
221
+ if (!paths || !paths[key]) continue;
222
+ const file = path.resolve(repoRoot, paths[key]);
223
+ if (!fsImpl.existsSync(file)) continue;
224
+ let result ;
225
+ try {
226
+ result = compactArtifact({ fsImpl, featureDir, file, date: stamp });
227
+ } catch (error ) {
228
+ const name = toPosix(path.relative(featureDir, file));
229
+ failed.push({ artifact: path.basename(file), path: name, message: `could not compact ${name}: ${error.message}; the artifact was left unchanged` });
230
+ continue;
231
+ }
232
+ if (result) moved.push(result);
233
+ }
234
+ return { status: failed.length ? 'partial' : moved.length ? 'compacted' : 'unchanged', moved, failed };
235
+ }
236
+
237
+ module.exports = { compactHistory, HISTORY_HEADING, POINTER, INITIAL_VERSION };
@@ -0,0 +1,321 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const { readReadinessRecord, newerVersionOf, RECORD_VERSION } = require('./readiness-record.js');
8
+ const { listCheckouts } = require('./checkouts.js');
9
+ const { readTaskState } = require('./task-state.js');
10
+ const { resolveActiveFeature } = require('./feature-resolve.js');
11
+ const { isSafeSlug } = require('./task-scope.js');
12
+ const { REPO_ROOT } = require('../helper/repo-root.js');
13
+
14
+ // The implementation gate: which stage of a workflow needs a READY readiness record before it is
15
+ // handed off, whether a task has one, and the words every refusal uses.
16
+ //
17
+ // The held set is read from the workflow registry, never written here: the gated stage is the
18
+ // first one that mutates source and carries a readiness template. The texts live here once, so the
19
+ // orchestrator, `verify` and the edit hook (which prints the same bytes) say the same thing.
20
+
21
+ const GATE = 'doflow gate readiness-before-implementation';
22
+
23
+ /** What clears a record a newer DoFlow wrote; `readiness` here refuses to overwrite it. */
24
+ const NEWER_NEXT = 'run this command with the DoFlow that wrote the record, or upgrade DoFlow here.';
25
+
26
+ /**
27
+ * The first stage that mutates source and carries a readiness template; with `editTime`, the first
28
+ * such stage that has not opted out of the edit-time check.
29
+ * @param {Array<Object>} stages a resolved workflow's stages, or a run's stage nodes
30
+ * @param {{editTime?: boolean}} [options]
31
+ * @returns {{id: string, readinessTemplate: string, editTimeGate: boolean}|null}
32
+ */
33
+ function gatedStage(stages , { editTime = false } = {}) {
34
+ const stage = (stages || []).find((s ) => s && s.mutatesSource === true
35
+ && typeof s.readinessTemplate === 'string' && s.readinessTemplate !== ''
36
+ && (!editTime || s.editTimeGate !== false));
37
+ return stage ? { id: stage.id, readinessTemplate: stage.readinessTemplate, editTimeGate: stage.editTimeGate !== false } : null;
38
+ }
39
+
40
+ /**
41
+ * The classes whose workflow has a gated stage, in registry order.
42
+ * @param {Object} engine a WorkflowEngine
43
+ * @param {{editTime?: boolean}} [options]
44
+ * @returns {Array<string>}
45
+ */
46
+ function heldClasses(engine , { editTime = false } = {}) {
47
+ return engine.listClasses().filter((cls ) => gatedStage(engine.resolveWorkflow(cls).stages, { editTime }) !== null);
48
+ }
49
+
50
+ /** The command that clears every readiness refusal. */
51
+ function nextCommand({ taskId, slug, template } ) {
52
+ const withSlug = slug && slug !== taskId ? ` --slug=${slug}` : '';
53
+ return `doflow-run readiness --task-class ${template} --task-id ${taskId}${withSlug}`;
54
+ }
55
+
56
+ /**
57
+ * The refusal for one check code.
58
+ * @param {'missing'|'not-ready'|'wrong-template'|'unusable'|'ambiguous'} code
59
+ * @param {Object} options
60
+ * @param {string} options.taskId
61
+ * @param {string|null} [options.slug]
62
+ * @param {string} options.template the template the gated stage needs
63
+ * @param {Object|null} [options.record] the record read, for `not-ready` and `wrong-template`
64
+ * @param {Array<string>} [options.candidates] the files found, for `ambiguous`
65
+ * @param {string|null} [options.detail] why the record cannot be used, for `unusable`
66
+ * @param {string|null} [options.file] the record's file, for `unusable` when a newer DoFlow wrote it
67
+ * @param {number|null} [options.newerVersion] that newer record version; `readiness` would refuse to
68
+ * replace such a record, so the text does not send the caller there
69
+ * @returns {string}
70
+ */
71
+ function refusalText(code , { taskId, slug = null, template, record = null, candidates = [], detail = null, file = null, newerVersion = null } ) {
72
+ const next = nextCommand({ taskId, slug, template });
73
+ const tail = `Next: ${next}, then gather what it lists until it reports READY. Nothing was changed.`;
74
+ if (code === 'unusable' && newerVersion !== null) {
75
+ return `${GATE}: task '${taskId}' has a readiness record ${file} written by a newer DoFlow (record version ${newerVersion}; this runtime reads ${RECORD_VERSION}), `
76
+ + `so this runtime cannot read it and readiness cannot replace it. Next: ${NEWER_NEXT} Nothing was changed.`;
77
+ }
78
+ switch (code) {
79
+ case 'missing':
80
+ return `${GATE}: task '${taskId}' has no readiness record for the '${template}' template. ${tail}`;
81
+ case 'not-ready':
82
+ return `${GATE}: task '${taskId}' was last evaluated ${record.state} at ${record.evaluatedAt} against the '${template}' template, not READY. ${tail}`;
83
+ case 'wrong-template':
84
+ return `${GATE}: task '${taskId}' has a READY record for the '${record.taskClass}' template, and this stage needs '${template}'. ${tail}`;
85
+ case 'unusable':
86
+ return `${GATE}: task '${taskId}' has a readiness record that cannot be used (${detail}). ${tail}`;
87
+ case 'ambiguous':
88
+ return `${GATE}: task '${taskId}' has records in more than one other checkout (${candidates.join(', ')}). Next: run the command from the checkout that holds the one you mean, or run ${next} here. Nothing was changed.`;
89
+ case 'ambiguous-run':
90
+ // A readiness record written here cannot say which run is meant, so it is not offered.
91
+ return `${GATE}: task '${taskId}' has runs in more than one other checkout (${candidates.join(', ')}). Next: run verify from the checkout that holds the run you mean. Nothing was changed.`;
92
+ default:
93
+ throw new Error(`no readiness refusal text for code '${code}'`);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Which harnesses run this gate before each source edit and which meet it first at handoff, from the
99
+ * gate's own mapping in lifecycle.json (`pre-implementation-gate`): `supported`, or `different`
100
+ * with no prerequisite, runs it; a prerequisite (such as Codex's hook review) is named, since until
101
+ * it is met nothing was checked; `unavailable` has no hook layer for it. Empty when the registry
102
+ * cannot be read.
103
+ * @param {{repoRoot?: string}} [options]
104
+ * @returns {string}
105
+ */
106
+ function harnessHookNote({ repoRoot = REPO_ROOT } = {}) {
107
+ try {
108
+ const { policies } = JSON.parse(fs.readFileSync(path.join(repoRoot, 'core', 'registry', 'lifecycle.json'), 'utf8'));
109
+ const { mappings } = policies.find((p ) => p.id === 'pre-implementation-gate');
110
+ const runs = [];
111
+ const gated = new Map();
112
+ const none = [];
113
+ for (const [id, m] of (Object.entries(mappings) )) {
114
+ const prerequisites = Array.isArray(m.prerequisites) ? m.prerequisites : [];
115
+ if (m.status === 'unavailable' || (m.status !== 'supported' && m.status !== 'different')) none.push(id);
116
+ else if (prerequisites.length === 0) runs.push(id);
117
+ else {
118
+ const key = prerequisites.join(' and ');
119
+ gated.set(key, [...(gated.get(key) || []), id]);
120
+ }
121
+ }
122
+ const parts = [];
123
+ if (runs.length) parts.push(`${runs.join(', ')} ${runs.length === 1 ? 'runs' : 'run'} this check before each source edit`);
124
+ for (const [key, ids] of gated) parts.push(`${ids.join(', ')} ${ids.length === 1 ? 'runs' : 'run'} it only after ${key}`);
125
+ if (none.length) parts.push(`${none.join(', ')} ${none.length === 1 ? 'has' : 'have'} no hook layer, so this refusal is ${none.length === 1 ? 'its' : 'their'} first check`);
126
+ return `Edit-time check: ${parts.join('; ')}.`;
127
+ } catch {
128
+ return '';
129
+ }
130
+ }
131
+
132
+ const isDir = (dir ) => {
133
+ try {
134
+ return fs.statSync(dir).isDirectory();
135
+ } catch {
136
+ return false;
137
+ }
138
+ };
139
+ const folderOf = (root , slug ) => path.join(root, 'agent-docs', 'doflow', slug);
140
+
141
+ /** The branch's feature slug as the resolver names it, or null. */
142
+ function branchFeatureSlug(cwd ) {
143
+ try {
144
+ const found = resolveActiveFeature({ projectRoot: cwd });
145
+ const s = !found.error && found.paths ? found.paths.feature_slug : null;
146
+ return isSafeSlug(s) ? s : null;
147
+ } catch {
148
+ return null;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * The feature a task belongs to, as seen from `projectRoot`: `--slug`, else the task id when it
154
+ * names a feature folder there, else the branch's feature.
155
+ * @param {{projectRoot: string, taskId?: string|null, slug?: string|null}} options
156
+ * @returns {string|null}
157
+ */
158
+ function featureSlugFor({ projectRoot, taskId = null, slug = null } ) {
159
+ if (slug) return slug;
160
+ if (isSafeSlug(taskId) && isDir(folderOf(projectRoot, taskId))) return taskId;
161
+ return branchFeatureSlug(projectRoot);
162
+ }
163
+
164
+ /** The feature a run belongs to: the slug recorded when it started, else its task id when that names
165
+ * a feature folder in the checkout holding the run. */
166
+ function runSlugOf(run , root ) {
167
+ if (run && typeof run.featureSlug === 'string' && run.featureSlug !== '') return run.featureSlug;
168
+ return run && isSafeSlug(run.taskId) && isDir(folderOf(root, run.taskId)) ? run.taskId : null;
169
+ }
170
+
171
+ /**
172
+ * A task's run file: in this checkout, else in exactly one other checkout whose run belongs to the
173
+ * same feature. Task ids repeat across features, so another checkout's run with the same id but
174
+ * another feature (or none) is not this task's and is never read or updated from here.
175
+ * @param {{stateRoot: string, taskId: string, slug: string|null, exec?: Function}} options
176
+ * @returns {{status: 'found'|'missing'|'ambiguous', file: string|null, root: string|null,
177
+ * origin: 'current'|'other'|null, candidates: Array<string>}}
178
+ */
179
+ function findRunFile({ stateRoot, taskId, slug, exec } ) {
180
+ const rel = path.join('.doflow', 'state', 'orchestration', `${taskId}.json`);
181
+ const missing = { status: 'missing', file: null, root: null, origin: null, candidates: [] };
182
+ if (fs.existsSync(path.join(stateRoot, rel))) {
183
+ return { status: 'found', file: path.join(stateRoot, rel), root: stateRoot, origin: 'current', candidates: [] };
184
+ }
185
+ if (!slug) return missing;
186
+ const checkouts = listCheckouts({ cwd: stateRoot, ...(exec ? { exec } : {}) });
187
+ if (!checkouts.ok || checkouts.sandbox) return missing;
188
+ const matches = checkouts.others.filter((root ) => {
189
+ const file = path.join(root, rel);
190
+ if (!fs.existsSync(file)) return false;
191
+ try {
192
+ return runSlugOf(JSON.parse(fs.readFileSync(file, 'utf8')), root) === slug;
193
+ } catch {
194
+ return false;
195
+ }
196
+ });
197
+ if (matches.length === 0) return missing;
198
+ if (matches.length > 1) return { ...missing, status: 'ambiguous', candidates: matches.map((root ) => path.join(root, rel)) };
199
+ return { status: 'found', file: path.join(matches[0], rel), root: matches[0], origin: 'other', candidates: [] };
200
+ }
201
+
202
+ /** The slug a refusal's next command names: one whose feature folder exists in this checkout or in
203
+ * the main checkout, which is when `readiness --slug` resolves it the same way the edit hook does. */
204
+ function nextSlugFor({ stateRoot, taskId, slug, exec } ) {
205
+ if (!slug || slug === taskId) return null;
206
+ if (isDir(folderOf(stateRoot, slug))) return slug;
207
+ const checkouts = listCheckouts({ cwd: stateRoot, ...(exec ? { exec } : {}) });
208
+ return checkouts.main && isDir(folderOf(checkouts.main, slug)) ? slug : null;
209
+ }
210
+
211
+ /** Why a record cannot be read as an evaluation, or null when it can. */
212
+ function unusableDetail(record , now ) {
213
+ if (!record || typeof record !== 'object' || Array.isArray(record)) return 'not a JSON object';
214
+ if (record.version !== RECORD_VERSION) return `version ${JSON.stringify(record.version)}, this runtime reads ${RECORD_VERSION}`;
215
+ for (const key of ['state', 'taskClass', 'evaluatedAt']) {
216
+ if (typeof record[key] !== 'string' || record[key] === '') return `no ${key}`;
217
+ }
218
+ const at = Date.parse(record.evaluatedAt);
219
+ if (Number.isNaN(at)) return `evaluatedAt '${record.evaluatedAt}' is not a time`;
220
+ if (at > now.getTime()) return `evaluated at ${record.evaluatedAt}, after this check at ${now.toISOString()}`;
221
+ return null;
222
+ }
223
+
224
+ /**
225
+ * Whether the task holds a READY record of `template` made no later than `now`.
226
+ * @param {Object} options
227
+ * @param {string} options.stateRoot
228
+ * @param {string} options.taskId
229
+ * @param {string|null} [options.slug] the task's feature slug: the record's namespace, and named in
230
+ * the next command when its folder exists here or in the main checkout
231
+ * @param {string} options.template
232
+ * @param {Date} [options.now]
233
+ * @param {Function} [options.exec]
234
+ * @returns {{ok: boolean, code: 'ready'|'missing'|'not-ready'|'wrong-template'|'unusable'|'ambiguous',
235
+ * record: Object|null, file: string|null, origin: string|null, candidates: Array<string>,
236
+ * detail: string|null, nextSlug: string|null, message: string|null}}
237
+ */
238
+ function checkReadiness({ stateRoot, taskId, slug = null, template, now = new Date(), exec } ) {
239
+ const read = readReadinessRecord({ stateRoot, taskId, slug, exec });
240
+ const newerVersion = read.status === 'found' ? newerVersionOf(read.record) : null;
241
+ const nextSlug = nextSlugFor({ stateRoot, taskId, slug, exec });
242
+ const result = (code , detail = null) => ({
243
+ ok: code === 'ready',
244
+ code,
245
+ record: read.record,
246
+ file: read.file,
247
+ origin: read.origin,
248
+ candidates: read.candidates,
249
+ detail,
250
+ newerVersion,
251
+ nextSlug,
252
+ message: code === 'ready' ? null : refusalText(code, {
253
+ taskId, slug: nextSlug, template, record: read.record, candidates: read.candidates, detail, file: read.file, newerVersion,
254
+ }),
255
+ });
256
+ if (read.status === 'ambiguous') return result('ambiguous');
257
+ if (read.status === 'missing') return result('missing');
258
+ const detail = read.status === 'unreadable' ? read.detail : unusableDetail(read.record, now);
259
+ if (detail) return result('unusable', detail);
260
+ if (read.record.state !== 'READY') return result('not-ready');
261
+ if (read.record.taskClass !== template) return result('wrong-template');
262
+ return result('ready');
263
+ }
264
+
265
+ /**
266
+ * `verify`'s readiness check: whether a run for the task, or for its feature slug, is open with its
267
+ * gated stage pending, and if so whether a READY record exists. Reads only.
268
+ * @param {Object} options
269
+ * @param {string} options.stateRoot
270
+ * @param {string} options.taskId
271
+ * @param {string|null} [options.slug] the feature slug, for a run keyed by it and for the record's namespace
272
+ * @param {Date} [options.now]
273
+ * @param {Function} [options.exec]
274
+ * @returns {Object} `{applies: false, reason}` or `{applies: true, runTaskId, stage, template, ok, code,
275
+ * record, message}`
276
+ */
277
+ function verifyReadinessCheck({ stateRoot, taskId, slug = null, now = new Date(), exec } ) {
278
+ let found = findRunFile({ stateRoot, taskId, slug, exec });
279
+ if (found.status === 'missing' && slug && slug !== taskId) found = findRunFile({ stateRoot, taskId: slug, slug, exec });
280
+ if (found.status === 'ambiguous') {
281
+ return {
282
+ applies: true, runTaskId: null, stage: null, template: null, ok: false, code: 'ambiguous', record: null,
283
+ message: refusalText('ambiguous-run', { taskId, candidates: found.candidates }),
284
+ };
285
+ }
286
+ if (found.status === 'missing') return { applies: false, reason: 'no-run' };
287
+ const run = readTaskState(fs, found.file);
288
+ if (run.state === 'COMPLETED' || run.state === 'REJECTED') return { applies: false, reason: 'run-finished' };
289
+ const stageNodes = (run.program || []).filter((n ) => n.type === 'stage');
290
+ const stage = gatedStage(stageNodes);
291
+ if (!stage) return { applies: false, reason: 'no-gated-stage' };
292
+ const node = stageNodes.find((n ) => n.id === stage.id);
293
+ if (node.status === 'completed' || node.status === 'skipped') return { applies: false, reason: 'gated-stage-done' };
294
+
295
+ const template = stage.readinessTemplate;
296
+ const check = checkReadiness({ stateRoot, taskId: run.taskId, slug, template, now, exec });
297
+ return {
298
+ applies: true,
299
+ runTaskId: run.taskId,
300
+ stage: stage.id,
301
+ template,
302
+ ok: check.ok,
303
+ code: check.code,
304
+ record: check.record ? {
305
+ file: check.file, origin: check.origin, state: check.record.state ?? null, taskClass: check.record.taskClass ?? null, evaluatedAt: check.record.evaluatedAt ?? null,
306
+ } : null,
307
+ message: check.message,
308
+ };
309
+ }
310
+
311
+ module.exports = {
312
+ gatedStage,
313
+ heldClasses,
314
+ refusalText,
315
+ harnessHookNote,
316
+ checkReadiness,
317
+ verifyReadinessCheck,
318
+ featureSlugFor,
319
+ runSlugOf,
320
+ findRunFile,
321
+ };