@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
@@ -2,10 +2,9 @@
2
2
 
3
3
  // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
4
 
5
- // Shared hook-command verification — the one piece of codex/hooks.ts and gemini/hooks.ts that
6
- // proved byte-identical on inspection (unlike SUPPORTED_EVENTS, validateHooksConfig, and
7
- // classifyClaudeGuardrails, which encode genuine per-harness differences and stay in each
8
- // adapter). verifyHookCommands and the two private helpers it calls, commandScriptNames and
5
+ // Hook-command verification, kept apart from codex/hooks.ts because it does not depend on the
6
+ // harness (unlike SUPPORTED_EVENTS, validateHooksConfig, and classifyClaudeGuardrails, which
7
+ // encode genuine per-harness differences and stay in each adapter). verifyHookCommands and the two private helpers it calls, commandScriptNames and
9
8
  // hookHandlers, check that a hook config's command handlers resolve to a real, executable script
10
9
  // in the harness's own hooks directory — that check does not vary by harness, so it lives once.
11
10
  const fs = require('node:fs');
@@ -23,12 +22,9 @@ function hookHandlers(config ) {
23
22
  (Array.isArray(group?.hooks) ? group.hooks : []).map((handler ) => ({ event, handler }))));
24
23
  }
25
24
 
26
- // On `trusted`: both harnesses gate hook execution behind their own trust prompt — Codex through
27
- // its config, Gemini by fingerprinting a hook's name and command and warning before running one
28
- // that changed (geminicli.com/docs/hooks/). DoFlow verifies neither gate live; doing so would need
29
- // a running CLI session. It reports trust as a prerequisite whose shape it observed, not one it
30
- // bypassed or assumed satisfied. That reading is identical for both harnesses, which is part of
31
- // why this function is shared.
25
+ // On `trusted`: Codex gates hook execution behind its own trust prompt, through its config.
26
+ // DoFlow does not verify that gate live; doing so would need a running CLI session. It reports
27
+ // trust as a prerequisite whose shape it observed, not one it bypassed or assumed satisfied.
32
28
  // Windows carries no executable permission bits — statSync().mode always reports 0666/0444 and
33
29
  // chmodSync can only toggle the read-only flag — so the POSIX "not executable" failure mode
34
30
  // cannot exist there. Requiring the bit would refuse every hooks install on win32 (the scripts
@@ -52,6 +52,7 @@ function projectAdapterInput({ registry, harness, scope, scopeRoot, assets = [],
52
52
  // source paths, which is all of them except Antigravity's directory-per-agent requirement.
53
53
  layout: projection.layout ?? null,
54
54
  transform: projection.transform ?? null,
55
+ select: projection.select ?? null,
55
56
  prerequisites: [...(capability.prerequisites || [])],
56
57
  });
57
58
  };
@@ -7,11 +7,13 @@
7
7
  // Kiro's native root is symmetric across scopes — `.kiro/` (project) / `~/.kiro/` (global) is the
8
8
  // one directory that holds everything Kiro reads: steering, agents, hooks, and settings/mcp.json
9
9
  // (see src/targets.js's toolDirs() for the same convention this adapter's own nativePaths()
10
- // mirrors). Unlike every other harness in this codebase, Kiro receives DoFlow's *full* guidance
11
- // tree as steering files rather than a single marker-wrapped pointer file plus a separately-copied
12
- // tree (design.md §4's "research decision D3"): there is no single-file instruction surface here
13
- // at all, so — unlike src/adapters/gemini or src/adapters/opencode — this adapter never renders a
14
- // marker-wrapped instruction file.
10
+ // mirrors). Unlike every other harness in this codebase, Kiro receives DoFlow's guidance as
11
+ // steering files rather than a single marker-wrapped pointer file plus a separately-copied tree
12
+ // (design.md §4's "research decision D3"): there is no single-file instruction surface here at all,
13
+ // so — unlike src/adapters/opencode — this adapter never renders a
14
+ // marker-wrapped instruction file. Steering holds the core set only (DOFLOW_CORE.md and the files it
15
+ // imports, selected by the `guidance-manifest` select); the full guidance tree remains under
16
+ // `.doflow/guidance` through kiro.guidance-tree, where `retrieve` and the runtime read it.
15
17
  //
16
18
  // Evidence: https://kiro.dev/docs/steering/, https://kiro.dev/docs/mcp/configuration/,
17
19
  // https://kiro.dev/docs/hooks/
@@ -37,6 +39,15 @@ const LEGACY_MCP_REGISTRATION = `${HARNESS}:mcp:registration`;
37
39
  * module-level exports delegate to a default-configured instance so direct callers keep their
38
40
  * historical shape.
39
41
  */
42
+ /** Hook tiers (maint-hooks): with Kiro outside `hooks.harnesses` (`context.hooksOff`) its agents carry no
43
+ * `hooks`, so they never name a hook script DoFlow did not install. */
44
+ const CONTENT_ONLY_TRANSFORMS = Object.freeze({ 'kiro-agents': 'kiro-agents-content' });
45
+ function transformFor(asset , context ) {
46
+ if (context?.hooksOff !== true) return asset.transform || null;
47
+ if (asset.id === 'kiro.doflow-agent') return 'kiro-agent-content';
48
+ return CONTENT_ONLY_TRANSFORMS[asset.transform] ?? (asset.transform || null);
49
+ }
50
+
40
51
  function createKiroAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] } = {}) {
41
52
  // Parity note: Kiro's historical nativePaths() derived even its user-scope root from scopeRoot
42
53
  // (the CLI passes $HOME there for -g installs), never from a separate homeDir argument — so the
@@ -59,8 +70,8 @@ function createKiroAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] } =
59
70
 
