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

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 (407) 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 +1057 -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 +46 -4
  250. package/src/adapters/antigravity/index.ts +631 -0
  251. package/src/adapters/claude/index.ts +548 -0
  252. package/src/adapters/claude/mcp.ts +94 -0
  253. package/src/adapters/codex/agents.ts +208 -0
  254. package/src/adapters/codex/config.ts +272 -0
  255. package/src/adapters/codex/hooks.ts +203 -0
  256. package/src/adapters/codex/index.ts +707 -0
  257. package/src/adapters/codex/mcp.ts +271 -0
  258. package/src/adapters/codex/native-projection.ts +34 -0
  259. package/src/adapters/copilot/index.ts +413 -0
  260. package/src/adapters/copy-tree.ts +620 -0
  261. package/src/adapters/gemini/hooks.ts +210 -0
  262. package/src/adapters/gemini/index.ts +390 -0
  263. package/src/adapters/hook-commands.ts +60 -0
  264. package/src/adapters/index.ts +106 -0
  265. package/src/adapters/kiro/index.ts +310 -0
  266. package/src/adapters/mcp-entries.ts +331 -0
  267. package/src/adapters/opencode/index.ts +466 -0
  268. package/src/adapters/pi/index.ts +279 -0
  269. package/src/adapters/pi/json-members.ts +185 -0
  270. package/src/adapters/pi/mcp.ts +373 -0
  271. package/src/adapters/resolve-source.ts +19 -0
  272. package/src/cli/commands/install.ts +218 -0
  273. package/src/cli/commands/list-backups.ts +21 -0
  274. package/src/cli/commands/reconcile.ts +120 -0
  275. package/src/cli/commands/remove.ts +82 -0
  276. package/src/cli/commands/rollback.ts +172 -0
  277. package/src/cli/commands/self-update.ts +33 -0
  278. package/src/cli/commands/status.ts +138 -0
  279. package/src/cli/commands/tools.ts +92 -0
  280. package/src/cli/commands/update.ts +162 -0
  281. package/src/cli/index.ts +568 -0
  282. package/src/cli/runtime-commands.ts +281 -0
  283. package/src/cli/shared.ts +218 -0
  284. package/src/helper/bash-helper.ts +34 -0
  285. package/src/helper/git.ts +24 -0
  286. package/src/helper/guardrails.ts +49 -0
  287. package/src/helper/harness-paths.ts +140 -0
  288. package/src/helper/marker-merge.ts +116 -0
  289. package/src/helper/prompt.ts +61 -0
  290. package/src/helper/repo-root.ts +29 -0
  291. package/src/helper/settings-merge.ts +151 -0
  292. package/src/helper/settings-scope.ts +33 -0
  293. package/src/helper/toml.ts +244 -0
  294. package/src/install/backup-set.ts +149 -0
  295. package/src/install/backup.ts +724 -0
  296. package/src/install/context.ts +58 -0
  297. package/src/install/manifest.ts +145 -0
  298. package/src/install/mcp.ts +254 -0
  299. package/src/install/paths.ts +84 -0
  300. package/src/install/targets.ts +90 -0
  301. package/src/install/tool-lifecycle.ts +151 -0
  302. package/src/lifecycle/bash-availability.ts +25 -0
  303. package/src/lifecycle/index.ts +812 -0
  304. package/src/lifecycle/mcp-index.ts +28 -0
  305. package/src/lifecycle/policies.ts +73 -0
  306. package/src/lifecycle/view.ts +175 -0
  307. package/src/registry/index.ts +611 -0
  308. package/src/release/sync-plugin-versions.ts +66 -0
  309. package/src/runtime/advice/cli.ts +283 -0
  310. package/src/runtime/advice/client.ts +324 -0
  311. package/src/runtime/advice/config.ts +189 -0
  312. package/src/runtime/advice/doctor.ts +169 -0
  313. package/src/runtime/advice/endpoint-guard.ts +134 -0
  314. package/src/runtime/advice/kinds.ts +154 -0
  315. package/src/runtime/advice/providers/ollama-systemone.ts +209 -0
  316. package/src/runtime/advice/record.ts +166 -0
  317. package/src/runtime/advice/registry.ts +170 -0
  318. package/src/runtime/advice/strength.ts +66 -0
  319. package/src/runtime/brief-tools.ts +342 -0
  320. package/src/runtime/c4-project.ts +371 -0
  321. package/src/runtime/capability-router.ts +484 -0
  322. package/src/runtime/checkouts.ts +170 -0
  323. package/src/runtime/claims.ts +468 -0
  324. package/src/runtime/cli-result.ts +55 -0
  325. package/src/runtime/cli.ts +678 -0
  326. package/src/runtime/command-detect.ts +516 -0
  327. package/src/runtime/context-pack.ts +492 -0
  328. package/src/runtime/decision-register.ts +493 -0
  329. package/src/runtime/evidence-ledger.ts +338 -0
  330. package/src/runtime/failure/capture.ts +272 -0
  331. package/src/runtime/failure/classifier.ts +68 -0
  332. package/src/runtime/failure/cli.ts +178 -0
  333. package/src/runtime/failure/home.ts +64 -0
  334. package/src/runtime/failure/store.ts +226 -0
  335. package/src/runtime/feature-resolve.ts +79 -0
  336. package/src/runtime/freshness.ts +177 -0
  337. package/src/runtime/health.ts +863 -0
  338. package/src/runtime/history-compactor.ts +237 -0
  339. package/src/runtime/implementation-gate.ts +321 -0
  340. package/src/runtime/inventory/coverage.ts +218 -0
  341. package/src/runtime/inventory/identity.ts +65 -0
  342. package/src/runtime/inventory/index.ts +705 -0
  343. package/src/runtime/inventory/precedence.ts +134 -0
  344. package/src/runtime/inventory/read-scopes.ts +131 -0
  345. package/src/runtime/inventory/siblings.ts +192 -0
  346. package/src/runtime/knowledge/chunker.ts +124 -0
  347. package/src/runtime/knowledge/hybrid.ts +149 -0
  348. package/src/runtime/knowledge/index-store.ts +145 -0
  349. package/src/runtime/knowledge/retrieval.ts +194 -0
  350. package/src/runtime/leak-scan.ts +185 -0
  351. package/src/runtime/lifecycle/cli.ts +276 -0
  352. package/src/runtime/lifecycle/event-store.ts +340 -0
  353. package/src/runtime/lifecycle/fold.ts +353 -0
  354. package/src/runtime/lifecycle/followup.ts +432 -0
  355. package/src/runtime/lifecycle/goal.ts +249 -0
  356. package/src/runtime/lifecycle/intent-writer.ts +86 -0
  357. package/src/runtime/lifecycle/overview.ts +292 -0
  358. package/src/runtime/lifecycle/release.ts +241 -0
  359. package/src/runtime/lifecycle/report-store.ts +221 -0
  360. package/src/runtime/lifecycle/retention.ts +212 -0
  361. package/src/runtime/lifecycle/root.ts +71 -0
  362. package/src/runtime/lifecycle/status.ts +353 -0
  363. package/src/runtime/lifecycle/store-upkeep.ts +393 -0
  364. package/src/runtime/locator-resolve.ts +158 -0
  365. package/src/runtime/mask.ts +210 -0
  366. package/src/runtime/mcp-consultations.ts +412 -0
  367. package/src/runtime/mcp-reach.ts +157 -0
  368. package/src/runtime/model-router.ts +153 -0
  369. package/src/runtime/outcome.ts +467 -0
  370. package/src/runtime/reach.ts +148 -0
  371. package/src/runtime/readiness-record.ts +150 -0
  372. package/src/runtime/readiness.ts +435 -0
  373. package/src/runtime/recovery.ts +282 -0
  374. package/src/runtime/research-request.ts +231 -0
  375. package/src/runtime/retrieval-plan.ts +578 -0
  376. package/src/runtime/scaffold/artifacts.ts +217 -0
  377. package/src/runtime/scaffold/fingerprint.ts +56 -0
  378. package/src/runtime/scaffold/generate.ts +911 -0
  379. package/src/runtime/scaffold/languages.ts +245 -0
  380. package/src/runtime/storage/cli.ts +64 -0
  381. package/src/runtime/storage/engine.ts +249 -0
  382. package/src/runtime/storage/registry.ts +130 -0
  383. package/src/runtime/storage/roots.ts +63 -0
  384. package/src/runtime/storage/rules.ts +282 -0
  385. package/src/runtime/storage/upkeep.ts +243 -0
  386. package/src/runtime/storage/work-status.ts +326 -0
  387. package/src/runtime/task-attempts.ts +415 -0
  388. package/src/runtime/task-classifier.ts +550 -0
  389. package/src/runtime/task-scope.ts +130 -0
  390. package/src/runtime/task-state.ts +176 -0
  391. package/src/runtime/trace/indicators.ts +235 -0
  392. package/src/runtime/trace/ledger.ts +626 -0
  393. package/src/runtime/trace/render.ts +76 -0
  394. package/src/runtime/trace/views.ts +459 -0
  395. package/src/runtime/verification/contract-runner.ts +341 -0
  396. package/src/runtime/verification/engine.ts +1461 -0
  397. package/src/runtime/verification/registry.ts +130 -0
  398. package/src/runtime/verification/scope-bound.ts +418 -0
  399. package/src/runtime/verification/test-selection.ts +172 -0
  400. package/src/runtime/workflow-engine.ts +588 -0
  401. package/src/runtime/workflow-orchestrator.ts +953 -0
  402. package/src/runtime/worktree.ts +340 -0
  403. package/src/state/index.ts +193 -0
  404. package/src/state/lockfile.ts +136 -0
  405. package/src/state/run-lock.ts +408 -0
  406. package/types/node-commonjs.d.ts +68 -0
  407. package/types/runtime.d.ts +91 -0
