@khoavu882/doflow 2.0.0-beta.5 → 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 (471) hide show
  1. package/README.md +12 -11
  2. package/bin/doflow.js +21 -11
  3. package/core/.antigravity-plugin/plugin.json +1 -2
  4. package/core/.claude-plugin/plugin.json +1 -1
  5. package/core/.codex-plugin/plugin.json +1 -1
  6. package/core/.plugin/plugin.json +1 -1
  7. package/core/harnesses/claude/hooks/lib.sh +43 -24
  8. package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
  9. package/core/harnesses/claude/hooks/post-compact.sh +35 -32
  10. package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
  11. package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
  12. package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
  13. package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
  14. package/core/harnesses/claude/hooks/session-end.sh +23 -10
  15. package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
  16. package/core/harnesses/claude/settings/settings.json +18 -0
  17. package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
  18. package/core/harnesses/codex/agents/core-implementer.toml +1 -0
  19. package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
  20. package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
  21. package/core/harnesses/codex/agents/research-writer.toml +1 -1
  22. package/core/harnesses/codex/agents/reviewer.toml +10 -0
  23. package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
  24. package/core/harnesses/codex/agents/system-architect.toml +1 -1
  25. package/core/harnesses/codex/hooks/lib.sh +44 -25
  26. package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
  27. package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
  28. package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
  29. package/core/harnesses/codex/hooks/subagent-audit.sh +1 -0
  30. package/core/harnesses/copilot/hooks/doflow-lib.sh +93 -0
  31. package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
  32. package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +9 -0
  33. package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
  34. package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
  35. package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
  36. package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
  37. package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
  38. package/core/harnesses/copilot/hooks/doflow.json +57 -0
  39. package/core/harnesses/kiro/agents/doflow.json +1 -1
  40. package/core/harnesses/kiro/hooks/lib.sh +26 -6
  41. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +1 -0
  42. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +1 -0
  43. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +1 -0
  44. package/core/harnesses/kiro/hooks/session-start.sh +1 -0
  45. package/core/harnesses/kiro/hooks/stop-check.sh +1 -0
  46. package/core/harnesses/pi/extensions/doflow-guard.js +8 -0
  47. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +0 -1
  48. package/core/harnesses/shared/hooks/bridge/bridge.sh +102 -9
  49. package/core/harnesses/shared/hooks/bridge/envelope.jq +87 -18
  50. package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
  51. package/core/harnesses/shared/hooks/bridge/respond.sh +10 -2
  52. package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +28 -14
  53. package/core/harnesses/shared/hooks/policies/capture-failure.sh +48 -15
  54. package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
  55. package/core/harnesses/shared/hooks/policies/contract.sh +3 -0
  56. package/core/harnesses/shared/hooks/policies/deny-json.sh +2 -11
  57. package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
  58. package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
  59. package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
  60. package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
  61. package/core/harnesses/shared/hooks/policies/lib.sh +63 -40
  62. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +3 -2
  63. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +213 -12
  64. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +114 -38
  65. package/core/harnesses/shared/hooks/policies/session-context.sh +30 -1
  66. package/core/harnesses/shared/hooks/policies/stop-check.sh +20 -9
  67. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +5 -2
  68. package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
  69. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +60 -155
  70. package/core/harnesses/shared/hooks/stream-hook-runner.js +47 -8
  71. package/core/harnesses/shared/locator/doflow-run +2 -3
  72. package/core/registry/advice.json +3 -3
  73. package/core/registry/assets.json +55 -82
  74. package/core/registry/capabilities.json +12 -0
  75. package/core/registry/contracts.json +58 -31
  76. package/core/registry/contribution-points.json +32 -0
  77. package/core/registry/gates.json +32 -0
  78. package/core/registry/guidance-caps.json +40 -0
  79. package/core/registry/harnesses.json +1442 -363
  80. package/core/registry/lifecycle.json +485 -304
  81. package/core/registry/mcp.json +5 -1
  82. package/core/registry/models.json +7 -1
  83. package/core/registry/package-templates/README.md +19 -0
  84. package/core/registry/package-templates/agent/files/agent.md +9 -0
  85. package/core/registry/package-templates/agent/template.json +4 -0
  86. package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
  87. package/core/registry/package-templates/capability-provider/template.json +11 -0
  88. package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
  89. package/core/registry/package-templates/debate-preset/template.json +4 -0
  90. package/core/registry/package-templates/review-preset/files/preset.json +30 -0
  91. package/core/registry/package-templates/review-preset/template.json +4 -0
  92. package/core/registry/package-templates/skill/files/SKILL.md +8 -0
  93. package/core/registry/package-templates/skill/template.json +4 -0
  94. package/core/registry/package-templates/worker/files/worker.json +92 -0
  95. package/core/registry/package-templates/worker/template.json +11 -0
  96. package/core/registry/packages/doflow/doflow-package.json +15 -0
  97. package/core/registry/packages/doflow/presets/debate-3.json +26 -0
  98. package/core/registry/protocols.json +61 -0
  99. package/core/registry/routes.json +17 -0
  100. package/core/registry/rules.json +431 -0
  101. package/core/registry/settings.json +276 -0
  102. package/core/registry/storage.json +679 -31
  103. package/core/registry/telemetry.json +236 -0
  104. package/core/registry/transcripts.json +142 -0
  105. package/core/registry/verification.json +76 -1
  106. package/core/registry/workflows.json +55 -0
  107. package/core/shared/agent-specs/core-implementer.md +3 -0
  108. package/core/shared/agent-specs/delivery-lead.md +68 -0
  109. package/core/shared/agent-specs/quality-guardian.md +5 -1
  110. package/core/shared/agent-specs/research-writer.md +10 -3
  111. package/core/shared/agent-specs/reviewer.md +53 -0
  112. package/core/shared/agent-specs/spec-analyst.md +5 -6
  113. package/core/shared/agent-specs/system-architect.md +5 -1
  114. package/core/shared/guidance/DOFLOW_CORE.md +1 -5
  115. package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
  116. package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
  117. package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
  118. package/core/shared/guidance/pointers/codex.md +1 -4
  119. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
  120. package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
  121. package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
  122. package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
  123. package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
  124. package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
  125. package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
  126. package/core/shared/guidance/references/ORCHESTRATION.md +1 -2
  127. package/core/shared/guidance/rules/RULES.md +45 -0
  128. package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
  129. package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
  130. package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
  131. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
  132. package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
  133. package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
  134. package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
  135. package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
  136. package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
  137. package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
  138. package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
  139. package/core/shared/scripts/doflow/bash/validate-artifacts.sh +48 -4
  140. package/core/shared/scripts/doflow/bin/doflow-run +149 -34
  141. package/core/shared/skills/do/SKILL.md +12 -10
  142. package/core/shared/skills/do/references/advice_calls.md +26 -0
  143. package/core/shared/skills/do/references/pm_routing.md +1 -2
  144. package/core/shared/skills/do/references/program_mode.md +31 -0
  145. package/core/shared/skills/do-brainstorm/SKILL.md +20 -74
  146. package/core/shared/skills/do-brainstorm/references/handoff_branches.md +35 -0
  147. package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
  148. package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
  149. package/core/shared/skills/do-code-review/SKILL.md +41 -144
  150. package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
  151. package/core/shared/skills/do-code-review/references/tools.md +90 -0
  152. package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
  153. package/core/shared/skills/do-design/SKILL.md +18 -17
  154. package/core/shared/skills/do-diagnose/SKILL.md +14 -4
  155. package/core/shared/skills/do-document/SKILL.md +11 -1
  156. package/core/shared/skills/do-document/references/deep_research.md +1 -2
  157. package/core/shared/skills/do-document/references/embedded-research.md +18 -6
  158. package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
  159. package/core/shared/skills/do-execute-plan/SKILL.md +15 -58
  160. package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
  161. package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
  162. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +26 -1
  163. package/core/shared/skills/do-execute-plan/references/run_position.md +13 -1
  164. package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
  165. package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
  166. package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
  167. package/core/shared/skills/do-flow/SKILL.md +8 -9
  168. package/core/shared/skills/do-flow/references/task_classes.md +2 -2
  169. package/core/shared/skills/do-git/SKILL.md +4 -0
  170. package/core/shared/skills/do-implement/SKILL.md +4 -7
  171. package/core/shared/skills/do-plan/SKILL.md +26 -15
  172. package/core/shared/skills/do-test/SKILL.md +7 -4
  173. package/core/shared/templates/doflow/constitution-template.md +9 -3
  174. package/core/shared/templates/doflow/data-model-template.md +1 -1
  175. package/core/shared/templates/doflow/design-template.md +113 -16
  176. package/core/shared/templates/doflow/plan-template.md +11 -10
  177. package/core/shared/templates/doflow/question-log-template.md +14 -2
  178. package/core/shared/templates/doflow/requirement-template.md +1 -1
  179. package/core/worker-evidence/antigravity/checks.json +14 -0
  180. package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
  181. package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
  182. package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
  183. package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
  184. package/core/worker-evidence/codex/checks.json +38 -0
  185. package/package.json +10 -6
  186. package/src/adapters/antigravity/index.js +143 -66
  187. package/src/adapters/claude/index.js +24 -4
  188. package/src/adapters/codex/agents.js +25 -15
  189. package/src/adapters/codex/index.js +48 -13
  190. package/src/adapters/codex/native-projection.js +6 -2
  191. package/src/adapters/copilot/index.js +58 -23
  192. package/src/adapters/copy-tree.js +290 -44
  193. package/src/adapters/guidance-manifest.js +71 -0
  194. package/src/adapters/guidance-pointer.js +19 -0
  195. package/src/adapters/hook-commands.js +6 -10
  196. package/src/adapters/index.js +1 -0
  197. package/src/adapters/kiro/index.js +23 -12
  198. package/src/adapters/opencode/index.js +8 -8
  199. package/src/adapters/pi/hooks.js +2 -1
  200. package/src/adapters/pi/index.js +14 -7
  201. package/src/adapters/resolve-source.js +26 -2
  202. package/src/cli/commands/install.js +2 -1
  203. package/src/cli/commands/reconcile.js +53 -9
  204. package/src/cli/commands/remove.js +2 -2
  205. package/src/cli/commands/rollback.js +6 -3
  206. package/src/cli/commands/self-update.js +18 -9
  207. package/src/cli/commands/status.js +21 -2
  208. package/src/cli/commands/ui.js +99 -0
  209. package/src/cli/commands/update.js +2 -1
  210. package/src/cli/index.js +322 -29
  211. package/src/cli/runtime-commands.js +209 -57
  212. package/src/cli/shared.js +0 -2
  213. package/src/helper/git.js +1 -1
  214. package/src/helper/guardrails.js +1 -1
  215. package/src/helper/marker-merge.js +31 -13
  216. package/src/helper/package-ids.js +37 -0
  217. package/src/helper/semver.js +94 -0
  218. package/src/install/backup.js +105 -15
  219. package/src/install/context.js +1 -1
  220. package/src/install/targets.js +31 -16
  221. package/src/lifecycle/coverage.js +20 -3
  222. package/src/lifecycle/hook-tier.js +160 -0
  223. package/src/lifecycle/index.js +412 -78
  224. package/src/lifecycle/retired-harnesses.js +318 -0
  225. package/src/lifecycle/view.js +35 -15
  226. package/src/registry/index.js +551 -26
  227. package/src/registry/worker-ids.js +45 -0
  228. package/src/release/build-package.js +0 -3
  229. package/src/release/docs-blocks.js +492 -0
  230. package/src/release/preflight.js +425 -0
  231. package/src/release/rehearse.js +401 -0
  232. package/src/release/release-workflow.js +733 -0
  233. package/src/release/sync-plugin-versions.js +43 -7
  234. package/src/runtime/advice/bench-gate.js +25 -0
  235. package/src/runtime/advice/cli.js +59 -16
  236. package/src/runtime/advice/client.js +32 -9
  237. package/src/runtime/advice/config.js +98 -133
  238. package/src/runtime/advice/doctor.js +1 -1
  239. package/src/runtime/advice/elevation.js +97 -0
  240. package/src/runtime/advice/endpoint-guard.js +9 -3
  241. package/src/runtime/advice/mode.js +37 -0
  242. package/src/runtime/advice/record.js +18 -5
  243. package/src/runtime/advice/registry.js +4 -1
  244. package/src/runtime/advice/shadow-config.js +229 -0
  245. package/src/runtime/advice/shadow-dispatcher.js +276 -0
  246. package/src/runtime/brief-rules.js +81 -0
  247. package/src/runtime/brief-tools.js +111 -34
  248. package/src/runtime/candidate.js +20 -11
  249. package/src/runtime/capabilities/executor.js +450 -0
  250. package/src/runtime/capabilities/loopback.js +41 -0
  251. package/src/runtime/capabilities/sanitizer.js +88 -0
  252. package/src/runtime/capability-router.js +135 -3
  253. package/src/runtime/checkouts.js +73 -11
  254. package/src/runtime/claims.js +13 -7
  255. package/src/runtime/cli.js +196 -19
  256. package/src/runtime/config-home.js +42 -0
  257. package/src/runtime/context/arbitrate.js +767 -0
  258. package/src/runtime/context/delivery-store.js +4 -6
  259. package/src/runtime/context/emission-rules.js +118 -0
  260. package/src/runtime/context/identity.js +227 -0
  261. package/src/runtime/context/primary-command.js +117 -0
  262. package/src/runtime/context/primary.js +344 -0
  263. package/src/runtime/context/summary-store.js +655 -0
  264. package/src/runtime/context-pack.js +154 -78
  265. package/src/runtime/decision-register.js +13 -4
  266. package/src/runtime/dispatch/admission.js +392 -0
  267. package/src/runtime/dispatch/attempt-end.js +274 -0
  268. package/src/runtime/dispatch/command.js +628 -0
  269. package/src/runtime/dispatch/controls.js +904 -0
  270. package/src/runtime/dispatch/envelope.js +276 -0
  271. package/src/runtime/dispatch/grants.js +310 -0
  272. package/src/runtime/dispatch/integrate.js +617 -0
  273. package/src/runtime/dispatch/loop.js +1835 -0
  274. package/src/runtime/dispatch/store.js +352 -0
  275. package/src/runtime/dispatch/verify-run.js +201 -0
  276. package/src/runtime/evidence-ledger.js +34 -7
  277. package/src/runtime/failure/capture.js +3 -1
  278. package/src/runtime/failure/cli.js +25 -9
  279. package/src/runtime/failure/home.js +57 -30
  280. package/src/runtime/failure/store.js +7 -2
  281. package/src/runtime/feature-pointer.js +174 -0
  282. package/src/runtime/feature-resolve.js +13 -2
  283. package/src/runtime/fleet/command.js +77 -0
  284. package/src/runtime/fleet/projects.js +178 -0
  285. package/src/runtime/fleet/tree.js +383 -0
  286. package/src/runtime/freshness.js +33 -3
  287. package/src/runtime/git-memo.js +46 -0
  288. package/src/runtime/guidance/ablation.js +427 -0
  289. package/src/runtime/health.js +565 -17
  290. package/src/runtime/history-compactor.js +13 -3
  291. package/src/runtime/implementation-gate.js +277 -22
  292. package/src/runtime/inventory/coverage.js +1 -1
  293. package/src/runtime/inventory/index.js +1 -1
  294. package/src/runtime/inventory/precedence.js +2 -2
  295. package/src/runtime/inventory/siblings.js +6 -4
  296. package/src/runtime/knowledge/code-corpus.js +277 -0
  297. package/src/runtime/knowledge/code-lexical.js +130 -0
  298. package/src/runtime/knowledge/code-retrieval.js +307 -0
  299. package/src/runtime/knowledge/code-validation.js +106 -0
  300. package/src/runtime/knowledge/gp3-gate.js +74 -0
  301. package/src/runtime/knowledge/graph-adapter.js +288 -0
  302. package/src/runtime/knowledge/graph-fallback.js +394 -0
  303. package/src/runtime/knowledge/hybrid.js +136 -8
  304. package/src/runtime/knowledge/retrieval.js +34 -1
  305. package/src/runtime/knowledge/rtk-compress.js +77 -0
  306. package/src/runtime/knowledge/semble-bridge.py +106 -0
  307. package/src/runtime/knowledge/semble-lifecycle.js +114 -0
  308. package/src/runtime/knowledge/semble-provider.js +365 -0
  309. package/src/runtime/lifecycle/cli.js +16 -6
  310. package/src/runtime/lifecycle/event-store.js +8 -6
  311. package/src/runtime/lifecycle/followup.js +6 -1
  312. package/src/runtime/lifecycle/overview.js +24 -1
  313. package/src/runtime/lifecycle/report-store.js +8 -10
  314. package/src/runtime/lifecycle/retention.js +95 -31
  315. package/src/runtime/lifecycle/root.js +9 -1
  316. package/src/runtime/lifecycle/status.js +34 -17
  317. package/src/runtime/lifecycle/store-upkeep.js +56 -7
  318. package/src/runtime/mask.js +134 -7
  319. package/src/runtime/mcp-consultations.js +40 -6
  320. package/src/runtime/model-router.js +79 -7
  321. package/src/runtime/models/catalogue.js +191 -0
  322. package/src/runtime/models/command.js +153 -0
  323. package/src/runtime/off-words.js +12 -0
  324. package/src/runtime/outcome.js +2 -2
  325. package/src/runtime/packages/command.js +1009 -0
  326. package/src/runtime/packages/contributions.js +116 -0
  327. package/src/runtime/packages/digest.js +132 -0
  328. package/src/runtime/packages/fetch.js +182 -0
  329. package/src/runtime/packages/grants.js +192 -0
  330. package/src/runtime/packages/index-file.js +134 -0
  331. package/src/runtime/packages/instruction.js +90 -0
  332. package/src/runtime/packages/manifest.js +331 -0
  333. package/src/runtime/packages/points.js +477 -0
  334. package/src/runtime/packages/programs.js +216 -0
  335. package/src/runtime/packages/scaffold.js +277 -0
  336. package/src/runtime/packages/source.js +90 -0
  337. package/src/runtime/packages/state.js +164 -0
  338. package/src/runtime/packages/store.js +449 -0
  339. package/src/runtime/packages/tar.js +106 -0
  340. package/src/runtime/packages/view.js +406 -0
  341. package/src/runtime/plan-check.js +59 -0
  342. package/src/runtime/plan-content.js +324 -0
  343. package/src/runtime/policy-init.js +118 -0
  344. package/src/runtime/profiles/doctor.js +96 -0
  345. package/src/runtime/profiles/resolve.js +244 -0
  346. package/src/runtime/profiles/rollback.js +254 -0
  347. package/src/runtime/profiles/sampler.js +41 -0
  348. package/src/runtime/readiness-record.js +16 -11
  349. package/src/runtime/readiness.js +46 -5
  350. package/src/runtime/research-request.js +50 -25
  351. package/src/runtime/retrieval-plan.js +10 -30
  352. package/src/runtime/retrieval-record.js +75 -5
  353. package/src/runtime/review/command.js +246 -0
  354. package/src/runtime/review/dominance.js +68 -0
  355. package/src/runtime/review/findings.js +434 -0
  356. package/src/runtime/review/identity.js +255 -0
  357. package/src/runtime/review/protocol-command.js +109 -0
  358. package/src/runtime/review/protocols.js +405 -0
  359. package/src/runtime/review/resolve-protocol.js +302 -0
  360. package/src/runtime/review/start.js +530 -0
  361. package/src/runtime/roadmap/facts.js +479 -0
  362. package/src/runtime/roadmap/gates.js +270 -0
  363. package/src/runtime/roadmap/loop.js +1636 -0
  364. package/src/runtime/roadmap/merge.js +381 -0
  365. package/src/runtime/roadmap/rows.js +1113 -0
  366. package/src/runtime/roadmap/seed.js +951 -0
  367. package/src/runtime/rules-report.js +336 -0
  368. package/src/runtime/scaffold/artifacts.js +60 -9
  369. package/src/runtime/scaffold/generate.js +21 -9
  370. package/src/runtime/settings/autopilot.js +106 -0
  371. package/src/runtime/settings/cli.js +280 -0
  372. package/src/runtime/settings/config-write.js +314 -0
  373. package/src/runtime/settings/files.js +271 -0
  374. package/src/runtime/settings/legacy.js +154 -0
  375. package/src/runtime/settings/lexical.js +56 -0
  376. package/src/runtime/settings/prepared-run.js +269 -0
  377. package/src/runtime/settings/project-layers.js +118 -0
  378. package/src/runtime/settings/refs.js +288 -0
  379. package/src/runtime/settings/registry.js +553 -0
  380. package/src/runtime/settings/resolve.js +713 -0
  381. package/src/runtime/settings/shared.js +37 -0
  382. package/src/runtime/settings/worker-billing.js +107 -0
  383. package/src/runtime/settings/write.js +269 -0
  384. package/src/runtime/slot-command.js +78 -0
  385. package/src/runtime/slots.js +671 -0
  386. package/src/runtime/source-version.js +1 -0
  387. package/src/runtime/state-root.js +217 -0
  388. package/src/runtime/storage/cli.js +12 -5
  389. package/src/runtime/storage/engine.js +9 -2
  390. package/src/runtime/storage/registry.js +14 -1
  391. package/src/runtime/storage/roots.js +66 -16
  392. package/src/runtime/storage/rules.js +82 -10
  393. package/src/runtime/storage/upkeep.js +10 -2
  394. package/src/runtime/storage/work-status.js +245 -26
  395. package/src/runtime/task-attempts.js +589 -69
  396. package/src/runtime/task-classifier.js +47 -11
  397. package/src/runtime/task-graph.js +373 -0
  398. package/src/runtime/task-run.js +778 -0
  399. package/src/runtime/task-scope.js +176 -55
  400. package/src/runtime/task-sources.js +147 -0
  401. package/src/runtime/task-state.js +374 -64
  402. package/src/runtime/team/admission.js +301 -0
  403. package/src/runtime/team/envelope.js +324 -0
  404. package/src/runtime/team/review-guard.js +156 -0
  405. package/src/runtime/trace/analyze.js +427 -0
  406. package/src/runtime/trace/baseline.js +770 -0
  407. package/src/runtime/trace/ledger.js +63 -140
  408. package/src/runtime/trace/otlp-export.js +568 -0
  409. package/src/runtime/trace/otlp-map.js +481 -0
  410. package/src/runtime/trace/spans.js +745 -0
  411. package/src/runtime/trace/telemetry.js +225 -0
  412. package/src/runtime/trace/views.js +1 -0
  413. package/src/runtime/transcripts/command.js +98 -0
  414. package/src/runtime/transcripts/cursor.js +326 -0
  415. package/src/runtime/transcripts/read.js +126 -0
  416. package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
  417. package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
  418. package/src/runtime/transcripts/readers/copilot-events.js +90 -0
  419. package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
  420. package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
  421. package/src/runtime/transcripts/registry.js +161 -0
  422. package/src/runtime/transcripts/stats.js +161 -0
  423. package/src/runtime/verification/check-key.js +628 -0
  424. package/src/runtime/verification/contract-runner.js +546 -11
  425. package/src/runtime/verification/engine.js +499 -9
  426. package/src/runtime/verification/impact.js +617 -0
  427. package/src/runtime/verification/inflight.js +205 -0
  428. package/src/runtime/verification/registry.js +176 -0
  429. package/src/runtime/verification/reuse.js +184 -0
  430. package/src/runtime/verification/telemetry-emit.js +254 -0
  431. package/src/runtime/verification/test-selection.js +67 -7
  432. package/src/runtime/worker/active.js +90 -0
  433. package/src/runtime/worker/cancel.js +222 -0
  434. package/src/runtime/worker/catalogue.js +122 -0
  435. package/src/runtime/worker/command.js +677 -0
  436. package/src/runtime/worker/evidence.js +288 -0
  437. package/src/runtime/worker/launch.js +288 -0
  438. package/src/runtime/worker/models.js +137 -0
  439. package/src/runtime/worker/parsers.js +218 -0
  440. package/src/runtime/worker/probes/index.js +706 -0
  441. package/src/runtime/worker/process-tree.js +415 -0
  442. package/src/runtime/worker/quota.js +43 -0
  443. package/src/runtime/worker/readiness.js +713 -0
  444. package/src/runtime/worker/run-store.js +166 -0
  445. package/src/runtime/worker/supervisor.js +541 -0
  446. package/src/runtime/worker/worktree-check.js +252 -0
  447. package/src/runtime/workflow-engine.js +5 -1
  448. package/src/runtime/workflow-orchestrator.js +578 -63
  449. package/src/runtime/worktree.js +149 -22
  450. package/src/state/index.js +1 -1
  451. package/src/state/lockfile.js +64 -10
  452. package/src/state/run-lock.js +1 -1
  453. package/types/node-commonjs.d.ts +106 -7
  454. package/core/harnesses/gemini/hooks/hooks.json +0 -77
  455. package/core/harnesses/gemini/hooks/lib.sh +0 -302
  456. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -10
  457. package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
  458. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -10
  459. package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
  460. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -10
  461. package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
  462. package/core/harnesses/gemini/hooks/session-end.sh +0 -5
  463. package/core/harnesses/gemini/hooks/session-start.sh +0 -7
  464. package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
  465. package/core/shared/guidance/FLAGS.md +0 -25
  466. package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
  467. package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
  468. package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
  469. package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
  470. package/src/adapters/gemini/hooks.js +0 -251
  471. package/src/adapters/gemini/index.js +0 -429