60
71
  /**
61
72
  * Kiro has no single-file, marker-managed instruction surface for this adapter to render into —
62
- * the guidance tree is projected file-for-file by the copy-tree engine below, once as steering
63
- * (guidance.context-layer) and once into the shared `.doflow/guidance` (kiro.guidance-tree). Every
73
+ * guidance is projected file-for-file by the copy-tree engine below, the core set as steering
74
+ * (guidance.context-layer) and the whole tree into the shared `.doflow/guidance` (kiro.guidance-tree). Every
64
75
  * asset routed to 'kiro' in core/registry/assets.json uses `renderer: copy-tree` or
65
76
  * `kiro-agents`, so nothing reaches this function. It is kept as a pure passthrough only to
66
77
  * satisfy the six-function adapter contract every harness must implement identically.
@@ -98,7 +109,7 @@ function createKiroAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] } =
98
109
  const destDir = copyTreeDestDir(paths.configDir, asset);
99
110
  const sourceDir = sourceDirFor(asset, context, fsImpl, 'Kiro');
100
111
  const previousResources = ledgerFileResources(ledger?.resources, HARNESS, asset.id);
101
- const result = planTree({ sourceDir, destDir, previousResources, siblingFingerprints: ledgerSiblingFingerprints(ledger?.resources, HARNESS), operation: removing ? 'remove' : 'apply', fsImpl, layout: asset.layout, transform: asset.transform, keepModified: !removing,
112
+ const result = planTree({ sourceDir, destDir, previousResources, siblingFingerprints: ledgerSiblingFingerprints(ledger?.resources, HARNESS), operation: removing ? 'remove' : 'apply', fsImpl, layout: asset.layout, transform: transformFor(asset, context), select: asset.select, keepModified: !removing,
102
113
  // Forwarded so the CLI's --force reaches planTree's conflict check; omitting it let
103
114
  // planTree's own `force = false` default stand in silently. Gated on `!removing` for the
104
115
  // reason codex/index.ts states in full: force heals drift on apply, but a hand-edited file
@@ -115,7 +126,7 @@ function createKiroAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] } =
115
126
  ownershipIdentity: `doflow:${HARNESS}:copy-tree:${asset.id}:${change.relPath}`,
116
127
  kind: 'copy-tree-file', identity: change.relPath,
117
128
  afterFingerprint: change.fingerprint, fingerprint: change.fingerprint, sourceVersion: 'registry-v1',
118
- transformName: asset.transform || null,
129
+ transformName: transformFor(asset, context),
119
130
  ...(change.kept ? { retained: true, retainedFor: [] } : {}),
120
131
  projection: { renderer: asset.renderer },
121
132
  });
@@ -153,7 +164,7 @@ function createKiroAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] } =
153
164
  for (const asset of kiroTreeAssets(assets)) {
154
165
  const destDir = copyTreeDestDir(paths.configDir, asset);
155
166
  const sourceDir = sourceDirFor(asset, context, fsImpl, 'Kiro');
156
- const result = verifyTree({ sourceDir, destDir, fsImpl, layout: asset.layout, transform: asset.transform, plannedWrites: context?.plannedWrites });
167
+ const result = verifyTree({ sourceDir, destDir, fsImpl, layout: asset.layout, transform: transformFor(asset, context), select: asset.select, plannedWrites: context?.plannedWrites });
157
168
  conflicts.push(...result.conflicts.map((reason ) => `${asset.id}: ${reason}`));
158
169
  for (const resource of result.resources) {
159
170
  resources.push({
@@ -209,8 +220,8 @@ function createKiroAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] } =
209
220
 
210
221
  /** Kiro has no hooks or mcp asset of its own in core/registry/assets.json (mcp servers come from
211
222
  * core/registry/mcp.json, not an asset), so an MCP entry change piggybacks on an asset id
212
- * this harness actually receives — the same "pseudo-component" technique
213
- * src/adapters/gemini/index.js#hooksAssetId already uses for its own settings-only component. */
223
+ * this harness actually receives — the same "pseudo-component" technique the opencode and
224
+ * copilot adapters use for their settings-only changes. */
214
225
  function pseudoAssetId(assets ) {
215
226
  return assets.find((asset ) => asset.id === 'guidance.context-layer')?.id ?? assets[0]?.id;
216
227
  }
@@ -258,6 +269,19 @@ function createKiroAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] } =
258
269
  };
259
270
  }
260
271
 
272
+ /** The hook front doors (IC-037): the scripts and the agent that wires them. */
273
+ function hookDoorAssets(assets ) { return (assets || []).filter((asset ) => asset.id === 'kiro.hooks-scripts' || asset.id === 'kiro.doflow-agent'); }
274
+
275
+ function planHookDoors({ scope, scopeRoot, assets = [], context = {}, ledger, fsImpl = fs } ) {
276
+ const doors = planCopyTreeAssets({ assets: hookDoorAssets(assets), scope, scopeRoot, context, ledger, removing: false, fsImpl });
277
+ return { changes: doors.changes, conflicts: doors.conflicts, prerequisites: [], notices: doors.notices };
278
+ }
279
+
280
+ function verifyHookDoors({ scope, scopeRoot, assets = [], context = {}, fsImpl = fs } ) {
281
+ const doors = verifyCopyTreeAssets({ assets: hookDoorAssets(assets), scope, scopeRoot, context, fsImpl });
282
+ return { ok: doors.conflicts.length === 0, statuses: doors.statuses, resources: doors.resources, conflicts: doors.conflicts };
283
+ }
284
+
261
285
  function apply({ changes = [], fsImpl = fs } ) {
262
286
  writeMcpEntries(changes, { container: MCP_CONTAINER, fsImpl });
263
287
  applyCopyTreeAssets(changes, { fsImpl });
@@ -297,7 +321,7 @@ function createKiroAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] } =
297
321
  };
