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

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 (409) 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 +1061 -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 +48 -4
  250. package/src/adapters/antigravity/index.js +631 -0
  251. package/src/adapters/claude/index.js +548 -0
  252. package/src/adapters/claude/mcp.js +94 -0
  253. package/src/adapters/codex/agents.js +208 -0
  254. package/src/adapters/codex/config.js +272 -0
  255. package/src/adapters/codex/hooks.js +203 -0
  256. package/src/adapters/codex/index.js +707 -0
  257. package/src/adapters/codex/mcp.js +271 -0
  258. package/src/adapters/codex/native-projection.js +34 -0
  259. package/src/adapters/copilot/index.js +413 -0
  260. package/src/adapters/copy-tree.js +620 -0
  261. package/src/adapters/gemini/hooks.js +210 -0
  262. package/src/adapters/gemini/index.js +390 -0
  263. package/src/adapters/hook-commands.js +60 -0
  264. package/src/adapters/index.js +106 -0
  265. package/src/adapters/kiro/index.js +310 -0
  266. package/src/adapters/mcp-entries.js +331 -0
  267. package/src/adapters/opencode/index.js +466 -0
  268. package/src/adapters/pi/index.js +279 -0
  269. package/src/adapters/pi/json-members.js +185 -0
  270. package/src/adapters/pi/mcp.js +373 -0
  271. package/src/adapters/resolve-source.js +19 -0
  272. package/src/cli/commands/install.js +218 -0
  273. package/src/cli/commands/list-backups.js +21 -0
  274. package/src/cli/commands/reconcile.js +120 -0
  275. package/src/cli/commands/remove.js +82 -0
  276. package/src/cli/commands/rollback.js +172 -0
  277. package/src/cli/commands/self-update.js +33 -0
  278. package/src/cli/commands/status.js +138 -0
  279. package/src/cli/commands/tools.js +92 -0
  280. package/src/cli/commands/update.js +162 -0
  281. package/src/cli/index.js +568 -0
  282. package/src/cli/runtime-commands.js +281 -0
  283. package/src/cli/shared.js +218 -0
  284. package/src/helper/bash-helper.js +34 -0
  285. package/src/helper/git.js +24 -0
  286. package/src/helper/guardrails.js +49 -0
  287. package/src/helper/harness-paths.js +140 -0
  288. package/src/helper/marker-merge.js +116 -0
  289. package/src/helper/prompt.js +61 -0
  290. package/src/helper/repo-root.js +29 -0
  291. package/src/helper/settings-merge.js +151 -0
  292. package/src/helper/settings-scope.js +33 -0
  293. package/src/helper/toml.js +244 -0
  294. package/src/install/backup-set.js +149 -0
  295. package/src/install/backup.js +724 -0
  296. package/src/install/context.js +58 -0
  297. package/src/install/manifest.js +145 -0
  298. package/src/install/mcp.js +254 -0
  299. package/src/install/paths.js +84 -0
  300. package/src/install/targets.js +90 -0
  301. package/src/install/tool-lifecycle.js +151 -0
  302. package/src/lifecycle/bash-availability.js +25 -0
  303. package/src/lifecycle/index.js +812 -0
  304. package/src/lifecycle/mcp-index.js +28 -0
  305. package/src/lifecycle/policies.js +73 -0
  306. package/src/lifecycle/view.js +175 -0
  307. package/src/registry/index.js +611 -0
  308. package/src/release/build-package.js +91 -0
  309. package/src/release/changelog-section.js +70 -0
  310. package/src/release/sync-plugin-versions.js +66 -0
  311. package/src/runtime/advice/cli.js +283 -0
  312. package/src/runtime/advice/client.js +324 -0
  313. package/src/runtime/advice/config.js +189 -0
  314. package/src/runtime/advice/doctor.js +169 -0
  315. package/src/runtime/advice/endpoint-guard.js +134 -0
  316. package/src/runtime/advice/kinds.js +154 -0
  317. package/src/runtime/advice/providers/ollama-systemone.js +209 -0
  318. package/src/runtime/advice/record.js +166 -0
  319. package/src/runtime/advice/registry.js +170 -0
  320. package/src/runtime/advice/strength.js +66 -0
  321. package/src/runtime/brief-tools.js +342 -0
  322. package/src/runtime/c4-project.js +371 -0
  323. package/src/runtime/capability-router.js +484 -0
  324. package/src/runtime/checkouts.js +170 -0
  325. package/src/runtime/claims.js +468 -0
  326. package/src/runtime/cli-result.js +55 -0
  327. package/src/runtime/cli.js +678 -0
  328. package/src/runtime/command-detect.js +516 -0
  329. package/src/runtime/context-pack.js +492 -0
  330. package/src/runtime/decision-register.js +493 -0
  331. package/src/runtime/evidence-ledger.js +338 -0
  332. package/src/runtime/failure/capture.js +272 -0
  333. package/src/runtime/failure/classifier.js +68 -0
  334. package/src/runtime/failure/cli.js +178 -0
  335. package/src/runtime/failure/home.js +64 -0
  336. package/src/runtime/failure/store.js +226 -0
  337. package/src/runtime/feature-resolve.js +79 -0
  338. package/src/runtime/freshness.js +177 -0
  339. package/src/runtime/health.js +863 -0
  340. package/src/runtime/history-compactor.js +237 -0
  341. package/src/runtime/implementation-gate.js +321 -0
  342. package/src/runtime/inventory/coverage.js +218 -0
  343. package/src/runtime/inventory/identity.js +65 -0
  344. package/src/runtime/inventory/index.js +705 -0
  345. package/src/runtime/inventory/precedence.js +134 -0
  346. package/src/runtime/inventory/read-scopes.js +131 -0
  347. package/src/runtime/inventory/siblings.js +192 -0
  348. package/src/runtime/knowledge/chunker.js +124 -0
  349. package/src/runtime/knowledge/hybrid.js +149 -0
  350. package/src/runtime/knowledge/index-store.js +145 -0
  351. package/src/runtime/knowledge/retrieval.js +194 -0
  352. package/src/runtime/leak-scan.js +185 -0
  353. package/src/runtime/lifecycle/cli.js +276 -0
  354. package/src/runtime/lifecycle/event-store.js +340 -0
  355. package/src/runtime/lifecycle/fold.js +353 -0
  356. package/src/runtime/lifecycle/followup.js +432 -0
  357. package/src/runtime/lifecycle/goal.js +249 -0
  358. package/src/runtime/lifecycle/intent-writer.js +86 -0
  359. package/src/runtime/lifecycle/overview.js +292 -0
  360. package/src/runtime/lifecycle/release.js +241 -0
  361. package/src/runtime/lifecycle/report-store.js +221 -0
  362. package/src/runtime/lifecycle/retention.js +212 -0
  363. package/src/runtime/lifecycle/root.js +71 -0
  364. package/src/runtime/lifecycle/status.js +353 -0
  365. package/src/runtime/lifecycle/store-upkeep.js +393 -0
  366. package/src/runtime/locator-resolve.js +158 -0
  367. package/src/runtime/mask.js +210 -0
  368. package/src/runtime/mcp-consultations.js +412 -0
  369. package/src/runtime/mcp-reach.js +157 -0
  370. package/src/runtime/model-router.js +153 -0
  371. package/src/runtime/outcome.js +467 -0
  372. package/src/runtime/reach.js +148 -0
  373. package/src/runtime/readiness-record.js +150 -0
  374. package/src/runtime/readiness.js +435 -0
  375. package/src/runtime/recovery.js +282 -0
  376. package/src/runtime/research-request.js +231 -0
  377. package/src/runtime/retrieval-plan.js +578 -0
  378. package/src/runtime/scaffold/artifacts.js +217 -0
  379. package/src/runtime/scaffold/fingerprint.js +56 -0
  380. package/src/runtime/scaffold/generate.js +911 -0
  381. package/src/runtime/scaffold/languages.js +245 -0
  382. package/src/runtime/storage/cli.js +64 -0
  383. package/src/runtime/storage/engine.js +249 -0
  384. package/src/runtime/storage/registry.js +130 -0
  385. package/src/runtime/storage/roots.js +63 -0
  386. package/src/runtime/storage/rules.js +282 -0
  387. package/src/runtime/storage/upkeep.js +243 -0
  388. package/src/runtime/storage/work-status.js +326 -0
  389. package/src/runtime/task-attempts.js +415 -0
  390. package/src/runtime/task-classifier.js +550 -0
  391. package/src/runtime/task-scope.js +130 -0
  392. package/src/runtime/task-state.js +176 -0
  393. package/src/runtime/trace/indicators.js +235 -0
  394. package/src/runtime/trace/ledger.js +626 -0
  395. package/src/runtime/trace/render.js +76 -0
  396. package/src/runtime/trace/views.js +459 -0
  397. package/src/runtime/verification/contract-runner.js +341 -0
  398. package/src/runtime/verification/engine.js +1461 -0
  399. package/src/runtime/verification/registry.js +130 -0
  400. package/src/runtime/verification/scope-bound.js +418 -0
  401. package/src/runtime/verification/test-selection.js +172 -0
  402. package/src/runtime/workflow-engine.js +588 -0
  403. package/src/runtime/workflow-orchestrator.js +953 -0
  404. package/src/runtime/worktree.js +340 -0
  405. package/src/state/index.js +193 -0
  406. package/src/state/lockfile.js +136 -0
  407. package/src/state/run-lock.js +408 -0
  408. package/types/node-commonjs.d.ts +68 -0
  409. package/types/runtime.d.ts +91 -0
