@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,620 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ // Shared copy-tree engine — the generic "own a tree of files" implementation every asset
6
+ // declaring `renderer: "copy-tree"` in core/registry/assets.json needs. Codex's adapter already
7
+ // proved this per-file fingerprint/conflict model for agents and hooks (ownedRemovalPlan,
8
+ // nativeManagedResources); this module generalizes it so Claude, Gemini, and Codex's own
9
+ // tree-shaped assets (skills, rules, agent-specs, templates, scripts, modes, references) share
10
+ // one implementation instead of three adapter-specific copies.
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+ const crypto = require('node:crypto');
14
+
15
+ function sha256(buf: any): string {
16
+ return crypto.createHash('sha256').update(buf).digest('hex');
17
+ }
18
+
19
+ function walkRelFiles(dir: string, fsImpl: any): string[] {
20
+ const out: string[] = [];
21
+ for (const entry of fsImpl.readdirSync(dir, { withFileTypes: true })) {
22
+ const abs = path.join(dir, entry.name);
23
+ if (entry.isDirectory()) out.push(...walkRelFiles(abs, fsImpl).map((rel) => path.join(entry.name, rel)));
24
+ else if (entry.isFile()) out.push(entry.name);
25
+ }
26
+ return out;
27
+ }
28
+
29
+ /** Destination layouts. A copy-tree normally mirrors source paths exactly, but a harness may
30
+ * require a different shape for the same content: Antigravity discovers a custom agent at
31
+ * `.agents/agents/<name>/agent.md`, a directory per agent, so DoFlow's flat `<name>.md` specs were
32
+ * written where that harness never looks. Declared per projection in the registry rather than
33
+ * branched on inside an adapter, so the shape stays visible next to the asset it applies to.
34
+ * @type {Record<string, (sourceRel: string) => string>}
35
+ */
36
+ const LAYOUTS: Record<string, (sourceRel: string) => string> = {
37
+ /** `spec-analyst.md` -> `spec-analyst/agent.md` */
38
+ 'dir-per-file:agent.md': (sourceRel: string) => {
39
+ const ext = path.extname(sourceRel);
40
+ return path.join(sourceRel.slice(0, sourceRel.length - ext.length), `agent${ext || '.md'}`);
41
+ },
42
+ /** `RULE_01_SAFETY.md` -> `RULE_01_SAFETY.instructions.md` — GitHub's path-specific
43
+ * instructions are discovered by their `.instructions.md` suffix. */
44
+ 'instructions-md': (sourceRel: string) => {
45
+ const ext = path.extname(sourceRel);
46
+ return `${sourceRel.slice(0, sourceRel.length - ext.length)}.instructions.md`;
47
+ },
48
+ /** `MODE_Orchestration.md` -> `doflow-orchestration.md` — Claude's /config picker lists
49
+ * output styles by file name; the doflow- prefix keeps DoFlow's styles grouped and
50
+ * non-colliding with anything a user authors by hand. */
51
+ 'doflow-output-style': (sourceRel: string) => {
52
+ const ext = path.extname(sourceRel);
53
+ const base = path.basename(sourceRel, ext).replace(/^MODE_/i, '').replace(/_/g, '-').toLowerCase();
54
+ return `doflow-${base}${ext || '.md'}`;
55
+ },
56
+ /** `core-implementer.md` -> `core-implementer.json` — kiro-cli discovers custom agents as
57
+ * `.kiro/agents/<name>.json` (2.27.1 `kiro-cli agent list` did not list `.md` agents). */
58
+ 'kiro-agent-json': (sourceRel: string) => {
59
+ const ext = path.extname(sourceRel);
60
+ return `${sourceRel.slice(0, sourceRel.length - ext.length)}.json`;
61
+ },
62
+ };
63
+
64
+ /** Resolve a declared layout name to its mapper. Unknown names fail loudly rather than silently
65
+ * mirroring, because a typo would otherwise install to the wrong shape and look like it worked. */
66
+ function resolveLayout(name: any): (sourceRel: string) => string {
67
+ if (!name) return (rel: string) => rel;
68
+ const layout = LAYOUTS[name];
69
+ if (!layout) throw new Error(`Unknown copy-tree layout '${name}' (known: ${Object.keys(LAYOUTS).join(', ')})`);
70
+ return layout;
71
+ }
72
+
73
+ /** Destination content transforms. Where a harness reads a supported but different file FORMAT
74
+ * than the shared source authors (Copilot's path-specific instructions require an `applyTo`
75
+ * frontmatter header; OpenCode's markdown agents want their own frontmatter vocabulary), the
76
+ * projection declares a named transform instead of an adapter branching on bytes. A transform is
77
+ * a pure function of (sourceRelPath, sourceBytes) -> Buffer: deterministic, so the fingerprint
78
+ * planTree records is the fingerprint verifyTree re-derives and removeTree re-checks, and so
79
+ * applyTree can re-derive the written bytes from the untouched source at any later retry.
80
+ * @type {Record<string, (sourceRel: string, content: Buffer) => Buffer>}
81
+ */
82
+ const TRANSFORMS: Record<string, (sourceRel: string, content: any) => any> = {
83
+ /** Shared mode doc -> Claude output style with name/description/keep-coding-instructions */
84
+ 'claude-output-styles': renderClaudeOutputStyle,
85
+ /** `<rule>.md` -> `<rule>.instructions.md` body under a Copilot applyTo header */
86
+ 'copilot-rule-instructions': (sourceRel: string, content: any) => {
87
+ void sourceRel;
88
+ return Buffer.from(`---\napplyTo: '**'\n---\n\n${stripFrontmatter(content.toString('utf8'))}`);
89
+ },
90
+ /** Shared agent spec frontmatter -> only the keys Gemini CLI's subagent schema accepts */
91
+ 'gemini-agents': (sourceRel: string, content: any) => {
92
+ void sourceRel;
93
+ return Buffer.from(filterGeminiAgentFrontmatter(content.toString('utf8')));
94
+ },
95
+ /** Shared agent spec frontmatter -> OpenCode's markdown-agent vocabulary */
96
+ 'opencode-agents': (sourceRel: string, content: any) => {
97
+ void sourceRel;
98
+ return Buffer.from(renderOpencodeAgent(content.toString('utf8')));
99
+ },
100
+ /** Shared agent spec -> kiro-cli custom agent JSON (name, description, prompt, tools, hooks) */
101
+ 'kiro-agents': (sourceRel: string, content: any) => {
102
+ void sourceRel;
103
+ return Buffer.from(renderKiroAgent(content.toString('utf8')));
104
+ },
105
+ };
106
+
107
+ function resolveTransform(name: any): ((sourceRel: string, content: any) => any) | null {
108
+ if (!name) return null;
109
+ const transform = TRANSFORMS[name];
110
+ if (!transform) throw new Error(`Unknown copy-tree transform '${name}' (known: ${Object.keys(TRANSFORMS).join(', ')})`);
111
+ return transform;
112
+ }
113
+
114
+ function stripFrontmatter(text: string): string {
115
+ if (!text.startsWith('---')) return text;
116
+ const end = text.indexOf('\n---', 3);
117
+ if (end === -1) return text;
118
+ return text.slice(end + 4).replace(/^\n+/, '');
119
+ }
120
+
121
+ /** Wrap a shared mode document as a Claude output style: system-prompt modifiers keep the
122
+ * built-in engineering instructions (these styles shape HOW DoFlow works, not WHETHER Claude
123
+ * codes). The description is lifted from the document's first `**Purpose**` line so the /config
124
+ * picker explains each style without opening it. */
125
+ function renderClaudeOutputStyle(sourceRel: string, content: any): string {
126
+ const text = Buffer.isBuffer(content) ? content.toString('utf8') : String(content);
127
+ const base = path.basename(sourceRel).replace(/\.[^.]+$/, '').replace(/^MODE_/i, '').replace(/_/g, ' ');
128
+ const purpose = text.match(/\*\*Purpose\*\*[:*]*\s*(.+)?/);
129
+ const description = (purpose && purpose[1] ? purpose[1] : `DoFlow ${base} mode`).trim().replace(/\s+/g, ' ');
130
+ return ['---', `name: DoFlow: ${base}`, `description: ${JSON.stringify(description)}`, 'keep-coding-instructions: true', '---', '', text.replace(/\n*$/, ''), ''].join('\n');
131
+ }
132
+
133
+ /** The frontmatter keys Gemini CLI documents for a custom subagent (https://geminicli.com/docs/core/subagents, accessed 2026-10-06).
134
+ * Its agent schema is strict, so a spec key outside this list (DoFlow's `effort`) would be rejected. */
135
+ const GEMINI_AGENT_KEYS = new Set(['name', 'description', 'kind', 'tools', 'mcpServers', 'model', 'temperature', 'max_turns', 'timeout_mins']);
136
+
137
+ /** Keep only Gemini's documented top-level frontmatter keys, with their indented continuation
138
+ * lines, in source order. The body after the closing `---` passes through byte for byte, and a
139
+ * file without frontmatter is returned unchanged. */
140
+ function filterGeminiAgentFrontmatter(text: string): string {
141
+ const match = text.match(/^---\r?\n([\s\S]*?\r?\n)---(?=\r?\n|$)/);
142
+ if (!match) return text;
143
+ let keep = false;
144
+ const kept = match[1].split(/(?<=\n)/).filter((line: string) => {
145
+ const key = line.match(/^([A-Za-z][A-Za-z0-9_-]*):/);
146
+ if (key) keep = GEMINI_AGENT_KEYS.has(key[1]);
147
+ return keep;
148
+ });
149
+ return `${text.slice(0, text.indexOf('\n') + 1)}${kept.join('')}${text.slice(match[0].length - 3)}`;
150
+ }
151
+
152
+ /** The command a Kiro hook runs for one front-door script: the project copy when the git root
153
+ * holds one, else the user-scope copy under $HOME. One builder, so the static `doflow` agent
154
+ * (core/harnesses/kiro/agents/doflow.json) and every generated specialist share the exact string. */
155
+ function kiroHookCommand(script: string): string {
156
+ return `bash "$(root=$(git rev-parse --show-toplevel 2>/dev/null) && [ -f "$root/.kiro/hooks/${script}" ] && printf '%s/.kiro/hooks/${script}' "$root" || printf '%s/.kiro/hooks/${script}' "$HOME")"`;
157
+ }
158
+
159
+ /** One kiro-cli agent hook entry. */
160
+ function kiroHook({ name, trigger, matcher, script, timeout }: { name: string, trigger: string, matcher?: string, script: string, timeout: number }): any {
161
+ return {
162
+ name, trigger, ...(matcher ? { matcher } : {}),
163
+ action: { type: 'command', command: kiroHookCommand(script) }, timeout,
164
+ };
165
+ }
166
+
167
+ /** The three preToolUse hooks every DoFlow-rendered Kiro agent carries. The MCP guard uses the
168
+ * "*" matcher: Kiro documents "@<server>" (every tool of one server) and "*" but no all-MCP
169
+ * matcher, and mcp-tool-guard.sh exits 0 immediately for any non-MCP tool name, so "*" costs one
170
+ * fast no-op for built-in tools and still covers every MCP server. */
171
+ function kiroPreToolUseHooks(): any[] {
172
+ return [
173
+ kiroHook({ name: 'doflow-pre-implementation-gate', trigger: 'preToolUse', matcher: 'write', script: 'pre-implement-gate.sh', timeout: 10 }),
174
+ kiroHook({ name: 'doflow-pre-bash-guard', trigger: 'preToolUse', matcher: 'shell', script: 'pre-bash-guard.sh', timeout: 10 }),
175
+ kiroHook({ name: 'doflow-mcp-tool-guard', trigger: 'preToolUse', matcher: '*', script: 'mcp-tool-guard.sh', timeout: 10 }),
176
+ ];
177
+ }
178
+
179
+ /** Map one core/shared agent-spec file onto a kiro-cli custom agent. Spec keys other than
180
+ * name/description (model: inherit, effort) are dropped: Kiro inherits the session model. */
181
+ function renderKiroAgent(text: string): string {
182
+ const fm: Record<string, string> = {};
183
+ let body = text;
184
+ if (text.startsWith('---')) {
185
+ const end = text.indexOf('\n---', 3);
186
+ if (end !== -1) {
187
+ for (const line of text.slice(4, end).split('\n')) {
188
+ const m = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
189
+ if (m) fm[m[1]] = m[2].trim().replace(/^"|"$/g, '');
190
+ }
191
+ body = text.slice(end + 4).replace(/^\n+/, '');
192
+ }
193
+ }
194
+ const agent = {
195
+ name: fm.name || '', description: fm.description || '',
196
+ prompt: `${body.replace(/\s+$/, '')}\n`,
197
+ tools: ['*'],
198
+ hooks: kiroPreToolUseHooks(),
199
+ };
200
+ return `${JSON.stringify(agent, null, 2)}\n`;
201
+ }
202
+
203
+ const OPENCODE_READONLY_AGENTS = new Set(['spec-analyst', 'system-architect', 'quality-guardian', 'research-writer']);
204
+
205
+ /** Map one core/shared agent-spec file onto OpenCode's documented markdown-agent frontmatter
206
+ * (description / mode / permission; model omitted because OpenCode has no 'inherit'). Unknown
207
+ * spec keys (tools, effort) are dropped rather than passed through, so OpenCode never sees
208
+ * vocabulary it does not define. Body below the frontmatter passes through untouched. */
209
+ function renderOpencodeAgent(text: string): string {
210
+ const fm: Record<string, string> = {};
211
+ let body = text;
212
+ if (text.startsWith('---')) {
213
+ const end = text.indexOf('\n---', 3);
214
+ if (end !== -1) {
215
+ for (const line of text.slice(4, end).split('\n')) {
216
+ const m = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
217
+ if (m) fm[m[1]] = m[2].trim().replace(/^"|"$/g, '');
218
+ }
219
+ body = text.slice(end + 4).replace(/^\n+/, '');
220
+ }
221
+ }
222
+ const lines = ['---'];
223
+ if (fm.description) lines.push(`description: ${JSON.stringify(fm.description)}`);
224
+ lines.push('mode: subagent');
225
+ if (OPENCODE_READONLY_AGENTS.has(fm.name)) {
226
+ lines.push('permission:', ' edit: deny', ' bash: deny');
227
+ }
228
+ lines.push('---', '', body.replace(/\n*$/, ''), '');
229
+ return lines.join('\n');
230
+ }
231
+
232
+ /** List every source file with its would-be destination and content fingerprint. Does not touch
233
+ * the destination tree beyond checking existence. `relPath` is destination-relative and doubles as
234
+ * the ledger identity, so a layout change relocates the recorded resource too — planTree's
235
+ * relocation handling then removes the old path rather than orphaning it. */
236
+ function discoverTree({ sourceDir, destDir, fsImpl = fs, layout, transform }: any): { files: any[] } {
237
+ if (!fsImpl.existsSync(sourceDir)) throw new Error(`copy-tree source is missing: ${sourceDir}`);
238
+ const mapRel = typeof layout === 'function' ? layout : resolveLayout(layout);
239
+ const mapContent = typeof transform === 'function' ? transform : resolveTransform(transform);
240
+ const files = walkRelFiles(sourceDir, fsImpl).map((sourceRel) => {
241
+ const relPath = mapRel(sourceRel);
242
+ const sourceAbs = path.join(sourceDir, sourceRel);
243
+ const destAbs = path.join(destDir, relPath);
244
+ const raw = fsImpl.readFileSync(sourceAbs);
245
+ return { relPath, sourceAbs, destAbs, exists: fsImpl.existsSync(destAbs), fingerprint: sha256(mapContent ? mapContent(sourceRel, raw) : raw) };
246
+ });
247
+ return { files };
248
+ }
249
+
250
+ /** A file's identity on disk (device + inode), or null when the path does not resolve to one. */
251
+ function fileId(file: string, fsImpl: any): string | null {
252
+ try {
253
+ const stat = fsImpl.statSync(file, { bigint: true });
254
+ // An fs stand-in that reports no inode identifies nothing, rather than every file as one.
255
+ if (stat?.dev === undefined || stat?.ino === undefined) return null;
256
+ return `${stat.dev}:${stat.ino}`;
257
+ } catch { return null; }
258
+ }
259
+
260
+ /**
261
+ * A lookup answering "is `target` the same file on disk as one of `paths`, and which one?".
262
+ * A case-insensitive filesystem (APFS, NTFS by default) resolves `Foo.js` and `foo.js` to one
263
+ * file, so a string compare calls them two and a removal of the old spelling deletes the file just
264
+ * written under the new one. Device + inode is the only answer that holds on every filesystem, and
265
+ * a hard link answers the same way. Built on first use, one stat per path; a path that does not
266
+ * exist matches nothing, and `target` never matches itself.
267
+ */
268
+ function sameFileIndex(paths: string[], fsImpl: any = fs): (target: string) => string | null {
269
+ let byId: Map<string, string> | undefined;
270
+ return (target: string) => {
271
+ if (byId === undefined) {
272
+ byId = new Map();
273
+ for (const candidate of paths) {
274
+ const id = fileId(candidate, fsImpl);
275
+ if (id !== null && !byId.has(id)) byId.set(id, candidate);
276
+ }
277
+ }
278
+ const id = fileId(target, fsImpl);
279
+ const match = id === null ? undefined : byId.get(id);
280
+ return match !== undefined && match !== target ? match : null;
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Diff the source tree against `previousResources` (the ledger's prior per-file ownership
286
+ * records: `{relPath, fingerprint}`) to produce create/update/remove changes, refusing to touch
287
+ * any destination file whose on-disk content doesn't match its last-recorded fingerprint (or, for
288
+ * a never-before-owned file, doesn't match the source it would be overwritten with) — the same
289
+ * conflict-safety Codex's adapter already applies to agents and hooks.
290
+ * `operation: 'remove'` skips the source tree entirely and only proposes removals for every
291
+ * previously-owned file, mirroring Codex's `ownedRemovalPlan`.
292
+ * `siblingReplaced` lists the harnesses whose recorded bytes this plan replaces: the file matches
293
+ * neither the source nor this harness's own record, only a sibling's (forced or not).
294
+ */
295
+ function planTree({ sourceDir, destDir, previousResources = [], operation = 'apply', fsImpl = fs, layout, transform, force = false, keepModified = false, siblingFingerprints = new Map() }: any): any {
296
+ const prevByPath = new Map<string, any>(previousResources.map((resource: any) => [resource.relPath, resource]));
297
+ const changes: any[] = [];
298
+ const conflicts: string[] = [];
299
+ const kept: any[] = [];
300
+ const siblingReplaced = new Set<string>();
301
+
302
+ // Fingerprints the CURRENT source would write, keyed by destination. Resolved lazily and only
303
+ // when a recorded fingerprint has already failed to match, so the common removal path still
304
+ // never reads the source tree. A missing source directory is not an error here — an asset can be
305
+ // removed after its source moved — it simply leaves the recorded fingerprint as the only signal.
306
+ let sourceByDest: Map<string, string> | undefined;
307
+ const sourceFingerprint = (destAbs: string): string | undefined => {
308
+ if (sourceByDest === undefined) {
309
+ sourceByDest = sourceDir && fsImpl.existsSync(sourceDir)
310
+ ? new Map<string, string>(discoverTree({ sourceDir, destDir, fsImpl, layout, transform }).files.map((file): [string, string] => [file.destAbs, file.fingerprint]))
311
+ : new Map();
312
+ }
313
+ return sourceByDest.get(destAbs);
314
+ };
315
+
316
+ // Every destination the current source produces, set once the apply path has discovered them.
317
+ let producedSameFile: ((target: string) => string | null) | undefined;
318
+
319
+ const proposeRemoval = (prev: any, { relocated = false }: { relocated?: boolean } = {}) => {
320
+ // prev's OWN recorded location, not the current destDir — an asset whose nativeDir changed
321
+ // since prev was recorded must be removed from where it actually is, not from where it would
322
+ // land today. Absent (pre-this-fix data, or a caller-constructed previousResources entry with
323
+ // no target) falls back to the old destDir-relative computation, unchanged.
324
+ const destAbs = prev.target ?? path.join(destDir, prev.relPath);
325
+ if (!fsImpl.existsSync(destAbs)) return;
326
+ const current = sha256(fsImpl.readFileSync(destAbs));
327
+ // A recorded path that is the same file as one the source produces (a case-only rename on a
328
+ // case-insensitive filesystem) is that produced file under its old spelling: deleting it
329
+ // deletes what this run writes. Only the old row is released; the file stays, and the
330
+ // produced path's row is recorded when the run verifies.
331
+ if (relocated && producedSameFile?.(destAbs)) {
332
+ changes.push({ relPath: prev.relPath, target: destAbs, operation: 'remove', fingerprint: current, kept: true });
333
+ return;
334
+ }
335
+ // Untampered on removal means the same two signals the apply path below already accepts: the
336
+ // bytes this harness last recorded, or the bytes the current source would write. The second
337
+ // one matters because a destination tree can be claimed by several harnesses (scripts.doflow
338
+ // is one `<project>/.doflow/scripts` for claude, codex and gemini), so a sibling's update
339
+ // legitimately rewrites files this harness's rows still describe — the same "a sibling
340
+ // changed bytes my row still describes" case the apply path had to be taught, arriving here
341
+ // as an un-releasable claim instead of a refused install. A hand edit matches neither and is
342
+ // still refused. The OBSERVED fingerprint travels with the change so removeTree's own
343
+ // pre-delete re-check agrees with the decision taken here rather than throwing mid-apply.
344
+ // A third signal, the bytes a sibling harness recorded at this target: in a shared tree the
345
+ // sibling may have written the file last, and calling that a hand edit would keep it under a
346
+ // false label while the lifecycle's runtime prune deletes it on the same proof.
347
+ const modified = current !== prev.fingerprint && current !== sourceFingerprint(destAbs) && !siblingFingerprints.get(destAbs)?.has(current);
348
+ // A row left behind by an apply (the asset moved, or its source file went away) is released,
349
+ // and a hand edit there is the user's, not DoFlow's: with `keepModified` the file stays where
350
+ // it is, force or not, and only the ledger row goes. The change carries `kept` so removeTree
351
+ // skips it and the caller can say so.
352
+ if (relocated && keepModified && modified) {
353
+ kept.push({ relPath: prev.relPath, target: destAbs });
354
+ changes.push({ relPath: prev.relPath, target: destAbs, operation: 'remove', fingerprint: current, kept: true });
355
+ return;
356
+ }
357
+ if (!force && modified) {
358
+ conflicts.push(`${prev.relPath} was modified outside DoFlow`);
359
+ return;
360
+ }
361
+ changes.push({ relPath: prev.relPath, target: destAbs, operation: 'remove', fingerprint: current });
362
+ };
363
+
364
+ if (operation === 'remove') {
365
+ for (const prev of previousResources) proposeRemoval(prev);
366
+ return { changes, conflicts, kept, siblingReplaced: [] };
367
+ }
368
+
369
+ const { files } = discoverTree({ sourceDir, destDir, fsImpl, layout, transform });
370
+ producedSameFile = sameFileIndex(files.map((file) => file.destAbs), fsImpl);
371
+ const satisfiedAtSameLocation = new Set();
372
+ for (const file of files) {
373
+ const prev = prevByPath.get(file.relPath);
374
+ // prev.target === undefined means "location unknown" (pre-this-fix ledger data, or a
375
+ // caller-constructed previousResources entry) — treat as same-location, matching pre-fix
376
+ // behavior exactly, rather than misreading "no data" as "relocated" and spuriously removing
377
+ // it. Only an explicit, differing target means the asset's nativeDir actually changed.
378
+ const sameLocation = prev !== undefined && (prev.target === undefined || prev.target === file.destAbs);
379
+ if (sameLocation) satisfiedAtSameLocation.add(file.relPath);
380
+ if (file.exists) {
381
+ // A destination file is untampered if it matches the source we are about to write, OR the
382
+ // fingerprint this harness last recorded AT THIS LOCATION. Source-match is checked FIRST and
383
+ // unconditionally: it is the stronger signal, and a present-but-stale `prev` must not shadow
384
+ // it.
385
+ //
386
+ // This matters because guidance.context-layer projects one destination for all three
387
+ // harnesses while ownership is recorded per harness, so a sibling's install legitimately
388
+ // changes bytes that this harness's row still describes. Comparing only against `prev` made
389
+ // that indistinguishable from a hand edit and refused the whole install. Ordering the
390
+ // disjunction this way adds no new notion of safety — it stops a weaker signal from
391
+ // preempting a check that would have passed. Gating the fallback on `sameLocation` (rather
392
+ // than `prev !== undefined` alone, as before) is strictly more precise: a relocated asset's
393
+ // old row describes different bytes at a different path, so it must not be consulted here.
394
+ const current = sha256(fsImpl.readFileSync(file.destAbs));
395
+ const ownRecord = current === file.fingerprint || (sameLocation && current === prev.fingerprint);
396
+ const sibling = ownRecord ? undefined : siblingFingerprints.get(file.destAbs)?.get(current);
397
+ const knownGood = force || ownRecord || sibling !== undefined;
398
+ if (!knownGood) { conflicts.push(`${file.relPath} was modified outside DoFlow`); continue; }
399
+ if (sibling !== undefined) siblingReplaced.add(sibling);
400
+ // A true no-op requires the DESTINATION bytes to equal the incoming source. Comparing only
401
+ // ledger-vs-source fingerprints here (the previous form) let a forced run over a hand-edited
402
+ // destination — force ⇒ knownGood — fall through and silently ignore exactly the drift it
403
+ // was asked to heal.
404
+ if (sameLocation && current === file.fingerprint && prev.fingerprint === file.fingerprint) continue; // unchanged, no-op
405
+ }
406
+ changes.push({ relPath: file.relPath, target: file.destAbs, source: file.sourceAbs,
407
+ operation: sameLocation ? 'update' : 'create', fingerprint: file.fingerprint });
408
+ }
409
+ for (const prev of previousResources) {
410
+ if (!satisfiedAtSameLocation.has(prev.relPath)) proposeRemoval(prev, { relocated: true });
411
+ }
412
+ return { changes, conflicts, kept, siblingReplaced: [...siblingReplaced].sort() };
413
+ }
414
+
415
+ /** Write every create/update change. Preserves the source file's mode (so a hook script's +x
416
+ * bit survives the copy without a separate chmod pass) and mtime, matching this codebase's
417
+ * general convention for a lifecycle-owned write (see src/adapters/claude/index.ts's
418
+ * applySettingsAsset for the same pattern applied to a transformed-content file). */
419
+ function applyTree({ changes = [], fsImpl = fs, transform }: any): { applied: number } {
420
+ const mapContent = typeof transform === 'function' ? transform : resolveTransform(transform);
421
+ let applied = 0;
422
+ for (const change of changes) {
423
+ if (change.operation === 'remove') continue;
424
+ fsImpl.mkdirSync(path.dirname(change.target), { recursive: true });
425
+ const sourceStat = fsImpl.statSync(change.source);
426
+ if (mapContent) {
427
+ // Re-derive the bytes from the source at write time instead of copying it: the transform,
428
+ // not the source file, is what the recorded fingerprint describes. Keeps recovery retries
429
+ // correct without ever persisting rendered content in state.
430
+ fsImpl.writeFileSync(change.target, mapContent(path.basename(change.source), fsImpl.readFileSync(change.source)));
431
+ fsImpl.chmodSync(change.target, sourceStat.mode & 0o777);
432
+ } else {
433
+ fsImpl.copyFileSync(change.source, change.target);
434
+ fsImpl.chmodSync(change.target, sourceStat.mode & 0o777);
435
+ }
436
+ fsImpl.utimesSync(change.target, Math.floor(sourceStat.atimeMs / 1000), Math.floor(sourceStat.mtimeMs / 1000));
437
+ applied += 1;
438
+ }
439
+ return { applied };
440
+ }
441
+
442
+ /** Why a removal the runtime prune planned may no longer run, or null when it still may: the
443
+ * prune's plan-time proof (src/lifecycle/index.ts pruneRetiredRuntimeFiles) re-run right before the
444
+ * unlink, so a directory swapped for a symlink, or a file linked to a shipped one, after the plan was
445
+ * made is not deleted through. `guard` is `{ root, source }`: the tree and the source it projects.
446
+ * One walk of the source and one stat per shipped path per tree per call, cached in `cache`. */
447
+ function pruneGuardRefusal(change: any, cache: Map<string, any>, fsImpl: any): string | null {
448
+ const { root, source } = change.pruneGuard;
449
+ const key = `${root}\0${source}`;
450
+ if (!cache.has(key)) {
451
+ const produced = fsImpl.existsSync(source) ? discoverTree({ sourceDir: source, destDir: root, fsImpl }).files.map((file: any) => file.destAbs) : [];
452
+ cache.set(key, { realRoot: fsImpl.realpathSync(root), sameFile: sameFileIndex(produced, fsImpl) });
453
+ }
454
+ const { realRoot, sameFile } = cache.get(key);
455
+ if (!fsImpl.realpathSync(change.target).startsWith(`${realRoot}${path.sep}`)) return `it resolves outside ${root}`;
456
+ const shipped = sameFile(change.target);
457
+ return shipped ? `it is the same file as ${shipped}, which this version ships` : null;
458
+ }
459
+
460
+ /** Delete every remove change, re-checking each file's fingerprint immediately before deletion —
461
+ * refuses (throws) rather than silently deleting a file modified since planTree ran. A removal the
462
+ * runtime prune planned (`pruneGuard`) also re-checks its containment and same-file proof there. */
463
+ function removeTree({ changes = [], fsImpl = fs }: any): { removed: number } {
464
+ let removed = 0;
465
+ const guards = new Map<string, any>();
466
+ for (const change of changes) {
467
+ if (change.operation !== 'remove' || change.kept) continue;
468
+ if (!fsImpl.existsSync(change.target)) continue;
469
+ const refusal = change.pruneGuard ? pruneGuardRefusal(change, guards, fsImpl) : null;
470
+ if (refusal) throw new Error(`Refusing to remove copy-tree resource ${change.relPath ?? change.target}: ${refusal}`);
471
+ const current = sha256(fsImpl.readFileSync(change.target));
472
+ if (current !== change.fingerprint) throw new Error(`Refusing to remove modified copy-tree resource: ${change.relPath}`);
473
+ fsImpl.rmSync(change.target, { force: true });
474
+ pruneEmptyAncestors(path.dirname(change.target), { fsImpl });
475
+ removed += 1;
476
+ }
477
+ return { removed };
478
+ }
479
+
480
+ const ANCHOR_DIRS = new Set(['.agents', '.claude', '.codex', '.copilot', '.doflow', '.gemini', '.github', '.kiro', '.opencode', '.pi']);
481
+
482
+ /** Delete now-empty ancestor directories after a file removal, so a relocated or fully-uninstalled
483
+ * tree doesn't leave a skeleton of empty folders behind. Walks upward only while rmdir succeeds
484
+ * (i.e. the directory is empty, so it holds nothing but what the removal just emptied); stops
485
+ * unconditionally at a directory whose basename is in ANCHOR_DIRS — the dot-folder harness roots,
486
+ * which may hold user content elsewhere. The match is by basename, so a root with another name
487
+ * (`~/.gemini/config`, `~/.pi/agent`, `~/.config/opencode`) is pruned once it is empty; no file is
488
+ * ever lost, since rmdir refuses a folder that holds one. */
489
+ function pruneEmptyAncestors(startDir: string, { fsImpl = fs }: { fsImpl?: any } = {}): void {
490
+ let current = startDir;
491
+ for (;;) {
492
+ const basename = path.basename(current);
493
+ if (!basename || basename === '.' || basename === path.sep || ANCHOR_DIRS.has(basename)) return;
494
+ try { fsImpl.rmdirSync(current); } catch { return; }
495
+ current = path.dirname(current);
496
+ }
497
+ }
498
+
499
+ /** Re-derive ownership resources from what's actually on disk right now, for status/verify.
500
+ * `plannedWrites` lists the destinations the run being verified wrote. An absent source file is
501
+ * not reported in general, because a user may delete one, but an absent file this run wrote means
502
+ * the run lost it (a case-only rename once deleted the file it had just written) and is a conflict. */
503
+ function verifyTree({ sourceDir, destDir, fsImpl = fs, layout, transform, plannedWrites = [] }: any): { ok: boolean, resources: any[], conflicts: string[] } {
504
+ const { files } = discoverTree({ sourceDir, destDir, fsImpl, layout, transform });
505
+ const written = new Set(plannedWrites);
506
+ const resources: any[] = [];
507
+ const conflicts: string[] = [];
508
+ for (const file of files) {
509
+ if (!file.exists) {
510
+ if (written.has(file.destAbs)) conflicts.push(`${file.relPath} was written by this run but is missing`);
511
+ continue;
512
+ }
513
+ const current = sha256(fsImpl.readFileSync(file.destAbs));
514
+ if (current !== file.fingerprint) { conflicts.push(`${file.relPath} does not match source`); continue; }
515
+ resources.push({ relPath: file.relPath, target: file.destAbs, fingerprint: file.fingerprint });
516
+ }
517
+ return { ok: conflicts.length === 0, resources, conflicts };
518
+ }
519
+
520
+ // ---- per-adapter copy-tree glue, shared so codex/claude/gemini don't each carry their own copy ----
521
+
522
+ /** Filter a harness's asset list down to the ones this engine handles. */
523
+ function copyTreeAssets(assets: any): any[] {
524
+ return (assets || []).filter((asset: any) => asset?.renderer === 'copy-tree');
525
+ }
526
+
527
+ /** Recursively sort object keys before serializing, so two logically-equal objects with
528
+ * differently-ordered keys (e.g. after a settings file is merged and re-merged) fingerprint
529
+ * identically instead of spuriously registering as changed. */
530
+ function stableSort(value: any): any {
531
+ if (Array.isArray(value)) return value.map(stableSort);
532
+ if (!value || typeof value !== 'object') return value;
533
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableSort(value[key])]));
534
+ }
535
+
536
+ /** Content fingerprint every adapter uses to track what it owns: sha256 of a string as-is, or of
537
+ * a stably key-sorted JSON serialization of anything else. */
538
+ function fingerprint(value: any): string {
539
+ return sha256(typeof value === 'string' ? value : JSON.stringify(stableSort(value)));
540
+ }
541
+
542
+ /** Read and parse a native JSON file an adapter merges into. Distinguishes "absent" (safe to
543
+ * create) from "present but unparseable" (a conflict to report, never silently overwritten) —
544
+ * every adapter that merges into a native JSON settings/config file needs exactly this. */
545
+ function readJson(file: string, { fsImpl = fs }: { fsImpl?: any } = {}): { exists: boolean, value: any, error: string | null } {
546
+ if (!fsImpl.existsSync(file)) return { exists: false, value: {}, error: null };
547
+ try { return { exists: true, value: JSON.parse(fsImpl.readFileSync(file, 'utf8')), error: null }; }
548
+ catch (error: any) { return { exists: true, value: null, error: `Invalid JSON in ${file}: ${error.message}` }; }
549
+ }
550
+
551
+ /** Resolve an asset's source directory against the repo root, refusing a path that escapes the
552
+ * repository or does not exist. `harnessName` only shapes the thrown error message. */
553
+ function sourceDirFor(asset: any, context: any = {}, fsImpl: any = fs, harnessName: string = 'Adapter'): string {
554
+ if (!asset || typeof asset.source !== 'string') throw new Error(`${harnessName} asset requires a source path`);
555
+ const repoRoot = context.repoRoot ? path.resolve(context.repoRoot) : process.cwd();
556
+ const source = path.resolve(repoRoot, asset.source);
557
+ if (!source.startsWith(`${repoRoot}${path.sep}`) || !fsImpl.existsSync(source)) {
558
+ throw new Error(`${harnessName} asset source is unavailable: ${asset.source}`);
559
+ }
560
+ return source;
561
+ }
562
+
563
+ /** Resolve an asset's native destination directory under the harness's already-resolved config dir. */
564
+ function copyTreeDestDir(configDir: string, asset: any): string {
565
+ return path.join(configDir, asset.nativeDir || '');
566
+ }
567
+
568
+ /** Destination of a `../.doflow` shared-tree asset at the scope root (`<project>/.doflow`, or
569
+ * `$HOME/.doflow` globally), for harnesses whose tree root sits more than one level under the scope
570
+ * root. Returns null for any other nativeDir, so the caller falls back to `copyTreeDestDir`. */
571
+ function sharedTreeDestDir(rootDir: string, nativeDir: any): string | null {
572
+ if (nativeDir !== '../.doflow' && !String(nativeDir || '').startsWith('../.doflow/')) return null;
573
+ const sharedRoot = path.join(rootDir, '.doflow');
574
+ const dest = path.join(rootDir, '.doflow', nativeDir.slice('../.doflow'.length));
575
+ if (dest !== sharedRoot && !dest.startsWith(`${sharedRoot}${path.sep}`)) {
576
+ throw new Error(`shared-tree nativeDir escapes .doflow: ${nativeDir}`);
577
+ }
578
+ return dest;
579
+ }
580
+
581
+ /** The assets that project a tree several harnesses claim at one scope root: the runtime tree, and
582
+ * the guidance tree. A harness whose
583
+ * install predates its own rows for a shared tree (Pi gained guidance rows in v1.15.0) can only
584
+ * recognise the bytes a sibling recorded. */
585
+ const SHARED_TREE_ASSETS = new Set([
586
+ 'scripts.doflow', 'runtime.cli', 'runtime.lib', 'runtime.registry',
587
+ 'guidance.context-layer', 'kiro.guidance-tree',
588
+ ]);
589
+
590
+ /** For each shared-tree target, the fingerprints other harnesses recorded and which harness
591
+ * recorded each, so an update accepts a runtime or guidance tree a sibling wrote (several harnesses
592
+ * claim one `.doflow` file) while bytes no such row recorded are still refused as a hand edit. */
593
+ function ledgerSiblingFingerprints(resources: any, harness: string): Map<string, Map<string, string>> {
594
+ const byTarget = new Map<string, Map<string, string>>();
595
+ for (const resource of resources || []) {
596
+ if (resource.kind !== 'copy-tree-file' || resource.harness === harness || !SHARED_TREE_ASSETS.has(resource.assetId)) continue;
597
+ if (typeof resource.target !== 'string' || typeof resource.fingerprint !== 'string') continue;
598
+ if (!byTarget.has(resource.target)) byTarget.set(resource.target, new Map());
599
+ const byFingerprint = byTarget.get(resource.target)!;
600
+ if (!byFingerprint.has(resource.fingerprint)) byFingerprint.set(resource.fingerprint, resource.harness);
601
+ }
602
+ return byTarget;
603
+ }
604
+
605
+ /** The one-line plan notice for a plan whose copy-tree results replaced sibling-written files, or
606
+ * an empty list when none did. */
607
+ function siblingReplacedNotices(results: any[]): string[] {
608
+ const harnesses = [...new Set(results.flatMap((result) => result.siblingReplaced))].sort();
609
+ if (!harnesses.length) return [];
610
+ return [`replaced shared files written by ${harnesses.join(', ')}; reinstall ${harnesses.length > 1 ? 'those harnesses' : 'that harness'} to restore them`];
611
+ }
612
+
613
+ /** Narrow a harness's flat neutral-resource list to one asset's previously-owned copy-tree files. */
614
+ function ledgerFileResources(resources: any, harness: string, assetId: string): any[] {
615
+ return (resources || [])
616
+ .filter((resource: any) => resource.harness === harness && resource.assetId === assetId && resource.kind === 'copy-tree-file')
617
+ .map((resource: any) => ({ relPath: resource.identity, fingerprint: resource.fingerprint, target: resource.target }));
618
+ }
619
+
620
+ module.exports = { kiroHook, kiroHookCommand, kiroPreToolUseHooks, walkRelFiles, discoverTree, sameFileIndex, planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, sharedTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, resolveLayout, LAYOUTS, resolveTransform, TRANSFORMS, fingerprint, pruneEmptyAncestors, readJson, sourceDirFor };