298
322
  }
299
323
 
300
- return { nativePaths, discover, render, plan, apply, remove, verify, buildServerEntry };
324
+ return { nativePaths, discover, render, plan, apply, remove, verify, planHookDoors, verifyHookDoors, buildServerEntry };
301
325
  }
302
326
 
303
327
  const singleton = createKiroAdapter();
@@ -305,6 +329,7 @@ const singleton = createKiroAdapter();
305
329
  module.exports = {
306
330
  nativePaths: singleton.nativePaths, discover: singleton.discover, render: singleton.render,
307
331
  plan: singleton.plan, apply: singleton.apply, remove: singleton.remove, verify: singleton.verify,
332
+ planHookDoors: singleton.planHookDoors, verifyHookDoors: singleton.verifyHookDoors,
308
333
  buildServerEntry: singleton.buildServerEntry,
309
334
  createKiroAdapter,
310
335
  };
@@ -4,7 +4,7 @@
4
4
 
5
5
  // OpenCode adapter.
6
6
  //
7
- // OpenCode gets its own copy of DoFlow's skills tree, the same as Claude, Codex, and Gemini: it
7
+ // OpenCode gets its own copy of DoFlow's skills tree, the same as Claude and Codex: it
8
8
  // natively discovers skills under `.opencode/skills/` (project) and `~/.config/opencode/skills/`
9
9
  // (global) without any settings entry, so the shared copy-tree engine in ../copy-tree.ts
10
10
  // materialises `skills.doflow` there directly. What remains registration, rather than
@@ -20,11 +20,11 @@ const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDes
20
20
  const { readMcpFiles, planMcpEntries, verifyMcpEntries, ownedMcpIds, writeMcpEntries } = require('../mcp-entries.js');
21
21
 
22
22
  // Only the marker constants: marker-merge.js reads and writes files itself, which cannot be used
23
- // from plan(), whose contract is to compute changes without touching disk. The gemini adapter
24
- // solves this the same way, with a pure managedInstruction() over strings.
23
+ // from plan(), whose contract is to compute changes without touching disk.
25
24
  const { MARKER_START, MARKER_END } = require('../../helper/marker-merge.js');
26
25
  const { destructiveCommandGlobs } = require('../../helper/guardrails.js');
27
26
  const { declaredHarnessPaths, resolveHarnessPaths } = require('../../helper/harness-paths.js');
27
+ const { renderGuidancePointer } = require('../guidance-pointer.js');
28
28
 
29
29
  const HARNESS = 'opencode';
30
30
  /**
@@ -107,9 +107,8 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
107
107
  }
108
108
 
109
109
  /** Replace only the span between DoFlow's markers, leaving everything else byte-identical.
110
- * An AGENTS.md that exists without a DoFlow section is refused rather than appended to — the same
111
- * policy the gemini adapter applies to GEMINI.md, and for the same reason: AGENTS.md is a shared,
112
- * cross-tool file, so where DoFlow's content belongs inside it is a decision for its owner, not a
110
+ * An AGENTS.md that exists without a DoFlow section is refused rather than appended to:
111
+ * AGENTS.md is a shared, cross-tool file, so where DoFlow's content belongs inside it is a decision for its owner, not a
113
112
  * guess for the installer. Add an empty marker pair to opt in. */
114
113
  /** Opt-in (--permissions) guardrail projection: DoFlow's destructive-command blocklist lands as
115
114
  * deny rules under permission.bash. Only keys this feature owns (each carries a matching
@@ -259,7 +258,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
259
258
  /** OpenCode's settings-merge change has no asset of its own in core/registry/assets.json (it's a
260
259
  * registration side-effect of the instructions/mcp assets, not a projected asset), so it piggybacks
261
260
  * on a real asset id already routed to this harness — the same "pseudo-component" technique
262
- * src/adapters/gemini/index.js#hooksAssetId and src/adapters/kiro/index.js#pseudoAssetId use. The
261
+ * src/adapters/kiro/index.js#pseudoAssetId uses. The
263
262
  * lifecycle layer validates every change's assetId against the harness's actual asset list, so an
264
263
  * invented id like the literal string 'opencode.config' is rejected as unknown. */
