@khoavu882/doflow 2.0.0-beta.5 → 2.0.1

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 (494) hide show
  1. package/README.md +16 -16
  2. package/bin/doflow.js +22 -12
  3. package/core/.antigravity-plugin/plugin.json +1 -2
  4. package/core/.claude-plugin/plugin.json +1 -1
  5. package/core/.codex-plugin/plugin.json +1 -1
  6. package/core/.plugin/plugin.json +1 -1
  7. package/core/harnesses/claude/hooks/lib.sh +43 -24
  8. package/core/harnesses/claude/hooks/mcp-tool-guard.sh +4 -1
  9. package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
  10. package/core/harnesses/claude/hooks/post-compact.sh +35 -32
  11. package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
  12. package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
  13. package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
  14. package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
  15. package/core/harnesses/claude/hooks/pre-implement-gate.sh +4 -1
  16. package/core/harnesses/claude/hooks/session-end.sh +23 -10
  17. package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
  18. package/core/harnesses/claude/settings/settings.json +26 -9
  19. package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
  20. package/core/harnesses/codex/agents/core-implementer.toml +1 -0
  21. package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
  22. package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
  23. package/core/harnesses/codex/agents/research-writer.toml +1 -1
  24. package/core/harnesses/codex/agents/reviewer.toml +10 -0
  25. package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
  26. package/core/harnesses/codex/agents/system-architect.toml +1 -1
  27. package/core/harnesses/codex/hooks/hooks.json +0 -10
  28. package/core/harnesses/codex/hooks/lib.sh +44 -25
  29. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +4 -1
  30. package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
  31. package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
  32. package/core/harnesses/codex/hooks/pre-implement-gate.sh +4 -1
  33. package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
  34. package/core/harnesses/codex/hooks/subagent-audit.sh +1 -0
  35. package/core/harnesses/copilot/hooks/doflow-lib.sh +94 -0
  36. package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
  37. package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
  38. package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
  39. package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
  40. package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
  41. package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
  42. package/core/harnesses/copilot/hooks/doflow.json +51 -0
  43. package/core/harnesses/kiro/agents/doflow.json +1 -11
  44. package/core/harnesses/kiro/hooks/doflow.json +0 -10
  45. package/core/harnesses/kiro/hooks/lib.sh +26 -6
  46. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +8 -3
  47. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +8 -3
  48. package/core/harnesses/kiro/hooks/session-start.sh +1 -0
  49. package/core/harnesses/kiro/hooks/stop-check.sh +1 -0
  50. package/core/harnesses/pi/extensions/doflow-guard.js +11 -2
  51. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +1 -2
  52. package/core/harnesses/shared/hooks/adapters/antigravity.js +1 -1
  53. package/core/harnesses/shared/hooks/bridge/bridge.sh +126 -23
  54. package/core/harnesses/shared/hooks/bridge/envelope.jq +87 -18
  55. package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
  56. package/core/harnesses/shared/hooks/bridge/respond.sh +16 -2
  57. package/core/harnesses/shared/hooks/policies/capture-failure.sh +49 -16
  58. package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
  59. package/core/harnesses/shared/hooks/policies/contract.sh +3 -0
  60. package/core/harnesses/shared/hooks/policies/deny-json.sh +20 -7
  61. package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
  62. package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
  63. package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
  64. package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
  65. package/core/harnesses/shared/hooks/policies/lib.sh +63 -40
  66. package/core/harnesses/shared/hooks/policies/mcp-policy.conf +1 -1
  67. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +7 -8
  68. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +132 -57
  69. package/core/harnesses/shared/hooks/policies/session-context.sh +30 -1
  70. package/core/harnesses/shared/hooks/policies/stop-check.sh +27 -15
  71. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +6 -3
  72. package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
  73. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +60 -155
  74. package/core/harnesses/shared/hooks/stream-hook-runner.js +58 -20
  75. package/core/harnesses/shared/locator/doflow-run +2 -3
  76. package/core/registry/advice.json +3 -3
  77. package/core/registry/assets.json +55 -82
  78. package/core/registry/capabilities.json +12 -0
  79. package/core/registry/contracts.json +58 -31
  80. package/core/registry/contribution-points.json +35 -0
  81. package/core/registry/gates.json +32 -0
  82. package/core/registry/guidance-caps.json +40 -0
  83. package/core/registry/harnesses.json +1445 -366
  84. package/core/registry/intake.json +53 -0
  85. package/core/registry/lifecycle.json +599 -996
  86. package/core/registry/mcp.json +5 -1
  87. package/core/registry/models.json +7 -1
  88. package/core/registry/package-templates/README.md +19 -0
  89. package/core/registry/package-templates/agent/files/agent.md +9 -0
  90. package/core/registry/package-templates/agent/template.json +4 -0
  91. package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
  92. package/core/registry/package-templates/capability-provider/template.json +11 -0
  93. package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
  94. package/core/registry/package-templates/debate-preset/template.json +4 -0
  95. package/core/registry/package-templates/intake-category/files/category.json +8 -0
  96. package/core/registry/package-templates/intake-category/template.json +4 -0
  97. package/core/registry/package-templates/review-preset/files/preset.json +30 -0
  98. package/core/registry/package-templates/review-preset/template.json +4 -0
  99. package/core/registry/package-templates/skill/files/SKILL.md +8 -0
  100. package/core/registry/package-templates/skill/template.json +4 -0
  101. package/core/registry/package-templates/worker/files/worker.json +92 -0
  102. package/core/registry/package-templates/worker/template.json +11 -0
  103. package/core/registry/packages/doflow/doflow-package.json +15 -0
  104. package/core/registry/packages/doflow/presets/debate-3.json +26 -0
  105. package/core/registry/protocols.json +61 -0
  106. package/core/registry/routes.json +17 -0
  107. package/core/registry/rules.json +431 -0
  108. package/core/registry/settings.json +272 -0
  109. package/core/registry/storage.json +687 -31
  110. package/core/registry/telemetry.json +236 -0
  111. package/core/registry/transcripts.json +142 -0
  112. package/core/registry/verification.json +76 -6
  113. package/core/registry/workflows.json +55 -0
  114. package/core/shared/agent-specs/core-implementer.md +3 -0
  115. package/core/shared/agent-specs/delivery-lead.md +68 -0
  116. package/core/shared/agent-specs/quality-guardian.md +5 -1
  117. package/core/shared/agent-specs/research-writer.md +10 -3
  118. package/core/shared/agent-specs/reviewer.md +53 -0
  119. package/core/shared/agent-specs/spec-analyst.md +5 -6
  120. package/core/shared/agent-specs/system-architect.md +5 -1
  121. package/core/shared/guidance/DOFLOW_CORE.md +1 -5
  122. package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
  123. package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
  124. package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
  125. package/core/shared/guidance/pointers/codex.md +1 -4
  126. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
  127. package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
  128. package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
  129. package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
  130. package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
  131. package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
  132. package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
  133. package/core/shared/guidance/references/ORCHESTRATION.md +3 -2
  134. package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +19 -1
  135. package/core/shared/guidance/rules/RULES.md +45 -0
  136. package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
  137. package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
  138. package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
  139. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
  140. package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
  141. package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
  142. package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
  143. package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
  144. package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
  145. package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
  146. package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
  147. package/core/shared/scripts/doflow/bash/validate-artifacts.sh +49 -5
  148. package/core/shared/scripts/doflow/bin/doflow-run +152 -34
  149. package/core/shared/skills/do/SKILL.md +14 -12
  150. package/core/shared/skills/do/references/advice_calls.md +26 -0
  151. package/core/shared/skills/do/references/pm_routing.md +1 -2
  152. package/core/shared/skills/do/references/program_mode.md +31 -0
  153. package/core/shared/skills/do-brainstorm/SKILL.md +45 -97
  154. package/core/shared/skills/do-brainstorm/references/handoff_branches.md +36 -0
  155. package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
  156. package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
  157. package/core/shared/skills/do-code-review/SKILL.md +48 -148
  158. package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
  159. package/core/shared/skills/do-code-review/references/tools.md +90 -0
  160. package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
  161. package/core/shared/skills/do-design/SKILL.md +23 -20
  162. package/core/shared/skills/do-diagnose/SKILL.md +18 -6
  163. package/core/shared/skills/do-document/SKILL.md +12 -1
  164. package/core/shared/skills/do-document/references/deep_research.md +1 -2
  165. package/core/shared/skills/do-document/references/embedded-research.md +19 -7
  166. package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
  167. package/core/shared/skills/do-execute-plan/SKILL.md +19 -60
  168. package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
  169. package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
  170. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +26 -1
  171. package/core/shared/skills/do-execute-plan/references/run_position.md +16 -2
  172. package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
  173. package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
  174. package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
  175. package/core/shared/skills/do-flow/SKILL.md +10 -11
  176. package/core/shared/skills/do-flow/references/task_classes.md +5 -5
  177. package/core/shared/skills/do-git/SKILL.md +4 -0
  178. package/core/shared/skills/do-implement/SKILL.md +13 -14
  179. package/core/shared/skills/do-plan/SKILL.md +34 -20
  180. package/core/shared/skills/do-test/SKILL.md +7 -4
  181. package/core/shared/templates/doflow/constitution-template.md +9 -3
  182. package/core/shared/templates/doflow/data-model-template.md +1 -1
  183. package/core/shared/templates/doflow/design-template.md +113 -16
  184. package/core/shared/templates/doflow/plan-template.md +11 -10
  185. package/core/shared/templates/doflow/question-log-template.md +14 -2
  186. package/core/shared/templates/doflow/requirement-template.md +1 -1
  187. package/core/shared/workflows/do-flow-chain.md +2 -0
  188. package/core/worker-evidence/antigravity/checks.json +14 -0
  189. package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
  190. package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
  191. package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
  192. package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
  193. package/core/worker-evidence/codex/checks.json +38 -0
  194. package/package.json +10 -6
  195. package/src/adapters/antigravity/index.js +143 -66
  196. package/src/adapters/claude/index.js +46 -5
  197. package/src/adapters/codex/agents.js +25 -15
  198. package/src/adapters/codex/hooks.js +39 -3
  199. package/src/adapters/codex/index.js +61 -13
  200. package/src/adapters/codex/native-projection.js +6 -2
  201. package/src/adapters/copilot/index.js +58 -23
  202. package/src/adapters/copy-tree.js +291 -46
  203. package/src/adapters/guidance-manifest.js +71 -0
  204. package/src/adapters/guidance-pointer.js +19 -0
  205. package/src/adapters/hook-commands.js +6 -10
  206. package/src/adapters/index.js +1 -0
  207. package/src/adapters/kiro/index.js +23 -12
  208. package/src/adapters/opencode/index.js +8 -8
  209. package/src/adapters/pi/hooks.js +2 -1
  210. package/src/adapters/pi/index.js +14 -7
  211. package/src/adapters/resolve-source.js +26 -2
  212. package/src/cli/commands/install.js +2 -1
  213. package/src/cli/commands/reconcile.js +53 -9
  214. package/src/cli/commands/remove.js +2 -2
  215. package/src/cli/commands/rollback.js +6 -3
  216. package/src/cli/commands/self-update.js +18 -9
  217. package/src/cli/commands/status.js +22 -3
  218. package/src/cli/commands/ui.js +99 -0
  219. package/src/cli/commands/update.js +2 -1
  220. package/src/cli/index.js +337 -32
  221. package/src/cli/runtime-commands.js +220 -60
  222. package/src/cli/shared.js +0 -2
  223. package/src/helper/git.js +1 -1
  224. package/src/helper/guardrails.js +1 -1
  225. package/src/helper/marker-merge.js +31 -13
  226. package/src/helper/package-ids.js +37 -0
  227. package/src/helper/semver.js +94 -0
  228. package/src/install/backup.js +105 -15
  229. package/src/install/context.js +1 -1
  230. package/src/install/targets.js +31 -16
  231. package/src/lifecycle/coverage.js +4 -16
  232. package/src/lifecycle/hook-tier.js +160 -0
  233. package/src/lifecycle/index.js +412 -78
  234. package/src/lifecycle/retired-harnesses.js +318 -0
  235. package/src/lifecycle/view.js +35 -15
  236. package/src/registry/index.js +551 -26
  237. package/src/registry/worker-ids.js +45 -0
  238. package/src/release/build-package.js +0 -3
  239. package/src/release/docs-blocks.js +472 -0
  240. package/src/release/preflight.js +425 -0
  241. package/src/release/rehearse.js +401 -0
  242. package/src/release/release-workflow.js +733 -0
  243. package/src/release/sync-plugin-versions.js +43 -7
  244. package/src/runtime/advice/bench-gate.js +25 -0
  245. package/src/runtime/advice/cli.js +59 -16
  246. package/src/runtime/advice/client.js +32 -9
  247. package/src/runtime/advice/config.js +98 -133
  248. package/src/runtime/advice/doctor.js +1 -1
  249. package/src/runtime/advice/elevation.js +97 -0
  250. package/src/runtime/advice/endpoint-guard.js +9 -3
  251. package/src/runtime/advice/mode.js +37 -0
  252. package/src/runtime/advice/record.js +18 -5
  253. package/src/runtime/advice/registry.js +4 -1
  254. package/src/runtime/advice/shadow-config.js +229 -0
  255. package/src/runtime/advice/shadow-dispatcher.js +276 -0
  256. package/src/runtime/brief-rules.js +81 -0
  257. package/src/runtime/brief-tools.js +111 -34
  258. package/src/runtime/candidate.js +20 -11
  259. package/src/runtime/capabilities/executor.js +450 -0
  260. package/src/runtime/capabilities/loopback.js +41 -0
  261. package/src/runtime/capabilities/sanitizer.js +88 -0
  262. package/src/runtime/capability-router.js +135 -3
  263. package/src/runtime/checkouts.js +73 -11
  264. package/src/runtime/claims.js +13 -7
  265. package/src/runtime/cli.js +196 -19
  266. package/src/runtime/config-home.js +42 -0
  267. package/src/runtime/context/arbitrate.js +767 -0
  268. package/src/runtime/context/delivery-store.js +4 -6
  269. package/src/runtime/context/emission-rules.js +118 -0
  270. package/src/runtime/context/identity.js +227 -0
  271. package/src/runtime/context/pointer.js +67 -12
  272. package/src/runtime/context/primary-command.js +117 -0
  273. package/src/runtime/context/primary.js +344 -0
  274. package/src/runtime/context/summary-store.js +655 -0
  275. package/src/runtime/context-pack.js +158 -79
  276. package/src/runtime/decision-register.js +13 -4
  277. package/src/runtime/dispatch/admission.js +392 -0
  278. package/src/runtime/dispatch/attempt-end.js +274 -0
  279. package/src/runtime/dispatch/command.js +628 -0
  280. package/src/runtime/dispatch/controls.js +904 -0
  281. package/src/runtime/dispatch/envelope.js +276 -0
  282. package/src/runtime/dispatch/grants.js +310 -0
  283. package/src/runtime/dispatch/integrate.js +617 -0
  284. package/src/runtime/dispatch/loop.js +1835 -0
  285. package/src/runtime/dispatch/store.js +352 -0
  286. package/src/runtime/dispatch/verify-run.js +201 -0
  287. package/src/runtime/evidence-ledger.js +52 -7
  288. package/src/runtime/failure/capture.js +3 -1
  289. package/src/runtime/failure/cli.js +25 -9
  290. package/src/runtime/failure/home.js +57 -30
  291. package/src/runtime/failure/store.js +7 -2
  292. package/src/runtime/feature-pointer.js +174 -0
  293. package/src/runtime/feature-resolve.js +13 -2
  294. package/src/runtime/fleet/command.js +77 -0
  295. package/src/runtime/fleet/projects.js +178 -0
  296. package/src/runtime/fleet/tree.js +383 -0
  297. package/src/runtime/freshness.js +33 -3
  298. package/src/runtime/git-memo.js +46 -0
  299. package/src/runtime/guidance/ablation.js +427 -0
  300. package/src/runtime/health.js +624 -18
  301. package/src/runtime/history-compactor.js +13 -3
  302. package/src/runtime/implementation-gate.js +281 -26
  303. package/src/runtime/intake/engine.js +677 -0
  304. package/src/runtime/intake/index.js +136 -0
  305. package/src/runtime/intake/manifest.js +140 -0
  306. package/src/runtime/intake/taxonomy.js +206 -0
  307. package/src/runtime/inventory/coverage.js +1 -1
  308. package/src/runtime/inventory/index.js +1 -1
  309. package/src/runtime/inventory/precedence.js +2 -2
  310. package/src/runtime/inventory/siblings.js +6 -4
  311. package/src/runtime/knowledge/code-corpus.js +277 -0
  312. package/src/runtime/knowledge/code-lexical.js +130 -0
  313. package/src/runtime/knowledge/code-retrieval.js +307 -0
  314. package/src/runtime/knowledge/code-validation.js +106 -0
  315. package/src/runtime/knowledge/gp3-gate.js +74 -0
  316. package/src/runtime/knowledge/graph-adapter.js +288 -0
  317. package/src/runtime/knowledge/graph-fallback.js +394 -0
  318. package/src/runtime/knowledge/hybrid.js +136 -8
  319. package/src/runtime/knowledge/retrieval.js +34 -1
  320. package/src/runtime/knowledge/rtk-compress.js +77 -0
  321. package/src/runtime/knowledge/semble-bridge.py +106 -0
  322. package/src/runtime/knowledge/semble-lifecycle.js +114 -0
  323. package/src/runtime/knowledge/semble-provider.js +365 -0
  324. package/src/runtime/lifecycle/cli.js +88 -10
  325. package/src/runtime/lifecycle/event-store.js +8 -6
  326. package/src/runtime/lifecycle/followup.js +38 -7
  327. package/src/runtime/lifecycle/overview.js +26 -2
  328. package/src/runtime/lifecycle/report-store.js +8 -10
  329. package/src/runtime/lifecycle/retention.js +95 -31
  330. package/src/runtime/lifecycle/root.js +9 -1
  331. package/src/runtime/lifecycle/status.js +34 -17
  332. package/src/runtime/lifecycle/store-upkeep.js +56 -7
  333. package/src/runtime/mask.js +134 -7
  334. package/src/runtime/mcp-consultations.js +40 -6
  335. package/src/runtime/model-router.js +79 -7
  336. package/src/runtime/models/catalogue.js +191 -0
  337. package/src/runtime/models/command.js +153 -0
  338. package/src/runtime/off-words.js +12 -0
  339. package/src/runtime/outcome.js +2 -2
  340. package/src/runtime/packages/command.js +1009 -0
  341. package/src/runtime/packages/contributions.js +116 -0
  342. package/src/runtime/packages/digest.js +132 -0
  343. package/src/runtime/packages/fetch.js +182 -0
  344. package/src/runtime/packages/grants.js +192 -0
  345. package/src/runtime/packages/index-file.js +134 -0
  346. package/src/runtime/packages/instruction.js +90 -0
  347. package/src/runtime/packages/manifest.js +331 -0
  348. package/src/runtime/packages/points.js +487 -0
  349. package/src/runtime/packages/programs.js +216 -0
  350. package/src/runtime/packages/scaffold.js +277 -0
  351. package/src/runtime/packages/source.js +90 -0
  352. package/src/runtime/packages/state.js +164 -0
  353. package/src/runtime/packages/store.js +449 -0
  354. package/src/runtime/packages/tar.js +106 -0
  355. package/src/runtime/packages/view.js +406 -0
  356. package/src/runtime/plan-check.js +59 -0
  357. package/src/runtime/plan-content.js +324 -0
  358. package/src/runtime/policy-init.js +118 -0
  359. package/src/runtime/profiles/doctor.js +96 -0
  360. package/src/runtime/profiles/resolve.js +244 -0
  361. package/src/runtime/profiles/rollback.js +254 -0
  362. package/src/runtime/profiles/sampler.js +41 -0
  363. package/src/runtime/readiness-record.js +16 -11
  364. package/src/runtime/readiness.js +46 -5
  365. package/src/runtime/research-request.js +50 -25
  366. package/src/runtime/retrieval-plan.js +10 -30
  367. package/src/runtime/retrieval-record.js +75 -5
  368. package/src/runtime/review/command.js +246 -0
  369. package/src/runtime/review/dominance.js +68 -0
  370. package/src/runtime/review/findings.js +434 -0
  371. package/src/runtime/review/identity.js +255 -0
  372. package/src/runtime/review/protocol-command.js +109 -0
  373. package/src/runtime/review/protocols.js +405 -0
  374. package/src/runtime/review/resolve-protocol.js +302 -0
  375. package/src/runtime/review/start.js +530 -0
  376. package/src/runtime/roadmap/attendance.js +137 -0
  377. package/src/runtime/roadmap/facts.js +479 -0
  378. package/src/runtime/roadmap/gates.js +273 -0
  379. package/src/runtime/roadmap/loop.js +1719 -0
  380. package/src/runtime/roadmap/merge.js +390 -0
  381. package/src/runtime/roadmap/rows.js +1122 -0
  382. package/src/runtime/roadmap/seed.js +1063 -0
  383. package/src/runtime/roadmap/worktree-cleanup.js +147 -0
  384. package/src/runtime/rules-report.js +336 -0
  385. package/src/runtime/scaffold/artifacts.js +60 -9
  386. package/src/runtime/scaffold/generate.js +21 -9
  387. package/src/runtime/settings/autopilot.js +106 -0
  388. package/src/runtime/settings/cli.js +280 -0
  389. package/src/runtime/settings/config-write.js +314 -0
  390. package/src/runtime/settings/files.js +271 -0
  391. package/src/runtime/settings/legacy.js +154 -0
  392. package/src/runtime/settings/lexical.js +56 -0
  393. package/src/runtime/settings/prepared-run.js +269 -0
  394. package/src/runtime/settings/project-layers.js +118 -0
  395. package/src/runtime/settings/refs.js +288 -0
  396. package/src/runtime/settings/registry.js +553 -0
  397. package/src/runtime/settings/resolve.js +713 -0
  398. package/src/runtime/settings/shared.js +37 -0
  399. package/src/runtime/settings/worker-billing.js +107 -0
  400. package/src/runtime/settings/write.js +269 -0
  401. package/src/runtime/slot-command.js +78 -0
  402. package/src/runtime/slots.js +671 -0
  403. package/src/runtime/source-version.js +1 -0
  404. package/src/runtime/state-root.js +217 -0
  405. package/src/runtime/storage/cli.js +12 -5
  406. package/src/runtime/storage/engine.js +9 -2
  407. package/src/runtime/storage/registry.js +14 -1
  408. package/src/runtime/storage/roots.js +66 -16
  409. package/src/runtime/storage/rules.js +82 -10
  410. package/src/runtime/storage/upkeep.js +10 -2
  411. package/src/runtime/storage/work-status.js +245 -26
  412. package/src/runtime/task-attempts.js +589 -69
  413. package/src/runtime/task-classifier.js +85 -14
  414. package/src/runtime/task-graph.js +373 -0
  415. package/src/runtime/task-run.js +778 -0
  416. package/src/runtime/task-scope.js +176 -55
  417. package/src/runtime/task-sources.js +147 -0
  418. package/src/runtime/task-state.js +373 -64
  419. package/src/runtime/team/admission.js +301 -0
  420. package/src/runtime/team/envelope.js +324 -0
  421. package/src/runtime/team/review-guard.js +156 -0
  422. package/src/runtime/trace/analyze.js +427 -0
  423. package/src/runtime/trace/baseline.js +770 -0
  424. package/src/runtime/trace/ledger.js +63 -140
  425. package/src/runtime/trace/otlp-export.js +568 -0
  426. package/src/runtime/trace/otlp-map.js +481 -0
  427. package/src/runtime/trace/spans.js +745 -0
  428. package/src/runtime/trace/telemetry.js +225 -0
  429. package/src/runtime/trace/views.js +1 -0
  430. package/src/runtime/transcripts/command.js +98 -0
  431. package/src/runtime/transcripts/cursor.js +326 -0
  432. package/src/runtime/transcripts/read.js +126 -0
  433. package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
  434. package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
  435. package/src/runtime/transcripts/readers/copilot-events.js +90 -0
  436. package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
  437. package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
  438. package/src/runtime/transcripts/registry.js +161 -0
  439. package/src/runtime/transcripts/stats.js +161 -0
  440. package/src/runtime/verb-view.js +267 -0
  441. package/src/runtime/verification/check-key.js +628 -0
  442. package/src/runtime/verification/contract-runner.js +546 -11
  443. package/src/runtime/verification/engine.js +499 -54
  444. package/src/runtime/verification/impact.js +617 -0
  445. package/src/runtime/verification/inflight.js +205 -0
  446. package/src/runtime/verification/registry.js +176 -0
  447. package/src/runtime/verification/reuse.js +184 -0
  448. package/src/runtime/verification/telemetry-emit.js +254 -0
  449. package/src/runtime/verification/test-selection.js +67 -7
  450. package/src/runtime/worker/active.js +90 -0
  451. package/src/runtime/worker/cancel.js +222 -0
  452. package/src/runtime/worker/catalogue.js +122 -0
  453. package/src/runtime/worker/command.js +677 -0
  454. package/src/runtime/worker/evidence.js +288 -0
  455. package/src/runtime/worker/launch.js +256 -0
  456. package/src/runtime/worker/models.js +137 -0
  457. package/src/runtime/worker/parsers.js +218 -0
  458. package/src/runtime/worker/probes/index.js +706 -0
  459. package/src/runtime/worker/process-tree.js +415 -0
  460. package/src/runtime/worker/quota.js +43 -0
  461. package/src/runtime/worker/readiness.js +713 -0
  462. package/src/runtime/worker/run-store.js +166 -0
  463. package/src/runtime/worker/supervisor.js +541 -0
  464. package/src/runtime/worker/worktree-check.js +252 -0
  465. package/src/runtime/workflow-engine.js +5 -1
  466. package/src/runtime/workflow-orchestrator.js +610 -67
  467. package/src/runtime/worktree.js +149 -22
  468. package/src/state/index.js +1 -1
  469. package/src/state/lockfile.js +64 -10
  470. package/src/state/run-lock.js +1 -1
  471. package/types/node-commonjs.d.ts +106 -7
  472. package/core/harnesses/claude/hooks/pre-bash-guard.sh +0 -10
  473. package/core/harnesses/codex/hooks/pre-bash-guard.sh +0 -10
  474. package/core/harnesses/gemini/hooks/hooks.json +0 -77
  475. package/core/harnesses/gemini/hooks/lib.sh +0 -302
  476. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -10
  477. package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
  478. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -10
  479. package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
  480. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -10
  481. package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
  482. package/core/harnesses/gemini/hooks/session-end.sh +0 -5
  483. package/core/harnesses/gemini/hooks/session-start.sh +0 -7
  484. package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
  485. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +0 -24
  486. package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +0 -91
  487. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +0 -378
  488. package/core/shared/guidance/FLAGS.md +0 -25
  489. package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
  490. package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
  491. package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
  492. package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
  493. package/src/adapters/gemini/hooks.js +0 -251
  494. package/src/adapters/gemini/index.js +0 -429