@@ -8,15 +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');
19
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');
20
23
 
21
24
  const OPERATIONS = new Set(['create', 'merge', 'update', 'remove']);
22
25
  /** A harness's MCP selection meaning "the servers it owns now", read through its adapter's
@@ -199,12 +202,14 @@ function releasedOwnershipKeys(harnessPlans , scope ) {
199
202
  * applied. Membership is decided per ROW, not per harness: a harness that is releasing its claim
200
203
  * on this file is not a claimant even if it is releasing nothing else, and a harness in the same
201
204
  * batch that keeps its row (an install that relocates one asset while a sibling's projection is
202
- * unchanged) still is. */
203
- 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 ) {
204
208
  const byTarget = new Map();
205
209
  for (const resource of ledger?.resources || []) {
206
210
  if (typeof resource?.target !== 'string' || typeof resource?.harness !== 'string') continue;
207
211
  if (releasedKeys.has(ownershipKey(resource))) continue;
212
+ if (isRetired(registry, resource.harness)) continue;
208
213
  const key = claimKey(resource);
209
214
  if (!byTarget.has(key)) byTarget.set(key, new Set());
210
215
  byTarget.get(key).add(resource.harness);
@@ -212,16 +217,10 @@ function survivingClaimantsByTarget(ledger , releasedKeys ) {
212
217
  return byTarget;
213
218
  }
214
219
 
215
- /** What one claim covers: a whole file, except an MCP entry, which is claimed on its own because
216
- * several harnesses keep their servers side by side in one MCP file. */
217
- function claimKey(record ) {
218
- return record.kind === 'mcp-server' ? `${record.target}\u0000${record.identity}` : record.target;
219
- }
220
-
221
220
  /** NFR-007: a removal reclaims only what no other harness still claims.
222
221
  *
223
222
  * Several assets project to ONE destination for several harnesses — `scripts.doflow` is a single
224
- * `<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
225
224
  * resolve to `<root>/.agents` at project scope — while ownership is recorded per harness. A
226
225
  * removal that deleted every file its own rows named therefore took the shared runtime out from
227
226
  * under the harnesses that are still installed, leaving them with rows pointing at files that no
@@ -233,8 +232,19 @@ function claimKey(record ) {
233
232
  * stays, and the last claimant's removal is the one that reclaims it. The neutral ledger is the
234
233
  * only thing consulted — it is already the record of who owns what, and a second ownership
235
234
  * registry would be one more thing to keep in sync. */
236
- function markRetainedRemovals(harnessPlans , ledger , scope ) {
237
- 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
+ }
238
248
  if (!claimants.size) return harnessPlans;
239
249
  return harnessPlans.map((target ) => {
240
250
  if (target.skipped) return target;
@@ -255,10 +265,11 @@ function markRetainedRemovals(harnessPlans , ledger , scope ) {
255
265
  }
256
266
 
257
267
  /** Whether this plan leaves no ownership row standing anywhere — the only case in which a shared,
258
- * install-wide artifact that no single harness owns is genuinely unclaimed. */
259
- 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 ) {
260
271
  const released = releasedOwnershipKeys(plan.targets || [], plan.scope);
261
- 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));
262
273
  }
263
274
 
264
275
  /** What a removal kept and why, ready to print. A command that says "removed" while silently
@@ -308,13 +319,17 @@ function harnessMcpSelection(registry , harness , raw ) {
308
319
  return [];
309
320
  }
310
321
 
311
- /** The runtime trees: each one source directory projected to one `<scopeRoot>/.doflow/runtime/...`
312
- * tree for every harness, with no layout and no transform (test/guards/runtime-tree.test.js pins
313
- * that). Nothing but the shared scripts reaches into them, so a file the current source no longer
314
- * ships is dead for every harness at once. Guidance and scripts are not here: a harness this run
315
- * does not update names files in those trees from its own native files, so NFR-007 retention is
316
- * right for them. */
317
- 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']);
318
333
 
319
334
  function sha256File(file , fsImpl ) {
320
335
  return crypto.createHash('sha256').update(fsImpl.readFileSync(file)).digest('hex');
@@ -364,12 +379,22 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
364
379
  const recorded = new Set(rows.map((row ) => row.target));
365
380
  const shown = (file ) => path.relative(scopeRoot, file);
366
381
 
367
- for (const assetId of RUNTIME_TREE_ASSETS) {
368
- 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
+ }
369
395
  if (!executor) continue;
370
- const asset = executor.assets.find((item ) => item.id === assetId);
371
- const nativeDir = typeof asset.nativeDir === 'string' ? asset.nativeDir : asset.nativeDir?.[executor.harness];
372
- const destDir = sharedTreeDestDir(scopeRoot, nativeDir);
396
+ const assetId = asset.id;
397
+ const destDir = sharedDest(executor, asset);
373
398
  const sourceDir = path.resolve(repoRoot, asset.source);
374
399
  if (!destDir || !fsImpl.existsSync(sourceDir) || !fsImpl.existsSync(destDir)) continue;
375
400
  const produced = discoverTree({ sourceDir, destDir, fsImpl }).files.map((file ) => file.destAbs);
@@ -386,7 +411,7 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
386
411
  const removalsAt = new Map ();
387
412
  for (const target of planned) {
388
413
  for (const change of target.changes) {
389
- 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;
390
415
  if (!removalsAt.has(change.target)) removalsAt.set(change.target, []);
391
416
  removalsAt.get(change.target) .push(change);
392
417
  }
@@ -397,11 +422,19 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
397
422
  if (!change.retained) replaced.get(change.harness) .set(change, Object.freeze({ ...change, retained: true, retainedFor: [] }));
398
423
  }
399
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
+ }
400
433
 
401
434
  for (const file of new Set([...rowsAt.keys(), ...removalsAt.keys()])) {
402
435
  const fileRows = rowsAt.get(file) ?? [];
403
436
  // Another asset's live row on this path is a claim this pass does not judge.
404
- if (fileRows.some((row ) => row.assetId !== assetId)) continue;
437
+ if (fileRows.some((row ) => !group.includes(row.assetId))) continue;
405
438
  const removals = removalsAt.get(file) ?? [];
406
439
  const identity = path.relative(destDir, file);
407
440
  // The rows no planned removal of this file releases on its own.
@@ -424,14 +457,25 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
424
457
  }
425
458
  if (sameFile(file)) { keepFile(file); continue; } // the produced file under an old spelling
426
459
  const current = sha256File(file, fsImpl);
427
- 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) {
428
466
  // A planned removal here was already kept by planTree, and its adapter named it.
429
467
  if (!removals.length) notices.get(executor.harness) .push(`kept ${shown(file)}: no longer shipped, changed since DoFlow wrote it`);
430
468
  continue;
431
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
+ }
432
476
  notices.get(executor.harness) .push(`prune ${shown(file)}: no longer shipped, unchanged since DoFlow wrote it`);