265
264
  function pseudoAssetId(assets ) {
@@ -308,7 +307,8 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
308
307
 
309
308
  const guidance = assets.find((asset ) => asset.capability === 'instructions');
310
309
  if (guidance) {
311
- const rendered = render({ content: fsImpl.readFileSync(path.resolve(context.repoRoot, guidance.source), 'utf8') });
310
+ const pointer = renderGuidancePointer(fsImpl.readFileSync(path.resolve(context.repoRoot, guidance.source), 'utf8'), { scope, scopeRoot });
311
+ const rendered = render({ content: pointer });
312
312
  const outcome = removing
313
313
  ? { ok: true, operation: 'remove', content: strippedInstruction(found.instruction) }
314
314
  : managedInstruction(found.instruction, rendered);
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const { fingerprint } = require('../copy-tree.js');
8
+
9
+ const ASSET_ID = 'pi.guard-extension';
10
+ const EXTENSION_NAME = 'doflow-guard.js';
11
+ const OWNERSHIP = 'pi:guard-extension';
12
+ const TEMPLATE = 'core/harnesses/pi/extensions/doflow-guard.js';
13
+ const BRIDGE_SUFFIX = ['.doflow', 'shared', 'hooks', 'bridge', 'bridge.sh'];
14
+
15
+ function userAgentDir(scopeRoot , env ) {
16
+ const override = env?.PI_CODING_AGENT_DIR;
17
+ return typeof override === 'string' && override.trim() ? path.resolve(override.trim()) : path.join(scopeRoot, '.pi', 'agent');
18
+ }
19
+
20
+ function targetPath(scopeRoot , env , scope = 'user') {
21
+ return scope === 'project'
22
+ ? path.join(scopeRoot, '.pi', 'extensions', EXTENSION_NAME)
23
+ : path.join(userAgentDir(scopeRoot, env), 'extensions', EXTENSION_NAME);
24
+ }
25
+
26
+ function renderExtension({ repoRoot, scopeRoot, target, fsImpl = fs } ) {
27
+ const templatePath = path.join(repoRoot, TEMPLATE);
28
+ const template = fsImpl.readFileSync(templatePath, 'utf8');
29
+ const bridgePath = path.join(scopeRoot, ...BRIDGE_SUFFIX);
30
+ const relative = path.relative(path.dirname(target), bridgePath).split(path.sep).join('/');
31
+ const rendered = template.replace("const BRIDGE = new URL('../../shared/hooks/bridge/bridge.sh', import.meta.url);",
32
+ `const BRIDGE = new URL(${JSON.stringify(relative)}, import.meta.url);`);
33
+ if (rendered === template) throw new Error('Pi extension bridge path template was not found');
34
+ return rendered;
35
+ }
36
+
37
+ function planPiHooks({ scope, scopeRoot, assets = [], ledger, context = {}, adopt = false, env = process.env, fsImpl = fs } ) {
38
+ if (scope !== 'user' && scope !== 'global' && scope !== 'project') return { changes: [], conflicts: [], notices: [] };
39
+ const asset = assets.find((row ) => row.id === ASSET_ID);
40
+ if (!asset) return { changes: [], conflicts: [], notices: [] };
41
+ const computedTarget = targetPath(scopeRoot, env, scope);
42
+ const removing = context.operation === 'remove';
43
+ const ownedRows = (ledger?.resources || []).filter((row ) => row.harness === 'pi' && row.assetId === ASSET_ID
44
+ && row.ownershipIdentity === OWNERSHIP);
45
+ const exactOwned = ownedRows.find((row ) => row.target === computedTarget);
46
+ const owned = exactOwned || (removing && ownedRows.length === 1 ? ownedRows[0] : undefined);
47
+ const target = removing && owned ? owned.target : computedTarget;
48
+ const exists = fsImpl.existsSync(target);
49
+ const notices = [];
50
+ if (removing) {
51
+ if (!owned) return { changes: [], conflicts: [], notices };
52
+ if (!exists) return { changes: [{ assetId: ASSET_ID, target, operation: 'remove', release: true,
53
+ ownershipIdentity: OWNERSHIP, harness: 'pi', projection: { renderer: 'pi-guard-extension' } }], conflicts: [], notices };
54
+ const current = fsImpl.readFileSync(target, 'utf8');
55
+ if (fingerprint(current) !== owned.fingerprint) {
56
+ notices.push(`kept hand-edited ${target}; DoFlow no longer manages it`);
57
+ return { changes: [{ assetId: ASSET_ID, target, operation: 'remove', release: true,
58
+ ownershipIdentity: OWNERSHIP, harness: 'pi', projection: { renderer: 'pi-guard-extension' } }], conflicts: [], notices };
59
+ }
60
+ return { changes: [{ assetId: ASSET_ID, target, operation: 'remove', content: null,
61
+ ownershipIdentity: OWNERSHIP, harness: 'pi', projection: { renderer: 'pi-guard-extension' } }], conflicts: [], notices };
62
+ }
63
+
64
+ const content = renderExtension({ repoRoot: context.repoRoot, scopeRoot, target, fsImpl });
65
+ if (!exists) return { changes: [{ assetId: ASSET_ID, target, operation: 'create', content,
66
+ ownershipIdentity: OWNERSHIP, harness: 'pi', fingerprint: fingerprint(content),
67
+ projection: { renderer: 'pi-guard-extension' } }], conflicts: [], notices };
68
+ if (!owned && !adopt) {
69
+ return { changes: [], conflicts: [`Pi extension already exists and is not owned by DoFlow: ${target}`], notices };
70
+ }
71
+ const current = fsImpl.readFileSync(target, 'utf8');
72
+ if (owned && fingerprint(current) !== owned.fingerprint) {
73
+ return { changes: [], conflicts: [`Pi extension was modified outside DoFlow: ${target}`], notices };
74
+ }
75
+ if (current === content) return { changes: [], conflicts: [], notices };
76
+ return { changes: [{ assetId: ASSET_ID, target, operation: 'update', content,
77
+ ownershipIdentity: OWNERSHIP, harness: 'pi', fingerprint: fingerprint(content),
78
+ projection: { renderer: 'pi-guard-extension' } }], conflicts: [], notices };
79
+ }
80
+
81
+ function verifyPiHooks({ scope, scopeRoot, assets = [], ledger, context = {}, adopt = false, env = process.env, fsImpl = fs, pathEnv = process.env.PATH } ) {
82
+ if (scope !== 'user' && scope !== 'global' && scope !== 'project') return { statuses: [], resources: [], conflicts: [] };
83
+ const asset = assets.find((row ) => row.id === ASSET_ID);
84
+ if (!asset) return { statuses: [], resources: [], conflicts: [] };
85
+ const target = targetPath(scopeRoot, env, scope);
86
+ const owned = (ledger?.resources || []).find((row ) => row.harness === 'pi' && row.assetId === ASSET_ID
87
+ && row.ownershipIdentity === OWNERSHIP && row.target === target);
88
+ const bridgePath = path.join(scopeRoot, ...BRIDGE_SUFFIX);
89
+ const present = fsImpl.existsSync(target);
90
+ const content = present ? fsImpl.readFileSync(target, 'utf8') : null;
91
+ const actualFingerprint = content === null ? null : fingerprint(content);
92
+ const fingerprintMatches = !!owned && actualFingerprint === owned.fingerprint;
93
+ const plannedWrite = (context?.plannedWrites || []).includes(target);
94
+ const bridgeMatches = !!content && content.includes(JSON.stringify(path.relative(path.dirname(target), bridgePath).split(path.sep).join('/')));
95
+ const commandOnPath = (command ) => String(pathEnv || '').split(path.delimiter)
96
+ .some((dir) => [command, ...(process.platform === 'win32' ? ['.exe', '.cmd', '.bat'].map((ext) => command + ext) : [])]
97
+ .some((name) => fsImpl.existsSync(path.join(dir, name))));
98
+ const status = present && !owned && !plannedWrite && !adopt ? 'conflict'
99
+ : (present && (fingerprintMatches || plannedWrite) && bridgeMatches && commandOnPath('bash') && commandOnPath('jq') ? 'managed' : 'absent');
100
+ const resources = status === 'managed' ? [{ assetId: ASSET_ID, target, ownershipIdentity: OWNERSHIP,
101
+ fingerprint: actualFingerprint, sourceVersion: context.sourceVersion ?? 'unknown',
102
+ projection: { renderer: 'pi-guard-extension' } }] : [];
103
+ const statuses = [{ harness: 'pi', assetId: ASSET_ID, capability: 'hooks', status, target,
104
+ ownershipIdentity: OWNERSHIP, fingerprint: actualFingerprint, fingerprintMatches,
105
+ bridgePath, bridgeExists: fsImpl.existsSync(bridgePath), bash: commandOnPath('bash'), jq: commandOnPath('jq'),
106
+ prerequisites: scope === 'project' ? ['project-trust'] : [], piCodingAgentDir: typeof env?.PI_CODING_AGENT_DIR === 'string' && env.PI_CODING_AGENT_DIR.trim() ? env.PI_CODING_AGENT_DIR.trim() : null }];
107
+ const conflicts = status === 'conflict' ? [`Pi extension is not owned by DoFlow: ${target}`] : [];
108
+ return { statuses, resources, conflicts };
109
+ }
110
+
111
+ function removePiHooks(changes , { fsImpl = fs } = {}) {
112
+ let removed = 0;
113
+ for (const change of changes) {
114
+ if (change.projection?.renderer !== 'pi-guard-extension' || change.operation !== 'remove' || change.release) continue;
115
+ if (fsImpl.existsSync(change.target)) { fsImpl.unlinkSync(change.target); removed += 1; }
116
+ }
117
+ return removed;
118
+ }
119
+
120
+ module.exports = { ASSET_ID, EXTENSION_NAME, OWNERSHIP, userAgentDir, targetPath, renderExtension, planPiHooks, verifyPiHooks, removePiHooks };
@@ -3,7 +3,7 @@
3
3
  // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
4
 
5
5
  // Pi adapter — a marker-merged instruction file plus copy-tree materialisation, the same shape
6
- // Claude/Codex/Gemini already use for their own skills trees.
6
+ // Claude/Codex already use for their own skills trees.
7
7
  //
8
8
  // Skills materialise into Pi's own directories (`~/.pi/agent/skills` global, `.pi/skills` project)
9
9
  // via the shared copy-tree engine, rather than pointing Pi's settings.json at DoFlow's already
@@ -13,9 +13,7 @@
13
13
  //
14
14
  // MCP servers are merged per server into Pi's mcp.json (.pi/mcp.json project; ~/.pi/agent/mcp.json
15
15
  // or $PI_CODING_AGENT_DIR/mcp.json user) by ./mcp.ts; Pi reads it through its built-in MCP from
16
- // 0.99.0. Hooks are absent — Pi lifecycle handlers are TypeScript modules registered via pi.on(),
17
- // executing with full system permissions, which is a different trust model from the shell scripts
18
- // DoFlow ships.
16
+ // 0.99.0. The user- and project-scope lifecycle extension is ledger-owned and resolves its shared bridge at install time.
19
17
  //
20
18
  // Evidence: https://pi.dev/docs/latest/settings, https://pi.dev/docs/latest/skills,
21
19
  // https://pi.dev/docs/latest/quickstart, https://pi.dev/docs/latest/extensions,
@@ -24,12 +22,15 @@ const fs = require('node:fs');
24
22
  const path = require('node:path');
25
23
 
26
24
  const { MARKER_START, MARKER_END } = require('../../helper/marker-merge.js');
27
- const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, sharedTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, fingerprint, sourceDirFor } = require('../copy-tree.js');
25
+ const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, sharedTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, fingerprint, sourceDirFor, resolveTransform } = require('../copy-tree.js');
28
26
  const { declaredHarnessPaths, resolveHarnessPaths } = require('../../helper/harness-paths.js');
