@khoavu882/doflow 2.0.0-beta.4 → 2.0.0

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 (497) hide show
  1. package/README.md +12 -11
  2. package/bin/doflow.js +21 -11
  3. package/core/.antigravity-plugin/plugin.json +1 -3
  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 +7 -17
  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-bash-guard.sh +7 -17
  15. package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
  16. package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
  17. package/core/harnesses/claude/hooks/session-end.sh +23 -10
  18. package/core/harnesses/claude/hooks/session-start.sh +5 -3
  19. package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
  20. package/core/harnesses/claude/hooks/stop-check.sh +5 -3
  21. package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
  22. package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
  23. package/core/harnesses/claude/settings/settings.json +18 -0
  24. package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
  25. package/core/harnesses/codex/agents/core-implementer.toml +1 -0
  26. package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
  27. package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
  28. package/core/harnesses/codex/agents/research-writer.toml +1 -1
  29. package/core/harnesses/codex/agents/reviewer.toml +10 -0
  30. package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
  31. package/core/harnesses/codex/agents/system-architect.toml +1 -1
  32. package/core/harnesses/codex/hooks/lib.sh +44 -25
  33. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
  34. package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
  35. package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
  36. package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
  37. package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
  38. package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
  39. package/core/harnesses/codex/hooks/session-start.sh +5 -3
  40. package/core/harnesses/codex/hooks/stop-check.sh +6 -5
  41. package/core/harnesses/codex/hooks/subagent-audit.sh +18 -6
  42. package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
  43. package/core/harnesses/copilot/hooks/doflow-lib.sh +93 -0
  44. package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
  45. package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +9 -0
  46. package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
  47. package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
  48. package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
  49. package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
  50. package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
  51. package/core/harnesses/copilot/hooks/doflow.json +57 -0
  52. package/core/harnesses/kiro/agents/doflow.json +1 -1
  53. package/core/harnesses/kiro/hooks/lib.sh +26 -6
  54. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +21 -5
  55. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +21 -5
  56. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +21 -5
  57. package/core/harnesses/kiro/hooks/session-start.sh +12 -5
  58. package/core/harnesses/kiro/hooks/stop-check.sh +22 -30
  59. package/core/harnesses/pi/extensions/doflow-guard.js +167 -0
  60. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -12
  61. package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
  62. package/core/harnesses/shared/hooks/bridge/bridge.sh +277 -0
  63. package/core/harnesses/shared/hooks/bridge/envelope.jq +129 -0
  64. package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
  65. package/core/harnesses/shared/hooks/bridge/respond.sh +70 -0
  66. package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +28 -14
  67. package/core/harnesses/shared/hooks/policies/capture-failure.sh +51 -16
  68. package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
  69. package/core/harnesses/shared/hooks/policies/contract.sh +78 -0
  70. package/core/harnesses/shared/hooks/policies/deny-json.sh +35 -9
  71. package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
  72. package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
  73. package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
  74. package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
  75. package/core/harnesses/shared/hooks/policies/lib.sh +65 -40
  76. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +48 -35
  77. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +284 -33
  78. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +180 -110
  79. package/core/harnesses/shared/hooks/policies/session-context.sh +66 -7
  80. package/core/harnesses/shared/hooks/policies/stop-check.sh +61 -23
  81. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +39 -6
  82. package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
  83. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +97 -159
  84. package/core/harnesses/shared/hooks/stream-hook-runner.js +114 -187
  85. package/core/harnesses/shared/locator/doflow-run +2 -3
  86. package/core/registry/advice.json +3 -3
  87. package/core/registry/assets.json +72 -80
  88. package/core/registry/capabilities.json +12 -0
  89. package/core/registry/contracts.json +60 -33
  90. package/core/registry/contribution-points.json +32 -0
  91. package/core/registry/gates.json +32 -0
  92. package/core/registry/guidance-caps.json +40 -0
  93. package/core/registry/harnesses.json +1542 -277
  94. package/core/registry/lifecycle.json +2837 -37
  95. package/core/registry/mcp.json +5 -1
  96. package/core/registry/models.json +7 -1
  97. package/core/registry/package-templates/README.md +19 -0
  98. package/core/registry/package-templates/agent/files/agent.md +9 -0
  99. package/core/registry/package-templates/agent/template.json +4 -0
  100. package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
  101. package/core/registry/package-templates/capability-provider/template.json +11 -0
  102. package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
  103. package/core/registry/package-templates/debate-preset/template.json +4 -0
  104. package/core/registry/package-templates/review-preset/files/preset.json +30 -0
  105. package/core/registry/package-templates/review-preset/template.json +4 -0
  106. package/core/registry/package-templates/skill/files/SKILL.md +8 -0
  107. package/core/registry/package-templates/skill/template.json +4 -0
  108. package/core/registry/package-templates/worker/files/worker.json +92 -0
  109. package/core/registry/package-templates/worker/template.json +11 -0
  110. package/core/registry/packages/doflow/doflow-package.json +15 -0
  111. package/core/registry/packages/doflow/presets/debate-3.json +26 -0
  112. package/core/registry/protocols.json +61 -0
  113. package/core/registry/routes.json +17 -0
  114. package/core/registry/rules.json +431 -0
  115. package/core/registry/settings.json +276 -0
  116. package/core/registry/storage.json +701 -30
  117. package/core/registry/telemetry.json +236 -0
  118. package/core/registry/transcripts.json +142 -0
  119. package/core/registry/verification.json +76 -1
  120. package/core/registry/workflows.json +55 -0
  121. package/core/shared/agent-specs/core-implementer.md +3 -0
  122. package/core/shared/agent-specs/delivery-lead.md +68 -0
  123. package/core/shared/agent-specs/quality-guardian.md +5 -1
  124. package/core/shared/agent-specs/research-writer.md +10 -3
  125. package/core/shared/agent-specs/reviewer.md +53 -0
  126. package/core/shared/agent-specs/spec-analyst.md +5 -6
  127. package/core/shared/agent-specs/system-architect.md +5 -1
  128. package/core/shared/guidance/DOFLOW_CORE.md +1 -5
  129. package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
  130. package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
  131. package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
  132. package/core/shared/guidance/pointers/codex.md +1 -4
  133. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
  134. package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
  135. package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
  136. package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
  137. package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
  138. package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
  139. package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
  140. package/core/shared/guidance/references/ORCHESTRATION.md +1 -2
  141. package/core/shared/guidance/rules/RULES.md +45 -0
  142. package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
  143. package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
  144. package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
  145. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
  146. package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
  147. package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
  148. package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
  149. package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
  150. package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
  151. package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
  152. package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
  153. package/core/shared/scripts/doflow/bash/validate-artifacts.sh +48 -4
  154. package/core/shared/scripts/doflow/bin/doflow-run +154 -35
  155. package/core/shared/skills/do/SKILL.md +12 -10
  156. package/core/shared/skills/do/references/advice_calls.md +26 -0
  157. package/core/shared/skills/do/references/pm_routing.md +1 -2
  158. package/core/shared/skills/do/references/program_mode.md +31 -0
  159. package/core/shared/skills/do-brainstorm/SKILL.md +20 -74
  160. package/core/shared/skills/do-brainstorm/references/handoff_branches.md +35 -0
  161. package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
  162. package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
  163. package/core/shared/skills/do-code-review/SKILL.md +41 -144
  164. package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
  165. package/core/shared/skills/do-code-review/references/tools.md +90 -0
  166. package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
  167. package/core/shared/skills/do-design/SKILL.md +24 -23
  168. package/core/shared/skills/do-diagnose/SKILL.md +14 -4
  169. package/core/shared/skills/do-document/SKILL.md +11 -1
  170. package/core/shared/skills/do-document/references/deep_research.md +1 -2
  171. package/core/shared/skills/do-document/references/embedded-research.md +18 -6
  172. package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
  173. package/core/shared/skills/do-execute-plan/SKILL.md +17 -59
  174. package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
  175. package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
  176. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +28 -1
  177. package/core/shared/skills/do-execute-plan/references/run_position.md +13 -1
  178. package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
  179. package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
  180. package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
  181. package/core/shared/skills/do-flow/SKILL.md +8 -9
  182. package/core/shared/skills/do-flow/references/task_classes.md +2 -2
  183. package/core/shared/skills/do-git/SKILL.md +4 -0
  184. package/core/shared/skills/do-implement/SKILL.md +7 -9
  185. package/core/shared/skills/do-plan/SKILL.md +29 -19
  186. package/core/shared/skills/do-test/SKILL.md +7 -4
  187. package/core/shared/templates/doflow/constitution-template.md +9 -3
  188. package/core/shared/templates/doflow/data-model-template.md +1 -1
  189. package/core/shared/templates/doflow/design-template.md +113 -16
  190. package/core/shared/templates/doflow/plan-template.md +11 -10
  191. package/core/shared/templates/doflow/question-log-template.md +14 -2
  192. package/core/shared/templates/doflow/requirement-template.md +1 -1
  193. package/core/worker-evidence/antigravity/checks.json +14 -0
  194. package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
  195. package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
  196. package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
  197. package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
  198. package/core/worker-evidence/codex/checks.json +38 -0
  199. package/package.json +10 -6
  200. package/src/adapters/antigravity/index.js +226 -113
  201. package/src/adapters/claude/index.js +39 -5
  202. package/src/adapters/codex/agents.js +25 -15
  203. package/src/adapters/codex/hooks.js +40 -5
  204. package/src/adapters/codex/index.js +100 -22
  205. package/src/adapters/codex/native-projection.js +6 -2
  206. package/src/adapters/copilot/index.js +58 -23
  207. package/src/adapters/copy-tree.js +294 -48
  208. package/src/adapters/guidance-manifest.js +71 -0
  209. package/src/adapters/guidance-pointer.js +19 -0
  210. package/src/adapters/hook-commands.js +6 -10
  211. package/src/adapters/index.js +1 -0
  212. package/src/adapters/kiro/index.js +38 -13
  213. package/src/adapters/opencode/index.js +8 -8
  214. package/src/adapters/pi/hooks.js +120 -0
  215. package/src/adapters/pi/index.js +48 -16
  216. package/src/adapters/resolve-source.js +26 -2
  217. package/src/cli/commands/install.js +3 -2
  218. package/src/cli/commands/reconcile.js +53 -9
  219. package/src/cli/commands/remove.js +2 -2
  220. package/src/cli/commands/rollback.js +6 -3
  221. package/src/cli/commands/self-update.js +18 -9
  222. package/src/cli/commands/status.js +21 -2
  223. package/src/cli/commands/ui.js +99 -0
  224. package/src/cli/commands/update.js +3 -2
  225. package/src/cli/index.js +338 -33
  226. package/src/cli/runtime-commands.js +243 -52
  227. package/src/cli/shared.js +0 -2
  228. package/src/helper/git.js +1 -1
  229. package/src/helper/guardrails.js +1 -1
  230. package/src/helper/marker-merge.js +31 -13
  231. package/src/helper/package-ids.js +37 -0
  232. package/src/helper/semver.js +94 -0
  233. package/src/install/backup.js +105 -15
  234. package/src/install/context.js +1 -1
  235. package/src/install/targets.js +31 -16
  236. package/src/lifecycle/coverage.js +74 -0
  237. package/src/lifecycle/hook-step.js +61 -0
  238. package/src/lifecycle/hook-tier.js +160 -0
  239. package/src/lifecycle/index.js +434 -86
  240. package/src/lifecycle/retired-harnesses.js +318 -0
  241. package/src/lifecycle/view.js +35 -15
  242. package/src/registry/index.js +686 -32
  243. package/src/registry/worker-ids.js +45 -0
  244. package/src/release/build-package.js +0 -3
  245. package/src/release/docs-blocks.js +492 -0
  246. package/src/release/preflight.js +425 -0
  247. package/src/release/rehearse.js +401 -0
  248. package/src/release/release-workflow.js +733 -0
  249. package/src/release/sync-plugin-versions.js +43 -7
  250. package/src/runtime/advice/bench-gate.js +25 -0
  251. package/src/runtime/advice/cli.js +59 -16
  252. package/src/runtime/advice/client.js +32 -9
  253. package/src/runtime/advice/config.js +98 -133
  254. package/src/runtime/advice/doctor.js +1 -1
  255. package/src/runtime/advice/elevation.js +97 -0
  256. package/src/runtime/advice/endpoint-guard.js +9 -3
  257. package/src/runtime/advice/mode.js +37 -0
  258. package/src/runtime/advice/record.js +18 -5
  259. package/src/runtime/advice/registry.js +4 -1
  260. package/src/runtime/advice/shadow-config.js +229 -0
  261. package/src/runtime/advice/shadow-dispatcher.js +276 -0
  262. package/src/runtime/brief-rules.js +81 -0
  263. package/src/runtime/brief-tools.js +111 -34
  264. package/src/runtime/candidate.js +514 -0
  265. package/src/runtime/capabilities/executor.js +450 -0
  266. package/src/runtime/capabilities/loopback.js +41 -0
  267. package/src/runtime/capabilities/sanitizer.js +88 -0
  268. package/src/runtime/capability-router.js +135 -3
  269. package/src/runtime/checkouts.js +73 -11
  270. package/src/runtime/claims.js +27 -12
  271. package/src/runtime/cli-result.js +11 -4
  272. package/src/runtime/cli.js +221 -277
  273. package/src/runtime/config-home.js +42 -0
  274. package/src/runtime/context/arbitrate.js +767 -0
  275. package/src/runtime/context/budget.js +203 -0
  276. package/src/runtime/context/compose.js +1367 -0
  277. package/src/runtime/context/delivery-store.js +100 -0
  278. package/src/runtime/context/emission-rules.js +118 -0
  279. package/src/runtime/context/identity.js +227 -0
  280. package/src/runtime/context/mandatory.js +594 -0
  281. package/src/runtime/context/pointer.js +142 -0
  282. package/src/runtime/context/primary-command.js +117 -0
  283. package/src/runtime/context/primary.js +344 -0
  284. package/src/runtime/context/summary-store.js +655 -0
  285. package/src/runtime/context-pack.js +1106 -245
  286. package/src/runtime/decision-register.js +13 -4
  287. package/src/runtime/dispatch/admission.js +392 -0
  288. package/src/runtime/dispatch/attempt-end.js +274 -0
  289. package/src/runtime/dispatch/command.js +628 -0
  290. package/src/runtime/dispatch/controls.js +904 -0
  291. package/src/runtime/dispatch/envelope.js +276 -0
  292. package/src/runtime/dispatch/grants.js +310 -0
  293. package/src/runtime/dispatch/integrate.js +617 -0
  294. package/src/runtime/dispatch/loop.js +1835 -0
  295. package/src/runtime/dispatch/store.js +352 -0
  296. package/src/runtime/dispatch/verify-run.js +201 -0
  297. package/src/runtime/evidence-fields.js +39 -0
  298. package/src/runtime/evidence-ledger.js +41 -7
  299. package/src/runtime/failure/capture.js +3 -1
  300. package/src/runtime/failure/cli.js +25 -9
  301. package/src/runtime/failure/home.js +57 -30
  302. package/src/runtime/failure/store.js +7 -2
  303. package/src/runtime/feature-pointer.js +174 -0
  304. package/src/runtime/feature-resolve.js +13 -2
  305. package/src/runtime/fleet/command.js +77 -0
  306. package/src/runtime/fleet/projects.js +178 -0
  307. package/src/runtime/fleet/tree.js +383 -0
  308. package/src/runtime/freshness.js +77 -10
  309. package/src/runtime/git-memo.js +46 -0
  310. package/src/runtime/guidance/ablation.js +427 -0
  311. package/src/runtime/health.js +565 -17
  312. package/src/runtime/history-compactor.js +13 -3
  313. package/src/runtime/implementation-gate.js +277 -22
  314. package/src/runtime/inventory/coverage.js +1 -1
  315. package/src/runtime/inventory/index.js +1 -1
  316. package/src/runtime/inventory/precedence.js +2 -2
  317. package/src/runtime/inventory/siblings.js +6 -4
  318. package/src/runtime/knowledge/code-corpus.js +277 -0
  319. package/src/runtime/knowledge/code-lexical.js +130 -0
  320. package/src/runtime/knowledge/code-retrieval.js +307 -0
  321. package/src/runtime/knowledge/code-validation.js +106 -0
  322. package/src/runtime/knowledge/gp3-gate.js +74 -0
  323. package/src/runtime/knowledge/graph-adapter.js +288 -0
  324. package/src/runtime/knowledge/graph-fallback.js +394 -0
  325. package/src/runtime/knowledge/hybrid.js +136 -8
  326. package/src/runtime/knowledge/retrieval.js +34 -1
  327. package/src/runtime/knowledge/rtk-compress.js +77 -0
  328. package/src/runtime/knowledge/semble-bridge.py +106 -0
  329. package/src/runtime/knowledge/semble-lifecycle.js +114 -0
  330. package/src/runtime/knowledge/semble-provider.js +365 -0
  331. package/src/runtime/lifecycle/cli.js +16 -6
  332. package/src/runtime/lifecycle/event-store.js +8 -6
  333. package/src/runtime/lifecycle/followup.js +6 -1
  334. package/src/runtime/lifecycle/overview.js +24 -1
  335. package/src/runtime/lifecycle/report-store.js +8 -10
  336. package/src/runtime/lifecycle/retention.js +95 -31
  337. package/src/runtime/lifecycle/root.js +9 -1
  338. package/src/runtime/lifecycle/status.js +34 -17
  339. package/src/runtime/lifecycle/store-upkeep.js +56 -7
  340. package/src/runtime/mask.js +134 -7
  341. package/src/runtime/mcp-consultations.js +40 -6
  342. package/src/runtime/model-router.js +79 -7
  343. package/src/runtime/models/catalogue.js +191 -0
  344. package/src/runtime/models/command.js +153 -0
  345. package/src/runtime/off-words.js +12 -0
  346. package/src/runtime/outcome.js +3 -3
  347. package/src/runtime/packages/command.js +1009 -0
  348. package/src/runtime/packages/contributions.js +116 -0
  349. package/src/runtime/packages/digest.js +132 -0
  350. package/src/runtime/packages/fetch.js +182 -0
  351. package/src/runtime/packages/grants.js +192 -0
  352. package/src/runtime/packages/index-file.js +134 -0
  353. package/src/runtime/packages/instruction.js +90 -0
  354. package/src/runtime/packages/manifest.js +331 -0
  355. package/src/runtime/packages/points.js +477 -0
  356. package/src/runtime/packages/programs.js +216 -0
  357. package/src/runtime/packages/scaffold.js +277 -0
  358. package/src/runtime/packages/source.js +90 -0
  359. package/src/runtime/packages/state.js +164 -0
  360. package/src/runtime/packages/store.js +449 -0
  361. package/src/runtime/packages/tar.js +106 -0
  362. package/src/runtime/packages/view.js +406 -0
  363. package/src/runtime/plan-check.js +59 -0
  364. package/src/runtime/plan-content.js +324 -0
  365. package/src/runtime/policy-init.js +118 -0
  366. package/src/runtime/profiles/doctor.js +96 -0
  367. package/src/runtime/profiles/resolve.js +244 -0
  368. package/src/runtime/profiles/rollback.js +254 -0
  369. package/src/runtime/profiles/sampler.js +41 -0
  370. package/src/runtime/readiness-record.js +16 -11
  371. package/src/runtime/readiness.js +127 -16
  372. package/src/runtime/research-request.js +50 -25
  373. package/src/runtime/retrieval-plan.js +402 -73
  374. package/src/runtime/retrieval-record.js +293 -0
  375. package/src/runtime/review/command.js +246 -0
  376. package/src/runtime/review/dominance.js +68 -0
  377. package/src/runtime/review/findings.js +434 -0
  378. package/src/runtime/review/identity.js +255 -0
  379. package/src/runtime/review/protocol-command.js +109 -0
  380. package/src/runtime/review/protocols.js +405 -0
  381. package/src/runtime/review/resolve-protocol.js +302 -0
  382. package/src/runtime/review/start.js +530 -0
  383. package/src/runtime/roadmap/facts.js +479 -0
  384. package/src/runtime/roadmap/gates.js +270 -0
  385. package/src/runtime/roadmap/loop.js +1636 -0
  386. package/src/runtime/roadmap/merge.js +381 -0
  387. package/src/runtime/roadmap/rows.js +1113 -0
  388. package/src/runtime/roadmap/seed.js +951 -0
  389. package/src/runtime/rules-report.js +336 -0
  390. package/src/runtime/scaffold/artifacts.js +63 -9
  391. package/src/runtime/scaffold/generate.js +21 -9
  392. package/src/runtime/settings/autopilot.js +106 -0
  393. package/src/runtime/settings/cli.js +280 -0
  394. package/src/runtime/settings/config-write.js +314 -0
  395. package/src/runtime/settings/files.js +271 -0
  396. package/src/runtime/settings/legacy.js +154 -0
  397. package/src/runtime/settings/lexical.js +56 -0
  398. package/src/runtime/settings/prepared-run.js +269 -0
  399. package/src/runtime/settings/project-layers.js +118 -0
  400. package/src/runtime/settings/refs.js +288 -0
  401. package/src/runtime/settings/registry.js +553 -0
  402. package/src/runtime/settings/resolve.js +713 -0
  403. package/src/runtime/settings/shared.js +37 -0
  404. package/src/runtime/settings/worker-billing.js +107 -0
  405. package/src/runtime/settings/write.js +269 -0
  406. package/src/runtime/slot-command.js +78 -0
  407. package/src/runtime/slots.js +671 -0
  408. package/src/runtime/source-version.js +290 -0
  409. package/src/runtime/state-root.js +217 -0
  410. package/src/runtime/storage/cli.js +12 -5
  411. package/src/runtime/storage/engine.js +9 -2
  412. package/src/runtime/storage/registry.js +14 -1
  413. package/src/runtime/storage/roots.js +66 -16
  414. package/src/runtime/storage/rules.js +82 -10
  415. package/src/runtime/storage/upkeep.js +10 -2
  416. package/src/runtime/storage/work-status.js +245 -26
  417. package/src/runtime/task-attempts.js +589 -69
  418. package/src/runtime/task-classifier.js +47 -11
  419. package/src/runtime/task-graph.js +373 -0
  420. package/src/runtime/task-run.js +778 -0
  421. package/src/runtime/task-scope.js +202 -56
  422. package/src/runtime/task-sources.js +147 -0
  423. package/src/runtime/task-state.js +440 -65
  424. package/src/runtime/team/admission.js +301 -0
  425. package/src/runtime/team/envelope.js +324 -0
  426. package/src/runtime/team/review-guard.js +156 -0
  427. package/src/runtime/trace/analyze.js +427 -0
  428. package/src/runtime/trace/baseline.js +770 -0
  429. package/src/runtime/trace/ledger.js +63 -140
  430. package/src/runtime/trace/otlp-export.js +568 -0
  431. package/src/runtime/trace/otlp-map.js +481 -0
  432. package/src/runtime/trace/spans.js +745 -0
  433. package/src/runtime/trace/telemetry.js +225 -0
  434. package/src/runtime/trace/views.js +1 -0
  435. package/src/runtime/transcripts/command.js +98 -0
  436. package/src/runtime/transcripts/cursor.js +326 -0
  437. package/src/runtime/transcripts/read.js +126 -0
  438. package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
  439. package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
  440. package/src/runtime/transcripts/readers/copilot-events.js +90 -0
  441. package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
  442. package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
  443. package/src/runtime/transcripts/registry.js +161 -0
  444. package/src/runtime/transcripts/stats.js +161 -0
  445. package/src/runtime/verification/check-key.js +628 -0
  446. package/src/runtime/verification/contract-runner.js +546 -11
  447. package/src/runtime/verification/engine.js +499 -9
  448. package/src/runtime/verification/impact.js +617 -0
  449. package/src/runtime/verification/inflight.js +205 -0
  450. package/src/runtime/verification/registry.js +176 -0
  451. package/src/runtime/verification/reuse.js +184 -0
  452. package/src/runtime/verification/telemetry-emit.js +254 -0
  453. package/src/runtime/verification/test-selection.js +67 -7
  454. package/src/runtime/worker/active.js +90 -0
  455. package/src/runtime/worker/cancel.js +222 -0
  456. package/src/runtime/worker/catalogue.js +122 -0
  457. package/src/runtime/worker/command.js +677 -0
  458. package/src/runtime/worker/evidence.js +288 -0
  459. package/src/runtime/worker/launch.js +288 -0
  460. package/src/runtime/worker/models.js +137 -0
  461. package/src/runtime/worker/parsers.js +218 -0
  462. package/src/runtime/worker/probes/index.js +706 -0
  463. package/src/runtime/worker/process-tree.js +415 -0
  464. package/src/runtime/worker/quota.js +43 -0
  465. package/src/runtime/worker/readiness.js +713 -0
  466. package/src/runtime/worker/run-store.js +166 -0
  467. package/src/runtime/worker/supervisor.js +541 -0
  468. package/src/runtime/worker/worktree-check.js +252 -0
  469. package/src/runtime/workflow-engine.js +5 -1
  470. package/src/runtime/workflow-orchestrator.js +578 -63
  471. package/src/runtime/worktree.js +149 -22
  472. package/src/state/index.js +1 -1
  473. package/src/state/lockfile.js +64 -10
  474. package/src/state/run-lock.js +1 -1
  475. package/types/node-commonjs.d.ts +106 -7
  476. package/core/.antigravity-plugin/hooks.json +0 -39
  477. package/core/harnesses/gemini/hooks/hooks.json +0 -77
  478. package/core/harnesses/gemini/hooks/lib.sh +0 -302
  479. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -21
  480. package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
  481. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -21
  482. package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
  483. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -22
  484. package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
  485. package/core/harnesses/gemini/hooks/session-end.sh +0 -5
  486. package/core/harnesses/gemini/hooks/session-start.sh +0 -5
  487. package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
  488. package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
  489. package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
  490. package/core/harnesses/shared/hooks/hooks.json +0 -47
  491. package/core/shared/guidance/FLAGS.md +0 -25
  492. package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
  493. package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
  494. package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
  495. package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
  496. package/src/adapters/gemini/hooks.js +0 -210
  497. package/src/adapters/gemini/index.js +0 -390