433
477
  // removeTree re-runs containment and the same-file check from this right before the unlink.
434
- const pruneGuard = { root: destDir, source: sourceDir };
478
+ const pruneGuard = { root: destDir, source: sourceDir, scope: scopeRoot };
435
479
  if (!removals.length) {
436
480
  added.get(executor.harness) .push(synthetic({ fingerprint: current, supersedes: superseded, pruneGuard }));
437
481
  continue;
@@ -446,7 +490,7 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
446
490
  // Files no row names and the source does not produce: never DoFlow's to delete, but named.
447
491
  for (const rel of walkRelFiles(destDir, fsImpl)) {
448
492
  const file = path.join(destDir, rel);
449
- 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;
450
494
  notices.get(executor.harness) .push(`kept ${shown(file)}: not shipped by this version and not recorded as DoFlow's`);
451
495
  }
452
496
  }
@@ -466,11 +510,230 @@ function targetFromResult({ harness, assets, mcp, mcpSelected, policies, adapter
466
510
  return { harness: harness.id, adapter: harness.adapter, assets, mcp, mcpSelected, policies, adapterInput, discovery, changes, requiredNativeResources, conflicts, prerequisites, notices: adapterNotices(result), skipped: false };
467
511
  }
468
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
+
469
716
  function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelections = {}, mcpAdoptable = {}, retainedMcpIds = [], ledger, context = {} } ) {
470
717
  assertScope(scope);
471
718
  if (!registry) throw new Error('registry is required');
472
719
  const baseLedger = ledger ?? defaultLedger({ scope, scopeRoot });
473
- 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) => {
474
737
  if (!harness.scopes.includes(registryScope(scope))) {
475
738
  return { harness: harness.id, assets: [], changes: [], conflicts: [`Harness '${harness.id}' does not support ${scope} scope`], prerequisites: [], notices: [], skipped: true, mcpSelected: null };
476
739
  }
@@ -480,7 +743,9 @@ function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelec
480
743
  const policies = renderPolicies(registry, { harness: harness.id });
481
744
  const adoptable = capable ? selectMcpServers(registry, catalogIds(registry, mcpAdoptable[harness.id] ?? [])) : [];
482
745
  const adapter = resolveAdapter(adapters, harness);
483
- 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 });
484
749
  // `keep` asks the adapter what it owns before the real projection: the probe carries no
