@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,149 @@
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
+
8
+ /**
9
+ * Combines ranked lists using Reciprocal Rank Fusion (RRF).
10
+ * @param {Array<Array<{ id?: string, path?: string, locator?: string|Object }>>} rankedLists
11
+ * @param {{ k?: number }} [options]
12
+ * @returns {Array<{ id: string, rrfScore: number }>}
13
+ */
14
+ function reciprocalRankFusion(rankedLists: any[][], { k = 60 }: { k?: number } = {}): any[] {
15
+ const scores = new Map<string, number>();
16
+ const items = new Map<string, any>();
17
+
18
+ for (const list of rankedLists) {
19
+ if (!Array.isArray(list)) continue;
20
+ for (let rank = 0; rank < list.length; rank += 1) {
21
+ const item = list[rank];
22
+ if (!item) continue;
23
+ const id = item.id
24
+ || item.path
25
+ || (item.locator ? (typeof item.locator === 'string' ? item.locator : `${item.locator.file}:${item.locator.line || 1}`) : null);
26
+ if (!id) continue;
27
+ const rrf = 1 / (k + rank + 1);
28
+ scores.set(id, (scores.get(id) || 0) + rrf);
29
+ if (!items.has(id)) {
30
+ items.set(id, item);
31
+ }
32
+ }
33
+ }
34
+
35
+ const fused: any[] = [];
36
+ for (const [id, rrfScore] of scores.entries()) {
37
+ fused.push({
38
+ ...items.get(id),
39
+ id,
40
+ rrfScore: Number(rrfScore.toFixed(6)),
41
+ });
42
+ }
43
+
44
+ fused.sort((a: any, b: any) => b.rrfScore - a.rrfScore || (a.id < b.id ? -1 : 1));
45
+ return fused;
46
+ }
47
+
48
+ /**
49
+ * Normalizes a locator into a canonical repository-relative file path and line numbers.
50
+ * @param {string|Object} raw
51
+ * @param {string} [projectRoot]
52
+ * @returns {{ file: string, line?: number, endLine?: number }}
53
+ */
54
+ function normalizeLocator(raw: any, projectRoot: string = process.cwd()): { file: string; line?: number; endLine?: number } {
55
+ if (!raw) return { file: '' };
56
+ if (typeof raw === 'string') {
57
+ const match = raw.match(/^([^:]+)(?::(\d+))?(?::(\d+))?$/);
58
+ if (match) {
59
+ const relFile = path.isAbsolute(match[1]) ? path.relative(projectRoot, match[1]) : match[1];
60
+ return {
61
+ file: relFile,
62
+ ...(match[2] ? { line: parseInt(match[2], 10) } : {}),
63
+ };
64
+ }
65
+ return { file: path.isAbsolute(raw) ? path.relative(projectRoot, raw) : raw };
66
+ }
67
+ if (typeof raw === 'object' && raw.file) {
68
+ const relFile = path.isAbsolute(raw.file) ? path.relative(projectRoot, raw.file) : raw.file;
69
+ return {
70
+ file: relFile,
71
+ ...(raw.line ? { line: raw.line } : {}),
72
+ ...(raw.endLine ? { endLine: raw.endLine } : {}),
73
+ };
74
+ }
75
+ return { file: '' };
76
+ }
77
+
78
+ /**
79
+ * Fuses lexical and semantic search results using RRF and reads bounded live source.
80
+ * Ranking scores are preserved in candidate metadata and kept strictly outside evidence verdicts.
81
+ *
82
+ * @param {Object} params
83
+ * @param {Array<Object>} [params.lexicalResults=[]]
84
+ * @param {Array<Object>} [params.semanticResults=[]]
85
+ * @param {number} [params.k=60]
86
+ * @param {number} [params.topN=10]
87
+ * @param {string} [params.projectRoot]
88
+ * @param {number} [params.maxSnippetLines=50]
89
+ * @param {Object} [params.fsImpl]
90
+ * @returns {Array<Object>} Fused candidates with bounded snippets
91
+ */
92
+ function fuseHybridResults({
93
+ lexicalResults = [],
94
+ semanticResults = [],
95
+ k = 60,
96
+ topN = 10,
97
+ projectRoot = process.cwd(),
98
+ maxSnippetLines = 50,
99
+ fsImpl = fs,
100
+ }: {
101
+ lexicalResults?: any[];
102
+ semanticResults?: any[];
103
+ k?: number;
104
+ topN?: number;
105
+ projectRoot?: string;
106
+ maxSnippetLines?: number;
107
+ fsImpl?: any;
108
+ } = {}): any[] {
109
+ const normLex = (Array.isArray(lexicalResults) ? lexicalResults : []).map((item: any, idx: number) => {
110
+ const loc = normalizeLocator(item.locator || item.path || item.file, projectRoot);
111
+ const id = `${loc.file}:${loc.line || 1}`;
112
+ return { ...item, id, locator: loc, sourceList: 'lexical', originalRank: idx };
113
+ });
114
+
115
+ const normSem = (Array.isArray(semanticResults) ? semanticResults : []).map((item: any, idx: number) => {
116
+ const loc = normalizeLocator(item.locator || item.path || item.file, projectRoot);
117
+ const id = `${loc.file}:${loc.line || 1}`;
118
+ return { ...item, id, locator: loc, sourceList: 'semantic', originalRank: idx };
119
+ });
120
+
121
+ const fused = reciprocalRankFusion([normLex, normSem], { k });
122
+ const candidates = fused.slice(0, topN);
123
+
124
+ for (const c of candidates) {
125
+ if (!c.snippet && c.locator && c.locator.file) {
126
+ const fullPath = path.resolve(projectRoot, c.locator.file);
127
+ try {
128
+ if (fsImpl.existsSync(fullPath)) {
129
+ const content = fsImpl.readFileSync(fullPath, 'utf8');
130
+ const lines = content.split('\n');
131
+ const startLine = Math.max(1, (c.locator.line || 1) - 2);
132
+ const endLine = Math.min(lines.length, startLine + maxSnippetLines - 1);
133
+ c.snippet = lines.slice(startLine - 1, endLine).join('\n');
134
+ c.liveRead = { startLine, endLine, totalFileLines: lines.length };
135
+ }
136
+ } catch {
137
+ c.liveRead = null;
138
+ }
139
+ }
140
+ }
141
+
142
+ return candidates;
143
+ }
144
+
145
+ module.exports = {
146
+ reciprocalRankFusion,
147
+ normalizeLocator,
148
+ fuseHybridResults,
149
+ };
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ // Index store for the guidance corpus. The index is derived state under the project's
6
+ // own `.doflow/index/`: every indexed file is addressed by its sha256, so freshness is intrinsic —
7
+ // a manifest whose digests no longer match the tree IS stale, by construction, and rebuilding only
8
+ // re-chunks what actually changed. Chunks are stored as JSONL so the store stays diffable and
9
+ // streamable without a database dependency.
10
+
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+ const crypto = require('node:crypto');
14
+ const { chunkMarkdown } = require('./chunker.ts');
15
+
16
+ const INDEX_VERSION = 1;
17
+ const MANIFEST_FILE = 'manifest.json';
18
+ const CHUNKS_FILE = 'chunks.jsonl';
19
+
20
+ function sha256(buffer: any): string {
21
+ return crypto.createHash('sha256').update(buffer).digest('hex');
22
+ }
23
+
24
+ /** Every .md file under the corpus root, as sorted relative paths (deterministic builds). */
25
+ function listMarkdownFiles(corpusDir: string, { fsImpl = fs }: { fsImpl?: any } = {}): string[] {
26
+ const out: string[] = [];
27
+ const walk = (dir: string) => {
28
+ let entries: any[];
29
+ try { entries = fsImpl.readdirSync(dir, { withFileTypes: true }); } catch { return; }
30
+ entries.sort((a: any, b: any) => (a.name < b.name ? -1 : 1));
31
+ for (const entry of entries) {
32
+ const abs = path.join(dir, entry.name);
33
+ if (entry.isDirectory()) walk(abs);
34
+ else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) out.push(path.relative(corpusDir, abs));
35
+ }
36
+ };
37
+ walk(corpusDir);
38
+ return out.sort((a, b) => (a < b ? -1 : 1));
39
+ }
40
+
41
+ function readManifest(indexDir: string, { fsImpl = fs }: { fsImpl?: any } = {}): any {
42
+ try {
43
+ return JSON.parse(fsImpl.readFileSync(path.join(indexDir, MANIFEST_FILE), 'utf8'));
44
+ } catch { return null; }
45
+ }
46
+
47
+ function loadChunksByPath(indexDir: string, { fsImpl = fs }: { fsImpl?: any } = {}): Map<string, any[]> {
48
+ const byPath = new Map<string, any[]>();
49
+ let raw: string;
50
+ try { raw = fsImpl.readFileSync(path.join(indexDir, CHUNKS_FILE), 'utf8'); } catch { return byPath; }
51
+ for (const line of raw.split('\n')) {
52
+ if (!line.trim()) continue;
53
+ let chunk: any;
54
+ try { chunk = JSON.parse(line); } catch { continue; }
55
+ if (!byPath.has(chunk.path)) byPath.set(chunk.path, []);
56
+ byPath.get(chunk.path)!.push(chunk);
57
+ }
58
+ return byPath;
59
+ }
60
+
61
+ /** Build (or incrementally refresh) the index. Files whose digest already sits in the manifest
62
+ * reuse their stored chunks byte-for-byte; only changed or new files are re-chunked. Returns
63
+ * {files, chunks, rebuilt[], reusedCount}. */
64
+ function buildIndex({ corpusDir, indexDir, fsImpl = fs }: { corpusDir: string; indexDir: string; fsImpl?: any } = {} as any): any {
65
+ const relPaths = listMarkdownFiles(corpusDir, { fsImpl });
66
+ const previous = readManifest(indexDir, { fsImpl });
67
+ const previousFiles = previous?.files ?? {};
68
+ const reusableChunks = loadChunksByPath(indexDir, { fsImpl });
69
+
70
+ const files: Record<string, any> = {};
71
+ const allChunks: any[] = [];
72
+ const rebuilt: string[] = [];
73
+ let reusedCount = 0;
74
+ for (const rel of relPaths) {
75
+ const content = fsImpl.readFileSync(path.join(corpusDir, rel), 'utf8');
76
+ const digest = sha256(Buffer.from(content, 'utf8'));
77
+ const prior = previousFiles[rel];
78
+ if (prior && prior.digest === digest && reusableChunks.has(rel)) {
79
+ const chunks = reusableChunks.get(rel)!;
80
+ files[rel] = { digest, chunks: chunks.length };
81
+ allChunks.push(...chunks);
82
+ reusedCount += 1;
83
+ continue;
84
+ }
85
+ const chunks = chunkMarkdown(content, { path: rel });
86
+ files[rel] = { digest, chunks: chunks.length };
87
+ allChunks.push(...chunks);
88
+ rebuilt.push(rel);
89
+ }
90
+
91
+ fsImpl.mkdirSync(indexDir, { recursive: true });
92
+ const body = allChunks.map((chunk: any) => JSON.stringify(chunk)).join('\n') + (allChunks.length ? '\n' : '');
93
+ const tmpChunks = `${path.join(indexDir, CHUNKS_FILE)}.${process.pid}.tmp`;
94
+ fsImpl.writeFileSync(tmpChunks, body, 'utf8');
95
+ fsImpl.renameSync(tmpChunks, path.join(indexDir, CHUNKS_FILE));
96
+ const manifest = {
97
+ version: INDEX_VERSION,
98
+ builtAt: new Date().toISOString(),
99
+ files,
100
+ };
101
+ const tmpManifest = `${path.join(indexDir, MANIFEST_FILE)}.${process.pid}.tmp`;
102
+ fsImpl.writeFileSync(tmpManifest, JSON.stringify(manifest, null, 2), 'utf8');
103
+ fsImpl.renameSync(tmpManifest, path.join(indexDir, MANIFEST_FILE));
104
+
105
+ // Dropped files leave no orphan chunks behind: the rewrite above already excludes them.
106
+ return { files: relPaths.length, chunks: allChunks.length, rebuilt, reusedCount, manifest };
107
+ }
108
+
109
+ /** Load the index for querying. Returns null when absent/malformed; staleness is the caller's
110
+ * policy decision (see ensureFreshIndex), never a silent assumption here. */
111
+ function loadIndex({ indexDir, fsImpl = fs }: { indexDir: string; fsImpl?: any } = {} as any): any {
112
+ const manifest = readManifest(indexDir, { fsImpl });
113
+ if (!manifest || manifest.version !== INDEX_VERSION) return null;
114
+ const chunks: any[] = [];
115
+ let raw: string;
116
+ try { raw = fsImpl.readFileSync(path.join(indexDir, CHUNKS_FILE), 'utf8'); } catch { return null; }
117
+ for (const line of raw.split('\n')) {
118
+ if (!line.trim()) continue;
119
+ try { chunks.push(JSON.parse(line)); } catch { return null; }
120
+ }
121
+ return { manifest, chunks };
122
+ }
123
+
124
+ /** True when every corpus file's current digest matches the manifest. Missing extra corpus files
125
+ * (present on disk, absent from the manifest) also count as stale — they are not yet searchable. */
126
+ function isFresh({ corpusDir, indexDir, fsImpl = fs }: { corpusDir: string; indexDir: string; fsImpl?: any } = {} as any): boolean {
127
+ const manifest = readManifest(indexDir, { fsImpl });
128
+ if (!manifest) return false;
129
+ const relPaths = listMarkdownFiles(corpusDir, { fsImpl });
130
+ if (relPaths.length !== Object.keys(manifest.files ?? {}).length) return false;
131
+ for (const rel of relPaths) {
132
+ const recorded = manifest.files?.[rel];
133
+ if (!recorded) return false;
134
+ try {
135
+ const digest = sha256(fsImpl.readFileSync(path.join(corpusDir, rel)));
136
+ if (digest !== recorded.digest) return false;
137
+ } catch { return false; }
138
+ }
139
+ return true;
140
+ }
141
+
142
+ module.exports = {
143
+ INDEX_VERSION, MANIFEST_FILE, CHUNKS_FILE,
144
+ listMarkdownFiles, buildIndex, loadIndex, isFresh, sha256,
145
+ };
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ // Retrieval over the guidance index. Ranking is BM25 — pure, deterministic, and the correctness
6
+ // floor: BM25 plus the import graph answer questions well enough that no downstream stage depends
7
+ // on embeddings being present. Dense/rerank slots (models.json `slots`, resolved by
8
+ // model-router's resolveRetrievalSlot) stay inert until a slot is declared AND enabled — an
9
+ // absent embedding provider degrades to lexical-only, never to a silent half-answer.
10
+
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+ const { buildIndex, loadIndex, isFresh } = require('./index-store.ts');
14
+ const { finishRuntime, usageError, captureCaught } = require('../cli-result.ts');
15
+ const { resolveRunsLocation } = require('../trace/ledger.ts');
16
+
17
+ const BM25_K1 = 1.5;
18
+ const BM25_B = 0.75;
19
+ const STOPWORDS = new Set([
20
+ 'the', 'a', 'an', 'and', 'or', 'but', 'of', 'to', 'in', 'on', 'for', 'with', 'as', 'by', 'at',
21
+ 'from', 'is', 'are', 'was', 'were', 'be', 'been', 'it', 'its', 'this', 'that', 'these', 'those',
22
+ 'do', 'does', 'did', 'how', 'what', 'when', 'which', 'who', 'why', 'not', 'no', 'yes', 'can',
23
+ ]);
24
+
25
+ function tokenize(text: any): string[] {
26
+ return String(text ?? '')
27
+ .toLowerCase()
28
+ .split(/[^a-z0-9]+/)
29
+ .filter((token) => token.length >= 2 && !STOPWORDS.has(token));
30
+ }
31
+
32
+ /** Deterministic BM25 (Okapi) over preloaded chunks. Corpus sizes here are thousands of chunks at
33
+ * most, so scoring at query time from plain arrays beats maintaining incremental postings. */
34
+ function bm25Rank(queryText: string, chunks: any[], { k = 5 }: { k?: number } = {}): any[] {
35
+ const queryTerms = [...new Set(tokenize(queryText))];
36
+ if (!queryTerms.length || !chunks.length) return [];
37
+ const docTokens = chunks.map((chunk: any) => tokenize(`${chunk.title ?? ''} ${chunk.text}`));
38
+ const avgLen = docTokens.reduce((sum: number, tokens: string[]) => sum + tokens.length, 0) / docTokens.length || 1;
39
+
40
+ const df = new Map<string, number>();
41
+ for (const term of queryTerms) {
42
+ let count = 0;
43
+ for (const tokens of docTokens) if (tokens.includes(term)) count += 1;
44
+ df.set(term, count);
45
+ }
46
+
47
+ const scored: Array<{ score: number; chunk: any }> = [];
48
+ for (let i = 0; i < chunks.length; i += 1) {
49
+ const tokens = docTokens[i];
50
+ if (!tokens.length) continue;
51
+ const tf = new Map<string, number>();
52
+ for (const token of tokens) tf.set(token, (tf.get(token) ?? 0) + 1);
53
+ let score = 0;
54
+ for (const term of queryTerms) {
55
+ const freq = tf.get(term) ?? 0;
56
+ if (!freq) continue;
57
+ const occurrences = df.get(term) || 0;
58
+ const idf = Math.log(1 + (chunks.length - occurrences + 0.5) / (occurrences + 0.5));
59
+ score += idf * ((freq * (BM25_K1 + 1)) / (freq + BM25_K1 * (1 - BM25_B + BM25_B * (tokens.length / avgLen))));
60
+ }
61
+ if (score > 0) scored.push({ score, chunk: chunks[i] });
62
+ }
63
+ scored.sort((a, b) => b.score - a.score || (a.chunk.id < b.chunk.id ? -1 : 1));
64
+ return scored.slice(0, k).map(({ chunk, score }) => ({ ...chunk, score }));
65
+ }
66
+
67
+ /** Registry-derived document graph: every `@file.md` import inside a corpus document is an edge.
68
+ * Guidance's own path-anchor rule makes these resolvable against the corpus root, which turns the
69
+ * content itself into the knowledge graph — no LLM extraction, no external store. */
70
+ function buildImportGraph(corpusDir: string, { fsImpl = fs }: { fsImpl?: any } = {}): Map<string, string[]> {
71
+ const edges = new Map<string, string[]>();
72
+ try { fsImpl.readdirSync(corpusDir); } catch { return edges; }
73
+ const walk = (dir: string, rel: string) => {
74
+ for (const entry of fsImpl.readdirSync(dir, { withFileTypes: true })) {
75
+ const abs = path.join(dir, entry.name);
76
+ const relPath = rel ? `${rel}/${entry.name}` : entry.name;
77
+ if (entry.isDirectory()) { walk(abs, relPath); continue; }
78
+ if (!entry.name.toLowerCase().endsWith('.md')) continue;
79
+ const text = (() => { try { return fsImpl.readFileSync(abs, 'utf8'); } catch { return ''; } })();
80
+ const targets = new Set<string>();
81
+ for (const match of text.matchAll(/(?:^|\s)@([^\s`]+\.md)\b/g)) {
82
+ const target = path.posix.normalize(match[1].replace(/^\.\//, ''));
83
+ if (target.startsWith('..') || path.isAbsolute(target)) continue;
84
+ targets.add(target);
85
+ }
86
+ if (targets.size) edges.set(relPath, [...targets]);
87
+ }
88
+ };
89
+ walk(corpusDir, '');
90
+ return edges;
91
+ }
92
+
93
+ /** Expand BM25 hits along import edges (1 hop): a linked document is "what to read next" for the
94
+ * question that surfaced its anchor. Graph results are capped separately and never displace
95
+ * lexical hits — they extend the answer instead of reranking it. */
96
+ function expandViaGraph(hits: any[], chunks: any[], graph: Map<string, string[]>, { maxExtra = 3 }: { maxExtra?: number } = {}): any[] {
97
+ const hitPaths = new Set(hits.map((h: any) => h.path));
98
+ const seen = new Set<string>();
99
+ const extras: any[] = [];
100
+ for (const hit of hits) {
101
+ for (const target of graph.get(hit.path) ?? []) {
102
+ if (hitPaths.has(target) || seen.has(target)) continue;
103
+ seen.add(target);
104
+ const linkedChunks = chunks.filter((c: any) => c.path === target);
105
+ if (!linkedChunks.length) continue;
106
+ const best = linkedChunks.reduce((a: any, b: any) => (b.text.length > a.text.length ? b : a));
107
+ extras.push({
108
+ path: best.path,
109
+ title: best.title,
110
+ score: null,
111
+ snippet: `${best.text}`.replace(/\s+/g, ' ').slice(0, 200),
112
+ viaGraph: hit.path,
113
+ });
114
+ if (extras.length >= maxExtra) return extras;
115
+ }
116
+ }
117
+ return extras;
118
+ }
119
+
120
+ /** Ensure the on-disk index matches the corpus, rebuilding only what changed, then rank.
121
+ * Returns {results, rebuilt}. A missing corpus is the caller's usage problem, thrown loudly. */
122
+ function searchGuidance({ corpusDir, indexDir, query, k = 5, fsImpl = fs }: { corpusDir: string; indexDir: string; query: string; k?: number; fsImpl?: any } = {} as any): any {
123
+ if (!fsImpl.existsSync(corpusDir)) {
124
+ throw new Error(`No guidance tree found at '${corpusDir}' — run \`doflow install\` first`);
125
+ }
126
+ let rebuiltFiles: string[] | null = null;
127
+ if (!isFresh({ corpusDir, indexDir, fsImpl })) {
128
+ const build = buildIndex({ corpusDir, indexDir, fsImpl });
129
+ rebuiltFiles = build.rebuilt;
130
+ }
131
+ const loaded = loadIndex({ indexDir, fsImpl });
132
+ if (!loaded) throw new Error(`Index at '${indexDir}' is unreadable after rebuild`);
133
+ const ranked = bm25Rank(query, loaded.chunks, { k }).map((hit: any) => ({
134
+ path: hit.path,
135
+ title: hit.title,
136
+ score: Number(hit.score.toFixed(4)),
137
+ snippet: `${hit.text}`.replace(/\s+/g, ' ').slice(0, 200),
138
+ }));
139
+ const graphHits = expandViaGraph(ranked, loaded.chunks, buildImportGraph(corpusDir, { fsImpl }));
140
+ return { results: [...ranked, ...graphHits], rebuilt: rebuiltFiles };
141
+ }
142
+
143
+ /** CLI handler for `doflow retrieve`. The corpus is the caller's installed guidance tree; the
144
+ * index lives beside it under `.doflow/index/`. Same two-roots posture as readiness: templates and
145
+ * code from this install, knowledge from the project being worked on. The `.doflow` root is
146
+ * $DOFLOW_CONFIG_DIR, else the nearest one above `stateRoot` that holds a guidance tree, else the
147
+ * home install; the index is kept beside the corpus chosen. */
148
+ function handleRetrieveCommand({ query, top, json = false, stateRoot }: { query?: any; top?: any; json?: boolean; stateRoot?: any } = {}): any {
149
+ const { configDir } = resolveRunsLocation({ start: stateRoot, containing: 'guidance' });
150
+ const corpusDir = path.join(configDir, 'guidance');
151
+ const indexDir = path.join(configDir, 'index', 'guidance');
152
+ if (!query || !String(query).trim()) {
153
+ return usageError('retrieve', '--query is required (what should be looked up in the guidance tree?)', json);
154
+ }
155
+ const limit = Math.max(1, Math.min(Number(top) || 5, 25));
156
+ try {
157
+ const { results, rebuilt } = searchGuidance({ corpusDir, indexDir, query: String(query), k: limit });
158
+ if (!json && rebuilt) console.log(`[INFO] Index refreshed (${rebuilt.length} file(s) re-chunked)`);
159
+ if (json) {
160
+ console.log(JSON.stringify({ query, results }, null, 2));
161
+ } else if (!results.length) {
162
+ console.log('[OK] No matching guidance found.');
163
+ } else {
164
+ results.forEach((result: any, i: number) => {
165
+ console.log(`${i + 1}. [${result.score}] ${result.path}${result.title ? ` — ${result.title}` : ''}`);
166
+ console.log(` ${result.snippet}`);
167
+ });
168
+ }
169
+ return finishRuntime(0);
170
+ } catch (error: any) {
171
+ captureCaught(error, 'retrieve', 1);
172
+ console.error(`[ERROR] retrieve: ${error.message}`);
173
+ return finishRuntime(1);
174
+ }
175
+ }
176
+
177
+ const {
178
+ reciprocalRankFusion,
179
+ normalizeLocator,
180
+ fuseHybridResults,
181
+ } = require('./hybrid.ts');
182
+
183
+ module.exports = {
184
+ bm25Rank,
185
+ tokenize,
186
+ searchGuidance,
187
+ buildImportGraph,
188
+ expandViaGraph,
189
+ handleRetrieveCommand,
190
+ STOPWORDS,
191
+ reciprocalRankFusion,
192
+ normalizeLocator,
193
+ fuseHybridResults,
194
+ };
@@ -0,0 +1,185 @@
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 { finishRuntime, usageError } = require('./cli-result.ts');
8
+
9
+ /**
10
+ * Reports DoFlow's own process vocabulary appearing in files that ship to people who never used
11
+ * DoFlow (FR-009, FR-010).
12
+ *
13
+ * The rule lives here, in one module, because it has two callers: the Stop hook scans the turn's
14
+ * edited files, and `/do-code-review` scans the reviewed set. Implementing it twice — once in bash
15
+ * and once in Python — is what G12's one-implementation-per-verb rule exists to prevent, and the
16
+ * drift would be invisible: the two would disagree about what counts as a leak and neither would
17
+ * say so (design R6).
18
+ *
19
+ * This module reports and never blocks. A legitimate occurrence exists — documentation *about*
20
+ * DoFlow — so a false positive that halted a write would cost more than the leak it prevented.
21
+ */
22
+
23
+ /**
24
+ * The vocabulary, as data rather than scattered through the callers, so a false positive is a
25
+ * one-line change here (design R4).
26
+ *
27
+ * Deliberately absent: a bare component reference like `C1`/`C#`. `C#` is a language name, so that
28
+ * pattern would report every shipped mention of C# as a DoFlow leak, and `C1` is too generic to
29
+ * carry meaning on its own. Precision matters more than coverage for a check nobody can silence.
30
+ */
31
+ const LEAK_PATTERNS = [
32
+ { id: 'requirement-item', label: 'requirement item reference', pattern: /\b(?:FR|NFR)-\d{3}\b/ },
33
+ { id: 'story-item', label: 'user story reference', pattern: /\bUS\d+\b/ },
34
+ { id: 'artifact-path', label: 'DoFlow artifact path', pattern: /\bagent-docs\// },
35
+ { id: 'state-path', label: 'DoFlow state path', pattern: /\.doflow\/state\// },
36
+ { id: 'chain-artifact', label: 'chain artifact cross-reference', pattern: /\b(?:requirement|design|plan)\.md\b/ },
37
+ ];
38
+
39
+ /** Occurrences inside the artifact tree are correct usage, so paths are excluded before matching. */
40
+ const DEFAULT_EXCLUDED_SEGMENTS = ['agent-docs'];
41
+
42
+ // The reasons a named path produced no findings without being clean — 'excluded', 'unreadable' and
43
+ // 'not-a-file' — are returned by `readScannable` below and are stated there. They were also declared
44
+ // here as an exported Set that nothing read, including no test: a second place to keep the list
45
+ // correct, with nothing to notice when the two disagreed.
46
+
47
+ function isExcluded(relPath: string, excludedSegments: string[]): boolean {
48
+ const segments = relPath.split(path.sep);
49
+ return excludedSegments.some((seg) => segments.includes(seg));
50
+ }
51
+
52
+ /**
53
+ * Resolves one requested path to readable content, or to the reason it was not read.
54
+ *
55
+ * Split out of `scanPaths` so that function is a loop over outcomes rather than a loop with path
56
+ * resolution, exclusion and error handling folded into it. Every return here is one of the two
57
+ * shapes `scanPaths` knows how to file.
58
+ *
59
+ * @returns {{relative: string, content: string}|{relative: string, reason: string}}
60
+ */
61
+ function readScannable(target: string, root: string, excluded: string[], impl: any): { relative: string; content?: string; reason?: string } {
62
+ const absolute = path.isAbsolute(target) ? target : path.join(root, target);
63
+ const relative = path.relative(root, absolute);
64
+
65
+ if (isExcluded(relative, excluded)) return { relative, reason: 'excluded' };
66
+
67
+ try {
68
+ if (!impl.existsSync(absolute) || !impl.statSync(absolute).isFile()) {
69
+ return { relative, reason: 'not-a-file' };
70
+ }
71
+ return { relative, content: impl.readFileSync(absolute, 'utf8') };
72
+ } catch {
73
+ // An unreadable path is reported, never fatal: this runs from a hook that must not fail a
74
+ // turn, and from a review that must not lose the rest of its file set to one bad path.
75
+ return { relative, reason: 'unreadable' };
76
+ }
77
+ }
78
+
79
+ /** Every pattern match in one file's lines, as finding records. */
80
+ function findLeaksInLines(lines: string[], relative: string): any[] {
81
+ const findings: any[] = [];
82
+ for (let i = 0; i < lines.length; i += 1) {
83
+ for (const rule of LEAK_PATTERNS) {
84
+ const match = lines[i].match(rule.pattern);
85
+ if (match) {
86
+ findings.push({ file: relative, line: i + 1, pattern: rule.id, label: rule.label, text: match[0] });
87
+ }
88
+ }
89
+ }
90
+ return findings;
91
+ }
92
+
93
+ /**
94
+ * Scans the named paths for internal-identifier occurrences.
95
+ *
96
+ * Every path given is accounted for in exactly one of `findings`' file set, `scanned`, or
97
+ * `unscanned` — a path that was skipped is reported as skipped rather than dropped, for the same
98
+ * reason the review bundle must report the files it could not read.
99
+ *
100
+ * @param {Object} options
101
+ * @param {string[]} options.paths files to scan; relative paths resolve against `repoRoot`
102
+ * @param {string} [options.repoRoot] defaults to cwd
103
+ * @param {string[]} [options.excludedSegments] path segments whose contents are correct usage
104
+ * @param {Object} [options.fsImpl] injectable fs, for tests
105
+ * @returns {{findings: Array, scanned: string[], unscanned: Array}}
106
+ */
107
+ function scanPaths({ paths: targets, repoRoot, excludedSegments, fsImpl }: any = {}): { findings: any[]; scanned: string[]; unscanned: any[] } {
108
+ const impl = fsImpl || fs;
109
+ const root = repoRoot || process.cwd();
110
+ const excluded = excludedSegments || DEFAULT_EXCLUDED_SEGMENTS;
111
+
112
+ const findings: any[] = [];
113
+ const scanned: string[] = [];
114
+ const unscanned: any[] = [];
115
+
116
+ for (const target of targets || []) {
117
+ if (typeof target !== 'string' || target.trim() === '') continue;
118
+
119
+ const read = readScannable(target, root, excluded, impl);
120
+ if (read.reason) {
121
+ unscanned.push({ file: read.relative, reason: read.reason });
122
+ continue;
123
+ }
124
+
125
+ scanned.push(read.relative);
126
+ findings.push(...findLeaksInLines((read.content as string).split('\n'), read.relative));
127
+ }
128
+
129
+ return { findings, scanned, unscanned };
130
+ }
131
+
132
+ /**
133
+ * Handles `doflow leak-scan`. Exit 0 clean, 1 on findings, and never non-zero for a path it could
134
+ * not read — that path is reported as unscanned instead.
135
+ *
136
+ * @param {Object} options
137
+ * @param {string[]} options.paths
138
+ * @param {string[]} [options.exclude] extra path segments to skip, on top of the artifact directory
139
+ * @param {boolean} [options.json=false]
140
+ * @param {string} [options.repoRoot]
141
+ * @returns {number} exit code
142
+ */
143
+ function handleLeakScanCommand({ paths: targets, exclude, json = false, repoRoot }: any = {}): number {
144
+ if (!Array.isArray(targets) || targets.length === 0) {
145
+ return usageError('leak-scan', '--path is required (repeatable): the files to scan', json);
146
+ }
147
+
148
+ // `--exclude` extends the built-in artifact-directory exclusion rather than replacing it: a
149
+ // caller narrowing the scan should not be able to accidentally widen it back over agent-docs/.
150
+ const extra = Array.isArray(exclude) ? exclude.filter((e: any) => typeof e === 'string' && e.trim() !== '') : [];
151
+ const excludedSegments = [...DEFAULT_EXCLUDED_SEGMENTS, ...extra];
152
+
153
+ const result = scanPaths({ paths: targets, repoRoot, excludedSegments });
154
+
155
+ if (json) {
156
+ // `scannedCount` sits next to `findingsCount` so the two are read together. With only
157
+ // `findingsCount`, a scan that examined nothing — every `--path` a directory, a typo, or
158
+ // excluded — serialized as `findingsCount: 0` and exit 0, which is the same shape as a genuinely
159
+ // clean scan. The human branch below already distinguishes them by printing the scanned count
160
+ // and the "Not scanned" list; the JSON branch did not, and `/do-code-review`'s process-leak step
161
+ // reads the JSON.
162
+ console.log(JSON.stringify({
163
+ ...result,
164
+ findingsCount: result.findings.length,
165
+ scannedCount: result.scanned.length,
166
+ }, null, 2));
167
+ } else {
168
+ console.log('\nDoFlow leak scan:');
169
+ console.log('═'.repeat(78));
170
+ if (result.findings.length === 0) {
171
+ console.log(`No internal identifiers found in ${result.scanned.length} scanned file(s).`);
172
+ } else {
173
+ for (const f of result.findings) console.log(` ${f.file}:${f.line} ${f.text} (${f.label})`);
174
+ }
175
+ if (result.unscanned.length > 0) {
176
+ console.log('\nNot scanned:');
177
+ for (const u of result.unscanned) console.log(` ${u.file} — ${u.reason}`);
178
+ }
179
+ console.log('═'.repeat(78) + '\n');
180
+ }
181
+
182
+ return finishRuntime(result.findings.length > 0 ? 1 : 0);
183
+ }
184
+
185
+ module.exports = { scanPaths, handleLeakScanCommand, LEAK_PATTERNS };