@khoavu882/doflow 2.0.0-beta.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (497) hide show
  1. package/README.md +12 -11
  2. package/bin/doflow.js +21 -11
  3. package/core/.antigravity-plugin/plugin.json +1 -3
  4. package/core/.claude-plugin/plugin.json +1 -1
  5. package/core/.codex-plugin/plugin.json +1 -1
  6. package/core/.plugin/plugin.json +1 -1
  7. package/core/harnesses/claude/hooks/lib.sh +43 -24
  8. package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
  9. package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
  10. package/core/harnesses/claude/hooks/post-compact.sh +35 -32
  11. package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
  12. package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
  13. package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
  14. package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
  15. package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
  16. package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
  17. package/core/harnesses/claude/hooks/session-end.sh +23 -10
  18. package/core/harnesses/claude/hooks/session-start.sh +5 -3
  19. package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
  20. package/core/harnesses/claude/hooks/stop-check.sh +5 -3
  21. package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
  22. package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
  23. package/core/harnesses/claude/settings/settings.json +18 -0
  24. package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
  25. package/core/harnesses/codex/agents/core-implementer.toml +1 -0
  26. package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
  27. package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
  28. package/core/harnesses/codex/agents/research-writer.toml +1 -1
  29. package/core/harnesses/codex/agents/reviewer.toml +10 -0
  30. package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
  31. package/core/harnesses/codex/agents/system-architect.toml +1 -1
  32. package/core/harnesses/codex/hooks/lib.sh +44 -25
  33. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
  34. package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
  35. package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
  36. package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
  37. package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
  38. package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
  39. package/core/harnesses/codex/hooks/session-start.sh +5 -3
  40. package/core/harnesses/codex/hooks/stop-check.sh +6 -5
  41. package/core/harnesses/codex/hooks/subagent-audit.sh +18 -6
  42. package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
  43. package/core/harnesses/copilot/hooks/doflow-lib.sh +93 -0
  44. package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
  45. package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +9 -0
  46. package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
  47. package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
  48. package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
  49. package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
  50. package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
  51. package/core/harnesses/copilot/hooks/doflow.json +57 -0
  52. package/core/harnesses/kiro/agents/doflow.json +1 -1
  53. package/core/harnesses/kiro/hooks/lib.sh +26 -6
  54. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +21 -5
  55. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +21 -5
  56. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +21 -5
  57. package/core/harnesses/kiro/hooks/session-start.sh +12 -5
  58. package/core/harnesses/kiro/hooks/stop-check.sh +22 -30
  59. package/core/harnesses/pi/extensions/doflow-guard.js +167 -0
  60. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -12
  61. package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
  62. package/core/harnesses/shared/hooks/bridge/bridge.sh +277 -0
  63. package/core/harnesses/shared/hooks/bridge/envelope.jq +129 -0
  64. package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
  65. package/core/harnesses/shared/hooks/bridge/respond.sh +70 -0
  66. package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +28 -14
  67. package/core/harnesses/shared/hooks/policies/capture-failure.sh +51 -16
  68. package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
  69. package/core/harnesses/shared/hooks/policies/contract.sh +78 -0
  70. package/core/harnesses/shared/hooks/policies/deny-json.sh +35 -9
  71. package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
  72. package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
  73. package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
  74. package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
  75. package/core/harnesses/shared/hooks/policies/lib.sh +65 -40
  76. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +48 -35
  77. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +284 -33
  78. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +180 -110
  79. package/core/harnesses/shared/hooks/policies/session-context.sh +66 -7
  80. package/core/harnesses/shared/hooks/policies/stop-check.sh +61 -23
  81. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +39 -6
  82. package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
  83. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +97 -159
  84. package/core/harnesses/shared/hooks/stream-hook-runner.js +114 -187
  85. package/core/harnesses/shared/locator/doflow-run +2 -3
  86. package/core/registry/advice.json +3 -3
  87. package/core/registry/assets.json +72 -80
  88. package/core/registry/capabilities.json +12 -0
  89. package/core/registry/contracts.json +60 -33
  90. package/core/registry/contribution-points.json +32 -0
  91. package/core/registry/gates.json +32 -0
  92. package/core/registry/guidance-caps.json +40 -0
  93. package/core/registry/harnesses.json +1542 -277
  94. package/core/registry/lifecycle.json +2837 -37
  95. package/core/registry/mcp.json +5 -1
  96. package/core/registry/models.json +7 -1
  97. package/core/registry/package-templates/README.md +19 -0
  98. package/core/registry/package-templates/agent/files/agent.md +9 -0
  99. package/core/registry/package-templates/agent/template.json +4 -0
  100. package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
  101. package/core/registry/package-templates/capability-provider/template.json +11 -0
  102. package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
  103. package/core/registry/package-templates/debate-preset/template.json +4 -0
  104. package/core/registry/package-templates/review-preset/files/preset.json +30 -0
  105. package/core/registry/package-templates/review-preset/template.json +4 -0
  106. package/core/registry/package-templates/skill/files/SKILL.md +8 -0
  107. package/core/registry/package-templates/skill/template.json +4 -0
  108. package/core/registry/package-templates/worker/files/worker.json +92 -0
  109. package/core/registry/package-templates/worker/template.json +11 -0
  110. package/core/registry/packages/doflow/doflow-package.json +15 -0
  111. package/core/registry/packages/doflow/presets/debate-3.json +26 -0
  112. package/core/registry/protocols.json +61 -0
  113. package/core/registry/routes.json +17 -0
  114. package/core/registry/rules.json +431 -0
  115. package/core/registry/settings.json +276 -0
  116. package/core/registry/storage.json +701 -30
  117. package/core/registry/telemetry.json +236 -0
  118. package/core/registry/transcripts.json +142 -0
  119. package/core/registry/verification.json +76 -1
  120. package/core/registry/workflows.json +55 -0
  121. package/core/shared/agent-specs/core-implementer.md +3 -0
  122. package/core/shared/agent-specs/delivery-lead.md +68 -0
  123. package/core/shared/agent-specs/quality-guardian.md +5 -1
  124. package/core/shared/agent-specs/research-writer.md +10 -3
  125. package/core/shared/agent-specs/reviewer.md +53 -0
  126. package/core/shared/agent-specs/spec-analyst.md +5 -6
  127. package/core/shared/agent-specs/system-architect.md +5 -1
  128. package/core/shared/guidance/DOFLOW_CORE.md +1 -5
  129. package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
  130. package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
  131. package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
  132. package/core/shared/guidance/pointers/codex.md +1 -4
  133. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
  134. package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
  135. package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
  136. package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
  137. package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
  138. package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
  139. package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
  140. package/core/shared/guidance/references/ORCHESTRATION.md +1 -2
  141. package/core/shared/guidance/rules/RULES.md +45 -0
  142. package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
  143. package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
  144. package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
  145. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
  146. package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
  147. package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
  148. package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
  149. package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
  150. package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
  151. package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
  152. package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
  153. package/core/shared/scripts/doflow/bash/validate-artifacts.sh +48 -4
  154. package/core/shared/scripts/doflow/bin/doflow-run +154 -35
  155. package/core/shared/skills/do/SKILL.md +12 -10
  156. package/core/shared/skills/do/references/advice_calls.md +26 -0
  157. package/core/shared/skills/do/references/pm_routing.md +1 -2
  158. package/core/shared/skills/do/references/program_mode.md +31 -0
  159. package/core/shared/skills/do-brainstorm/SKILL.md +20 -74
  160. package/core/shared/skills/do-brainstorm/references/handoff_branches.md +35 -0
  161. package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
  162. package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
  163. package/core/shared/skills/do-code-review/SKILL.md +41 -144
  164. package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
  165. package/core/shared/skills/do-code-review/references/tools.md +90 -0
  166. package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
  167. package/core/shared/skills/do-design/SKILL.md +24 -23
  168. package/core/shared/skills/do-diagnose/SKILL.md +14 -4
  169. package/core/shared/skills/do-document/SKILL.md +11 -1
  170. package/core/shared/skills/do-document/references/deep_research.md +1 -2
  171. package/core/shared/skills/do-document/references/embedded-research.md +18 -6
  172. package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
  173. package/core/shared/skills/do-execute-plan/SKILL.md +17 -59
  174. package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
  175. package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
  176. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +28 -1
  177. package/core/shared/skills/do-execute-plan/references/run_position.md +13 -1
  178. package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
  179. package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
  180. package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
  181. package/core/shared/skills/do-flow/SKILL.md +8 -9
  182. package/core/shared/skills/do-flow/references/task_classes.md +2 -2
  183. package/core/shared/skills/do-git/SKILL.md +4 -0
  184. package/core/shared/skills/do-implement/SKILL.md +7 -9
  185. package/core/shared/skills/do-plan/SKILL.md +29 -19
  186. package/core/shared/skills/do-test/SKILL.md +7 -4
  187. package/core/shared/templates/doflow/constitution-template.md +9 -3
  188. package/core/shared/templates/doflow/data-model-template.md +1 -1
  189. package/core/shared/templates/doflow/design-template.md +113 -16
  190. package/core/shared/templates/doflow/plan-template.md +11 -10
  191. package/core/shared/templates/doflow/question-log-template.md +14 -2
  192. package/core/shared/templates/doflow/requirement-template.md +1 -1
  193. package/core/worker-evidence/antigravity/checks.json +14 -0
  194. package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
  195. package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
  196. package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
  197. package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
  198. package/core/worker-evidence/codex/checks.json +38 -0
  199. package/package.json +10 -6
  200. package/src/adapters/antigravity/index.js +226 -113
  201. package/src/adapters/claude/index.js +39 -5
  202. package/src/adapters/codex/agents.js +25 -15
  203. package/src/adapters/codex/hooks.js +40 -5
  204. package/src/adapters/codex/index.js +100 -22
  205. package/src/adapters/codex/native-projection.js +6 -2
  206. package/src/adapters/copilot/index.js +58 -23
  207. package/src/adapters/copy-tree.js +294 -48
  208. package/src/adapters/guidance-manifest.js +71 -0
  209. package/src/adapters/guidance-pointer.js +19 -0
  210. package/src/adapters/hook-commands.js +6 -10
  211. package/src/adapters/index.js +1 -0
  212. package/src/adapters/kiro/index.js +38 -13
  213. package/src/adapters/opencode/index.js +8 -8
  214. package/src/adapters/pi/hooks.js +120 -0
  215. package/src/adapters/pi/index.js +48 -16
  216. package/src/adapters/resolve-source.js +26 -2
  217. package/src/cli/commands/install.js +3 -2
  218. package/src/cli/commands/reconcile.js +53 -9
  219. package/src/cli/commands/remove.js +2 -2
  220. package/src/cli/commands/rollback.js +6 -3
  221. package/src/cli/commands/self-update.js +18 -9
  222. package/src/cli/commands/status.js +21 -2
  223. package/src/cli/commands/ui.js +99 -0
  224. package/src/cli/commands/update.js +3 -2
  225. package/src/cli/index.js +338 -33
  226. package/src/cli/runtime-commands.js +243 -52
  227. package/src/cli/shared.js +0 -2
  228. package/src/helper/git.js +1 -1
  229. package/src/helper/guardrails.js +1 -1
  230. package/src/helper/marker-merge.js +31 -13
  231. package/src/helper/package-ids.js +37 -0
  232. package/src/helper/semver.js +94 -0
  233. package/src/install/backup.js +105 -15
  234. package/src/install/context.js +1 -1
  235. package/src/install/targets.js +31 -16
  236. package/src/lifecycle/coverage.js +74 -0
  237. package/src/lifecycle/hook-step.js +61 -0
  238. package/src/lifecycle/hook-tier.js +160 -0
  239. package/src/lifecycle/index.js +434 -86
  240. package/src/lifecycle/retired-harnesses.js +318 -0
  241. package/src/lifecycle/view.js +35 -15
  242. package/src/registry/index.js +686 -32
  243. package/src/registry/worker-ids.js +45 -0
  244. package/src/release/build-package.js +0 -3
  245. package/src/release/docs-blocks.js +492 -0
  246. package/src/release/preflight.js +425 -0
  247. package/src/release/rehearse.js +401 -0
  248. package/src/release/release-workflow.js +733 -0
  249. package/src/release/sync-plugin-versions.js +43 -7
  250. package/src/runtime/advice/bench-gate.js +25 -0
  251. package/src/runtime/advice/cli.js +59 -16
  252. package/src/runtime/advice/client.js +32 -9
  253. package/src/runtime/advice/config.js +98 -133
  254. package/src/runtime/advice/doctor.js +1 -1
  255. package/src/runtime/advice/elevation.js +97 -0
  256. package/src/runtime/advice/endpoint-guard.js +9 -3
  257. package/src/runtime/advice/mode.js +37 -0
  258. package/src/runtime/advice/record.js +18 -5
  259. package/src/runtime/advice/registry.js +4 -1
  260. package/src/runtime/advice/shadow-config.js +229 -0
  261. package/src/runtime/advice/shadow-dispatcher.js +276 -0
  262. package/src/runtime/brief-rules.js +81 -0
  263. package/src/runtime/brief-tools.js +111 -34
  264. package/src/runtime/candidate.js +514 -0
  265. package/src/runtime/capabilities/executor.js +450 -0
  266. package/src/runtime/capabilities/loopback.js +41 -0
  267. package/src/runtime/capabilities/sanitizer.js +88 -0
  268. package/src/runtime/capability-router.js +135 -3
  269. package/src/runtime/checkouts.js +73 -11
  270. package/src/runtime/claims.js +27 -12
  271. package/src/runtime/cli-result.js +11 -4
  272. package/src/runtime/cli.js +221 -277
  273. package/src/runtime/config-home.js +42 -0
  274. package/src/runtime/context/arbitrate.js +767 -0
  275. package/src/runtime/context/budget.js +203 -0
  276. package/src/runtime/context/compose.js +1367 -0
  277. package/src/runtime/context/delivery-store.js +100 -0
  278. package/src/runtime/context/emission-rules.js +118 -0
  279. package/src/runtime/context/identity.js +227 -0
  280. package/src/runtime/context/mandatory.js +594 -0
  281. package/src/runtime/context/pointer.js +142 -0
  282. package/src/runtime/context/primary-command.js +117 -0
  283. package/src/runtime/context/primary.js +344 -0
  284. package/src/runtime/context/summary-store.js +655 -0
  285. package/src/runtime/context-pack.js +1106 -245
  286. package/src/runtime/decision-register.js +13 -4
  287. package/src/runtime/dispatch/admission.js +392 -0
  288. package/src/runtime/dispatch/attempt-end.js +274 -0
  289. package/src/runtime/dispatch/command.js +628 -0
  290. package/src/runtime/dispatch/controls.js +904 -0
  291. package/src/runtime/dispatch/envelope.js +276 -0
  292. package/src/runtime/dispatch/grants.js +310 -0
  293. package/src/runtime/dispatch/integrate.js +617 -0
  294. package/src/runtime/dispatch/loop.js +1835 -0
  295. package/src/runtime/dispatch/store.js +352 -0
  296. package/src/runtime/dispatch/verify-run.js +201 -0
  297. package/src/runtime/evidence-fields.js +39 -0
  298. package/src/runtime/evidence-ledger.js +41 -7
  299. package/src/runtime/failure/capture.js +3 -1
  300. package/src/runtime/failure/cli.js +25 -9
  301. package/src/runtime/failure/home.js +57 -30
  302. package/src/runtime/failure/store.js +7 -2
  303. package/src/runtime/feature-pointer.js +174 -0
  304. package/src/runtime/feature-resolve.js +13 -2
  305. package/src/runtime/fleet/command.js +77 -0
  306. package/src/runtime/fleet/projects.js +178 -0
  307. package/src/runtime/fleet/tree.js +383 -0
  308. package/src/runtime/freshness.js +77 -10
  309. package/src/runtime/git-memo.js +46 -0
  310. package/src/runtime/guidance/ablation.js +427 -0
  311. package/src/runtime/health.js +565 -17
  312. package/src/runtime/history-compactor.js +13 -3
  313. package/src/runtime/implementation-gate.js +277 -22
  314. package/src/runtime/inventory/coverage.js +1 -1
  315. package/src/runtime/inventory/index.js +1 -1
  316. package/src/runtime/inventory/precedence.js +2 -2
  317. package/src/runtime/inventory/siblings.js +6 -4
  318. package/src/runtime/knowledge/code-corpus.js +277 -0
  319. package/src/runtime/knowledge/code-lexical.js +130 -0
  320. package/src/runtime/knowledge/code-retrieval.js +307 -0
  321. package/src/runtime/knowledge/code-validation.js +106 -0
  322. package/src/runtime/knowledge/gp3-gate.js +74 -0
  323. package/src/runtime/knowledge/graph-adapter.js +288 -0
  324. package/src/runtime/knowledge/graph-fallback.js +394 -0
  325. package/src/runtime/knowledge/hybrid.js +136 -8
  326. package/src/runtime/knowledge/retrieval.js +34 -1
  327. package/src/runtime/knowledge/rtk-compress.js +77 -0
  328. package/src/runtime/knowledge/semble-bridge.py +106 -0
  329. package/src/runtime/knowledge/semble-lifecycle.js +114 -0
  330. package/src/runtime/knowledge/semble-provider.js +365 -0
  331. package/src/runtime/lifecycle/cli.js +16 -6
  332. package/src/runtime/lifecycle/event-store.js +8 -6
  333. package/src/runtime/lifecycle/followup.js +6 -1
  334. package/src/runtime/lifecycle/overview.js +24 -1
  335. package/src/runtime/lifecycle/report-store.js +8 -10
  336. package/src/runtime/lifecycle/retention.js +95 -31
  337. package/src/runtime/lifecycle/root.js +9 -1
  338. package/src/runtime/lifecycle/status.js +34 -17
  339. package/src/runtime/lifecycle/store-upkeep.js +56 -7
  340. package/src/runtime/mask.js +134 -7
  341. package/src/runtime/mcp-consultations.js +40 -6
  342. package/src/runtime/model-router.js +79 -7
  343. package/src/runtime/models/catalogue.js +191 -0
  344. package/src/runtime/models/command.js +153 -0
  345. package/src/runtime/off-words.js +12 -0
  346. package/src/runtime/outcome.js +3 -3
  347. package/src/runtime/packages/command.js +1009 -0
  348. package/src/runtime/packages/contributions.js +116 -0
  349. package/src/runtime/packages/digest.js +132 -0
  350. package/src/runtime/packages/fetch.js +182 -0
  351. package/src/runtime/packages/grants.js +192 -0
  352. package/src/runtime/packages/index-file.js +134 -0
  353. package/src/runtime/packages/instruction.js +90 -0
  354. package/src/runtime/packages/manifest.js +331 -0
  355. package/src/runtime/packages/points.js +477 -0
  356. package/src/runtime/packages/programs.js +216 -0
  357. package/src/runtime/packages/scaffold.js +277 -0
  358. package/src/runtime/packages/source.js +90 -0
  359. package/src/runtime/packages/state.js +164 -0
  360. package/src/runtime/packages/store.js +449 -0
  361. package/src/runtime/packages/tar.js +106 -0
  362. package/src/runtime/packages/view.js +406 -0
  363. package/src/runtime/plan-check.js +59 -0
  364. package/src/runtime/plan-content.js +324 -0
  365. package/src/runtime/policy-init.js +118 -0
  366. package/src/runtime/profiles/doctor.js +96 -0
  367. package/src/runtime/profiles/resolve.js +244 -0
  368. package/src/runtime/profiles/rollback.js +254 -0
  369. package/src/runtime/profiles/sampler.js +41 -0
  370. package/src/runtime/readiness-record.js +16 -11
  371. package/src/runtime/readiness.js +127 -16
  372. package/src/runtime/research-request.js +50 -25
  373. package/src/runtime/retrieval-plan.js +402 -73
  374. package/src/runtime/retrieval-record.js +293 -0
  375. package/src/runtime/review/command.js +246 -0
  376. package/src/runtime/review/dominance.js +68 -0
  377. package/src/runtime/review/findings.js +434 -0
  378. package/src/runtime/review/identity.js +255 -0
  379. package/src/runtime/review/protocol-command.js +109 -0
  380. package/src/runtime/review/protocols.js +405 -0
  381. package/src/runtime/review/resolve-protocol.js +302 -0
  382. package/src/runtime/review/start.js +530 -0
  383. package/src/runtime/roadmap/facts.js +479 -0
  384. package/src/runtime/roadmap/gates.js +270 -0
  385. package/src/runtime/roadmap/loop.js +1636 -0
  386. package/src/runtime/roadmap/merge.js +381 -0
  387. package/src/runtime/roadmap/rows.js +1113 -0
  388. package/src/runtime/roadmap/seed.js +951 -0
  389. package/src/runtime/rules-report.js +336 -0
  390. package/src/runtime/scaffold/artifacts.js +63 -9
  391. package/src/runtime/scaffold/generate.js +21 -9
  392. package/src/runtime/settings/autopilot.js +106 -0
  393. package/src/runtime/settings/cli.js +280 -0
  394. package/src/runtime/settings/config-write.js +314 -0
  395. package/src/runtime/settings/files.js +271 -0
  396. package/src/runtime/settings/legacy.js +154 -0
  397. package/src/runtime/settings/lexical.js +56 -0
  398. package/src/runtime/settings/prepared-run.js +269 -0
  399. package/src/runtime/settings/project-layers.js +118 -0
  400. package/src/runtime/settings/refs.js +288 -0
  401. package/src/runtime/settings/registry.js +553 -0
  402. package/src/runtime/settings/resolve.js +713 -0
  403. package/src/runtime/settings/shared.js +37 -0
  404. package/src/runtime/settings/worker-billing.js +107 -0
  405. package/src/runtime/settings/write.js +269 -0
  406. package/src/runtime/slot-command.js +78 -0
  407. package/src/runtime/slots.js +671 -0
  408. package/src/runtime/source-version.js +290 -0
  409. package/src/runtime/state-root.js +217 -0
  410. package/src/runtime/storage/cli.js +12 -5
  411. package/src/runtime/storage/engine.js +9 -2
  412. package/src/runtime/storage/registry.js +14 -1
  413. package/src/runtime/storage/roots.js +66 -16
  414. package/src/runtime/storage/rules.js +82 -10
  415. package/src/runtime/storage/upkeep.js +10 -2
  416. package/src/runtime/storage/work-status.js +245 -26
  417. package/src/runtime/task-attempts.js +589 -69
  418. package/src/runtime/task-classifier.js +47 -11
  419. package/src/runtime/task-graph.js +373 -0
  420. package/src/runtime/task-run.js +778 -0
  421. package/src/runtime/task-scope.js +202 -56
  422. package/src/runtime/task-sources.js +147 -0
  423. package/src/runtime/task-state.js +440 -65
  424. package/src/runtime/team/admission.js +301 -0
  425. package/src/runtime/team/envelope.js +324 -0
  426. package/src/runtime/team/review-guard.js +156 -0
  427. package/src/runtime/trace/analyze.js +427 -0
  428. package/src/runtime/trace/baseline.js +770 -0
  429. package/src/runtime/trace/ledger.js +63 -140
  430. package/src/runtime/trace/otlp-export.js +568 -0
  431. package/src/runtime/trace/otlp-map.js +481 -0
  432. package/src/runtime/trace/spans.js +745 -0
  433. package/src/runtime/trace/telemetry.js +225 -0
  434. package/src/runtime/trace/views.js +1 -0
  435. package/src/runtime/transcripts/command.js +98 -0
  436. package/src/runtime/transcripts/cursor.js +326 -0
  437. package/src/runtime/transcripts/read.js +126 -0
  438. package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
  439. package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
  440. package/src/runtime/transcripts/readers/copilot-events.js +90 -0
  441. package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
  442. package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
  443. package/src/runtime/transcripts/registry.js +161 -0
  444. package/src/runtime/transcripts/stats.js +161 -0
  445. package/src/runtime/verification/check-key.js +628 -0
  446. package/src/runtime/verification/contract-runner.js +546 -11
  447. package/src/runtime/verification/engine.js +499 -9
  448. package/src/runtime/verification/impact.js +617 -0
  449. package/src/runtime/verification/inflight.js +205 -0
  450. package/src/runtime/verification/registry.js +176 -0
  451. package/src/runtime/verification/reuse.js +184 -0
  452. package/src/runtime/verification/telemetry-emit.js +254 -0
  453. package/src/runtime/verification/test-selection.js +67 -7
  454. package/src/runtime/worker/active.js +90 -0
  455. package/src/runtime/worker/cancel.js +222 -0
  456. package/src/runtime/worker/catalogue.js +122 -0
  457. package/src/runtime/worker/command.js +677 -0
  458. package/src/runtime/worker/evidence.js +288 -0
  459. package/src/runtime/worker/launch.js +288 -0
  460. package/src/runtime/worker/models.js +137 -0
  461. package/src/runtime/worker/parsers.js +218 -0
  462. package/src/runtime/worker/probes/index.js +706 -0
  463. package/src/runtime/worker/process-tree.js +415 -0
  464. package/src/runtime/worker/quota.js +43 -0
  465. package/src/runtime/worker/readiness.js +713 -0
  466. package/src/runtime/worker/run-store.js +166 -0
  467. package/src/runtime/worker/supervisor.js +541 -0
  468. package/src/runtime/worker/worktree-check.js +252 -0
  469. package/src/runtime/workflow-engine.js +5 -1
  470. package/src/runtime/workflow-orchestrator.js +578 -63
  471. package/src/runtime/worktree.js +149 -22
  472. package/src/state/index.js +1 -1
  473. package/src/state/lockfile.js +64 -10
  474. package/src/state/run-lock.js +1 -1
  475. package/types/node-commonjs.d.ts +106 -7
  476. package/core/.antigravity-plugin/hooks.json +0 -39
  477. package/core/harnesses/gemini/hooks/hooks.json +0 -77
  478. package/core/harnesses/gemini/hooks/lib.sh +0 -302
  479. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -21
  480. package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
  481. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -21
  482. package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
  483. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -22
  484. package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
  485. package/core/harnesses/gemini/hooks/session-end.sh +0 -5
  486. package/core/harnesses/gemini/hooks/session-start.sh +0 -5
  487. package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
  488. package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
  489. package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
  490. package/core/harnesses/shared/hooks/hooks.json +0 -47
  491. package/core/shared/guidance/FLAGS.md +0 -25
  492. package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
  493. package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
  494. package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
  495. package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
  496. package/src/adapters/gemini/hooks.js +0 -210
  497. package/src/adapters/gemini/index.js +0 -390
