@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
@@ -7,6 +7,7 @@
7
7
 
8
8
  import path from "node:path";
9
9
  import { THINKING_LEVELS, type ThinkingLevel } from "../../core/defaults.js";
10
+ import { asDirectoryPathBoundary, resolvePathBoundary } from "../../core/path-boundary.js";
10
11
  import { cloneValidatedResponseSchema } from "../../core/response-schema.js";
11
12
  import { isToolProfileName, type ToolProfileName } from "../../tools/profiles.js";
12
13
  import type { AgentProduct } from "../agents/spec.js";
@@ -19,6 +20,7 @@ import {
19
20
  } from "./execution-handoff.js";
20
21
  import { isExecutionRole } from "./execution-role.js";
21
22
  import { COMPETE_STANCES, type CompeteStance } from "./gate-role-prompts.js";
23
+ import { type DispatchIntent, isDispatchIntent } from "./intent.js";
22
24
  import { isRoutingIntent, type RoutingIntent } from "./routing-intent.js";
23
25
  import type {
24
26
  DispatchRequestOrigin,
@@ -121,6 +123,10 @@ export interface JobSpec {
121
123
  */
122
124
  denyTools?: ReadonlyArray<string>;
123
125
  cwd?: string;
126
+ /** Run this writer in an isolated git worktree. */
127
+ worktree?: true;
128
+ /** How a successful isolated writer is applied to its parent checkout. */
129
+ apply?: "merge" | "preserve";
124
130
  memorySection?: string;
125
131
  noSkills?: boolean;
126
132
  skillPaths?: ReadonlyArray<string>;
@@ -134,6 +140,8 @@ export interface JobSpec {
134
140
  * per-tool calls (subprocess).
135
141
  */
136
142
  writeRoots?: ReadonlyArray<string>;
143
+ /** Normalized model-declared path, output, and host verification intent. */
144
+ intent?: DispatchIntent;
137
145
  requestOrigin?: DispatchRequestOrigin;
138
146
  /**
139
147
  * Threaded output from the previous pipeline step. Set only by the dispatch
@@ -159,6 +167,8 @@ export interface JobSpec {
159
167
  autonomy?: AutonomyLevel;
160
168
  /** Review/compete gate provenance sealed into the run's receipt. */
161
169
  gate?: RunGateProvenance;
170
+ /** Council grouping projected to receipts and fleet surfaces. */
171
+ council?: { group: string; label: string; color?: string; round: number };
162
172
  /** Plan-approval provenance sealed into the run's receipt. */
163
173
  plan?: RunPlanProvenance;
164
174
  /**
@@ -195,18 +205,22 @@ const KNOWN_KEYS = new Set([
195
205
  "toolProfile",
196
206
  "denyTools",
197
207
  "cwd",
208
+ "worktree",
209
+ "apply",
198
210
  "memorySection",
199
211
  "noSkills",
200
212
  "skillPaths",
201
213
  "trustProjectCompatRoots",
202
214
  "responseSchema",
203
215
  "writeRoots",
216
+ "intent",
204
217
  "requestOrigin",
205
218
  "pipelineInput",
206
219
  "predecessorHandoffs",
207
220
  "lineage",
208
221
  "autonomy",
209
222
  "gate",
223
+ "council",
210
224
  "plan",
211
225
  "competeStance",
212
226
  ]);
@@ -399,7 +413,7 @@ export function validateJobSpec(spec: unknown): Validated {
399
413
 
400
414
  if ("toolProfile" in spec && spec.toolProfile !== undefined) {
401
415
  if (typeof spec.toolProfile !== "string" || !isToolProfileName(spec.toolProfile)) {
402
- errors.push("toolProfile must be one of: minimal-local|science-local|full-agent");
416
+ errors.push("toolProfile must be one of: minimal-local|science-local|full-agent|council-read-only");
403
417
  }
404
418
  }
405
419
 
@@ -414,6 +428,13 @@ export function validateJobSpec(spec: unknown): Validated {
414
428
  errors.push("cwd must be a non-empty string");
415
429
  }
416
430
  }
431
+ if ("worktree" in spec && spec.worktree !== undefined && spec.worktree !== true) {
432
+ errors.push("worktree must be true when present");
433
+ }
434
+ if ("apply" in spec && spec.apply !== undefined && spec.apply !== "merge" && spec.apply !== "preserve") {
435
+ errors.push("apply must be merge or preserve");
436
+ }
437
+ if (spec.apply !== undefined && spec.worktree !== true) errors.push("apply requires worktree: true");
417
438
 
418
439
  if ("memorySection" in spec && spec.memorySection !== undefined) {
419
440
  if (typeof spec.memorySection !== "string") {
@@ -457,6 +478,15 @@ export function validateJobSpec(spec: unknown): Validated {
457
478
  errors.push("writeRoots must be a non-empty array of non-empty strings");
458
479
  }
459
480
  }
481
+ if ("intent" in spec && spec.intent !== undefined && !isDispatchIntent(spec.intent)) {
482
+ errors.push("intent must be a normalized dispatch intent");
483
+ }
484
+ if (isDispatchIntent(spec.intent) && spec.intent.writeRoots.length > 0 && Array.isArray(spec.writeRoots)) {
485
+ const jobCwd = typeof spec.cwd === "string" && spec.cwd.length > 0 ? spec.cwd : process.cwd();
486
+ const legacy = [...new Set(spec.writeRoots.map((root) => resolvePathBoundary(jobCwd, String(root))))].sort();
487
+ const declared = [...new Set(spec.intent.writeRoots.map((root) => resolvePathBoundary(jobCwd, root)))].sort();
488
+ if (JSON.stringify(legacy) !== JSON.stringify(declared)) errors.push("intent_write_roots_contradiction");
489
+ }
460
490
 
461
491
  if ("requestOrigin" in spec && spec.requestOrigin !== undefined) {
462
492
  if (typeof spec.requestOrigin !== "string" || !VALID_REQUEST_ORIGINS.has(spec.requestOrigin)) {
@@ -550,18 +580,27 @@ export function validateJobSpec(spec: unknown): Validated {
550
580
  if (typeof spec.toolProfile === "string" && isToolProfileName(spec.toolProfile)) out.toolProfile = spec.toolProfile;
551
581
  if (Array.isArray(spec.denyTools)) out.denyTools = spec.denyTools.map((tool) => String(tool));
552
582
  if (typeof spec.cwd === "string") out.cwd = spec.cwd;
583
+ if (spec.worktree === true) out.worktree = true;
584
+ if (spec.apply === "merge" || spec.apply === "preserve") out.apply = spec.apply;
553
585
  if (typeof spec.memorySection === "string") out.memorySection = spec.memorySection;
554
586
  if (typeof spec.noSkills === "boolean") out.noSkills = spec.noSkills;
555
587
  if (Array.isArray(spec.skillPaths)) out.skillPaths = spec.skillPaths.map((p) => String(p));
556
588
  if (typeof spec.trustProjectCompatRoots === "boolean") out.trustProjectCompatRoots = spec.trustProjectCompatRoots;
557
589
  if (responseSchema) out.responseSchema = responseSchema;
558
- if (
590
+ if (isDispatchIntent(spec.intent)) out.intent = structuredClone(spec.intent);
591
+ const effectiveWriteRoots =
559
592
  Array.isArray(spec.writeRoots) &&
560
593
  spec.writeRoots.length > 0 &&
561
594
  spec.writeRoots.every((root) => typeof root === "string" && root.length > 0)
562
- ) {
595
+ ? spec.writeRoots
596
+ : isDispatchIntent(spec.intent) && spec.intent.writeRoots.length > 0
597
+ ? spec.intent.writeRoots
598
+ : undefined;
599
+ if (effectiveWriteRoots !== undefined) {
563
600
  const jobCwd = typeof spec.cwd === "string" && spec.cwd.length > 0 ? spec.cwd : process.cwd();
564
- out.writeRoots = spec.writeRoots.map((root) => path.resolve(jobCwd, String(root)));
601
+ out.writeRoots = isDispatchIntent(spec.intent)
602
+ ? effectiveWriteRoots.map((root) => resolvePathBoundary(jobCwd, String(root)))
603
+ : effectiveWriteRoots.map((root) => asDirectoryPathBoundary(path.resolve(jobCwd, String(root))));
565
604
  }
566
605
  if (typeof spec.requestOrigin === "string" && VALID_REQUEST_ORIGINS.has(spec.requestOrigin)) {
567
606
  out.requestOrigin = spec.requestOrigin as DispatchRequestOrigin;
@@ -572,6 +611,7 @@ export function validateJobSpec(spec: unknown): Validated {
572
611
  if (isValidLineage(spec.lineage)) out.lineage = spec.lineage;
573
612
  if (isAutonomyLevel(spec.autonomy)) out.autonomy = spec.autonomy;
574
613
  if (isValidGate(spec.gate)) out.gate = cloneGate(spec.gate);
614
+ if (isValidCouncil(spec.council)) out.council = { ...spec.council };
575
615
  if (typeof spec.competeStance === "string" && VALID_COMPETE_STANCES.has(spec.competeStance)) {
576
616
  out.competeStance = spec.competeStance as CompeteStance;
577
617
  }
@@ -654,9 +694,18 @@ function isValidProtectedArtifactRemap(value: unknown): value is ProtectedArtifa
654
694
  );
655
695
  }
656
696
 
657
- const VALID_GATE_ROLES = new Set(["builder", "reviewer", "candidate", "judge"]);
697
+ const VALID_GATE_ROLES = new Set(["builder", "reviewer", "candidate", "judge", "member", "synthesis"]);
658
698
  const VALID_GATE_VERDICTS = new Set(["pass", "fail", "revise"]);
659
- const VALID_PLAN_TOPOLOGIES = new Set(["parallel", "sequential", "pipeline", "review", "compete", "detached", "fleet"]);
699
+ const VALID_PLAN_TOPOLOGIES = new Set([
700
+ "parallel",
701
+ "sequential",
702
+ "pipeline",
703
+ "review",
704
+ "compete",
705
+ "council",
706
+ "detached",
707
+ "fleet",
708
+ ]);
660
709
  const VALID_PLAN_APPROVALS = new Set(["operator", "full-auto"]);
661
710
 
662
711
  function isValidGate(value: unknown): value is RunGateProvenance {
@@ -688,6 +737,20 @@ function cloneGate(gate: RunGateProvenance): RunGateProvenance {
688
737
  };
689
738
  }
690
739
 
740
+ function isValidCouncil(value: unknown): value is NonNullable<JobSpec["council"]> {
741
+ return (
742
+ isPlainObject(value) &&
743
+ typeof value.group === "string" &&
744
+ value.group.length > 0 &&
745
+ typeof value.label === "string" &&
746
+ value.label.length > 0 &&
747
+ (value.color === undefined || typeof value.color === "string") &&
748
+ typeof value.round === "number" &&
749
+ Number.isInteger(value.round) &&
750
+ value.round >= 1
751
+ );
752
+ }
753
+
691
754
  function isValidPlan(value: unknown): value is RunPlanProvenance {
692
755
  if (!isPlainObject(value)) return false;
693
756
  if (typeof value.hash !== "string" || value.hash.length === 0) return false;
@@ -8,6 +8,11 @@
8
8
  * a path that changed cannot be pinned on one of them, and the plan compiler
9
9
  * has already refused the case where more than one of them was allowed to
10
10
  * write. Every step in a violating window fails, and the verdict says why.
11
+ *
12
+ * The window also carries what its runs recorded writing, which is what keeps
13
+ * the checkout diff from being read as authorship. A change nobody in the
14
+ * window recorded belongs to whoever else has the checkout open, and the
15
+ * verdict reports it without touching it.
11
16
  */
12
17
 
13
18
  import { existsSync } from "node:fs";
@@ -16,9 +21,11 @@ import type { ExecutionPlan } from "./execution-plan.js";
16
21
  import type { ExecutionWriteBoundaryOutcome } from "./execution-scheduler.js";
17
22
  import {
18
23
  assertWriteBoundaryInsideRoot,
24
+ assertWriteBoundaryVisibleToGit,
19
25
  captureWorkspaceSnapshot,
20
26
  enforceWriteBoundary,
21
27
  type WorkspaceSnapshot,
28
+ type WriteBoundaryAttribution,
22
29
  type WriteBoundaryVerdict,
23
30
  writeWriteBoundaryVerdict,
24
31
  } from "./write-boundary.js";
@@ -30,6 +37,15 @@ export interface WriteBoundaryEnforcerInput {
30
37
  rootId: string;
31
38
  /** Declared allowlist for a step, or undefined when it declares no boundary. */
32
39
  boundaryFor(stepId: string): ReadonlyArray<string> | undefined;
40
+ /**
41
+ * What the step's own run recorded writing, or null when the step has no
42
+ * closed write record: a code step running a registered command, a runtime
43
+ * that publishes no tool telemetry, a run whose telemetry had holes, or a run
44
+ * that shelled out or spawned a worker and could have written a path no
45
+ * argument names. Absent entirely means the caller offers no record at all,
46
+ * and every change outside the declaration is blamed on the window.
47
+ */
48
+ recordedWritesFor?(stepId: string): ReadonlyArray<string> | null;
33
49
  /** Called once per closed window with the sealed verdict and where it landed. */
34
50
  onVerdict?(verdict: WriteBoundaryVerdict, path: string): void;
35
51
  }
@@ -54,6 +70,9 @@ export function preflightWriteBoundaries(plan: ExecutionPlan, root: string): voi
54
70
  );
55
71
  }
56
72
  for (const step of declared) assertWriteBoundaryInsideRoot(root, step.writes ?? []);
73
+ // One question over the whole plan: an entry is refused for being invisible
74
+ // to git whichever step declared it, and the diagnostic names the path.
75
+ assertWriteBoundaryVisibleToGit(root, [...new Set(declared.flatMap((step) => [...(step.writes ?? [])]))]);
57
76
  }
