@iowarp/clio-coder 0.3.6 → 0.3.8

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 (323) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +22 -6
  3. package/dist/{acp-2BEHC4DL.js → acp-U67UHUK2.js} +14 -14
  4. package/dist/{agents-LNNFTM53.js → agents-YU6SGALZ.js} +42 -35
  5. package/dist/assets/codewiki.json +1 -1
  6. package/dist/{auth-KXXFI2VS.js → auth-5ZPJOIVG.js} +32 -24
  7. package/dist/builtins-C6JMZVV6.js +17 -0
  8. package/dist/{chunk-E25LMLRW.js → chunk-26LEYJZH.js} +2 -2
  9. package/dist/{chunk-TSHXZTOQ.js → chunk-2HEJ2F35.js} +5 -5
  10. package/dist/{chunk-PBTHKCPN.js → chunk-2HFZQUHL.js} +7 -7
  11. package/dist/{chunk-FO5ZOVUY.js → chunk-3BINW3FP.js} +5 -5
  12. package/dist/{chunk-4OC57DA6.js → chunk-4DGYLA73.js} +53 -2
  13. package/dist/{chunk-CKXWIANG.js → chunk-4SPRNWDE.js} +18 -16
  14. package/dist/{chunk-E2ER4LJF.js → chunk-5C3AQNDW.js} +25 -1
  15. package/dist/{chunk-XF5N4U5A.js → chunk-5DHKRSMQ.js} +9 -8
  16. package/dist/{chunk-43AOLP7E.js → chunk-5FR74PWO.js} +2 -1
  17. package/dist/{chunk-EKY57CSP.js → chunk-5H3GB5BO.js} +68 -772
  18. package/dist/{chunk-6XXKFVSN.js → chunk-5Q2VVUKB.js} +4 -4
  19. package/dist/{chunk-DJVECN66.js → chunk-7RFXX52T.js} +295 -3705
  20. package/dist/{chunk-ZXF4XRKW.js → chunk-7RGZWPB6.js} +158 -8
  21. package/dist/{chunk-FYYLNIL5.js → chunk-A2NJGIB3.js} +2 -2
  22. package/dist/{chunk-XXQNGV4M.js → chunk-A3WNZD3P.js} +548 -72
  23. package/dist/{chunk-VEZEGCGW.js → chunk-B5CSFE7B.js} +23 -21
  24. package/dist/{chunk-2SFS6XQE.js → chunk-DGSYXYMX.js} +3 -2
  25. package/dist/chunk-DR52UMZW.js +21 -0
  26. package/dist/{chunk-ZWLZP4ZT.js → chunk-DYIM5TJT.js} +98 -12
  27. package/dist/{chunk-QKMUKYO7.js → chunk-E77JEWSD.js} +270 -125
  28. package/dist/{chunk-LYF7OHWH.js → chunk-EMYUUSFG.js} +12 -467
  29. package/dist/{chunk-4VP4KH3K.js → chunk-EQ63NRB7.js} +8 -8
  30. package/dist/chunk-FBVTI2TJ.js +518 -0
  31. package/dist/{chunk-R46L2BIR.js → chunk-FHJEP5SW.js} +19 -25
  32. package/dist/{chunk-WR67VIZY.js → chunk-GN57SG4G.js} +66 -8
  33. package/dist/{chunk-RD5U66HV.js → chunk-GPIEI3LY.js} +9 -9
  34. package/dist/{chunk-PCZJO5TI.js → chunk-GU2UIAFZ.js} +13 -178
  35. package/dist/chunk-GWS3VEIW.js +195 -0
  36. package/dist/chunk-H7IXIC72.js +103 -0
  37. package/dist/{chunk-3BPUFZDL.js → chunk-HLE42MG7.js} +3 -3
  38. package/dist/{chunk-24I7BN55.js → chunk-IFBNV6H6.js} +3 -3
  39. package/dist/{chunk-QNQHSOLF.js → chunk-IGWKHNIQ.js} +114 -55
  40. package/dist/chunk-IIZWH4XA.js +172 -0
  41. package/dist/{chunk-AD2SYQYC.js → chunk-IJ7RPIYJ.js} +124 -6
  42. package/dist/chunk-J3YUBZWY.js +382 -0
  43. package/dist/{chunk-5JGRAMKL.js → chunk-JOZYP4GM.js} +8 -6
  44. package/dist/{chunk-OH3TOQTB.js → chunk-K4XHGFR5.js} +751 -15
  45. package/dist/{chunk-EYPA3EGJ.js → chunk-KTYTFRMB.js} +178 -14
  46. package/dist/chunk-LU7P4LHA.js +33 -0
  47. package/dist/chunk-ME6CCNFO.js +108 -0
  48. package/dist/chunk-MXKJU4JB.js +1100 -0
  49. package/dist/{chunk-RY3LY4J5.js → chunk-N22QMJKY.js} +21 -18
  50. package/dist/chunk-NMPKI6XL.js +3006 -0
  51. package/dist/chunk-NUGM5KR6.js +165 -0
  52. package/dist/{chunk-22NAGB7X.js → chunk-P43ETTHK.js} +5 -94
  53. package/dist/{chunk-NILBFAPG.js → chunk-PMDBGQSJ.js} +2 -2
  54. package/dist/chunk-PT7HYKEM.js +165 -0
  55. package/dist/chunk-RVG5JXAL.js +41 -0
  56. package/dist/{chunk-GEYXPTRF.js → chunk-RWSI4YD7.js} +2 -2
  57. package/dist/{chunk-4BPJXDWC.js → chunk-TANS5ZJS.js} +35 -19
  58. package/dist/{verifiers-NCBTHHN2.js → chunk-TB5666IT.js} +67 -324
  59. package/dist/{chunk-QM3F2GKX.js → chunk-TLQJPP24.js} +7927 -8088
  60. package/dist/{chunk-G7MUEIGA.js → chunk-TT36MB5S.js} +2 -1
  61. package/dist/chunk-TTHACPOM.js +961 -0
  62. package/dist/{chunk-6US73PDB.js → chunk-TYPGUK6W.js} +7 -7
  63. package/dist/{chunk-MFFY33HR.js → chunk-U6MBIEMB.js} +554 -209
  64. package/dist/chunk-VAWNZU7Z.js +242 -0
  65. package/dist/{chunk-IR4CFBFN.js → chunk-VCBR6CU7.js} +12 -12
  66. package/dist/{chunk-WHJYKASB.js → chunk-VHN4MY6O.js} +2 -2
  67. package/dist/{chunk-XYDYPRZI.js → chunk-VWZOAB7K.js} +10 -10
  68. package/dist/{chunk-ZRGEBJ4T.js → chunk-WLFILSD5.js} +48 -48
  69. package/dist/{chunk-K7T3E2SR.js → chunk-WNIJTQQK.js} +12 -11
  70. package/dist/{chunk-CYQKWTG3.js → chunk-WSB3FPX7.js} +68 -20
  71. package/dist/{chunk-KHSFENX2.js → chunk-WWCZ5F23.js} +116 -14
  72. package/dist/{chunk-XE2VEJHX.js → chunk-WXY7KU3G.js} +2 -2
  73. package/dist/{chunk-PPAMZ32Z.js → chunk-XK56QHLX.js} +6 -1
  74. package/dist/{chunk-KOHPCX4K.js → chunk-XWSF374K.js} +5 -5
  75. package/dist/{chunk-CJUB2JJ2.js → chunk-YS5VLNH5.js} +10 -10
  76. package/dist/{chunk-ZZMN5OM4.js → chunk-ZNLWCMVZ.js} +2 -2
  77. package/dist/{chunk-WHGPSPT5.js → chunk-ZVJ5BLO2.js} +2 -2
  78. package/dist/cli/index.js +33 -31
  79. package/dist/{clio-M2KGYUFZ.js → clio-QVTYJ57A.js} +10 -10
  80. package/dist/{code-nav-GQNL7XA6.js → code-nav-FGGFIE7L.js} +5 -5
  81. package/dist/codewiki/build-worker.js +4 -4
  82. package/dist/{components-5TTYYX6G.js → components-ZFA3SAER.js} +9 -9
  83. package/dist/{config-XUUYQIWO.js → config-LW5IJFQN.js} +65 -55
  84. package/dist/{configure-IHJ7YOMV.js → configure-7XIZCOU4.js} +29 -24
  85. package/dist/{context-75MIWW3U.js → context-L3WL3X7K.js} +54 -43
  86. package/dist/{context-ZQ7SIFJV.js → context-N52ZA626.js} +30 -14
  87. package/dist/{context-74JLXAWD.js → context-Y6Y7QPR6.js} +12 -12
  88. package/dist/{context-clear-GYKWNUML.js → context-clear-MBQRLSDQ.js} +54 -43
  89. package/dist/{context-index-SSR5ECNE.js → context-index-HVMFQHK3.js} +5 -5
  90. package/dist/{context-working-set-UX5KEP4J.js → context-working-set-GS6DSO7F.js} +18 -18
  91. package/dist/{dispatch-runner-GIJBHNFL.js → dispatch-runner-22ZCNOM3.js} +367 -79
  92. package/dist/{docs-6FZSCG5B.js → docs-7LQ23DLM.js} +9 -9
  93. package/dist/{doctor-SVJ5BZCW.js → doctor-M7YEDGAE.js} +27 -23
  94. package/dist/{eval-CG6LLBLD.js → eval-BEC2WHDA.js} +73 -20
  95. package/dist/{evidence-ZYFIEN42.js → evidence-REJUMSKM.js} +70 -59
  96. package/dist/{evolve-QGEXEMDW.js → evolve-PY5ZBA5K.js} +50 -39
  97. package/dist/{extensions-ADGNCJJD.js → extensions-HVKU65YU.js} +7 -7
  98. package/dist/{fleet-S5R4ZOQY.js → fleet-7WZEWRFA.js} +236 -377
  99. package/dist/fleet-commands-UVHWM76J.js +70 -0
  100. package/dist/fleet-graph-6ULH7PES.js +125 -0
  101. package/dist/fleet-new-RDVJLHHH.js +48 -0
  102. package/dist/{fleet-preflight-BHSNPBMH.js → fleet-preflight-J53T6CCE.js} +5 -5
  103. package/dist/fleet-validate-72PC4SLA.js +79 -0
  104. package/dist/{init-5DRU55YR.js → init-OG3TPGQG.js} +69 -57
  105. package/dist/library-CNTMPLRF.js +217 -0
  106. package/dist/{memory-7YKKR6UC.js → memory-6IS7F275.js} +52 -41
  107. package/dist/{models-ZPOLRU2C.js → models-ENRJDA5W.js} +37 -31
  108. package/dist/{monitor-US5F5YGZ.js → monitor-XLDVO7TN.js} +79 -42
  109. package/dist/{orchestrator-E2AL4T5N.js → orchestrator-6KSPYRHA.js} +4126 -762
  110. package/dist/{paths-E7KYAQWE.js → paths-DBXMZMDU.js} +6 -6
  111. package/dist/registry-LG64LTF4.js +11 -0
  112. package/dist/{reset-KZ652EK6.js → reset-RZ4ER727.js} +12 -12
  113. package/dist/{run-SRNBKDWD.js → run-Y2CNK5RU.js} +89 -76
  114. package/dist/{share-CGZE33UP.js → share-A55GYP6Z.js} +37 -13
  115. package/dist/{skills-S2X4DLY5.js → skills-ALC5J6AT.js} +29 -13
  116. package/dist/{skills-eval-W2GGIC4R.js → skills-eval-JPBEBYQU.js} +60 -48
  117. package/dist/support-MIETYA5E.js +38 -0
  118. package/dist/{targets-54SWINWB.js → targets-VGNXIR3S.js} +46 -36
  119. package/dist/{terminal-lease-SAIF2OGY.js → terminal-lease-WOBR64YA.js} +4 -4
  120. package/dist/{uninstall-BVLWXKBT.js → uninstall-ZJF5H5ZN.js} +9 -9
  121. package/dist/{upgrade-JKAR27XC.js → upgrade-FUSUAGHR.js} +31 -28
  122. package/dist/{usage-MSAWCLX4.js → usage-N4MKVHKD.js} +140 -68
  123. package/dist/verifiers-YAWOJ3H2.js +336 -0
  124. package/dist/{verify-X5HDROLA.js → verify-LTDHYBGY.js} +10 -9
  125. package/dist/{wiki-generate-GUSOQ6ZP.js → wiki-generate-6M7GHTBJ.js} +75 -62
  126. package/dist/worker/entry.js +67 -57
  127. package/docs/README.md +3 -2
  128. package/docs/acp.md +1 -1
  129. package/docs/alcf-provider.md +1 -1
  130. package/docs/architecture.md +2 -2
  131. package/docs/artifact-versions.md +11 -6
  132. package/docs/built-in-agents.md +26 -2
  133. package/docs/capacity-and-scheduling.md +1 -1
  134. package/docs/commands-and-modes.md +83 -3
  135. package/docs/configuration-and-targets.md +86 -4
  136. package/docs/context-engine.md +1 -1
  137. package/docs/development-pipeline.md +1 -1
  138. package/docs/dispatch-architecture-rationale.md +1 -1
  139. package/docs/documentation-coverage.md +3 -3
  140. package/docs/documentation-guide.md +3 -3
  141. package/docs/eval-runner.md +1 -1
  142. package/docs/evals-internal.md +1 -1
  143. package/docs/evidence-and-memory.md +74 -10
  144. package/docs/evolution.md +1 -1
  145. package/docs/exit-codes-and-output.md +4 -1
  146. package/docs/extensions-and-sharing.md +6 -2
  147. package/docs/fleet-demo-runbook.md +2 -2
  148. package/docs/fleet-dispatch.md +228 -16
  149. package/docs/git-commit-provenance.md +2 -2
  150. package/docs/glossary.md +22 -2
  151. package/docs/installation-and-lifecycle.md +2 -2
  152. package/docs/middleware-and-components.md +2 -1
  153. package/docs/model-catalog.md +1 -1
  154. package/docs/observability.md +56 -9
  155. package/docs/proactive-memory.md +1 -1
  156. package/docs/prompt-envelope-and-tools.md +4 -2
  157. package/docs/provider-adapter-cookbook.md +1 -1
  158. package/docs/release-cut-checklist.md +83 -65
  159. package/docs/resource-library.md +59 -0
  160. package/docs/safety-model.md +2 -2
  161. package/docs/scientific-validation.md +3 -3
  162. package/docs/session-lifecycle.md +37 -1
  163. package/docs/skills-marketplace.md +16 -3
  164. package/docs/tool-usage.md +14 -7
  165. package/docs/trace-store.md +1 -1
  166. package/docs/troubleshooting.md +1 -1
  167. package/docs/tui-design.md +1 -1
  168. package/docs/worker-dispatch-mechanics.md +3 -3
  169. package/package.json +1 -2
  170. package/src/cli/argv.ts +5 -0
  171. package/src/cli/configure.ts +107 -23
  172. package/src/cli/doctor.ts +5 -1
  173. package/src/cli/evidence.ts +30 -25
  174. package/src/cli/fleet-commands.ts +37 -0
  175. package/src/cli/fleet-graph.ts +102 -0
  176. package/src/cli/fleet-new.ts +36 -0
  177. package/src/cli/fleet-preflight.ts +111 -0
  178. package/src/cli/fleet-validate.ts +30 -0
  179. package/src/cli/fleet.ts +173 -335
  180. package/src/cli/index.ts +3 -1
  181. package/src/cli/library.ts +190 -0
  182. package/src/cli/share.ts +13 -1
  183. package/src/cli/shared.ts +1 -0
  184. package/src/cli/targets.ts +4 -1
  185. package/src/cli/usage.ts +111 -19
  186. package/src/cli/validate-model.ts +60 -5
  187. package/src/core/bus-events.ts +29 -0
  188. package/src/core/commit-attribution.ts +4 -4
  189. package/src/core/config.ts +130 -0
  190. package/src/core/defaults.ts +81 -0
  191. package/src/core/path-boundary.ts +100 -0
  192. package/src/domains/agents/builtins/architect.md +1 -0
  193. package/src/domains/agents/builtins/oracle.md +33 -0
  194. package/src/domains/agents/catalog.ts +13 -1
  195. package/src/domains/agents/extension.ts +2 -11
  196. package/src/domains/agents/fleet-contract.ts +304 -24
  197. package/src/domains/agents/index.ts +14 -0
  198. package/src/domains/agents/recipe.ts +7 -1
  199. package/src/domains/agents/registry.ts +73 -5
  200. package/src/domains/agents/result-contract.ts +360 -15
  201. package/src/domains/agents/write-boundary.ts +15 -50
  202. package/src/domains/config/classify.ts +4 -0
  203. package/src/domains/context/project-rules.ts +51 -1
  204. package/src/domains/dispatch/active-route-planner.ts +14 -0
  205. package/src/domains/dispatch/assignment-reconcile.ts +22 -5
  206. package/src/domains/dispatch/assignment-store.ts +151 -14
  207. package/src/domains/dispatch/backoff.ts +2 -1
  208. package/src/domains/dispatch/capability-match.ts +1 -0
  209. package/src/domains/dispatch/checkout-writer-lease.ts +175 -0
  210. package/src/domains/dispatch/contract.ts +48 -0
  211. package/src/domains/dispatch/delegation-plan.ts +164 -0
  212. package/src/domains/dispatch/execution-plan.ts +76 -5
  213. package/src/domains/dispatch/execution-role.ts +12 -2
  214. package/src/domains/dispatch/execution-scheduler.ts +183 -67
  215. package/src/domains/dispatch/extension.ts +412 -74
  216. package/src/domains/dispatch/fleet-gate.ts +14 -0
  217. package/src/domains/dispatch/fleet-plan.ts +63 -3
  218. package/src/domains/dispatch/fleet-run.ts +791 -0
  219. package/src/domains/dispatch/gate-role-prompts.ts +47 -0
  220. package/src/domains/dispatch/host-verification.ts +178 -0
  221. package/src/domains/dispatch/index.ts +41 -1
  222. package/src/domains/dispatch/intent-requirements.ts +40 -0
  223. package/src/domains/dispatch/intent.ts +235 -0
  224. package/src/domains/dispatch/path-scope.ts +370 -0
  225. package/src/domains/dispatch/receipt-integrity.ts +9 -4
  226. package/src/domains/dispatch/state.ts +36 -3
  227. package/src/domains/dispatch/types.ts +64 -12
  228. package/src/domains/dispatch/validation.ts +69 -6
  229. package/src/domains/dispatch/write-boundary-enforcer.ts +45 -0
  230. package/src/domains/dispatch/write-boundary.ts +201 -22
  231. package/src/domains/eval/metrics/evidence.ts +79 -2
  232. package/src/domains/eval/runners/clio-run.ts +12 -2
  233. package/src/domains/evidence/build.ts +69 -11
  234. package/src/domains/evidence/index.ts +21 -0
  235. package/src/domains/evidence/provenance.ts +46 -11
  236. package/src/domains/evidence/trust-projection.ts +274 -0
  237. package/src/domains/evidence/trust-status.ts +155 -18
  238. package/src/domains/evidence/types.ts +4 -0
  239. package/src/domains/extensions/discovery.ts +88 -1
  240. package/src/domains/extensions/resources.ts +20 -8
  241. package/src/domains/extensions/state.ts +6 -2
  242. package/src/domains/extensions/types.ts +4 -1
  243. package/src/domains/lifecycle/doctor.ts +140 -1
  244. package/src/domains/middleware/index.ts +15 -0
  245. package/src/domains/middleware/watchdog.ts +281 -0
  246. package/src/domains/observability/contract.ts +3 -1
  247. package/src/domains/observability/cost.ts +12 -1
  248. package/src/domains/observability/extension.ts +2 -2
  249. package/src/domains/observability/index.ts +10 -0
  250. package/src/domains/observability/out-of-turn-usage.ts +223 -0
  251. package/src/domains/prompts/contract.ts +3 -5
  252. package/src/domains/providers/extension.ts +30 -2
  253. package/src/domains/resources/common-loader.ts +3 -0
  254. package/src/domains/resources/index.ts +20 -0
  255. package/src/domains/resources/library.ts +326 -0
  256. package/src/domains/resources/prompts/loader.ts +119 -14
  257. package/src/domains/resources/skills/marketplace.ts +37 -12
  258. package/src/domains/safety/policy-engine.ts +5 -5
  259. package/src/domains/safety/run-effects.ts +64 -1
  260. package/src/domains/safety/scope.ts +7 -12
  261. package/src/domains/session/handoff.ts +629 -0
  262. package/src/domains/share/archive.ts +67 -2
  263. package/src/engine/acp/server.ts +4 -1
  264. package/src/engine/prompt-templates.ts +18 -1
  265. package/src/engine/worker-runtime.ts +6 -3
  266. package/src/entry/orchestrator.ts +37 -0
  267. package/src/interactive/bus-notices.ts +26 -0
  268. package/src/interactive/chat-loop.ts +235 -1
  269. package/src/interactive/chat-renderer.ts +22 -0
  270. package/src/interactive/cost-overlay.ts +31 -3
  271. package/src/interactive/council-dispatch.ts +30 -0
  272. package/src/interactive/council-grid.ts +213 -0
  273. package/src/interactive/council.ts +99 -0
  274. package/src/interactive/dispatch-board.ts +311 -17
  275. package/src/interactive/fleet-run-preview.ts +307 -0
  276. package/src/interactive/footer/notifications.ts +219 -0
  277. package/src/interactive/handoff-round.ts +56 -0
  278. package/src/interactive/interactive-application.ts +43 -1
  279. package/src/interactive/interactive-event-projection.ts +23 -1
  280. package/src/interactive/interactive-slash-runtime.ts +52 -2
  281. package/src/interactive/interactive-subscriptions.ts +14 -2
  282. package/src/interactive/oracle.ts +179 -0
  283. package/src/interactive/overlay-ask-user-lifecycle.ts +6 -0
  284. package/src/interactive/overlay-general-openers.ts +190 -1
  285. package/src/interactive/overlay-key-routing.ts +17 -1
  286. package/src/interactive/overlay-lifecycle.ts +41 -1
  287. package/src/interactive/overlay-permission-lifecycle.ts +10 -0
  288. package/src/interactive/overlay-resource-openers.ts +11 -3
  289. package/src/interactive/overlay-session-lifecycle.ts +234 -2
  290. package/src/interactive/overlays/fleet-run-approval.ts +208 -0
  291. package/src/interactive/overlays/handoff-review.ts +185 -0
  292. package/src/interactive/overlays/library-install-confirm.ts +151 -0
  293. package/src/interactive/overlays/list-overlay.ts +168 -2
  294. package/src/interactive/overlays/settings.ts +221 -30
  295. package/src/interactive/overlays/side-question.ts +139 -0
  296. package/src/interactive/overlays/skills-hub.ts +401 -15
  297. package/src/interactive/side-question.ts +171 -0
  298. package/src/interactive/slash-commands.ts +439 -7
  299. package/src/interactive/slash-spec.ts +19 -6
  300. package/src/interactive/theme/tokens.ts +30 -0
  301. package/src/interactive/turn-middleware.ts +15 -1
  302. package/src/interactive/view/artifacts.ts +42 -9
  303. package/src/interactive/view/view-overlay.ts +15 -3
  304. package/src/interactive/watchdog-run.ts +75 -0
  305. package/src/interactive/worker-receipts.ts +14 -2
  306. package/src/interactive/worker-share.ts +56 -1
  307. package/src/interactive/worker-stream.ts +15 -0
  308. package/src/tools/bootstrap.ts +3 -0
  309. package/src/tools/compete-worktrees.ts +13 -79
  310. package/src/tools/dispatch-admission.ts +251 -18
  311. package/src/tools/dispatch-arguments.ts +84 -1
  312. package/src/tools/dispatch-plan.ts +165 -6
  313. package/src/tools/dispatch-runner.ts +364 -23
  314. package/src/tools/dispatch-types.ts +20 -1
  315. package/src/tools/dispatch.ts +72 -2
  316. package/src/tools/monitor.ts +29 -0
  317. package/src/tools/profiles.ts +18 -4
  318. package/src/tools/task-worktree.ts +238 -0
  319. package/src/tools/verify/authoring.ts +61 -1
  320. package/src/tools/verify/scripts.ts +62 -0
  321. package/src/tools/worker-evidence.ts +21 -14
  322. package/src/worker/spec-contract.ts +3 -1
  323. package/dist/chunk-HC4CLZ2Y.js +0 -68
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import { lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import path from "node:path";
4
4
  import { parseCommandArgs, substituteArgs } from "../../../engine/prompt-templates.js";
@@ -13,7 +13,6 @@ import {
13
13
  import {
14
14
  COMPAT_RESOURCE_PRECEDENCE,
15
15
  defaultScopedResourceRoots,
16
- readRootEntries,
17
16
  sourceInfoForRoot,
18
17
  splitYamlFrontmatter,
19
18
  stringField,
@@ -33,6 +32,8 @@ export interface PromptTemplate {
33
32
 
34
33
  export interface PromptTemplateRoot {
35
34
  path: string;
35
+ /** Present only for an installed extension resource root. */
36
+ rootPath?: string;
36
37
  scope: ResourceScope;
37
38
  source?: string;
38
39
  precedence?: number;
@@ -139,6 +140,61 @@ function fallbackDescription(body: string): string {
139
140
  return normalized.length > 60 ? `${normalized.slice(0, 57)}...` : normalized;
140
141
  }
141
142
 
143
+ // Keep the suffix deliberately conservative. Prompt prose commonly places a
144
+ // comma, parenthesis, or full stop immediately after an @path; treating that
145
+ // punctuation as part of the filesystem reference makes a valid template look
146
+ // unresolved. Extension bundles use portable path components, so these are the
147
+ // only characters needed in a reference while still allowing `..` to be
148
+ // detected and rejected by the containment check below.
149
+ const EXTENSION_ROOT_REFERENCE = /\$\{extensionRoot\}(\/[A-Za-z0-9._~%+@/-]*)?/g;
150
+ const EXTENSION_ROOT_TOKEN = "$" + "{extensionRoot}";
151
+
152
+ function contained(root: string, candidate: string): boolean {
153
+ const relative = path.relative(root, candidate);
154
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
155
+ }
156
+
157
+ /** Resolve only existing paths contained by the declaring extension package. */
158
+ function resolveExtensionReferences(
159
+ body: string,
160
+ root: PromptTemplateRoot,
161
+ filePath: string,
162
+ diagnostics: ResourceDiagnostic[],
163
+ ): string | null {
164
+ if (root.rootPath === undefined || !body.includes(EXTENSION_ROOT_TOKEN)) return body;
165
+ let canonicalRoot: string;
166
+ try {
167
+ canonicalRoot = realpathSync(root.rootPath);
168
+ } catch (error) {
169
+ diagnostics.push({
170
+ type: "warning",
171
+ message: `extension root could not be resolved: ${error instanceof Error ? error.message : String(error)}`,
172
+ path: filePath,
173
+ });
174
+ return null;
175
+ }
176
+ let invalid: string | null = null;
177
+ const resolved = body.replace(EXTENSION_ROOT_REFERENCE, (reference, suffix: string | undefined) => {
178
+ const candidate = path.resolve(canonicalRoot, `.${suffix ?? ""}`);
179
+ try {
180
+ const canonicalCandidate = realpathSync(candidate);
181
+ if (!contained(canonicalRoot, canonicalCandidate)) invalid = reference;
182
+ } catch {
183
+ invalid = reference;
184
+ }
185
+ return candidate;
186
+ });
187
+ if (invalid !== null) {
188
+ diagnostics.push({
189
+ type: "warning",
190
+ message: `prompt template has an unresolved or escaping extension reference: ${invalid}`,
191
+ path: filePath,
192
+ });
193
+ return null;
194
+ }
195
+ return resolved;
196
+ }
197
+
142
198
  function loadPromptFile(
143
199
  filePath: string,
144
200
  root: PromptTemplateRoot,
@@ -155,16 +211,28 @@ function loadPromptFile(
155
211
  return null;
156
212
  }
157
213
 
158
- const name = path.basename(filePath, ext);
214
+ const relativePath = path.relative(root.path, filePath);
215
+ if (
216
+ relativePath === "" ||
217
+ relativePath === ".." ||
218
+ relativePath.startsWith(`..${path.sep}`) ||
219
+ path.isAbsolute(relativePath)
220
+ ) {
221
+ diagnostics.push({ type: "warning", message: "prompt template escaped its discovery root", path: filePath });
222
+ return null;
223
+ }
224
+ const name = relativePath.slice(0, -ext.length).split(path.sep).join(":");
159
225
  if (name.trim().length === 0) return null;
160
226
  const { frontmatter, body } = splitOptionalFrontmatter(raw, filePath, diagnostics);
161
- const description = stringField(frontmatter, "description") ?? fallbackDescription(body);
227
+ const resolvedBody = resolveExtensionReferences(body, root, filePath, diagnostics);
228
+ if (resolvedBody === null) return null;
229
+ const description = stringField(frontmatter, "description") ?? fallbackDescription(resolvedBody);
162
230
  const argumentHint = stringField(frontmatter, "argument-hint") ?? stringField(frontmatter, "argumentHint");
163
231
  const sourceInfo: ResourceSourceInfo = sourceInfoForRoot(root, filePath);
164
232
  const template: PromptTemplate = {
165
233
  name,
166
234
  description,
167
- content: body.trim(),
235
+ content: resolvedBody.trim(),
168
236
  filePath,
169
237
  sourceInfo,
170
238
  trusted: root.trusted !== false,
@@ -178,10 +246,42 @@ function loadPromptRoot(
178
246
  diagnostics: ResourceDiagnostic[],
179
247
  ): ResourceCandidate<PromptTemplate>[] {
180
248
  const candidates: ResourceCandidate<PromptTemplate>[] = [];
181
- for (const entry of readRootEntries(root, "prompt template", diagnostics)) {
182
- if (!entry.isFile()) continue;
183
- const candidate = loadPromptFile(path.join(root.path, entry.name), root, diagnostics);
184
- if (candidate) candidates.push(candidate);
249
+ const pending = [root.path];
250
+ while (pending.length > 0) {
251
+ const directory = pending.pop();
252
+ if (!directory) continue;
253
+ let entries: import("node:fs").Dirent[];
254
+ try {
255
+ const stat = lstatSync(directory);
256
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
257
+ diagnostics.push({
258
+ type: "warning",
259
+ message: "prompt template root contains a non-directory or symbolic link",
260
+ path: directory,
261
+ });
262
+ continue;
263
+ }
264
+ entries = readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
265
+ } catch (error) {
266
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
267
+ diagnostics.push({
268
+ type: "warning",
269
+ message: `prompt template directory could not be read: ${error instanceof Error ? error.message : String(error)}`,
270
+ path: directory,
271
+ });
272
+ continue;
273
+ }
274
+ for (const entry of [...entries].reverse()) {
275
+ const filePath = path.join(directory, entry.name);
276
+ if (entry.isSymbolicLink()) {
277
+ diagnostics.push({ type: "warning", message: "prompt template symbolic link was ignored", path: filePath });
278
+ } else if (entry.isDirectory()) {
279
+ pending.push(filePath);
280
+ } else if (entry.isFile()) {
281
+ const candidate = loadPromptFile(filePath, root, diagnostics);
282
+ if (candidate) candidates.push(candidate);
283
+ }
284
+ }
185
285
  }
186
286
  return candidates;
187
287
  }
@@ -210,13 +310,18 @@ export function loadPromptTemplates(input: LoadPromptTemplatesInput = {}): Promp
210
310
  }
211
311
 
212
312
  function parsePromptCommand(input: string): { name: string; rest: string } | null {
213
- const trimmed = input.trim();
214
- if (!trimmed.startsWith("/")) return null;
215
- const withoutSlash = trimmed.slice(1);
313
+ const framed = input.trimStart();
314
+ if (!framed.startsWith("/")) return null;
315
+ const withoutSlash = framed.slice(1);
216
316
  const separator = withoutSlash.search(/\s/);
217
317
  const name = separator === -1 ? withoutSlash : withoutSlash.slice(0, separator);
218
318
  if (name.length === 0 || name.includes("/")) return null;
219
- const rest = separator === -1 ? "" : withoutSlash.slice(separator).trim();
319
+ // Leading whitespace before `/` and the first whitespace after the command
320
+ // are framing. A CRLF pair is one delimiter. Every subsequent byte belongs
321
+ // to the argument payload, including indentation, blank lines, and trailing
322
+ // whitespace, so raw `$ARGUMENTS` substitution can be exact at its boundary.
323
+ const delimiterWidth = separator !== -1 && withoutSlash.slice(separator, separator + 2) === "\r\n" ? 2 : 1;
324
+ const rest = separator === -1 ? "" : withoutSlash.slice(separator + delimiterWidth);
220
325
  return { name, rest };
221
326
  }
222
327
 
@@ -238,7 +343,7 @@ export function expandPromptTemplateInput(input: string, templates: PromptTempla
238
343
  const args = parseCommandArgs(command.rest);
239
344
  return {
240
345
  expanded: true,
241
- text: substituteArgs(template.content, args),
346
+ text: substituteArgs(template.content, args, command.rest),
242
347
  args,
243
348
  template,
244
349
  diagnostics: templates.diagnostics,
@@ -35,6 +35,8 @@ import { loadSkills, type Skill } from "./loader.js";
35
35
  */
36
36
 
37
37
  export type MarketplaceSkillOrigin = "catalog" | "index";
38
+ export type LibraryEntryKind = "skill" | "agent" | "prompt" | "fleet";
39
+ export type LibraryRequirementRef = `${LibraryEntryKind}:${string}`;
38
40
 
39
41
  /**
40
42
  * The diagnostic that means "nothing is wrong, nothing is configured". Callers
@@ -44,6 +46,7 @@ export type MarketplaceSkillOrigin = "catalog" | "index";
44
46
  export const MARKETPLACE_UNCONFIGURED = "no local skill marketplace catalog or index configured";
45
47
 
46
48
  export interface MarketplaceSkill {
49
+ kind: LibraryEntryKind;
47
50
  name: string;
48
51
  description: string;
49
52
  /** Local path or URL accepted by `clio-coder skills install`. */
@@ -53,6 +56,7 @@ export interface MarketplaceSkill {
53
56
  /** Catalog grouping ("git", "research", ...); absent in a flat catalog. */
54
57
  category?: string;
55
58
  origin: MarketplaceSkillOrigin;
59
+ requires?: LibraryRequirementRef[];
56
60
  }
57
61
 
58
62
  export type MarketplaceStatus = "installed" | "installable" | "unavailable";
@@ -131,7 +135,7 @@ function indexAudit(value: unknown): MarketplaceSkill["audit"] {
131
135
  return value === "pass" || value === "warn" || value === "fail" || value === "unknown" ? value : undefined;
132
136
  }
133
137
 
134
- function indexSkills(indexPath: string, diagnostics: string[]): MarketplaceSkill[] {
138
+ function parseMarketplaceIndex(indexPath: string, diagnostics: string[] = []): MarketplaceSkill[] {
135
139
  try {
136
140
  const parsed = JSON.parse(readFileSync(indexPath, "utf8")) as unknown;
137
141
  const rawSkills = Array.isArray(parsed)
@@ -139,7 +143,7 @@ function indexSkills(indexPath: string, diagnostics: string[]): MarketplaceSkill
139
143
  : parsed && typeof parsed === "object" && Array.isArray((parsed as { skills?: unknown }).skills)
140
144
  ? (parsed as { skills: unknown[] }).skills
141
145
  : [];
142
- return rawSkills.filter(isIndexSkill).map((skill) => {
146
+ return rawSkills.filter(isIndexSkill).flatMap((skill): MarketplaceSkill[] => {
143
147
  const record = skill as unknown as Record<string, unknown>;
144
148
  // Version, audit and category are published by `npm run skills:pin`.
145
149
  // They are advisory display metadata; an index entry stays installable
@@ -147,15 +151,35 @@ function indexSkills(indexPath: string, diagnostics: string[]): MarketplaceSkill
147
151
  const version = optionalString(record.version);
148
152
  const audit = indexAudit(record.audit);
149
153
  const category = optionalString(record.category);
150
- return {
151
- name: skill.name.trim(),
152
- description: skill.description.trim(),
153
- sourceUrl: skill.sourceUrl.trim(),
154
- ...(version ? { version } : {}),
155
- ...(audit ? { audit } : {}),
156
- ...(category ? { category } : {}),
157
- origin: "index" as const,
158
- };
154
+ if (record.kind !== undefined && !["skill", "agent", "prompt", "fleet"].includes(String(record.kind))) {
155
+ diagnostics.push(`skill marketplace index entry has unsupported kind: ${skill.name}`);
156
+ return [];
157
+ }
158
+ if (
159
+ record.requires !== undefined &&
160
+ (!Array.isArray(record.requires) || record.requires.some((entry) => typeof entry !== "string"))
161
+ ) {
162
+ diagnostics.push(`library_requirement_malformed: ${skill.name}`);
163
+ return [];
164
+ }
165
+ const kind: LibraryEntryKind =
166
+ record.kind === "agent" || record.kind === "prompt" || record.kind === "fleet" ? record.kind : "skill";
167
+ const requires = Array.isArray(record.requires)
168
+ ? record.requires.filter((entry): entry is LibraryRequirementRef => typeof entry === "string")
169
+ : undefined;
170
+ return [
171
+ {
172
+ kind,
173
+ name: skill.name.trim(),
174
+ description: skill.description.trim(),
175
+ sourceUrl: skill.sourceUrl.trim(),
176
+ ...(version ? { version } : {}),
177
+ ...(audit ? { audit } : {}),
178
+ ...(category ? { category } : {}),
179
+ ...(requires ? { requires } : {}),
180
+ origin: "index" as const,
181
+ },
182
+ ];
159
183
  });
160
184
  } catch (err) {
161
185
  diagnostics.push(`skill marketplace index unreadable: ${err instanceof Error ? err.message : String(err)}`);
@@ -226,6 +250,7 @@ function catalogEntry(skill: Skill, catalogDir: string): MarketplaceSkill {
226
250
  ...(skill.provenance?.audit ? { audit: skill.provenance.audit } : {}),
227
251
  ...(category ? { category } : {}),
228
252
  origin: "catalog",
253
+ kind: "skill",
229
254
  };
230
255
  }
231
256
 
@@ -256,7 +281,7 @@ export function discoverMarketplaceSkills(options: DiscoverMarketplaceOptions =
256
281
  options.indexPath === null ? null : (options.indexPath ?? process.env.CLIO_CODER_SKILL_MARKETPLACE_INDEX ?? null);
257
282
  const resolvedIndexPath = indexPath ?? defaultIndexPath();
258
283
  if (options.indexPath !== null && resolvedIndexPath && existsSync(resolvedIndexPath)) {
259
- for (const skill of indexSkills(resolvedIndexPath, diagnostics)) {
284
+ for (const skill of parseMarketplaceIndex(resolvedIndexPath, diagnostics)) {
260
285
  if (seen.has(skill.name)) continue;
261
286
  seen.add(skill.name);
262
287
  skills.push(skill);
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import path from "node:path";
3
3
  import { artifactDefaultPath } from "../../core/artifact-paths.js";
4
+ import { pathBoundaryCovers, resolvePathBoundary } from "../../core/path-boundary.js";
4
5
  import { canonicalizeExistingPath } from "../../core/path-canonical.js";
5
6
  import { ToolNames } from "../../core/tool-names.js";
6
7
  import { clioConfigDir } from "../../core/xdg.js";
@@ -89,7 +90,8 @@ export interface SafetyPolicyEngineOptions {
89
90
  rulePacks?: RulePacks;
90
91
  projectPolicy?: LoadedProjectSafetyPolicy;
91
92
  /**
92
- * Absolute directories a write-class tool call is confined to for this run.
93
+ * Absolute path boundaries a write-class tool call is confined to for this run.
94
+ * Exact files omit a trailing slash and subtrees retain one.
93
95
  * When present and non-empty, a write/edit target outside every root is a
94
96
  * final BLOCK (reason code "write-root"). Empty or absent disables the check.
95
97
  * Enforced at the worker safety seam so both the native worker registry and
@@ -182,9 +184,7 @@ function evaluateWriteRoots(roots: ReadonlyArray<string>, writeRootCwd: string,
182
184
  const target = writeRootTargetPath(call);
183
185
  if (target === null) return null;
184
186
  const resolved = path.resolve(writeRootCwd, target);
185
- for (const root of roots) {
186
- if (resolved === root || resolved.startsWith(`${root}${path.sep}`)) return null;
187
- }
187
+ if (pathBoundaryCovers(roots, resolved)) return null;
188
188
  return `write target '${target}' resolves to '${resolved}', which is outside the permitted write roots for this run: ${roots.join(", ")}`;
189
189
  }
190
190
 
@@ -193,7 +193,7 @@ export function createSafetyPolicyEngine(options: SafetyPolicyEngineOptions = {}
193
193
  // Write-root containment resolves lexically, so it keeps its own un-canonicalized
194
194
  // cwd and roots to compare like against like (the design mandates no symlink chasing).
195
195
  const writeRootCwd = path.resolve(options.cwd ?? process.cwd());
196
- const writeRoots = (options.writeRoots ?? []).map((root) => path.resolve(root));
196
+ const writeRoots = (options.writeRoots ?? []).map((root) => resolvePathBoundary(writeRootCwd, root));
197
197
  const packs = options.rulePacks ?? getCachedDefaultRulePacks();
198
198
  const projectPolicy = options.projectPolicy ?? loadProjectSafetyPolicy(cwd);
199
199
  const projectPolicyRoot =
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { ToolNames } from "../../core/tool-names.js";
3
+ import { classify } from "./action-classifier.js";
3
4
  import { typedValidationSummary } from "./finish-contract.js";
4
5
  import {
5
6
  detectValidationCommand,
@@ -25,6 +26,13 @@ import {
25
26
  * bash command that writes through a script) leaves the path absent from the
26
27
  * set and the validator falls back to the filesystem rather than to a failed
27
28
  * conformance.
29
+ *
30
+ * `writeRecordComplete` is the other half of that sentence, and it exists for
31
+ * the reader that cannot fall back to the filesystem. The write boundary uses
32
+ * this record the other way round, to decide that a path the run did *not*
33
+ * write is somebody else's and must survive. Reading an under-reporting set as
34
+ * a closed list would clear a step of a write it really made, so the recorder
35
+ * says outright whether the set is closed.
28
36
  */
29
37
  export interface RunEffects {
30
38
  /** Absolute paths the run's successful tool calls aimed a mutation at. */
@@ -56,6 +64,18 @@ export interface RunEffects {
56
64
  * wider set.
57
65
  */
58
66
  verificationCommands: ReadonlySet<string>;
67
+ /**
68
+ * False once the run made a successful call to a tool that can mutate a path
69
+ * its own arguments do not name. After such a call `mutatedPaths` is a lower
70
+ * bound rather than the whole list, so a reader that would treat an absent
71
+ * path as proof the run did not write it has to stop treating it that way.
72
+ *
73
+ * The downgrade covers the whole run rather than the one call, because an
74
+ * opaque call's effect is unbounded. A shell command, a project script, or a
75
+ * spawned worker can write anywhere the process can reach, so once one has
76
+ * run there is no path left in the checkout that the record can clear.
77
+ */
78
+ writeRecordComplete: boolean;
59
79
  }
60
80
 
61
81
  export interface RunEffectsRecorder {
@@ -70,6 +90,39 @@ interface PendingEffects {
70
90
  paths: ReadonlyArray<string>;
71
91
  validationCommand: string | null;
72
92
  verificationCommand: string | null;
93
+ opaque: boolean;
94
+ }
95
+
96
+ /**
97
+ * Action classes whose successful calls write only paths their own arguments
98
+ * name. `read` covers the observe, retrieve, interact, and orchestrate-bookkeeping
99
+ * tools, none of which touch a source-workspace file: `git` is a closed
100
+ * status/diff/log surface, and `tasks`, `ledger`, and `context` write Clio's own
101
+ * state. `write` covers `write`, `edit`, and `artifact`, whose target
102
+ * `toolMutationPaths` resolves from the call itself, including the artifact
103
+ * tool's default path.
104
+ */
105
+ const ENUMERABLE_ACTION_CLASSES: ReadonlySet<string> = new Set(["read", "write"]);
106
+
107
+ /**
108
+ * Whether a successful call to this tool can mutate a path its own recorded
109
+ * arguments do not name.
110
+ *
111
+ * Asked of the classifier rather than of a hand-written list, because
112
+ * `src/tools/policy.ts` asserts at bootstrap that every registered tool's
113
+ * `baseActionClass` equals what this classifier returns and what the plane
114
+ * table declares. A tool added to the execute or orchestrate-dispatch plane is
115
+ * therefore opaque the day it is registered, with no list to remember to update.
116
+ *
117
+ * The classifier is asked about the tool name alone. Bash's argument scan can
118
+ * escalate a single call to `system_modify` or `git_destructive`, and those are
119
+ * still bash; the question here is what the tool can do, not what one call was
120
+ * caught doing. An unregistered name (a dynamic or MCP tool) classifies as
121
+ * `unknown` and is opaque, which is the fail-closed answer for a tool whose
122
+ * arguments this process has no schema for.
123
+ */
124
+ export function toolWritesOpaquely(toolName: string): boolean {
125
+ return !ENUMERABLE_ACTION_CLASSES.has(classify({ tool: toolName }).actionClass);
73
126
  }
74
127
 
75
128
  /**
@@ -157,18 +210,23 @@ export function createRunEffectsRecorder(cwd: string): RunEffectsRecorder {
157
210
  const failedMutationPaths = new Set<string>();
158
211
  const validationCommands = new Set<string>();
159
212
  const verificationCommands = new Set<string>();
213
+ let opaqueWriteObserved = false;
160
214
  return {
161
215
  start(toolCallId, toolName, args) {
162
216
  const paths = mutationTargets(toolName, args);
163
217
  const validationCommand = validationCommandOf(toolName, args, "finish-contract");
164
218
  const verificationCommand = validationCommandOf(toolName, args, "grounding");
165
- if (paths.length === 0 && validationCommand === null && verificationCommand === null) return;
219
+ const opaque = toolWritesOpaquely(toolName);
220
+ // An opaque call is tracked even when it exposed no path and no
221
+ // command, because the fact that it ran at all is the finding.
222
+ if (!opaque && paths.length === 0 && validationCommand === null && verificationCommand === null) return;
166
223
  // A bash call may re-base its relative paths with its own cwd argument.
167
224
  const base = typeof args?.cwd === "string" && args.cwd.length > 0 ? path.resolve(cwd, args.cwd) : cwd;
168
225
  pending.set(toolCallId, {
169
226
  paths: paths.map((target) => path.resolve(base, target)),
170
227
  validationCommand,
171
228
  verificationCommand,
229
+ opaque,
172
230
  });
173
231
  },
174
232
  finish(toolCallId, failed) {
@@ -179,6 +237,10 @@ export function createRunEffectsRecorder(cwd: string): RunEffectsRecorder {
179
237
  for (const target of effects.paths) failedMutationPaths.add(target);
180
238
  return;
181
239
  }
240
+ // Success-gated on purpose. A shell command the safety policy blocked,
241
+ // or one that came back an error, never reached the filesystem, so it
242
+ // leaves the record closed.
243
+ if (effects.opaque) opaqueWriteObserved = true;
182
244
  for (const target of effects.paths) mutatedPaths.add(target);
183
245
  if (effects.validationCommand !== null) validationCommands.add(effects.validationCommand);
184
246
  if (effects.verificationCommand !== null) verificationCommands.add(effects.verificationCommand);
@@ -191,6 +253,7 @@ export function createRunEffectsRecorder(cwd: string): RunEffectsRecorder {
191
253
  failedMutationPaths: new Set([...failedMutationPaths].filter((target) => !mutatedPaths.has(target))),
192
254
  validationCommands: new Set(validationCommands),
193
255
  verificationCommands: new Set(verificationCommands),
256
+ writeRecordComplete: !opaqueWriteObserved,
194
257
  };
195
258
  },
196
259
  };
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { asDirectoryPathBoundary, pathBoundaryEntryCovers, resolvePathBoundary } from "../../core/path-boundary.js";
2
3
  import type { ActionClass } from "./action-classifier.js";
3
4
 
4
5
  /**
@@ -15,15 +16,7 @@ export interface ScopeSpec {
15
16
  }
16
17
 
17
18
  function normalizeRoot(root: string): string {
18
- return path.resolve(root);
19
- }
20
-
21
- function isUnder(child: string, parent: string): boolean {
22
- const c = normalizeRoot(child);
23
- const p = normalizeRoot(parent);
24
- if (c === p) return true;
25
- const rel = path.relative(p, c);
26
- return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
19
+ return resolvePathBoundary(process.cwd(), root);
27
20
  }
28
21
 
29
22
  export function isSubset(worker: ScopeSpec, orchestrator: ScopeSpec): boolean {
@@ -31,7 +24,9 @@ export function isSubset(worker: ScopeSpec, orchestrator: ScopeSpec): boolean {
31
24
  if (!orchestrator.allowedActions.has(action)) return false;
32
25
  }
33
26
  for (const root of worker.allowedWriteRoots) {
34
- const covered = orchestrator.allowedWriteRoots.some((outer) => isUnder(root, outer));
27
+ const covered = orchestrator.allowedWriteRoots.some((outer) =>
28
+ pathBoundaryEntryCovers(normalizeRoot(outer), normalizeRoot(root)),
29
+ );
35
30
  if (!covered) return false;
36
31
  }
37
32
  if (worker.allowNetwork && !orchestrator.allowNetwork) return false;
@@ -48,14 +43,14 @@ export const READONLY_SCOPE: ScopeSpec = {
48
43
 
49
44
  export const WORKSPACE_SCOPE: ScopeSpec = {
50
45
  allowedActions: new Set<ActionClass>(["read", "write", "execute", "dispatch"]),
51
- allowedWriteRoots: [process.cwd()],
46
+ allowedWriteRoots: [asDirectoryPathBoundary(path.resolve(process.cwd()))],
52
47
  allowNetwork: true,
53
48
  allowDispatch: true,
54
49
  };
55
50
 
56
51
  export const CONFIRMED_SCOPE: ScopeSpec = {
57
52
  allowedActions: new Set<ActionClass>(["read", "write", "execute", "dispatch", "system_modify"]),
58
- allowedWriteRoots: [process.cwd()],
53
+ allowedWriteRoots: [asDirectoryPathBoundary(path.resolve(process.cwd()))],
59
54
  allowNetwork: true,
60
55
  allowDispatch: true,
61
56
  };