485
750
  // selection, so discovery reads the files as they are.
486
751
  const ids = selection === MCP_KEEP
@@ -491,29 +756,54 @@ function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelec
491
756
  const input = { ...adapterInput, registry, ledger: baseLedger };
492
757
  const discovery = adapter.discover(input);
493
758
  const result = adapter.plan({ ...input, discovery });
494
- return targetFromResult({ harness, assets, mcp, mcpSelected: capable ? mcp.map((server ) => server.id) : null, policies, adapterInput, discovery, result });
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;
495
763
  });
496
764
  // The MCP index names every server a harness of this scope uses: the selections planned here and
497
765
  // the recorded selections of the harnesses this run does not touch.
498
766
  const selectedMcp = selectMcpServers(registry, catalogIds(registry,
499
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));
500
781
  // Only once every harness's plan is known: whether a file may be deleted depends on the rows
501
782
  // the WHOLE plan leaves standing, which no single harness's plan can see.
502
- const retained = markRetainedRemovals(harnessPlans, baseLedger, scope);
503
- const annotated = context.operation === 'remove' ? retained
783
+ const retained = markRetainedRemovals(withPackages, baseLedger, scope, registry);
784
+ const annotated = context.operation === 'remove' || packagesOnly ? retained
504
785
  : pruneRetiredRuntimeFiles(retained, baseLedger, { scopeRoot, repoRoot: context.repoRoot });