@@ -0,0 +1,210 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ // Gemini hook planning. Unlike Codex (which owns a standalone hooks.json file), Gemini's hooks
6
+ // live as a "hooks" key inside settings.json — a file DoFlow does not fully own (it commonly
7
+ // pre-exists with unrelated content: mcpServers, ui, telemetry, model, etc.). planGeminiHooks
8
+ // therefore returns a key-scoped merge plan, never a full-file replace.
9
+ //
10
+ // NOTE: validateHooksConfig below intentionally duplicates codex-hooks.js's shape-checking logic
11
+ // (event -> array of {matcher, hooks:[{type,command,...}]} groups) rather than sharing it, since
12
+ // unifying it would mean introducing a new shared module beyond this feature's planned scope
13
+ // (agent-docs/doflow/013-codex-gemini-hooks-parity/plan.md's component list). Worth factoring out
14
+ // if a third harness with the same shape is ever added.
15
+ const fs = require('node:fs');
16
+ const path = require('node:path');
17
+ const { verifyHookCommands } = require('../hook-commands.js');
18
+ const { mergeHooks } = require('../../helper/settings-merge.js');
19
+
20
+ const SUPPORTED_EVENTS = new Set([
21
+ 'SessionStart', 'SessionEnd', 'BeforeAgent', 'AfterAgent', 'BeforeModel', 'AfterModel',
22
+ 'BeforeToolSelection', 'BeforeTool', 'AfterTool', 'PreCompress', 'Notification',
23
+ ]);
24
+
25
+ // Claude guardrail event -> Gemini event, only where a real Gemini event has matching semantics
26
+ // (source: geminicli.com/docs/hooks/reference.md). UserPromptSubmit,
27
+ // Stop, SubagentStart/Stop, and ConfigChange have no correct Gemini equivalent and are
28
+ // intentionally omitted rather than approximated onto BeforeAgent/AfterAgent (design.md §7/§8 —
29
+ // no false parity).
30
+ const CLAUDE_TO_GEMINI_EVENT = {
31
+ SessionStart: 'SessionStart',
32
+ SessionEnd: 'SessionEnd',
33
+ PreToolUse: 'BeforeTool',
34
+ PostToolUse: 'AfterTool',
35
+ PreCompact: 'PreCompress',
36
+ };
37
+
38
+ function validationError(errors , message ) { errors.push(message); }
39
+
40
+ function validateHooksConfig(config ) {
41
+ const errors = [];
42
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
43
+ return { ok: false, errors: ['Hook configuration must be an object'], events: [] };
44
+ }
45
+ if (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) {
46
+ return { ok: false, errors: ['Hook configuration requires a hooks object'], events: [] };
47
+ }
48
+ for (const [event, groups] of Object.entries(config.hooks)) {
49
+ if (!SUPPORTED_EVENTS.has(event)) {
50
+ validationError(errors, `Unsupported Gemini hook event '${event}'`);
51
+ continue;
52
+ }
53
+ if (!Array.isArray(groups) || groups.length === 0) {
54
+ validationError(errors, `Hook event '${event}' requires a non-empty array of matcher groups`);
55
+ continue;
56
+ }
57
+ groups.forEach((group , groupIndex ) => {
58
+ if (!group || typeof group !== 'object' || Array.isArray(group)) {
59
+ validationError(errors, `Hook event '${event}' group ${groupIndex} must be an object`);
60
+ return;
61
+ }
62
+ if (group.matcher !== undefined && typeof group.matcher !== 'string') {
63
+ validationError(errors, `Hook event '${event}' group ${groupIndex} matcher must be a string`);
64
+ }
65
+ if (!Array.isArray(group.hooks) || group.hooks.length === 0) {
66
+ validationError(errors, `Hook event '${event}' group ${groupIndex} requires a non-empty hooks array`);
67
+ return;
68
+ }
69
+ group.hooks.forEach((handler , handlerIndex ) => {
70
+ const label = `Hook event '${event}' group ${groupIndex} handler ${handlerIndex}`;
71
+ if (!handler || typeof handler !== 'object' || Array.isArray(handler)) {
72
+ validationError(errors, `${label} must be an object`); return;
73
+ }
74
+ if (handler.type !== 'command') validationError(errors, `${label} must have type 'command'`);
75
+ if (typeof handler.command !== 'string' || !handler.command.trim()) {
76
+ validationError(errors, `${label} requires a non-empty command string`);
77
+ }
78
+ if (handler.timeout !== undefined && typeof handler.timeout !== 'number') {
79
+ validationError(errors, `${label} timeout must be a number`);
80
+ }
81
+ });
82
+ });
83
+ }
84
+ return { ok: errors.length === 0, errors, events: Object.keys(config.hooks) };
85
+ }
86
+
87
+ /** Maps Claude guardrail intent, never fabricating a Gemini equivalent for unavailable events. */
88
+ function classifyClaudeGuardrails(claudeHooks = {}) {
89
+ const supported = [];
90
+ const unsupported = [];
91
+ for (const [event, groups] of Object.entries(claudeHooks || {})) {
92
+ const targetEvent = CLAUDE_TO_GEMINI_EVENT[event] || null;
93
+ const item = { sourceEvent: event, targetEvent, groups: Array.isArray(groups) ? groups.length : 0 };
94
+ if (targetEvent) supported.push(item);
95
+ else unsupported.push({ ...item, reason: `Gemini has no equivalent lifecycle event for Claude's '${event}'` });
96
+ }
97
+ return { supported, unsupported };
98
+ }
99
+
100
+ /**
101
+ * Plan a key-scoped merge of a "hooks" config into an existing (or absent) settings.json, never
102
+ * touching any other key. Mirrors src/mcp.js's readJsonOrThrow discipline: a settings.json that
103
+ * exists but fails to parse is a hard error, not silently treated as empty (this path merges
104
+ * into content DoFlow does not own — a malformed-file work-around here risks losing it).
105
+ */
106
+ function planGeminiHooks({ config, sourceFile, sourceHooksDir, settingsFile, destinationHooksDir: declaredHooksDir, trusted = false, fsImpl = fs } = {}) {
107
+ let desired = config;
108
+ try {
109
+ if (!desired && sourceFile) desired = JSON.parse(fsImpl.readFileSync(sourceFile, 'utf8'));
110
+ } catch (error ) {
111
+ return { ok: false, status: 'malformed', changes: [], errors: [`Could not parse ${sourceFile}: ${error.message}`] };
112
+ }
113
+ if (!settingsFile) return { ok: false, status: 'invalid-request', changes: [], errors: ['settingsFile is required'] };
114
+ const schema = validateHooksConfig(desired);
115
+ const commands = verifyHookCommands(desired || {}, { scriptsDir: sourceHooksDir, trusted, fsImpl });
116
+ const errors = [...schema.errors, ...commands.checks.filter((check ) => !check.ok).map((check ) => `${check.event}: ${check.reason}${check.script ? ` (${check.script})` : ''}`)];
117
+
118
+ let existing = {};
119
+ if (fsImpl.existsSync(settingsFile)) {
120
+ try { existing = JSON.parse(fsImpl.readFileSync(settingsFile, 'utf8')); }
121
+ catch (error ) { return { ok: false, status: 'malformed', changes: [], errors: [`Refusing to touch malformed ${settingsFile}: ${error.message}`] }; }
122
+ }
123
+ if (errors.length) return { ok: false, status: 'invalid-request', settingsFile, existing, changes: [], errors, commands };
124
+
125
+ const mergedHooks = existing.hooks ? mergeHooks(existing.hooks, desired.hooks) : desired.hooks;
126
+ const merged = { ...existing, hooks: mergedHooks };
127
+ const changed = JSON.stringify(existing.hooks) !== JSON.stringify(mergedHooks);
128
+ // Preferred: the declared hooks directory (harnesses.json "paths".hooksDirectory); the derived
129
+ // sibling-of-settings fallback stays for direct callers passing raw inputs, and resolves to the
130
+ // same location.
131
+ const destinationHooksDir = declaredHooksDir || path.join(path.dirname(settingsFile), 'hooks');
132
+ // The scripts deployGeminiHooks copies beside settings.json, so a backup of this change holds them too.
133
+ const shipped = sourceHooksDir && fsImpl.existsSync(sourceHooksDir)
134
+ ? fsImpl.readdirSync(sourceHooksDir).filter((name ) => !name.endsWith('.json') && fsImpl.statSync(path.join(sourceHooksDir, name)).isFile())
135
+ : [];
136
+ const companionTargets = [...new Set([
137
+ ...commands.checks.map((check ) => path.join(destinationHooksDir, check.script)),
138
+ ...shipped.map((name ) => path.join(destinationHooksDir, name)),
139
+ ])].sort();
140
+ return { ok: true, status: changed ? 'change' : 'unchanged', settingsFile, existing, merged,
141
+ changes: changed ? [{ type: fsImpl.existsSync(settingsFile) ? 'update' : 'create', file: settingsFile, key: 'hooks' }] : [],
142
+ errors: [], commands, trust: commands.trust, scriptsDir: sourceHooksDir, destinationHooksDir, companionTargets };
143
+ }
144
+
145
+ function deployGeminiHooks(plan , { dryRun = false, fsImpl = fs } = {}) {
146
+ if (!plan.ok) return { ...plan, applied: false };
147
+ if (dryRun || plan.status === 'unchanged') return { ...plan, applied: false };
148
+ fsImpl.mkdirSync(path.dirname(plan.settingsFile), { recursive: true });
149
+ const content = `${JSON.stringify(plan.merged, null, 2)}\n`;
150
+ const temporary = path.join(path.dirname(plan.settingsFile), `.${path.basename(plan.settingsFile)}.${process.pid}.${Date.now()}.tmp`);
151
+ try {
152
+ fsImpl.writeFileSync(temporary, content, { flag: 'wx' });
153
+ fsImpl.renameSync(temporary, plan.settingsFile);
154
+ } finally {
155
+ if (fsImpl.existsSync(temporary)) fsImpl.unlinkSync(temporary);
156
+ }
157
+ const deployed = new Set ();
158
+ for (const check of plan.commands.checks) {
159
+ const target = path.join(plan.destinationHooksDir, check.script);
160
+ fsImpl.mkdirSync(path.dirname(target), { recursive: true });
161
+ fsImpl.copyFileSync(check.file, target);
162
+ fsImpl.chmodSync(target, fsImpl.statSync(check.file).mode & 0o777);
163
+ deployed.add(check.script);
164
+ }
165
+ if (plan.scriptsDir && fsImpl.existsSync(plan.scriptsDir)) {
166
+ for (const name of fsImpl.readdirSync(plan.scriptsDir)) {
167
+ if (deployed.has(name) || name.endsWith('.json')) continue;
168
+ const source = path.join(plan.scriptsDir, name);
169
+ if (!fsImpl.statSync(source).isFile()) continue;
170
+ const target = path.join(plan.destinationHooksDir, name);
171
+ fsImpl.mkdirSync(path.dirname(target), { recursive: true });
172
+ fsImpl.copyFileSync(source, target);
173
+ fsImpl.chmodSync(target, fsImpl.statSync(source).mode & 0o777);
174
+ }
175
+ }
176
+ return { ...plan, applied: true };
177
+ }
178
+
179
+ /**
180
+ * Plan stripping just the "hooks" key back out of settings.json — mirrors this codebase's
181
+ * existing GEMINI.md-instructions removal (structural presence check, not a ledger fingerprint
182
+ * cross-check): if the key isn't present, there's nothing to do; otherwise propose removing only
183
+ * that key, leaving every other key in the shared file untouched (FR-002).
184
+ */
185
+ function planRemoveGeminiHooks({ settingsFile, fsImpl = fs } = {}) {
186
+ if (!settingsFile) return { ok: false, status: 'invalid-request', changes: [], errors: ['settingsFile is required'] };
187
+ if (!fsImpl.existsSync(settingsFile)) return { ok: true, status: 'unchanged', settingsFile, changes: [] };
188
+ let existing;
189
+ try { existing = JSON.parse(fsImpl.readFileSync(settingsFile, 'utf8')); }
190
+ catch (error ) { return { ok: false, status: 'malformed', changes: [], errors: [`Could not parse ${settingsFile}: ${error.message}`] }; }
191
+ if (existing.hooks === undefined) return { ok: true, status: 'unchanged', settingsFile, changes: [] };
192
+ const { hooks: _dropped, ...merged } = existing;
193
+ return { ok: true, status: 'change', settingsFile, merged, changes: [{ type: 'update', file: settingsFile, key: 'hooks', operation: 'remove' }] };
194
+ }
195
+
196
+ function deployRemoveGeminiHooks(plan , { dryRun = false, fsImpl = fs } = {}) {
197
+ if (!plan.ok || plan.status !== 'change' || dryRun) return { ...plan, applied: false };
198
+ fsImpl.mkdirSync(path.dirname(plan.settingsFile), { recursive: true });
199
+ const content = `${JSON.stringify(plan.merged, null, 2)}\n`;
200
+ const temporary = path.join(path.dirname(plan.settingsFile), `.${path.basename(plan.settingsFile)}.${process.pid}.${Date.now()}.tmp`);
201
+ try {
202
+ fsImpl.writeFileSync(temporary, content, { flag: 'wx' });
203
+ fsImpl.renameSync(temporary, plan.settingsFile);
204
+ } finally {
205
+ if (fsImpl.existsSync(temporary)) fsImpl.unlinkSync(temporary);
206
+ }
207
+ return { ...plan, applied: true };
208
+ }
209
+
210
+ module.exports = { SUPPORTED_EVENTS, validateHooksConfig, classifyClaudeGuardrails, verifyHookCommands, planGeminiHooks, deployGeminiHooks, planRemoveGeminiHooks, deployRemoveGeminiHooks };
@@ -0,0 +1,390 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ // Gemini CLI adapter. Native settings are JSON and Gemini's extension installation is a host
6
+ // workflow, so this adapter deliberately plans only safe instruction projection while exposing
7
+ // settings/MCP/extensions as first-class native-surface results for the lifecycle UI.
8
+ const fs = require('node:fs');
9
+ const path = require('node:path');
10
+ const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, fingerprint, readJson, sourceDirFor, resolveTransform } = require('../copy-tree.js');
11
+ const { declaredHarnessPaths, resolveHarnessPaths } = require('../../helper/harness-paths.js');
12
+ const { planGeminiHooks, deployGeminiHooks, planRemoveGeminiHooks, deployRemoveGeminiHooks } = require('./hooks.js');
13
+
14
+ const MARKER_START = '<!-- doflow:start -->';
15
+ const MARKER_END = '<!-- doflow:end -->';
16
+ /**
17
+ * Residue (not a path fact): the one non-path native fact this harness still reads from its own
18
+ * fixture file — how extensions are installed ("native-host-workflow"), reported verbatim in plan
19
+ * surfaces. The file-name facts that used to live beside it (instructionFile, settingsFile) are now
20
+ * declared in core/registry/harnesses.json "paths" and resolve below like every other harness.
21
+ */
22
+ const DEFAULTS_FILE = path.resolve(__dirname, '../../../core/harnesses/gemini/settings/adapter-defaults.json');
23
+ const HARNESS = 'gemini';
24
+
25
+ function defaults({ fsImpl = fs } = {}) { return JSON.parse(fsImpl.readFileSync(DEFAULTS_FILE, 'utf8')); }
26
+
27
+ /** Pure string helper (no path facts): replace only the span between DoFlow's markers in an
28
+ * existing GEMINI.md body. An existing file with no DoFlow section is refused rather than appended
29
+ * to — GEMINI.md is shared with the user and other tools. */
30
+ function managedInstruction(existing , rendered ) {
31
+ if (existing === null) return { ok: true, operation: 'create', content: rendered };
32
+ const start = existing.indexOf(MARKER_START);
33
+ const end = existing.indexOf(MARKER_END);
34
+ if (start === -1 && end === -1) return { ok: false, conflict: 'GEMINI.md exists without a DoFlow managed section' };
35
+ if (start === -1 || end === -1 || end < start) return { ok: false, conflict: 'GEMINI.md has malformed DoFlow managed-section markers' };
36
+ const content = `${existing.slice(0, start)}${rendered}${existing.slice(end + MARKER_END.length).replace(/^\n?/, '')}`;
37
+ return { ok: true, operation: content === existing ? 'none' : 'merge', content };
38
+ }
39
+
40
+ /** Server names in one settings read, or null when the file is absent or unreadable. */
41
+ function settingsServerNames(read ) {
42
+ if (!read.exists || read.error || !read.value || typeof read.value !== 'object') return null;
43
+ const servers = read.value.mcpServers;
44
+ return Object.keys(servers && typeof servers === 'object' ? servers : {});
45
+ }
46
+
47
+ /**
48
+ * The server names DoFlow can see for Gemini at a scope (`mcpConfigured`): the scope's settings, and
49
+ * at project scope the user's `~/.gemini/settings.json` too, since Gemini merges both. `null` while
50
+ * neither can be read. Gemini also merges system settings and extension servers, which DoFlow does
51
+ * not read, so the view is never complete (`mcpComplete: false`) and absence is never proof.
52
+ */
53
+ function configuredGeminiMcp({ scope, settings, homeDir, fsImpl } ) {
54
+ const own = settingsServerNames(settings);
55
+ const user = scope === 'project' && homeDir ? settingsServerNames(readJson(path.join(homeDir, '.gemini', 'settings.json'), { fsImpl })) : null;
56
+ const mcpConfigured = own || user ? [...new Set([...(own || []), ...(user || [])])].sort() : null;
57
+ return { mcpConfigured, mcpComplete: false };
58
+ }
59
+
60
+ /**
61
+ * Native path facts live in core/registry/harnesses.json under this harness's "paths" section and
62
+ * resolve through the shared harness-paths resolver: project-scope config follows the Antigravity
63
+ * customization convention (.agents/) while user-scope config lives under ~/.gemini/config/.
64
+ * createGeminiAdapter({ declaredPaths }) is the injection point buildAdapterRegistry() uses;
65
+ * module-level exports delegate to a default-configured instance so direct callers keep their
66
+ * historical shape.
67
+ */
68
+ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] } = {}) {
69
+ function nativePaths({ scope, scopeRoot, homeDir, fsImpl = fs } ) {
70
+ const resolved = resolveHarnessPaths(declaredPaths, { scope, scopeRoot, homeDir });
71
+ // Non-path native fact carried alongside the resolved surfaces (see DEFAULTS_FILE residue note).
72
+ return { ...resolved, extensionMode: defaults({ fsImpl }).extensionInstall };
73
+ }
74
+
75
+ function discover({ scope, scopeRoot, context = {}, fsImpl = fs } ) {
76
+ const paths = nativePaths({ scope, scopeRoot, homeDir: context.homeDir, fsImpl });
77
+ const instruction = fsImpl.existsSync(paths.instruction) ? fsImpl.readFileSync(paths.instruction, 'utf8') : null;
78
+ // Gemini takes no servers from DoFlow's MCP catalog.
79
+ const settings = readJson(paths.settings, { fsImpl });
80
+ const { mcpConfigured, mcpComplete } = configuredGeminiMcp({ scope, settings, homeDir: context.homeDir, fsImpl });
81
+ return { paths, instruction, settings, mcpOwned: [], mcpConfigured, mcpComplete };
82
+ }
83
+
84
+ function render({ content = '' } = {}) {
85
+ const body = String(content).trimEnd();
86
+ return `${MARKER_START}\n${body}\n${MARKER_END}\n`;
87
+ }
88
+
89
+ /**
90
+ * Rewrite the pointer's guidance import so it resolves from wherever the instruction file
91
+ * actually lands. The shared pointer source carries one hardcoded `../`, which is correct only
92
+ * when the instruction file sits one level below the install root — true for Gemini's global
93
+ * scope (`.gemini/GEMINI.md`) and false for its project scope, where Gemini reads `GEMINI.md`
94
+ * from the workspace root itself. A static prefix cannot satisfy both, because the depth varies
95
+ * by scope rather than by harness, so it is computed here instead. Left unrewritten, the import
96
+ * pointed above the install root and the whole guidance chain silently failed to load.
97
+ *
98
+ * Logic residue: the relative prefix depends on the DISTANCE between two declared surfaces
99
+ * (instruction vs. the guidance tree), not on any single declared path — expressible only by
100
+ * joining and diffing, which is computation, not declaration.
101
+ */
102
+ function geminiDestDir(paths , asset ) {
103
+ if (asset.nativeDir && asset.nativeDir.startsWith('../')) {
104
+ const dest = path.join(paths.root, asset.nativeDir.slice(3));
105
+ const rel = path.relative(paths.root, dest);
106
+ if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
107
+ throw new Error(`gemini nativeDir escapes the scope root: ${asset.nativeDir}`);
108
+ }
109
+ return dest;
110
+ }
111
+ return copyTreeDestDir(paths.configDir, asset);
112
+ }
113
+
114
+ function rewriteGuidanceImport(content , assets , paths ) {
115
+ const guidance = assets.find((item ) => item.id === 'guidance.context-layer');
116
+ if (!guidance?.nativeDir || !paths?.instruction || !paths?.configDir) return content;
117
+ const guidanceDir = geminiDestDir(paths, guidance);
118
+ const rel = path.relative(path.dirname(paths.instruction), guidanceDir).split(path.sep).join('/');
119
+ if (!rel) return content;
120
+ // `./` is added explicitly for a same-directory descent: the bare form starts with the dot of
121
+ // `.doflow`, which reads as an extension rather than a path segment.
122
+ const prefix = rel.startsWith('../') ? rel : `./${rel}`;
123
+ return content.replace(/@\S*?(DOFLOW_CORE\.md)/g, `@${prefix}/$1`);
124
+ }
125
+
126
+ function instructionContent({ assets, context, paths, fsImpl = fs } ) {
127
+ if (context.instructionContent !== undefined) return String(context.instructionContent);
128
+ const asset = assets.find((item ) => item.id === 'guidance.core');
129
+ if (!asset || !context.repoRoot) return null;
130
+ const source = path.resolve(context.repoRoot, asset.source);
131
+ if (!fsImpl.existsSync(source)) return null;
132
+ return rewriteGuidanceImport(fsImpl.readFileSync(source, 'utf8'), assets, paths);
133
+ }
134
+
135
+ function policyStatuses(policies = []) {
136
+ return policies.map((policy ) => ({ id: policy.id, status: 'unavailable', fallback: 'guidance', reason: 'Gemini policy automation is not rendered by this adapter' }));
137
+ }
138
+
139
+ // ---- copy-tree assets (rules, skills, agents, modes, references) ----
140
+
141
+ function planCopyTreeAssets({ assets, scope, scopeRoot, context, ledger, removing, fsImpl = fs } ) {
142
+ const paths = nativePaths({ scope, scopeRoot, homeDir: context.homeDir, fsImpl });
143
+ const changes = [];
144
+ const conflicts = [];
145
+ const treeResults = [];
146
+ const notices = [];
147
+ for (const asset of copyTreeAssets(assets)) {
148
+ const destDir = geminiDestDir(paths, asset);
149
+ const sourceDir = sourceDirFor(asset, context, fsImpl, 'Gemini');
150
+ const previousResources = ledgerFileResources(ledger?.resources, HARNESS, asset.id);
151
+ const result = planTree({ sourceDir, destDir, previousResources, siblingFingerprints: ledgerSiblingFingerprints(ledger?.resources, HARNESS), operation: removing ? 'remove' : 'apply', fsImpl, layout: asset.layout, transform: asset.transform, keepModified: !removing,
152
+ // Was `force: context?.force`, ungated. Gemini was one of only two adapters forwarding force
153
+ // at all, so it looked like the reference implementation — but it handed force to the remove
154
+ // path too, where copy-tree deliberately stays strict: force heals drift on apply, and a
155
+ // hand-edited file is never deleted, forced or not (codex/index.ts states the rule in full).
156
+ // Found by the guard written for the six adapters that forwarded nothing.
157
+ force: !removing && context?.force === true });
158
+ treeResults.push(result);
159
+ conflicts.push(...result.conflicts.map((reason ) => `${asset.id}: ${reason}`));
160
+ // A hand-edited file at a path DoFlow no longer writes (the agents folder layout) is the
161
+ // user's: it stays, only its ownership row is released.
162
+ notices.push(...result.kept.map((item ) => `kept hand-edited ${path.relative(paths.root, item.target)}; DoFlow no longer manages it`));
163
+ for (const change of result.changes) {
164
+ changes.push({
165
+ assetId: asset.id, target: change.target, source: change.source, operation: change.operation,
166
+ ownershipIdentity: `doflow:gemini:copy-tree:${asset.id}:${change.relPath}`,
167
+ kind: 'copy-tree-file', identity: change.relPath,
168
+ afterFingerprint: change.fingerprint, fingerprint: change.fingerprint, sourceVersion: 'registry-v1',
169
+ transformName: asset.transform || null,
170
+ ...(change.kept ? { retained: true, retainedFor: [] } : {}),
171
+ projection: { renderer: 'copy-tree' },
172
+ });
173
+ }
174
+ }
175
+ return { changes, conflicts, notices: [...siblingReplacedNotices(treeResults), ...notices] };
176
+ }
177
+
178
+ function applyCopyTreeAssets(changes , { fsImpl = fs } = {}) {
179
+ let applied = 0;
180
+ const grouped = new Map();
181
+ for (const change of changes) {
182
+ if (change.projection?.renderer !== 'copy-tree' || change.operation === 'remove') continue;
183
+ const key = change.transformName || null;
184
+ if (!grouped.has(key)) grouped.set(key, []);
185
+ grouped.get(key).push({ relPath: change.identity, target: change.target, source: change.source, operation: change.operation, fingerprint: change.fingerprint });
186
+ }
187
+ for (const [transformName, treeChanges] of grouped) {
188
+ applied += applyTree({ changes: treeChanges, fsImpl, transform: resolveTransform(transformName) }).applied;
189
+ }
190
+ return applied;
191
+ }
192
+
193
+ function removeCopyTreeAssets(changes , { fsImpl = fs } = {}) {
194
+ const treeChanges = changes.filter((change ) => change.projection?.renderer === 'copy-tree' && change.operation === 'remove' && !change.retained)
195
+ .map((change ) => ({ relPath: change.identity, target: change.target, operation: 'remove', fingerprint: change.fingerprint, pruneGuard: change.pruneGuard }));
196
+ return removeTree({ changes: treeChanges, fsImpl }).removed;
197
+ }
198
+
199
+ function verifyCopyTreeAssets({ assets, scope, scopeRoot, context, fsImpl = fs } ) {
200
+ const paths = nativePaths({ scope, scopeRoot, homeDir: context.homeDir, fsImpl });
201
+ const statuses = [];
202
+ const resources = [];
203
+ const conflicts = [];
204
+ for (const asset of copyTreeAssets(assets)) {
205
+ const destDir = geminiDestDir(paths, asset);
206
+ const sourceDir = sourceDirFor(asset, context, fsImpl, 'Gemini');
207
+ const result = verifyTree({ sourceDir, destDir, fsImpl, layout: asset.layout, transform: asset.transform, plannedWrites: context?.plannedWrites });
208
+ conflicts.push(...result.conflicts.map((reason ) => `${asset.id}: ${reason}`));
209
+ for (const resource of result.resources) {
210
+ resources.push({
211
+ assetId: asset.id, target: resource.target, ownershipIdentity: `doflow:gemini:copy-tree:${asset.id}:${resource.relPath}`,
212
+ kind: 'copy-tree-file', identity: resource.relPath,
213
+ fingerprint: resource.fingerprint, sourceVersion: 'registry-v1',
214
+ projection: { renderer: 'copy-tree' },
215
+ });
216
+ }
217
+ statuses.push({ assetId: asset.id, capability: asset.capability, status: result.ok ? 'managed' : 'conflict', target: destDir });
218
+ }
219
+ return { statuses, resources, conflicts };
220
+ }
221
+
222
+ // ---- shared adapter contract ----
223
+
224
+ // Hooks are a pseudo-component, not a registry-declared asset (like Codex's own hooks
225
+ // integration) — the lifecycle layer's normalizeChange() requires every change's assetId to
226
+ // match a real registered asset, so this piggybacks on guidance.core's id the same way Codex's
227
+ // assetIdFor(options, 'hooks') does, rather than inventing an unregistered 'hooks' id.
228
+ function hooksAssetId(assets ) {
229
+ return assets.find((asset ) => asset.id === 'guidance.core')?.id ?? assets[0]?.id;
230
+ }
231
+
232
+ /** Plan the hooks pseudo-component's single change (or none), mirroring the {changes, conflicts}
233
+ * shape planCopyTreeAssets already returns so plan() can fold it in the same way. The hook
234
+ * scripts directory beside settings.json is declared (paths.hooksDirectory); the merge/remove
235
+ * planning itself lives in ./hooks.ts. */
236
+ function planHooksChange({ removing, found, context, assets, fsImpl } ) {
237
+ if (found.settings.error) return { changes: [], conflicts: [] };
238
+ const hooksPlan = removing
239
+ ? planRemoveGeminiHooks({ settingsFile: found.paths.settings, fsImpl })
240
+ : (context.geminiHooksSourceFile
241
+ ? planGeminiHooks({ sourceFile: context.geminiHooksSourceFile, sourceHooksDir: context.geminiHooksSourceDir,
242
+ settingsFile: found.paths.settings, destinationHooksDir: found.paths.hooksDirectory, trusted: context.hooksTrusted, fsImpl })
243
+ : null);
244
+ if (hooksPlan && !hooksPlan.ok) return { changes: [], conflicts: hooksPlan.errors || [] };
245
+ if (hooksPlan?.status !== 'change') return { changes: [], conflicts: [] };
246
+ return { changes: [{ assetId: hooksAssetId(assets), target: hooksPlan.settingsFile, operation: removing ? 'remove' : (hooksPlan.changes[0]?.type ?? 'update'),
247
+ ownershipIdentity: 'gemini:hooks', nativeComponent: 'hooks', nativePlan: hooksPlan, projection: { renderer: 'gemini-hooks' },
248
+ companionTargets: removing ? [] : (hooksPlan.companionTargets ?? []) }], conflicts: [] };
249
+ }
250
+
251
+ function plan({ scope, scopeRoot, assets = [], mcp = [], discovery, context = {}, ledger, fsImpl = fs } ) {
252
+ const found = discovery || discover({ scope, scopeRoot, context, fsImpl });
253
+ const changes = []; const conflicts = [];
254
+ const removing = context.operation === 'remove';
255
+ if (removing) {
256
+ // Removal only ever targets a section this adapter itself owns — a foreign or absent
257
+ // GEMINI.md yields no change, never a forced deletion of user content.
258
+ const owns = found.instruction !== null && found.instruction.includes(MARKER_START) && found.instruction.includes(MARKER_END);
259
+ if (assets.some((asset ) => asset.id === 'guidance.core') && owns) {
260
+ changes.push({ assetId: 'guidance.core', target: found.paths.instruction, operation: 'remove', ownershipIdentity: 'gemini:instructions', projection: { renderer: 'gemini-instructions' } });
261
+ }
262
+ } else {
263
+ const content = instructionContent({ assets, context, paths: found.paths, fsImpl });
264
+ if (content !== null) {
265
+ const next = managedInstruction(found.instruction, render({ content }));
266
+ if (!next.ok) conflicts.push(next.conflict);
267
+ else if (next.operation !== 'none') changes.push({ assetId: 'guidance.core', target: found.paths.instruction, operation: next.operation, ownershipIdentity: 'gemini:instructions', afterFingerprint: fingerprint(next.content), content: next.content, projection: { renderer: 'gemini-instructions' } });
268
+ }
269
+ }
270
+ const copyTree = planCopyTreeAssets({ assets, scope, scopeRoot, context, ledger, removing, fsImpl });
271
+ changes.push(...copyTree.changes); conflicts.push(...copyTree.conflicts);
272
+ if (found.settings.error) conflicts.push(found.settings.error);
273
+ const hooks = planHooksChange({ removing, found, context, assets, fsImpl });
274
+ changes.push(...hooks.changes); conflicts.push(...hooks.conflicts);
275
+ return {
276
+ changes, conflicts, prerequisites: [], notices: copyTree.notices,
277
+ surfaces: {
278
+ instructions: { status: 'supported', target: found.paths.instruction },
279
+ settings: { status: found.settings.error ? 'invalid' : 'supported', target: found.paths.settings },
280
+ mcp: { status: found.settings.error ? 'blocked' : 'supported', target: found.paths.settings, selected: mcp.map((item ) => item.id) },
281
+ extensions: { status: 'different', mode: found.paths.extensionMode, action: 'use Gemini CLI extension management' },
282
+ policies: policyStatuses(context.policies),
283
+ },
284
+ };
285
+ }
286
+
287
+ function atomicWrite(file , content , { fsImpl = fs } = {}) {
288
+ fsImpl.mkdirSync(path.dirname(file), { recursive: true });
289
+ const temp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
290
+ try {
291
+ fsImpl.writeFileSync(temp, content, { encoding: 'utf8', flag: 'wx' });
292
+ fsImpl.renameSync(temp, file);
293
+ } finally {
294
+ if (fsImpl.existsSync(temp)) fsImpl.rmSync(temp, { force: true });
295
+ }
296
+ }
297
+ function apply({ changes = [], fsImpl = fs } ) {
298
+ for (const change of changes) if (change.content !== undefined) atomicWrite(change.target, change.content, { fsImpl });
299
+ const hooksChange = changes.find((change ) => change.nativeComponent === 'hooks');
300
+ if (hooksChange?.nativePlan) deployGeminiHooks(hooksChange.nativePlan, { fsImpl });
301
+ applyCopyTreeAssets(changes, { fsImpl });
302
+ // An update that relocates an asset (the agents folder layout -> flat files) carries the old
303
+ // rows as removals inside the apply batch; the lifecycle calls remove() only for `doflow remove`.
304
+ removeCopyTreeAssets(changes, { fsImpl });
305
+ }
306
+
307
+ /** Strip only the DoFlow-managed span, exactly like the Claude adapter's removeManagedSection —
308
+ * a file with user content before/after the managed section must keep that content. Only delete
309
+ * the file outright once nothing but the managed section (and its separator) remains. */
310
+ function removeManagedSection(file , { fsImpl = fs } = {}) {
311
+ if (!fsImpl.existsSync(file)) return false;
312
+ const existing = fsImpl.readFileSync(file, 'utf8');
313
+ const start = existing.indexOf(MARKER_START);
314
+ if (start === -1) return false;
315
+ const end = existing.indexOf(MARKER_END, start + MARKER_START.length);
316
+ if (end === -1 || existing.indexOf(MARKER_START, end + MARKER_END.length) !== -1) {
317
+ throw new Error(`Refusing to remove malformed DoFlow markers in ${file}`);
318
+ }
319
+ let after = end + MARKER_END.length;
320
+ if (existing[after] === '\n') after += 1;
321
+ let next = existing.slice(0, start) + existing.slice(after);
322
+ if (next === '\n') next = '';
323
+ if (next === '') fsImpl.rmSync(file, { force: true });
324
+ else fsImpl.writeFileSync(file, next);
325
+ return true;
326
+ }
327
+ function remove({ changes = [], fsImpl = fs } ) {
328
+ for (const change of changes) if (change.target && change.projection?.renderer === 'gemini-instructions') removeManagedSection(change.target, { fsImpl });
329
+ const hooksChange = changes.find((change ) => change.nativeComponent === 'hooks');
330
+ if (hooksChange?.nativePlan) deployRemoveGeminiHooks(hooksChange.nativePlan, { fsImpl });
331
+ removeCopyTreeAssets(changes, { fsImpl });
332
+ }
333
+
334
+ function verify({ scope, scopeRoot, assets = [], context = {}, fsImpl = fs } ) {
335
+ const found = discover({ scope, scopeRoot, context, fsImpl });
336
+ const resources = [];
337
+ if (assets.some((asset ) => asset.id === 'guidance.core') && found.instruction?.includes(MARKER_START) && found.instruction.includes(MARKER_END)) {
338
+ resources.push({ assetId: 'guidance.core', target: found.paths.instruction, ownershipIdentity: 'gemini:instructions', fingerprint: fingerprint(found.instruction), sourceVersion: context.sourceVersion ?? 'unknown', projection: { renderer: 'gemini-instructions' } });
339
+ }
340
+ const copyTree = verifyCopyTreeAssets({ assets, scope, scopeRoot, context, fsImpl });
341
+ resources.push(...copyTree.resources);
342
+ let hooksStatus = null; const conflicts = [...(found.settings.error ? [found.settings.error] : []), ...copyTree.conflicts];
343
+ if (context.geminiHooksSourceFile && !found.settings.error) {
344
+ const hooksPlan = planGeminiHooks({ sourceFile: context.geminiHooksSourceFile, sourceHooksDir: context.geminiHooksSourceDir,
345
+ settingsFile: found.paths.settings, destinationHooksDir: found.paths.hooksDirectory, trusted: context.hooksTrusted, fsImpl });
346
+ hooksStatus = hooksPlan.ok && hooksPlan.status === 'unchanged' ? 'managed' : (hooksPlan.ok ? 'missing' : 'conflict');
347
+ if (!hooksPlan.ok) conflicts.push(...(hooksPlan.errors || []));
348
+ if (hooksStatus === 'managed') {
349
+ resources.push({ assetId: hooksAssetId(assets), target: hooksPlan.settingsFile, ownershipIdentity: 'gemini:hooks',
350
+ fingerprint: fingerprint(hooksPlan.merged.hooks), sourceVersion: context.sourceVersion ?? 'unknown', projection: { renderer: 'gemini-hooks' } });
351
+ }
352
+ }
353
+ return { ok: !found.settings.error && copyTree.conflicts.length === 0 && hooksStatus !== 'conflict', resources,
354
+ statuses: { settings: found.settings.error ? 'invalid' : 'supported', extensions: 'different', policies: policyStatuses(context.policies), copyTree: copyTree.statuses, hooks: hooksStatus },
355
+ conflicts };
356
+ }
357
+
358
+ return { nativePaths, discover, render, plan, apply, remove, verify };
359
+ }
360
+
361
+ const singleton = createGeminiAdapter();
362
+
363
+ /** Gemini's hooks trust, observed for a lifecycle target. Trust is not a static registry
364
+ * prerequisite the way Codex's `trusted-project`/`hook-review` are: Gemini fingerprints hook
365
+ * name/command and warns before running one that changed, so trust is computed live against the
366
+ * current hooks.json source and the harness's current settings.json. This is the adapter's own
367
+ * API for that observation (review A3 — lifecycle used to import planGeminiHooks and re-derive
368
+ * the plan itself, which put Gemini's hook semantics on the lifecycle side of the boundary).
369
+ * Read-only and side-effect free; returns the trust value or null when unobservable.
370
+ * @param {Object} target a lifecycle status target ({adapterInput, discovery})
371
+ * @returns {*} the plan's trust, or null
372
+ */
373
+ function hookTrustFor(target ) {
374
+ const context = target.adapterInput?.context || {};
375
+ const settingsFile = target.discovery?.paths?.settings;
376
+ if (!context.geminiHooksSourceFile || !settingsFile || target.discovery?.settings?.error) return null;
377
+ const plan = planGeminiHooks({
378
+ sourceFile: context.geminiHooksSourceFile, sourceHooksDir: context.geminiHooksSourceDir,
379
+ settingsFile, trusted: context.hooksTrusted,
380
+ });
381
+ return plan.ok ? (plan.trust ?? null) : null;
382
+ }
383
+
384
+ module.exports = {
385
+ MARKER_START, MARKER_END,
386
+ nativePaths: singleton.nativePaths, discover: singleton.discover, render: singleton.render,
387
+ managedInstruction, plan: singleton.plan, apply: singleton.apply, remove: singleton.remove,
388
+ verify: singleton.verify, planGeminiHooks, hookTrustFor,
389
+ createGeminiAdapter,
390
+ };