@@ -183,6 +183,24 @@ function createClaudeAdapter({ declaredPaths = declaredHarnessPaths().claude } =
183
183
  function settingsContent(fileName , raw , scope ) {
184
184
  return fileName === 'settings.json' && scope !== 'global' ? raw.split(GLOBAL_HOOK_PREFIX).join(PROJECT_HOOK_PREFIX) : raw;
185
185
  }
186
+ /** Hook tiers (maint-hooks): with Claude outside `hooks.harnesses` the managed settings carry no
187
+ * `hooks` block. Returns the managed text to write and the hooks it left out, which a merge strips
188
+ * from the live file so an earlier install's entries do not stay behind. */
189
+ function tieredSettings(fileName , raw , context ) {
190
+ if (fileName !== 'settings.json' || context?.hooksOff !== true) return { raw, droppedHooks: null };
191
+ let parsed;
192
+ try { parsed = JSON.parse(raw); } catch { return { raw, droppedHooks: null }; }
193
+ if (!parsed || parsed.hooks === undefined) return { raw, droppedHooks: null };
194
+ const { hooks, ...rest } = parsed;
195
+ return { raw: serializeSettings(rest), droppedHooks: hooks };
196
+ }
197
+ /** The live settings without DoFlow's own entries of `hooks`; every other key is left alone. */
198
+ function withoutManagedHooks(current , hooks ) {
199
+ if (!hooks || !current || typeof current !== 'object' || current.hooks === undefined) return current;
200
+ const { hooks: liveHooks, ...rest } = current;
201
+ const stripped = stripManagedSettings({ hooks: liveHooks, _: true }, { hooks });
202
+ return stripped?.hooks ? { ...rest, hooks: stripped.hooks } : rest;
203
+ }
186
204
  function settingsLedgerResources(ledger , assetId ) {
187
205
  const byFile = new Map();
188
206
  for (const resource of ledger?.resources || []) {
@@ -242,7 +260,8 @@ function createClaudeAdapter({ declaredPaths = declaredHarnessPaths().claude } =
242
260
  for (const fileName of SETTINGS_FILES) {
243
261
  const sourceAbs = path.join(sourceDir, fileName);
244
262
  if (!fs.existsSync(sourceAbs)) continue;
245
- let managedRaw = settingsContent(fileName, fs.readFileSync(sourceAbs, 'utf8'), scope);
263
+ const tiered = tieredSettings(fileName, settingsContent(fileName, fs.readFileSync(sourceAbs, 'utf8'), scope), context);
264
+ let managedRaw = tiered.raw;
246
265
  if (fileName === 'settings.json' && context.statusline && !removing) {
247
266
  // --statusline: point Claude Code's statusLine key at the projected script. It rides the
248
267
  // same managed settings write as everything else this asset owns — one serialization, one
@@ -267,7 +286,7 @@ function createClaudeAdapter({ declaredPaths = declaredHarnessPaths().claude } =
267
286
  const currentRaw = fs.readFileSync(target, 'utf8');
268
287
  let merged;
269
288
  try {
270
- merged = mergeSettings(JSON.parse(currentRaw), JSON.parse(managedRaw));
289
+ merged = mergeSettings(withoutManagedHooks(JSON.parse(currentRaw), tiered.droppedHooks), JSON.parse(managedRaw));
271
290
  } catch {
272
291
  conflicts.push(`${fileName} is not valid JSON and cannot be merged`);
273
292
  continue;
@@ -358,7 +377,8 @@ function createClaudeAdapter({ declaredPaths = declaredHarnessPaths().claude } =
358
377
  for (const fileName of SETTINGS_FILES) {
359
378
  const sourceAbs = path.join(sourceDir, fileName);
360
379
  if (!fs.existsSync(sourceAbs)) continue;
361
- const fingerprint = sha256(settingsContent(fileName, fs.readFileSync(sourceAbs, 'utf8'), scope));
380
+ const sourceText = tieredSettings(fileName, settingsContent(fileName, fs.readFileSync(sourceAbs, 'utf8'), scope), context).raw;
381
+ const fingerprint = sha256(sourceText);
362
382
  const target = settingsTarget(fileName, paths);
363
383
  // 'absent' (not 'missing') matches the same convention the instructions asset already uses
364
384
  // for "no file here" — 'missing' is reserved by the lifecycle layer's removal reconciliation
@@ -373,7 +393,7 @@ function createClaudeAdapter({ declaredPaths = declaredHarnessPaths().claude } =
373
393
  let managed;
374
394
  try {
375
395
  current = JSON.parse(fs.readFileSync(target, 'utf8'));
376
- managed = JSON.parse(settingsContent(fileName, fs.readFileSync(sourceAbs, 'utf8'), scope));
396
+ managed = JSON.parse(sourceText);
377
397
  } catch {
378
398
  conflicts.push(`${fileName} is not valid JSON`);
379
399
  statuses.push({ assetId: asset.id, capability: 'settings', status: 'conflict', target });
@@ -480,6 +500,19 @@ function createClaudeAdapter({ declaredPaths = declaredHarnessPaths().claude } =
480
500
  return { changes, conflicts, prerequisites: [], notices: [...copyTree.notices, ...mcpPlan.notices] };
481
501
  }
482
502
 
503
+ /** The hook front doors (IC-037): the one asset that holds the scripts the settings hooks run. */
504
+ function hookDoorAssets(assets ) { return (assets || []).filter((asset ) => asset.id === 'claude.hooks-scripts'); }
505
+
506
+ function planHookDoors({ assets, scope, scopeRoot, context = {}, ledger } ) {
507
+ const doors = planCopyTreeAssets({ assets: hookDoorAssets(assets), scope, scopeRoot, context, ledger, removing: false });
508
+ return { changes: doors.changes, conflicts: doors.conflicts, prerequisites: [], notices: doors.notices };
509
+ }
510
+
511
+ function verifyHookDoors({ assets, scope, scopeRoot, context = {} } ) {
512
+ const doors = verifyCopyTreeAssets({ assets: hookDoorAssets(assets), scope, scopeRoot, context });
513
+ return { ok: doors.conflicts.length === 0, statuses: doors.statuses, resources: doors.resources, conflicts: doors.conflicts };
514
+ }
515
+
483
516
  function apply({ changes, scope, scopeRoot, context = {} } ) {
484
517
  for (const change of changes) {
485
518
  if (change.projection?.renderer !== INSTRUCTION_RENDERER) continue;
@@ -536,7 +569,7 @@ function createClaudeAdapter({ declaredPaths = declaredHarnessPaths().claude } =
536
569
  return { ok: conflicts.length === 0, statuses, resources, conflicts };
537
570
  }
538
571
 
539
- return { nativePaths, discover, render, plan, apply, remove, verify };
572
+ return { nativePaths, discover, render, plan, apply, remove, verify, planHookDoors, verifyHookDoors };
540
573
  }
541
574
 
542
575
  const singleton = createClaudeAdapter();
@@ -544,5 +577,6 @@ const singleton = createClaudeAdapter();
544
577
  module.exports = {
545
578
  nativePaths: singleton.nativePaths, discover: singleton.discover, render: singleton.render,
546
579
  plan: singleton.plan, apply: singleton.apply, remove: singleton.remove, verify: singleton.verify,
580
+ planHookDoors: singleton.planHookDoors, verifyHookDoors: singleton.verifyHookDoors,
547
581
  createClaudeAdapter,
548
582
  };
@@ -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
  };
@@ -7,6 +7,7 @@
7
7
  // that are not executable. Trust is reported as a prerequisite rather than being bypassed.
8
8
  const fs = require('node:fs');
9
9
  const path = require('node:path');
10
+ const crypto = require('node:crypto');
10
11
  const { verifyHookCommands } = require('../hook-commands.js');
11
12
  const { mergeHooks } = require('../../helper/settings-merge.js');
12
13
  const { atomicWrite } = require('./config.js');
@@ -102,7 +103,32 @@ function destinationFor(context = {}) {
102
103
  throw new Error('A hook destination or Codex scope is required');
103
104
  }
104
105
 
105
- function planCodexHooks({ config, sourceFile, sourceHooksDir, destinationContext = {}, trusted = false, fsImpl = fs } = {}) {
106
+ /** DoFlow's record of a deployed hook script, in the ledger's fingerprint form: sha256 of its bytes. */
107
+ function scriptFingerprint(bytes ) { return `sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`; }
108
+
109
+ /** The scripts beside the config that differ from the shipped source (or are absent), by name, and
110
+ * those among them a ledger row says were edited since DoFlow wrote them. A script with no row
111
+ * (an install from before scripts were recorded) is DoFlow's to refresh: the config deploy always
112
+ * overwrote it. `rows` are this harness's script rows: { target, fingerprint }. */
113
+ function planHookScripts({ commands, sourceHooksDir, destinationHooksDir, rows = [], force = false, fsImpl = fs } ) {
114
+ const sources = new Map (commands.checks.map((check ) => [check.script, check.file] ));
115
+ for (const name of shippedHookScripts(sourceHooksDir, fsImpl)) if (!sources.has(name)) sources.set(name, path.join(sourceHooksDir, name));
116
+ const stale = [];
117
+ const edited = [];
118
+ for (const [name, source] of sources) {
119
+ const target = path.join(destinationHooksDir, name);
120
+ if (fsImpl.existsSync(target)) {
121
+ const current = fsImpl.readFileSync(target);
122
+ if (current.equals(fsImpl.readFileSync(source))) continue;
123
+ const row = rows.find((item ) => path.resolve(item.target) === path.resolve(target));
124
+ if (row && row.fingerprint !== scriptFingerprint(current) && !force) edited.push(name);
125
+ }
126
+ stale.push(name);
127
+ }
128
+ return { names: [...sources.keys()], stale, edited };
129
+ }
130
+
131
+ function planCodexHooks({ config, sourceFile, sourceHooksDir, destinationContext = {}, trusted = false, scriptRows = [], force = false, fsImpl = fs } = {}) {
106
132
  let desired = config;
107
133
  try {
108
134
  if (!desired && sourceFile) desired = JSON.parse(fsImpl.readFileSync(sourceFile, 'utf8'));
@@ -130,16 +156,25 @@ function planCodexHooks({ config, sourceFile, sourceHooksDir, destinationContext
130
156
  }
131
157
  const content = schema.ok && mergedDesired ? `${JSON.stringify(mergedDesired, null, 2)}\n` : null;
132
158
  if (errors.length) return { ok: false, status: 'invalid-request', destination, original, changes: [], errors, commands };
133
- const changed = original !== content;
159
+ const configChanged = original !== content;
134
160
  const destinationHooksDir = destinationContext.paths?.hooksDirectory
135
161
  || destinationContext.hooksDir
136
162
  || path.join(path.dirname(destination), 'hooks');
163
+ // The scripts are deployed with the config but are not part of it: a script whose bytes differ
164
+ // from the source is redeployed even when the config is already right.
165
+ const scripts = planHookScripts({ commands, sourceHooksDir, destinationHooksDir, rows: scriptRows, force, fsImpl });
166
+ if (scripts.edited.length) {
167
+ return { ok: false, status: 'conflict', destination, original, changes: [], commands,
168
+ errors: scripts.edited.map((name ) => `Codex hook script '${name}' was modified outside DoFlow`) };
169
+ }
170
+ const scriptsOnly = !configChanged && scripts.stale.length > 0;
171
+ const changed = configChanged || scriptsOnly;
137
172
  // The scripts deployCodexHooks copies beside hooks.json, so a backup of this change holds them too.
138
173
  const companionTargets = [...new Set([
139
174
  ...commands.checks.map((check ) => path.join(destinationHooksDir, check.script)),
140
175
  ...shippedHookScripts(sourceHooksDir, fsImpl).map((name ) => path.join(destinationHooksDir, name)),
141
176
  ])].sort();
142
- return { ok: true, status: changed ? 'change' : 'unchanged', destination, original, content,
177
+ return { ok: true, status: changed ? 'change' : 'unchanged', destination, original, content, scriptsOnly, scriptNames: scripts.names,
143
178
  changes: changed ? [{ type: fsImpl.existsSync(destination) ? 'update' : 'create', file: destination, companionTargets }] : [],
144
179
  errors: [], commands, trust: commands.trust, scriptsDir: sourceHooksDir, destinationHooksDir };
145
180
  }
@@ -147,7 +182,7 @@ function planCodexHooks({ config, sourceFile, sourceHooksDir, destinationContext
147
182
  function deployCodexHooks(plan , { dryRun = false, fsImpl = fs } = {}) {
148
183
  if (!plan.ok) return { ...plan, applied: false };
149
184
  if (dryRun || plan.status === 'unchanged') return { ...plan, applied: false };
150
- atomicWrite(plan.destination, plan.content, fsImpl);
185
+ if (!plan.scriptsOnly) atomicWrite(plan.destination, plan.content, fsImpl);
151
186
  const deployed = new Set ();
152
187
  for (const check of plan.commands.checks) {
153
188
  const target = path.join(plan.destinationHooksDir, check.script);
@@ -200,4 +235,4 @@ function removeCodexHookScripts({ scriptsDir, destinationHooksDir }
200
235
  return { removed };
201
236
  }
202
237
 
203
- module.exports = { SUPPORTED_EVENTS, validateHooksConfig, classifyClaudeGuardrails, verifyHookCommands, planCodexHooks, deployCodexHooks, shippedHookScripts, removeCodexHookScripts };
238
+ module.exports = { SUPPORTED_EVENTS, validateHooksConfig, classifyClaudeGuardrails, verifyHookCommands, planCodexHooks, deployCodexHooks, scriptFingerprint, shippedHookScripts, removeCodexHookScripts };
@@ -12,9 +12,10 @@ const crypto = require('node:crypto');
12
12
  const { configPath, fingerprint: configFingerprint, parseToml, planCodexConfig, applyCodexConfig } = require('./config.js');
13
13
  const { renderServer, planCodexMcp, applyCodexMcp, ownedCodexMcpIds, userDefinedCodexMcpIds, serverTableText } = require('./mcp.js');
14
14
  const { agentDirectory, discoverCodexAgents, planCodexAgents, applyCodexAgents } = require('./agents.js');
15
- const { planCodexHooks, deployCodexHooks, removeCodexHookScripts } = require('./hooks.js');
16
- const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices } = require('../copy-tree.js');
15
+ const { planCodexHooks, deployCodexHooks, removeCodexHookScripts, scriptFingerprint } = require('./hooks.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({
@@ -445,9 +480,16 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
445
480
  /** Codex hooks.json is only replaced when the file on disk still matches the ledger's last-known
446
481
  * fingerprint — a foreign edit (or a record the ledger never owned) downgrades an otherwise-clean
447
482
  * plan to a conflict rather than silently overwriting it. */
448
- function planHooksComponent({ native, neutralResources, context, adopt = false } ) {
483
+ /** This scope's ledger rows for the hook scripts deployed beside hooks.json. */
484
+ function scriptRows(neutralResources , context ) {
485
+ return (neutralResources || []).filter((resource ) => resource?.harness === HARNESS && resource.kind === 'hooks-script'
486
+ && (!resource.scope || resource.scope === context.scope));
487
+ }
488
+
489
+ function planHooksComponent({ native, neutralResources, context, adopt = false, force = false } ) {
449
490
  let hooks = planCodexHooks({ config: native.hooksConfig, sourceFile: native.hooksSourceFile,
450
- sourceHooksDir: native.hooksSourceDir, trusted: native.hooksTrusted, destinationContext: context });
491
+ sourceHooksDir: native.hooksSourceDir, trusted: native.hooksTrusted, destinationContext: context,
492
+ scriptRows: scriptRows(neutralResources, context), force });
451
493
  const hooksFile = hooks.destination;
452
494
  if (hooks.ok && fs.existsSync(hooksFile)) {
453
495
  const [record] = nativeManagedResources(neutralResources, context, { kind: 'hooks-file', target: hooksFile });
@@ -457,7 +499,7 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
457
499
  // conflict — that one is `--force`'s business, not adoption's.
458
500
  if (!record && !adopt) {
459
501
  hooks = { ...hooks, ok: false, status: 'conflict', changes: [], errors: ['Codex hooks.json exists but is not owned by the neutral ledger'] };
460
- } else if (record && record.fingerprint !== current) {
502
+ } else if (record && record.fingerprint !== current && !force) {
461
503
  hooks = { ...hooks, ok: false, status: 'conflict', changes: [], errors: ['Codex hooks.json was modified outside DoFlow'] };
462
504
  }
463
505
  }
@@ -466,13 +508,22 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
466
508
 
467
509
  /** Agents/hooks share one removing-vs-planning fork: removal always owns both via the neutral
468
510
  * ledger's own records, regardless of what native input the caller provided this run. */
511
+ /** A removal drops the rows of the hook scripts too. The scripts themselves go by their bytes
512
+ * (removeCodexHookScripts), so each row is only released here. */
513
+ function withScriptReleases(plan , neutralResources , context , extra ) {
514
+ const releases = scriptRows(neutralResources, context).map((row ) => ({ type: 'remove', assetId: row.assetId, identity: row.identity,
515
+ target: row.target, ownershipIdentity: row.ownershipIdentity, kind: row.kind, release: true }));
516
+ return { ...plan, ...extra, changes: [...plan.changes, ...releases],
517
+ status: plan.status === 'unchanged' && releases.length ? 'change' : plan.status };
518
+ }
519
+
469
520
  function planAgentsAndHooksComponents({ native, neutralResources, context, agentsDirectory, removing, options, adopt = false } ) {
470
521
  if (removing) {
471
522
  return {
472
523
  agents: ownedRemovalPlan(neutralResources, context, 'custom-agent', agentsDirectory),
473
524
  // The scripts deployed beside hooks.json have no ledger rows; remove() matches them by bytes.
474
- hooks: { ...ownedRemovalPlan(neutralResources, context, 'hooks-file'),
475
- scriptsDir: native.hooksSourceDir, destinationHooksDir: context.paths.hooksDirectory },
525
+ hooks: withScriptReleases(ownedRemovalPlan(neutralResources, context, 'hooks-file'), neutralResources, context,
526
+ { scriptsDir: native.hooksSourceDir, destinationHooksDir: context.paths.hooksDirectory }),
476
527
  };
477
528
  }
478
529
  const components = {};
@@ -483,7 +534,7 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
483
534
  components.agents = planCodexAgents({ ...context, managedResources, sourceDir: native.agentsSourceDir, sourceVersion: options.sourceVersion ?? options.context?.sourceVersion, adopt });
484
535
  }
485
536
  if (native.hooksSourceFile !== undefined || native.hooksConfig !== undefined) {
486
- components.hooks = planHooksComponent({ native, neutralResources, context, adopt });
537
+ components.hooks = planHooksComponent({ native, neutralResources, context, adopt, force: options.context?.force === true });
487
538
  }
488
539
  return components;
489
540
  }
@@ -542,6 +593,25 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
542
593
  notices: [...copyTree.notices, ...(components.mcp?.notices || [])] };
543
594
  }
544
595
 
596
+ /** The hook front doors (IC-037): hooks.json and the scripts deployed beside it, planned and
597
+ * verified alone. A conflict reaches the lifecycle through `components`, as in plan(). */
598
+ function planHookDoors(options ) {
599
+ const native = projectedNativeOptions(options);
600
+ const hooks = native.hooksSourceFile === undefined && native.hooksConfig === undefined ? null
601
+ : planHooksComponent({ native, neutralResources: options.managedResources || options.ledger?.resources || [], context: nativeContext(options),
602
+ adopt: options.context?.adopt === true, force: options.context?.force === true });
603
+ const changes = [];
604
+ if (hooks) addChanges(changes, assetIdFor(options, 'hooks'), hooks, 'hooks');
605
+ return { changes, components: hooks ? { hooks } : {}, prerequisites: [], notices: [] };
606
+ }
607
+
608
+ function verifyHookDoors(options = {}) {
609
+ const hooks = verifyHooksResource({ native: projectedNativeOptions(options), context: nativeContext(options), options,
610
+ sourceVersion: options.sourceVersion ?? options.context?.sourceVersion, recoveryRef: options.recoveryRef });
611
+ return { ok: hooks.conflicts.length === 0 && hooks.statuses.every((status ) => status.status !== 'missing' && status.status !== 'conflict'),
612
+ resources: hooks.resources, statuses: hooks.statuses, conflicts: hooks.conflicts };
613
+ }
614
+
545
615
  /** Apply only native plans emitted by this adapter. MCP is first because it and
546
616
  * config.toml share a file; config is then re-planned over the MCP result so it
547
617
  * cannot overwrite an adjacent MCP table. */
@@ -579,7 +649,7 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
579
649
  function remove({ changes = [], dryRun = false } = {}) {
580
650
  const result = apply({ changes, dryRun });
581
651
  const direct = new Map();
582
- for (const change of changes) if (change?.nativePlan?.directRemove) direct.set(change.target, change);
652
+ for (const change of changes) if (change?.nativePlan?.directRemove && !change.retained) direct.set(change.target, change);
583
653
  for (const change of direct.values()) {
584
654
  if (!fs.existsSync(change.target)) continue;
585
655
  const current = sha256(fs.readFileSync(change.target, 'utf8'));
@@ -651,6 +721,13 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
651
721
  if (!hooks.ok) conflicts.push(...hooks.errors);
652
722
  if (status === 'managed') resources.push(lifecycleResource({ scope: options.scope, assetId: assetIdFor(options, 'hooks'), kind: 'hooks-file', identity: 'hooks.json',
653
723
  target: hooks.destination, fingerprint: sha256(fs.readFileSync(hooks.destination, 'utf8')), sourceVersion, recoveryRef, projection: { renderer: 'codex-hooks' } }));
724
+ if (status === 'managed') {
725
+ for (const name of hooks.scriptNames) {
726
+ resources.push(lifecycleResource({ scope: options.scope, assetId: assetIdFor(options, 'hooks'), kind: 'hooks-script', identity: name,
727
+ target: path.join(hooks.destinationHooksDir, name), fingerprint: scriptFingerprint(fs.readFileSync(path.join(hooks.destinationHooksDir, name))),
728
+ sourceVersion, recoveryRef, projection: { renderer: 'codex-hooks' } }));
729
+ }
730
+ }
654
731
  return { statuses, resources, conflicts };
655
732
  }
656
733
 
@@ -691,7 +768,7 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
691
768
  return { ok: conflicts.length === 0 && statuses.every((status) => status.status !== 'missing' && status.status !== 'conflict'), resources, statuses, conflicts };
692
769
  }
693
770
 
694
- return { normalizeContext, nativeContext, projectedNativeOptions, nativeManagedResources, ownershipIdentity, discover, render, plan, apply, remove, verify };
771
+ return { normalizeContext, nativeContext, projectedNativeOptions, nativeManagedResources, ownershipIdentity, discover, render, plan, apply, remove, verify, planHookDoors, verifyHookDoors };
695
772
  }
696
773
 
697
774
  const singleton = createCodexAdapter();
@@ -703,5 +780,6 @@ module.exports = {
703
780
  ownershipIdentity: singleton.ownershipIdentity,
704
781
  discover: singleton.discover, render: singleton.render, plan: singleton.plan, apply: singleton.apply,
705
782
  remove: singleton.remove, verify: singleton.verify,
783
+ planHookDoors: singleton.planHookDoors, verifyHookDoors: singleton.verifyHookDoors,
706
784
  createCodexAdapter,
707
785
  };
@@ -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,