505
786
  // The shared hooks tree may have moved under harnesses this run does not name (IC-037): their
506
787
  // front doors come along, as targets of this same plan.
507
- const companions = planHookCompanions({ registry, adapters, scope, harnessScope: registryScope(scope), scopeRoot, ledger: baseLedger, context,
788
+ const companions = packagesOnly ? [] : planHookCompanions({ registry, adapters, scope, harnessScope: registryScope(scope), scopeRoot, ledger: baseLedger, context,
508
789
  targets: annotated, targetFromResult });
509
790
  const planned = [...annotated, ...companions];
510
- const changes = planned.flatMap((item ) => item.changes);
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)];
511
798
  const conflicts = planned.flatMap((item ) => item.conflicts.map((reason ) => ({ harness: item.harness, reason })));
512
799
  const prerequisites = planned.flatMap((item ) => item.prerequisites.map((prerequisite ) => ({ harness: item.harness, prerequisite })));
513
800
  const requiredNativeResources = planned.flatMap((item ) => item.requiredNativeResources || []);
514
- const notices = planned.flatMap((item ) => item.notices.map((notice ) => ({ harness: item.harness, notice })));
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}` }))];
515
803
  return Object.freeze({ scope, scopeRoot, mcp: selectedMcp, ledger: baseLedger, targets: planned, changes, requiredNativeResources, conflicts, prerequisites, notices,
516
- retained: retainedChanges(changes), safe: conflicts.length === 0 && prerequisites.length === 0 });
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 } : {}) });
517
807
  }
518
808
 
519
809
  function verificationResources(verification , changes , scope , recoveryRef ) {
@@ -571,11 +861,20 @@ function applyMcpIndex({ scopeRoot, selectedMcp, mode, retain = false, fsImpl =
571
861
  fsImpl.writeFileSync(file, content);
572
862
  }
573
863
 
574
- function updateLedger({ ledger, scope, scopeRoot, verifications, changes, recoveryRef, fsImpl = fs } ) {
864
+ function updateLedger({ ledger, scope, scopeRoot, verifications, changes, recoveryRef, retiredHarnesses = [], fsImpl = fs } ) {
575
865
  // upgradeLedger makes every written ledger v2-shaped (tombstone log present) while v1 inputs
576
866
  // stay readable — migration happens on write, never as a separate user-facing step.
577
867
  const next = JSON.parse(JSON.stringify(upgradeLedger(ledger ?? defaultLedger({ scope, scopeRoot }))));
578
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
+ }
579
878
  // Snapshot pre-run claims by ownership identity (harness+asset+identity) so a re-verification
580
879
  // that relocates the same claim to a new target is detectable as a MOVE, not remove-plus-add.
581
880
  const priorByIdentity = new Map();
@@ -657,10 +956,13 @@ function verifyLifecycle({ plan, adapters, context = {} } ) {
657
956
  ? target.changes.filter((change ) => !change.retained && (change.operation === 'create' || change.operation === 'update')).map((change ) => change.target)
658
957
  : undefined;
659
958
  // A hook-front-door companion (IC-037) is verified for its doors alone: the rest of that
660
- // harness is not part of this run and may legitimately trail the current source.
661
- let verification = normalizeVerification(adapter[target.hookDoorsOnly ? 'verifyHookDoors' : 'verify']({ ...target.adapterInput,
662
- registry: context.registry ?? plan.registry, discovery: target.discovery, ledger: plan.ledger,
663
- operation, context: { ...target.adapterInput.context, operation, ...(plannedWrites ? { plannedWrites } : {}) } }), harness);
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 });
664
966
  if (operation === 'remove') verification = normalizeRemovalVerification(verification, target.changes, plan.ledger);
665
967
  verifications.push({ ...verification, hookWiring: hookWiringStatus(harness, target, verification) });
666
968
  }
@@ -668,24 +970,32 @@ function verifyLifecycle({ plan, adapters, context = {} } ) {
668
970
  return { verifications, conflicts, ok: conflicts.length === 0 && verifications.every((result) => result.ok !== false) };
669
971
  }
670
972
 
671
- 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
+ }
672
980
  if (!plan.safe && !(acceptPrerequisites && plan.conflicts.length === 0)) throw new Error('Refusing to apply an unsafe lifecycle plan');
673
981
  if (!stateRoot) throw new Error('stateRoot is required before lifecycle mutation');
674
- 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');
675
985
  }
676
986
 
677
987
  /** Registry-declared hooks assets (`kind: "hooks"` in core/registry/assets.json) this harness's
678
- * plan selected — currently only claude.hooks-scripts and kiro.hooks-scripts. Codex and Gemini
679
- * 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
680
990
  * registered asset, so they never appear here. */
681
991
  function hookAssetIds(target ) {
682
992
  return new Set((target.assets || []).filter((asset ) => asset.kind === 'hooks').map((asset ) => asset.id));
683
993
  }
684
994
 
685
995
  /** Whether this harness's plan includes at least one hooks-bearing change. Claude's hooks are a
686
- * registry-declared asset (`kind: "hooks"` in core/registry/assets.json); Codex and Gemini deploy
687
- * hooks via their own bespoke code paths and tag the resulting change with `nativeComponent:
688
- * '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
689
999
  * signal is sufficient — this stays harness-agnostic on purpose. */