29
27
  const { mcpRows, discoverPiMcp, ownedPiMcpIds, planPiMcp, applyPiMcp, verifyPiMcp } = require('./mcp.js');
28
+ const { planPiHooks, verifyPiHooks, removePiHooks } = require('./hooks.js');
29
+ const { renderGuidancePointer } = require('../guidance-pointer.js');
30
30
 
31
31
  const HARNESS = 'pi';
32
- const AGENT_DIR_NOTICE = 'PI_CODING_AGENT_DIR is set: Pi reads its whole agent dir from it, but DoFlow moves only mcp.json there; skills and AGENTS.md stay in ~/.pi/agent.';
32
+ const AGENT_DIR_MCP_NOTICE = 'PI_CODING_AGENT_DIR is set: Pi reads its whole agent dir from it, but DoFlow moves only mcp.json there; skills and AGENTS.md stay in ~/.pi/agent.';
33
+ const AGENT_DIR_NOTICE = 'PI_CODING_AGENT_DIR is set: Pi reads its whole agent dir from it, and DoFlow moves mcp.json and the guard extension there; skills and AGENTS.md stay in ~/.pi/agent.';
33
34
 
34
35
  // Native path facts live in core/registry/harnesses.json under this harness's "paths" section and
35
36
  // resolve through the shared harness-paths resolver; nothing below hardcodes them.