58
77
 
59
78
  export function createWriteBoundaryEnforcer(input: WriteBoundaryEnforcerInput): WriteBoundaryEnforcer {
@@ -63,10 +82,34 @@ export function createWriteBoundaryEnforcer(input: WriteBoundaryEnforcerInput):
63
82
  for (const stepId of stepIds) for (const entry of input.boundaryFor(stepId) ?? []) allow.add(entry);
64
83
  return [...allow].sort();
65
84
  };
85
+ /**
86
+ * The window's write witness. One step without a record makes the whole
87
+ * window's record incomplete: the steps share a checkout, so a change no
88
+ * other step wrote could still be the unobserved one's.
89
+ */
90
+ const attributionFor = (stepIds: ReadonlyArray<string>): WriteBoundaryAttribution | undefined => {
91
+ const recordedWritesFor = input.recordedWritesFor;
92
+ if (recordedWritesFor === undefined) return undefined;
93
+ const recorded = new Set<string>();
94
+ let complete = true;
95
+ for (const stepId of stepIds) {
96
+ const observed = recordedWritesFor(stepId);
97
+ if (observed === null) {
98
+ complete = false;
99
+ continue;
100
+ }
101
+ for (const target of observed) recorded.add(target);
102
+ }
103
+ return { recorded: [...recorded].sort(), complete };
104
+ };
66
105
  return {
67
106
  begin(window, stepIds) {
68
107
  const allow = allowFor(stepIds);
69
108
  assertWriteBoundaryInsideRoot(input.root, allow);
109
+ // Also checked whole-plan at preflight. Repeated here because a
110
+ // delegation plan splices steps into a running fleet, and their
111
+ // declarations reach this function without ever passing preflight.
112
+ assertWriteBoundaryVisibleToGit(input.root, allow);
70
113
  open.set(window, { snapshot: captureWorkspaceSnapshot(input.root), allow });
71
114
  },
72
115
  async verify(window, stepIds) {
@@ -75,11 +118,13 @@ export function createWriteBoundaryEnforcer(input: WriteBoundaryEnforcerInput):
75
118
  throw new Error(`write boundary: window '${window}' was verified without a snapshot`);
76
119
  }
77
120
  open.delete(window);
121
+ const attribution = attributionFor(stepIds);
78
122
  const verdict = enforceWriteBoundary({
79
123
  snapshot: pending.snapshot,
80
124
  window,
81
125
  stepIds,
82
126
  allow: pending.allow,
127
+ ...(attribution === undefined ? {} : { attribution }),
83
128
  });
84
129
  const path = writeWriteBoundaryVerdict(input.rootId, verdict);
85
130
  input.onVerdict?.(verdict, path);
@@ -21,18 +21,27 @@
21
21
  * Everything is pinned to the baseline commit recorded at snapshot time, so a
22
22
  * commit step that moves HEAD without touching the working tree reads as "no
23
23
  * change" instead of as a wholesale rewrite.
24
+ *
25
+ * A window's diff answers "what changed", never "who changed it". An operator
26
+ * editing the checkout while a fleet runs produces changes inside the window
27
+ * that no step made, and rolling those back to the baseline destroys work
28
+ * nobody asked Clio to touch. So a change is blamed on the window only when it
29
+ * intersects what the window's own runs recorded writing; everything else is
30
+ * reported as an unattributed concurrent change and left exactly as it is.
24
31
  */
25
32
 
26
33
  import { execFileSync } from "node:child_process";
27
34
  import { createHash } from "node:crypto";
28
35
  import { existsSync, lstatSync, mkdirSync, readlinkSync, realpathSync, rmSync, writeFileSync } from "node:fs";
29
- import { dirname, join, relative, resolve, sep } from "node:path";
36
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
37
+ import { asDirectoryPathBoundary, pathBoundaryEntryCovers } from "../../core/path-boundary.js";
30
38
  import { clioStateDir } from "../../core/xdg.js";
31
39
  import { atomicWrite } from "../../engine/session.js";
32
40
  import { isGitRepository } from "../../tools/compete-worktrees.js";
33
41
  import {
34
42
  describeWriteBoundary,
35
43
  normalizeWriteBoundary,
44
+ normalizeWriteBoundaryEntry,
36
45
  type WriteBoundary,
37
46
  writeBoundaryCovers,
38
47
  } from "../agents/write-boundary.js";
@@ -71,10 +80,15 @@ function gitBytes(root: string, args: ReadonlyArray<string>): Buffer {
71
80
  *
72
81
  * Enforcement sees exactly what git sees, so an ignored path is outside it: a
73
82
  * repository decides what counts as its own content, and this is not the place
74
- * to overrule that. The one thing subtracted is Clio's own state directory when
75
- * an operator has placed it inside the workspace. Receipts, code-step logs, and
76
- * boundary verdicts are the orchestrator writing its journal while the step
77
- * runs, and blaming the step for them would make every window a violation.
83
+ * to overrule that. The consequence is that a boundary declared over an ignored
84
+ * path could never be verified, which is why `assertWriteBoundaryVisibleToGit`
85
+ * refuses that declaration before anything runs rather than letting this
86
+ * function certify an unobserved window as clean.
87
+ *
88
+ * The one thing subtracted is Clio's own state directory when an operator has
89
+ * placed it inside the workspace. Receipts, code-step logs, and boundary
90
+ * verdicts are the orchestrator writing its journal while the step runs, and
91
+ * blaming the step for them would make every window a violation.
78
92
  */
79
93
  function dirtyPaths(root: string): string[] {
80
94
  const raw = git(root, ["status", "--porcelain=v1", "-z", "-uall", "--no-renames"]);
@@ -222,14 +236,55 @@ export interface WriteBoundaryUnrecoverable {
222
236
  }
223
237
 
224
238
  /**
225
- * `clean` means nothing outside the allowlist changed. `rolled-back` means
226
- * something did and the repository now looks as it did before. Anything else is
227
- * `rollback-incomplete`: the working tree is left exactly as the step made it
228
- * and handed to the operator with the list, because a rollback that guesses at
229
- * content it never recorded destroys work.
239
+ * `clean` means no change outside the allowlist is attributable to this
240
+ * window's steps. `rolled-back` means one was and the repository now looks as
241
+ * it did before. Anything else is `rollback-incomplete`: the working tree is
242
+ * left exactly as the step made it and handed to the operator with the list,
243
+ * because a rollback that guesses at content it never recorded destroys work.
244
+ *
245
+ * A `clean` verdict can still carry `unattributed` paths. Those changed inside
246
+ * the window and no run in it recorded writing them, so they are the
247
+ * operator's, another process's, or a build artifact's, and nothing here may
248
+ * touch them.
230
249
  */
231
250
  export type WriteBoundaryStatus = "clean" | "rolled-back" | "rollback-incomplete";
232
251
 
252
+ /**
253
+ * What the window's own runs recorded aiming a successful mutation at.
254
+ *
255
+ * This is the write-side witness the diff cannot be: the checkout says a path
256
+ * changed, and only the run's own tool-call record says whether the run is what
257
+ * changed it. The record comes from the same fold that grounds a sealed
258
+ * mutation report, so the boundary and the result contract judge one set of
259
+ * facts.
260
+ *
261
+ * A run can write through a channel no tool event enumerates, and then its path
262
+ * set is a lower bound rather than the whole list. That is not silently
263
+ * tolerated: the recorder knows which tools can do it (bash, verify, dispatch,
264
+ * steer, and any tool this process has no schema for) and reports the record
265
+ * open, which arrives here as `complete: false`. So the set is only ever read
266
+ * as a closed list for a run that could not have written outside it, and
267
+ * enforcement over a run that could stays exactly as strong as it was before
268
+ * attribution existed.
269
+ */
270
+ export interface WriteBoundaryAttribution {
271
+ /**
272
+ * Paths the window's runs aimed a successful mutation at. Absolute, or
273
+ * relative to the workspace root; anything resolving outside the workspace is
274
+ * dropped, because git could not have reported it as a change either.
275
+ */
276
+ recorded: ReadonlyArray<string>;
277
+ /**
278
+ * False when at least one step in the window has no closed write record: a
279
+ * code step running a registered command, a runtime that publishes no tool
280
+ * telemetry, a run whose telemetry came back with holes, or a run that
281
+ * called a tool able to write a path its arguments do not name. An open
282
+ * record cannot clear anybody, so the window falls back to blaming every
283
+ * change outside the allowlist, and the verdict says it did.
284
+ */
285
+ complete: boolean;
286
+ }
287
+
233
288
  export interface WriteBoundaryVerdict {
234
289
  version: 1;
235
290
  /** Which scheduling window this covers, e.g. `wave-2` or `revalidate-suite.check.1`. */
@@ -243,12 +298,25 @@ export interface WriteBoundaryVerdict {
243
298
  checkedAt: string;
244
299
  changedPaths: ReadonlyArray<string>;
245
300
  violations: ReadonlyArray<string>;
301
+ /**
302
+ * Paths outside the allowlist that changed inside the window and that no run
303
+ * in it recorded writing. Recorded so the operator can see them, never rolled
304
+ * back: the window did not make them, so the baseline commit is not their
305
+ * prior content.
306
+ */
307
+ unattributed: ReadonlyArray<string>;
308
+ /** Whether every step in the window offered an enumerable write record. */
309
+ attributionComplete: boolean;
246
310
  rolledBack: ReadonlyArray<WriteBoundaryRollback>;
247
311
  unrecoverable: ReadonlyArray<WriteBoundaryUnrecoverable>;
248
312
  status: WriteBoundaryStatus;
249
313
  /** Typed failure reason, or null when the window stayed inside its boundary. */
250
314
  reason: typeof WRITE_BOUNDARY_VIOLATION_REASON | null;
251
- /** Operator-facing message naming the offending paths and the declaration. */
315
+ /**
316
+ * Operator-facing message naming the offending paths and the declaration, or
317
+ * naming the concurrent changes the window saw but did not cause. Null only
318
+ * when neither happened.
319
+ */
252
320
  detail: string | null;
253
321
  digest: string;
254
322
  }
@@ -258,6 +326,12 @@ export interface EnforceWriteBoundaryInput {
258
326
  window: string;
259
327
  stepIds: ReadonlyArray<string>;
260
328
  allow: WriteBoundary;
329
+ /**
330
+ * What the window's runs recorded writing. Absent means no caller offered a
331
+ * record, which reads the same as an incomplete one: every change outside the
332
+ * allowlist is blamed on the window.
333
+ */
334
+ attribution?: WriteBoundaryAttribution;
261
335
  }
262
336
 
263
337
  function canonicalVerdict(verdict: Omit<WriteBoundaryVerdict, "digest">): string {
@@ -317,6 +391,22 @@ function rollbackPath(
317
391
  return { rolledBack: { path: change.path, action: "restored", restoredFrom: `${snapshot.head}:${change.path}` } };
318
392
  }
319
393
 
394
+ /**
395
+ * Whether one changed path falls inside what a run recorded writing.
396
+ *
397
+ * A recorded target is usually the exact file a write or edit named, but a
398
+ * command's delete target can be a directory, so a change under a recorded
399
+ * prefix counts as the same write. Matching is on segment boundaries: `work`
400
+ * does not cover `workspace/a.txt`.
401
+ */
402
+ function withinRecordedWrite(recorded: ReadonlySet<string>, path: string): boolean {
403
+ if (recorded.has(path)) return true;
404
+ for (const target of recorded) {
405
+ if (path.startsWith(`${target}/`)) return true;
406
+ }
407
+ return false;
408
+ }
409
+
320
410
  /**
321
411
  * Compare, roll back, and return the verdict. Never throws for a violation: a
322
412
  * violation is evidence the caller records and acts on, not an exception that
@@ -325,7 +415,21 @@ function rollbackPath(
325
415
  export function enforceWriteBoundary(input: EnforceWriteBoundaryInput): WriteBoundaryVerdict {
326
416
  const allow = normalizeWriteBoundary(input.allow);
327
417
  const changes = diffWorkspace(input.snapshot);
328
- const violations = changes.filter((change) => !writeBoundaryCovers(allow, change.path));
418
+ const outside = changes.filter((change) => !writeBoundaryCovers(allow, change.path));
419
+ const attributionComplete = input.attribution?.complete === true;
420
+ const recorded = new Set<string>();
421
+ for (const target of input.attribution?.recorded ?? []) {
422
+ const relativeTarget = repoRelative(input.snapshot.root, target);
423
+ if (relativeTarget !== null) recorded.add(relativeTarget);
424
+ }
425
+ // Without a complete record nothing can be cleared, so the window keeps the
426
+ // whole diff. With one, only what the runs actually wrote is theirs.
427
+ const violations = attributionComplete
428
+ ? outside.filter((change) => withinRecordedWrite(recorded, change.path))
429
+ : outside;
430
+ const unattributed = attributionComplete
431
+ ? outside.filter((change) => !withinRecordedWrite(recorded, change.path))
432
+ : [];
329
433
  const rolledBack: WriteBoundaryRollback[] = [];
330
434
  const unrecoverable: WriteBoundaryUnrecoverable[] = [];
331
435
  for (const change of violations) {
@@ -335,15 +439,15 @@ export function enforceWriteBoundary(input: EnforceWriteBoundaryInput): WriteBou
335
439
  }
336
440
  const status: WriteBoundaryStatus =
337
441
  violations.length === 0 ? "clean" : unrecoverable.length === 0 ? "rolled-back" : "rollback-incomplete";
338
- const attribution =
442
+ const who =
339
443
  input.stepIds.length === 1
340
444
  ? `step '${input.stepIds[0]}'`
341
445
  : `steps ${input.stepIds.map((id) => `'${id}'`).join(", ")} ran concurrently in one checkout, so the write cannot be attributed to one of them`;
342
- const detail =
446
+ const violationDetail =
343
447
  violations.length === 0
344
- ? null
448
+ ? []
345
449
  : [
346
- `${attribution} wrote outside its declared boundary.`,
450
+ `${who} wrote outside its declared boundary.`,
347
451
  `Unauthorized paths: ${violations.map((change) => change.path).join(", ")}.`,
348
452
  `Declared writes: ${describeWriteBoundary(allow)}.`,
349
453
  status === "rolled-back"
@@ -351,8 +455,21 @@ export function enforceWriteBoundary(input: EnforceWriteBoundaryInput): WriteBou
351
455
  : `Rollback is incomplete; the working tree is left as the step made it. Unrestorable: ${unrecoverable
352
456
  .map((entry) => `${entry.path} (${entry.reason})`)
353
457
  .join("; ")}.`,
458
+ attributionComplete
459
+ ? "Each was blamed on this window because a run in it recorded writing that path."
460
+ : "This window had no complete write record, so every change outside the declaration was blamed on it.",
354
461
  "If the change was legitimate, widen the step's `writes:` declaration to cover it.",
355
- ].join(" ");
462
+ ];
463
+ const concurrentDetail =
464
+ unattributed.length === 0
465
+ ? []
466
+ : [
467
+ `Concurrent changes were seen in this window that no run in it recorded writing: ${unattributed
468
+ .map((change) => change.path)
469
+ .join(", ")}.`,
470
+ "They were left untouched, because a path this window did not write is not this window's to restore.",
471
+ ];
472
+ const detail = [...violationDetail, ...concurrentDetail];
356
473
  const body: Omit<WriteBoundaryVerdict, "digest"> = {
357
474
  version: 1,
358
475
  window: input.window,
@@ -363,11 +480,13 @@ export function enforceWriteBoundary(input: EnforceWriteBoundaryInput): WriteBou
363
480
  checkedAt: new Date().toISOString(),
364
481
  changedPaths: changes.map((change) => change.path),
365
482
  violations: violations.map((change) => change.path),
483
+ unattributed: unattributed.map((change) => change.path),
484
+ attributionComplete,
366
485
  rolledBack,
367
486
  unrecoverable,
368
487
  status,
369
488
  reason: violations.length === 0 ? null : WRITE_BOUNDARY_VIOLATION_REASON,
370
- detail,
489
+ detail: detail.length === 0 ? null : detail.join(" "),
371
490
  };
372
491
  return { ...body, digest: createHash("sha256").update(canonicalVerdict(body), "utf8").digest("hex") };
373
492
  }
@@ -387,16 +506,76 @@ export function assertWriteBoundaryInsideRoot(root: string, allow: WriteBoundary
387
506
  // path a step is about to create.
388
507
  while (probe !== realRoot && !existsSync(probe)) probe = dirname(probe);
389
508
  const resolved = realpathSync(probe);
390
- if (resolved !== realRoot && !resolved.startsWith(realRoot + sep)) {
509
+ // The root is inside itself. The prefix walk above stops at the root when
510
+ // a declared directory does not exist yet, and `pathBoundaryEntryCovers`
511
+ // answers a different question: it asks whether a changed path falls under
512
+ // a declaration, where the directory entry itself is not a change git ever
513
+ // reports. Asking it about the root would refuse every boundary naming a
514
+ // path the step is about to create.
515
+ if (resolved !== realRoot && !pathBoundaryEntryCovers(asDirectoryPathBoundary(realRoot), resolved)) {
391
516
  throw new Error(`write boundary: entry '${entry}' resolves to ${resolved}, outside the workspace at ${realRoot}`);
392
517
  }
393
518
  }
394
519
  }
395
520
 
396
- /** Repo-relative form of an absolute path, or null when it is outside. */
521
+ /**
522
+ * Refuse a declaration git will never report on.
523
+ *
524
+ * `git status` does not list an ignored path, so a boundary entry the
525
+ * repository ignores names a region enforcement is blind to: every window over
526
+ * it is certified clean without a single observation, which is a stronger claim
527
+ * than "nothing was checked" and reads as a passing one. That is the same
528
+ * defect `assertWriteBoundaryInsideRoot` refuses for an entry that leaves the
529
+ * workspace through a symlink, so it is refused in the same place and at the
530
+ * same time, before anything runs.
531
+ *
532
+ * A tracked path is reportable whatever the ignore rules say, and git's own
533
+ * index-aware check is what decides that, so this never refuses a path the
534
+ * repository is actually following.
535
+ */
536
+ export function assertWriteBoundaryVisibleToGit(root: string, allow: WriteBoundary): void {
537
+ // Entry by entry rather than through `normalizeWriteBoundary`, whose 32-entry
538
+ // cap is a per-step declaration limit. A caller may legitimately ask this
539
+ // question about every entry a whole plan declared.
540
+ const entries = [...new Set(allow.map(normalizeWriteBoundaryEntry))].sort();
541
+ if (entries.length === 0) return;
542
+ const absolute = resolve(root);
543
+ let raw: string;
544
+ try {
545
+ raw = git(absolute, ["check-ignore", "-v", "-z", "--stdin"], `${entries.join("\0")}\0`);
546
+ } catch (error) {
547
+ // `check-ignore` exits 1 when nothing matched, which is the common answer
548
+ // and not a failure. Any other exit is a real inability to decide, and
549
+ // enforcement fails closed rather than assuming visibility.
550
+ const status = (error as { status?: number }).status;
551
+ if (status === 1) return;
552
+ throw new Error(
553
+ `write boundary: could not determine whether ${describeWriteBoundary(entries)} is ignored by git in ${absolute}: ${(error as Error).message}`,
554
+ );
555
+ }
556
+ const fields = raw.split("\0");
557
+ const refusals: string[] = [];
558
+ // `-v -z` emits source, line number, pattern, and pathname per match.
559
+ for (let index = 0; index + 3 < fields.length; index += 4) {
560
+ const [source, line, pattern, path] = fields.slice(index, index + 4);
561
+ if (path === undefined || path.length === 0) continue;
562
+ refusals.push(`'${path}' is ignored by ${source}:${line}:${pattern}`);
563
+ }
564
+ if (refusals.length === 0) return;
565
+ throw new Error(
566
+ `write boundary: ${refusals.join(", ")}. git status never reports an ignored path, so a boundary declared over one is verified against nothing and every window would be certified clean without an observation. Declare a path the repository tracks, or stop ignoring this one.`,
567
+ );
568
+ }
569
+
570
+ /**
571
+ * Workspace-relative form of a path, or null when it resolves outside. A
572
+ * relative input is taken as already workspace-relative, which is how a
573
+ * recorded write target and an absolute state directory both normalize here.
574
+ */
397
575
  function repoRelative(root: string, path: string): string | null {
398
- const rel = relative(resolve(root), resolve(path));
399
- if (rel.length === 0 || rel.startsWith("..") || rel.startsWith(`..${sep}`)) return null;
576
+ const base = resolve(root);
577
+ const rel = relative(base, resolve(base, path));
578
+ if (rel.length === 0 || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return null;
400
579
  return rel.split(sep).join("/");
401
580
  }
402
581