690
1000
  function targetNeedsHooks(target ) {
691
1001
  const hookIds = hookAssetIds(target);
@@ -694,20 +1004,10 @@ function targetNeedsHooks(target ) {
694
1004
 
695
1005
  /** Whether a verified resource is the hooks resource this harness owns. A registry-declared hooks
696
1006
  * asset (Claude, Kiro) is matched by assetId; Codex's bespoke hooks-file resource is matched by
697
- * `kind`; Gemini's bespoke hooks resource (a key inside settings.json, not a standalone file) is
698
- * matched by its `projection.renderer` tag — see src/adapters/codex/index.js's
699
- * `lifecycleResource(..., kind: 'hooks-file', ...)` and src/adapters/gemini/index.js's verify(),
700
- * which tags its hooks resource `projection: { renderer: 'gemini-hooks' }`. */
1007
+ * `kind` — see src/adapters/codex/index.js's `lifecycleResource(..., kind: 'hooks-file', ...)`. */
701
1008
  function verificationOwnsHooks(verification , hookIds ) {
702
1009
  return (verification?.resources || []).some((resource ) =>
703
- hookIds.has(resource.assetId) || resource.kind === 'hooks-file' || resource.projection?.renderer === 'gemini-hooks');
704
- }
705
-
706
- /** Gemini's hooks trust, observed through the adapter's own API. How trust is computed — the
707
- * fingerprint semantics, which context fields matter — lives in src/adapters/gemini (review A3:
708
- * this file used to import planGeminiHooks and re-derive the plan itself). */
709
- function geminiHookTrust(target ) {
710
- return geminiHookTrustFor(target);
1010
+ hookIds.has(resource.assetId) || resource.kind === 'hooks-file');
711
1011
  }
712
1012
 
713
1013
  /** General, per-harness hook-wiring status — the one place install status distinguishes an
@@ -716,18 +1016,12 @@ function geminiHookTrust(target ) {
716
1016
  * - 'absent': this harness owns no hooks resource in the current verification (nothing installed).
717
1017
  * - 'installed-pending': a hooks resource is installed, but a prerequisite is unmet — Codex's
718
1018
  * static `capabilities.hooks.prerequisites` (`trusted-project`, `hook-review`), which DoFlow has
719
- * no way to confirm a human satisfied, so it is reported unmet unconditionally; or Gemini's live
720
- * trust computation via geminiHookTrust above.
1019
+ * no way to confirm a human satisfied, so it is reported unmet unconditionally.
721
1020
  * - 'active': installed with no unmet prerequisite (Claude and Kiro today, since neither declares
722
1021
  * any `capabilities.hooks.prerequisites` and Kiro hooks activate automatically per kiro.dev). */
723
1022
  function hookWiringStatus(harness , target , verification ) {
724
1023
  if (!target || target.skipped) return { status: 'absent', prerequisites: [] };
725
1024
  if (!verificationOwnsHooks(verification, hookAssetIds(target))) return { status: 'absent', prerequisites: [] };
726
- if (harness.id === 'gemini') {
727
- const trust = geminiHookTrust(target);
728
- if (trust?.required && !trust.trusted) return { status: 'installed-pending', prerequisites: ['hook-trust'] };
729
- return { status: 'active', prerequisites: [] };
730
- }
731
1025
  const prerequisites = harness.capabilities?.hooks?.prerequisites || [];
732
1026
  return prerequisites.length
733
1027
  ? { status: 'installed-pending', prerequisites: [...prerequisites] }
@@ -748,11 +1042,17 @@ function assertBashAvailableForHooks(plan , mode , hasBashCapableShellFn
748
1042
  }
749
1043
 
750
1044
  function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.ledger, context = {}, mode = 'apply', acceptPrerequisites = false, writeLedgerFn = writeLedger, writeRecoveryRecordFn = writeRecoveryRecord, hasBashCapableShellFn = hasBashCapableShell } ) {
751
- assertPlanApplicable(plan, stateRoot, acceptPrerequisites);
1045
+ assertPlanApplicable(plan, stateRoot, acceptPrerequisites, mode);
752
1046
  assertBashAvailableForHooks(plan, mode, hasBashCapableShellFn);
753
1047
  const recovery = writeRecoveryRecordFn(stateRoot, { status: 'pending', changes: plan.changes });
754
1048
  const completedHarnesses = [];
755
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
+ }
756
1056
  for (const target of plan.targets) {
757
1057
  if (target.skipped || target.changes.length === 0) continue;
758
1058
  // A retained change is a ledger release, not a native mutation: the adapter must never see
@@ -763,7 +1063,12 @@ function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.led
763
1063
  if (!applicable.length) continue;
764
1064
  const harness = harnessFor(registry, target.harness);
765
1065
  const adapter = resolveAdapter(adapters, harness);
766
- 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 });
767
1072
  completedHarnesses.push(target.harness);