@@ -29,6 +29,12 @@ const SETTINGS_RENDERER = 'claude-settings';
29
29
  * its install destination is declared (paths.statuslineScript).
30
30
  */
31
31
  const SETTINGS_FILES = ['settings.json', 'keybindings.json'];
32
+ /** Hook entries an earlier DoFlow wrote into settings.json and no longer ships. The update prunes their
33
+ * scripts, so a merge strips these entries from the live file (and a remove does too) instead of leaving a
34
+ * hook that points at a missing script. Written in the global form; `retiredHooks` rewrites the prefix for project scope. */
35
+ const RETIRED_SETTINGS_HOOKS = {
36
+ PreToolUse: [{ matcher: 'Bash', hooks: [{ type: 'command', command: `${GLOBAL_HOOK_PREFIX}pre-bash-guard.sh` }] }],
37
+ };
32
38
 
33
39
  function sha256(value ) {
34
40
  return crypto.createHash('sha256').update(value).digest('hex');
@@ -183,6 +189,36 @@ function createClaudeAdapter({ declaredPaths = declaredHarnessPaths().claude } =
183
189
  function settingsContent(fileName , raw , scope ) {
184
190
  return fileName === 'settings.json' && scope !== 'global' ? raw.split(GLOBAL_HOOK_PREFIX).join(PROJECT_HOOK_PREFIX) : raw;
185
191
  }
192
+ /** Hook tiers (maint-hooks): with Claude outside `hooks.harnesses` the managed settings carry no
193
+ * `hooks` block. Returns the managed text to write and the hooks it left out, which a merge strips
194
+ * from the live file so an earlier install's entries do not stay behind. */
195
+ function tieredSettings(fileName , raw , context ) {
196
+ if (fileName !== 'settings.json' || context?.hooksOff !== true) return { raw, droppedHooks: null };
197
+ let parsed;
198
+ try { parsed = JSON.parse(raw); } catch { return { raw, droppedHooks: null }; }
199
+ if (!parsed || parsed.hooks === undefined) return { raw, droppedHooks: null };
200
+ const { hooks, ...rest } = parsed;
201
+ return { raw: serializeSettings(rest), droppedHooks: hooks };
202
+ }
203
+ function retiredHooks(scope ) {
204
+ const raw = JSON.stringify(RETIRED_SETTINGS_HOOKS);
205
+ return JSON.parse(scope === 'global' ? raw : raw.split(GLOBAL_HOOK_PREFIX).join(PROJECT_HOOK_PREFIX));
206
+ }
207
+ /** The managed settings text with the retired hook entries added, so a remove strips them as well. */
208
+ function withRetiredHooks(raw , scope ) {
209
+ let parsed;
210
+ try { parsed = JSON.parse(raw); } catch { return raw; }
211
+ const hooks = { ...(parsed.hooks || {}) };
212
+ for (const [event, entries] of Object.entries (retiredHooks(scope))) hooks[event] = [...(hooks[event] || []), ...entries];
213
+ return JSON.stringify({ ...parsed, hooks });
214
+ }
215
+ /** The live settings without DoFlow's own entries of `hooks`; every other key is left alone. */
216
+ function withoutManagedHooks(current , hooks ) {
217
+ if (!hooks || !current || typeof current !== 'object' || current.hooks === undefined) return current;
218
+ const { hooks: liveHooks, ...rest } = current;
219
+ const stripped = stripManagedSettings({ hooks: liveHooks, _: true }, { hooks });
220
+ return stripped?.hooks ? { ...rest, hooks: stripped.hooks } : rest;
221
+ }
186
222
  function settingsLedgerResources(ledger , assetId ) {
187
223
  const byFile = new Map();
188
224
  for (const resource of ledger?.resources || []) {
@@ -228,7 +264,9 @@ function createClaudeAdapter({ declaredPaths = declaredHarnessPaths().claude } =
228
264
  // entries, so a drifted file is safe to process rather than a reason to refuse.
229
265
  changes.push({ assetId: asset.id, target, operation: 'remove', ownershipIdentity: `doflow:claude:settings:${fileName}`,
230
266
  ...settingsChangeExtras(fileName), fingerprint: prev.fingerprint,
231
- managed: settingsContent(fileName, fs.readFileSync(sourceAbs, 'utf8'), scope),
267
+ managed: fileName === 'settings.json'
268
+ ? withRetiredHooks(settingsContent(fileName, fs.readFileSync(sourceAbs, 'utf8'), scope), scope)
269
+ : settingsContent(fileName, fs.readFileSync(sourceAbs, 'utf8'), scope),
232
270
  projection: { renderer: SETTINGS_RENDERER } });
233
271
  }
234
272
  if (previous.get(statuslineScriptName)) {
@@ -242,7 +280,8 @@ function createClaudeAdapter({ declaredPaths = declaredHarnessPaths().claude } =
242
280
  for (const fileName of SETTINGS_FILES) {
243
281
  const sourceAbs = path.join(sourceDir, fileName);
244
282
  if (!fs.existsSync(sourceAbs)) continue;
245
- let managedRaw = settingsContent(fileName, fs.readFileSync(sourceAbs, 'utf8'), scope);
283
+ const tiered = tieredSettings(fileName, settingsContent(fileName, fs.readFileSync(sourceAbs, 'utf8'), scope), context);
284
+ let managedRaw = tiered.raw;
246
285
  if (fileName === 'settings.json' && context.statusline && !removing) {
247
286
  // --statusline: point Claude Code's statusLine key at the projected script. It rides the
248
287
  // same managed settings write as everything else this asset owns — one serialization, one
@@ -267,7 +306,8 @@ function createClaudeAdapter({ declaredPaths = declaredHarnessPaths().claude } =
267
306
  const currentRaw = fs.readFileSync(target, 'utf8');
268
307
  let merged;
269
308
  try {
270
- merged = mergeSettings(JSON.parse(currentRaw), JSON.parse(managedRaw));
309
+ const live = fileName === 'settings.json' ? withoutManagedHooks(JSON.parse(currentRaw), retiredHooks(scope)) : JSON.parse(currentRaw);
310
+ merged = mergeSettings(withoutManagedHooks(live, tiered.droppedHooks), JSON.parse(managedRaw));
271
311
  } catch {
272
312
  conflicts.push(`${fileName} is not valid JSON and cannot be merged`);
273
313
  continue;
@@ -358,7 +398,8 @@ function createClaudeAdapter({ declaredPaths = declaredHarnessPaths().claude } =
358
398
  for (const fileName of SETTINGS_FILES) {
359
399
  const sourceAbs = path.join(sourceDir, fileName);
360
400
  if (!fs.existsSync(sourceAbs)) continue;
361
- const fingerprint = sha256(settingsContent(fileName, fs.readFileSync(sourceAbs, 'utf8'), scope));
401
+ const sourceText = tieredSettings(fileName, settingsContent(fileName, fs.readFileSync(sourceAbs, 'utf8'), scope), context).raw;
402
+ const fingerprint = sha256(sourceText);
362
403
  const target = settingsTarget(fileName, paths);
363
404
  // 'absent' (not 'missing') matches the same convention the instructions asset already uses
364
405
  // for "no file here" — 'missing' is reserved by the lifecycle layer's removal reconciliation
@@ -373,7 +414,7 @@ function createClaudeAdapter({ declaredPaths = declaredHarnessPaths().claude } =
373
414
  let managed;
374
415
  try {
375
416
  current = JSON.parse(fs.readFileSync(target, 'utf8'));
376
- managed = JSON.parse(settingsContent(fileName, fs.readFileSync(sourceAbs, 'utf8'), scope));
417
+ managed = JSON.parse(sourceText);
377
418
  } catch {
378
419
  conflicts.push(`${fileName} is not valid JSON`);
379
420
  statuses.push({ assetId: asset.id, capability: 'settings', status: 'conflict', target });
@@ -9,18 +9,12 @@ const crypto = require('node:crypto');
9
9
  const fs = require('node:fs');
10
10
  const path = require('node:path');
11
11
  const { atomicWrite } = require('./config.js');
12
+ const { REPO_ROOT } = require('../../helper/repo-root.js');
12
13
 
13
14
  const AGENT_DIRECTORY = 'agents';
14
15
  const AGENT_KIND = 'custom-agent';
15
16
  const REQUIRED_FIELDS = ['name', 'description', 'developer_instructions'];
16
- const READ_ONLY_ROLES = new Set([
17
- 'backend-architect', 'deep-research-agent', 'devops-architect', 'frontend-architect',
18
- 'quality-engineer', 'requirements-analyst', 'root-cause-analyst', 'security-engineer',
19
- 'system-architect', 'technical-writer',
20
- ]);
21
- const IMPLEMENTATION_ROLES = new Set([
22
- 'performance-engineer', 'pm-agent', 'python-expert', 'refactoring-expert',
23
- ]);
17
+ const SPEC_DIRECTORY = path.join(REPO_ROOT, 'core', 'shared', 'agent-specs');
24
18
  const ALLOWED_FIELDS = new Set([...REQUIRED_FIELDS, 'sandbox_mode']);
25
19
 
26
20
  function fingerprint(content ) {
@@ -85,7 +79,21 @@ function parseAgentToml(source , { fileName } = {})
85
79
  return values;
86
80
  }
87
81
 
88
- function validateAgentContract(source , { fileName } = {}) {
82
+ /** `access` of every shared agent spec by name (118 IC-002). A TOML whose name has no spec is the
83
+ * user's own agent and is not tied to any access. */
84
+ function specAccess(specDir = SPEC_DIRECTORY, fsImpl = fs) {
85
+ const access = new Map ();
86
+ if (!fsImpl.existsSync(specDir)) return access;
87
+ for (const entry of fsImpl.readdirSync(specDir)) {
88
+ if (!entry.endsWith('.md')) continue;
89
+ const frontmatter = fsImpl.readFileSync(path.join(specDir, entry), 'utf8').match(/^---\r?\n([\s\S]*?)\r?\n---/);
90
+ const value = frontmatter?.[1].match(/^access:\s*(\S+)\s*$/m)?.[1];
91
+ if (value) access.set(entry.slice(0, -'.md'.length), value);
92
+ }
93
+ return access;
94
+ }
95
+
96
+ function validateAgentContract(source , { fileName, access = specAccess() } = {}) {
89
97
  const agent = parseAgentToml(source, { fileName });
90
98
  for (const field of REQUIRED_FIELDS) {
91
99
  if (typeof agent[field] !== 'string' || !agent[field].trim()) throw new Error(`Agent is missing required '${field}'`);
@@ -97,11 +105,12 @@ function validateAgentContract(source , { fileName }
97
105
  if (agent.sandbox_mode !== undefined && !['read-only', 'workspace-write', 'danger-full-access'].includes(agent.sandbox_mode)) {
98
106
  throw new Error(`Agent '${agent.name}' has an unsupported sandbox_mode`);
99
107
  }
100
- if (READ_ONLY_ROLES.has(agent.name) && agent.sandbox_mode !== 'read-only') {
101
- throw new Error(`Analysis role '${agent.name}' must use sandbox_mode = "read-only"`);
108
+ const specAccessValue = access.get(agent.name);
109
+ if (specAccessValue === 'read-only' && agent.sandbox_mode !== 'read-only') {
110
+ throw new Error(`Agent '${agent.name}' has access: read-only and must use sandbox_mode = "read-only"`);
102
111
  }
103
- if (IMPLEMENTATION_ROLES.has(agent.name) && agent.sandbox_mode !== undefined) {
104
- throw new Error(`Implementation role '${agent.name}' must inherit its parent sandbox`);
112
+ if (specAccessValue === 'write' && agent.sandbox_mode !== undefined) {
113
+ throw new Error(`Agent '${agent.name}' has access: write and must inherit its parent sandbox`);
105
114
  }
106
115
  return Object.freeze({ ...agent, fileName: filenameFor(agent.name) });
107
116
  }
@@ -134,12 +143,13 @@ function discoverCodexAgents(sourceDir , fsImpl = fs) {
134
143
  const directory = path.resolve(sourceDir);
135
144
  if (!fsImpl.existsSync(directory)) throw new Error(`Codex agent source directory does not exist: ${sourceDir}`);
136
145
  const agents = [];
146
+ const access = specAccess();
137
147
  for (const entry of fsImpl.readdirSync(directory, { withFileTypes: true }).sort((a , b ) => a.name.localeCompare(b.name))) {
138
148
  if (!entry.name.endsWith('.toml')) continue;
139
149
  if (!entry.isFile()) throw new Error(`Codex agent source '${entry.name}' must be a regular file`);
140
150
  const name = expectedName(entry.name);
141
151
  const source = fsImpl.readFileSync(path.join(directory, entry.name), 'utf8');
142
- agents.push({ ...validateAgentContract(source, { fileName: entry.name }), name, source, fingerprint: fingerprint(source) });
152
+ agents.push({ ...validateAgentContract(source, { fileName: entry.name, access }), name, source, fingerprint: fingerprint(source) });
143
153
  }
144
154
  if (!agents.length) throw new Error(`No Codex agent TOML contracts found in ${sourceDir}`);
145
155
  return agents;
@@ -202,7 +212,7 @@ function reconcileCodexAgents(options ) {
202
212
  }
203
213
 
204
214
  module.exports = {
205
- AGENT_DIRECTORY, AGENT_KIND, READ_ONLY_ROLES, IMPLEMENTATION_ROLES, fingerprint,
215
+ AGENT_DIRECTORY, AGENT_KIND, fingerprint, specAccess,
206
216
  parseAgentToml, validateAgentContract, agentDirectory, destinationFor, discoverCodexAgents,
207
217
  planCodexAgents, applyCodexAgents, reconcileCodexAgents,
208
218
  };
@@ -17,6 +17,41 @@ const SUPPORTED_EVENTS = new Set([
17
17
  'UserPromptSubmit', 'SubagentStop', 'Stop', 'SessionStart', 'SubagentStart', 'SessionEnd',
18
18
  ]);
19
19
 
20
+ // An earlier DoFlow wired a pre-bash-guard front door into hooks.json and deployed its script. Both
21
+ // are gone from the shipped source; a merge drops the entry from the live file so an updated install
22
+ // does not keep a hook that runs a script nothing refreshes, and the script goes with it. Only the
23
+ // exact command DoFlow shipped is dropped: a user's own hook that happens to name a script of the same
24
+ // file name stays.
25
+ const RETIRED_HOOK_SCRIPT = 'pre-bash-guard.sh';
26
+ const RETIRED_HOOK_COMMAND = 'bash "$(root=$(git rev-parse --show-toplevel 2>/dev/null) && [ -f "$root/.codex/hooks/pre-bash-guard.sh" ] && printf \'%s/.codex/hooks/pre-bash-guard.sh\' "$root" || printf \'%s/hooks/pre-bash-guard.sh\' "${CODEX_HOME:-$HOME/.codex}")"';
27
+
28
+ function withoutRetiredHooks(hooks ) {
29
+ const result = {};
30
+ for (const [event, groups] of Object.entries (hooks || {})) {
31
+ if (!Array.isArray(groups)) { result[event] = groups; continue; }
32
+ const kept = groups.map((group ) => (Array.isArray(group?.hooks)
33
+ ? { ...group, hooks: group.hooks.filter((handler ) => handler?.command !== RETIRED_HOOK_COMMAND) }
34
+ : group)).filter((group ) => !Array.isArray(group?.hooks) || group.hooks.length > 0);
35
+ if (kept.length) result[event] = kept;
36
+ }
37
+ return result;
38
+ }
39
+
40
+ /** Whether the retired front door is still on disk as DoFlow's own (it names the bridge call it was written with). */
41
+ function ownsRetiredHookScript(destinationHooksDir , fsImpl = fs) {
42
+ const target = path.join(destinationHooksDir, RETIRED_HOOK_SCRIPT);
43
+ try {
44
+ return fsImpl.lstatSync(target).isFile() && fsImpl.readFileSync(target, 'utf8').includes('doflow_bridge codex PreToolUse pre-bash-guard');
45
+ } catch { return false; }
46
+ }
47
+
48
+ /** Delete the retired front door when it is still DoFlow's own. */
49
+ function removeRetiredHookScript(destinationHooksDir , fsImpl = fs, dryRun = false) {
50
+ if (!ownsRetiredHookScript(destinationHooksDir, fsImpl)) return 0;
51
+ if (!dryRun) { try { fsImpl.unlinkSync(path.join(destinationHooksDir, RETIRED_HOOK_SCRIPT)); } catch { return 0; } }
52
+ return 1;
53
+ }
54
+
20
55
  function validationError(errors , message ) { errors.push(message); }
21
56
 
22
57
  function validateHooksConfig(config ) {
@@ -148,7 +183,7 @@ function planCodexHooks({ config, sourceFile, sourceHooksDir, destinationContext
148
183
  try {
149
184
  const existing = JSON.parse(original);
150
185
  if (existing?.hooks && desired?.hooks) {
151
- mergedDesired = { ...existing, ...desired, hooks: mergeHooks(existing.hooks, desired.hooks) };
186
+ mergedDesired = { ...existing, ...desired, hooks: mergeHooks(withoutRetiredHooks(existing.hooks), desired.hooks) };
152
187
  }
153
188
  } catch {
154
189
  // malformed existing will be flagged or left for schema validation
@@ -167,7 +202,7 @@ function planCodexHooks({ config, sourceFile, sourceHooksDir, destinationContext
167
202
  return { ok: false, status: 'conflict', destination, original, changes: [], commands,
168
203
  errors: scripts.edited.map((name ) => `Codex hook script '${name}' was modified outside DoFlow`) };
169
204
  }
170
- const scriptsOnly = !configChanged && scripts.stale.length > 0;
205
+ const scriptsOnly = !configChanged && (scripts.stale.length > 0 || ownsRetiredHookScript(destinationHooksDir, fsImpl));
171
206
  const changed = configChanged || scriptsOnly;
172
207
  // The scripts deployCodexHooks copies beside hooks.json, so a backup of this change holds them too.
173
208
  const companionTargets = [...new Set([
@@ -204,6 +239,7 @@ function deployCodexHooks(plan , { dryRun = false, fsImpl = fs }
204
239
  fsImpl.copyFileSync(source, target);
205
240
  fsImpl.chmodSync(target, fsImpl.statSync(source).mode & 0o777);
206
241
  }
242
+ removeRetiredHookScript(plan.destinationHooksDir, fsImpl);
207
243
  return { ...plan, applied: true };
208
244
  }
209
245
 
@@ -220,7 +256,7 @@ function shippedHookScripts(scriptsDir , fsImpl = fs) {
220
256
  * user edited or added stays. The directory goes only once it is empty. */
221
257
  function removeCodexHookScripts({ scriptsDir, destinationHooksDir } = {}, { dryRun = false, fsImpl = fs } = {}) {
222
258
  if (!destinationHooksDir || !fsImpl.existsSync(destinationHooksDir)) return { removed: 0 };
223
- let removed = 0;
259
+ let removed = removeRetiredHookScript(destinationHooksDir, fsImpl, dryRun);
224
260
  for (const name of shippedHookScripts(scriptsDir, fsImpl)) {
225
261
  const target = path.join(destinationHooksDir, name);
226
262
  // lstat, not stat: a symlink (even to a file with the shipped bytes) is not DoFlow's copy.
@@ -13,8 +13,9 @@ const { configPath, fingerprint: configFingerprint, parseToml, planCodexConfig,
13
13
  const { renderServer, planCodexMcp, applyCodexMcp, ownedCodexMcpIds, userDefinedCodexMcpIds, serverTableText } = require('./mcp.js');
14
14
  const { agentDirectory, discoverCodexAgents, planCodexAgents, applyCodexAgents } = require('./agents.js');
15
15
  const { planCodexHooks, deployCodexHooks, removeCodexHookScripts, scriptFingerprint } = require('./hooks.js');
16
- const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices } = require('../copy-tree.js');
16
+ const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, resolveTransform } = require('../copy-tree.js');
17
17
  const { mergeMarkedSection, removeMarkedSection, MARKER_START, MARKER_END } = require('../../helper/marker-merge.js');
18
+ const { renderGuidancePointer } = require('../guidance-pointer.js');
18
19
  const { nativeMcpCatalog } = require('../../registry/index.js');
19
20
  const { declaredHarnessPaths, resolveHarnessPaths } = require('../../helper/harness-paths.js');
20
21
 
@@ -275,6 +276,26 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
275
276
  }
276
277
  function sha256Text(text ) { return crypto.createHash('sha256').update(text).digest('hex'); }
277
278
 
279
+ function instructionBody(content ) {
280
+ if (typeof content !== 'string') return null;
281
+ const start = content.indexOf(MARKER_START);
282
+ if (start === -1) return null;
283
+ const bodyStart = start + MARKER_START.length;
284
+ const end = content.indexOf(MARKER_END, bodyStart);
285
+ if (end === -1 || content.indexOf(MARKER_START, end + MARKER_END.length) !== -1) return null;
286
+ let from = bodyStart;
287
+ if (content[from] === '\n') from += 1;
288
+ let to = end;
289
+ if (to > from && content[to - 1] === '\n') to -= 1;
290
+ return content.slice(from, to);
291
+ }
292
+
293
+ function renderedInstructionContent(asset , context , repoRoot ) {
294
+ const source = sourceDirFor(asset, repoRoot);
295
+ const content = fs.readFileSync(source, 'utf8');
296
+ return renderGuidancePointer(content, { scope: context.scope, scopeRoot: context.paths.root }).replace(/\s+$/, '');
297
+ }
298
+
278
299
  function planInstructionsAsset({ assets, context, removing, repoRoot, claimedElsewhere = () => false } ) {
279
300
  const asset = instructionsAsset(assets);
280
301
  const changes = [];
@@ -298,18 +319,19 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
298
319
  return { changes, conflicts };
299
320
  }
300
321
  const source = sourceDirFor(asset, repoRoot);
301
- if (!mergeMarkedSection(source, target, { dryRun: true }).changed) return { changes, conflicts };
302
- const content = fs.readFileSync(source, 'utf8').replace(/\s+$/, '');
322
+ const content = renderedInstructionContent(asset, context, repoRoot);
323
+ const merged = mergeMarkedSection(source, target, { dryRun: true, content });
324
+ if (!merged.changed) return { changes, conflicts };
303
325
  changes.push({ assetId: asset.id, target, source, operation: fs.existsSync(target) ? 'update' : 'create',
304
326
  ownershipIdentity: 'doflow:codex:instructions:managed-section', kind: 'instructions-section', identity: 'AGENTS.md',
305
- afterFingerprint: sha256Text(content), sourceVersion: 'registry-v1', projection: { renderer: 'codex-agents' } });
327
+ afterFingerprint: sha256Text(content), renderedContent: content, sourceVersion: 'registry-v1', projection: { renderer: 'codex-agents' } });
306
328
  return { changes, conflicts };
307
329
  }
308
330
 
309
331
  function applyInstructionsAsset(changes ) {
310
332
  for (const change of changes) {
311
333
  if (change.projection?.renderer !== 'codex-agents' || change.operation === 'remove') continue;
312
- mergeMarkedSection(change.source, change.target);
334
+ mergeMarkedSection(change.source, change.target, { content: change.renderedContent });
313
335
  }
314
336
  }
315
337
 
@@ -325,11 +347,14 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
325
347
  if (!asset) return { statuses, resources };
326
348
  const target = instructionsPath(context);
327
349
  const existing = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : null;
328
- const managed = existing !== null && existing.includes(MARKER_START) && existing.includes(MARKER_END);
329
- statuses.push({ assetId: asset.id, capability: asset.capability, status: managed ? 'managed' : 'absent', target });
350
+ const expected = renderedInstructionContent(asset, context, repoRoot);
351
+ const actual = instructionBody(existing);
352
+ const hasMarkers = existing !== null && existing.includes(MARKER_START) && existing.includes(MARKER_END);
353
+ const managed = actual !== null && actual === expected;
354
+ statuses.push({ assetId: asset.id, capability: asset.capability, status: managed ? 'managed' : (hasMarkers ? 'missing' : 'absent'), target });
330
355
  if (managed) {
331
356
  resources.push({ assetId: asset.id, target, ownershipIdentity: 'doflow:codex:instructions:managed-section',
332
- kind: 'instructions-section', identity: 'AGENTS.md', fingerprint: sha256Text(existing), sourceVersion: 'registry-v1',
357
+ kind: 'instructions-section', identity: 'AGENTS.md', fingerprint: sha256Text(expected), sourceVersion: 'registry-v1',
333
358
  projection: { renderer: 'codex-agents' } });
334
359
  }
335
360
  return { statuses, resources };
@@ -344,7 +369,7 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
344
369
  const destDir = copyTreeDestDir(codexConfigDir(context), asset);
345
370
  const sourceDir = sourceDirFor(asset, repoRoot);
346
371
  const previousResources = ledgerFileResources(neutralResources, HARNESS, asset.id);
347
- const result = planTree({ sourceDir, destDir, previousResources, siblingFingerprints: ledgerSiblingFingerprints(neutralResources, HARNESS), operation: removing ? 'remove' : 'apply', layout: asset.layout, keepModified: !removing,
372
+ const result = planTree({ sourceDir, destDir, previousResources, siblingFingerprints: ledgerSiblingFingerprints(neutralResources, HARNESS), operation: removing ? 'remove' : 'apply', layout: asset.layout, transform: asset.transform, keepModified: !removing,
348
373
  // `force` is the CLI's --force reaching the one conflict class a plan can actually
349
374
  // downgrade: a ledger-owned destination whose bytes were edited underneath us. With force,
350
375
  // that is drift to heal rather than a refusal — exactly what `doflow reconcile` (always
@@ -362,6 +387,7 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
362
387
  ownershipIdentity: `doflow:codex:copy-tree:${asset.id}:${change.relPath}`,
363
388
  kind: 'copy-tree-file', identity: change.relPath,
364
389
  afterFingerprint: change.fingerprint, fingerprint: change.fingerprint, sourceVersion: sourceVersion ?? 'unknown',
390
+ transformName: asset.transform || null,
365
391
  ...(change.kept ? { retained: true, retainedFor: [] } : {}),
366
392
  projection: { renderer: 'copy-tree' },
367
393
  });
@@ -371,9 +397,18 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
371
397
  }
372
398
 
373
399
  function applyCopyTreeAssets(changes ) {
374
- const treeChanges = changes.filter((change ) => change.projection?.renderer === 'copy-tree' && change.operation !== 'remove')
375
- .map((change ) => ({ relPath: change.identity, target: change.target, source: change.source, operation: change.operation, fingerprint: change.fingerprint }));
376
- return applyTree({ changes: treeChanges }).applied;
400
+ let applied = 0;
401
+ const grouped = new Map();
402
+ for (const change of changes) {
403
+ if (change.projection?.renderer !== 'copy-tree' || change.operation === 'remove') continue;
404
+ const key = change.transformName || null;
405
+ if (!grouped.has(key)) grouped.set(key, []);
406
+ grouped.get(key).push({ relPath: change.identity, target: change.target, source: change.source, operation: change.operation, fingerprint: change.fingerprint });
407
+ }
408
+ for (const [transformName, treeChanges] of grouped) {
409
+ applied += applyTree({ changes: treeChanges, transform: resolveTransform(transformName) }).applied;
410
+ }
411
+ return applied;
377
412
  }
378
413
 
379
414
  function removeCopyTreeAssets(changes ) {
@@ -389,7 +424,7 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
389
424
  for (const asset of copyTreeAssets(assets)) {
390
425
  const destDir = copyTreeDestDir(codexConfigDir(context), asset);
391
426
  const sourceDir = sourceDirFor(asset, repoRoot);
392
- const result = verifyTree({ sourceDir, destDir, layout: asset.layout, plannedWrites });
427
+ const result = verifyTree({ sourceDir, destDir, layout: asset.layout, transform: asset.transform, plannedWrites });
393
428
  conflicts.push(...result.conflicts.map((reason ) => `${asset.id}: ${reason}`));
394
429
  for (const resource of result.resources) {
395
430
  resources.push({
@@ -468,9 +503,22 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
468
503
  hooks = { ...hooks, ok: false, status: 'conflict', changes: [], errors: ['Codex hooks.json was modified outside DoFlow'] };
469
504
  }
470
505
  }
506
+ if (hooks.ok) hooks = withStaleScriptReleases(hooks, scriptRows(neutralResources, context));
471
507
  return hooks;
472
508
  }
473
509
 
510
+ /** A script row for a name the source no longer ships (a retired front door) is released with the
511
+ * plan, so the ledger does not keep a row for a file the update deletes. The script itself goes by
512
+ * its bytes (deployCodexHooks), so this only drops the row. */
513
+ function withStaleScriptReleases(hooks , rows ) {
514
+ const shipped = new Set(hooks.scriptNames);
515
+ const releases = rows.filter((row ) => !shipped.has(row.identity)).map((row ) => ({ type: 'remove', assetId: row.assetId, identity: row.identity,
516
+ target: row.target, ownershipIdentity: row.ownershipIdentity, kind: row.kind, release: true }));
517
+ if (!releases.length) return hooks;
518
+ return { ...hooks, changes: [...hooks.changes, ...releases],
519
+ ...(hooks.status === 'unchanged' ? { status: 'change', scriptsOnly: true } : {}) };
520
+ }
521
+
474
522
  /** Agents/hooks share one removing-vs-planning fork: removal always owns both via the neutral
475
523
  * ledger's own records, regardless of what native input the caller provided this run. */
476
524
  /** A removal drops the rows of the hook scripts too. The scripts themselves go by their bytes
@@ -19,11 +19,15 @@ function normalizeNativeProjection(registry , harness , mcp , cont
19
19
  if (!override || typeof override !== 'object' || Array.isArray(override)) throw new Error(`Projection override for '${harness.id}' must be an object`);
20
20
  const trusted = override.hooks?.trusted;
21
21
  if (trusted !== undefined && typeof trusted !== 'boolean') throw new Error(`Projection override '${harness.id}.hooks.trusted' must be boolean`);
22
+ // Hook tiers (maint-hooks): Codex outside `hooks.harnesses` gets no hooks.json, no hook scripts and no
23
+ // `features.hooks` entry; the lifecycle removes what an earlier install wrote through the remove path.
24
+ const hooksOff = harness.capabilities?.hooks?.tiered === true;
25
+ const configResources = (declaration.config?.resources || []).filter((resource ) => !(hooksOff && resource.identity === 'features.hooks'));
22
26
  return Object.freeze({
23
- configResources: Object.freeze((declaration.config?.resources || []).map((resource ) => Object.freeze({ ...resource }))),
27
+ configResources: Object.freeze(configResources.map((resource ) => Object.freeze({ ...resource }))),
24
28
  mcp: Object.freeze({ catalog: Object.freeze(mcp.map((server ) => Object.freeze({ ...server }))), selected: Object.freeze(mcp.map((server ) => server.id)) }),
25
29
  agents: Object.freeze({ sourceDir: resolveRegistrySource(registry, declaration.agents?.source, `${harness.id} agents`) }),
26
- hooks: Object.freeze({
30
+ hooks: hooksOff ? Object.freeze({}) : Object.freeze({
27
31
  sourceFile: resolveRegistrySource(registry, declaration.hooks?.configSource, `${harness.id} hooks config`),
28
32
  sourceHooksDir: resolveRegistrySource(registry, declaration.hooks?.scriptsSource, `${harness.id} hooks scripts`),
29
33
  trusted: trusted ?? declaration.hooks?.trusted,
@@ -5,28 +5,37 @@
5
5
  // GitHub Copilot CLI adapter.
6
6
  //
7
7
  // Copilot has no single native root the way Claude's `.claude` is one — instructions, skills,
8
- // agents, and MCP each live under a different path, split further by scope. Skills and agents are
9
- // both trees materialised via the shared copy-tree engine in ../copy-tree.ts; the single instruction
10
- // pointer is a marker-merged section in a shared file, the same shape opencode/pi/gemini already use;
11
- // MCP servers are entries of one JSON map that DoFlow owns one at a time through ../mcp-entries.ts,
12
- // never touching an entry it does not own.
8
+ // agents, and MCP each live under a different path, split further by scope. Skills, agents and the
9
+ // core guidance rules are trees materialised via the shared copy-tree engine in ../copy-tree.ts; the
10
+ // single instruction pointer is a marker-merged section in a shared file, the same shape
11
+ // opencode/pi already use; MCP servers are entries of one JSON map that DoFlow owns one at a
12
+ // time through ../mcp-entries.ts, never touching an entry it does not own.
13
13
  //
14
14
  // Native paths (see design.md §4's harness-native-surfaces table):
15
- // instructions -> <projectRoot>/.github/copilot-instructions.md (project only — Copilot documents
16
- // no global instructions file, so global scope installs nothing for this asset)
15
+ // instructions -> <projectRoot>/.github/copilot-instructions.md, the pointer to .doflow/guidance
16
+ // (project only — Copilot CLI also reads a user-level ~/.copilot/copilot-instructions.md,
17
+ // but this adapter does not project a pointer there, so global scope writes none)
18
+ // rules -> <projectRoot>/.github/instructions/ (project) / ~/.copilot/instructions/ (global):
19
+ // the core set (DOFLOW_CORE.md and the files it imports) as `*.instructions.md`
20
+ // with an `applyTo: '**'` header, under instructions/rules/ for the rule files;
21
+ // the full guidance tree stays in .doflow/guidance
17
22
  // skills -> ~/.agents/skills (global) / <projectRoot>/.agents/skills (project)
18
23
  // agents -> ~/.copilot/agents (global) / <projectRoot>/.github/agents (project)
19
24
  // mcp -> ~/.copilot/mcp-config.json (global) / <projectRoot>/.mcp.json (project)
25
+ // hooks -> <projectRoot>/.github/hooks (project) / ~/.copilot/hooks (global): doflow.json plus the
26
+ // doflow-*.sh front doors; the shared bridge tree lands in <root>/.doflow/shared/hooks
20
27
  //
21
28
  // Evidence: https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot,
22
29
  // https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-skills,
23
30
  // https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/create-custom-agents-for-cli,
24
- // https://docs.github.com/en/copilot/reference/custom-agents-configuration
31
+ // https://docs.github.com/en/copilot/reference/custom-agents-configuration,
32
+ // https://docs.github.com/en/copilot/reference/hooks-reference
25
33
  const fs = require('node:fs');
26
34
  const path = require('node:path');
27
35
 
28
36
  const { MARKER_START, MARKER_END } = require('../../helper/marker-merge.js');
29
37
  const { planTree, applyTree, removeTree, verifyTree, copyTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, fingerprint, sourceDirFor, resolveTransform } = require('../copy-tree.js');
38
+ const { renderGuidancePointer } = require('../guidance-pointer.js');
30
39
  const { readMcpFiles, planMcpEntries, verifyMcpEntries, ownedMcpIds, writeMcpEntries } = require('../mcp-entries.js');
31
40
  const { declaredHarnessPaths, resolveHarnessPaths } = require('../../helper/harness-paths.js');
32
41
 
@@ -49,18 +58,31 @@ const LEGACY_MCP_REGISTRATION = `${HARNESS}:mcp:registration`;
49
58
  * list per https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-skills.
50
59
  * - `agentsConfigDir` is genuinely asymmetric: `~/.copilot` globally, `.github` at project scope,
51
60
  * because that is where Copilot's own custom-agents documentation places each.
52
- * - `instruction` is project-only: Copilot documents no global instructions file, so no user-scope
53
- * rule is declared and it resolves to null rather than inventing a path with no reader.
61
+ * - `instruction` is project-only: Copilot CLI reads a user-level ~/.copilot/copilot-instructions.md,
62
+ * but this adapter does not project a pointer there, so no user-scope rule is declared and it
63
+ * resolves to null.
54
64
  * createCopilotAdapter({ declaredPaths }) is the injection point buildAdapterRegistry() uses;
55
65
  * module-level exports delegate to a default-configured instance so direct callers keep their
56
66
  * historical shape.
57
67
  */
58
- function createCopilotAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] } = {}) {
68
+ function createCopilotAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env = process.env } = {}) {
59
69
  // Parity note: Copilot's historical nativePaths() derived even its user-scope root from scopeRoot
60
70
  // (the CLI passes $HOME there for -g installs), never from a separate homeDir argument — so the
61
71
  // resolver is fed no homeDir and "root" keeps resolving exactly as before.
62
72
  function nativePaths({ scope, scopeRoot } = {}) {
63
- return resolveHarnessPaths(declaredPaths, { scope, scopeRoot });
73
+ const paths = resolveHarnessPaths(declaredPaths, { scope, scopeRoot });
74
+ // Copilot CLI loads user hooks from $COPILOT_HOME/hooks when COPILOT_HOME is set (hooks reference,
75
+ // "Hook locations"), and DoFlow's generated commands read it too. The path schema has no
76
+ // environment base (src/helper/harness-paths.js), so the override stays here, as Pi's agent dir
77
+ // does; skills, agents, instructions and MCP keep their declared locations.
78
+ if ((scope === 'user' || scope === 'global') && copilotHomeOverride()) paths.hooksConfigDir = copilotHomeOverride();
79
+ return paths;
80
+ }
81
+
82
+ /** $COPILOT_HOME when it is an absolute path, else null (a relative value would move with the cwd). */
83
+ function copilotHomeOverride() {
84
+ const home = typeof env.COPILOT_HOME === 'string' ? env.COPILOT_HOME.trim() : '';
85
+ return path.isAbsolute(home) ? path.resolve(home) : null;
64
86
  }
65
87
 
66
88
  /** Discovery keeps the MCP files as plan read them (`mcpSnapshot`), which verify compares against,
@@ -81,7 +103,7 @@ function createCopilotAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
81
103
  }
82
104
 
83
105
  /** Replace only the span between DoFlow's markers. A copilot-instructions.md that exists with no
84
- * DoFlow section is refused rather than appended to — the same policy opencode/pi/gemini apply to
106
+ * DoFlow section is refused rather than appended to — the same policy opencode/pi apply to
85
107
  * their own shared instruction files, and for the same reason: where DoFlow's content belongs in a
86
108
  * file it doesn't fully own is a decision for the file's owner, not a guess for the installer. Add
87
109
  * an empty marker pair to opt in. */
@@ -108,9 +130,9 @@ function createCopilotAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
108
130
  }
109
131
 
110
132
  /** Plan the single instructions change (or none). Global scope always yields no change and no
111
- * conflict — Copilot documents no global instructions file, so there is nothing to skip *to*,
112
- * mirroring how other adapters skip a capability with no native equivalent for a given scope. */
113
- function planInstructionsChange({ assets, found, context, removing, fsImpl } ) {
133
+ * conflict — the user-level ~/.copilot/copilot-instructions.md is not projected, so there is no
134
+ * global target, the same way other adapters skip a capability they do not project at a scope. */
135
+ function planInstructionsChange({ assets, found, scope, scopeRoot, context, removing, fsImpl } ) {
114
136
  if (!found.paths.instruction) return { changes: [], conflicts: [] };
115
137
  // The marker-managed instruction file is one specific asset: exclude tree-projected rule
116
138
  // assets (copilot-rule-instructions), whose `source` is a directory, not a file.
@@ -118,7 +140,7 @@ function createCopilotAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
118
140
  if (!guidance) return { changes: [], conflicts: [] };
119
141
  const outcome = removing
120
142
  ? { ok: true, operation: 'remove', content: strippedInstruction(found.instruction) }
121
- : managedInstruction(found.instruction, render({ content: fsImpl.readFileSync(path.resolve(context.repoRoot, guidance.source), 'utf8') }));
143
+ : managedInstruction(found.instruction, render({ content: renderGuidancePointer(fsImpl.readFileSync(path.resolve(context.repoRoot, guidance.source), 'utf8'), { scope, scopeRoot }) }));
122
144
  if (!outcome.ok) return { changes: [], conflicts: [outcome.conflict] };
123
145
  if (outcome.operation === 'none' || outcome.content === null) return { changes: [], conflicts: [] };
124
146
  return {
@@ -138,7 +160,7 @@ function createCopilotAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
138
160
  * (https://docs.github.com/en/copilot/reference/custom-agents-configuration) requires the
139
161
  * `.agent.md` extension, while `core/shared/agent-specs/*.md` — already `name`/`description`
140
162
  * YAML-frontmatter files, the same shape `.agent.md` expects — carries a plain `.md` extension.
141
- * This is the same class of shape mismatch Gemini/Antigravity's `dir-per-file:agent.md` layout in
163
+ * This is the same class of shape mismatch Antigravity's `dir-per-file:agent.md` layout in
142
164
  * copy-tree.ts already solves for a directory-per-agent requirement; here the mismatch is only the
143
165
  * extension, so a flat rename is enough and no directory restructuring is needed. Declared as a
144
166
  * function (rather than a name registered in copy-tree.ts's LAYOUTS) because it is specific to this
@@ -160,7 +182,7 @@ function createCopilotAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
160
182
  const destDir = copyTreeDestDir(destRoot, asset);
161
183
  const sourceDir = sourceDirFor(asset, context, fsImpl, 'Copilot');
162
184
  const previousResources = ledgerFileResources(ledger?.resources, HARNESS, asset.id);
163
- const result = planTree({ sourceDir, destDir, previousResources, siblingFingerprints: ledgerSiblingFingerprints(ledger?.resources, HARNESS), operation: removing ? 'remove' : 'apply', fsImpl, layout: layout || asset.layout, transform: asset.transform, keepModified: !removing,
185
+ const result = planTree({ sourceDir, destDir, previousResources, siblingFingerprints: ledgerSiblingFingerprints(ledger?.resources, HARNESS), operation: removing ? 'remove' : 'apply', fsImpl, layout: layout || asset.layout, transform: asset.transform, select: asset.select, keepModified: !removing,
164
186
  // Forwarded so the CLI's --force reaches planTree's conflict check; omitting it let
165
187
  // planTree's own `force = false` default stand in silently. Gated on `!removing` for the
166
188
  // reason codex/index.ts states in full: force heals drift on apply, but a hand-edited file
@@ -214,7 +236,7 @@ function createCopilotAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
214
236
  for (const asset of treeAssetsFor(assets, renderer)) {
215
237
  const destDir = copyTreeDestDir(destRoot, asset);
216
238
  const sourceDir = sourceDirFor(asset, context, fsImpl, 'Copilot');
217
- const result = verifyTree({ sourceDir, destDir, fsImpl, layout: layout || asset.layout, transform: asset.transform, plannedWrites: context?.plannedWrites });
239
+ const result = verifyTree({ sourceDir, destDir, fsImpl, layout: layout || asset.layout, transform: asset.transform, select: asset.select, plannedWrites: context?.plannedWrites });
218
240
  conflicts.push(...result.conflicts.map((reason ) => `${asset.id}: ${reason}`));
219
241
  for (const resource of result.resources) {
220
242
  resources.push({
@@ -268,7 +290,7 @@ function createCopilotAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
268
290
  /** Copilot's MCP entry changes have no asset of their own in core/registry/assets.json (they are a
269
291
  * registration side-effect of the selected MCP servers, not a projected asset), so they piggyback
270
292
  * on a real asset id already routed to this harness — the same "pseudo-component" technique
271
- * src/adapters/gemini/index.js#hooksAssetId and src/adapters/kiro/index.js#pseudoAssetId use. The
293
+ * src/adapters/kiro/index.js#pseudoAssetId uses. The
272
294
  * lifecycle layer validates every change's assetId against the harness's actual asset list, so an
273
295
  * invented id like a literal 'copilot.mcp' string is rejected as unknown. */
274
296
  function pseudoAssetId(assets ) {
@@ -283,7 +305,7 @@ function createCopilotAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
283
305
  const conflicts = [];
284
306
  const removing = context.operation === 'remove';
285
307
 
286
- const instructions = planInstructionsChange({ assets, found, context, removing, fsImpl });
308
+ const instructions = planInstructionsChange({ assets, found, scope, scopeRoot, context, removing, fsImpl });
287
309
  changes.push(...instructions.changes);
288
310
  conflicts.push(...instructions.conflicts);
289
311
 
@@ -299,13 +321,21 @@ function createCopilotAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
299
321
  changes.push(...rules.changes);
300
322
  conflicts.push(...rules.conflicts);
301
323
 
324
+ // Hooks: DoFlow's own .github/hooks/doflow.json and doflow-*.sh front doors (renderer
325
+ // copilot-hooks, a flat tree beside any hook files the user keeps there), and the shared bridge
326
+ // tree under ../.doflow, which hooks.shared projects through the plain copy-tree renderer above.
327
+ // Outside `hooks.harnesses` the registry marks the capability unavailable and selects neither.
328
+ const hooks = planTreeAssets({ assets, renderer: 'copilot-hooks', scopeRoot, destRoot: found.paths.hooksConfigDir, layout: null, context, ledger, removing, fsImpl });
329
+ changes.push(...hooks.changes);
330
+ conflicts.push(...hooks.conflicts);
331
+
302
332
  const mcpPlan = planMcpEntries({
303
333
  ...mcpEntriesInput({ paths: found.paths, mcp, mcpAdoptable, ledger, assets }), files: found.mcpSnapshot, removing,
304
334
  });
305
335
  changes.push(...legacyRegistrationReleases(ledger), ...mcpPlan.changes);
306
336
  conflicts.push(...mcpPlan.conflicts);
307
337
 
308
- return { changes, conflicts, notices: [...skills.notices, ...agents.notices, ...rules.notices, ...mcpPlan.notices], paths: found.paths };
338
+ return { changes, conflicts, notices: [...skills.notices, ...agents.notices, ...rules.notices, ...hooks.notices, ...mcpPlan.notices], paths: found.paths };
309
339
  }
310
340
 
311
341
  function writeChange(change , fsImpl ) {
@@ -380,6 +410,11 @@ function createCopilotAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
380
410
  statuses.push(...rules.statuses);
381
411
  conflicts.push(...rules.conflicts);
382
412
 
413
+ const hooks = verifyTreeAssets({ assets, renderer: 'copilot-hooks', destRoot: found.paths.hooksConfigDir, layout: null, context, fsImpl });
414
+ resources.push(...hooks.resources);
415
+ statuses.push(...hooks.statuses);
416
+ conflicts.push(...hooks.conflicts);
417
+
383
418
  const mcpResult = verifyMcpEntries({
384
419
  ...mcpEntriesInput({ paths: found.paths, mcp, mcpAdoptable, ledger, assets }), files: found.mcpSnapshot,
385
420
  snapshot: discovery?.mcpSnapshot ?? found.mcpSnapshot, removing: (operation ?? context.operation) === 'remove',