@@ -0,0 +1,590 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * DoFlow Cross-Harness Hook Runner (design.md C2) — front door for every in-scope lifecycle
6
+ * event (SessionStart, PreToolUse, PostToolUse, Stop, PreInvocation) for the two harnesses whose
7
+ * native hook contract speaks JSON stdin/stdout rather than a bare exit code: Gemini CLI and
8
+ * Antigravity. It owns event dispatch and per-harness payload/decision translation (delegated to
9
+ * adapters/{gemini,antigravity}.js, D2) and delegates every policy decision to the Canonical
10
+ * Policy Library (core/harnesses/shared/hooks/policies/*.sh, Phase A) — it implements no guard
11
+ * policy logic itself.
12
+ *
13
+ * Invocation: `node stream-hook-runner.js [EventName]`, JSON payload on stdin, JSON decision on
14
+ * stdout. EventName is the preferred way to select which canonical policy applies; when omitted
15
+ * (older hooks.json wiring that doesn't yet pass it), the event is inferred from the payload's
16
+ * own shape as a fallback, matching this runner's behavior before event names were threaded
17
+ * through argv.
18
+ */
19
+
20
+ const { execFileSync } = require('node:child_process');
21
+ const fs = require('node:fs');
22
+ const os = require('node:os');
23
+ const path = require('node:path');
24
+
25
+ const geminiAdapter = require('./adapters/gemini');
26
+ const antigravityAdapter = require('./adapters/antigravity');
27
+
28
+ // Tool-name classification for PreToolUse, generalized to the union every Phase A canonical
29
+ // script's own case statement already recognizes (session-context.sh/pre-implementation-gate.sh/
30
+ // mcp-tool-guard.sh/pre-bash-guard.sh headers) rather than only the two harnesses this runner
31
+ // drives — kept in sync with those scripts, not redefined independently of them.
32
+ const MCP_TOOL_PATTERN = /^mcp_/i;
33
+ const EDIT_TOOL_PATTERN = /^(replace_file_content|write_to_file|multi_replace_file_content|edit_file|write_file|create_file|replace_file|replace|Edit|Write|MultiEdit|apply_patch)$/i;
34
+ const COMMAND_TOOL_PATTERN = /^(run_command|run_shell_command|Bash|bash)$/i;
35
+
36
+ // ── Failure capture (feature 046: IC-010, IC-011, IC-014, IC-015, IC-018) ─────────────────────────
37
+ //
38
+ // Hook files are installed without DoFlow's runtime library, so this runner carries its own copy of
39
+ // the IC-010 classifier and of the Node failure writer (src/runtime/failure/{classifier,home,capture}.js);
40
+ // a test runs both classifier copies over one fixture set and requires equal answers. Capture is a
41
+ // side channel at exactly two points, never changes the runner's stdout, stderr or exit status,
42
+ // never throws, and is a no-op while capture is off or when no failure home can be resolved. A line
43
+ // holds only names from DoFlow's own code and the working directory, never the payload.
44
+
45
+ const CAPTURE_MAX_LINE_BYTES = 2048;
46
+ const CAPTURE_ROTATE_AT_BYTES = 1048576;
47
+ const CAPTURE_KEEP_ROTATED = 4;
48
+ const CAPTURE_OFF_VALUES = ['off', '0', 'false', 'no'];
49
+ const ROTATED_FILE = /^events-\d{8}T\d{6}Z-\d+\.jsonl$/;
50
+ const PROGRAMMING_ERROR_NAMES = ['TypeError', 'RangeError', 'ReferenceError'];
51
+
52
+ /** Reads one property without ever throwing (a throwing getter reads as undefined). */
53
+ function readProp(error, key) {
54
+ try { return error[key]; } catch { return undefined; }
55
+ }
56
+
57
+ function isInstance(error, Class) {
58
+ try { return error instanceof Class; } catch { return false; }
59
+ }
60
+
61
+ /** IC-010. Pure and total: never throws whatever it is handed. Kept equal to src/runtime/failure/classifier.js. */
62
+ function isProgrammingError(error) {
63
+ try {
64
+ if (error === null || typeof error !== 'object') return false;
65
+ const code = readProp(error, 'code');
66
+ const name = readProp(error, 'name');
67
+ if (code === 'EPIPE') return false;
68
+ if (isInstance(error, SyntaxError) || name === 'SyntaxError') return false;
69
+ if (isInstance(error, TypeError) || isInstance(error, RangeError) || isInstance(error, ReferenceError)) return true;
70
+ if (PROGRAMMING_ERROR_NAMES.includes(name)) return true;
71
+ if (name === 'AssertionError' || code === 'ERR_ASSERTION') return true;
72
+ if (code === 'MODULE_NOT_FOUND') return true;
73
+ return typeof code === 'string' && (typeof readProp(error, 'syscall') === 'string' || typeof readProp(error, 'errno') === 'number');
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ /** IC-011: `$XDG_CONFIG_HOME/doflow/failures` when absolute, else `$HOME/.config/doflow/failures` when HOME is absolute, else null. */
80
+ function failureHome(env) {
81
+ const xdg = env.XDG_CONFIG_HOME;
82
+ if (typeof xdg === 'string' && xdg !== '') return path.isAbsolute(xdg) ? path.join(xdg, 'doflow', 'failures') : null;
83
+ const home = env.HOME;
84
+ return typeof home === 'string' && path.isAbsolute(home) ? path.join(home, '.config', 'doflow', 'failures') : null;
85
+ }
86
+
87
+ /** IC-014: the environment first, then one stat of the sentinel file. */
88
+ function captureIsOff(home, env) {
89
+ const setting = env.DOFLOW_FAILURE_CAPTURE;
90
+ if (typeof setting === 'string' && CAPTURE_OFF_VALUES.includes(setting.trim().toLowerCase())) return true;
91
+ try { return fs.statSync(path.join(home, 'off')).isFile(); } catch { return false; }
92
+ }
93
+
94
+ /** The first stack frame inside this hooks folder as `<path relative to it>:<function>`, no line number. */
95
+ function runnerFrame(error) {
96
+ try {
97
+ const root = __dirname + path.sep;
98
+ for (const line of String(error.stack || '').split('\n')) {
99
+ const match = /^\s*at (?:async )?(?:(.+?) \()?(.+?):\d+:\d+\)?$/.exec(line);
100
+ if (!match) continue;
101
+ const file = match[2].replace(/^file:\/\//, '');
102
+ if (!file.startsWith(root) || file.includes(`${path.sep}node_modules${path.sep}`)) continue;
103
+ const fn = (match[1] || '').replace(/^new /, '').split('.').pop().replace(/\s.*$/, '') || '<anonymous>';
104
+ return `${path.relative(__dirname, file).split(path.sep).join('/')}:${fn}`;
105
+ }
106
+ } catch { /* no frame */ }
107
+ return null;
108
+ }
109
+
110
+ /** The package version in a checkout, else `script_version` of the nearest install manifest, else `unknown`. */
111
+ function hookVersion(env) {
112
+ const valid = (v) => (typeof v === 'string' && /^[A-Za-z0-9._+-]{1,40}$/.test(v) ? v : null);
113
+ try {
114
+ const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', '..', '..', '..', 'package.json'), 'utf8'));
115
+ if (/doflow$/.test(pkg.name) && valid(pkg.version)) return pkg.version;
116
+ } catch { /* not a checkout */ }
117
+ const starts = [__dirname];
118
+ if (typeof env.HOME === 'string' && path.isAbsolute(env.HOME)) starts.push(env.HOME);
119
+ for (const start of starts) {
120
+ for (let dir = start; ; dir = path.dirname(dir)) {
121
+ try {
122
+ const version = valid(JSON.parse(fs.readFileSync(path.join(dir, '.doflow', '.install-manifest.json'), 'utf8')).script_version);
123
+ if (version) return version;
124
+ } catch { /* none here */ }
125
+ if (path.dirname(dir) === dir || start === env.HOME) break;
126
+ }
127
+ }
128
+ return 'unknown';
129
+ }
130
+
131
+ /**
132
+ * IC-015: at 1 MiB the live file is renamed and only the newest four full rotated files are kept. A
133
+ * rename that took a not-full file (a lost race) is linked back as the live file, or kept as a small
134
+ * rotated file that never counts toward the four and is removed after an hour. Same rules as
135
+ * src/runtime/failure/capture.js.
136
+ */
137
+ function rotateFailures(home) {
138
+ const live = path.join(home, 'events.jsonl');
139
+ try {
140
+ if (fs.statSync(live).size < CAPTURE_ROTATE_AT_BYTES) return;
141
+ } catch { return; }
142
+ // Never rename onto an earlier rotated file of this process (same second, same pid): take the next free second.
143
+ const stampAt = (ms) => new Date(ms).toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
144
+ let rotatedPath = path.join(home, `events-${stampAt(Date.now())}-${process.pid}.jsonl`);
145
+ for (let i = 1; i < 120 && fs.existsSync(rotatedPath); i++) {
146
+ rotatedPath = path.join(home, `events-${stampAt(Date.now() + i * 1000)}-${process.pid}.jsonl`);
147
+ }
148
+ try {
149
+ fs.renameSync(live, rotatedPath);
150
+ } catch { return; }
151
+ try {
152
+ if (fs.statSync(rotatedPath).size < CAPTURE_ROTATE_AT_BYTES) {
153
+ try { fs.linkSync(rotatedPath, live); fs.unlinkSync(rotatedPath); } catch { /* the live file exists again: the small file stays */ }
154
+ return;
155
+ }
156
+ } catch { return; }
157
+ try {
158
+ const files = fs.readdirSync(home).filter((name) => ROTATED_FILE.test(name)).map((name) => {
159
+ let size = 0;
160
+ let mtime = 0;
161
+ try { const st = fs.statSync(path.join(home, name)); size = st.size; mtime = st.mtimeMs; } catch { /* gone */ }
162
+ return { name, size, mtime };
163
+ }).sort((x, y) => x.mtime - y.mtime || (x.name < y.name ? -1 : x.name > y.name ? 1 : 0));
164
+ const full = files.filter((f) => f.size >= CAPTURE_ROTATE_AT_BYTES);
165
+ const doomed = [
166
+ ...full.slice(0, Math.max(0, full.length - CAPTURE_KEEP_ROTATED)),
167
+ ...files.filter((f) => f.size < CAPTURE_ROTATE_AT_BYTES && Date.now() - f.mtime > 60 * 60 * 1000),
168
+ ];
169
+ for (const f of doomed) {
170
+ try { fs.unlinkSync(path.join(home, f.name)); } catch { /* ignored */ }
171
+ }
172
+ } catch { /* ignored */ }
173
+ }
174
+
175
+ /**
176
+ * The working directory with the home prefix as `~`, matching HOME as given, its real path and
177
+ * `os.homedir()`; under no known home, when HOME is not usable, only the folder name. Same rule as
178
+ * projectName in src/runtime/failure/capture.js (this file ships without src/).
179
+ */
180
+ function hookProject(cwd, env) {
181
+ const homes = [];
182
+ const add = (value) => {
183
+ if (typeof value !== 'string' || !path.isAbsolute(value)) return;
184
+ const home = value.replace(/\/+$/, '');
185
+ if (home.length > 1 && !homes.includes(home)) homes.push(home);
186
+ };
187
+ add(env.HOME);
188
+ if (typeof env.HOME === 'string' && path.isAbsolute(env.HOME)) { try { add(fs.realpathSync(env.HOME)); } catch { /* HOME does not exist */ } }
189
+ try { add(os.homedir()); add(fs.realpathSync(os.homedir())); } catch { /* no home known to the system */ }
190
+ const home = homes.find((h) => cwd === h || cwd.startsWith(`${h}/`));
191
+ if (home) return `~${cwd.slice(home.length)}`;
192
+ return typeof env.HOME === 'string' && path.isAbsolute(env.HOME) ? cwd : path.basename(cwd);
193
+ }
194
+
195
+ /**
196
+ * Appends one line only when `file` is absent or a regular file: a FIFO there would block the open
197
+ * until something reads it and stall a guarded tool call. Same rule as appendRegular in
198
+ * src/runtime/failure/capture.js (this file ships without src/).
199
+ */
200
+ function appendRegularLine(file, line) {
201
+ const c = fs.constants;
202
+ let fd;
203
+ try {
204
+ fd = fs.openSync(file, c.O_WRONLY | c.O_APPEND | c.O_CREAT | (c.O_NOFOLLOW || 0) | (c.O_NONBLOCK || 0), 0o600);
205
+ if (!fs.fstatSync(fd).isFile()) return false;
206
+ fs.writeSync(fd, line);
207
+ return true;
208
+ } catch { return false; } finally {
209
+ if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* nothing more to do */ } }
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Appends one `source: "hook"` line (IC-011) with an empty `message` and `exit: null`. Silent and
215
+ * best-effort: every failure is swallowed.
216
+ * @param {{command: string, kind: string, frame?: string|null}} fields
217
+ * @param {Object} [env]
218
+ * @returns {boolean} whether a line was written
219
+ */
220
+ function captureHookFailure({ command, kind, frame = null }, env = process.env) {
221
+ try {
222
+ const home = failureHome(env);
223
+ if (!home || captureIsOff(home, env)) return false;
224
+ let project = '';
225
+ try { project = hookProject(process.cwd(), env); } catch { project = ''; }
226
+ const record = {
227
+ v: 1,
228
+ at: new Date().toISOString(),
229
+ source: 'hook',
230
+ command: /^[a-z][a-z0-9-]{0,39}$/.test(command) ? command : 'unknown',
231
+ harness: /^[A-Za-z0-9._-]{1,40}$/.test(env.DOFLOW_AGENT || '') ? env.DOFLOW_AGENT : 'none',
232
+ version: hookVersion(env),
233
+ project: project.slice(0, 200),
234
+ kind: String(kind).slice(0, 80),
235
+ message: '',
236
+ frame: typeof frame === 'string' ? frame : null,
237
+ exit: null,
238
+ };
239
+ const bytes = (r) => Buffer.byteLength(JSON.stringify(r), 'utf8') + 1;
240
+ while (bytes(record) > CAPTURE_MAX_LINE_BYTES && record.project.length > 0) {
241
+ record.project = record.project.slice(0, Math.max(0, record.project.length - Math.max(8, bytes(record) - CAPTURE_MAX_LINE_BYTES)));
242
+ }
243
+ if (bytes(record) > CAPTURE_MAX_LINE_BYTES) return false;
244
+ fs.mkdirSync(home, { recursive: true, mode: 0o700 });
245
+ rotateFailures(home);
246
+ return appendRegularLine(path.join(home, 'events.jsonl'), `${JSON.stringify(record)}\n`);
247
+ } catch {
248
+ return false;
249
+ }
250
+ }
251
+
252
+ // Canonical script name -> pre-normalization per-harness file name, for the fallback search only.
253
+ const LEGACY_SCRIPT_NAMES = {
254
+ 'session-context.sh': 'session-start.sh',
255
+ 'pre-implementation-gate.sh': 'pre-implement-gate.sh',
256
+ 'mcp-tool-guard.sh': 'mcp-tool-guard.sh',
257
+ 'stop-check.sh': 'stop-check.sh',
258
+ 'pre-bash-guard.sh': 'pre-bash-guard.sh',
259
+ };
260
+
261
+ // A hook payload is one tool call's JSON — a few KB in the overwhelming majority of cases, a few
262
+ // hundred KB for a large file edit at most. These bounds exist only to stop a misbehaving harness
263
+ // process (stdin that never closes, or streams unboundedly) from hanging this front door forever;
264
+ // they are far above anything a real payload should ever reach.
265
+ const MAX_STDIN_BYTES = 10 * 1024 * 1024;
266
+ const STDIN_TIMEOUT_MS = 5000;
267
+
268
+ function readStdin() {
269
+ return new Promise((resolve) => {
270
+ let data = '';
271
+ let settled = false;
272
+ const settle = (value) => {
273
+ if (settled) return;
274
+ settled = true;
275
+ process.stdin.removeAllListeners('data');
276
+ process.stdin.removeAllListeners('end');
277
+ clearTimeout(timer);
278
+ resolve(value);
279
+ };
280
+ const timer = setTimeout(() => settle(data), STDIN_TIMEOUT_MS);
281
+ process.stdin.setEncoding('utf8');
282
+ process.stdin.on('data', (chunk) => {
283
+ data += chunk;
284
+ if (data.length > MAX_STDIN_BYTES) settle(data);
285
+ });
286
+ process.stdin.on('end', () => {
287
+ settle(data);
288
+ });
289
+ if (process.stdin.isTTY) {
290
+ settle('');
291
+ }
292
+ });
293
+ }
294
+
295
+ // Resolve <dir>'s git toplevel, the way `git rev-parse --show-toplevel` would, without shelling
296
+ // out — this runner is invoked once per hook event, so avoiding a subprocess here matters for
297
+ // NFR-001. Walks up from <dir> looking for a `.git` entry (directory or file — a submodule/
298
+ // worktree gitlink is a file, not a directory) and returns the directory that contains it, which
299
+ // may differ from <dir> itself when <dir> is a subdirectory of the repo. Returns null if <dir> is
300
+ // not inside a git working tree at all.
301
+ function gitToplevelOf(dir) {
302
+ let d = dir;
303
+ while (true) {
304
+ if (fs.existsSync(path.join(d, '.git'))) return d;
305
+ const parent = path.dirname(d);
306
+ if (parent === d) return null;
307
+ d = parent;
308
+ }
309
+ }
310
+
311
+ function resolveProjectRoot(workspacePaths) {
312
+ // The replaced Antigravity shim (pre-implementation-gate.sh) iterated every documented
313
+ // workspacePaths entry, verifying each is a real git repo via `git -C "$dir" rev-parse
314
+ // --show-toplevel`, rather than trusting the first existing path — restored here so a
315
+ // multi-root workspace listing a non-git directory first still resolves to the actual
316
+ // DoFlow-tracked repo's toplevel rather than failing the gate open against the wrong directory.
317
+ if (Array.isArray(workspacePaths)) {
318
+ for (const candidate of workspacePaths) {
319
+ if (!candidate || !fs.existsSync(candidate)) continue;
320
+ const toplevel = gitToplevelOf(candidate);
321
+ if (toplevel) return toplevel;
322
+ }
323
+ // No candidate was inside a verified git repo; fall through rather than trusting an
324
+ // unverified one.
325
+ }
326
+ let d = process.cwd();
327
+ while (d !== path.dirname(d)) {
328
+ if (fs.existsSync(path.join(d, '.git')) || fs.existsSync(path.join(d, '.doflow'))) {
329
+ return d;
330
+ }
331
+ d = path.dirname(d);
332
+ }
333
+ return process.cwd();
334
+ }
335
+
336
+ /**
337
+ * Resolve a Canonical Policy Library script's absolute path. Primary location is this runner's
338
+ * own `policies/` sibling directory (core/harnesses/shared/hooks/policies/) — the canonical
339
+ * source of truth (design.md C1) that ships alongside this file, so the lookup needs no
340
+ * project-root guessing and works identically in-repo and once installed, as long as the
341
+ * `shared/hooks/` tree is projected together. The per-harness candidate search below is kept only
342
+ * as a fallback for an install that hasn't picked up the canonical policies/ directory yet.
343
+ */
344
+ function resolvePolicyScript(projectRoot, scriptName) {
345
+ const primary = path.join(__dirname, 'policies', scriptName);
346
+ if (fs.existsSync(primary)) return primary;
347
+
348
+ const legacyName = LEGACY_SCRIPT_NAMES[scriptName] || scriptName;
349
+ const candidates = [
350
+ path.join(projectRoot, '.gemini/hooks', legacyName),
351
+ path.join(projectRoot, '.claude/hooks', legacyName),
352
+ path.join(projectRoot, '.codex/hooks', legacyName),
353
+ path.join(projectRoot, '.doflow/scripts', legacyName),
354
+ ];
355
+ for (const candidate of candidates) {
356
+ if (fs.existsSync(candidate)) return candidate;
357
+ }
358
+ return null;
359
+ }
360
+
361
+ /**
362
+ * Delegate one canonical-decision evaluation to a Phase A policy script (design.md's Canonical
363
+ * Policy Script Contract): the canonical payload goes in on stdin, DOFLOW_PROJECT_DIR/DOFLOW_AGENT
364
+ * are set, and the exit code decides allow/deny — exit 0 = allow, non-zero = deny with the reason
365
+ * read from stderr (falling back to stdout if a script has none). Returns the CANONICAL decision
366
+ * shape ({decision:'allow'} | {decision:'deny', reason}); harness-native translation happens one
367
+ * layer up, in the adapter's toNative, not here — this function never speaks a harness-specific
368
+ * decision vocabulary.
369
+ */
370
+ function delegateToPolicy(projectRoot, agent, scriptName, canonicalPayload) {
371
+ const scriptPath = resolvePolicyScript(projectRoot, scriptName);
372
+ if (!scriptPath) return { decision: 'allow' };
373
+
374
+ try {
375
+ execFileSync('bash', [scriptPath], {
376
+ cwd: projectRoot,
377
+ input: JSON.stringify(canonicalPayload || {}),
378
+ stdio: ['pipe', 'pipe', 'pipe'],
379
+ env: { ...process.env, DOFLOW_PROJECT_DIR: projectRoot, DOFLOW_AGENT: agent },
380
+ });
381
+ return { decision: 'allow' };
382
+ } catch (err) {
383
+ // A real policy deny has a numeric exit status — the script ran and its own canonical exit
384
+ // code (1 = deny) is the contract this delegates to. `err.status` is null/undefined when the
385
+ // process never produced an exit code at all (bash missing from PATH, EACCES, etc.) — that is
386
+ // an execution fault, not a policy decision, and every sourced .sh policy in this library
387
+ // fails open on its own uncertainty (`command -v jq || exit 0`); this delegator matches that
388
+ // posture instead of turning an environment fault into a silent deny.
389
+ if (typeof err.status !== 'number') {
390
+ // A policy killed by a signal is a fault in DoFlow's own install worth recording (IC-018); a
391
+ // missing `bash` or a permission error is the user's environment and is not. Either way the
392
+ // decision is the same as it always was.
393
+ // SIGTERM, SIGINT and SIGPIPE are the user or the harness stopping the process (DEC-031), not
394
+ // a fault. Any other signal is recorded under its own kind, so a SEGV and a KILL do not share a fingerprint.
395
+ if (typeof err.signal === 'string' && err.signal && !['SIGTERM', 'SIGINT', 'SIGPIPE'].includes(err.signal)) {
396
+ captureHookFailure({ command: scriptName.replace(/\.sh$/, ''), kind: `policy-exec-fault:${err.signal}` });
397
+ }
398
+ return { decision: 'allow' };
399
+ }
400
+ const reason = err.stderr ? err.stderr.toString().trim() : (err.stdout ? err.stdout.toString().trim() : '');
401
+ return {
402
+ decision: 'deny',
403
+ reason: reason || `[${scriptName}] blocked by DoFlow guard policy`,
404
+ };
405
+ }
406
+ }
407
+
408
+ function classifyPreToolUsePolicy(toolName) {
409
+ if (MCP_TOOL_PATTERN.test(toolName)) return 'mcp-tool-guard.sh';
410
+ if (EDIT_TOOL_PATTERN.test(toolName)) return 'pre-implementation-gate.sh';
411
+ if (COMMAND_TOOL_PATTERN.test(toolName)) return 'pre-bash-guard.sh';
412
+ return null;
413
+ }
414
+
415
+ function evaluatePreToolUsePolicy(canonicalPayload, projectRoot, agent) {
416
+ const toolName = (canonicalPayload && canonicalPayload.tool_name) || '';
417
+ const scriptName = classifyPreToolUsePolicy(toolName);
418
+ if (!scriptName) return { decision: 'allow' };
419
+ return delegateToPolicy(projectRoot, agent, scriptName, canonicalPayload);
420
+ }
421
+
422
+ /**
423
+ * Evaluate one lifecycle event against the Canonical Policy Library, returning a CANONICAL
424
+ * decision. Every event this runner drives maps to at most one Phase A script:
425
+ * SessionStart -> session-context.sh (side-effect only; the script's own contract says it never
426
+ * denies, so its exit code is not propagated as a decision)
427
+ * PreToolUse -> mcp-tool-guard.sh | pre-implementation-gate.sh | pre-bash-guard.sh, chosen by
428
+ * the tool-name classification above
429
+ * Stop -> stop-check.sh
430
+ * PostToolUse, PreInvocation -> no Phase A policy owns these yet (post-edit-lint.sh's collector
431
+ * role is out of FR-001's four-policy scope) — ack only; the adapter's toNative
432
+ * decides the harness-native shape regardless of this placeholder decision.
433
+ */
434
+ function evaluateEvent(event, canonicalPayload, projectRoot, agent) {
435
+ switch (event) {
436
+ case 'SessionStart':
437
+ delegateToPolicy(projectRoot, agent, 'session-context.sh', canonicalPayload);
438
+ return { decision: 'allow' };
439
+
440
+ case 'PreToolUse':
441
+ return evaluatePreToolUsePolicy(canonicalPayload, projectRoot, agent);
442
+
443
+ case 'Stop':
444
+ return delegateToPolicy(projectRoot, agent, 'stop-check.sh', canonicalPayload);
445
+
446
+ case 'PostToolUse':
447
+ case 'PreInvocation':
448
+ default:
449
+ return { decision: 'allow' };
450
+ }
451
+ }
452
+
453
+ // Falls back to 'antigravity' (this runner's legacy default) for any value other than
454
+ // 'gemini', including unset/empty. An unrecognized non-empty value is likely a typo, so it
455
+ // gets a stderr warning even though the fallback still applies.
456
+ function resolveAgent() {
457
+ const raw = process.env.DOFLOW_AGENT || '';
458
+ const agent = raw.toLowerCase();
459
+ if (agent === 'gemini') return 'gemini';
460
+ if (raw && agent !== 'antigravity') {
461
+ process.stderr.write(`stream-hook-runner: unrecognized DOFLOW_AGENT="${raw}", falling back to antigravity\n`);
462
+ }
463
+ return 'antigravity';
464
+ }
465
+
466
+ function resolveAdapter(agent) {
467
+ return agent === 'gemini' ? geminiAdapter : antigravityAdapter;
468
+ }
469
+
470
+ function sniffEventFromPayload(payload) {
471
+ if (!payload || typeof payload !== 'object') return null;
472
+ // 'toolCall' in payload (key presence), not payload.toolCall (truthiness): a real Antigravity
473
+ // PreToolUse payload can carry a null/empty toolCall alongside stepIdx (also present on
474
+ // PostToolUse payloads) — truthiness alone let such a payload fall through to the stepIdx check
475
+ // below and get misclassified as PostToolUse, silently skipping the gate entirely
476
+ // (022-code-review finding). Checked before Stop/PostToolUse since a missed PreToolUse
477
+ // classification fails the gate open, the worse failure mode of the two.
478
+ if ('toolCall' in payload || payload.tool_name !== undefined) return 'PreToolUse';
479
+ if (payload.invocationNum !== undefined) return 'PreInvocation';
480
+ if (payload.terminationReason !== undefined || payload.executionNum !== undefined) return 'Stop';
481
+ if (payload.stepIdx !== undefined || payload.terminationBehavior !== undefined) return 'PostToolUse';
482
+ if (payload.session_id !== undefined && payload.cwd !== undefined) return 'SessionStart';
483
+ return null;
484
+ }
485
+
486
+ // ── Back-compat helpers ──────────────────────────────────────────────────────
487
+ //
488
+ // evaluateToolCall/evaluatePayload predate the event-dispatcher/adapter split and are kept for
489
+ // callers (including test/hooks/stream-hook-runner.test.js) that evaluate a single Antigravity-
490
+ // shaped payload directly without going through main()'s argv/env-driven dispatch. Both return the
491
+ // CANONICAL decision shape (never harness-native-translated) exactly as they always have.
492
+
493
+ function evaluateToolCall(toolCall, projectRoot) {
494
+ if (!toolCall || typeof toolCall !== 'object') {
495
+ return { decision: 'allow' };
496
+ }
497
+ const canonicalPayload = antigravityAdapter.toCanonical('PreToolUse', { toolCall });
498
+ return evaluatePreToolUsePolicy(canonicalPayload, projectRoot, 'antigravity');
499
+ }
500
+
501
+ function evaluatePayload(payload, projectRoot) {
502
+ if (!payload || typeof payload !== 'object') {
503
+ return { decision: 'allow' };
504
+ }
505
+
506
+ // PreToolUse: toolCall object present
507
+ if (payload.toolCall) {
508
+ return evaluateToolCall(payload.toolCall, projectRoot);
509
+ }
510
+
511
+ // PreInvocation: injection hook
512
+ if (payload.invocationNum !== undefined) {
513
+ return { injectSteps: [] };
514
+ }
515
+
516
+ // Stop hook: delegate to the canonical stop-check policy
517
+ if (payload.terminationReason !== undefined) {
518
+ const canonicalPayload = antigravityAdapter.toCanonical('Stop', payload);
519
+ return delegateToPolicy(projectRoot, 'antigravity', 'stop-check.sh', canonicalPayload);
520
+ }
521
+
522
+ // PostToolUse / PostInvocation: empty object response
523
+ if (payload.stepIdx !== undefined || payload.terminationBehavior !== undefined) {
524
+ return {};
525
+ }
526
+
527
+ return { decision: 'allow' };
528
+ }
529
+
530
+ async function main() {
531
+ try {
532
+ const explicitEvent = process.argv[2] || null;
533
+ const rawInput = await readStdin();
534
+
535
+ let payload = {};
536
+ if (rawInput && rawInput.trim()) {
537
+ try {
538
+ payload = JSON.parse(rawInput);
539
+ } catch {
540
+ payload = {};
541
+ }
542
+ }
543
+
544
+ const projectRoot = resolveProjectRoot(payload.workspacePaths);
545
+ const agent = resolveAgent();
546
+ const adapter = resolveAdapter(agent);
547
+ const event = explicitEvent || sniffEventFromPayload(payload);
548
+
549
+ if (!event) {
550
+ process.stdout.write(JSON.stringify({ decision: 'allow' }) + '\n');
551
+ return;
552
+ }
553
+
554
+ const canonicalPayload = adapter.toCanonical(event, payload);
555
+ const decision = evaluateEvent(event, canonicalPayload, projectRoot, agent);
556
+ const native = adapter.toNative(event, decision);
557
+
558
+ // An empty object is a harness's documented "say nothing" answer (e.g. Antigravity's Stop
559
+ // contract: "silence (exit 0, no stdout) lets the session end" — a positive field, not a
560
+ // missing one, is what a real decision looks like). Writing literal "{}" instead of true
561
+ // silence would be a byte-for-byte behavior change from every native shim this runner
562
+ // replaces, all of which stayed genuinely silent on this path.
563
+ if (native && typeof native === 'object' && Object.keys(native).length > 0) {
564
+ process.stdout.write(JSON.stringify(native) + '\n');
565
+ }
566
+ } catch (err) {
567
+ // Recorded silently when it is a programming error (IC-018); the output below is unchanged.
568
+ if (isProgrammingError(err)) captureHookFailure({ command: 'stream-hook-runner', kind: 'runner-exception', frame: runnerFrame(err) });
569
+ process.stderr.write(`[stream-hook-runner error] ${err.message}\n`);
570
+ process.stdout.write(JSON.stringify({ decision: 'allow' }) + '\n');
571
+ }
572
+ }
573
+
574
+ if (require.main === module) {
575
+ main();
576
+ }
577
+
578
+ module.exports = {
579
+ evaluateToolCall,
580
+ evaluatePayload,
581
+ resolveProjectRoot,
582
+ evaluateEvent,
583
+ resolveAgent,
584
+ resolveAdapter,
585
+ resolvePolicyScript,
586
+ classifyPreToolUsePolicy,
587
+ sniffEventFromPayload,
588
+ isProgrammingError,
589
+ captureHookFailure,
590
+ };
@@ -0,0 +1,87 @@
1
+ #!/bin/sh
2
+ # DoFlow locator shim — projected to all eight harnesses by the `locator.doflow` asset in
3
+ # core/registry/assets.json (design.md §3 C2, §4.1).
4
+ #
5
+ # Its ONLY job is to find the single dispatcher (at $REL below, under the config dir) and exec it
6
+ # with every argument passed through unmodified. It deliberately holds NO verb table and NO verb
7
+ # knowledge:
8
+ # the verb namespace lives in exactly one place — the dispatcher — so adding a verb never edits
9
+ # seven projected copies of this file. Its content is byte-identical on every harness; only the
10
+ # harness-native path it is written to differs.
11
+ #
12
+ # It is named `doflow-run` like the dispatcher it fronts, not `doflow`, so it is never confused
13
+ # with the installer CLI (`bin/doflow.js`, published as the `doflow` command) — a different program
14
+ # with a different verb surface.
15
+ #
16
+ # Resolution order (design.md §4.1), first hit wins. $REL is defined once below and interpolated
17
+ # into every step, so no step can drift onto a different path than the others:
18
+ # 1. $DOFLOW_CONFIG_DIR/$REL — explicit override; a documented extension point, not an active
19
+ # path. design.md §4.1 justified it as "what harness session hooks already export", which is
20
+ # false: nothing under core/harnesses/*/hooks/ exports DOFLOW_CONFIG_DIR, so step 1 never
21
+ # fires unless a caller sets the variable itself. Kept because that override is the only way
22
+ # to point at a non-standard install; do not restate the hook claim.
23
+ # 2. nearest .doflow/$REL walking up from the working directory — project-local wins over global.
24
+ # In practice this is the step that resolves, and it is the one a skill's own inline resolver
25
+ # reproduces.
26
+ # 3. $HOME/.doflow/$REL — global install
27
+ #
28
+ # Reaching this file is the caller's problem, not this file's. It resolves the dispatcher correctly
29
+ # whenever it is invoked by a path correct for the caller — but a skill cannot name it by a literal
30
+ # relative path: a relative path in a shell command resolves against the working directory, which is
31
+ # the user's project root, not the skill's directory. Skills therefore inline the step 2/3 walk-up
32
+ # directly (see any chain SKILL.md step 1) rather than calling this shim.
33
+ # None found: exit 2 — a resolution error under the dispatcher's uniform exit codes (0 success,
34
+ # 1 actionable finding, 2 usage/resolution error) — with one message naming all three places
35
+ # searched. design.md R3 accepts this outcome deliberately: a harness install with no `.doflow/`
36
+ # payload anywhere cannot reach the runtime, and one clear message is the required failure.
37
+ #
38
+ # Plain POSIX sh, no bashisms: this runs before anything else DoFlow ships, on whatever shell the
39
+ # harness hands it.
40
+
41
+ # The dispatcher ships inside the `scripts.doflow` asset (`core/shared/scripts`), so it sits at
42
+ # `scripts/doflow/bin/doflow-run` under the shared tree. Every harness projects that tree to its
43
+ # scope root (`<project>/.doflow` or `$HOME/.doflow`), so this one path holds for all eight,
44
+ # Antigravity at global scope excepted (it projects no runtime there).
45
+ REL='scripts/doflow/bin/doflow-run'
46
+ dispatcher=''
47
+
48
+ # 1. Explicit override.
49
+ if [ -n "${DOFLOW_CONFIG_DIR:-}" ] && [ -f "$DOFLOW_CONFIG_DIR/$REL" ]; then
50
+ dispatcher="$DOFLOW_CONFIG_DIR/$REL"
51
+ fi
52
+
53
+ # 2. Nearest project-local install, walking up. Terminates on the filesystem root and also on a
54
+ # parent that stops changing, so an unusual root (or a `dirname` that returns its input) cannot
55
+ # spin forever.
56
+ if [ -z "$dispatcher" ]; then
57
+ dir="${PWD:-$(pwd)}"
58
+ while [ -n "$dir" ]; do
59
+ if [ -f "$dir/.doflow/$REL" ]; then
60
+ dispatcher="$dir/.doflow/$REL"
61
+ break
62
+ fi
63
+ parent="$(dirname "$dir")"
64
+ [ "$parent" = "$dir" ] && break
65
+ dir="$parent"
66
+ done
67
+ fi
68
+
69
+ # 3. Global install.
70
+ if [ -z "$dispatcher" ] && [ -n "${HOME:-}" ] && [ -f "$HOME/.doflow/$REL" ]; then
71
+ dispatcher="$HOME/.doflow/$REL"
72
+ fi
73
+
74
+ if [ -z "$dispatcher" ]; then
75
+ cat >&2 <<EOF
76
+ doflow-run: no DoFlow runtime found. Searched for '$REL' in, in order:
77
+ 1. \$DOFLOW_CONFIG_DIR -> ${DOFLOW_CONFIG_DIR:-(unset)}
78
+ 2. nearest .doflow/ above the working directory -> ${PWD:-(unknown)}
79
+ 3. \$HOME/.doflow -> ${HOME:-(unset)}/.doflow
80
+ Install the runtime into one of them:
81
+ npx @khoavu882/doflow install -t <harness> # project-local, creates ./.doflow
82
+ npx @khoavu882/doflow install -t <harness> -g # global, creates \$HOME/.doflow
83
+ EOF
84
+ exit 2
85
+ fi
86
+
87
+ exec "$dispatcher" "$@"