@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
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ // SemVer 2.0.0 parsing and precedence, and the package host-range grammar (IC-016).
4
+ // No dependency: DoFlow ships `2.0.0-beta.N`, so prerelease precedence is part of the contract.
5
+
6
+
7
+
8
+
9
+
10
+ const HOST_RANGE_GRAMMAR = 'one or more comparators separated by single spaces, all of which must hold: '
11
+ + '>=V, <V, =V or a bare V (exact), where V is a full semver 2.0.0 version; '
12
+ + 'an upper bound (<V, =V or a bare V) is required; no ^, ~, ||, x or *';
13
+
14
+ function parseSemVer(value ) {
15
+ const match = value.match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/);
16
+ if (!match) return null;
17
+ const prerelease = match[4] ? match[4].split('.') : [];
18
+ if (prerelease.some((identifier) => /^\d+$/.test(identifier)
19
+ && identifier.length > 1 && identifier.startsWith('0'))) return null;
20
+ return { major: match[1], minor: match[2], patch: match[3], prerelease };
21
+ }
22
+
23
+ function compareNumeric(left , right ) {
24
+ if (left.length !== right.length) return left.length < right.length ? -1 : 1;
25
+ if (left === right) return 0;
26
+ return left < right ? -1 : 1;
27
+ }
28
+
29
+ function compareSemVer(leftValue , rightValue ) {
30
+ const left = parseSemVer(leftValue);
31
+ const right = parseSemVer(rightValue);
32
+ if (!left || !right) return null;
33
+
34
+ for (const key of ['major', 'minor', 'patch'] ) {
35
+ const order = compareNumeric(left[key], right[key]);
36
+ if (order !== 0) return order;
37
+ }
38
+
39
+ if (left.prerelease.length === 0 && right.prerelease.length === 0) return 0;
40
+ if (left.prerelease.length === 0) return 1;
41
+ if (right.prerelease.length === 0) return -1;
42
+
43
+ const length = Math.min(left.prerelease.length, right.prerelease.length);
44
+ for (let index = 0; index < length; index++) {
45
+ const leftId = left.prerelease[index];
46
+ const rightId = right.prerelease[index];
47
+ const leftNumeric = /^\d+$/.test(leftId);
48
+ const rightNumeric = /^\d+$/.test(rightId);
49
+ if (leftNumeric && rightNumeric) {
50
+ const order = compareNumeric(leftId, rightId);
51
+ if (order !== 0) return order;
52
+ } else if (leftNumeric !== rightNumeric) {
53
+ return leftNumeric ? -1 : 1;
54
+ } else if (leftId !== rightId) {
55
+ return leftId < rightId ? -1 : 1;
56
+ }
57
+ }
58
+ if (left.prerelease.length === right.prerelease.length) return 0;
59
+ return left.prerelease.length < right.prerelease.length ? -1 : 1;
60
+ }
61
+
62
+ function refused(message ) {
63
+ return { ok: false, error: { code: 'bad-type', message, allowed: HOST_RANGE_GRAMMAR } };
64
+ }
65
+
66
+ // Never throws: a refused range comes back as { ok: false, error } for the caller to turn into a finding.
67
+ function parseHostRange(text ) {
68
+ if (typeof text !== 'string') return refused('the host range must be a string');
69
+ const comparators = [];
70
+ for (const token of text.split(' ')) {
71
+ const match = token.match(/^(>=|<|=)?(.+)$/);
72
+ if (!match || parseSemVer(match[2]) === null) {
73
+ return refused(`"${token}" is not a comparator of the host range grammar`);
74
+ }
75
+ comparators.push({ op: (match[1] || '=') , version: match[2] });
76
+ }
77
+ if (!comparators.some((comparator) => comparator.op !== '>=')) {
78
+ return refused('the host range has no upper bound');
79
+ }
80
+ return { ok: true, comparators };
81
+ }
82
+
83
+ // `range` is the range text or a parseHostRange result; a refused range or an invalid version satisfies nothing.
84
+ function satisfiesHostRange(version , range ) {
85
+ const parsed = typeof range === 'string' ? parseHostRange(range) : range;
86
+ if (!parsed.ok) return false;
87
+ return parsed.comparators.every(({ op, version: bound }) => {
88
+ const order = compareSemVer(version, bound);
89
+ if (order === null) return false;
90
+ return op === '>=' ? order >= 0 : op === '<' ? order < 0 : order === 0;
91
+ });
92
+ }
93
+
94
+ module.exports = { parseSemVer, compareSemVer, parseHostRange, satisfiesHostRange, HOST_RANGE_GRAMMAR };
@@ -24,7 +24,7 @@ const { sourceCommit: gitSourceCommit } = require('../helper/git.js');
24
24
  const {
25
25
  legacyBackupReadRoot, isLegacyBackupReadRoot, scopeRootFromCanonicalBackupRoot,
26
26
  } = require('./paths.js');
27
- const { validateLedger } = require('../state/index.js');
27
+ const { validateLedger, ownershipKey } = require('../state/index.js');
28
28
 
29
29
  /** Where a listed/restorable backup was found. 'current' = the canonical `.doflow/backups` root the
30
30
  * installer writes to; 'legacy' = the pre-7de6d5f `.claude/backups` root, readable only. Callers
@@ -69,6 +69,8 @@ function backupId(op , date ) {
69
69
  const MANIFEST_FILE = '.manifest.json';
70
70
  const OWNER_FILE = '.owner.json';
71
71
  const TEMP_PREFIX = '.tmp-';
72
+ /** A backup retention is removing: renamed here first so a half-removed backup is never listed or counted. */
73
+ const REMOVING_PREFIX = '.tmp-del-';
72
74
  const STALE_TEMP_MS = 24 * 60 * 60 * 1000;