@@ -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'),
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const CLASS_ORDER = Object.freeze(['unknown', 'unsupported', 'observation', 'advisory', 'blocking']);
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', 'opencode', 'pi', 'copilot', 'kiro', 'antigravity']);
7
+ const BEHAVIOUR_ORDER = Object.freeze(['context-authority', 'default-silence', 'summary-scope', 'summary-grant', 'identity', 'check-freshness']);
8
+ const NEVER_BLOCKING_EVENTS = new Set([
9
+ 'pi\u0000tool_execution_start', 'antigravity\u0000PostToolUse', 'antigravity\u0000PreInvocation',
10
+ 'antigravity\u0000PostInvocation', 'claude\u0000PostToolUse', 'codex\u0000PostToolUse',
11
+ ]);
12
+ function stable(value ) {
13
+ if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
14
+ if (value && typeof value === 'object') return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stable(value[key])}`).join(',')}}`;
15
+ return JSON.stringify(value);
16
+ }
17
+ function rowsDigest(rows ) { return `sha256:${crypto.createHash('sha256').update(stable(rows)).digest('hex')}`; }
18
+ class UnknownEnforcementTarget extends Error {
19
+ code ;
20
+ constructor(code , detail ) { super(detail); this.name = 'UnknownEnforcementTarget'; this.code = code; }
21
+ }
22
+ function enforcementFor(coverage , query ) {
23
+ const rows = coverage?.rows || [];
24
+ const profiles = coverage?.profiles || {};
25
+ if (query.policy === '*') throw new UnknownEnforcementTarget('unknown-policy', "Unknown policy '*'");
26
+ if (!rows.some((row ) => row.harness === query.harness)) throw new UnknownEnforcementTarget('unknown-harness', `Unknown harness '${query.harness}'`);
27
+ if (!rows.some((row ) => row.policy === query.policy)) throw new UnknownEnforcementTarget('unknown-policy', `Unknown policy '${query.policy}'`);
28
+ if (query.profile !== undefined && !Object.hasOwn(profiles, query.profile)) throw new UnknownEnforcementTarget('unknown-profile', `Unknown profile '${query.profile}'`);
29
+ const pairRows = rows.filter((row ) => row.harness === query.harness && (row.policy === query.policy || row.policy === '*'));
30
+ const timing = query.policy === 'stop-check' ? 'stop' : 'pre-tool';
31
+ const applicable = pairRows.filter((row ) => row.timing === timing && (query.profile === undefined || row.profile === query.profile || row.profile === 'any'));
32
+ const enforced = applicable.length > 0 && applicable.every((row ) => row.wired && row.class === 'blocking' && row.verification === 'verified');
33
+ const rank = (row ) => CLASS_ORDER.indexOf(row.class);
34
+ const weakest = [...applicable].sort((a , b ) => rank(a) - rank(b))[0];
35
+ const intersection = (field ) => applicable.length ? [...new Set (applicable[0][field] || [])].filter((item) => applicable.every((row ) => (row[field] || []).includes(item))) : [];
36
+ const union = (field ) => [...new Set(applicable.flatMap((row ) => row[field] || []))];
37
+ const parity = enforced && pairRows.length > 0 && pairRows.every((row ) => row.class === 'blocking');
38
+ return {
39
+ harness: query.harness, policy: query.policy, profile: query.profile ?? null, enforced, parity,
40
+ class: weakest?.class || 'unknown', verification: weakest?.verification || 'unverified',
41
+ prerequisites: union('prerequisites'), covers: intersection('covers'), doesNotCover: union('doesNotCover'), caveats: union('caveats'),
42
+ rows: applicable, matrix: { schemaVersion: coverage.schemaVersion, revision: coverage.revision, rowsDigest: coverage.rowsDigest },
43
+ reason: weakest ? `${weakest.class} at (${weakest.policy}, ${weakest.harness}, ${weakest.event}, ${weakest.profile})` : `unknown at (${query.policy}, ${query.harness}, *, ${query.profile ?? '*'})`,
44
+ };
45
+ }
46
+ function renderEnforcementMatrix(coverage ) {
47
+ const order = (items , value ) => {
48
+ const index = items.indexOf(value);
49
+ return index < 0 ? items.length : index;
50
+ };
51
+ const rows = [...coverage.rows].sort((a , b ) => order(POLICY_ORDER, a.policy) - order(POLICY_ORDER, b.policy)
52
+ || order(HARNESS_ORDER, a.harness) - order(HARNESS_ORDER, b.harness)
53
+ || a.event.localeCompare(b.event) || a.profile.localeCompare(b.profile));
54
+ const lines = [`Schema ${coverage.schemaVersion}; revision ${coverage.revision}; ${coverage.rowsDigest}`, '| Policy | Harness | Native event | Profile | Class | Verification | Prerequisites | Does not cover |', '|---|---|---|---|---|---|---|---|'];
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(', ')} |`);
56
+ return lines.join('\n');
57
+ }
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,61 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ // IC-037 (DEC-034): the shared hooks tree and the hook front doors of every harness that reads it
6
+ // move together. An install or update that writes changed bytes into `hooks.shared` also plans the
7
+ // front doors of the other harnesses installed in the same scope, so a harness the run does not
8
+ // name is never left with doors written for an older tree. The doors ride the same plan, backup
9
+ // and ledger write as the run's own targets, and a door edited outside DoFlow refuses the whole plan.
10
+ const { selectAssets } = require('../registry/index.js');
11
+ const { resolveAdapter, projectAdapterInput } = require('../adapters/index.js');
12
+ const { renderPolicies } = require('./policies.js');
13
+
14
+ const HOOKS_TREE_ASSET = 'hooks.shared';
15
+ const OPERATIONS = ['install', 'update'];
16
+
17
+ /** Whether any target of the plan creates or updates a file of the shared hooks tree. */
18
+ function hooksTreeChanged(targets ) {
19
+ return targets.some((target ) => !target.skipped
20
+ && target.changes.some((change ) => change.assetId === HOOKS_TREE_ASSET && (change.operation === 'create' || change.operation === 'update')));
21
+ }
22
+
23
+ /** The plan entries for the hook front doors of every harness installed in this scope that the run
24
+ * does not name. Only the doors that differ from the current source appear, each with one notice;
25
+ * a door the adapter refuses to overwrite carries the conflict and the command that resolves it.
26
+ * `harnessScope` is the registry's name for the scope. `targetFromResult` is the lifecycle's own
27
+ * conversion of an adapter result into a plan entry. */
28
+ function planHookCompanions({ registry, adapters, scope, harnessScope, scopeRoot, ledger, context, targets, targetFromResult } ) {
29
+ // DEC-034 is about install and update: every other operation plans exactly the targets it was asked for.
30
+ if (!OPERATIONS.includes(context.operation) || !hooksTreeChanged(targets)) return [];
31
+ const named = new Set(targets.map((target ) => target.harness));
32
+ const installed = new Set((ledger?.resources ?? []).map((resource ) => resource.harness));
33
+ const run = targets.filter((target ) => !target.skipped).map((target ) => target.harness).join(',');
34
+ const companions = [];
35
+ for (const harness of registry.harnesses) {
36
+ if (named.has(harness.id) || !installed.has(harness.id) || !harness.scopes.includes(harnessScope)) continue;
37
+ if (!harness.capabilities?.hooks || harness.capabilities.hooks.status === 'unavailable') continue;
38
+ const adapter = resolveAdapter(adapters, harness);
39
+ if (typeof adapter.planHookDoors !== 'function') continue;
40
+ const assets = selectAssets(registry, { harness: harness.id });
41
+ const policies = renderPolicies(registry, { harness: harness.id });
42
+ const adapterInput = projectAdapterInput({ registry, harness, scope, scopeRoot, assets, mcp: [], mcpAdoptable: [], policies, context });
43
+ const input = { ...adapterInput, registry, ledger };
44
+ const discovery = adapter.discover(input);
45
+ const target = targetFromResult({ harness, assets, mcp: [], mcpSelected: null, policies, adapterInput, discovery,
46
+ result: adapter.planHookDoors({ ...input, discovery }) });
47
+ if (!target.changes.length && !target.conflicts.length) continue;
48
+ companions.push({
49
+ ...target, hookDoorsOnly: true,
50
+ // The assets that own the doors this run plans, not the harness's whole set.
51
+ assets: target.assets.filter((asset ) => target.changes.some((change ) => change.assetId === asset.id)),
52
+ conflicts: target.conflicts.length
53
+ ? [...target.conflicts, `${harness.id} hook front doors must move with the shared hooks tree; run: doflow update -t ${run},${harness.id} (add -f to overwrite edited doors). Working command: doflow update -t ${run},${harness.id} -f`]
54
+ : target.conflicts,
55
+ notices: target.changes.length ? [`refreshed ${harness.id} hook front doors to match the shared hooks tree`, ...target.notices] : target.notices,
56
+ });
57
+ }
58
+ return companions;
59
+ }
60
+
61
+ module.exports = { planHookCompanions };