@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,863 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ /**
6
+ * Capability health probes and the health-aware `doctor` report (plan task C.3, components C4 and
7
+ * C9; requirement FR-013).
8
+ *
9
+ * The distinction this module exists to make: **a binary on `PATH` is not a working capability**.
10
+ * `capabilities` answers "which provider would be selected" from presence; `doctor` answers
11
+ * "did the provider actually answer", which is a different question and the only one that can
12
+ * catch a `semble` whose index is missing, a `graphify` whose graph was never built, or an `rtk`
13
+ * that dies on startup. A provider that is installed but cannot answer reports UNHEALTHY, and a
14
+ * provider that cannot be probed at all reports UNVERIFIED — never HEALTHY. That last rule is
15
+ * deliberate: this feature's own audit found three defects of the shape "reported PASS over no
16
+ * evidence" (a check with no command, a contract with no checks, a readiness verdict about the
17
+ * wrong task), and "the binary exists, so call it healthy" is the same defect wearing a hat.
18
+ */
19
+
20
+ const fs = require('node:fs');
21
+ const os = require('node:os');
22
+ const path = require('node:path');
23
+ const { execFileSync } = require('node:child_process');
24
+ const { CapabilityRouter } = require('./capability-router.ts');
25
+ const { loadRegistry } = require('../registry/index.ts');
26
+ const { evaluateReach } = require('./reach.ts');
27
+ const { mcpReach } = require('./mcp-reach.ts');
28
+ const { OUTCOME_LABELS } = require('./mcp-consultations.ts');
29
+ const { REPO_ROOT } = require('../helper/repo-root.ts');
30
+
31
+ /** Answered a probe. */
32
+ const HEALTHY = 'HEALTHY';
33
+ /** Present, but the probe failed, timed out, or returned nothing. */
34
+ const UNHEALTHY = 'UNHEALTHY';
35
+ /** Not installed. For an optional provider this is a normal state, not a fault. */
36
+ const ABSENT = 'ABSENT';
37
+ /** Installed (or needs no binary) but declares no probe, so function cannot be established. */
38
+ const UNVERIFIED = 'UNVERIFIED';
39
+
40
+ /** A probe must be fast enough that `doctor` stays interactive with every provider installed. */
41
+ const PROBE_TIMEOUT_MS = 2500;
42
+
43
+ /** Version banners are one short line; anything longer is not a banner and is not worth printing. */
44
+ const BANNER_MAX = 80;
45
+
46
+ /**
47
+ * Where an index- or graph-backed provider keeps the artifact whose age determines whether its
48
+ * answers can be trusted. Each entry lists every location DoFlow knows to look in; when none of
49
+ * them exists the result is UNKNOWN naming the paths searched, because a provider that owns its
50
+ * own cache layout must not have a freshness verdict invented for it.
51
+ */
52
+ const PROVIDER_ARTIFACTS: Record<string, any> = Object.freeze({
53
+ 'graphify.query': {
54
+ label: 'code graph',
55
+ candidates: (root: any) => [path.join(root, 'graphify-out', 'graph.json')],
56
+ refresh: 'graphify update .',
57
+ },
58
+ 'semble.search': {
59
+ label: 'semantic index',
60
+ candidates: (root: any, home: any) => [
61
+ path.join(root, '.semble', 'index.json'),
62
+ path.join(root, '.semble'),
63
+ path.join(home, '.cache', 'semble', 'index'),
64
+ ],
65
+ refresh: 'semble search "<any query>" . (rebuilds the index on first run)',
66
+ },
67
+ });
68
+
69
+ /** Directories a source-freshness scan must not descend into: build output, dependency trees and
70
+ * the provider artifacts themselves — a graph that indexed itself would always look fresh. */
71
+ const SCAN_SKIP = new Set([
72
+ '.git', 'node_modules', 'graphify-out', '.doflow', 'dist', 'build', 'out', 'coverage',
73
+ '.venv', 'venv', '__pycache__', '.next', '.nuxt', 'target', '.cache', '.pytest_cache', 'tmp',
74
+ ]);
75
+
76
+ /** A bound on the freshness scan. Hitting it does not produce a guess — it produces UNKNOWN. */
77
+ const SCAN_FILE_LIMIT = 4000;
78
+
79
+ /**
80
+ * Runs a provider's declared probe command.
81
+ * @param {string[]} command
82
+ * @param {Object} [options]
83
+ * @param {Function} [options.execFileImpl=execFileSync]
84
+ * @param {number} [options.timeoutMs=PROBE_TIMEOUT_MS]
85
+ * @returns {{ok: boolean, output: string, durationMs: number, timedOut: boolean, error: string|null}}
86
+ */
87
+ function runProbe(command: any, { execFileImpl = execFileSync, timeoutMs = PROBE_TIMEOUT_MS }: any = {}) {
88
+ const [bin, ...args] = command;
89
+ const started = Number(process.hrtime.bigint() / 1000000n);
90
+ try {
91
+ const stdout = execFileImpl(bin, args, {
92
+ encoding: 'utf8',
93
+ timeout: timeoutMs,
94
+ stdio: ['ignore', 'pipe', 'pipe'],
95
+ });
96
+ return {
97
+ ok: true,
98
+ output: String(stdout || '').trim(),
99
+ durationMs: Number(process.hrtime.bigint() / 1000000n) - started,
100
+ timedOut: false,
101
+ error: null,
102
+ };
103
+ } catch (error: any) {
104
+ // A killed process and a non-zero exit are both "did not answer", but they are different
105
+ // repairs — a timeout usually means a first-run index build, a non-zero exit usually means a
106
+ // broken install — so they are reported apart rather than as one failure string.
107
+ const timedOut = error.killed === true || error.code === 'ETIMEDOUT' || error.signal === 'SIGTERM';
108
+ return {
109
+ ok: false,
110
+ output: '',
111
+ durationMs: Number(process.hrtime.bigint() / 1000000n) - started,
112
+ timedOut,
113
+ error: timedOut ? `probe exceeded ${timeoutMs}ms` : (error.message || 'probe failed'),
114
+ };
115
+ }
116
+ }
117
+
118
+ /** First line of probe output, trimmed to a banner. Control characters are stripped so a provider
119
+ * cannot rewrite the doctor report with an escape sequence. */
120
+ function banner(output: any) {
121
+ if (!output) return null;
122
+ // Control characters are removed by code point rather than by a literal class, so this source
123
+ // file stays free of the escape sequences it is defending against.
124
+ const firstLine = [...output.split('\n')[0]]
125
+ .map((character) => (character.codePointAt(0) < 0x20 || character.codePointAt(0) === 0x7f ? ' ' : character))
126
+ .join('')
127
+ .trim();
128
+ return firstLine.length > BANNER_MAX ? `${firstLine.slice(0, BANNER_MAX - 1)}…` : firstLine;
129
+ }
130
+
131
+ /**
132
+ * Newest modification time among the project's own source files, used as the lower bound an index
133
+ * must be at least as new as.
134
+ * @param {string} root
135
+ * @param {Object} [options]
136
+ * @param {Object} [options.fsImpl=fs]
137
+ * @param {number} [options.limit=SCAN_FILE_LIMIT]
138
+ * @returns {{mtimeMs: number|null, filesScanned: number, truncated: boolean}}
139
+ */
140
+ function newestSourceMtime(root: any, { fsImpl = fs, limit = SCAN_FILE_LIMIT }: any = {}) {
141
+ let mtimeMs = null;
142
+ let filesScanned = 0;
143
+ let truncated = false;
144
+ const queue = [root];
145
+
146
+ while (queue.length) {
147
+ if (filesScanned >= limit) { truncated = true; break; }
148
+ const dir = queue.shift();
149
+ let entries;
150
+ try {
151
+ entries = fsImpl.readdirSync(dir, { withFileTypes: true });
152
+ } catch {
153
+ continue; // An unreadable directory narrows the scan; it does not fail the probe.
154
+ }
155
+ for (const entry of entries) {
156
+ if (entry.name.startsWith('.') && entry.isDirectory()) continue;
157
+ if (SCAN_SKIP.has(entry.name)) continue;
158
+ const full = path.join(dir, entry.name);
159
+ if (entry.isDirectory()) { queue.push(full); continue; }
160
+ if (!entry.isFile()) continue;
161
+ if (filesScanned >= limit) { truncated = true; break; }
162
+ filesScanned += 1;
163
+ try {
164
+ const stat = fsImpl.statSync(full);
165
+ if (mtimeMs === null || stat.mtimeMs > mtimeMs) mtimeMs = stat.mtimeMs;
166
+ } catch {
167
+ // Vanished between readdir and stat — a normal race in a working tree.
168
+ }
169
+ }
170
+ }
171
+ return { mtimeMs, filesScanned, truncated };
172
+ }
173
+
174
+ /**
175
+ * Freshness of a provider's index or graph relative to the project's source.
176
+ * @param {string} providerId
177
+ * @param {Object} [options]
178
+ * @param {string} [options.root=process.cwd()]
179
+ * @param {string} [options.homeDir]
180
+ * @param {Object} [options.fsImpl=fs]
181
+ * @returns {Object|null} null when the provider is not index-backed
182
+ */
183
+ function probeFreshness(providerId: any, { root = process.cwd(), homeDir = os.homedir(), fsImpl = fs }: any = {}) {
184
+ const spec = PROVIDER_ARTIFACTS[providerId];
185
+ if (!spec) return null;
186
+
187
+ const searched = spec.candidates(path.resolve(root), path.resolve(homeDir || '.'));
188
+ let artifact = null;
189
+ let artifactMtimeMs = null;
190
+ for (const candidate of searched) {
191
+ try {
192
+ const stat = fsImpl.statSync(candidate);
193
+ artifact = candidate;
194
+ artifactMtimeMs = stat.mtimeMs;
195
+ break;
196
+ } catch {
197
+ // Not at this location; try the next.
198
+ }
199
+ }
200
+
201
+ const base = { providerId, label: spec.label, searched, artifact, refresh: spec.refresh };
202
+ if (!artifact) {
203
+ return {
204
+ ...base,
205
+ state: 'UNKNOWN',
206
+ reason: `no ${spec.label} found at any location DoFlow knows to check; the provider owns its own layout, so no age can be established for it`,
207
+ };
208
+ }
209
+
210
+ const source = newestSourceMtime(path.resolve(root), { fsImpl });
211
+ if (source.mtimeMs === null) {
212
+ return { ...base, state: 'UNKNOWN', indexedAt: new Date(artifactMtimeMs).toISOString(), reason: 'no source file was readable under the project root, so there is nothing to compare the index against' };
213
+ }
214
+ const behindMs = Math.round(source.mtimeMs - artifactMtimeMs);
215
+ if (behindMs > 0) {
216
+ return {
217
+ ...base,
218
+ state: 'STALE',
219
+ indexedAt: new Date(artifactMtimeMs).toISOString(),
220
+ newestSourceAt: new Date(source.mtimeMs).toISOString(),
221
+ behindMs,
222
+ filesScanned: source.filesScanned,
223
+ reason: `source has changed since the ${spec.label} was built`,
224
+ };
225
+ }
226
+ if (source.truncated) {
227
+ // The scan stopped early, so "nothing newer was seen" is not "nothing newer exists". Saying
228
+ // FRESH here would be a verdict over incomplete evidence.
229
+ return {
230
+ ...base,
231
+ state: 'UNKNOWN',
232
+ indexedAt: new Date(artifactMtimeMs).toISOString(),
233
+ filesScanned: source.filesScanned,
234
+ reason: `freshness scan stopped at ${source.filesScanned} files, so no file newer than the ${spec.label} was seen — which is not the same as none existing`,
235
+ };
236
+ }
237
+ return {
238
+ ...base,
239
+ state: 'FRESH',
240
+ indexedAt: new Date(artifactMtimeMs).toISOString(),
241
+ newestSourceAt: new Date(source.mtimeMs).toISOString(),
242
+ filesScanned: source.filesScanned,
243
+ };
244
+ }
245
+
246
+ /**
247
+ * Probes one provider declared in `capabilities.json`.
248
+ * @param {Object} provider
249
+ * @param {Object} options
250
+ * @param {CapabilityRouter} options.router
251
+ * @param {string} [options.projectRoot]
252
+ * @param {Function} [options.execFileImpl]
253
+ * @param {Object} [options.fsImpl]
254
+ * @returns {Object} probe result
255
+ */
256
+ function probeProvider(provider: any, { router, projectRoot = process.cwd(), execFileImpl, fsImpl = fs }: any = {}) {
257
+ if (!provider || !provider.id) {
258
+ return { id: null, status: UNVERIFIED, detail: 'no provider declared' };
259
+ }
260
+ const result: any = {
261
+ id: provider.id,
262
+ name: provider.name || provider.id,
263
+ binary: provider.binary || null,
264
+ present: null,
265
+ answered: false,
266
+ status: UNVERIFIED,
267
+ detail: null,
268
+ durationMs: null,
269
+ version: null,
270
+ freshness: null,
271
+ };
272
+
273
+ if (provider.binary) {
274
+ result.present = router.isBinaryAvailable(provider.binary);
275
+ if (!result.present) {
276
+ result.status = ABSENT;
277
+ result.detail = `binary '${provider.binary}' is not on PATH`;
278
+ return result;
279
+ }
280
+ }
281
+
282
+ const command = Array.isArray(provider.checkCommand) && provider.checkCommand.length
283
+ ? provider.checkCommand
284
+ : null;
285
+ if (!command) {
286
+ result.status = UNVERIFIED;
287
+ result.detail = provider.binary
288
+ ? `binary '${provider.binary}' is present but the registry declares no probe command, so DoFlow cannot establish that it answers`
289
+ : 'declares neither a binary nor a probe command, so its function depends entirely on the project (see the project commands section)';
290
+ return result;
291
+ }
292
+
293
+ const probe = runProbe(command, { execFileImpl });
294
+ result.durationMs = probe.durationMs;
295
+ result.probeCommand = command.join(' ');
296
+ if (!probe.ok) {
297
+ result.status = UNHEALTHY;
298
+ // `banner` rather than the raw error: a failing provider can emit a stack trace, and a
299
+ // multi-line detail turns the report into an unreadable wall. The full text is not lost — the
300
+ // reader is told the exact command to re-run.
301
+ result.detail = `installed, but \`${result.probeCommand}\` did not answer: ${banner(probe.error) || 'no error text'}`;
302
+ return result;
303
+ }
304
+ if (!probe.output) {
305
+ // An exit-0 with no output is the exact "PASS over zero evidence" shape this feature is
306
+ // correcting: the process ran, but nothing establishes that the capability works.
307
+ result.status = UNHEALTHY;
308
+ result.detail = `installed, but \`${result.probeCommand}\` returned no output, so nothing establishes that it answered`;
309
+ return result;
310
+ }
311
+
312
+ result.answered = true;
313
+ result.status = HEALTHY;
314
+ result.version = banner(probe.output);
315
+ result.freshness = probeFreshness(provider.id, { root: projectRoot, fsImpl });
316
+ result.detail = result.version;
317
+ return result;
318
+ }
319
+
320
+ /**
321
+ * Probes every provider the capability registry declares, de-duplicated by provider id.
322
+ * @param {Object} [options]
323
+ * @param {string} [options.repoRoot] where the registry lives
324
+ * @param {string} [options.projectRoot] the tree whose index freshness is judged
325
+ * @param {string[]} [options.ids] restrict to these provider ids
326
+ * @param {CapabilityRouter} [options.router]
327
+ * @returns {Object} map of provider id -> probe result
328
+ */
329
+ function probeProviders({ repoRoot, projectRoot = process.cwd(), ids = null, router, execFileImpl, fsImpl = fs }: any = {}) {
330
+ const activeRouter = router || new CapabilityRouter({ repoRoot: repoRoot || REPO_ROOT, projectRoot });
331
+ const wanted = ids ? new Set(ids) : null;
332
+ const results: Record<string, any> = {};
333
+ for (const capability of Object.values<any>(activeRouter.capabilities || {})) {
334
+ for (const provider of capability.providers || []) {
335
+ if (!provider || !provider.id) continue;
336
+ if (wanted && !wanted.has(provider.id)) continue;
337
+ if (results[provider.id]) continue; // one probe per provider, however many capabilities list it
338
+ results[provider.id] = probeProvider(provider, { router: activeRouter, projectRoot, execFileImpl, fsImpl });
339
+ }
340
+ }
341
+ return results;
342
+ }
343
+
344
+ /**
345
+ * The project's build and test commands.
346
+ *
347
+ * Detection itself belongs to `command-detect.ts` (plan task C.2) and is deliberately **not**
348
+ * reimplemented here — two detectors would be exactly the "one verb, two implementations" defect
349
+ * this feature exists to remove (FR-005). This function reports what that module found, and when
350
+ * the module is absent it says so by name rather than guessing a command. A guessed build command
351
+ * is worse than an admitted gap, because it is a command the model will run.
352
+ *
353
+ * @param {Object} [options]
354
+ * @param {string} [options.projectRoot=process.cwd()]
355
+ * @param {Function} [options.detector] injection seam; defaults to the C.2 module when present
356
+ * @returns {{status: string, build: Object|null, test: Object|null, reason?: string}}
357
+ */
358
+ function detectProjectCommands({ projectRoot = process.cwd(), detector }: any = {}) {
359
+ let detect = detector;
360
+ if (!detect) {
361
+ let mod = null;
362
+ try {
363
+ mod = require('./command-detect.ts');
364
+ } catch {
365
+ return {
366
+ status: 'UNKNOWN',
367
+ build: null,
368
+ test: null,
369
+ reason: 'src/runtime/command-detect.ts is not present in this build; DoFlow reports detected commands but does not detect them itself, and will not guess',
370
+ };
371
+ }
372
+ detect = typeof mod.detectCommands === 'function' ? mod.detectCommands : null;
373
+ if (!detect) {
374
+ return {
375
+ status: 'UNKNOWN',
376
+ build: null,
377
+ test: null,
378
+ reason: `src/runtime/command-detect.ts exports no detectCommands() (found: ${Object.keys(mod).join(', ') || 'nothing'})`,
379
+ };
380
+ }
381
+ }
382
+
383
+ let detected;
384
+ try {
385
+ detected = detect({ projectRoot }) || {};
386
+ } catch (error: any) {
387
+ return { status: 'UNKNOWN', build: null, test: null, reason: `command detection failed: ${error.message}` };
388
+ }
389
+
390
+ const commands = detected.commands || {};
391
+ const build = commands.build || null;
392
+ const test = commands.test || null;
393
+ // Reported per role rather than as one boolean: "test detected, build absent" is the common and
394
+ // correct state for a project with no build step (this repository is one), and collapsing it into
395
+ // a single NOT_DETECTED would read as a broken detector.
396
+ const status = build && test ? 'DETECTED'
397
+ : build || test ? 'PARTIAL'
398
+ : detected.manifestFound ? 'NOT_DETECTED'
399
+ : 'UNKNOWN';
400
+ const absent = Array.isArray(detected.absent) ? detected.absent : [];
401
+ const reason = status === 'DETECTED' ? null
402
+ : status === 'UNKNOWN' ? 'no project manifest was recognised under this root, so no command could be detected'
403
+ : `detector found no ${absent.length ? absent.join(', ') : 'build'} command declared by ${(detected.manifests || ['the project manifest']).join(', ')}`;
404
+
405
+ return {
406
+ status,
407
+ build,
408
+ test,
409
+ manifests: detected.manifests || [],
410
+ absent,
411
+ errors: detected.errors || [],
412
+ ...(reason ? { reason } : {}),
413
+ };
414
+ }
415
+
416
+ /** Roll a capability's provider probes up into one status. */
417
+ function capabilityStatus(probes: any) {
418
+ if (!probes.length) return { status: 'UNAVAILABLE', activeProvider: null };
419
+ const firstHealthy = probes.findIndex((probe: any) => probe.status === HEALTHY);
420
+ if (firstHealthy === 0) return { status: HEALTHY, activeProvider: probes[0].id };
421
+ if (firstHealthy > 0) return { status: 'FALLBACK', activeProvider: probes[firstHealthy].id };
422
+ // Nothing answered. "Installed but broken" and "not installed" are different problems: the first
423
+ // is actionable now, the second is the graceful degradation NFR-002 requires.
424
+ if (probes.some((probe: any) => probe.status === UNHEALTHY)) return { status: 'DEGRADED', activeProvider: null };
425
+ if (probes.every((probe: any) => probe.status === ABSENT)) return { status: 'UNAVAILABLE', activeProvider: null };
426
+ return { status: UNVERIFIED, activeProvider: null };
427
+ }
428
+
429
+ /**
430
+ * The full health model behind `doflow doctor`.
431
+ * @param {Object} [options]
432
+ * @param {string} [options.repoRoot] where the registry lives
433
+ * @param {string} [options.projectRoot] the project being reported on
434
+ * @returns {Object} report model
435
+ */
436
+ /** The installed DoFlow source's own version, read from its package.json; null when unreadable. */
437
+ function installVersion(root: any, fsImpl = fs) {
438
+ try {
439
+ return JSON.parse(fsImpl.readFileSync(path.join(root, 'package.json'), 'utf8')).version || null;
440
+ } catch {
441
+ return null;
442
+ }
443
+ }
444
+
445
+ /** The project's git identity — short commit and branch — or null outside a repository. */
446
+ function projectGitIdentity(projectRoot: any) {
447
+ const git = (args: any) => {
448
+ try {
449
+ return execFileSync('git', ['-C', projectRoot, ...args], {
450
+ encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'],
451
+ }).trim() || null;
452
+ } catch {
453
+ return null;
454
+ }
455
+ };
456
+ const commit = git(['rev-parse', '--short', 'HEAD']);
457
+ if (!commit) return null;
458
+ return { commit, branch: git(['rev-parse', '--abbrev-ref', 'HEAD']) };
459
+ }
460
+
461
+ /** How many of the most recent recorded stage runs per stage the consultations view counts. */
462
+ const CONSULTATION_WINDOW = 20;
463
+ /** How many no-outcome entries the view lists per server and stage; the count covers the rest. */
464
+ const NO_OUTCOME_SHOWN = 3;
465
+
466
+ /**
467
+ * The `[MCP Servers]` data (IC-014): per server a shipped stage names, which installed harness has
468
+ * it and the command that adds it where it is missing. Read-only; starts no server (NFR-001).
469
+ * Server ids come from the registry, never from this file.
470
+ */
471
+ function mcpServersView({ registry, projectRoot, fsImpl, adapters }: any) {
472
+ const named = registry.mcp.filter((server: any) => (server.steps || []).length);
473
+ if (!named.length) return [];
474
+ if (!adapters) throw new Error('no adapter registry was given');
475
+ const reach = mcpReach({ registry, projectRoot, fsImpl, adapters });
476
+ const unreadableFile = (harness: any) => reach.unreadable.find((item: any) => item.harness === harness.id && item.scope === harness.scope);
477
+ return named.map((server: any) => ({
478
+ id: server.id,
479
+ selection: server.selection,
480
+ namedBy: [...new Set(server.steps.map((step: any) => (step.expectedWhen === 'research-request' ? `${step.skill} (research request)` : step.skill)))],
481
+ harnesses: reach.harnesses.map((harness: any) => {
482
+ const state = harness.servers[server.id];
483
+ const row: any = { id: harness.id, scope: harness.scope, state };
484
+ const unreadable = state === 'unknown' ? unreadableFile(harness) : null;
485
+ // An unreadable config is fixed before anything is added to it (IC-014).
486
+ if (unreadable) row.fix = unreadable.file || `${harness.id}'s MCP config`;
487
+ else if (state !== 'selected' && state !== 'configured') row.add = reach.addCommand(harness.id, harness.scope, server.id);
488
+ return row;
489
+ }),
490
+ }));
491
+ }
492
+
493
+ const byText = (a: any, b: any) => (a < b ? -1 : a > b ? 1 : 0);
494
+
495
+ /**
496
+ * The `[MCP Consultations]` data (IC-015): the recorded `consultations` of the project's run
497
+ * journals, grouped by calling skill, the most recent CONSULTATION_WINDOW entries per skill by
498
+ * `at`, counted per server. An entry without `consultations` predates the feature and is never
499
+ * counted (NFR-005); "no outcome" is `named` minus the servers with an outcome, never stored.
500
+ */
501
+ function mcpConsultationsView({ projectRoot, fsImpl }: any) {
502
+ const dir = path.join(projectRoot, '.doflow', 'state', 'orchestration');
503
+ let files = [];
504
+ try { files = fsImpl.readdirSync(dir).filter((name: any) => name.endsWith('.json')).sort(); } catch { files = []; }
505
+ let unreadableRuns = 0;
506
+ const bySkill = new Map();
507
+ for (const name of files) {
508
+ let run;
509
+ try { run = JSON.parse(fsImpl.readFileSync(path.join(dir, name), 'utf8')); } catch { run = null; }
510
+ if (!run || typeof run !== 'object' || Array.isArray(run)) { unreadableRuns += 1; continue; }
511
+ for (const item of Array.isArray(run.history) ? run.history : []) {
512
+ const record = item && item.consultations;
513
+ if (!record || typeof record !== 'object' || typeof record.skill !== 'string') continue;
514
+ if (!bySkill.has(record.skill)) bySkill.set(record.skill, []);
515
+ bySkill.get(record.skill).push({ taskId: run.taskId || name.replace(/\.json$/, ''), node: item.node ?? null, at: String(item.at ?? ''), record });
516
+ }
517
+ }
518
+ const stages = [...bySkill.keys()].sort(byText).map((skill) => {
519
+ const kept = bySkill.get(skill)
520
+ .sort((a: any, b: any) => byText(b.at, a.at) || byText(a.taskId, b.taskId) || byText(String(a.node), String(b.node)))
521
+ .slice(0, CONSULTATION_WINDOW);
522
+ const counts = new Map();
523
+ const row = (server: any) => {
524
+ if (!counts.has(server)) counts.set(server, { server, used: 0, nothingUsable: 0, skipped: 0, unavailable: 0, noOutcome: 0 });
525
+ return counts.get(server);
526
+ };
527
+ const key: Record<string, any> = { 'consulted-used': 'used', 'consulted-nothing-usable': 'nothingUsable', skipped: 'skipped', unavailable: 'unavailable' };
528
+ const noOutcome = [];
529
+ const shown = new Map();
530
+ for (const { taskId, node, at, record } of kept) {
531
+ const outcomes = new Map();
532
+ for (const outcome of Array.isArray(record.outcomes) ? record.outcomes : []) {
533
+ if (outcome && typeof outcome.server === 'string' && key[outcome.state]) outcomes.set(outcome.server, key[outcome.state]);
534
+ }
535
+ for (const [server, field] of outcomes) row(server)[field] += 1;
536
+ for (const server of (Array.isArray(record.named) ? record.named : []).filter((id: any) => typeof id === 'string').sort(byText)) {
537
+ if (outcomes.has(server)) continue;
538
+ row(server).noOutcome += 1;
539
+ // `kept` is newest first, so the first entries per server are its latest.
540
+ shown.set(server, (shown.get(server) || 0) + 1);
541
+ if (shown.get(server) <= NO_OUTCOME_SHOWN) noOutcome.push({ taskId, node, at, server });
542
+ }
543
+ }
544
+ return { skill, runs: kept.length, servers: [...counts.values()].sort((a, b) => byText(a.server, b.server)), noOutcome };
545
+ });
546
+ return { window: CONSULTATION_WINDOW, stages, unreadableRuns };
547
+ }
548
+
549
+ /** The doctor's `mcp` block. A servers view that cannot be built degrades to a one-line note; the
550
+ * sections never enter findings or change the exit code (FR-011, FR-012). */
551
+ function mcpView({ registry, projectRoot, fsImpl, adapters }: any) {
552
+ let servers = [];
553
+ let serversNote;
554
+ try { servers = mcpServersView({ registry, projectRoot, fsImpl, adapters }); } catch (error: any) { serversNote = `MCP server view unavailable: ${error.message}`; }
555
+ return { servers, ...(serversNote ? { serversNote } : {}), consultations: mcpConsultationsView({ projectRoot, fsImpl }) };
556
+ }
557
+
558
+ const MCP_STATE_MARK: Record<string, any> = Object.freeze({
559
+ selected: '✓ selected',
560
+ configured: '✓ configured',
561
+ 'not-selected': '○ not selected',
562
+ 'not-taken': '○ not taken',
563
+ unknown: '? unknown',
564
+ });
565
+
566
+ function printMcpSections(mcp: any) {
567
+ console.log('\n[MCP Servers] — named by shipped stages; not blocking');
568
+ if (mcp.serversNote) {
569
+ console.log(` ${mcp.serversNote}`);
570
+ } else if (!mcp.servers.some((server: any) => server.harnesses.length)) {
571
+ console.log(' No harness has DoFlow ledger rows here, so no server selection to report.');
572
+ } else {
573
+ for (const server of mcp.servers) {
574
+ console.log(` ${server.id.padEnd(22)}named by ${server.namedBy.join(', ')}`);
575
+ for (const harness of server.harnesses) {
576
+ // An install command reads `add: <command>`; "configure it in ..." text stands as it is.
577
+ const add = harness.fix ? `fix ${harness.fix} first`
578
+ : harness.add ? (harness.add.startsWith('doflow ') ? `add: ${harness.add}` : harness.add) : '';
579
+ console.log(` ${harness.id.padEnd(14)}${harness.scope.padEnd(9)}${(MCP_STATE_MARK[harness.state] || harness.state).padEnd(19)}${add}`.trimEnd());
580
+ }
581
+ }
582
+ }
583
+
584
+ const { window, stages, unreadableRuns } = mcp.consultations;
585
+ console.log(`\n[MCP Consultations] — last ${window} recorded stage runs per stage in this project; not blocking`);
586
+ if (!stages.length) console.log(' No stage run in this project has recorded MCP outcomes yet.');
587
+ for (const item of stages) {
588
+ for (const row of item.servers) {
589
+ const counts = [
590
+ `${OUTCOME_LABELS['consulted-used']} ${row.used}`,
591
+ `${OUTCOME_LABELS['consulted-nothing-usable']} ${row.nothingUsable}`,
592
+ `${OUTCOME_LABELS.skipped} ${row.skipped}`,
593
+ `${OUTCOME_LABELS.unavailable} ${row.unavailable}`,
594
+ `${OUTCOME_LABELS['no-outcome']} ${row.noOutcome}`,
595
+ ];
596
+ console.log(` ${item.skill.padEnd(16)}${row.server.padEnd(22)}${counts.join(' · ')}`);
597
+ }
598
+ for (const miss of item.noOutcome) {
599
+ console.log(` ${OUTCOME_LABELS['no-outcome']}: ${miss.taskId} ${miss.node} ${miss.at} (${miss.server})`);
600
+ }
601
+ }
602
+ if (unreadableRuns) console.log(` ${unreadableRuns} run file(s) could not be read`);
603
+ }
604
+
605
+ function buildHealthReport({ repoRoot, projectRoot = process.cwd(), router, execFileImpl, fsImpl = fs, detector, adapters }: any = {}) {
606
+ const root = repoRoot || REPO_ROOT;
607
+ const registry = loadRegistry({ repoRoot: root });
608
+ const activeRouter = router || new CapabilityRouter({ repoRoot: root, projectRoot });
609
+ const probes: Record<string, any> = probeProviders({ repoRoot: root, projectRoot, router: activeRouter, execFileImpl, fsImpl });
610
+ const project = { root: path.resolve(projectRoot), commands: detectProjectCommands({ projectRoot, detector }) };
611
+
612
+ // `native.test` is the one provider with neither a binary nor a probe: it stands for whatever the
613
+ // project runs its tests with. Its detail is resolved from the detected commands so the report
614
+ // says which of the two very different situations applies — a test command exists but running a
615
+ // suite is too expensive to use as a probe, or no test command was found at all. It stays
616
+ // UNVERIFIED either way, because neither case establishes that it answers.
617
+ const projectTest = probes['native.test'];
618
+ if (projectTest && projectTest.status === UNVERIFIED) {
619
+ projectTest.detail = project.commands.test
620
+ ? `no probe of its own; the project's test command was detected (\`${project.commands.test.command}\`), but running a suite is not a health probe`
621
+ : 'no probe of its own, and no project test command was detected — nothing establishes that this capability can answer';
622
+ }
623
+
624
+ const capabilities = Object.entries<any>(activeRouter.capabilities || {}).map(([id, capability]) => {
625
+ const declared = (capability.providers || []).map((provider: any) => probes[provider.id]).filter(Boolean);
626
+ const rolled = capabilityStatus(declared);
627
+ return {
628
+ capability: id,
629
+ description: capability.description,
630
+ status: rolled.status,
631
+ activeProvider: rolled.activeProvider,
632
+ totalProviders: declared.length,
633
+ providers: declared.map((probe: any) => ({ id: probe.id, status: probe.status, detail: probe.detail })),
634
+ };
635
+ });
636
+
637
+ // External tools are the same binaries the providers use, so their status comes from the probe
638
+ // that already ran rather than from a second, weaker presence check.
639
+ const externalTools = registry.externalTools.map((tool: any) => {
640
+ const probe: any = Object.values(probes).find((candidate) => candidate.binary === tool.id);
641
+ if (probe) return { id: tool.id, displayName: tool.displayName, status: probe.status, detail: probe.detail };
642
+ const present = activeRouter.isBinaryAvailable(tool.id);
643
+ return {
644
+ id: tool.id,
645
+ displayName: tool.displayName,
646
+ status: present ? UNVERIFIED : ABSENT,
647
+ detail: present ? 'on PATH, but no capability provider declares a probe for it' : `binary '${tool.id}' is not on PATH`,
648
+ };
649
+ });
650
+
651
+ const freshness = Object.values(probes).map((probe) => probe.freshness).filter(Boolean);
652
+ const reach = evaluateReach({ registry, projectRoot, fsImpl });
653
+
654
+ // A finding is a thing to act on now: something installed that does not work. An absent optional
655
+ // provider is not a finding (NFR-002 makes degradation normal), and a stale index still answers,
656
+ // so both are warnings that leave the exit code at 0.
657
+ const findings = [
658
+ ...Object.values(probes)
659
+ .filter((probe) => probe.status === UNHEALTHY)
660
+ .map((probe) => ({ kind: 'provider-unhealthy', subject: probe.id, detail: probe.detail })),
661
+ ...capabilities
662
+ .filter((capability) => capability.status === 'DEGRADED')
663
+ .map((capability) => ({ kind: 'capability-degraded', subject: capability.capability, detail: 'every installed provider for this capability failed its probe' })),
664
+ ...reach.rows
665
+ .filter((row: any) => row.state === 'NO-REACH')
666
+ .map((row: any) => ({ kind: 'runtime-no-reach', subject: `${row.harness}:${row.scope}`, detail: `${row.reason}; fix: ${row.fix}` })),
667
+ ];
668
+ const warnings = [
669
+ ...freshness
670
+ .filter((item) => item.state === 'STALE')
671
+ .map((item) => ({ kind: 'index-stale', subject: item.providerId, detail: `${item.label} is behind the working tree; refresh with: ${item.refresh}` })),
672
+ ...freshness
673
+ .filter((item) => item.state === 'UNKNOWN')
674
+ .map((item) => ({ kind: 'index-age-unknown', subject: item.providerId, detail: item.reason })),
675
+ ...Object.values(probes)
676
+ .filter((probe) => probe.status === UNVERIFIED)
677
+ .map((probe) => ({ kind: 'provider-unverified', subject: probe.id, detail: probe.detail })),
678
+ ...reach.unreadable
679
+ .map((item: any) => ({ kind: 'reach-ledger-unreadable', subject: `${item.scope}: ${item.ledger}`, detail: item.detail })),
680
+ ...(project.commands.status === 'DETECTED' ? [] : [{ kind: 'project-commands', subject: 'build/test', detail: project.commands.reason }]),
681
+ ];
682
+
683
+ // Effective identity, in one block (review A3): which DoFlow source answered, which project it
684
+ // was asked about, and which capabilities are actually connected — so "a clean install has a
685
+ // usable X" claims can be checked against what THIS machine effectively runs, rather than
686
+ // inferred from the pieces separately.
687
+ const identity = {
688
+ source: { root, version: installVersion(root, fsImpl) },
689
+ project: { root: path.resolve(projectRoot), git: projectGitIdentity(projectRoot) },
690
+ connectedCapabilities: capabilities.filter((capability) => capability.activeProvider).map((capability) => capability.capability),
691
+ };
692
+
693
+ return {
694
+ identity,
695
+ harnesses: registry.harnesses.map((harness: any) => ({
696
+ id: harness.id,
697
+ displayName: harness.displayName,
698
+ status: 'PASS',
699
+ reach: reach.rows.filter((row: any) => row.harness === harness.id).map(({ harness: _harness, ...row }: any) => row),
700
+ })),
701
+ externalTools,
702
+ capabilities,
703
+ providers: Object.values(probes),
704
+ freshness,
705
+ project,
706
+ mcp: mcpView({ registry, projectRoot, fsImpl, adapters }),
707
+ findings,
708
+ warnings,
709
+ summary: {
710
+ probed: Object.keys(probes).length,
711
+ healthy: Object.values(probes).filter((probe) => probe.status === HEALTHY).length,
712
+ unhealthy: Object.values(probes).filter((probe) => probe.status === UNHEALTHY).length,
713
+ absent: Object.values(probes).filter((probe) => probe.status === ABSENT).length,
714
+ unverified: Object.values(probes).filter((probe) => probe.status === UNVERIFIED).length,
715
+ },
716
+ };
717
+ }
718
+
719
+ /**
720
+ * Records the report's exit code and hands it back to the caller.
721
+ *
722
+ * Design §4.2: 0 = answered, 1 = a finding the caller must act on. An installed provider that
723
+ * does not answer is a finding, and so is an installed harness whose skills cannot reach a runtime
724
+ * (`runtime-no-reach`). An absent optional provider is the graceful degradation NFR-002 requires,
725
+ * and a stale index still answers, so both leave the code at 0 and appear as warnings; so do a
726
+ * harness with no skills at a scope (N/A) and an unreadable ledger. Setting `process.exitCode`
727
+ * rather than calling `process.exit` lets stdout flush.
728
+ * @param {number} code
729
+ * @returns {number}
730
+ */
731
+ function finish(code: any) {
732
+ process.exitCode = code;
733
+ return code;
734
+ }
735
+
736
+ const STATUS_MARK: Record<string, any> = Object.freeze({
737
+ HEALTHY: '✓ HEALTHY',
738
+ UNHEALTHY: '✗ UNHEALTHY',
739
+ ABSENT: '○ ABSENT',
740
+ UNVERIFIED: '? UNVERIFIED',
741
+ FALLBACK: '▲ FALLBACK',
742
+ DEGRADED: '✗ DEGRADED',
743
+ UNAVAILABLE: '○ UNAVAILABLE',
744
+ });
745
+
746
+ const REACH_MARK: Record<string, any> = Object.freeze({
747
+ REACHED: '✓ REACHED',
748
+ 'NO-REACH': '✗ NO-REACH',
749
+ 'N/A': '○ N/A',
750
+ });
751
+
752
+ const FRESHNESS_MARK: Record<string, any> = Object.freeze({
753
+ FRESH: '✓ FRESH',
754
+ STALE: '▲ STALE',
755
+ UNKNOWN: '? UNKNOWN',
756
+ });
757
+
758
+ /**
759
+ * Handles `doflow doctor` — installation and capability health.
760
+ * @param {Object} options
761
+ * @param {boolean} [options.json=false]
762
+ * @param {string} [options.repoRoot]
763
+ * @param {string} [options.projectRoot]
764
+ * @returns {number} process exit code — 1 when a provider is installed but does not answer, or an
765
+ * installed harness's skills cannot reach a runtime
766
+ */
767
+ async function handleDoctorCommand({ json = false, repoRoot, projectRoot = process.cwd(), ...rest }: any = {}) {
768
+ const report: any = buildHealthReport({ repoRoot, projectRoot, ...rest });
769
+ const { buildAdviceDoctor, printAdviceDoctor } = require('./advice/doctor.ts');
770
+ const adviceSections = await buildAdviceDoctor({ repoRoot, projectRoot, fsImpl: rest.fsImpl });
771
+ report.decisionModel = adviceSections.decisionModel;
772
+ report.advice = adviceSections.advice;
773
+
774
+ if (json) {
775
+ console.log(JSON.stringify(report, null, 2));
776
+ return finish(report.findings.length ? 1 : 0);
777
+ }
778
+
779
+ console.log('\nDoFlow System Diagnostics (doflow doctor)');
780
+ console.log('═'.repeat(72));
781
+ console.log('Health means a provider answered a probe, not that a binary exists on PATH.');
782
+
783
+ console.log('\n[Effective Identity]');
784
+ const identity = report.identity;
785
+ console.log(` source ${identity.source.root}${identity.source.version ? ` (v${identity.source.version})` : ' (version unreadable)'}`);
786
+ console.log(` project ${identity.project.root}${identity.project.git ? ` (${identity.project.git.branch} @ ${identity.project.git.commit})` : ' (not a git repository)'}`);
787
+ console.log(` connected ${identity.connectedCapabilities.length ? identity.connectedCapabilities.join(', ') : 'no capability has an answering provider'}`);
788
+
789
+ console.log('\n[Harness Adapters]');
790
+ for (const harness of report.harnesses) console.log(` ${harness.displayName.padEnd(28)} adapter PASS`);
791
+
792
+ console.log('\n[Runtime Reach]');
793
+ const reachLines = report.harnesses.flatMap((harness: any) => harness.reach.map((row: any) => {
794
+ const detail = ({ REACHED: row.root, 'NO-REACH': `fix: ${row.fix}`, 'N/A': row.reason } as Record<string, any>)[row.state];
795
+ const where = row.scope === 'project' ? ` [install at ${row.installRoot}]` : '';
796
+ return ` ${harness.id.padEnd(14)}${row.scope.padEnd(9)}${(REACH_MARK[row.state] || row.state).padEnd(13)}${detail}${where}`;
797
+ }));
798
+ if (reachLines.length) for (const line of reachLines) console.log(line);
799
+ else console.log(' No harness has DoFlow ledger rows in this project or in the home directory.');
800
+
801
+ console.log('\n[Capability Providers]');
802
+ for (const probe of report.providers) {
803
+ console.log(` ${probe.id.padEnd(18)}${(STATUS_MARK[probe.status] || probe.status).padEnd(15)}${probe.detail || ''}`);
804
+ }
805
+
806
+ console.log('\n[Runtime Capabilities]');
807
+ for (const capability of report.capabilities) {
808
+ console.log(` ${capability.capability.padEnd(26)}${(STATUS_MARK[capability.status] || capability.status).padEnd(15)}(${capability.activeProvider || 'none answering'})`);
809
+ }
810
+
811
+ console.log('\n[Index Freshness]');
812
+ if (!report.freshness.length) {
813
+ console.log(' No index- or graph-backed provider answered, so there is no index to age.');
814
+ } else {
815
+ for (const item of report.freshness) {
816
+ console.log(` ${item.providerId.padEnd(18)}${FRESHNESS_MARK[item.state].padEnd(15)}${item.reason || item.artifact || ''}`);
817
+ }
818
+ }
819
+
820
+ console.log('\n[Project Commands]');
821
+ const commands = report.project.commands;
822
+ const commandLine = (role: any) => (role
823
+ ? `✓ ${role.command}${role.source ? ` (${role.source}, ${role.derivation || 'declared'})` : ''}`
824
+ : '? not detected');
825
+ console.log(` build ${commandLine(commands.build)}`);
826
+ console.log(` test ${commandLine(commands.test)}`);
827
+ if (commands.reason) console.log(` note ${commands.reason}`);
828
+
829
+ printMcpSections(report.mcp);
830
+ printAdviceDoctor(adviceSections);
831
+
832
+ if (report.warnings.length) {
833
+ console.log('\n[Warnings] — not blocking; a stale or unprobeable provider still degrades cleanly');
834
+ for (const warning of report.warnings) console.log(` ${warning.subject.padEnd(18)}${warning.detail}`);
835
+ }
836
+
837
+ console.log('\n[Findings]');
838
+ if (!report.findings.length) {
839
+ console.log(` None. ${report.summary.healthy} of ${report.summary.probed} provider(s) answered; ${report.summary.absent} not installed, ${report.summary.unverified} unprobeable.`);
840
+ } else {
841
+ for (const finding of report.findings) console.log(` ${finding.kind.padEnd(22)}${finding.subject}: ${finding.detail}`);
842
+ }
843
+ console.log('═'.repeat(72) + '\n');
844
+
845
+ return finish(report.findings.length ? 1 : 0);
846
+ }
847
+
848
+ module.exports = {
849
+ HEALTHY,
850
+ UNHEALTHY,
851
+ ABSENT,
852
+ UNVERIFIED,
853
+ runProbe,
854
+ probeProvider,
855
+ probeProviders,
856
+ probeFreshness,
857
+ PROVIDER_ARTIFACTS,
858
+ newestSourceMtime,
859
+ detectProjectCommands,
860
+ buildHealthReport,
861
+ handleDoctorCommand,
862
+ PROBE_TIMEOUT_MS,
863
+ };