73
75
  const MAX_ID_ATTEMPTS = 1000;
74
76
  const RENAME_COLLISION_CODES = new Set(['EEXIST', 'ENOTEMPTY', 'EPERM', 'EACCES']);
@@ -129,7 +131,9 @@ function createFileBackup({
129
131
  fsImpl.mkdirSync(backupRoot, { recursive: true });
130
132
  fsImpl.mkdirSync(tmpDir);
131
133
  tmpCreated = true;
132
- const owner = { pid: process.pid, hostname: os.hostname(), startedAt: date.toISOString() };
134
+ // When this run started, not `date`: that is the time the backup is named for, and a caller may pass any
135
+ // date. isStaleTemp measures a temp's age from this field.
136
+ const owner = { pid: process.pid, hostname: os.hostname(), startedAt: new Date().toISOString() };
133
137
  fsImpl.writeFileSync(path.join(tmpDir, OWNER_FILE), `${JSON.stringify(owner)}\n`, { flag: 'wx' });
134
138
  } catch (err) { throw writeFailure(err); }
135
139
 
@@ -234,6 +238,35 @@ function defaultIsAlive(pid ) {
234
238
  }
235
239
  }
236
240
 
241
+ /**
242
+ * The id of this process's pid namespace where the platform exposes one (Linux: the target of
243
+ * `/proc/self/ns/pid`, such as `pid:[4026531836]`), else the empty string. A pid means something only
244
+ * inside the namespace that issued it; two containers can share a hostname and not a pid space.
245
+ */
246
+ function pidNamespaceId() {
247
+ try { return fs.readlinkSync('/proc/self/ns/pid'); } catch { return ''; }
248
+ }
249
+
250
+ /**
251
+ * Eight hex digits that identify the host and pid namespace in a rename-aside name. A pid means
252
+ * something only in the namespace it was issued in, so an aside is judged by pid only by a process
253
+ * in the same namespace on the same host. Where no namespace id exists the tag is the hostname alone.
254
+ */
255
+ function hostTag(hostname = os.hostname(), pidNamespace = pidNamespaceId()) {
256
+ return crypto.createHash('sha1').update(`${hostname}\0${pidNamespace}`).digest('hex').slice(0, 8);
257
+ }
258
+
259
+ /**
260
+ * The owner a rename-aside folder (`.tmp-del-<id>-<host tag>-<pid>-<8 hex>`) names, or null when the
261
+ * name does not carry one. Ids contain `-`, so the fields are read from the end. Names without a host
262
+ * tag (an earlier layout that never shipped: it existed only on unreleased commits, so no migration)
263
+ * are unparseable and are kept.
264
+ */
265
+ function removingOwner(name ) {
266
+ const m = /-([0-9a-f]{8})-([0-9]+)-[0-9a-f]{8}$/.exec(name);
267
+ return m ? { host: m[1], pid: Number(m[2]) } : null;
268
+ }
269
+
237
270
  function isStaleTemp(dir , { now, fsImpl, isAlive, hostname } ) {
238
271
  let owner = null;
239
272
  try { owner = JSON.parse(fsImpl.readFileSync(path.join(dir, OWNER_FILE), 'utf8')); } catch { /* no owner */ }
@@ -247,15 +280,25 @@ function isStaleTemp(dir , { now, fsImpl, isAlive, hostname } )
247
280
  /**
248
281
  * Remove `.tmp-*` directories left in `backupRoot` by runs that died: the owner process is gone on
249
282
  * this host, or the temp is older than 24 hours. Returns how many were removed.
283
+ *
284
+ * A `.tmp-del-*` rename-aside is a backup a retention pass is deleting, so age says nothing about it
285
+ * (it keeps the backup's own, possibly old, mtime and may be rolled back if the delete fails). It is
286
+ * removed only by the host and pid namespace named in it, and only when the pid in its name is not
287
+ * alive; any other host's or namespace's aside, and one whose name carries no owner, is left alone.
250
288
  */
251
- function sweepStaleTemps(backupRoot , { now = Date.now(), fsImpl = fs, isAlive = defaultIsAlive, hostname = os.hostname() } = {}) {
289
+ function sweepStaleTemps(backupRoot , { now = Date.now(), fsImpl = fs, isAlive = defaultIsAlive, hostname = os.hostname(), pidNamespace = pidNamespaceId() } = {}) {
252
290
  assertMutableBackupRoot(backupRoot, 'clean up backups');
253
291
  if (!fsImpl.existsSync(backupRoot)) return 0;
254
292
  let removed = 0;
255
293
  for (const e of fsImpl.readdirSync(backupRoot, { withFileTypes: true })) {
256
294
  if (!e.isDirectory() || !e.name.startsWith(TEMP_PREFIX)) continue;
257
295
  const dir = path.join(backupRoot, e.name);
258
- if (!isStaleTemp(dir, { now, fsImpl, isAlive, hostname })) continue;
296
+ if (e.name.startsWith(REMOVING_PREFIX)) {
297
+ // Removed only by the host that made it, and only once its pid is gone there. Another host's
298
+ // (or an unparseable) aside is kept: its pass may be live where this one cannot see it.
299
+ const owner = removingOwner(e.name);
300
+ if (owner === null || owner.host !== hostTag(hostname, pidNamespace) || isAlive(owner.pid)) continue;
301
+ } else if (!isStaleTemp(dir, { now, fsImpl, isAlive, hostname })) continue;
259
302
  try {
260
303
  fsImpl.rmSync(dir, { recursive: true, force: true });
261
304
  removed += 1;
@@ -347,7 +390,7 @@ function formatBytes(n ) {
347
390
 
348
391
  /** Security guard: a backup id is a directory *name*, never a path — reject traversal/absolute ids. */
349
392
  function assertSafeBackupId(bid ) {
350
- if (!bid || bid.includes('/') || bid.includes('\\') || bid === '..' || bid.includes('..')) {
393
+ if (!bid || bid === '.' || bid.includes('/') || bid.includes('\\') || bid === '..' || bid.includes('..')) {
351
394
  throw new Error(`Invalid backup id: '${bid}'`);
352
395
  }
353
396
  }
@@ -441,6 +484,9 @@ function refusalOf(entry , target , { scope, scopeRoot, bkDir, fsImpl
441
484
  } catch {
442
485
  return 'content missing from backup';
443
486
  }
487
+ // A restore chmods the file it writes to this mode, so a manifest that lacks one (or holds anything
488
+ // but a permission mask) is refused here, with a name, rather than failing inside the write.
489
+ if (!Number.isInteger(entry.mode) || entry.mode < 0 || entry.mode > 0o7777) return 'invalid mode';
444
490
  return null;
445
491
  }
446
492
 
@@ -473,6 +519,8 @@ function planRestore({ bid, backupRoot, scope, scopeRoot, targets, explicitTarge
473
519
  untargeted: 0,
474
520
  snapshot: [],
475
521
  v1: null,
522
+ // --target narrows the ledger with the files: the retired-harness rule of mergeRestoredLedger is off.
523
+ narrowed: Boolean(explicitTargets),
476
524
  };
477
525
  const wanted = new Set(targets);
478
526
 
@@ -535,13 +583,25 @@ function planRestore({ bid, backupRoot, scope, scopeRoot, targets, explicitTarge
535
583
  * the backup does not hold and rows of MCP entries (a file holding only MCP entries is never backed
536
584
  * up) stay as they are; restoring the whole old ledger would make DoFlow forget files and entries it
537
585
  * still has on disk. With no current ledger the backup's is taken whole.
586
+ *
587
+ * A harness in `retired` (retired in the registry now, row 127) is the exception: every row of it
588
+ * and its `targets` entry come back as the backup holds them. A sweep that retired it released some
589
+ * rows with no file effect (a file a live harness still claims), so no file of theirs is in the backup,
590
+ * yet the run dropped them. Only the sweep ever changes a retired row, so the backup's copy is never
591
+ * staler than the current one; a current row of it the backup lacks stays. A restored row whose file
592
+ * a live harness now claims is released by the next sweep (claim rule 2), file untouched.
538
593
  */
539
- function mergeRestoredLedger(backup , current , restored ) {
594
+ function mergeRestoredLedger(backup , current , restored , retired = new Set()) {
540
595
  if (!current) return backup;
541
596
  const at = (target ) => typeof target === 'string' && restored.has(path.resolve(target));
597
+ const whole = (row ) => retired.has(row?.harness);
598
+ const back = backup.resources.filter((r ) => at(r.target) || whole(r));
599
+ const backKeys = new Set(back.filter(whole).map(ownershipKey));
600
+ const targets = Object.entries(backup.targets ?? {}).filter(([id]) => retired.has(id));
542
601
  return {
543
602
  ...current,
544
- resources: [...current.resources.filter((r ) => !at(r.target)), ...backup.resources.filter((r ) => at(r.target))],
603
+ ...(targets.length ? { targets: { ...current.targets, ...Object.fromEntries(targets) } } : {}),
604
+ resources: [...current.resources.filter((r ) => !at(r.target) && !(whole(r) && backKeys.has(ownershipKey(r)))), ...back],
545
605
  tombstones: [
546
606
  ...(current.tombstones ?? []).filter((t ) => !at(t.fromTarget)),
547
607
  ...(backup.tombstones ?? []).filter((t ) => at(t.fromTarget)),
@@ -549,21 +609,32 @@ function mergeRestoredLedger(backup , current , restored )
549
609
  };
550
610
  }
551
611
 
612
+ /** Ids the registry retires (row 127 IC-001): `registry` when a caller passes one, else the shipped
613
+ * registry. One that cannot be read retires nothing, which leaves the merge per restored file. */
614
+ function retiredIds(registry ) {
615
+ if (registry === undefined) {
616
+ const { REPO_ROOT } = require('../helper/repo-root.js');
617
+ try { registry = require('../registry/index.js').loadRegistry({ repoRoot: REPO_ROOT }); } catch { registry = null; }
618
+ }
619
+ return new Set((Array.isArray(registry?.retired) ? registry.retired : []).map((entry ) => entry?.id).filter((id ) => typeof id === 'string'));
620
+ }
621
+
552
622
  /** The bytes a ledger restore item writes: the backup's ledger merged onto the current one. */
553
- function restoredLedgerBytes(item , restored , fsImpl ) {
623
+ function restoredLedgerBytes(item , restored , retired , fsImpl ) {
554
624
  const backup = validateLedger(JSON.parse(fsImpl.readFileSync(item.stored, 'utf8')));
555
625
  const current = fsImpl.existsSync(item.path) ? validateLedger(JSON.parse(fsImpl.readFileSync(item.path, 'utf8'))) : null;
556
- return Buffer.from(`${JSON.stringify(validateLedger(mergeRestoredLedger(backup, current, restored)), null, 2)}\n`);
626
+ return Buffer.from(`${JSON.stringify(validateLedger(mergeRestoredLedger(backup, current, restored, retired)), null, 2)}\n`);
557
627
  }
558
628
 
559
629
  /**
560
630
  * Carry out a plan from planRestore. Format 2 writes each restore item through a temp file renamed
561
631
  * over the target (or straight through a symlinked target, which stays a link); a failed item is
562
632
  * reported and the rest continue. The ownership ledger goes last and restores only the rows of the
563
- * files written back before it (mergeRestoredLedger). It never deletes a file and never writes
633
+ * files written back before it, plus every row of a retired harness unless --target narrowed the
634
+ * restore (mergeRestoredLedger); `registry` is for tests, omitted the shipped one is read. It never deletes a file and never writes
564
635
  * outside `plan.restore`. A 1.19 plan goes through restoreBackup unchanged.
565
636
  */
566
- function executeRestore(plan , { backupRoot, dryRun = false, fsImpl = fs } ) {
637
+ function executeRestore(plan , { backupRoot, dryRun = false, registry, fsImpl = fs } ) {
567
638
  if (plan.format !== 2) {
568
639
  const toolDirs = Object.fromEntries(plan.v1.tools.map((t ) => [t.tool, t.dstDir]));
569
640
  restoreBackup({ bid: plan.bid, backupRoot, dirs: toolDirs, dryRun });
@@ -583,7 +654,7 @@ function executeRestore(plan , { backupRoot, dryRun = false, fsImpl = fs }
583
654
  try {
584
655
  const dir = path.dirname(item.path);
585
656
  fsImpl.mkdirSync(dir, { recursive: true });
586
- const bytes = item.ledger ? restoredLedgerBytes(item, files, fsImpl) : fsImpl.readFileSync(item.stored);
657
+ const bytes = item.ledger ? restoredLedgerBytes(item, files, plan.narrowed ? new Set() : retiredIds(registry), fsImpl) : fsImpl.readFileSync(item.stored);
587
658
  let isLink = false;
588
659
  try {
589
660
  isLink = fsImpl.lstatSync(item.path).isSymbolicLink();
@@ -685,7 +756,12 @@ function applyRetention({ backupRoot, keep, protect = [], reserve = 0, dryRun =
685
756
  const protectedCandidates = candidates.filter((c) => protectedIds.has(c.id));
686
757
  const ordered = [...protectedCandidates, ...candidates.filter((c) => !protectedIds.has(c.id))];
687
758
  const keepCount = keep === 0 ? ordered.length : Math.max(keep - reserve, protectedCandidates.length);
688
- const removal = ordered.slice(keepCount).map((c) => c.id);
759
+ // The protected backup is the run's own, so one newer than it was made by another run that finished
760
+ // after it; this pass only reaps history older than its own backup.
761
+ const newestProtected = protectedCandidates[0];
762
+ const removal = ordered.slice(keepCount)
763
+ .filter((c) => !newestProtected || compareSortKeys(c, newestProtected) >= 0)
764
+ .map((c) => c.id);
689
765
 
690
766
  if (dryRun || keep === 0) {
691
767
  if (dryRun) result.wouldRemove = removal;
@@ -693,16 +769,30 @@ function applyRetention({ backupRoot, keep, protect = [], reserve = 0, dryRun =
693
769
  return result;
694
770
  }
695
771
 
772
+ // Also deletes what an earlier pass renamed aside and died before deleting (a `.tmp-del-*` whose pid
773
+ // is dead): already out of every listing, so it goes at once. One whose pid is alive belongs to a
774
+ // pass that may still roll it back, so it is never touched here, whatever its age.
696
775
  result.sweptTemps = sweepStaleTemps(backupRoot, { now, fsImpl });
697
776
  for (const id of removal) {
698
777
  const dir = path.join(backupRoot, id);
699
778
  // Another run's retention pass may have removed it already.
700
779
  if (!fsImpl.existsSync(dir)) continue;
780
+ // One rename takes the backup out of the listing whole; the deletion then happens out of sight.
781
+ const aside = path.join(backupRoot, `${REMOVING_PREFIX}${id}-${hostTag()}-${process.pid}-${crypto.randomBytes(4).toString('hex')}`);
701
782
  try {
702
- fsImpl.rmSync(dir, { recursive: true, force: true });
783
+ fsImpl.renameSync(dir, aside);
784
+ } catch (err ) {
785
+ if (err.code !== 'ENOENT') result.failed.push({ id, error: err.message });
786
+ continue;
787
+ }
788
+ try {
789
+ fsImpl.rmSync(aside, { recursive: true, force: true });
703
790
  result.removed.push(id);
704
791
  } catch (err ) {
792
+ // Not removed: put it back under its id so it is still counted and listed, as when it was
793
+ // removed in place. If that fails too the aside folder is not listed and the next pass deletes it.
705
794
  result.failed.push({ id, error: err.message });
795
+ try { fsImpl.renameSync(aside, dir); } catch { /* the next pass cleans the aside folder */ }
706
796
  }
707
797
  }
708
798
  result.kept = ordered.filter((c) => fsImpl.existsSync(path.join(backupRoot, c.id))).length;
@@ -718,7 +808,7 @@ function retentionNote(prune , keep ) {
718
808
 
719
809
  module.exports = {
720
810
  backupId, restoreBackup, listBackups, assertSafeBackupId, scopeRelative, retentionNote,
721
- backupReadRoots, BACKUP_ORIGIN_CURRENT, BACKUP_ORIGIN_LEGACY,
811
+ backupReadRoots, hostTag, BACKUP_ORIGIN_CURRENT, BACKUP_ORIGIN_LEGACY,
722
812
  BackupError, DEFAULT_BACKUP_RETENTION, createFileBackup, sweepStaleTemps, classifyBackupDir,
723
813
  sortKey, backupSize, formatBytes, planRestore, executeRestore, mergeRestoredLedger, applyRetention,
724
814
  };
@@ -46,7 +46,7 @@ function printContext(ctx ) {
46
46
  '[CONTEXT] Resolved install context',
47
47
  ` scope : ${ctx.scope} (root: ${ctx.scopeRoot})`,
48
48
  ` targets : ${ctx.targets.join(', ')}`,
49
- ...ctx.targets.map((t ) => ` ${t.padEnd(7)}-> ${ctx.dirs[t]}`),
49
+ ...ctx.targets.map((t ) => ` ${t.padEnd(7)}-> ${ctx.dirs[t] ?? '(resolved by adapter)'}`),
50
50
  ` repo root : ${ctx.repoRoot}`,
51
51
  ` source commit : ${ctx.sourceCommit}`,
52
52
  ` node : ${ctx.nodeVersion}`,
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
  // targets.js — target validation + install-dir resolution.
3
- // PARITY: sync.sh is global-only — $HOME/.{claude,codex,gemini}, hardcoded, NOT CLAUDE_CONFIG_DIR.
3
+ // PARITY: sync.sh is global-only — $HOME/.{claude,codex}, hardcoded, NOT CLAUDE_CONFIG_DIR.
4
4
  // doflow extends this with a second, DoFlow-only scope: project-local install (this repo's own
5
5
  // .claude/ is a real precedent — Claude Code, Codex, and Copilot all support project-scoped config
6
6
  // alongside global, see agent-docs/research/context-setup-claude-codex-copilot.md).
@@ -15,25 +15,43 @@
15
15
  const os = require('node:os');
16
16
  const path = require('node:path');
17
17
 
18
- const VALID = ['claude', 'codex', 'gemini', 'copilot', 'kiro', 'opencode', 'pi', 'antigravity'];
18
+ const VALID = ['claude', 'codex', 'copilot', 'kiro', 'opencode', 'pi', 'antigravity'];
19
19
 
20
20
  /** The target used when `--target` is omitted. Deliberately one harness, not all of VALID: an
21
21
  * install with no flag should touch the one tool the user almost certainly has, not write
22
22
  * configuration into every harness DoFlow knows about — several of which may not be installed at
23
23
  * all. That mattered less when VALID was three entries and matters more as it grows. Ask for the
24
- * rest explicitly with `--target claude,codex,gemini`. */
24
+ * rest explicitly with `--target claude,codex,antigravity`. */
25
25
  const DEFAULT_TARGETS = ['claude'];
26
26
 
27
27
  const TARGET_ALIASES = {
28
28
  agy: 'antigravity',
29
29
  };
30
30
 
31
- /** Default to DEFAULT_TARGETS; validate any explicitly requested. */
32
- function resolveTargets(requested ) {
31
+ /** The registry's retired record for `id`, or null. The registry is read only for an id outside
32
+ * VALID, and a registry that cannot be read leaves the id unknown rather than failing differently. */
33
+ function retiredTarget(id , registry ) {
34
+ const { loadRegistry, retiredFor } = require('../registry/index.js');
35
+ if (!registry) {
36
+ const { REPO_ROOT } = require('../helper/repo-root.js');
37
+ try { registry = loadRegistry({ repoRoot: REPO_ROOT }); } catch { return null; }
38
+ }
39
+ return retiredFor(registry, id);
40
+ }
41
+
42
+ /** Default to DEFAULT_TARGETS; validate any explicitly requested. A retired id (row 127 IC-002) is
43
+ * refused like an unknown one, naming its replacement. `registry` is for tests; omitted, the
44
+ * shipped registry is read. */
45
+ function resolveTargets(requested , { registry } = {}) {
33
46
  const rawTargets = requested && requested.length ? requested : [...DEFAULT_TARGETS];
34
47
  const targets = rawTargets.map((t) => TARGET_ALIASES[t] || t);
35
48
  for (const t of targets) {
36
49
  if (!VALID.includes(t)) {
50
+ const retired = retiredTarget(t, registry);
51
+ if (retired) {
52
+ const alias = Object.keys(TARGET_ALIASES).find((key) => TARGET_ALIASES[key] === retired.replacement);
53
+ throw new Error(`Target '${t}' was retired on ${retired.retiredOn}; use '${retired.replacement}'${alias ? ` (alias ${alias})` : ''}. Its DoFlow files are released by the next 'doflow install' or 'doflow update' in that scope.`);
54
+ }
37
55
  throw new Error(`Unknown target: '${t}' (valid: ${VALID.join(', ')})`);
38
56
  }
39
57
  }
@@ -47,16 +65,14 @@ function resolveTargets(requested ) {
47
65
 
48
66
  /**
49
67
  * Map tool -> install root.
50
- * global=true -> $HOME/.{claude,codex,gemini,copilot,kiro,pi}, $HOME/.config/opencode
51
- * (sync.sh parity for claude/codex/gemini; the four new harnesses follow
52
- * each one's own documented global root — see below)
53
- * global=false (dflt) -> <resolved projectRoot>/.{claude,codex,kiro,opencode,pi}, but .agents for
54
- * gemini and .github for copilot — both deliberately asymmetric with their own global-scope
55
- * directory, for reasons native to each harness (see per-key notes below).
68
+ * global=true -> $HOME/.{claude,codex,copilot,kiro,pi}, $HOME/.config/opencode
69
+ * (sync.sh parity for claude/codex; the other harnesses follow each one's
70
+ * own documented global root — see below)
71
+ * global=false (dflt) -> <resolved projectRoot>/.{claude,codex,kiro,opencode,pi}, but .github for
72
+ * copilot — deliberately asymmetric with its own global-scope directory, for reasons native to
73
+ * the harness (see per-key notes below). antigravity has no entry: its adapter resolves its
74
+ * own paths from core/registry/harnesses.json.
56
75
  *
57
- * gemini -> project-scoped Gemini config follows the Antigravity customization convention, not
58
- * `.gemini/`, so it is asymmetric with its own global directory and with the other
59
- * targets.
60
76
  * copilot -> Copilot has no single native root the way Claude's `.claude` is one: instructions,
61
77
  * skills, agents, and MCP each live under a different path (see design.md §4's
62
78
  * native-surfaces table). `.github` is the project root DoFlow treats as Copilot's
@@ -67,7 +83,7 @@ function resolveTargets(requested ) {
67
83
  * the clean, symmetric case.
68
84
  * opencode -> Global config is `~/.config/opencode`, NOT `~/.opencode/` — a path DoFlow's own
69
85
  * docs asserted incorrectly for several releases (see src/adapters/opencode/index.js).
70
- * Project scope is `.opencode`, asymmetric with the global path the same way gemini's
86
+ * Project scope is `.opencode`, asymmetric with the global path the same way copilot's
71
87
  * is.
72
88
  * pi -> Global config is `~/.pi/agent` — note the `agent/` segment; `~/.pi/settings.json`
73
89
  * is not a location Pi reads. Project scope is `.pi` (see
@@ -79,7 +95,6 @@ function toolDirs(opts = {}) {
79
95
  return {
80
96
  claude: path.join(root, '.claude'),
81
97
  codex: path.join(root, '.codex'),
82
- gemini: global ? path.join(root, '.gemini') : path.join(root, '.agents'),
83
98
  copilot: global ? path.join(root, '.copilot') : path.join(root, '.github'),
84
99
  kiro: path.join(root, '.kiro'),
85
100
  opencode: global ? path.join(root, '.config', 'opencode') : path.join(root, '.opencode'),
@@ -3,10 +3,11 @@
3
3
  const crypto = require('node:crypto');
4
4
  const CLASS_ORDER = Object.freeze(['unknown', 'unsupported', 'observation', 'advisory', 'blocking']);
5
5
  const POLICY_ORDER = Object.freeze(['pre-implementation-gate', 'pre-bash-guard', 'session-context', 'mcp-tool-guard', 'stop-check']);
6
- const HARNESS_ORDER = Object.freeze(['claude', 'codex', 'gemini', 'opencode', 'pi', 'copilot', 'kiro', 'antigravity']);
6
+ const HARNESS_ORDER = Object.freeze(['claude', 'codex', 'opencode', 'pi', 'copilot', 'kiro', 'antigravity']);
7
+ const BEHAVIOUR_ORDER = Object.freeze(['context-authority', 'default-silence', 'summary-scope', 'summary-grant', 'identity', 'check-freshness']);
7
8
  const NEVER_BLOCKING_EVENTS = new Set([
8
9
  'pi\u0000tool_execution_start', 'antigravity\u0000PostToolUse', 'antigravity\u0000PreInvocation',
9
- 'antigravity\u0000PostInvocation', 'gemini\u0000AfterTool', 'claude\u0000PostToolUse', 'codex\u0000PostToolUse',
10
+ 'antigravity\u0000PostInvocation', 'claude\u0000PostToolUse', 'codex\u0000PostToolUse',
10
11
  ]);
11
12
  function stable(value ) {
12
13
  if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
@@ -54,4 +55,20 @@ function renderEnforcementMatrix(coverage ) {
54
55
  for (const row of rows) lines.push(`| ${row.policy} | ${row.harness} | ${row.event} | ${row.profile} | ${row.class} | ${row.verification} | ${(row.prerequisites || []).join(', ')} | ${(row.doesNotCover || []).join(', ')} |`);
55
56
  return lines.join('\n');
56
57
  }
57
- module.exports = { enforcementFor, renderEnforcementMatrix, rowsDigest, UnknownEnforcementTarget, NEVER_BLOCKING_EVENTS };
58
+ // IC-019: one row per behaviour x harness, ordered by IC-018's behaviour list, then HARNESS_ORDER.
59
+ function renderBehaviourMatrix(coverage ) {
60
+ const order = (items , value ) => {
61
+ const index = items.indexOf(value);
62
+ return index < 0 ? items.length : index;
63
+ };
64
+ const entries = [...(coverage.behaviours || [])].sort((a , b ) => order(BEHAVIOUR_ORDER, a.behaviour) - order(BEHAVIOUR_ORDER, b.behaviour)
65
+ || order(HARNESS_ORDER, a.harness) - order(HARNESS_ORDER, b.harness)
66
+ || a.behaviour.localeCompare(b.behaviour) || a.harness.localeCompare(b.harness));
67
+ const lines = [`Schema ${coverage.schemaVersion}; revision ${coverage.revision}`, '| Behaviour | Harness | Status | Limitation or tests |', '|---|---|---|---|'];
68
+ for (const entry of entries) {
69
+ const detail = entry.status === 'enforced' ? (entry.tests || []).join(', ') : (entry.limitation || '');
70
+ lines.push(`| ${entry.behaviour} | ${entry.harness} | ${entry.status} | ${detail} |`);
71
+ }
72
+ return lines.join('\n');
73
+ }
74
+ module.exports = { enforcementFor, renderEnforcementMatrix, renderBehaviourMatrix, BEHAVIOUR_ORDER, HARNESS_ORDER, rowsDigest, UnknownEnforcementTarget, NEVER_BLOCKING_EVENTS };
@@ -0,0 +1,160 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ // Hook tiers (maint-hooks). Content (guidance, skills, agents, MCP) goes to every harness; DoFlow's
6
+ // enforcement (hooks, gates, the bridge) goes only to the harnesses the `hooks.harnesses` setting
7
+ // names. A harness outside that list is planned against a registry copy in which its `hooks`
8
+ // capability is unavailable, so the same seams that already handle a harness without hooks drop
9
+ // its hook assets, render its lifecycle policies as guidance fallbacks and skip its bespoke hook
10
+ // config; the removal of what an earlier install wrote is planned from the ledger, as an uninstall
11
+ // of those resources alone.
12
+
13
+ const HOOKS_KEY = 'hooks.harnesses';
14
+ const TIER_REASON = `not in ${HOOKS_KEY}`;
15
+ const WORKERS_KEY = 'workers.enabled';
16
+
17
+ /** The hook tier plus the worker harnesses the person configured (`workers.enabled` set in a settings
18
+ * file; null when it is the shipped default), for the run's warnings (maint-hooks R-4). */
19
+ function resolveHookTier({ env = process.env, loadSettings } = {}) {
20
+ if (!loadSettings) {
21
+ const fs = require('node:fs');
22
+ const path = require('node:path');
23
+ const { configHome } = require('../runtime/config-home.js');
24
+ const dir = configHome(env).dir;
25
+ if (!dir || !fs.existsSync(path.join(dir, 'settings.json'))) {
26
+ const registry = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'core', 'registry', 'settings.json'), 'utf8'));
27
+ return { harnesses: [...registry.keys[HOOKS_KEY].default], workers: null };
28
+ }
29
+ }
30
+ const load = loadSettings ?? require('../runtime/settings/resolve.js').loadSettings;
31
+ const view = load({ global: true, env: { ...env }, only: [HOOKS_KEY, WORKERS_KEY] });
32
+ const resolution = view.get(HOOKS_KEY, 'global');
33
+ const workers = view.get(WORKERS_KEY, 'global');
34
+ const configured = Array.isArray(workers?.value) && workers?.origin?.layer && workers.origin.layer !== 'default';
35
+ return {
36
+ harnesses: Array.isArray(resolution?.value) ? [...resolution.value] : [],
37
+ workers: configured ? workers.value.filter((id ) => typeof id === 'string' && id !== 'primary').map((id ) => id.split(/[:/]/)[0]) : null,
38
+ };
39
+ }
40
+
41
+ /** One WARN line per planned harness that gets no hooks although it is the primary session (claude)
42
+ * or a worker the person configured: there it runs without DoFlow's destructive-command guard. */
43
+ function hookTierWarnings(registry , targets , tier ) {
44
+ const warnings = [];
45
+ for (const harness of registry.harnesses) {
46
+ if (!targets.includes(harness.id) || tier.harnesses.includes(harness.id) || !getsHooks(registry, harness)) continue;
47
+ const role = harness.id === 'claude' ? 'the primary session' : tier.workers?.includes(harness.id) ? `a worker in ${WORKERS_KEY}` : null;
48
+ if (role) warnings.push(`${harness.id} is ${role} but not in ${HOOKS_KEY}: it runs without DoFlow's hooks, gates or destructive-command guard`);
49
+ }
50
+ return warnings;
51
+ }
52
+
53
+ /** Whether a harness is outside the hook tier. `hookHarnesses` undefined means no tier was resolved
54
+ * (an in-process caller): every harness keeps its hooks, as before the setting existed. */
55
+ function outsideHookTier(harnessId , hookHarnesses ) {
56
+ return Array.isArray(hookHarnesses) && !hookHarnesses.includes(harnessId);
57
+ }
58
+
59
+ /** Whether DoFlow installs any hook for a harness: its hooks are supported, or a hook asset applies
60
+ * to it (Pi's guard extension). OpenCode gets none, so the tier leaves it as it is. */
61
+ function getsHooks(registry , harness ) {
62
+ const hooks = harness.capabilities?.hooks;
63
+ if (!hooks || hooks.status === 'unavailable') return false;
64
+ if (hooks.status === 'supported') return true;
65
+ const { selectAssets } = require('../registry/index.js');
66
+ return selectAssets(registry, { harness: harness.id, capability: 'hooks' }).length > 0;
67
+ }
68
+
69
+ /** The registry a harness outside the tier is planned against: the same object graph, with that
70
+ * harness's `hooks` capability unavailable. A harness DoFlow installs no hook for, or that is in the
71
+ * tier, is returned unchanged. */
72
+ function tieredRegistry(registry , hookHarnesses ) {
73
+ if (!Array.isArray(hookHarnesses)) return registry;
74
+ let changed = false;
75
+ const harnesses = registry.harnesses.map((harness ) => {
76
+ const hooks = harness.capabilities?.hooks;
77
+ if (!outsideHookTier(harness.id, hookHarnesses) || !getsHooks(registry, harness)) return harness;
78
+ changed = true;
79
+ return { ...harness, capabilities: { ...harness.capabilities, hooks: { ...hooks, status: 'unavailable', reason: TIER_REASON, tiered: true } } };
80
+ });
81
+ return changed ? { ...registry, harnesses } : registry;
82
+ }
83
+
84
+ /** The harness ids whose hooks a removal must take: those the ledger records hook resources for. A
85
+ * removal plans what an install wrote, so a harness installed outside the tier (no hook rows) plans
86
+ * no hook removal, and never claims a shared hook file another harness still owns. */
87
+ function hookHoldersFromLedger(registry , ledger ) {
88
+ const { selectAssets } = require('../registry/index.js');
89
+ const rows = ledger?.resources ?? [];
90
+ return registry.harnesses.filter((harness ) => {
91
+ const hookIds = new Set (selectAssets(registry, { harness: harness.id, capability: 'hooks' }).map((asset ) => asset.id));
92
+ return rows.some((row ) => row?.harness === harness.id && isHookResource(row, hookIds));
93
+ }).map((harness ) => harness.id);
94
+ }
95
+
96
+ /** Whether a (possibly tiered) harness declaration had its hooks switched off by the tier. */
97
+ function hooksTieredOff(harness ) {
98
+ return harness?.capabilities?.hooks?.tiered === true;
99
+ }
100
+
101
+ /** The context keys that hand an adapter a bespoke hook source (Codex hooks.json). A harness outside the tier is planned without them, and with `hooksOff` set for the
102
+ * adapters whose hooks ride a non-hook asset (Claude's settings.json). */
103
+ const HOOK_SOURCE_KEYS = ['codexHooksSourceFile', 'codexHooksSourceDir'];
104
+
105
+ function withoutHookSources(context ) {
106
+ const next = { ...context, hooksOff: true };
107
+ for (const key of HOOK_SOURCE_KEYS) delete next[key];
108
+ return next;
109
+ }
110
+
111
+ /** Renderers and resource kinds that only ever carry hook state, whatever asset id they ride on
112
+ * (Codex files its bespoke hook resources under a guidance asset id). */
113
+ const HOOK_RENDERERS = new Set(['codex-hooks', 'antigravity-hooks', 'pi-guard-extension']);
114
+ const HOOK_KINDS = new Set(['hooks-file', 'hooks-script']);
115
+
116
+ function isHookResource(item , hookIds ) {
117
+ return hookIds.has(item?.assetId) || item?.nativeComponent === 'hooks' || HOOK_KINDS.has(item?.kind)
118
+ || HOOK_RENDERERS.has(item?.projection?.renderer);
119
+ }
120
+
121
+ /**
122
+ * Add to a harness's content plan the removal of the hooks an earlier install wrote for it, planned by
123
+ * the adapter's own remove path against the full registry and kept to hook changes only. Nothing is
124
+ * planned when the ledger holds no hook row for the harness. The removals carry `hookTier: true`, so
125
+ * applyLifecycle runs them through `adapter.remove` with `hookTierInput`.
126
+ */
127
+ function planHookTierRemovals({ target, registry, adapter, harness, scope, scopeRoot, ledger, context, normalizeChange, adapterConflicts } ) {
128
+ const { selectAssets } = require('../registry/index.js');
129
+ const { projectAdapterInput } = require('../adapters/index.js');
130
+ const { renderPolicies } = require('./policies.js');
131
+ if (harness.capabilities?.hooks?.status === 'unavailable') return target;
132
+ const hookIds = new Set (selectAssets(registry, { harness: harness.id, capability: 'hooks' }).map((asset ) => asset.id));
133
+ const rows = (ledger?.resources ?? []).filter((row ) => row?.harness === harness.id && isHookResource(row, hookIds));
134
+ if (!rows.length) return target;
135
+ const assets = selectAssets(registry, { harness: harness.id });
136
+ const policies = renderPolicies(registry, { harness: harness.id });
137
+ const input = projectAdapterInput({ registry, harness, scope, scopeRoot, assets, mcp: [], mcpAdoptable: [], policies,
138
+ context: { ...context, operation: 'remove' } });
139
+ const discovery = adapter.discover({ ...input, registry, ledger });
140
+ const result = adapter.plan({ ...input, registry, ledger, discovery });
141
+ // A removal the content plan already carries (an adapter that plans its own hooks off) is not planned twice.
142
+ const planned = new Set(target.changes.map((change ) => `${change.ownershipIdentity}\0${change.target}`));
143
+ const removals = (result.changes || []).filter((change ) => isHookResource(change, hookIds)
144
+ && !planned.has(`${change.ownershipIdentity ?? change.ownership}\0${change.target}`))
145
+ .map((change ) => Object.freeze({ ...normalizeChange(change, { harness, assets }), hookTier: true }));
146
+ const conflicts = adapterConflicts(result, harness).filter((reason ) => /hook/i.test(String(reason)));
147
+ if (!removals.length && !conflicts.length) return target;
148
+ const used = new Set(removals.map((change ) => change.assetId));
149
+ const extraAssets = assets.filter((asset ) => used.has(asset.id) && !target.assets.some((own ) => own.id === asset.id));
150
+ const changes = [...target.changes, ...removals];
151
+ const requiredNativeResources = target.requiredNativeResources === target.changes ? changes : [...target.requiredNativeResources, ...removals];
152
+ return {
153
+ ...target, assets: [...target.assets, ...extraAssets], changes, requiredNativeResources,
154
+ conflicts: [...target.conflicts, ...conflicts],
155
+ notices: [...target.notices, `removing ${harness.id} hooks: ${TIER_REASON} (content stays)`],
156
+ hookTierInput: input,
157
+ };
158
+ }
159
+
160
+ module.exports = { TIER_REASON, resolveHookTier, hookTierWarnings, tieredRegistry, hookHoldersFromLedger, hooksTieredOff, withoutHookSources, planHookTierRemovals };