768
1073
  }
769
1074
  } catch (error ) {
@@ -781,7 +1086,8 @@ function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.led
781
1086
  throw new Error('Lifecycle verification failed; ledger was not updated');
782
1087
  }
783
1088
  try {
784
- 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) });
785
1091
  } catch (error ) {
786
1092
  // Same reasoning as the harness-loop catch above: every native resource already applied and
787
1093
  // verified successfully by this point, so leaving the record at 'pending' on a failure here
@@ -789,9 +1095,13 @@ function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.led
789
1095
  writeRecoveryRecordFn(stateRoot, { ...recovery.record, status: 'failed', completedHarnesses, verification, error: error.message });
790
1096
  throw error;
791
1097
  }
792
- 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 ?? [] });
793
1100
  writeLedgerFn(stateRoot, nextLedger);
794
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
+ }
795
1105
  return { recovery, verification, ledger: nextLedger, retained: plan.retained ?? [] };
796
1106
  }
797
1107
 
@@ -818,9 +1128,33 @@ function recordMcpOwnership({ plan, registry, adapters, stateRoot, ledger = plan
818
1128
  return { ledger: next, recorded };
819
1129
  }
820
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
+
821
1155
  function removeLifecycle(options ) {
822
1156
  const plan = planLifecycle({ ...options, context: { ...(options.context || {}), operation: 'remove' } });
823
1157
  return applyLifecycle({ ...options, plan, mode: 'remove', acceptPrerequisites: options.acceptPrerequisites });
824
1158
  }
825
1159
 
826
- 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 };