@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
@@ -8,14 +8,18 @@
8
8
  const fs = require('node:fs');
9
9
  const path = require('node:path');
10
10
  const crypto = require('node:crypto');
11
- const { harnessFor, selectAssets, selectMcpServers, mcpCapable } = require('../registry/index.js');
11
+ const { harnessFor, selectAssets, selectMcpServers, mcpCapable, isRetired } = require('../registry/index.js');
12
12
  const { defaultLedger, ownershipKey, writeLedger, writeRecoveryRecord, upgradeLedger, LEDGER_VERSION } = require('../state/index.js');
13
13
  const { resolveAdapter, projectAdapterInput } = require('../adapters/index.js');
14
14
  const { pruneEmptyAncestors, discoverTree, sameFileIndex, sharedTreeDestDir, walkRelFiles } = require('../adapters/copy-tree.js');
15
15
  const { renderPolicies } = require('./policies.js');
16
16
  const { renderMcpIndex } = require('./mcp-index.js');
17
17
  const { hasBashCapableShell } = require('./bash-availability.js');
18
- const { hookTrustFor: geminiHookTrustFor } = require('../adapters/gemini/index.js');
18
+ const { planHookCompanions } = require('./hook-step.js');
19
+ const { tieredRegistry, hookHoldersFromLedger, hooksTieredOff, withoutHookSources, planHookTierRemovals } = require('./hook-tier.js');
20
+ const { readLock, lockPackages } = require('../state/lockfile.js');
21
+ const { resolveRegistrySource } = require('../adapters/resolve-source.js');
22
+ const { planRetiredHarnesses, planHandovers, handoversFor, applyRetiredHarnesses, retiredReleaseKeys, claimKey } = require('./retired-harnesses.js');
19
23
 
20
24
  const OPERATIONS = new Set(['create', 'merge', 'update', 'remove']);
