@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,282 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ /**
6
+ * Failure classifier and bounded recovery planner — the JavaScript port of
7
+ * `core/shared/scripts/doflow/recovery/recovery_manager.py` (plan task B.3, component C8).
8
+ *
9
+ * All **eleven** failure classes are kept, against the specification's seven (design R4, plan
10
+ * decision D3 and risk RK6): TOOL_FAILURE, AGENT_FAILURE, BUDGET_EXCEEDED and UNKNOWN_FAILURE are
11
+ * real discriminations the Python already made, and narrowing to match a document would throw away
12
+ * working behaviour. The classification order, every keyword, the strategy table, the
13
+ * agent-switch rule and the three-iteration bound are reproduced exactly.
14
+ *
15
+ * Two defects are fixed rather than reproduced, each explained at its site:
16
+ * 1. `plan_recovery` returned two incompatible record shapes — the abort path omitted `canRetry`
17
+ * and `failureClass`, so the natural caller check `result["canRetry"]` raised on exactly the
18
+ * path that must stop the loop.
19
+ * 2. `classify_failure` assumed its arguments' types; a `None` message or a bare string of check
20
+ * names either raised or silently iterated characters.
21
+ */
22
+
23
+ const { finishRuntime, usageError } = require('./cli-result.js');
24
+
25
+ /** The eleven classes, in the order the Python declared them. */
26
+ const FAILURE_CLASSES = Object.freeze([
27
+ 'CONTEXT_MISSING',
28
+ 'WRONG_IMPLEMENTATION',
29
+ 'TEST_FAILURE',
30
+ 'BUILD_FAILURE',
31
+ 'ARCHITECTURE_FAILURE',
32
+ 'REQUIREMENT_FAILURE',
33
+ 'ENVIRONMENT_FAILURE',
34
+ 'TOOL_FAILURE',
35
+ 'AGENT_FAILURE',
36
+ 'BUDGET_EXCEEDED',
37
+ 'UNKNOWN_FAILURE',
38
+ ]);
39
+
40
+ const DEFAULT_MAX_ITERATIONS = 3;
41
+
42
+ /** Check-name fragments that identify a build-stage failure regardless of the error text. */
43
+ const BUILD_CHECK_MARKERS = Object.freeze(['syntax', 'compile', 'build']);
44
+ /** Check-name fragments that identify a test-stage failure. */
45
+ const TEST_CHECK_MARKERS = Object.freeze(['test', 'unit']);
46
+
47
+ /**
48
+ * Per-class recovery strategy — **one entry per class, all eleven** (plan task C.2).
49
+ *
50
+ * The B.3 port preserved the Python's six-entry table, under which WRONG_IMPLEMENTATION,
51
+ * TOOL_FAILURE, AGENT_FAILURE, BUDGET_EXCEEDED and UNKNOWN_FAILURE all fell through to
52
+ * `GENERIC_DEBUGGER_FIX`. C.2 completes the table, because that fallthrough directly contradicts
53
+ * FR-010 — "the response MUST be the targeted action for that class" — and in two cases the generic
54
+ * action is not merely untargeted but wrong:
55
+ *
56
+ * - BUDGET_EXCEEDED: handing the task back to a debugger spends more of the budget that is
57
+ * already gone. It is the one class where the targeted action is to stop, so it is `terminal`.
58
+ * - TOOL_FAILURE: an unreachable MCP server or a missing provider is not fixed by rewriting code,
59
+ * and swapping the agent changes nothing about the tool. The action routes to a fallback
60
+ * provider instead (the capability router already declares those fallbacks).
61
+ *
62
+ * Classifying eleven ways and then acting six ways discarded most of what the classifier
63
+ * established. `GENERIC_STRATEGY` is retained and still reachable — a caller may pass a class
64
+ * string that is not one of the eleven — but no declared class relies on it any more.
65
+ *
66
+ * `switchAtIteration` encodes the Python's three agent-switch behaviours: `null` never switches,
67
+ * `0` always switches (its unconditional `True`), and `2` switches only once two attempts have
68
+ * already been spent (its `current_iteration >= 2`). `terminal` is new and means the class is not
69
+ * retryable at any iteration.
70
+ */
71
+ const STRATEGY_MAP = Object.freeze({
72
+ CONTEXT_MISSING: { action: 'RETRIEVE_MORE_CONTEXT', targetRole: 'SCOUT', switchAtIteration: null },
73
+ WRONG_IMPLEMENTATION: { action: 'REVISE_IMPLEMENTATION', targetRole: 'IMPLEMENTER', switchAtIteration: 2 },
74
+ TEST_FAILURE: { action: 'TARGETED_DEBUGGER_FIX', targetRole: 'DEBUGGER', switchAtIteration: 2 },
75
+ BUILD_FAILURE: { action: 'TARGETED_COMPILER_FIX', targetRole: 'DEBUGGER', switchAtIteration: 2 },
76
+ ARCHITECTURE_FAILURE: { action: 'GRAPH_RE_EVALUATION', targetRole: 'PLANNER', switchAtIteration: 0 },
77
+ REQUIREMENT_FAILURE: { action: 'SPEC_RECONCILIATION', targetRole: 'PLANNER', switchAtIteration: 0 },
78
+ ENVIRONMENT_FAILURE: { action: 'ENVIRONMENT_DIAGNOSTIC', targetRole: 'VERIFIER', switchAtIteration: null },
79
+ TOOL_FAILURE: { action: 'ROUTE_TO_FALLBACK_PROVIDER', targetRole: 'VERIFIER', switchAtIteration: null },
80
+ // The agent itself is the failure, so the switch is the action rather than a later escalation.
81
+ AGENT_FAILURE: { action: 'SWITCH_AGENT_AND_RESTATE_TASK', targetRole: 'IMPLEMENTER', switchAtIteration: 0 },
82
+ BUDGET_EXCEEDED: { action: 'ESCALATE_TO_USER', targetRole: null, switchAtIteration: null, terminal: true },
83
+ // Diagnose before acting: an unclassified failure is the one case where a targeted fix cannot be
84
+ // chosen, and guessing one is how a recovery loop burns its whole bound on the wrong repair.
85
+ UNKNOWN_FAILURE: { action: 'DIAGNOSTIC_TRIAGE', targetRole: 'VERIFIER', switchAtIteration: null },
86
+ });
87
+
88
+ /** Reachable only for a class string outside the eleven — see the note on STRATEGY_MAP. */
89
+ const GENERIC_STRATEGY = Object.freeze({
90
+ action: 'GENERIC_DEBUGGER_FIX',
91
+ targetRole: 'DEBUGGER',
92
+ switchAtIteration: 2,
93
+ });
94
+
95
+ /**
96
+ * @param {*} value
97
+ * @returns {Array<string>} only the string members, lowercased
98
+ */
99
+ function normalizeCheckNames(value ) {
100
+ // Defect fix (2a): the Python accepted `failed_checks` unvalidated. Handed a plain string it
101
+ // iterated characters, so every single-letter "check" was tested for "syntax" and none matched —
102
+ // a build failure silently classified as WRONG_IMPLEMENTATION.
103
+ if (!Array.isArray(value)) return [];
104
+ return value.filter((v ) => typeof v === 'string').map((v ) => v.toLowerCase());
105
+ }
106
+
107
+ class RecoveryManager {
108
+ maxIterations ;
109
+
110
+ /**
111
+ * @param {Object|number} [options] a number is accepted for parity with the Python's positional
112
+ * `max_iterations` argument
113
+ * @param {number} [options.maxIterations=3]
114
+ */
115
+ constructor(options = {}) {
116
+ const opts = typeof options === 'number' ? { maxIterations: options } : options;
117
+ this.maxIterations = opts.maxIterations || DEFAULT_MAX_ITERATIONS;
118
+ }
119
+
120
+ /**
121
+ * Classifies a failure into one of the eleven classes.
122
+ *
123
+ * Order matters and is preserved: the names of the checks that failed outrank the wording of the
124
+ * error message, because a check name is structural evidence and an error string is prose.
125
+ * @param {string} errorMessage
126
+ * @param {Array<string>} [failedChecks=[]]
127
+ * @returns {string} one of FAILURE_CLASSES
128
+ */
129
+ classifyFailure(errorMessage , failedChecks = []) {
130
+ const checks = normalizeCheckNames(failedChecks);
131
+ // Defect fix (2b): `None.lower()` raised. A classifier handed no message should return
132
+ // UNKNOWN_FAILURE, not abort the recovery path that exists to handle failures.
133
+ const msg = typeof errorMessage === 'string' ? errorMessage.toLowerCase() : '';
134
+
135
+ const anyCheck = (markers ) => checks.some((chk) => markers.some((m) => chk.includes(m)));
136
+
137
+ if (anyCheck(BUILD_CHECK_MARKERS) || msg.includes('build error')) {
138
+ return 'BUILD_FAILURE';
139
+ }
140
+ if (anyCheck(TEST_CHECK_MARKERS) || msg.includes('test failed') || msg.includes('assertionerror')) {
141
+ return 'TEST_FAILURE';
142
+ }
143
+ if (msg.includes('budget') || msg.includes('token limit')) {
144
+ return 'BUDGET_EXCEEDED';
145
+ }
146
+ if (msg.includes('missing file') || msg.includes('cannot find module') || msg.includes('not found')) {
147
+ return 'CONTEXT_MISSING';
148
+ }
149
+ if (msg.includes('timeout') || msg.includes('connection refused') || msg.includes('econnrefused')) {
150
+ return 'ENVIRONMENT_FAILURE';
151
+ }
152
+ if (msg.includes('tool error') || msg.includes('mcp error')) {
153
+ return 'TOOL_FAILURE';
154
+ }
155
+ if (msg.includes('architecture') || msg.includes('boundary')) {
156
+ return 'ARCHITECTURE_FAILURE';
157
+ }
158
+ if (msg.includes('requirement') || msg.includes('acceptance criterion')) {
159
+ return 'REQUIREMENT_FAILURE';
160
+ }
161
+ if (msg.includes('agent') || msg.includes('hallucination')) {
162
+ return 'AGENT_FAILURE';
163
+ }
164
+ if (checks.length > 0) {
165
+ return 'WRONG_IMPLEMENTATION';
166
+ }
167
+ return 'UNKNOWN_FAILURE';
168
+ }
169
+
170
+ /**
171
+ * Determines the next recovery action and whether to hand the task to a different agent.
172
+ * @param {string} failureClass
173
+ * @param {number} currentIteration zero-based count of retries already spent
174
+ * @param {string} [lastAgent='codex']
175
+ * @returns {{ iteration: number, failureClass: string, action: string, targetRole: string|null, agent: string, canRetry: boolean, reason?: string }}
176
+ */
177
+ planRecovery(failureClass , currentIteration , lastAgent = 'codex') {
178
+ // A negative or non-numeric iteration is caller nonsense; clamping keeps it from reading as
179
+ // "before the first attempt" and skipping the agent switch that iteration 0 is supposed to get.
180
+ const iteration = Number.isFinite(currentIteration) ? Math.max(0, Math.trunc(currentIteration)) : 0;
181
+ const agent = lastAgent || 'codex';
182
+
183
+ if (iteration >= this.maxIterations) {
184
+ // Defect fix (1): the Python's abort record carried only `action`, `reason` and
185
+ // `switchAgent`, while its retry record carried `iteration`, `failureClass`, `targetRole`,
186
+ // `agent` and `canRetry`. A caller written against either shape broke on the other, and the
187
+ // key it most needed — `canRetry` — was the one missing from the stop path. One shape now,
188
+ // with `canRetry: false` stating the bound explicitly.
189
+ return {
190
+ iteration,
191
+ failureClass,
192
+ action: 'ABORT',
193
+ targetRole: null,
194
+ agent,
195
+ canRetry: false,
196
+ reason: `Maximum retry iterations (${this.maxIterations}) exceeded.`,
197
+ };
198
+ }
199
+
200
+ const strategy = STRATEGY_MAP[failureClass] || GENERIC_STRATEGY;
201
+
202
+ // A terminal class stops the loop regardless of how much of the bound is left. Today that is
203
+ // BUDGET_EXCEEDED only: retrying is the one response guaranteed to make the failure worse.
204
+ if (strategy.terminal) {
205
+ return {
206
+ iteration,
207
+ failureClass,
208
+ action: strategy.action,
209
+ targetRole: strategy.targetRole,
210
+ agent,
211
+ canRetry: false,
212
+ reason: `Failure class '${failureClass}' is not retryable; a retry would repeat the condition that caused it.`,
213
+ };
214
+ }
215
+
216
+ const switchAgent =
217
+ strategy.switchAtIteration !== null && iteration >= strategy.switchAtIteration;
218
+
219
+ // Preserved verbatim: the swap is a two-way toggle between codex and claude, so any other
220
+ // agent name switches to codex.
221
+ const recommendedAgent = switchAgent ? (agent === 'codex' ? 'claude' : 'codex') : agent;
222
+
223
+ return {
224
+ iteration: iteration + 1,
225
+ failureClass,
226
+ action: strategy.action,
227
+ targetRole: strategy.targetRole,
228
+ agent: recommendedAgent,
229
+ canRetry: true,
230
+ };
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Handles `doflow recover` — classify a verification failure and return the targeted action for
236
+ * that class (FR-010).
237
+ *
238
+ * Exit 0 means a bounded retry is available and the plan says what to change; exit 1 means the
239
+ * loop must stop — the retry budget is spent, or the class is one where retrying is guaranteed to
240
+ * reproduce the failure. That is the distinction a caller has to branch on, so it is the one the
241
+ * exit code carries.
242
+ *
243
+ * @param {Object} options
244
+ * @param {string} options.errorMessage
245
+ * @param {Array<string>} [options.failedChecks]
246
+ * @param {number} [options.iteration=0] retries already spent
247
+ * @param {string} [options.agent]
248
+ * @param {boolean} [options.json=false]
249
+ * @returns {number} exit code
250
+ */
251
+ function handleRecoverCommand({ errorMessage, failedChecks = [], iteration = 0, agent, json = false } = {}) {
252
+ // Handed nothing, the classifier answers UNKNOWN_FAILURE — a confident-looking verdict about a
253
+ // failure it never saw. Refuse instead.
254
+ if ((typeof errorMessage !== 'string' || errorMessage.trim() === '') && failedChecks.length === 0) {
255
+ return usageError('recover', '--error (and/or one or more --failed-check) is required — classifying a failure nobody described would name a class over no evidence.', json);
256
+ }
257
+ const manager = new RecoveryManager();
258
+ const failureClass = manager.classifyFailure(errorMessage, failedChecks);
259
+ const plan = manager.planRecovery(failureClass, iteration, agent || undefined);
260
+
261
+ if (json) console.log(JSON.stringify({ failureClass, failedChecks, ...(plan ) }, null, 2));
262
+ else {
263
+ console.log(`\nDoFlow Recovery [${plan.failureClass}]:`);
264
+ console.log('═'.repeat(78));
265
+ console.log(`Action: ${plan.action}`);
266
+ console.log(`Target: ${plan.targetRole || 'unchanged'}`);
267
+ console.log(`Agent: ${plan.agent}`);
268
+ console.log(`Retry: ${plan.canRetry ? `yes — iteration ${plan.iteration}` : 'no'}`);
269
+ if (plan.reason) console.log(`Reason: ${plan.reason}`);
270
+ console.log('═'.repeat(78) + '\n');
271
+ }
272
+ return finishRuntime(plan.canRetry ? 0 : 1);
273
+ }
274
+
275
+ module.exports = {
276
+ RecoveryManager,
277
+ FAILURE_CLASSES,
278
+ STRATEGY_MAP,
279
+ GENERIC_STRATEGY,
280
+ DEFAULT_MAX_ITERATIONS,
281
+ handleRecoverCommand,
282
+ };
@@ -0,0 +1,231 @@
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 crypto = require('node:crypto');
8
+ const { acquireLock, updateTaskState, readTaskState } = require('./task-state.js');
9
+ const { EvidenceLedger, assertSafeTaskId } = require('./evidence-ledger.js');
10
+ const { ClaimsManager } = require('./claims.js');
11
+ const { taskStoreDir } = require('./task-scope.js');
12
+ const { FreshnessValidator } = require('./freshness.js');
13
+ const { resolveLocator } = require('./locator-resolve.js');
14
+ const { finishRuntime, usageError } = require('./cli-result.js');
15
+
16
+ const STATES = new Set(['OPEN', 'ANSWERED', 'UNRESOLVED']);
17
+ const REASONS = new Set(['detected-gap', 'user-request']);
18
+ const safeId = (value , label ) => {
19
+ if (typeof value !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) {
20
+ throw new Error(`${label} must be a safe non-empty identifier`);
21
+ }
22
+ return value;
23
+ };
24
+
25
+ function hasLocatedSource(item , projectRoot ) {
26
+ const locator = item.locator || {};
27
+ if (locator.uri !== undefined) {
28
+ if (typeof locator.uri !== 'string') return false;
29
+ try {
30
+ const url = new URL(locator.uri);
31
+ if (!['https:', 'http:'].includes(url.protocol) || !url.hostname || url.username || url.password) return false;
32
+ } catch { return false; }
33
+ }
34
+ return Boolean((locator.uri && typeof locator.uri === 'string')
35
+ || (locator.file && resolveLocator({ locator, repoRoot: projectRoot }).resolved));
36
+ }
37
+
38
+ class ResearchRequestStore {
39
+
40
+
41
+
42
+
43
+ constructor({ projectRoot = process.cwd(), fsImpl = fs, slug = null } = {}) {
44
+ this.projectRoot = projectRoot;
45
+ this.fsImpl = fsImpl;
46
+ this.slug = slug;
47
+ this.stateDir = path.join(projectRoot, '.doflow', 'state', 'research');
48
+ }
49
+
50
+ /** The feature's namespace directory when the task has one (task-scope.js), else the flat store. */
51
+ file(taskId ) {
52
+ const dir = taskStoreDir({ projectRoot: this.projectRoot, store: 'research', taskId: assertSafeTaskId(taskId), slug: this.slug });
53
+ return path.join(dir, `${taskId}.json`);
54
+ }
55
+
56
+ runFile(taskId ) {
57
+ return path.join(this.projectRoot, '.doflow', 'state', 'orchestration', `${assertSafeTaskId(taskId)}.json`);
58
+ }
59
+
60
+ read(taskId ) {
61
+ const data = readTaskState(this.fsImpl, this.file(taskId));
62
+ if (!data) return { taskId, revision: 0, requests: [] };
63
+ if (data.taskId !== taskId || !Array.isArray(data.requests)
64
+ || data.requests.some((r ) => !STATES.has(r.status))) {
65
+ throw new Error(`Invalid research state for task '${taskId}'`);
66
+ }
67
+ return data;
68
+ }
69
+
70
+ // Re-read inside updateTaskState's lock: overlapping writers cannot lose a request.
71
+ change(taskId , update ) {
72
+ let result;
73
+ updateTaskState({ fsImpl: this.fsImpl, file: this.file(taskId), build: (disk ) => {
74
+ if (disk && (disk.taskId !== taskId || !Array.isArray(disk.requests))) {
75
+ throw new Error(`Invalid research state for task '${taskId}'`);
76
+ }
77
+ const requests = disk?.requests || [];
78
+ result = update(requests);
79
+ return { taskId, requests };
80
+ } });
81
+ return result;
82
+ }
83
+
84
+ stageFor(taskId , stageId ) {
85
+ const run = readTaskState(this.fsImpl, this.runFile(taskId));
86
+ if (!run || run.taskClass !== 'feature' || run.state !== 'RUNNING') {
87
+ throw new Error(`Task '${taskId}' has no active feature stage`);
88
+ }
89
+ const current = run.program[run.cursor];
90
+ if (current?.type !== 'stage' || current.id !== stageId) {
91
+ throw new Error(`Current feature stage is '${current?.type === 'stage' ? current.id : 'none'}', not '${stageId}'`);
92
+ }
93
+ }
94
+
95
+ open({ taskId, stageId, question, reason, blocking } = {}) {
96
+ safeId(taskId, 'taskId'); safeId(stageId, 'stageId');
97
+ if (typeof question !== 'string' || !question.trim()) throw new Error('question is required');
98
+ if (!REASONS.has(reason)) throw new Error(`reason must be one of: ${[...REASONS].join(', ')}`);
99
+ if (blocking !== undefined && typeof blocking !== 'boolean') throw new Error('blocking must be true or false');
100
+ // Lock order is run -> research for both open and completion. The stage check and request
101
+ // write must be indivisible with the orchestrator's final completion check.
102
+ const runFile = this.runFile(taskId);
103
+ if (!this.fsImpl.existsSync(runFile)) throw new Error(`Task '${taskId}' has no active feature stage`);
104
+ const release = acquireLock(this.fsImpl, runFile);
105
+ try {
106
+ this.stageFor(taskId, stageId);
107
+ return this.change(taskId, (requests ) => {
108
+ const existing = requests.find((r ) => r.stageId === stageId && r.question === question.trim() && r.status === 'OPEN');
109
+ if (existing) {
110
+ if ((blocking ?? reason === 'detected-gap') && !existing.blocking) {
111
+ existing.blocking = true;
112
+ existing.reason = reason;
113
+ existing.updatedAt = new Date().toISOString();
114
+ }
115
+ return existing;
116
+ }
117
+ const at = new Date().toISOString();
118
+ const record = { id: `req_${crypto.randomBytes(12).toString('hex')}`, taskId, stageId,
119
+ question: question.trim(), reason, blocking: blocking ?? reason === 'detected-gap',
120
+ status: 'OPEN', claimId: null, evidenceIds: [], gap: null, createdAt: at, updatedAt: at };
121
+ requests.push(record);
122
+ return record;
123
+ });
124
+ } finally { release(); }
125
+ }
126
+
127
+ list(taskId ) {
128
+ safeId(taskId, 'taskId');
129
+ return this.read(taskId).requests;
130
+ }
131
+
132
+ // Validate references before taking the write lock; only the status transition itself mutates state.
133
+ resolve({ taskId, requestId, outcome, claimId, evidenceIds = [], gap } = {}) {
134
+ safeId(taskId, 'taskId'); safeId(requestId, 'requestId');
135
+ if (!['answered', 'unresolved'].includes(outcome)) throw new Error('outcome must be answered or unresolved');
136
+ if (!Array.isArray(evidenceIds)) throw new Error('evidenceIds must be an array');
137
+ if (outcome === 'unresolved') {
138
+ if (!gap || !String(gap).trim()) throw new Error('unresolved requires a gap');
139
+ if (claimId) throw new Error('unresolved cannot carry a claim');
140
+ } else {
141
+ if (gap) throw new Error('answered cannot carry a gap');
142
+ if (!claimId || evidenceIds.length === 0) throw new Error('answered requires a claim and evidence IDs');
143
+ const ledger = new EvidenceLedger({ repoRoot: this.projectRoot, slug: this.slug });
144
+ ledger.load(taskId);
145
+ new FreshnessValidator({ repoRoot: this.projectRoot }).validateLedgerFreshness(ledger);
146
+ const claims = new ClaimsManager({ repoRoot: this.projectRoot, evidenceLedger: ledger, slug: this.slug });
147
+ claims.load(taskId); claims.evaluateAll();
148
+ const claim = claims.getClaim(claimId);
149
+ if (!claim || claim.taskId !== taskId || claim.status !== 'supported') {
150
+ throw new Error('answered requires a supported claim in the same task');
151
+ }
152
+ for (const id of evidenceIds) {
153
+ const item = ledger.getEvidence(id);
154
+ if (!item || item.taskId !== taskId || item.provenance !== 'extracted'
155
+ || item.status === 'superseded' || item.freshness?.status !== 'FRESH'
156
+ || !hasLocatedSource(item, this.projectRoot)
157
+ || !claim.supportingEvidence.includes(id)) {
158
+ throw new Error(`Evidence '${id}' is not fresh, located support for this claim and task`);
159
+ }
160
+ }
161
+ }
162
+ return this.change(taskId, (requests ) => {
163
+ const request = requests.find((r ) => r.id === requestId);
164
+ if (!request) throw new Error(`Unknown research request '${requestId}' for task '${taskId}'`);
165
+ if (request.status !== 'OPEN') throw new Error(`Request '${requestId}' is ${request.status}, not OPEN`);
166
+ Object.assign(request, { status: outcome === 'answered' ? 'ANSWERED' : 'UNRESOLVED',
167
+ claimId: claimId || null, evidenceIds: [...evidenceIds], gap: gap || null,
168
+ updatedAt: new Date().toISOString() });
169
+ return request;
170
+ });
171
+ }
172
+
173
+ blockingGap(taskId , stageId ) {
174
+ const requests = this.list(taskId).filter((r ) => r.stageId === stageId && r.blocking);
175
+ for (const request of requests) {
176
+ if (request.status !== 'ANSWERED') return request;
177
+ try {
178
+ this.validateAnswer(taskId, request);
179
+ } catch (error ) {
180
+ return { ...request, failureReason: error.message };
181
+ }
182
+ }
183
+ return null;
184
+ }
185
+
186
+ validateAnswer(taskId , request ) {
187
+ const ledger = new EvidenceLedger({ repoRoot: this.projectRoot, slug: this.slug });
188
+ ledger.load(taskId);
189
+ new FreshnessValidator({ repoRoot: this.projectRoot }).validateLedgerFreshness(ledger);
190
+ const claims = new ClaimsManager({ repoRoot: this.projectRoot, evidenceLedger: ledger, slug: this.slug });
191
+ claims.load(taskId); claims.evaluateAll();
192
+ const claim = claims.getClaim(request.claimId);
193
+ if (!claim || claim.taskId !== taskId) {
194
+ throw new Error(`Research claim '${request.claimId}' is missing from this task`);
195
+ }
196
+ if (claim.status !== 'supported') {
197
+ throw new Error(`Research claim '${request.claimId}' is ${claim.status}, not supported`);
198
+ }
199
+ if (!request.evidenceIds?.length) throw new Error('Research answer has no linked evidence');
200
+ for (const id of request.evidenceIds) {
201
+ const item = ledger.getEvidence(id);
202
+ if (!item || item.taskId !== taskId || item.provenance !== 'extracted'
203
+ || item.status === 'superseded' || item.freshness?.status !== 'FRESH'
204
+ || !hasLocatedSource(item, this.projectRoot)
205
+ || !claim.supportingEvidence.includes(id)) {
206
+ throw new Error(`Research evidence '${id}' is no longer fresh, located support`);
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ function handleResearchRequestCommand({ action = 'list', taskId, stageId, question, reason,
213
+ blocking, requestId, outcome, claimId, evidenceIds, gap, json = false, projectRoot, slug } = {}) {
214
+ try {
215
+ const store = new ResearchRequestStore({ projectRoot, slug });
216
+ let result;
217
+ switch (action) {
218
+ case 'open': result = store.open({ taskId, stageId, question, reason, blocking }); break;
219
+ case 'list': result = { taskId, requests: store.list(taskId) }; break;
220
+ case 'resolve': result = store.resolve({ taskId, requestId, outcome, claimId, evidenceIds, gap }); break;
221
+ default: return usageError('research-request', `unknown action '${action}'; valid: open, list, resolve`, json);
222
+ }
223
+ if (json) console.log(JSON.stringify(result, null, 2));
224
+ else console.log(JSON.stringify(result));
225
+ return finishRuntime(0);
226
+ } catch (error ) {
227
+ return usageError('research-request', error.message, json, error);
228
+ }
229
+ }
230
+
231
+ module.exports = { ResearchRequestStore, handleResearchRequestCommand };