@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,705 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ /**
6
+ * C7 — report assembler and the `inventory` verb (FR-005, FR-008, FR-009; IC-001, IC-002).
7
+ *
8
+ * The five modules beside this one each answer one question about one thing. This module is the
9
+ * only place that puts them together: it reads both scopes once (`read-scopes`), joins the recorded
10
+ * resources into logical assets by a derived key (`identity`), names or withholds a winner per
11
+ * asset (`precedence`), finds the unmanaged files that share a wholesale-loaded directory
12
+ * (`siblings`), and states how much of the result the existing repair path can act on
13
+ * (`coverage`). Nothing here re-implements any of those judgements.
14
+ *
15
+ * **Read-only (NFR-001).** Nothing in this file or below it writes, moves or deletes. `readScopes`
16
+ * reports an absent ledger rather than creating one, `inspectSiblings` only `readdir`s, and
17
+ * `readScopeLocks` only reads. The remedy on each asset entry is text the caller may run; the verb
18
+ * never runs it.
19
+ *
20
+ * Four seams are easy to get wrong here, so each is named where it is crossed:
21
+ *
22
+ * 1. **Two scope vocabularies.** The ledger and `coverage.js` say `global`/`project`; the registry's
23
+ * `scopePrecedence.order` says `user`/`project` (the same word `registryScope()` uses in
24
+ * `src/lifecycle/index.js`). `precedence.js` passes both `presentAt` and `winner` through in
25
+ * whatever vocabulary it was handed, so this module converts on the way in and back on the way
26
+ * out (`toRegistryScope`/`toReportScope`). Skipping either half produces a verdict naming a scope
27
+ * the rest of the report never mentions.
28
+ * 2. **`reconcile` is claimed for one finding kind only.** `coverage.js` grades a finding by the
29
+ * `repairPath` it declares, and FR-006 forbids over-claiming a remedy. A divergence declares
30
+ * `reconcile`, because converging a scope onto its pin is exactly what heals one. A shadow, a
31
+ * withheld verdict, an unjudged copy and an unmanaged sibling declare `manual`: each has a literal
32
+ * remedy — stated on the asset entry — but `reconcile` cannot perform any of them, since it does
33
+ * not delete a rival copy at the other scope, cannot establish a harness's undocumented precedence,
34
+ * cannot supply the basis a copy was never judged against, and never touches a file DoFlow did not
35
+ * install. Declaring `manual` rather than leaving `repairPath`
36
+ * absent is deliberate: absence classifies as `repair-path-unstated`, whose own definition is
37
+ * "the finding names no remedy at all", and that would be false of all four.
38
+ * 3. **A finding's unit is the thing the caller must act on, not the number of entries that mention
39
+ * it.** IC-002 makes the asset entry the place every threatened asset lists an unmanaged file, so
40
+ * one stray in a directory holding four managed files appears on four entries — correctly. The
41
+ * `findings` array is a different question, and answers FR-007's, whose unit is the file: one
42
+ * unmanaged file is one finding however many managed assets share its directory. Emitting one per
43
+ * (asset, file) pair multiplied the reported problem by the size of the directory and inflated the
44
+ * FR-006 denominator with it. The same rule sets a withheld verdict's `scope` to `null`: that
45
+ * finding spans both scopes and belongs to neither, so naming whichever copy sorted first stated
46
+ * something no computation had established.
47
+ * 4. **A resource whose identity cannot be derived is never dropped.** `deriveAssetIdentity` returns
48
+ * `null` for a row carrying no harness or no `ownershipIdentity`. `src/lifecycle/index.js`
49
+ * refuses to record such a row, so no install produces one — but a ledger is a JSON file a user
50
+ * can hand-edit, so the branch stays reachable. `null` is not a join key — two such rows are not
51
+ * the same asset — so they cannot become logical-asset entries. They are reported one row each
52
+ * under `unidentified`,
53
+ * carrying the same location and currency an asset entry would, plus the reason the cross-scope
54
+ * question could not be asked of them. They still contribute a divergence finding when their own
55
+ * copy has diverged, and an unjudged-copy finding when nothing judged it, because neither
56
+ * judgement needs a cross-scope join.
57
+ */
58
+
59
+ const fs = require('node:fs');
60
+ const path = require('node:path');
61
+ const crypto = require('node:crypto');
62
+ const { harnessFor, loadRegistry } = require('../../registry/index.ts');
63
+ const { readScopes } = require('./read-scopes.ts');
64
+ const { deriveAssetIdentity } = require('./identity.ts');
65
+ const { resolveShadowVerdict } = require('./precedence.ts');
66
+ const { inspectSiblings } = require('./siblings.ts');
67
+ const { repairCoverage, readScopeLocks } = require('./coverage.ts');
68
+ const { finishRuntime, usageError } = require('../cli-result.ts');
69
+
70
+ /** The three currency values of IC-002, and never a fourth. The third is not a weaker second: a
71
+ * copy nothing in this scope's plan spoke about has not "diverged", it cannot be judged at all.
72
+ * IC-002 names one condition for the third value — no recorded fingerprint — and `currencyOf` holds
73
+ * three more that are the same absence of a basis rather than a fourth meaning; see its comment. */
74
+ const CURRENCY = Object.freeze({
75
+ MATCHES: 'matches',
76
+ DIVERGED: 'diverged',
77
+ INDETERMINABLE: 'indeterminable',
78
+ });
79
+
80
+ /**
81
+ * The finding kinds the report can make. Only the first is reachable by `doflow reconcile`.
82
+ *
83
+ * `UNJUDGED_COPY` is the third `CURRENCY` value raised to a finding, and it is a finding for the
84
+ * reason IC-001 already gives for a withheld verdict: the caller cannot conclude the install is
85
+ * sound. The two statements are the same statement about different questions — a withheld verdict
86
+ * says no scope was established as operative, an unjudged copy says no content comparison was made
87
+ * at all — and IC-001 defines exit 0 positively, as every logical asset resolving to a copy that
88
+ * *matches* what the current source would produce. A copy nothing compared does not satisfy that,
89
+ * so reporting it at exit 0 asserted soundness from the one place the report holds no basis. It is
90
+ * also the case a user most needs told: a malformed native config file makes its adapter refuse a
91
+ * plan, and every copy of that harness silently drops out of evaluation.
92
+ *
93
+ * Ceiling: the grain is the copy, as it is for a divergence, not the copy precedence names the winner.
94
+ * An asset whose global copy matches while its project copy was never judged is still a finding, even
95
+ * where the global copy is the one the harness loads — which copy that is comes from the registry
96
+ * block, and a copy on disk that nothing compared is actionable whether or not it wins today.
97
+ */
98
+ const FINDING_KINDS = Object.freeze({
99
+ DIVERGENCE: 'divergence',
100
+ SHADOW: 'shadow',
101
+ WITHHELD_VERDICT: 'withheld-verdict',
102
+ UNJUDGED_COPY: 'unjudged-copy',
103
+ UNMANAGED_SIBLING: 'unmanaged-sibling',
104
+ });
105
+
106
+ /** The one repair path `coverage.js` can measure, and the value it recognises. */
107
+ const RECONCILE = 'reconcile';
108
+ /** Everything else this report can recommend: a real, stated action no command performs. See the
109
+ * header's note 2 for why this is declared rather than left absent. */
110
+ const MANUAL = 'manual';
111
+
112
+ /** Report vocabulary -> registry vocabulary (`scopePrecedence.order` names the global scope `user`). */
113
+ function toRegistryScope(scope: any): any { return scope === 'global' ? 'user' : scope; }
114
+ /** Registry vocabulary -> report vocabulary. The inverse of the above, applied to every scope name
115
+ * `precedence.js` hands back, so a verdict never names a scope the rest of the report does not. */
116
+ function toReportScope(scope: any): any { return scope === 'user' ? 'global' : scope; }
117
+
118
+ /** The four fields that identify one recorded resource, and the same four an adapter change carries.
119
+ * Not `ownershipKey` from src/state: that one includes the scope, and a plan change has none. */
120
+ function recordKey(record: any): string {
121
+ return [record?.harness, record?.assetId, record?.target, record?.ownershipIdentity].join('\u0000');
122
+ }
123
+
124
+ /**
125
+ * What this scope's plan says about each recorded resource, and which harnesses it spoke about at
126
+ * all — the whole basis a currency judgement has.
127
+ *
128
+ * `changes` keeps the change object, not just its key, because `operation` is the difference between
129
+ * rewriting a resource and deleting it, and only the first is a divergence.
130
+ *
131
+ * `evaluated` exists because absence from `changes` carries two meanings that must not be conflated:
132
+ * a copy-tree file whose destination bytes already equal what the source would produce yields no
133
+ * change at all (`src/adapters/copy-tree.js`, the `unchanged, no-op` branch) — but so does a harness
134
+ * whose plan was never produced. A harness the plan skipped, or whose adapter reported a conflict
135
+ * (`codex: Malformed TOML value` is the live example), emitted a change set that is not a statement
136
+ * of what the current source would write, so a resource missing from it was not examined.
137
+ *
138
+ * Ceiling: `evaluated` is per harness, because that is the grain conflicts are recorded at. One
139
+ * malformed native file makes every resource of that harness indeterminable rather than only the
140
+ * component that could not be planned; narrowing it would mean reproducing each adapter's internal
141
+ * component boundaries here. A prerequisite is deliberately *not* disqualifying — it says the change
142
+ * cannot yet be applied, not that the plan could not determine it.
143
+ */
144
+ function planBasis(snapshot: any): any {
145
+ const changes = new Map();
146
+ for (const change of snapshot?.plan?.changes ?? []) {
147
+ if (change && typeof change === 'object') changes.set(recordKey(change), change);
148
+ }
149
+ const evaluated = new Set();
150
+ for (const target of snapshot?.plan?.targets ?? []) {
151
+ if (!target || target.skipped === true || (target.conflicts ?? []).length > 0) continue;
152
+ evaluated.add(target.harness);
153
+ }
154
+ return { changes, evaluated };
155
+ }
156
+
157
+ /**
158
+ * Whether the bytes a planned change would write are the bytes already at its destination.
159
+ *
160
+ * A change is not by itself a statement that the content differs. `src/adapters/copy-tree.js` treats
161
+ * a destination as a no-op only when three things agree — same location, disk bytes equal to the
162
+ * source fingerprint, **and** the recorded ledger row equal to it too — which is right for its own
163
+ * purpose, planning a write that also refreshes the row. It means a change is emitted whenever the
164
+ * row alone is stale, and that happens routinely: the shared hooks asset projects one destination for
165
+ * claude, codex, gemini, kiro and antigravity while ownership is recorded per harness, so a sibling's
166
+ * install legitimately refreshes bytes the other harnesses' rows still describe by their old hash.
167
+ * Reading such a change as divergence reported one file, identical bytes, under contradictory
168
+ * verdicts per harness, with a remedy offering to "restore" a file that was already correct.
169
+ *
170
+ * So the comparison is made against the destination, which is what IC-002's "the content differs"
171
+ * is about, and not against the recorded row — a stale row and a hand edit both make
172
+ * `change.fingerprint !== resource.fingerprint`, so that test cannot tell them apart.
173
+ *
174
+ * Ceiling: this compares a hash of the destination file's whole bytes with the change's fingerprint,
175
+ * so it is decisive only where the fingerprint describes those same bytes — every `copy-tree` asset.
176
+ * An adapter that merges into a user-owned native file fingerprints the managed subset instead
177
+ * (`fingerprint(value)` over a projection, not over the file), so the two never compare equal and
178
+ * such a change stays a divergence, exactly as before. That is the conservative direction: this
179
+ * function can only ever withdraw a divergence claim about bytes it has read and found identical.
180
+ *
181
+ * @returns {boolean} false whenever the destination cannot be read, or the change names no
182
+ * fingerprint — an absent or unreadable file is no evidence that its content is current.
183
+ */
184
+ function contentAlreadyCurrent(change: any, fsImpl: any): boolean {
185
+ const intended = change?.fingerprint;
186
+ if (typeof intended !== 'string' || intended === '') return false;
187
+ let observed;
188
+ try { observed = crypto.createHash('sha256').update(fsImpl.readFileSync(change.target)).digest('hex'); }
189
+ catch { return false; }
190
+ // Both spellings are in the corpus: copy-tree records bare hex, several adapters prefix `sha256:`.
191
+ return observed === intended.replace(/^sha256:/, '');
192
+ }
193
+
194
+ /**
195
+ * The `CURRENCY` entity for one recorded copy.
196
+ *
197
+ * Order of the tests is the contract. The absence of a recorded fingerprint comes first, because
198
+ * IC-002 names exactly that condition: a copy with no fingerprint is indeterminable whether or not
199
+ * the plan would touch it. What the plan says comes next, so a resource the plan named explicitly is
200
+ * always judged on that. Only a resource the plan was *silent* about reaches the last tests, and
201
+ * silence is evidence of currency only where the plan actually ran and this row could have appeared
202
+ * in it.
203
+ *
204
+ * Three of the four paths to the third value are not the fingerprint condition IC-002 names. All are
205
+ * the same thing that condition is — no basis for a judgement — rather than a fourth value:
206
+ *
207
+ * - **A planned removal.** The plan would delete this copy rather than rewrite it, so it states
208
+ * nothing about whether the content is current. Calling that `diverged` is how a correctly
209
+ * installed, merely deselected resource came to be reported as broken, with a remedy promising to
210
+ * "restore" it and a plan behind that remedy saying *remove* — the over-claim FR-006 forbids.
211
+ * - **A harness whose plan was not produced.** Reporting `matches` there asserts currency from a
212
+ * computation that never ran.
213
+ * - **A row no change could be looked up for.** The plan is keyed by four components including the
214
+ * harness and the ownership identity, so a row missing either — the same absence that makes
215
+ * `deriveAssetIdentity` return `null` — can never match a change. Its absence from the plan
216
+ * compared nothing, and reading `matches` out of that absence made the report's *strongest* claim
217
+ * about the one row nothing had examined, up to and including rows whose file does not exist.
218
+ *
219
+ * A planned *rewrite* is likewise not automatically the second value: see `contentAlreadyCurrent`.
220
+ * A change whose bytes are already on disk leaves the content current, so this returns the first
221
+ * value — the scope holds a full basis here, an affirmative comparison against what the source would
222
+ * produce, which is precisely what the third value is the absence of.
223
+ *
224
+ * @param {Object} resource a recorded resource
225
+ * @param {{changes: Map<string, Object>, evaluated: Set<string>}} basis what the scope's plan says
226
+ * @param {Object} [fsImpl] filesystem implementation, for reading the destination's current bytes
227
+ * @returns {{value: string, reason: string|null, resolution: string|null}} `resolution` is what would
228
+ * give this scope a basis for judging the copy. It is stated here rather than composed by the remedy
229
+ * because each of the four paths to the third value needs a different one: a reason and the action
230
+ * that answers it are one thought, and separating them means matching on reason strings elsewhere.
231
+ * `null` for the other two values — a copy that was judged needs no basis, and a diverged one's
232
+ * remedy is the restore `remedyFor` already states.
233
+ */
234
+ function currencyOf(resource: any, basis: any, fsImpl: any = fs): any {
235
+ const recorded = resource?.fingerprint;
236
+ if (typeof recorded !== 'string' || recorded === '') {
237
+ return {
238
+ value: CURRENCY.INDETERMINABLE,
239
+ reason: 'no fingerprint is recorded for this copy',
240
+ resolution: 'record a fingerprint for it: re-installing this harness at this scope rewrites the copy '
241
+ + 'and records one — or delete the row, if DoFlow no longer owns the copy',
242
+ };
243
+ }
244
+ const change = basis.changes.get(recordKey(resource));
245
+ if (change?.operation === 'remove') {
246
+ return {
247
+ value: CURRENCY.INDETERMINABLE,
248
+ reason: 'this scope\'s plan would remove this copy rather than rewrite it — what the scope '
249
+ + 'records no longer wants it here — so the plan states nothing about whether its content is current',
250
+ resolution: 'settle what this scope records about it: re-select the resource so this scope\'s plan '
251
+ + 'describes its content again, or let the planned removal run. Until one of those happens nothing '
252
+ + 'states what the content should be, so nothing can say whether it is current',
253
+ };
254
+ }
255
+ if (change) {
256
+ if (contentAlreadyCurrent(change, fsImpl)) {
257
+ return {
258
+ value: CURRENCY.MATCHES,
259
+ reason: 'this scope\'s plan would rewrite this copy, but the bytes it would write are already '
260
+ + 'the bytes on disk: the change refreshes what this scope recorded about the copy, not the '
261
+ + 'copy itself, so its content is current',
262
+ resolution: null,
263
+ };
264
+ }
265
+ return {
266
+ value: CURRENCY.DIVERGED,
267
+ reason: 'the current source would produce different content here',
268
+ resolution: null,
269
+ };
270
+ }
271
+ if (!basis.evaluated.has(resource?.harness)) {
272
+ return {
273
+ value: CURRENCY.INDETERMINABLE,
274
+ reason: `no plan was produced for ${resource?.harness ?? 'this harness'} in this scope, so this `
275
+ + 'copy was never compared with what the current source would produce',
276
+ resolution: `resolve whatever stopped the plan for ${resource?.harness ?? 'this harness'} — `
277
+ + '`doflow doctor` and `doflow status` name the adapter conflict, a native config file the adapter '
278
+ + 'could not parse being the usual cause — then re-run `doflow inventory`. Every copy of this '
279
+ + 'harness in this scope is unjudged until its plan runs',
280
+ };
281
+ }
282
+ if (deriveAssetIdentity(resource) === null) {
283
+ return {
284
+ value: CURRENCY.INDETERMINABLE,
285
+ reason: 'this row records no harness or no ownership identity, two of the four components this '
286
+ + 'scope\'s plan is keyed by, so no planned change could ever be looked up for it and its '
287
+ + 'absence from the plan compared nothing',
288
+ resolution: 'repair the recorded row so a planned change can be looked up for it — re-installing '
289
+ + 'this harness at this scope re-records it — or delete the row, if DoFlow no longer owns the '
290
+ + 'copy. No install writes a row in this shape, so this one was hand-edited',
291
+ };
292
+ }
293
+ return { value: CURRENCY.MATCHES, reason: null, resolution: null };
294
+ }
295
+
296
+ /** The literal command that would converge one scope. Text only — this verb never runs it. */
297
+ function reconcileCommand(scope: any, projectRoot: any): string {
298
+ return scope === 'global' ? 'doflow reconcile -g' : `doflow reconcile ${projectRoot}`;
299
+ }
300
+
301
+ /** One copy of a logical asset, as IC-002 requires: where it is, and how current it is — plus, where
302
+ * it could not be judged at all, what would give a later run a basis for judging it. */
303
+ function copyEntry(resource: any, scope: any, basis: any, fsImpl: any): any {
304
+ const currency = currencyOf(resource, basis, fsImpl);
305
+ return {
306
+ scope,
307
+ location: resource.target,
308
+ currency: currency.value,
309
+ currencyReason: currency.reason,
310
+ currencyResolution: currency.resolution,
311
+ assetId: resource.assetId,
312
+ ownershipIdentity: resource.ownershipIdentity ?? null,
313
+ };
314
+ }
315
+
316
+ /**
317
+ * Group every recorded resource of both scopes by its derived cross-scope identity.
318
+ *
319
+ * @param {Array<Object>} snapshots the scope snapshots `readScopes` returned
320
+ * @param {Object} [fsImpl] filesystem implementation, threaded to the currency judgement
321
+ * @returns {{groups: Map<string, Object>, unidentified: Array<Object>}} `unidentified` holds one
322
+ * row per resource whose identity could not be derived — never grouped, since `null` is not a key.
323
+ */
324
+ function groupByIdentity(snapshots: any, fsImpl: any): any {
325
+ const groups = new Map();
326
+ const unidentified: any[] = [];
327
+ for (const snapshot of snapshots) {
328
+ const basis = planBasis(snapshot);
329
+ for (const resource of snapshot.resources ?? []) {
330
+ const copy = copyEntry(resource, snapshot.scope, basis, fsImpl);
331
+ const identity = deriveAssetIdentity(resource);
332
+ if (identity === null) {
333
+ unidentified.push({
334
+ ...copy,
335
+ harness: resource.harness ?? null,
336
+ reason: 'this resource records no harness or no ownership identity, so no cross-scope '
337
+ + 'identity could be derived for it and it cannot be compared with a copy at the other '
338
+ + 'scope (IC-004); its own currency is still reported',
339
+ });
340
+ continue;
341
+ }
342
+ if (!groups.has(identity)) {
343
+ groups.set(identity, { identity, harness: resource.harness, assetId: resource.assetId, copies: [] });
344
+ }
345
+ groups.get(identity).copies.push(copy);
346
+ }
347
+ }
348
+ return { groups, unidentified };
349
+ }
350
+
351
+ /** The harness's recorded precedence block, or `null` when it declares none — the fifth IC-003
352
+ * state (the question has not been asked), which `precedence.js` must see as an absence rather than
353
+ * as an empty object it could misread as a block with no mode. */
354
+ function precedenceFor(registry: any, harness: any): any {
355
+ try { return harnessFor(registry, harness).scopePrecedence ?? null; }
356
+ catch { return null; }
357
+ }
358
+
359
+ /** Every unmanaged entry sharing a directory with one of this asset's copies, attributed to the
360
+ * scope whose copy put it in view. Never carries a currency (data model: `UNMANAGED_CANDIDATE`). */
361
+ function unmanagedFor(copies: any, candidatesByTarget: any): any[] {
362
+ const found: any[] = [];
363
+ for (const copy of copies) {
364
+ for (const candidate of candidatesByTarget[copy.location] ?? []) {
365
+ found.push({ ...candidate, scope: copy.scope });
366
+ }
367
+ }
368
+ return found;
369
+ }
370
+
371
+ /** The literal actions that would resolve this asset's findings, in the order a reader should take
372
+ * them. One object per asset (IC-002's "the remedy"), holding every step rather than the first, so
373
+ * an asset that is both diverged and shadowed does not have half its remedy silently dropped. */
374
+ function remedyFor({ copies, verdict, unmanaged, projectRoot }: any): any {
375
+ const actions: any[] = [];
376
+ for (const copy of copies) {
377
+ if (copy.currency === CURRENCY.DIVERGED) {
378
+ actions.push({
379
+ finding: FINDING_KINDS.DIVERGENCE,
380
+ action: `restore ${copy.location} to what the current source produces`,
381
+ command: reconcileCommand(copy.scope, projectRoot),
382
+ });
383
+ } else if (copy.currency === CURRENCY.INDETERMINABLE) {
384
+ // Never "restore it": nothing here knows what this copy should contain, which is the whole of
385
+ // what makes it unjudged. The action is what would give the next run a basis, which differs per
386
+ // absence — `currencyOf` states it beside the reason, so nothing here matches on reason strings.
387
+ actions.push({
388
+ finding: FINDING_KINDS.UNJUDGED_COPY,
389
+ action: `establish a basis for judging ${copy.location}, which nothing in the ${copy.scope} scope `
390
+ + `compared with what the current source would produce — ${copy.currencyResolution}`,
391
+ command: null,
392
+ });
393
+ }
394
+ }
395
+ if (verdict?.shadowed && verdict.winner) {
396
+ const losers = copies.filter((copy: any) => copy.scope !== verdict.winner).map((copy: any) => copy.location);
397
+ actions.push({
398
+ finding: FINDING_KINDS.SHADOW,
399
+ action: `edit the copy the harness actually loads — the ${verdict.winner} one — or delete the `
400
+ + `outranked ${losers.length === 1 ? 'copy' : 'copies'}: ${losers.join(', ')}`,
401
+ command: null,
402
+ });
403
+ }
404
+ if (verdict?.shadowed && !verdict.winner) {
405
+ actions.push({
406
+ finding: FINDING_KINDS.WITHHELD_VERDICT,
407
+ action: `keep exactly one copy — ${copies.map((copy: any) => copy.location).join(' or ')} — because `
408
+ + `${verdict.withheldReason}, so which one takes effect cannot be stated`,
409
+ command: null,
410
+ });
411
+ }
412
+ for (const candidate of unmanaged) {
413
+ actions.push({
414
+ finding: FINDING_KINDS.UNMANAGED_SIBLING,
415
+ action: `review ${candidate.path}: ${candidate.harness} loads every entry in `
416
+ + `${candidate.parentDir}, so this ${candidate.kind} is live context DoFlow did not install. `
417
+ + 'Move it out of that directory if it should not be',
418
+ command: null,
419
+ });
420
+ }
421
+ if (actions.length === 0) return null;
422
+ return { summary: `${actions.length} action(s) would resolve this entry`, actions };
423
+ }
424
+
425
+ /**
426
+ * The findings this asset contributes, in the shape `coverage.js` grades.
427
+ *
428
+ * `repairPath: 'reconcile'` is set on divergence findings **only**; the other three declare
429
+ * `manual`, which `coverage.js` counts unrepairable with the reason `outside-repair-path`. See the
430
+ * module header's note 2 for why that is not the same as omitting the field.
431
+ *
432
+ * Each finding's `scope` is the scope whose copy a remedy would have to change: a divergence names
433
+ * the diverged copy's scope, a shadow the winning scope (the copy an edit must land on), and an
434
+ * unmanaged sibling the scope whose copy put the directory in view. An unjudged copy names the scope
435
+ * holding it, which is determinate and is the scope the missing basis belongs to — the ledger row, the
436
+ * selection or the refused plan is that scope's. A **withheld verdict names
437
+ * none** — `null`. It is a statement about an asset held at both scopes with no way to say which
438
+ * takes effect, so its remedy ("keep exactly one copy") can be performed at either; naming
439
+ * `copies[0].scope` named the scope `readScopes` happens to read first. `coverage.js` grades a
440
+ * scope-less finding `unknown-scope`, which is the correct verdict — no lock can repair it — reached
441
+ * through a reason whose own definition is "the finding names a scope that is neither global nor
442
+ * project", written for a caller bug rather than for a finding that legitimately owns no scope. A
443
+ * reason of its own belongs to that module, whose write-set this is not; the gap is recorded as a
444
+ * risk in design §7 rather than worked around here by naming a scope that has not been established.
445
+ *
446
+ * @param {Set<string>} seenUnmanaged Unmanaged files already reported by an earlier asset in this
447
+ * run, keyed harness-and-path. FR-007's unit is the file: the second managed asset sharing a
448
+ * directory with a stray still **lists** it (IC-002, the asset entry) but no longer emits a second
449
+ * finding about it. The harness is in the key because a directory two harnesses both load
450
+ * wholesale makes the same file operative twice, which is two facts.
451
+ */
452
+ function findingsFor({ identity, harness, copies, verdict, unmanaged }: any, seenUnmanaged: any): any[] {
453
+ const findings: any[] = [];
454
+ for (const copy of copies) {
455
+ if (copy.currency === CURRENCY.DIVERGED) {
456
+ findings.push({ id: identity, kind: FINDING_KINDS.DIVERGENCE, harness, scope: copy.scope, repairPath: RECONCILE });
457
+ } else if (copy.currency === CURRENCY.INDETERMINABLE) {
458
+ findings.push({ id: identity, kind: FINDING_KINDS.UNJUDGED_COPY, harness, scope: copy.scope, repairPath: MANUAL });
459
+ }
460
+ }
461
+ if (verdict?.shadowed && verdict.winner) {
462
+ findings.push({ id: identity, kind: FINDING_KINDS.SHADOW, harness, scope: verdict.winner, repairPath: MANUAL });
463
+ }
464
+ if (verdict?.shadowed && !verdict.winner) {
465
+ findings.push({ id: identity, kind: FINDING_KINDS.WITHHELD_VERDICT, harness, scope: null, repairPath: MANUAL });
466
+ }
467
+ for (const candidate of unmanaged) {
468
+ const key = `${candidate.harness} ${candidate.path}`;
469
+ if (seenUnmanaged.has(key)) continue;
470
+ seenUnmanaged.add(key);
471
+ // `path` names the finding's actual subject, because `id` cannot: the stray threatens every
472
+ // managed asset in its directory, and the one named here is simply the first in identity order —
473
+ // the entry whose `remedy` states the action.
474
+ findings.push({
475
+ id: identity, kind: FINDING_KINDS.UNMANAGED_SIBLING, harness, scope: candidate.scope,
476
+ path: candidate.path, repairPath: MANUAL,
477
+ });
478
+ }
479
+ return findings;
480
+ }
481
+
482
+ /** One legible sentence a reader meets before the detail, so the verdict cannot be missed.
483
+ *
484
+ * A restriction to named harnesses is named in the sentence, because the sentence is where a reader
485
+ * decides whether to act. "nothing requires attention" read off a run restricted to one harness — or
486
+ * to a harness that is not installed — would otherwise be taken for a statement about the install. */
487
+ function summarize({ findings, assets, unidentified, targets }: any): string {
488
+ const counts = new Map();
489
+ for (const finding of findings) counts.set(finding.kind, (counts.get(finding.kind) ?? 0) + 1);
490
+ const subject = `${assets.length} logical asset(s)${targets ? ` recorded for ${targets.join(', ')}` : ''}`;
491
+ const trailer = unidentified.length > 0
492
+ ? ` ${unidentified.length} recorded resource(s) carry no derivable cross-scope identity and were `
493
+ + 'reported individually instead of joined.'
494
+ : '';
495
+ if (findings.length === 0) {
496
+ return `${subject} across both scopes; nothing requires attention.${trailer}`;
497
+ }
498
+ const parts = [...counts.entries()].map(([kind, count]) => `${count} ${kind}`).join(', ');
499
+ return `${findings.length} finding(s) across ${subject}: ${parts}.${trailer}`;
500
+ }
501
+
502
+ /**
503
+ * Compose the cross-scope report (IC-002).
504
+ *
505
+ * Field order is the contract's: the verdict first, then the two scope roots each marked present or
506
+ * absent, then one entry per logical asset, then the resources that could not be joined, then the
507
+ * findings, then the repair-coverage statement.
508
+ *
509
+ * **IC-002's last field, the count of unmanaged entries inspected but not individually reported, is
510
+ * deliberately absent.** `inspectSiblings` reads an inspected directory in full and reports every
511
+ * entry in it that DoFlow does not manage, and never reads a directory it does not inspect, so no
512
+ * input can put a number other than 0 in that field. Emitting it anyway stated nothing and read as
513
+ * a reassurance — `0` beside eight reported siblings invites the reading that eight more were held
514
+ * back. IC-002 is amended to drop the field rather than the report satisfying it vacuously (F.3).
515
+ *
516
+ * @param {Object} options
517
+ * @param {string} options.repoRoot Root of the DoFlow source the lifecycle view reads from.
518
+ * @param {string} options.projectRoot Root of the project scope. The global scope's root is derived
519
+ * from the process home directory and is not a parameter (design R7).
520
+ * @param {string[]} [options.targets] Harnesses to inventory; defaults to every lifecycle harness.
521
+ * IC-001 makes this a restriction, so it bounds the reported resources as well as the plan: a
522
+ * harness outside it appears nowhere, rather than appearing with a currency nothing evaluated.
523
+ * @param {Object} [options.registry] A loaded registry, for a caller that already has one.
524
+ * @param {Object} [options.locks] Lock documents per scope, as `readScopeLocks` returns them. Used
525
+ * twice: to bound the remedy (`coverage.js`) and to supply each scope's recorded MCP selections to
526
+ * its plan (`read-scopes.js`).
527
+ * @param {Object} [options.fsImpl] Filesystem implementation, threaded to sibling inspection.
528
+ * @returns {Object} the report object
529
+ * @throws when the registry is unreadable, a ledger or a lock is unparseable, or a named harness the
530
+ * registry does not declare was requested. IC-001 assigns all of these to exit 2 — a lock that
531
+ * exists but cannot be read is the same class as a ledger that cannot: the report would have to
532
+ * invent the selection the plan is computed from.
533
+ */
534
+ function buildInventoryReport({ repoRoot, projectRoot, targets, registry, locks, fsImpl }: any = {}): any {
535
+ const loaded = registry ?? loadRegistry({ repoRoot });
536
+ const requested = Array.isArray(targets) && targets.length > 0 ? targets : undefined;
537
+ // Validated before anything is read, so an unknown harness fails as the argument error it is
538
+ // rather than as an empty report. harnessFor throws naming the id it did not find.
539
+ for (const harness of requested ?? []) harnessFor(loaded, harness);
540
+
541
+ const resolvedProjectRoot = path.resolve(projectRoot ?? '.');
542
+ // One read of the locks, two consumers. `coverage.js` needs them to bound the remedy; `readScopes`
543
+ // needs their MCP selections to compute a plan that describes what is installed rather than one
544
+ // proposing to remove every server the scope chose.
545
+ const resolvedLocks = locks ?? readScopeLocks({ projectRoot: resolvedProjectRoot }, fsImpl ? { fsImpl } : {});
546
+ const { scopes } = readScopes({
547
+ registry: loaded, repoRoot, projectRoot: resolvedProjectRoot, locks: resolvedLocks,
548
+ ...(requested ? { targets: requested } : {}),
549
+ });
550
+
551
+ const { groups, unidentified } = groupByIdentity(scopes, fsImpl ?? fs);
552
+ const siblings = inspectSiblings({
553
+ resources: scopes.flatMap((snapshot: any) => snapshot.resources ?? []),
554
+ ...(fsImpl ? { fsImpl } : {}),
555
+ });
556
+
557
+ const assets: any[] = [];
558
+ const findings: any[] = [];
559
+ // One finding per unmanaged file across the whole report, not per asset that lists it (FR-007;
560
+ // see the header's note 3). Assets are walked in identity order, so which entry carries the
561
+ // finding is deterministic.
562
+ const seenUnmanaged = new Set();
563
+ for (const identity of [...groups.keys()].sort()) {
564
+ const { harness, assetId, copies } = groups.get(identity);
565
+ // Both crossings of the vocabulary seam, in one place: in, then straight back out.
566
+ const raw = resolveShadowVerdict(copies.map((copy: any) => toRegistryScope(copy.scope)), precedenceFor(loaded, harness));
567
+ const verdict = raw && {
568
+ presentAt: raw.presentAt.map(toReportScope),
569
+ shadowed: raw.shadowed,
570
+ winner: raw.winner === null ? null : toReportScope(raw.winner),
571
+ withheldReason: raw.withheldReason,
572
+ };
573
+ const unmanaged = unmanagedFor(copies, siblings.candidatesByTarget);
574
+ assets.push({
575
+ identity,
576
+ harness,
577
+ assetId,
578
+ copies,
579
+ verdict,
580
+ unmanaged,
581
+ remedy: remedyFor({ copies, verdict, unmanaged, projectRoot: resolvedProjectRoot }),
582
+ });
583
+ findings.push(...findingsFor({ identity, harness, copies, verdict, unmanaged }, seenUnmanaged));
584
+ }
585
+
586
+ // An unidentifiable row cannot be joined, so it can never be a shadow or a withheld verdict — but
587
+ // its own copy can still have diverged, and that judgement needs no join. Dropping it would hide
588
+ // a real, repairable finding behind a limitation of the join key. The same holds of the copy nothing
589
+ // judged: a row missing the components the plan is keyed by is the one reaching that state most
590
+ // often, so exempting the unidentified list would exempt the commonest unjudged copy there is. Its
591
+ // remedy is on the row — `currencyResolution` — rather than on an asset entry it has none of.
592
+ for (const row of unidentified) {
593
+ if (row.currency === CURRENCY.DIVERGED) {
594
+ findings.push({ id: null, kind: FINDING_KINDS.DIVERGENCE, harness: row.harness, scope: row.scope, repairPath: RECONCILE });
595
+ } else if (row.currency === CURRENCY.INDETERMINABLE) {
596
+ findings.push({ id: null, kind: FINDING_KINDS.UNJUDGED_COPY, harness: row.harness, scope: row.scope, repairPath: MANUAL });
597
+ }
598
+ }
599
+
600
+ const coverage = repairCoverage({ findings, locks: resolvedLocks });
601
+
602
+ return {
603
+ status: findings.length === 0 ? 'CLEAN' : 'FINDINGS',
604
+ exitCode: findings.length === 0 ? 0 : 1,
605
+ summary: summarize({ findings, assets, unidentified, targets: requested }),
606
+ scopes: scopes.map((snapshot: any) => ({
607
+ scope: snapshot.scope,
608
+ root: snapshot.scopeRoot,
609
+ present: snapshot.recorded,
610
+ resources: (snapshot.resources ?? []).length,
611
+ })),
612
+ assets,
613
+ unidentified,
614
+ findings,
615
+ repairCoverage: coverage,
616
+ };
617
+ }
618
+
619
+ /** Human rendering. The same facts as `--json`, in the same order, so neither shape is the one that
620
+ * tells the truth. */
621
+ function printReport(report: any): void {
622
+ console.log('\nDoFlow cross-scope inventory:');
623
+ console.log('═'.repeat(78));
624
+ console.log(report.summary);
625
+ for (const scope of report.scopes) {
626
+ console.log(` ${scope.scope.padEnd(8)} ${scope.present ? 'present' : 'absent '} ${scope.root} (${scope.resources} resource(s))`);
627
+ }
628
+ for (const asset of report.assets) {
629
+ // An asset with no remedy has no finding, so it is left out for brevity; every asset a reader must
630
+ // act on has one, including an unjudged copy since `remedyFor` states what would let it be judged.
631
+ // A `matches` copy carrying a reason is the one thing this skip still hides — see `currencyOf`'s
632
+ // rewrite-already-on-disk branch — and it is genuinely nothing to act on.
633
+ if (!asset.remedy) continue;
634
+ console.log(`\n ${asset.identity}`);
635
+ for (const copy of asset.copies) {
636
+ // The reason, not only the value: `indeterminable` alone does not say which of the four absences
637
+ // this is, and the four call for different actions.
638
+ console.log(` ${copy.scope}: ${copy.location} — ${copy.currency}`
639
+ + `${copy.currencyReason ? ` (${copy.currencyReason})` : ''}`);
640
+ }
641
+ if (asset.verdict?.shadowed) {
642
+ console.log(` verdict: ${asset.verdict.winner ? `${asset.verdict.winner} wins` : `withheld — ${asset.verdict.withheldReason}`}`);
643
+ }
644
+ for (const action of asset.remedy.actions) {
645
+ console.log(` remedy: ${action.action}${action.command ? ` (${action.command})` : ''}`);
646
+ }
647
+ }
648
+ if (report.unidentified.length > 0) {
649
+ console.log(`\n ${report.unidentified.length} resource(s) with no derivable cross-scope identity:`);
650
+ for (const row of report.unidentified) {
651
+ console.log(` ${row.scope}: ${row.location} — ${row.currency}`
652
+ + `${row.currencyReason ? ` (${row.currencyReason})` : ''}`);
653
+ // These rows carry no asset entry, so the row is the only place their remedy can be stated.
654
+ if (row.currencyResolution) console.log(` remedy: ${row.currencyResolution}`);
655
+ }
656
+ }
657
+ console.log(`\n ${report.repairCoverage.summary}`);
658
+ console.log('═'.repeat(78) + '\n');
659
+ }
660
+
661
+ /**
662
+ * The `inventory` verb (IC-001). Reads both scopes and reports; writes nothing.
663
+ *
664
+ * Exit statuses are the seam's existing three, with no fourth meaning: 0 answered and nothing to act
665
+ * on; 1 answered and there is a finding — a shadow, a divergence, an unmanaged contender, a withheld
666
+ * verdict or a copy nothing judged, since the caller cannot conclude the install is sound from any of
667
+ * them; 2 the verb could not do what was asked. Exit 0 is the positive statement IC-001 makes it: every
668
+ * asset resolved to a copy that matches what the current source would produce. A scope with nothing
669
+ * recorded is not an error and never produces 2.
670
+ *
671
+ * @param {Object} options
672
+ * @param {string} options.repoRoot Root of the DoFlow source.
673
+ * @param {string} options.projectRoot Root of the project scope (the optional positional, or cwd).
674
+ * @param {string[]} [options.targets] The optional restriction to named harnesses.
675
+ * @param {boolean} [options.json] The seam's standard machine-readable output flag.
676
+ * @param {boolean} [options.global] The scope selector, accepted by the shared parser and refused
677
+ * here: reading both scopes is this verb's entire purpose, so honouring a scope flag would make
678
+ * its one distinguishing behaviour optional, and ignoring one silently would answer a different
679
+ * question from the one the caller asked.
680
+ * @returns {number} the exit status, already set on the process
681
+ */
682
+ function handleInventoryCommand({ repoRoot, projectRoot, targets, json = false, global: globalScope = false }: any = {}): any {
683
+ if (globalScope) {
684
+ return usageError('inventory', '-g/--global is not accepted: this verb reads both scopes in one '
685
+ + 'invocation, which is its entire purpose. Re-run without the flag.', json);
686
+ }
687
+
688
+ let report;
689
+ try {
690
+ report = buildInventoryReport({ repoRoot, projectRoot, targets });
691
+ } catch (error) {
692
+ // IC-001 assigns an unreadable registry, an unparseable ledger and an undeclared harness alike
693
+ // to exit 2. A scope with nothing recorded never reaches here — `readScopes` returns it empty.
694
+ return usageError('inventory', (error as any).message, json, error);
695
+ }
696
+
697
+ if (json) console.log(JSON.stringify(report, null, 2));
698
+ else printReport(report);
699
+ return finishRuntime(report.exitCode);
700
+ }
701
+
702
+ module.exports = {
703
+ buildInventoryReport, handleInventoryCommand, printReport,
704
+ CURRENCY, FINDING_KINDS, RECONCILE, MANUAL,
705
+ };