21
25
  /** A harness's MCP selection meaning "the servers it owns now", read through its adapter's
@@ -198,12 +202,14 @@ function releasedOwnershipKeys(harnessPlans , scope ) {
198
202
  * applied. Membership is decided per ROW, not per harness: a harness that is releasing its claim
199
203
  * on this file is not a claimant even if it is releasing nothing else, and a harness in the same
200
204
  * batch that keeps its row (an install that relocates one asset while a sibling's projection is
201
- * unchanged) still is. */
202
- function survivingClaimantsByTarget(ledger , releasedKeys ) {
205
+ * unchanged) still is. A retired harness's row is never a claimant (row 127 IC-003): nothing will
206
+ * ever maintain the file for it, so it must not keep a live harness's removal from reclaiming it. */
207
+ function survivingClaimantsByTarget(ledger , releasedKeys , registry ) {
203
208
  const byTarget = new Map();
204
209
  for (const resource of ledger?.resources || []) {
205
210
  if (typeof resource?.target !== 'string' || typeof resource?.harness !== 'string') continue;
206
211
  if (releasedKeys.has(ownershipKey(resource))) continue;
212
+ if (isRetired(registry, resource.harness)) continue;
207
213
  const key = claimKey(resource);
208
214
  if (!byTarget.has(key)) byTarget.set(key, new Set());
209
215
  byTarget.get(key).add(resource.harness);
@@ -211,16 +217,10 @@ function survivingClaimantsByTarget(ledger , releasedKeys ) {
211
217
  return byTarget;
212
218
  }
213
219
 
214
- /** What one claim covers: a whole file, except an MCP entry, which is claimed on its own because
215
- * several harnesses keep their servers side by side in one MCP file. */
216
- function claimKey(record ) {
217
- return record.kind === 'mcp-server' ? `${record.target}\u0000${record.identity}` : record.target;
218
- }
219
-
220
220
  /** NFR-007: a removal reclaims only what no other harness still claims.
221
221
  *
222
222
  * Several assets project to ONE destination for several harnesses — `scripts.doflow` is a single
223
- * `<project>/.doflow/scripts` tree for claude, codex and gemini, and gemini and copilot both
223
+ * `<project>/.doflow/scripts` tree for claude and codex, and antigravity and copilot both
224
224
  * resolve to `<root>/.agents` at project scope — while ownership is recorded per harness. A
225
225
  * removal that deleted every file its own rows named therefore took the shared runtime out from
226
226
  * under the harnesses that are still installed, leaving them with rows pointing at files that no
@@ -232,8 +232,19 @@ function claimKey(record ) {
232
232
  * stays, and the last claimant's removal is the one that reclaims it. The neutral ledger is the
233
233
  * only thing consulted — it is already the record of who owns what, and a second ownership
234
234
  * registry would be one more thing to keep in sync. */
235
- function markRetainedRemovals(harnessPlans , ledger , scope ) {
236
- const claimants = survivingClaimantsByTarget(ledger, releasedOwnershipKeys(harnessPlans, scope));
235
+ function markRetainedRemovals(harnessPlans , ledger , scope , registry ) {
236
+ const claimants = survivingClaimantsByTarget(ledger, releasedOwnershipKeys(harnessPlans, scope), registry);
237
+ // A harness this same plan writes a destination for claims it after the run (maint-hooks R-2: the
238
+ // hook tier moves from claude to kiro in one update, so the shared hooks tree must stay standing).
239
+ for (const target of harnessPlans) {
240
+ if (target.skipped) continue;
241
+ for (const change of target.changes || []) {
242
+ if (change.operation === 'remove' || typeof change.target !== 'string' || typeof change.harness !== 'string') continue;
243
+ const key = claimKey(change);
244
+ if (!claimants.has(key)) claimants.set(key, new Set());
245
+ claimants.get(key).add(change.harness);
246
+ }
247
+ }
237
248
  if (!claimants.size) return harnessPlans;
238
249
  return harnessPlans.map((target ) => {
239
250
  if (target.skipped) return target;
@@ -254,10 +265,11 @@ function markRetainedRemovals(harnessPlans , ledger , scope ) {
254
265
  }
255
266
 
256
267
  /** Whether this plan leaves no ownership row standing anywhere — the only case in which a shared,
257
- * install-wide artifact that no single harness owns is genuinely unclaimed. */
258
- function removalIsTotal(plan ) {
268
+ * install-wide artifact that no single harness owns is genuinely unclaimed. A retired harness's row
269
+ * claims nothing (survivingClaimantsByTarget), so it does not count as standing. */
270
+ function removalIsTotal(plan , registry ) {
259
271
  const released = releasedOwnershipKeys(plan.targets || [], plan.scope);
260
- return (plan.ledger?.resources || []).every((resource ) => released.has(ownershipKey(resource)));
272
+ return (plan.ledger?.resources || []).every((resource ) => released.has(ownershipKey(resource)) || isRetired(registry, resource?.harness));
261
273
  }
262
274
 
263
275
  /** What a removal kept and why, ready to print. A command that says "removed" while silently
@@ -307,13 +319,17 @@ function harnessMcpSelection(registry , harness , raw ) {
307
319
  return [];
308
320
  }
309
321
 
310
- /** The runtime trees: each one source directory projected to one `<scopeRoot>/.doflow/runtime/...`
311
- * tree for every harness, with no layout and no transform (test/guards/runtime-tree.test.js pins
312
- * that). Nothing but the shared scripts reaches into them, so a file the current source no longer
313
- * ships is dead for every harness at once. Guidance and scripts are not here: a harness this run
314
- * does not update names files in those trees from its own native files, so NFR-007 retention is
315
- * right for them. */
316
- const RUNTIME_TREE_ASSETS = ['runtime.cli', 'runtime.lib', 'runtime.registry'];
322
+ /** The shared trees the orphan prune judges, each a group of the assets that project it. Each tree
323
+ * is one source directory projected to one `<scopeRoot>/.doflow/...` tree for every harness, with
324
+ * no layout and no transform (test/guards/runtime-tree.test.js pins that), so a file the current
325
+ * source no longer ships is dead for every harness at once. The guidance tree is two assets: Kiro
326
+ * projects it through kiro.guidance-tree, every other harness through guidance.context-layer. A
327
+ * harness's own native files name only DOFLOW_CORE.md in it, which every source ships. The scripts
328
+ * tree is not here: a harness this run does not update calls those scripts by name from its own
329
+ * native files, so NFR-007 retention is right for it. */
330
+ const PRUNED_TREES = [['runtime.cli'], ['runtime.lib'], ['runtime.registry'], ['guidance.context-layer', 'kiro.guidance-tree']];
331
+ /** Generated in the guidance tree per install, never a ledger row (mcpIndexPath): not an orphan. */
332
+ const PRUNE_EXEMPT = new Set(['MCP_INDEX.md']);
317
333
 
318
334
  function sha256File(file , fsImpl ) {
319
335
  return crypto.createHash('sha256').update(fsImpl.readFileSync(file)).digest('hex');
@@ -363,12 +379,22 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
363
379
  const recorded = new Set(rows.map((row ) => row.target));
364
380
  const shown = (file ) => path.relative(scopeRoot, file);
365
381
 
366
- for (const assetId of RUNTIME_TREE_ASSETS) {
367
- const executor = planned.find((target ) => target.assets.some((asset ) => asset.id === assetId));
382
+ const sharedDest = (target , asset ) => {
383
+ const nativeDir = typeof asset.nativeDir === 'string' ? asset.nativeDir : asset.nativeDir?.[target.harness];
384
+ return sharedTreeDestDir(scopeRoot, nativeDir);
385
+ };
386
+ for (const group of PRUNED_TREES) {
387
+ // The first planned harness projecting this group into a shared tree (Kiro's guidance.context-layer
388
+ // projects to its own steering directory, so its kiro.guidance-tree is the one that counts).
389
+ let executor = null;
390
+ let asset = null;
391
+ for (const target of planned) {
392
+ asset = target.assets.find((item ) => group.includes(item.id) && sharedDest(target, item));
393
+ if (asset) { executor = target; break; }
394
+ }
368
395
  if (!executor) continue;
369
- const asset = executor.assets.find((item ) => item.id === assetId);
370
- const nativeDir = typeof asset.nativeDir === 'string' ? asset.nativeDir : asset.nativeDir?.[executor.harness];
371
- const destDir = sharedTreeDestDir(scopeRoot, nativeDir);
396
+ const assetId = asset.id;
397
+ const destDir = sharedDest(executor, asset);
372
398
  const sourceDir = path.resolve(repoRoot, asset.source);
373
399
  if (!destDir || !fsImpl.existsSync(sourceDir) || !fsImpl.existsSync(destDir)) continue;
374
400
  const produced = discoverTree({ sourceDir, destDir, fsImpl }).files.map((file ) => file.destAbs);
@@ -385,7 +411,7 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
385
411
  const removalsAt = new Map ();
386
412
  for (const target of planned) {
387
413
  for (const change of target.changes) {
388
- if (change.operation !== 'remove' || change.assetId !== assetId || !isUnder(change.target, destDir) || producedSet.has(change.target)) continue;
414
+ if (change.operation !== 'remove' || !group.includes(change.assetId) || !isUnder(change.target, destDir) || producedSet.has(change.target)) continue;
389
415
  if (!removalsAt.has(change.target)) removalsAt.set(change.target, []);
390
416
  removalsAt.get(change.target) .push(change);
391
417
  }
@@ -396,11 +422,19 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
396
422
  if (!change.retained) replaced.get(change.harness) .set(change, Object.freeze({ ...change, retained: true, retainedFor: [] }));
397
423
  }
398
424
  };
425
+ // The tree itself, or any directory above it, may be a link out of the scope (a project's
426
+ // .doflow/guidance pointing at ~/.doflow/guidance). realpath resolves every component, so the
427
+ // whole tree is then left alone: nothing in it is pruned, and no planned removal runs.
428
+ if (!isUnder(realRoot, fsImpl.realpathSync(scopeRoot))) {
429
+ for (const file of removalsAt.keys()) keepFile(file);
430
+ notices.get(executor.harness) .push(`kept ${shown(destDir)}: it resolves outside the scope root, so no file in it is pruned`);
431
+ continue;
432
+ }
399
433
 
400
434
  for (const file of new Set([...rowsAt.keys(), ...removalsAt.keys()])) {
401
435
  const fileRows = rowsAt.get(file) ?? [];
402
436
  // Another asset's live row on this path is a claim this pass does not judge.
403
- if (fileRows.some((row ) => row.assetId !== assetId)) continue;
437
+ if (fileRows.some((row ) => !group.includes(row.assetId))) continue;
404
438
  const removals = removalsAt.get(file) ?? [];
405
439
  const identity = path.relative(destDir, file);
406
440
  // The rows no planned removal of this file releases on its own.
@@ -423,14 +457,25 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
423
457
  }
424
458
  if (sameFile(file)) { keepFile(file); continue; } // the produced file under an old spelling
425
459
  const current = sha256File(file, fsImpl);
426
- if (!fileRows.some((row ) => row.fingerprint === current)) {
460
+ // Each row is its harness's latest record of this path, and every row in this scope's ledger
461
+ // counts, not only the run's harnesses. Only bytes equal to ALL of them prove the file is
462
+ // DoFlow's last write: a match with one claimant's older record (a user restoring v1 after
463
+ // another harness wrote v2) authorizes nothing.
464
+ const stale = fileRows.filter((row ) => row.fingerprint !== current);
465
+ if (stale.length === fileRows.length) {
427
466
  // A planned removal here was already kept by planTree, and its adapter named it.
428
467
  if (!removals.length) notices.get(executor.harness) .push(`kept ${shown(file)}: no longer shipped, changed since DoFlow wrote it`);
429
468
  continue;
430
469
  }
470
+ if (stale.length) {
471
+ keepFile(file);
472
+ const who = [...new Set(stale.map((row ) => row.harness))].sort().join(', ');
473
+ notices.get(executor.harness) .push(`kept ${shown(file)}: no longer shipped, and its bytes differ from what ${who} last recorded`);
474
+ continue;
475
+ }
431
476
  notices.get(executor.harness) .push(`prune ${shown(file)}: no longer shipped, unchanged since DoFlow wrote it`);
432
477
  // removeTree re-runs containment and the same-file check from this right before the unlink.
433
- const pruneGuard = { root: destDir, source: sourceDir };
478
+ const pruneGuard = { root: destDir, source: sourceDir, scope: scopeRoot };
434
479
  if (!removals.length) {
435
480
  added.get(executor.harness) .push(synthetic({ fingerprint: current, supersedes: superseded, pruneGuard }));
436
481
  continue;
@@ -445,7 +490,7 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
445
490
  // Files no row names and the source does not produce: never DoFlow's to delete, but named.
446
491
  for (const rel of walkRelFiles(destDir, fsImpl)) {
447
492
  const file = path.join(destDir, rel);
448
- if (producedSet.has(file) || recorded.has(file) || sameFile(file)) continue;
493
+ if (producedSet.has(file) || recorded.has(file) || sameFile(file) || PRUNE_EXEMPT.has(rel)) continue;
449
494
  notices.get(executor.harness) .push(`kept ${shown(file)}: not shipped by this version and not recorded as DoFlow's`);
450
495
  }
451
496
  }
@@ -453,11 +498,242 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
453
498
  : withChanges(target, replaced.get(target.harness) , added.get(target.harness) , notices.get(target.harness) )));
454
499
  }
455
500
 
501
+ /** One harness's plan entry, from what its adapter returned: the changes checked against the
502
+ * harness's assets, and the conflicts, prerequisites and notices in the lifecycle's common shape. */
503
+ function targetFromResult({ harness, assets, mcp, mcpSelected, policies, adapterInput, discovery, result } ) {
504
+ if (!result || typeof result !== 'object') throw new Error(`Adapter '${harness.id}' returned an invalid plan`);
505
+ const changes = (result.changes || []).map((change ) => normalizeChange(change, { harness, assets }));
506
+ const conflicts = adapterConflicts(result, harness);
507
+ const prerequisites = [...(result.prerequisites || []), ...changes.map((change ) => change.prerequisite).filter(Boolean)];
508
+ const requiredNativeResources = result.requiredNativeResources ?? changes;
509
+ if (!Array.isArray(requiredNativeResources)) throw new Error(`Adapter '${harness.id}' returned invalid requiredNativeResources`);
510
+ return { harness: harness.id, adapter: harness.adapter, assets, mcp, mcpSelected, policies, adapterInput, discovery, changes, requiredNativeResources, conflicts, prerequisites, notices: adapterNotices(result), skipped: false };
511
+ }
512
+
513
+ // ── package skills and agents (122 IC-017) ──────────────────────────────────────────────────────────
514
+
515
+ /** A package contribution's asset id is `pkg.<qualified id>`; no registry asset id starts with `pkg.`. */
516
+ const PACKAGE_ASSET_PREFIX = 'pkg.';
517
+ const isPackageAsset = (id ) => typeof id === 'string' && id.startsWith(PACKAGE_ASSET_PREFIX);
518
+
519
+ /** IC-017's ownership identity: the harness, the qualified id and the path inside the projected tree. It names no
520
+ * scope and no destination, as G20 requires of every identity. */
521
+ function packageIdentity(harness , assetId , relPath ) {
522
+ return `doflow:${harness}:package:${assetId.slice(PACKAGE_ASSET_PREFIX.length)}:${relPath}`;
523
+ }
524
+
525
+ /** Whether an adapter's conflict line or status belongs to one of `ids` (copy-tree prefixes each with `<asset id>: `). */
526
+ const namesAsset = (reason , ids ) => [...ids].some((id) => String(reason).includes(`${id}: `));
527
+
528
+ /**
529
+ * The active third-party packages with skill or agent contributions, read from `context.packageView` when given (a
530
+ * test seam, and the verbs' own view) or else from the package view of the global lock under `scopeRoot`. Nothing is
531
+ * read when the lock records no third-party package. The view is taken with no `packages.disabled` applied: harness
532
+ * files are machine-wide, so only the lock bit, the trust grant and the store's integrity decide them.
533
+ */
534
+ function activePackageEntries(registry , scopeRoot , context ) {
535
+ let view = context.packageView;
536
+ if (!view) {
537
+ let rows ;
538
+ try { rows = lockPackages(readLock({ scope: 'global', homeDir: scopeRoot })); } catch { return []; }
539
+ if (!rows.some((row ) => row?.source?.type !== 'bundled')) return [];
540
+ view = require('../runtime/packages/view.js').packageView({ repoRoot: registry.repoRoot, homeDir: scopeRoot, settingsView: {} });
541
+ }
542
+ const { instructionEntries } = require('../runtime/packages/store.js');
543
+ const out = [];
544
+ for (const pkg of view.packages ?? []) {
545
+ if (pkg.firstParty || pkg.state !== 'active' || typeof pkg.storeDir !== 'string') continue;
546
+ let manifest ;
547
+ try { manifest = JSON.parse(fs.readFileSync(path.join(pkg.storeDir, 'doflow-package.json'), 'utf8')); } catch { continue; }
548
+ const entries = instructionEntries(manifest);
549
+ if (entries.length) out.push({ id: pkg.id, storeDir: pkg.storeDir, entries });
550
+ }
551
+ return out;
552
+ }
553
+
554
+ /** The registry-shaped asset for one contribution on one harness, or `{ warning }` when it cannot be projected there.
555
+ * The source resolves inside the store entry with real paths on both sides (`resolveRegistrySource` with a source
556
+ * root); a skill lands in its own directory under the borrowed asset's destination, an agent beside DoFlow's. */
557
+ function packageAsset(registry , harnessId , pkg , entry , realRoot ) {
558
+ const { projectionReach } = require('../runtime/packages/store.js');
559
+ const reach = projectionReach(registry, harnessId, entry.point);
560
+ if (!reach.ok) return { warning: `${harnessId} cannot take ${entry.point} ${entry.qualifiedId}: ${reach.reason}` };
561
+ let real ;
562
+ try { real = resolveRegistrySource(registry, entry.path, `package ${entry.qualifiedId}`, pkg.storeDir); } catch (error ) {
563
+ return { warning: `${entry.point} ${entry.qualifiedId} is not projected: ${error.message}` };
564
+ }
565
+ let sourceDir = real;
566
+ let nativeDir = reach.nativeDir ? `${reach.nativeDir}/${entry.name}` : entry.name;
567
+ if (entry.point === 'agent') {
568
+ // The adapters project a whole source directory, so an agent file is projected only from a directory of its own.
569
+ sourceDir = path.dirname(real);
570
+ nativeDir = reach.nativeDir;
571
+ const others = fs.readdirSync(sourceDir).filter((name ) => name !== path.basename(real));
572
+ if (others.length) return { warning: `agent ${entry.qualifiedId} is not projected: its directory ${path.relative(realRoot, sourceDir) || '.'} holds other entries (${others.slice(0, 3).join(', ')}); give the agent file a directory of its own` };
573
+ }
574
+ return {
575
+ asset: { id: `${PACKAGE_ASSET_PREFIX}${entry.qualifiedId}`, kind: entry.point, source: path.relative(realRoot, sourceDir), ownership: 'managed-tree',
576
+ projection: { [harnessId]: reach.projection }, nativeDir: { [harnessId]: nativeDir } },
577
+ };
578
+ }
579
+
580
+ /** What one harness's adapter plans for one package's assets alone, with every other component's output dropped and
581
+ * the identities rewritten to IC-017's: `{ part, changes, notices }`, or `{ warnings }` when it cannot be planned. The
582
+ * part (assets, adapter input, discovery) is what verification re-reads after the apply. */
583
+ function planPackagePart({ registry, adapters, harness, scope, scopeRoot, ledger, context, pkg, assets } ) {
584
+ const realRoot = fs.realpathSync(pkg.storeDir);
585
+ // The native projection (codex's TOML agents and hooks) is DoFlow's own, never a package's: it is left out, so the
586
+ // adapter plans the package's tree assets and nothing that reads the DoFlow checkout.
587
+ const adapterInput = projectAdapterInput({ registry, harness: { ...harness, nativeProjection: undefined }, scope, scopeRoot, assets, mcp: [], mcpAdoptable: [], policies: [],
588
+ context: { repoRoot: realRoot, homeDir: context.homeDir ?? scopeRoot, projectRoot: scopeRoot, sourceVersion: context.sourceVersion, operation: context.operation } });
589
+ const adapter = resolveAdapter(adapters, harness);
590
+ const ids = new Set (assets.map((asset ) => asset.id));
591
+ let discovery ;
592
+ let result ;
593
+ try {
594
+ discovery = adapter.discover({ ...adapterInput, registry, ledger });
595
+ result = adapter.plan({ ...adapterInput, registry, ledger, discovery });
596
+ } catch (error ) {
597
+ return { warnings: [`package ${pkg.id} is not projected into ${harness.id}: ${error.message}`] };
598
+ }
599
+ const conflicts = adapterConflicts(result, harness).filter((reason ) => namesAsset(reason, ids));
600
+ if (conflicts.length) return { warnings: conflicts.map((reason ) => `package ${pkg.id} is not projected into ${harness.id}: ${reason}`) };
601
+ const changes = (result.changes || []).filter((change ) => ids.has(change.assetId))
602
+ .map((change ) => normalizeChange({ ...change, ownershipIdentity: packageIdentity(harness.id, change.assetId, change.identity ?? path.basename(change.target)) }, { harness, assets }));
603
+ const notices = adapterNotices(result).filter((notice ) => notice.startsWith('kept hand-edited '));
604
+ return { part: { packageId: pkg.id, assets, adapterInput, discovery }, changes, notices };
605
+ }
606
+
607
+ /** The removals of a package's recorded rows on one harness (IC-017 unprojection), planned from the ledger alone, so a
608
+ * store entry that is gone or changed is no obstacle: a file whose bytes are still DoFlow's is removed, a file a person
609
+ * edited is kept and only its row released (named in a notice), a file already gone only releases its row. The change
610
+ * shape is the one every adapter's copy-tree removal reads (`kind`, `projection.renderer`), as the runtime prune's. */
611
+ function packageRemovals({ harness, scopeRoot, rows } ) {
612
+ const notices = [];
613
+ const changes = rows.map((row ) => {
614
+ const base = { harness: harness.id, assetId: row.assetId, target: row.target, operation: 'remove', kind: 'copy-tree-file', identity: row.identity,
615
+ ownershipIdentity: row.ownershipIdentity, projection: { renderer: 'copy-tree' } };
616
+ if (!fs.existsSync(row.target)) return Object.freeze({ ...base, fingerprint: null, retained: true, retainedFor: [] });
617
+ const current = sha256File(row.target, fs);
618
+ if (current !== row.fingerprint) {
619
+ notices.push(`kept person-edited ${path.relative(scopeRoot, row.target)}; DoFlow no longer manages it`);
620
+ return Object.freeze({ ...base, fingerprint: current, retained: true, retainedFor: [] });
621
+ }
622
+ return Object.freeze({ ...base, fingerprint: current });
623
+ });
624
+ return { changes, notices };
625
+ }
626
+
627
+ /**
628
+ * The package work of a global-scope plan (IC-017), per harness: the changes that project each active package's skills
629
+ * and agents through the harness's own adapter (one `part` per package, for verification), and the removals of every
630
+ * recorded row that no longer belongs to an active, reachable contribution (disabled, revoked, removed, suspended,
631
+ * re-digested, or a `remove` run). A package fault never makes the plan unsafe (FR-009): it becomes a warning, and that
632
+ * package's files on that harness stay as they are. Nothing happens at project scope (NFR-005), and nothing is read
633
+ * when no package is recorded.
634
+ */
635
+ function planPackageWork({ registry, adapters, scope, scopeRoot, harnesses, ledger, context } ) {
636
+ const byHarness = new Map ();
637
+ const warnings = [];
638
+ if (scope !== 'global' || !harnesses.length) return { byHarness, warnings };
639
+ const recorded = (ledger?.resources ?? []).filter((row ) => isPackageAsset(row.assetId) && typeof row.target === 'string');
640
+ const active = context.operation === 'remove' ? [] : activePackageEntries(registry, scopeRoot, context);
641
+ if (!active.length && !recorded.length) return { byHarness, warnings };
642
+ for (const harness of harnesses) {
643
+ const work = { changes: [] , notices: [] , parts: [] };
644
+ const wanted = new Set ();
645
+ for (const pkg of active) {
646
+ const realRoot = fs.realpathSync(pkg.storeDir);
647
+ const assets = [];
648
+ for (const entry of pkg.entries) {
649
+ const made = packageAsset(registry, harness.id, pkg, entry, realRoot);
650
+ if (made.warning) warnings.push({ harness: harness.id, packageId: pkg.id, warning: made.warning });
651
+ else { assets.push(made.asset); wanted.add(made.asset.id); }
652
+ }
653
+ if (!assets.length) continue;
654
+ const planned = planPackagePart({ registry, adapters, harness, scope, scopeRoot, ledger, context, pkg, assets });
655
+ for (const warning of planned.warnings ?? []) warnings.push({ harness: harness.id, packageId: pkg.id, warning });
656
+ if (!planned.part) continue;
657
+ work.parts.push(planned.part);
658
+ work.changes.push(...planned.changes);
659
+ work.notices.push(...planned.notices);
660
+ }
661
+ const removals = packageRemovals({ harness, scopeRoot, rows: recorded.filter((row ) => row.harness === harness.id && !wanted.has(row.assetId)) });
662
+ work.changes.push(...removals.changes);
663
+ work.notices.push(...removals.notices);
664
+ if (work.parts.length || work.changes.length || work.notices.length) byHarness.set(harness.id, work);
665
+ }
666
+ return { byHarness, warnings };
667
+ }
668
+
669
+ /** A harness's plan entry with its package work added, `requiredNativeResources` kept in step as withChanges does. */
670
+ function withPackageWork(target , work ) {
671
+ const changes = [...target.changes, ...work.changes];
672
+ const requiredNativeResources = target.requiredNativeResources === target.changes ? changes
673
+ : [...target.requiredNativeResources, ...work.changes.map((change ) => ({ harness: change.harness, component: 'package', target: change.target, operation: change.operation, identity: change.identity }))];
674
+ return { ...target, changes, requiredNativeResources, notices: [...target.notices, ...work.notices], packageParts: work.parts };
675
+ }
676
+
677
+ /** The plan entry of a package-only plan for one harness: no DoFlow asset, only the package work. */
678
+ function packageHostTarget({ registry, harness, scope, scopeRoot, context, work } ) {
679
+ const adapterInput = projectAdapterInput({ registry, harness: { ...harness, nativeProjection: undefined }, scope, scopeRoot, assets: [], mcp: [], mcpAdoptable: [], policies: [],
680
+ context: { repoRoot: registry.repoRoot, homeDir: context.homeDir ?? scopeRoot, projectRoot: scopeRoot, operation: context.operation } });
681
+ return { harness: harness.id, adapter: harness.adapter, assets: [], mcp: [], mcpSelected: null, policies: [], adapterInput, discovery: null,
682
+ changes: work.changes, requiredNativeResources: work.changes, conflicts: [], prerequisites: [], notices: work.notices, skipped: false,
683
+ packagesOnly: true, packageParts: work.parts };
684
+ }
685
+
686
+ /** The verification of one package part: the adapter's verify of that package's assets, filtered to them, with the
687
+ * identities rewritten as the plan wrote them. A throw is a conflict, so the run stops before the ledger is written. */
688
+ function verifyPackagePart({ part, adapter, harness, plan, context, operation, plannedWrites } ) {
689
+ const ids = new Set (part.assets.map((asset ) => asset.id));
690
+ let raw ;
691
+ try {
692
+ raw = normalizeVerification(adapter.verify({ ...part.adapterInput, registry: context.registry ?? plan.registry, discovery: part.discovery, ledger: plan.ledger,
693
+ operation, context: { ...part.adapterInput.context, operation, ...(plannedWrites ? { plannedWrites } : {}) } }), harness);
694
+ } catch (error ) {
695
+ return { ok: false, statuses: [], resources: [], conflicts: [`package ${part.packageId}: ${error.message}`] };
696
+ }
697
+ const conflicts = (raw.conflicts || []).filter((reason ) => namesAsset(reason, ids));
698
+ const statuses = raw.statuses.filter((status ) => ids.has(status?.assetId));
699
+ const resources = raw.resources.filter((resource ) => ids.has(resource?.assetId))
700
+ .map((resource ) => ({ ...resource, ownershipIdentity: packageIdentity(harness.id, resource.assetId, resource.identity ?? path.basename(resource.target)) }));
701
+ const failed = statuses.some((status ) => ['missing', 'conflict', 'error', 'invalid'].includes(status?.status));
702
+ return { ok: conflicts.length === 0 && !failed, statuses, resources, conflicts };
703
+ }
704
+
705
+ /** A harness's verification with its package parts' verifications merged in, so each harness keeps one entry. */
706
+ function withPackageVerification(verification , target , options ) {
707
+ let merged = verification;
708
+ for (const part of target.packageParts ?? []) {
709
+ const one = verifyPackagePart({ ...options, part });
710
+ merged = { ...merged, statuses: [...merged.statuses, ...one.statuses], resources: [...merged.resources, ...one.resources],
711
+ conflicts: [...(merged.conflicts || []), ...one.conflicts], ok: merged.ok !== false && one.ok };
712
+ }
713
+ return merged;
714
+ }
715
+
456
716
  function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelections = {}, mcpAdoptable = {}, retainedMcpIds = [], ledger, context = {} } ) {
457
717
  assertScope(scope);
458
718
  if (!registry) throw new Error('registry is required');
459
719
  const baseLedger = ledger ?? defaultLedger({ scope, scopeRoot });
460
- const harnessPlans = normalizeTargets(registry, targets).map((harness) => {
720
+ // Hook tiers (maint-hooks): a harness outside `context.hookHarnesses` is planned against a registry
721
+ // in which its hooks capability is unavailable, so it gets content only; the hooks an earlier run
722
+ // wrote for it are removed in the same plan. No list (an in-process caller) keeps every harness's hooks.
723
+ const fullRegistry = registry;
724
+ // A removal takes the hooks the ledger records, whatever the setting says now (maint-hooks).
725
+ registry = tieredRegistry(registry, context.operation === 'remove' ? hookHoldersFromLedger(registry, baseLedger) : context.hookHarnesses);
726
+ const harnesses = normalizeTargets(registry, targets);
727
+ // `packagesOnly` (the package verbs): only the package targets, for a runtime that ships no DoFlow content to plan.
728
+ const packagesOnly = context.packagesOnly === true;
729
+ // Row 127 rule 3a: a retired harness's file its replacement takes over is decided before any live
730
+ // plan, since the replacement plans that one write itself (`context.handovers`, IC-007).
731
+ const sweeping = context.operation !== 'remove' && !packagesOnly;
732
+ const handovers = sweeping
733
+ ? planHandovers({ registry, ledger: baseLedger, scope, scopeRoot, planned: harnesses.filter((harness ) => harness.scopes.includes(registryScope(scope))).map((harness ) => harness.id) })
734
+ : new Map();
735
+ const handedTo = handoversFor(handovers);
736
+ const harnessPlans = packagesOnly ? [] : harnesses.map((harness) => {
461
737
  if (!harness.scopes.includes(registryScope(scope))) {
462
738
  return { harness: harness.id, assets: [], changes: [], conflicts: [`Harness '${harness.id}' does not support ${scope} scope`], prerequisites: [], notices: [], skipped: true, mcpSelected: null };
463
739
  }
@@ -467,7 +743,9 @@ function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelec
467
743
  const policies = renderPolicies(registry, { harness: harness.id });
468
744
  const adoptable = capable ? selectMcpServers(registry, catalogIds(registry, mcpAdoptable[harness.id] ?? [])) : [];
469
745
  const adapter = resolveAdapter(adapters, harness);
470
- const project = (mcp ) => projectAdapterInput({ registry, harness, scope, scopeRoot, assets, mcp, mcpAdoptable: adoptable, policies, context });
746
+ const baseContext = handedTo.has(harness.id) ? { ...context, handovers: handedTo.get(harness.id) } : context;
747
+ const harnessContext = hooksTieredOff(harness) ? withoutHookSources(baseContext) : baseContext;
748
+ const project = (mcp ) => projectAdapterInput({ registry, harness, scope, scopeRoot, assets, mcp, mcpAdoptable: adoptable, policies, context: harnessContext });
471
749
  // `keep` asks the adapter what it owns before the real projection: the probe carries no
472
750
  // selection, so discovery reads the files as they are.
473
751
  const ids = selection === MCP_KEEP
@@ -478,30 +756,54 @@ function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelec
478
756
  const input = { ...adapterInput, registry, ledger: baseLedger };
479
757
  const discovery = adapter.discover(input);
480
758
  const result = adapter.plan({ ...input, discovery });
481
- if (!result || typeof result !== 'object') throw new Error(`Adapter '${harness.id}' returned an invalid plan`);
482
- const changes = (result.changes || []).map((change ) => normalizeChange(change, { harness, assets }));
483
- const conflicts = adapterConflicts(result, harness);
484
- const prerequisites = [...(result.prerequisites || []), ...changes.map((change ) => change.prerequisite).filter(Boolean)];
485
- const requiredNativeResources = result.requiredNativeResources ?? changes;
486
- if (!Array.isArray(requiredNativeResources)) throw new Error(`Adapter '${harness.id}' returned invalid requiredNativeResources`);
487
- return { harness: harness.id, adapter: harness.adapter, assets, mcp, mcpSelected: capable ? mcp.map((server ) => server.id) : null, policies, adapterInput, discovery, changes, requiredNativeResources, conflicts, prerequisites, notices: adapterNotices(result), skipped: false };
759
+ const target = targetFromResult({ harness, assets, mcp, mcpSelected: capable ? mcp.map((server ) => server.id) : null, policies, adapterInput, discovery, result });
760
+ return hooksTieredOff(harness)
761
+ ? planHookTierRemovals({ target, registry: fullRegistry, adapter, harness: harnessFor(fullRegistry, harness.id), scope, scopeRoot, ledger: baseLedger, context, normalizeChange, adapterConflicts })
762
+ : target;
488
763
  });
489
764
  // The MCP index names every server a harness of this scope uses: the selections planned here and
490
765
  // the recorded selections of the harnesses this run does not touch.
491
766
  const selectedMcp = selectMcpServers(registry, catalogIds(registry,
492
767
  [...retainedMcpIds, ...harnessPlans.flatMap((item) => (item.skipped ? [] : item.mcpSelected ?? []))]));
768
+ // Package skills and agents ride the same plan, at global scope only (IC-017), for the harnesses it plans; each
769
+ // harness keeps one plan entry, its package work added to it.
770
+ let packages ;
771
+ try {
772
+ packages = planPackageWork({ registry, adapters, scope, scopeRoot, ledger: baseLedger, context,
773
+ harnesses: harnesses.filter((harness ) => harness.scopes.includes(registryScope(scope))) });
774
+ } catch (error ) {
775
+ packages = { byHarness: new Map(), warnings: [{ harness: null, packageId: null, warning: `package skills and agents are not projected: ${error.message}` }] };
776
+ }
777
+ const withPackages = packagesOnly
778
+ ? harnesses.filter((harness ) => packages.byHarness.has(harness.id))
779
+ .map((harness ) => packageHostTarget({ registry, harness, scope, scopeRoot, context, work: packages.byHarness.get(harness.id) }))
780
+ : harnessPlans.map((target ) => (!target.skipped && packages.byHarness.has(target.harness) ? withPackageWork(target, packages.byHarness.get(target.harness)) : target));
493
781
  // Only once every harness's plan is known: whether a file may be deleted depends on the rows
494
782
  // the WHOLE plan leaves standing, which no single harness's plan can see.
495
- const retained = markRetainedRemovals(harnessPlans, baseLedger, scope);
496
- const annotated = context.operation === 'remove' ? retained
783
+ const retained = markRetainedRemovals(withPackages, baseLedger, scope, registry);
784
+ const annotated = context.operation === 'remove' || packagesOnly ? retained
497
785
  : pruneRetiredRuntimeFiles(retained, baseLedger, { scopeRoot, repoRoot: context.repoRoot });
498
- const changes = annotated.flatMap((item ) => item.changes);
499
- const conflicts = annotated.flatMap((item ) => item.conflicts.map((reason ) => ({ harness: item.harness, reason })));
500
- const prerequisites = annotated.flatMap((item ) => item.prerequisites.map((prerequisite ) => ({ harness: item.harness, prerequisite })));
501
- const requiredNativeResources = annotated.flatMap((item ) => item.requiredNativeResources || []);
502
- const notices = annotated.flatMap((item ) => item.notices.map((notice ) => ({ harness: item.harness, notice })));
503
- return Object.freeze({ scope, scopeRoot, mcp: selectedMcp, ledger: baseLedger, targets: annotated, changes, requiredNativeResources, conflicts, prerequisites, notices,
504
- retained: retainedChanges(changes), safe: conflicts.length === 0 && prerequisites.length === 0 });
786
+ // The shared hooks tree may have moved under harnesses this run does not name (IC-037): their
787
+ // front doors come along, as targets of this same plan.
788
+ const companions = packagesOnly ? [] : planHookCompanions({ registry, adapters, scope, harnessScope: registryScope(scope), scopeRoot, ledger: baseLedger, context,
789
+ targets: annotated, targetFromResult });
790
+ const planned = [...annotated, ...companions];
791
+ const liveChanges = planned.flatMap((item ) => item.changes);
792
+ // Row 127 IC-003: what a retired harness left in this scope's ledger is released, deleted or
793
+ // stripped by the same run, once every live change is known (a row a live change already removes
794
+ // is that change's). Never on remove, which changes only what `-t` names, nor for package work.
795
+ const retiredHarnesses = sweeping
796
+ ? planRetiredHarnesses({ registry, ledger: baseLedger, scope, scopeRoot, livePlanChanges: liveChanges, handovers }) : [];
797
+ const changes = [...liveChanges, ...retiredHarnesses.flatMap((item ) => item.changes)];
798
+ const conflicts = planned.flatMap((item ) => item.conflicts.map((reason ) => ({ harness: item.harness, reason })));
799
+ const prerequisites = planned.flatMap((item ) => item.prerequisites.map((prerequisite ) => ({ harness: item.harness, prerequisite })));
800
+ const requiredNativeResources = planned.flatMap((item ) => item.requiredNativeResources || []);
801
+ const notices = [...planned.flatMap((item ) => item.notices.map((notice ) => ({ harness: item.harness, notice }))),
802
+ ...packages.warnings.map((item ) => ({ harness: item.harness, notice: `warning: ${item.warning}` }))];
803
+ return Object.freeze({ scope, scopeRoot, mcp: selectedMcp, ledger: baseLedger, targets: planned, changes, requiredNativeResources, conflicts, prerequisites, notices,
804
+ retained: retainedChanges(changes), safe: conflicts.length === 0 && prerequisites.length === 0,
805
+ ...(retiredHarnesses.length ? { retiredHarnesses } : {}),
806
+ ...(packagesOnly ? { packagesOnly: true } : {}), ...(packages.warnings.length ? { packageWarnings: packages.warnings } : {}) });
505
807
  }
506
808
 
507
809
  function verificationResources(verification , changes , scope , recoveryRef ) {
@@ -559,11 +861,20 @@ function applyMcpIndex({ scopeRoot, selectedMcp, mode, retain = false, fsImpl =
559
861
  fsImpl.writeFileSync(file, content);
560
862
  }
561
863
 
562
- function updateLedger({ ledger, scope, scopeRoot, verifications, changes, recoveryRef, fsImpl = fs } ) {
864
+ function updateLedger({ ledger, scope, scopeRoot, verifications, changes, recoveryRef, retiredHarnesses = [], fsImpl = fs } ) {
563
865
  // upgradeLedger makes every written ledger v2-shaped (tombstone log present) while v1 inputs
564
866
  // stay readable — migration happens on write, never as a separate user-facing step.
565
867
  const next = JSON.parse(JSON.stringify(upgradeLedger(ledger ?? defaultLedger({ scope, scopeRoot }))));
566
868
  if (!Array.isArray(next.tombstones)) next.tombstones = [];
869
+ // A retired harness's rows leave in this write, whatever the sweep did with each file, and its
870
+ // `targets` entry goes once no (deferred) row of it remains.
871
+ const retiredKeys = retiredReleaseKeys(retiredHarnesses);
872
+ if (retiredKeys.size) {
873
+ next.resources = next.resources.filter((resource ) => !retiredKeys.has(ownershipKey(resource)));
874
+ for (const { harness } of retiredHarnesses) {
875
+ if (!next.resources.some((resource ) => resource.harness === harness)) delete next.targets[harness];
876
+ }
877
+ }
567
878
  // Snapshot pre-run claims by ownership identity (harness+asset+identity) so a re-verification
568
879
  // that relocates the same claim to a new target is detectable as a MOVE, not remove-plus-add.
569
880
  const priorByIdentity = new Map();
@@ -644,9 +955,14 @@ function verifyLifecycle({ plan, adapters, context = {} } ) {
644
955
  const plannedWrites = context.checkPlannedWrites
645
956
  ? target.changes.filter((change ) => !change.retained && (change.operation === 'create' || change.operation === 'update')).map((change ) => change.target)
646
957
  : undefined;
647
- let verification = normalizeVerification(adapter.verify({ ...target.adapterInput,
648
- registry: context.registry ?? plan.registry, discovery: target.discovery, ledger: plan.ledger,
649
- operation, context: { ...target.adapterInput.context, operation, ...(plannedWrites ? { plannedWrites } : {}) } }), harness);
958
+ // A hook-front-door companion (IC-037) is verified for its doors alone: the rest of that
959
+ // harness is not part of this run and may legitimately trail the current source. A package-only entry has no
960
+ // DoFlow asset to verify, only its package parts (IC-017).
961
+ let verification = target.packagesOnly ? { harness: target.harness, ok: true, statuses: [], resources: [], conflicts: [] }
962
+ : normalizeVerification(adapter[target.hookDoorsOnly ? 'verifyHookDoors' : 'verify']({ ...target.adapterInput,
963
+ registry: context.registry ?? plan.registry, discovery: target.discovery, ledger: plan.ledger,
964
+ operation, context: { ...target.adapterInput.context, operation, ...(plannedWrites ? { plannedWrites } : {}) } }), harness);
965
+ verification = withPackageVerification(verification, target, { adapter, harness, plan, context, operation, plannedWrites });
650
966
  if (operation === 'remove') verification = normalizeRemovalVerification(verification, target.changes, plan.ledger);
651
967
  verifications.push({ ...verification, hookWiring: hookWiringStatus(harness, target, verification) });
652
968
  }
@@ -654,24 +970,32 @@ function verifyLifecycle({ plan, adapters, context = {} } ) {
654
970
  return { verifications, conflicts, ok: conflicts.length === 0 && verifications.every((result) => result.ok !== false) };
655
971
  }
656
972
 
657
- function assertPlanApplicable(plan , stateRoot , acceptPrerequisites ) {
973
+ function assertPlanApplicable(plan , stateRoot , acceptPrerequisites , mode = 'apply') {
974
+ // A repeated remove: the ledger holds nothing for these harnesses and the plan changes nothing. That is a
975
+ // finished job, not the malformed plan refused below; the CLI answers this code with a note and exit 0.
976
+ if (mode === 'remove' && plan.safe && !plan.changes.length && !(plan.retiredHarnesses ?? []).some((item ) => item.changes.length)
977
+ && !(plan.ledger?.resources ?? []).some((resource ) => plan.targets.some((target ) => target.harness === resource.harness))) {
978
+ throw Object.assign(new Error(`Nothing to remove for ${plan.targets.map((target ) => target.harness).join(', ')}: DoFlow owns no native resources there.`), { code: 'DOFLOW_NOTHING_TO_REMOVE' });
979
+ }
658
980
  if (!plan.safe && !(acceptPrerequisites && plan.conflicts.length === 0)) throw new Error('Refusing to apply an unsafe lifecycle plan');
659
981
  if (!stateRoot) throw new Error('stateRoot is required before lifecycle mutation');
660
- if (!plan.requiredNativeResources?.length || !plan.changes.length) throw new Error('Refusing to apply a lifecycle plan with no required native resources');
982
+ // A retirement is native work no live harness lists: a plan holding only that is applicable.
983
+ const retiring = (plan.retiredHarnesses ?? []).some((item ) => item.changes.length);
984
+ if ((!plan.requiredNativeResources?.length && !retiring) || !plan.changes.length) throw new Error('Refusing to apply a lifecycle plan with no required native resources');
661
985
  }
662
986
 
663
987
  /** Registry-declared hooks assets (`kind: "hooks"` in core/registry/assets.json) this harness's
664
- * plan selected — currently only claude.hooks-scripts and kiro.hooks-scripts. Codex and Gemini
665
- * deploy hooks via their own bespoke code paths (see targetNeedsHooks below) rather than a
988
+ * plan selected — currently only claude.hooks-scripts and kiro.hooks-scripts. Codex deploys
989
+ * hooks via its own bespoke code path (see targetNeedsHooks below) rather than a
666
990
  * registered asset, so they never appear here. */
667
991
  function hookAssetIds(target ) {
668
992
  return new Set((target.assets || []).filter((asset ) => asset.kind === 'hooks').map((asset ) => asset.id));
669
993
  }
670
994
 
671
995
  /** Whether this harness's plan includes at least one hooks-bearing change. Claude's hooks are a
672
- * registry-declared asset (`kind: "hooks"` in core/registry/assets.json); Codex and Gemini deploy
673
- * hooks via their own bespoke code paths and tag the resulting change with `nativeComponent:
674
- * 'hooks'` instead (see src/adapters/codex/index.js and src/adapters/gemini/index.js). Either
996
+ * registry-declared asset (`kind: "hooks"` in core/registry/assets.json); Codex deploys hooks
997
+ * via its own bespoke code path and tags the resulting change with `nativeComponent: 'hooks'`
998
+ * instead (see src/adapters/codex/index.js). Either
675
999
  * signal is sufficient — this stays harness-agnostic on purpose. */
676
1000
  function targetNeedsHooks(target ) {
677
1001
  const hookIds = hookAssetIds(target);
@@ -680,20 +1004,10 @@ function targetNeedsHooks(target ) {
680
1004
 
681
1005
  /** Whether a verified resource is the hooks resource this harness owns. A registry-declared hooks
682
1006
  * asset (Claude, Kiro) is matched by assetId; Codex's bespoke hooks-file resource is matched by
683
- * `kind`; Gemini's bespoke hooks resource (a key inside settings.json, not a standalone file) is
684
- * matched by its `projection.renderer` tag — see src/adapters/codex/index.js's
685
- * `lifecycleResource(..., kind: 'hooks-file', ...)` and src/adapters/gemini/index.js's verify(),
686
- * which tags its hooks resource `projection: { renderer: 'gemini-hooks' }`. */
1007
+ * `kind` — see src/adapters/codex/index.js's `lifecycleResource(..., kind: 'hooks-file', ...)`. */
687
1008
  function verificationOwnsHooks(verification , hookIds ) {
688
1009
  return (verification?.resources || []).some((resource ) =>
689
- hookIds.has(resource.assetId) || resource.kind === 'hooks-file' || resource.projection?.renderer === 'gemini-hooks');
690
- }
691
-
692
- /** Gemini's hooks trust, observed through the adapter's own API. How trust is computed — the
693
- * fingerprint semantics, which context fields matter — lives in src/adapters/gemini (review A3:
694
- * this file used to import planGeminiHooks and re-derive the plan itself). */
695
- function geminiHookTrust(target ) {
696
- return geminiHookTrustFor(target);
1010
+ hookIds.has(resource.assetId) || resource.kind === 'hooks-file');
697
1011
  }
698
1012
 
699
1013
  /** General, per-harness hook-wiring status — the one place install status distinguishes an
@@ -702,18 +1016,12 @@ function geminiHookTrust(target ) {
702
1016
  * - 'absent': this harness owns no hooks resource in the current verification (nothing installed).
703
1017
  * - 'installed-pending': a hooks resource is installed, but a prerequisite is unmet — Codex's
704
1018
  * static `capabilities.hooks.prerequisites` (`trusted-project`, `hook-review`), which DoFlow has
705
- * no way to confirm a human satisfied, so it is reported unmet unconditionally; or Gemini's live
706
- * trust computation via geminiHookTrust above.
1019
+ * no way to confirm a human satisfied, so it is reported unmet unconditionally.
707
1020
  * - 'active': installed with no unmet prerequisite (Claude and Kiro today, since neither declares
708
1021
  * any `capabilities.hooks.prerequisites` and Kiro hooks activate automatically per kiro.dev). */
709
1022
  function hookWiringStatus(harness , target , verification ) {
710
1023
  if (!target || target.skipped) return { status: 'absent', prerequisites: [] };
711
1024
  if (!verificationOwnsHooks(verification, hookAssetIds(target))) return { status: 'absent', prerequisites: [] };
712
- if (harness.id === 'gemini') {
713
- const trust = geminiHookTrust(target);
714
- if (trust?.required && !trust.trusted) return { status: 'installed-pending', prerequisites: ['hook-trust'] };
715
- return { status: 'active', prerequisites: [] };
716
- }
717
1025
  const prerequisites = harness.capabilities?.hooks?.prerequisites || [];
718
1026
  return prerequisites.length
719
1027
  ? { status: 'installed-pending', prerequisites: [...prerequisites] }
@@ -734,11 +1042,17 @@ function assertBashAvailableForHooks(plan , mode , hasBashCapableShellFn
734
1042
  }
735
1043
 
736
1044
  function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.ledger, context = {}, mode = 'apply', acceptPrerequisites = false, writeLedgerFn = writeLedger, writeRecoveryRecordFn = writeRecoveryRecord, hasBashCapableShellFn = hasBashCapableShell } ) {
737
- assertPlanApplicable(plan, stateRoot, acceptPrerequisites);
1045
+ assertPlanApplicable(plan, stateRoot, acceptPrerequisites, mode);
738
1046
  assertBashAvailableForHooks(plan, mode, hasBashCapableShellFn);
739
1047
  const recovery = writeRecoveryRecordFn(stateRoot, { status: 'pending', changes: plan.changes });
740
1048
  const completedHarnesses = [];
741
1049
  try {
1050
+ // The retirement first (IC-003): it touches only files no live harness claims, and a throw here
1051
+ // leaves every live harness unapplied.
1052
+ for (const retired of plan.retiredHarnesses ?? []) {
1053
+ applyRetiredHarnesses([retired]);
1054
+ completedHarnesses.push(`retired:${retired.harness}`);
1055
+ }
742
1056
  for (const target of plan.targets) {
743
1057
  if (target.skipped || target.changes.length === 0) continue;
744
1058
  // A retained change is a ledger release, not a native mutation: the adapter must never see
@@ -749,7 +1063,12 @@ function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.led
749
1063
  if (!applicable.length) continue;
750
1064
  const harness = harnessFor(registry, target.harness);
751
1065
  const adapter = resolveAdapter(adapters, harness);
752
- adapter[mode === 'remove' ? 'remove' : 'apply']({ ...target.adapterInput, registry, changes: applicable, ledger, recoveryRef: recovery.id });
1066
+ // Hook-tier removals (maint-hooks) run through the adapter's own remove path, before the content
1067
+ // apply, so a shared native file is read by each step as the step before left it.
1068
+ const tierRemovals = mode === 'remove' ? [] : applicable.filter((change ) => change.hookTier === true);
1069
+ if (tierRemovals.length) adapter.remove({ ...target.hookTierInput, registry, changes: tierRemovals, ledger, recoveryRef: recovery.id });
1070
+ const rest = tierRemovals.length ? applicable.filter((change ) => change.hookTier !== true) : applicable;
1071
+ if (rest.length) adapter[mode === 'remove' ? 'remove' : 'apply']({ ...target.adapterInput, registry, changes: rest, ledger, recoveryRef: recovery.id });
753
1072
  completedHarnesses.push(target.harness);
754
1073
  }
755
1074
  } catch (error ) {
@@ -767,7 +1086,8 @@ function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.led
767
1086
  throw new Error('Lifecycle verification failed; ledger was not updated');
768
1087
  }
769
1088
  try {
770
- applyMcpIndex({ scopeRoot: plan.scopeRoot, selectedMcp: plan.mcp, mode, retain: !removalIsTotal(plan) });
1089
+ // A package-only plan selects no MCP server; the index stays as the last install wrote it.
1090
+ if (!plan.packagesOnly) applyMcpIndex({ scopeRoot: plan.scopeRoot, selectedMcp: plan.mcp, mode, retain: !removalIsTotal(plan, registry) });
771
1091
  } catch (error ) {
772
1092
  // Same reasoning as the harness-loop catch above: every native resource already applied and
773
1093
  // verified successfully by this point, so leaving the record at 'pending' on a failure here
@@ -775,9 +1095,13 @@ function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.led
775
1095
  writeRecoveryRecordFn(stateRoot, { ...recovery.record, status: 'failed', completedHarnesses, verification, error: error.message });
776
1096
  throw error;
777
1097
  }
778
- const nextLedger = updateLedger({ ledger, scope: plan.scope, scopeRoot: plan.scopeRoot, verifications: verification.verifications, changes: plan.changes, recoveryRef: recovery.id });
1098
+ const nextLedger = updateLedger({ ledger, scope: plan.scope, scopeRoot: plan.scopeRoot, verifications: verification.verifications, changes: plan.changes, recoveryRef: recovery.id,
1099
+ retiredHarnesses: plan.retiredHarnesses ?? [] });
779
1100
  writeLedgerFn(stateRoot, nextLedger);
780
1101
  writeRecoveryRecordFn(stateRoot, { ...recovery.record, status: 'verified', verification });
1102
+ if (plan.scope === 'global' && plan.changes.some((change ) => isPackageAsset(change.assetId))) {
1103
+ settleSuspendedProjection({ homeDir: plan.scopeRoot });
1104
+ }
781
1105
  return { recovery, verification, ledger: nextLedger, retained: plan.retained ?? [] };
782
1106
  }
783
1107
 
@@ -804,9 +1128,33 @@ function recordMcpOwnership({ plan, registry, adapters, stateRoot, ledger = plan
804
1128
  return { ledger: next, recorded };
805
1129
  }
806
1130
 
1131
+ function settleSuspendedProjection({ homeDir } ) {
1132
+ try {
1133
+ const { readState, recordSuspension } = require('../runtime/packages/state.js');
1134
+ const { stateRoot, readLedger } = require('../state/index.js');
1135
+ const root = stateRoot({ scope: 'global', projectRoot: homeDir, homeDir });
1136
+ const ledger = readLedger(root);
1137
+ const read = readState({ homeDir });
1138
+ if (!ledger || read.unreadable) return;
1139
+ for (const suspension of read.state.suspended) {
1140
+ if (!suspension.projected) continue;
1141
+ const prefix = `pkg.${suspension.package}.`;
1142
+ const stillProjected = ledger.resources.some((row ) =>
1143
+ typeof row.assetId === 'string' && row.assetId.startsWith(prefix));
1144
+ if (!stillProjected) {
1145
+ recordSuspension({ homeDir }, {
1146
+ packageId: suspension.package, reason: suspension.reason, since: suspension.since, projected: false,
1147
+ });
1148
+ }
1149
+ }
1150
+ } catch {
1151
+ // Settlement is best-effort and never blocks the verb that removed package rows.
1152
+ }
1153
+ }
1154
+
807
1155
  function removeLifecycle(options ) {
808
1156
  const plan = planLifecycle({ ...options, context: { ...(options.context || {}), operation: 'remove' } });
809
1157
  return applyLifecycle({ ...options, plan, mode: 'remove', acceptPrerequisites: options.acceptPrerequisites });
810
1158
  }
811
1159
 
812
- module.exports = { OPERATIONS, MCP_KEEP, recordMcpOwnership, registryScope, normalizeTargets, normalizeChange, matchesChange, normalizeRemovalVerification, adapterConflicts, planLifecycle, verifyLifecycle, applyLifecycle, removeLifecycle, updateLedger, mcpIndexPath, applyMcpIndex, targetNeedsHooks, assertBashAvailableForHooks, hookWiringStatus, markRetainedRemovals, retentionSummary, RUNTIME_TREE_ASSETS };
1160
+ module.exports = { OPERATIONS, MCP_KEEP, recordMcpOwnership, isPackageAsset, registryScope, normalizeTargets, normalizeChange, matchesChange, normalizeRemovalVerification, adapterConflicts, planLifecycle, verifyLifecycle, applyLifecycle, removeLifecycle, updateLedger, mcpIndexPath, applyMcpIndex, targetNeedsHooks, assertBashAvailableForHooks, hookWiringStatus, markRetainedRemovals, retentionSummary, PRUNED_TREES, settleSuspendedProjection };