@@ -43,8 +44,8 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
43
44
  function nativePaths({ scope, scopeRoot } = {}) {
44
45
  const paths = resolveHarnessPaths(declaredPaths, { scope, scopeRoot });
45
46
  // Pi's user-scope mcp.json follows $PI_CODING_AGENT_DIR when set. It is not declared in the
46
- // registry because the path schema has no environment base (src/helper/harness-paths.js); only
47
- // mcp moves, skills and AGENTS.md keep their declared locations.
47
+ // registry because the path schema has no environment base (src/helper/harness-paths.js); the
48
+ // guard extension follows it too, while skills and AGENTS.md keep their declared locations.
48
49
  if ((scope === 'user' || scope === 'global') && agentDirOverride()) paths.mcp = path.join(path.resolve(agentDirOverride()), 'mcp.json');
49
50
  return paths;
50
51
  }
@@ -105,7 +106,7 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
105
106
  const destDir = sharedTreeDestDir(paths.root, asset.nativeDir) ?? copyTreeDestDir(paths.configDir, asset);
106
107
  const sourceDir = sourceDirFor(asset, context, fsImpl, 'Pi');
107
108
  const previousResources = ledgerFileResources(ledger?.resources, HARNESS, asset.id);
108
- const result = planTree({ sourceDir, destDir, previousResources, siblingFingerprints: ledgerSiblingFingerprints(ledger?.resources, HARNESS), operation: removing ? 'remove' : 'apply', fsImpl, layout: asset.layout, keepModified: !removing,
109
+ const result = planTree({ sourceDir, destDir, previousResources, siblingFingerprints: ledgerSiblingFingerprints(ledger?.resources, HARNESS), operation: removing ? 'remove' : 'apply', fsImpl, layout: asset.layout, transform: asset.transform, keepModified: !removing,
109
110
  // Forwarded so the CLI's --force reaches planTree's conflict check; omitting it let
110
111
  // planTree's own `force = false` default stand in silently. Gated on `!removing` for the
111
112
  // reason codex/index.ts states in full: force heals drift on apply, but a hand-edited file
@@ -123,6 +124,7 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
123
124
  kind: 'copy-tree-file', identity: change.relPath,
124
125
  afterFingerprint: change.fingerprint, fingerprint: change.fingerprint, sourceVersion: 'registry-v1',
125
126
  ...(change.kept ? { retained: true, retainedFor: [] } : {}),
127
+ transformName: asset.transform || null,
126
128
  projection: { renderer: 'copy-tree' },
127
129
  });
128
130
  }
@@ -132,8 +134,12 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
132
134
 
133
135
  function applyCopyTreeAssets(changes , { fsImpl = fs } = {}) {
134
136
  const treeChanges = changes.filter((change ) => change.projection?.renderer === 'copy-tree' && change.operation !== 'remove')
135
- .map((change ) => ({ relPath: change.identity, target: change.target, source: change.source, operation: change.operation, fingerprint: change.fingerprint }));
136
- return applyTree({ changes: treeChanges, fsImpl }).applied;
137
+ .map((change ) => ({ relPath: change.identity, target: change.target, source: change.source, operation: change.operation, fingerprint: change.fingerprint, transformName: change.transformName || null }));
138
+ const grouped = new Map ();
139
+ for (const change of treeChanges) grouped.set(change.transformName, [...(grouped.get(change.transformName) || []), change]);
140
+ let applied = 0;
141
+ for (const [transformName, group] of grouped) applied += applyTree({ changes: group, fsImpl, transform: resolveTransform(transformName) }).applied;
142
+ return applied;
137
143
  }
138
144
 
139
145
  function removeCopyTreeAssets(changes , { fsImpl = fs } = {}) {
@@ -150,7 +156,7 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
150
156
  for (const asset of copyTreeAssets(assets)) {
151
157
  const destDir = sharedTreeDestDir(paths.root, asset.nativeDir) ?? copyTreeDestDir(paths.configDir, asset);
152
158
  const sourceDir = sourceDirFor(asset, context, fsImpl, 'Pi');
153
- const result = verifyTree({ sourceDir, destDir, fsImpl, layout: asset.layout, plannedWrites: context?.plannedWrites });
159
+ const result = verifyTree({ sourceDir, destDir, fsImpl, layout: asset.layout, transform: asset.transform, plannedWrites: context?.plannedWrites });
154
160
  conflicts.push(...result.conflicts.map((reason ) => `${asset.id}: ${reason}`));
155
161
  for (const resource of result.resources) {
156
162
  resources.push({
@@ -175,7 +181,8 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
175
181
 
176
182
  const guidance = assets.find((asset ) => asset.capability === 'instructions');
177
183
  if (guidance) {
178
- const rendered = render({ content: fsImpl.readFileSync(path.resolve(context.repoRoot, guidance.source), 'utf8') });
184
+ const pointer = renderGuidancePointer(fsImpl.readFileSync(path.resolve(context.repoRoot, guidance.source), 'utf8'), { scope, scopeRoot });
185
+ const rendered = render({ content: pointer });
179
186
  const outcome = removing
180
187
  ? { ok: true, operation: 'remove', content: strippedInstruction(found.instruction) }
181
188
  : managedInstruction(found.instruction, rendered);
@@ -191,15 +198,32 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
191
198
  changes.push(...copyTree.changes);
192
199
  conflicts.push(...copyTree.conflicts);
193
200
 
201
+ const piHooks = planPiHooks({ scope, scopeRoot, assets, ledger, context, adopt: context?.adopt === true, env, fsImpl });
202
+ changes.push(...piHooks.changes);
203
+ conflicts.push(...piHooks.conflicts);
204
+
194
205
  const piMcp = planPiMcp({ selected: mcp, rows: mcpRows(ledger), file: found.paths.mcp, scope, removing, snapshot: found.mcp, assets });
195
206
  changes.push(...piMcp.changes);
196
207
  conflicts.push(...piMcp.conflicts);
197
208
 
198
- const notices = [...copyTree.notices, ...piMcp.notices];
199
- if (!removing && scope !== 'project' && agentDirOverride()) notices.push(AGENT_DIR_NOTICE);
209
+ const notices = [...copyTree.notices, ...piMcp.notices, ...piHooks.notices];
210
+ if (!removing && scope !== 'project' && agentDirOverride()) {
211
+ notices.push(assets.some((asset ) => asset.id === 'pi.guard-extension') ? AGENT_DIR_NOTICE : AGENT_DIR_MCP_NOTICE);
212
+ }
200
213
  return { changes, conflicts, notices, paths: found.paths };
201
214
  }
202
215
 
216
+ /** The hook front door (IC-037): the guard extension. */
217
+ function planHookDoors({ scope, scopeRoot, assets = [], context = {}, ledger, fsImpl = fs } ) {
218
+ const door = planPiHooks({ scope, scopeRoot, assets, ledger, context, adopt: context?.adopt === true, env, fsImpl });
219
+ return { changes: door.changes, conflicts: door.conflicts, notices: door.notices };
220
+ }
221
+
222
+ function verifyHookDoors({ scope, scopeRoot, assets = [], ledger, context = {}, fsImpl = fs } ) {
223
+ const door = verifyPiHooks({ scope, scopeRoot, assets, ledger, context, adopt: context?.adopt === true, env, fsImpl });
224
+ return { ok: door.conflicts.length === 0 && !door.statuses.some((s ) => s.status === 'invalid' || s.status === 'conflict'), ...door };
225
+ }
226
+
203
227
  function writeChange(change , fsImpl ) {
204
228
  fsImpl.mkdirSync(path.dirname(change.target), { recursive: true });
205
229
  fsImpl.writeFileSync(change.target, change.content, 'utf8');
@@ -226,10 +250,12 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
226
250
  let removed = applyPiMcp(changes.filter(isPiMcp), { fsImpl }).applied;
227
251
  for (const change of changes) {
228
252
  if (change.operation !== 'remove' || change.projection?.renderer === 'copy-tree' || isPiMcp(change)) continue;
253
+ if (change.projection?.renderer === 'pi-guard-extension') continue;
229
254
  if (change.content === null) continue;
230
255
  writeChange(change, fsImpl);
231
256
  removed += 1;
232
257
  }
258
+ removed += removePiHooks(changes, { fsImpl });
233
259
  removed += removeCopyTreeAssets(changes, { fsImpl });
234
260
  return { removed };
235
261
  }
@@ -263,10 +289,15 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
263
289
  resources.push(...piMcp.resources);
264
290
  conflicts.push(...piMcp.conflicts);
265
291
 
292
+ const piHooks = verifyPiHooks({ scope, scopeRoot, assets, ledger, context, adopt: context?.adopt === true, env, fsImpl });
293
+ statuses.push(...piHooks.statuses);
294
+ resources.push(...piHooks.resources);
295
+ conflicts.push(...piHooks.conflicts);
296
+
266
297
  return { ok: conflicts.length === 0 && !statuses.some((s) => s.status === 'invalid' || s.status === 'conflict'), resources, statuses, conflicts };
267
298
  }
268
299
 
269
- return { nativePaths, discover, render, plan, apply, remove, verify };
300
+ return { nativePaths, discover, render, plan, apply, remove, verify, planHookDoors, verifyHookDoors };
270
301
  }
271
302
 
272
303
  const singleton = createPiAdapter();
@@ -274,6 +305,7 @@ const singleton = createPiAdapter();
274
305
  module.exports = {
275
306
  HARNESS, nativePaths: singleton.nativePaths, discover: singleton.discover, render: singleton.render,
276
307
  plan: singleton.plan, apply: singleton.apply, remove: singleton.remove, verify: singleton.verify,
308
+ planHookDoors: singleton.planHookDoors, verifyHookDoors: singleton.verifyHookDoors,
277
309
  managedInstruction: (singleton ).managedInstruction, strippedInstruction: (singleton ).strippedInstruction,
278
310
  createPiAdapter,
279
311
  };
@@ -2,12 +2,36 @@
2
2
 
3
3
  // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
4
 
5
+ const fs = require('node:fs');
5
6
  const path = require('node:path');
6
7
 
7
8
  /** Resolve a registry-declared source path against the repo root, refusing escapes. Shared by the
8
9
  * common projection module and by per-adapter native-projection normalizers, which is why it lives
9
- * in its own module rather than in index.js — a normalizer importing index.js would be a cycle. */
10
- function resolveRegistrySource(registry , source , label ) {
10
+ * in its own module rather than in index.js — a normalizer importing index.js would be a cycle.
11
+ * `sourceRoot` checks containment against that root using real paths, as required by IC-017.
12
+ * When omitted, the existing lexical repository-root check is preserved. */
13
+ function resolveRegistrySource(registry , source , label , sourceRoot ) {
14
+ if (sourceRoot !== undefined) {
15
+ if (typeof sourceRoot !== 'string' || !sourceRoot) {
16
+ throw new Error(`Registry ${label} sourceRoot must be a non-empty string`);
17
+ }
18
+ if (typeof source !== 'string' || !source) throw new Error(`Registry ${label} source is required`);
19
+ let realRoot ;
20
+ let realSource ;
21
+ try {
22
+ realRoot = fs.realpathSync(path.resolve(sourceRoot));
23
+ realSource = fs.realpathSync(path.resolve(sourceRoot, source));
24
+ } catch (error ) {
25
+ if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') {
26
+ throw new Error(`Registry ${label} source is missing under source root: ${source}`);
27
+ }
28
+ throw error;
29
+ }
30
+ if (!realSource.startsWith(`${realRoot}${path.sep}`)) {
31
+ throw new Error(`Registry ${label} source escapes source root: ${source}`);
32
+ }
33
+ return realSource;
34
+ }
11
35
  if (typeof registry.repoRoot !== 'string' || !registry.repoRoot) throw new Error(`Registry repoRoot is required for ${label}`);
12
36
  if (typeof source !== 'string' || !source) throw new Error(`Registry ${label} source is required`);
13
37
  const root = path.resolve(registry.repoRoot);
@@ -20,7 +20,7 @@ const { loadRegistry } = require('../../registry/index.js');
20
20
  const { storageAfterInstall } = require('../../runtime/storage/upkeep.js');
21
21
  const { applyLifecycle, applyMcpIndex, recordMcpOwnership } = require('../../lifecycle/index.js');
22
22
  const {
23
- codexScope, registryLifecycleView, printRegistryLifecycle, printPlanNotices, assertSafeRegistryPlan,
23
+ codexScope, registryLifecycleView, printRegistryLifecycle, printRetiredPlans, printPlanNotices, assertSafeRegistryPlan,
24
24
  lockDocument, recordLock,
25
25
  } = require('../../lifecycle/view.js');
26
26
  const {
@@ -55,7 +55,7 @@ function cmdInstall(o ) {
55
55
  // One lifecycle view across every requested target — computed unconditionally (not only under
56
56
  // --dry-run) so its safety gate and its plan are the exact same object the real apply below uses.
57
57
  const lifecycleView = registryLifecycleView({ registry, repoRoot: REPO_ROOT, scope, dirs, targets,
58
- mcpSelections: selection.selections, mcpAdoptable: selection.adoptable, retainedMcpIds: selection.retainedMcpIds,
58
+ mcpSelections: selection.selections, mcpAdoptable: selection.adoptable, retainedMcpIds: selection.retainedMcpIds, operation: 'install',
59
59
  force: o.force, adopt: o.adopt === true, permissions: o.permissions === true, statusline: o.statusline === true });
60
60
  if (!lifecycleView.plan.safe) { assertSafeRegistryPlan(lifecycleView); return; }
61
61
 
@@ -120,6 +120,7 @@ function cmdInstall(o ) {
120
120
  const owned = result.ledger.resources.filter((resource ) => resource.harness === target.harness).length;
121
121
  console.log(`[INFO] ${target.harness}: lifecycle verified (${owned} owned resource(s))`);
122
122
  }
123
+ printRetiredPlans(lifecycleView);
123
124
  ledgerAfter = result.ledger;
124
125
  } else {
125
126
  // MCP_INDEX.md is generated, not a tracked resource, so it never appears in plan.changes and