@iowarp/clio-coder 0.3.7 → 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 (223) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +7 -2
  3. package/dist/{acp-SK4MD6MM.js → acp-U67UHUK2.js} +13 -13
  4. package/dist/{agents-2FN2K6ME.js → agents-YU6SGALZ.js} +35 -33
  5. package/dist/assets/codewiki.json +1 -1
  6. package/dist/{auth-QIYZWM5I.js → auth-5ZPJOIVG.js} +32 -24
  7. package/dist/builtins-C6JMZVV6.js +17 -0
  8. package/dist/{chunk-YD734TPH.js → chunk-26LEYJZH.js} +2 -2
  9. package/dist/{chunk-TSHXZTOQ.js → chunk-2HEJ2F35.js} +5 -5
  10. package/dist/{chunk-WJHBC77E.js → chunk-2HFZQUHL.js} +5 -5
  11. package/dist/{chunk-FO5ZOVUY.js → chunk-3BINW3FP.js} +5 -5
  12. package/dist/{chunk-EOOQZZDE.js → chunk-4SPRNWDE.js} +18 -18
  13. package/dist/{chunk-EBEFWSGL.js → chunk-5DHKRSMQ.js} +7 -7
  14. package/dist/{chunk-OBMAI2DP.js → chunk-5H3GB5BO.js} +11 -9
  15. package/dist/{chunk-JTSEDYVQ.js → chunk-5Q2VVUKB.js} +7 -7
  16. package/dist/{chunk-VQNODYQ4.js → chunk-7RFXX52T.js} +294 -3704
  17. package/dist/{chunk-XEGB6BCN.js → chunk-7RGZWPB6.js} +5 -5
  18. package/dist/{chunk-GH5622CP.js → chunk-A2NJGIB3.js} +2 -2
  19. package/dist/{chunk-CEYBNUGC.js → chunk-A3WNZD3P.js} +323 -27
  20. package/dist/{chunk-D4MDIG46.js → chunk-B5CSFE7B.js} +7 -7
  21. package/dist/{chunk-2SFS6XQE.js → chunk-DGSYXYMX.js} +3 -2
  22. package/dist/{chunk-ZWLZP4ZT.js → chunk-DYIM5TJT.js} +98 -12
  23. package/dist/{chunk-KCMKRQX4.js → chunk-E77JEWSD.js} +42 -49
  24. package/dist/{chunk-J7PIKKWC.js → chunk-EMYUUSFG.js} +7 -7
  25. package/dist/{chunk-JRIO5UD2.js → chunk-EQ63NRB7.js} +5 -5
  26. package/dist/{chunk-UVDSQ6LW.js → chunk-FBVTI2TJ.js} +74 -28
  27. package/dist/{chunk-BMWK7ZIZ.js → chunk-FHJEP5SW.js} +13 -13
  28. package/dist/{chunk-FOT2FX5J.js → chunk-GN57SG4G.js} +7 -7
  29. package/dist/{chunk-SROCI7ZU.js → chunk-GPIEI3LY.js} +5 -5
  30. package/dist/{chunk-UFQ3F4FW.js → chunk-GU2UIAFZ.js} +4 -4
  31. package/dist/{chunk-UND3GU2L.js → chunk-H7IXIC72.js} +2 -2
  32. package/dist/{chunk-VREKEFLL.js → chunk-HLE42MG7.js} +3 -3
  33. package/dist/{chunk-465YSENW.js → chunk-IFBNV6H6.js} +3 -3
  34. package/dist/{chunk-LADCF22A.js → chunk-IGWKHNIQ.js} +113 -54
  35. package/dist/chunk-IIZWH4XA.js +172 -0
  36. package/dist/{chunk-UHXRNZ2J.js → chunk-IJ7RPIYJ.js} +4 -4
  37. package/dist/{chunk-M4AKACEO.js → chunk-J3YUBZWY.js} +2 -2
  38. package/dist/{chunk-AB4XIIVB.js → chunk-JOZYP4GM.js} +6 -6
  39. package/dist/{chunk-ZGH7FGS5.js → chunk-K4XHGFR5.js} +742 -12
  40. package/dist/{chunk-EELBMBT6.js → chunk-KTYTFRMB.js} +62 -5
  41. package/dist/chunk-LU7P4LHA.js +33 -0
  42. package/dist/{chunk-DQA7QLMD.js → chunk-ME6CCNFO.js} +9 -24
  43. package/dist/{chunk-KZ2H5X4G.js → chunk-MXKJU4JB.js} +88 -14
  44. package/dist/{chunk-DMD2AGVS.js → chunk-N22QMJKY.js} +20 -18
  45. package/dist/chunk-NMPKI6XL.js +3006 -0
  46. package/dist/chunk-NUGM5KR6.js +165 -0
  47. package/dist/{chunk-5C77SEEY.js → chunk-P43ETTHK.js} +2 -2
  48. package/dist/{chunk-JNXPYBB4.js → chunk-PMDBGQSJ.js} +2 -2
  49. package/dist/chunk-PT7HYKEM.js +165 -0
  50. package/dist/{chunk-GEYXPTRF.js → chunk-RWSI4YD7.js} +2 -2
  51. package/dist/{chunk-C4JBQ5SR.js → chunk-TANS5ZJS.js} +6 -6
  52. package/dist/{chunk-YTYFXUI3.js → chunk-TB5666IT.js} +9 -9
  53. package/dist/{chunk-UUANF5CR.js → chunk-TLQJPP24.js} +375 -687
  54. package/dist/{chunk-OB5HIGJY.js → chunk-TT36MB5S.js} +2 -1
  55. package/dist/{chunk-DJNLUABN.js → chunk-TTHACPOM.js} +135 -17
  56. package/dist/{chunk-6M7VS3J3.js → chunk-TYPGUK6W.js} +4 -4
  57. package/dist/{chunk-THKY7CD7.js → chunk-U6MBIEMB.js} +108 -24
  58. package/dist/{chunk-PD3MESLB.js → chunk-VAWNZU7Z.js} +4 -4
  59. package/dist/{chunk-IR4CFBFN.js → chunk-VCBR6CU7.js} +12 -12
  60. package/dist/{chunk-WHJYKASB.js → chunk-VHN4MY6O.js} +2 -2
  61. package/dist/{chunk-MXI6J5JF.js → chunk-VWZOAB7K.js} +10 -10
  62. package/dist/{chunk-QCTRSGHQ.js → chunk-WLFILSD5.js} +36 -36
  63. package/dist/{chunk-5UJ6ECTS.js → chunk-WNIJTQQK.js} +6 -6
  64. package/dist/{chunk-4DWFMQDR.js → chunk-WSB3FPX7.js} +68 -20
  65. package/dist/{chunk-3HAPLH5M.js → chunk-WWCZ5F23.js} +112 -10
  66. package/dist/{chunk-X2KV5FXT.js → chunk-WXY7KU3G.js} +2 -2
  67. package/dist/{chunk-PPAMZ32Z.js → chunk-XK56QHLX.js} +6 -1
  68. package/dist/{chunk-DOOEX22V.js → chunk-XWSF374K.js} +4 -4
  69. package/dist/{chunk-6TUKSZVF.js → chunk-YS5VLNH5.js} +8 -8
  70. package/dist/{chunk-ZZMN5OM4.js → chunk-ZNLWCMVZ.js} +2 -2
  71. package/dist/{chunk-WHGPSPT5.js → chunk-ZVJ5BLO2.js} +2 -2
  72. package/dist/cli/index.js +31 -31
  73. package/dist/{clio-WBVQEBKO.js → clio-QVTYJ57A.js} +8 -8
  74. package/dist/code-nav-FGGFIE7L.js +2 -2
  75. package/dist/codewiki/build-worker.js +4 -4
  76. package/dist/{components-F7OEATSO.js → components-ZFA3SAER.js} +8 -8
  77. package/dist/{config-TRBL3RCF.js → config-LW5IJFQN.js} +58 -53
  78. package/dist/{configure-OLCVPHNM.js → configure-7XIZCOU4.js} +26 -21
  79. package/dist/{context-XEWE3MOJ.js → context-L3WL3X7K.js} +47 -41
  80. package/dist/{context-WFPKQSM6.js → context-N52ZA626.js} +22 -22
  81. package/dist/{context-MJIJ6GOX.js → context-Y6Y7QPR6.js} +12 -12
  82. package/dist/{context-clear-KNOS2JPB.js → context-clear-MBQRLSDQ.js} +47 -41
  83. package/dist/{context-index-SSR5ECNE.js → context-index-HVMFQHK3.js} +5 -5
  84. package/dist/{context-working-set-EUXAZI6N.js → context-working-set-GS6DSO7F.js} +14 -14
  85. package/dist/{dispatch-runner-B7MTOVKL.js → dispatch-runner-22ZCNOM3.js} +83 -55
  86. package/dist/{docs-FLJTIDSE.js → docs-7LQ23DLM.js} +8 -8
  87. package/dist/{doctor-RN4YKO2X.js → doctor-M7YEDGAE.js} +24 -20
  88. package/dist/{eval-RUBJVSNQ.js → eval-BEC2WHDA.js} +71 -19
  89. package/dist/{evidence-JZNBUOQZ.js → evidence-REJUMSKM.js} +62 -56
  90. package/dist/{evolve-FJVC4KKI.js → evolve-PY5ZBA5K.js} +41 -35
  91. package/dist/{extensions-IQL36S7K.js → extensions-HVKU65YU.js} +6 -6
  92. package/dist/{fleet-BDKYJFCP.js → fleet-7WZEWRFA.js} +72 -67
  93. package/dist/{fleet-commands-ZFIWZSB3.js → fleet-commands-UVHWM76J.js} +10 -10
  94. package/dist/{fleet-graph-Y6HPXIVF.js → fleet-graph-6ULH7PES.js} +13 -13
  95. package/dist/{fleet-preflight-BHSNPBMH.js → fleet-preflight-J53T6CCE.js} +5 -5
  96. package/dist/{fleet-validate-BIYREGIK.js → fleet-validate-72PC4SLA.js} +17 -17
  97. package/dist/{init-LQUB5COQ.js → init-OG3TPGQG.js} +61 -56
  98. package/dist/{library-NJAHIGG4.js → library-CNTMPLRF.js} +18 -18
  99. package/dist/{memory-OG6HOYKM.js → memory-6IS7F275.js} +43 -37
  100. package/dist/{models-5ZG5XY7J.js → models-ENRJDA5W.js} +33 -28
  101. package/dist/{monitor-TJ7AMTGB.js → monitor-XLDVO7TN.js} +56 -38
  102. package/dist/{orchestrator-WZYB54DM.js → orchestrator-6KSPYRHA.js} +421 -303
  103. package/dist/{paths-XUC7GS6E.js → paths-DBXMZMDU.js} +5 -5
  104. package/dist/registry-LG64LTF4.js +11 -0
  105. package/dist/{reset-PXQT45IY.js → reset-RZ4ER727.js} +10 -10
  106. package/dist/{run-FQ74YF62.js → run-Y2CNK5RU.js} +79 -74
  107. package/dist/{share-FW7SVCL3.js → share-A55GYP6Z.js} +16 -16
  108. package/dist/{skills-7E7IRB3R.js → skills-ALC5J6AT.js} +19 -19
  109. package/dist/{skills-eval-LI75W6OK.js → skills-eval-JPBEBYQU.js} +52 -47
  110. package/dist/support-MIETYA5E.js +38 -0
  111. package/dist/{targets-4CIFKCTW.js → targets-VGNXIR3S.js} +42 -33
  112. package/dist/{terminal-lease-WUZY7ZV5.js → terminal-lease-WOBR64YA.js} +3 -3
  113. package/dist/{uninstall-7FV7IP4E.js → uninstall-ZJF5H5ZN.js} +8 -8
  114. package/dist/{upgrade-K2HVIVMQ.js → upgrade-FUSUAGHR.js} +27 -25
  115. package/dist/{usage-GTZELZQX.js → usage-N4MKVHKD.js} +51 -46
  116. package/dist/{verifiers-RLAHT27O.js → verifiers-YAWOJ3H2.js} +13 -13
  117. package/dist/{verify-BX3BRKH5.js → verify-LTDHYBGY.js} +9 -9
  118. package/dist/{wiki-generate-ASIFASCN.js → wiki-generate-6M7GHTBJ.js} +66 -61
  119. package/dist/worker/entry.js +68 -61
  120. package/docs/alcf-provider.md +1 -1
  121. package/docs/architecture.md +1 -1
  122. package/docs/artifact-versions.md +6 -5
  123. package/docs/built-in-agents.md +1 -1
  124. package/docs/commands-and-modes.md +2 -2
  125. package/docs/configuration-and-targets.md +8 -3
  126. package/docs/context-engine.md +1 -1
  127. package/docs/documentation-guide.md +1 -1
  128. package/docs/eval-runner.md +1 -1
  129. package/docs/evals-internal.md +1 -1
  130. package/docs/evidence-and-memory.md +70 -6
  131. package/docs/evolution.md +1 -1
  132. package/docs/extensions-and-sharing.md +1 -1
  133. package/docs/fleet-dispatch.md +34 -9
  134. package/docs/glossary.md +21 -1
  135. package/docs/installation-and-lifecycle.md +1 -1
  136. package/docs/middleware-and-components.md +1 -1
  137. package/docs/model-catalog.md +1 -1
  138. package/docs/observability.md +2 -2
  139. package/docs/proactive-memory.md +1 -1
  140. package/docs/prompt-envelope-and-tools.md +4 -2
  141. package/docs/provider-adapter-cookbook.md +1 -1
  142. package/docs/release-cut-checklist.md +41 -38
  143. package/docs/safety-model.md +1 -1
  144. package/docs/scientific-validation.md +1 -1
  145. package/docs/skills-marketplace.md +1 -1
  146. package/docs/tool-usage.md +1 -1
  147. package/docs/trace-store.md +1 -1
  148. package/docs/tui-design.md +1 -1
  149. package/docs/worker-dispatch-mechanics.md +1 -1
  150. package/package.json +1 -2
  151. package/src/cli/argv.ts +5 -0
  152. package/src/cli/configure.ts +107 -23
  153. package/src/cli/doctor.ts +5 -1
  154. package/src/cli/evidence.ts +30 -25
  155. package/src/cli/fleet-preflight.ts +2 -12
  156. package/src/cli/shared.ts +1 -0
  157. package/src/cli/targets.ts +4 -1
  158. package/src/cli/validate-model.ts +60 -5
  159. package/src/core/bus-events.ts +25 -0
  160. package/src/core/commit-attribution.ts +4 -4
  161. package/src/core/path-boundary.ts +100 -0
  162. package/src/domains/agents/extension.ts +2 -11
  163. package/src/domains/agents/fleet-contract.ts +30 -12
  164. package/src/domains/agents/recipe.ts +7 -1
  165. package/src/domains/agents/registry.ts +73 -5
  166. package/src/domains/agents/result-contract.ts +128 -17
  167. package/src/domains/agents/write-boundary.ts +15 -50
  168. package/src/domains/context/project-rules.ts +51 -1
  169. package/src/domains/dispatch/assignment-reconcile.ts +22 -5
  170. package/src/domains/dispatch/assignment-store.ts +151 -14
  171. package/src/domains/dispatch/contract.ts +15 -1
  172. package/src/domains/dispatch/delegation-plan.ts +2 -5
  173. package/src/domains/dispatch/execution-role.ts +9 -1
  174. package/src/domains/dispatch/extension.ts +156 -67
  175. package/src/domains/dispatch/fleet-run.ts +57 -3
  176. package/src/domains/dispatch/gate-role-prompts.ts +38 -0
  177. package/src/domains/dispatch/index.ts +3 -1
  178. package/src/domains/dispatch/intent-requirements.ts +40 -0
  179. package/src/domains/dispatch/intent.ts +84 -8
  180. package/src/domains/dispatch/path-scope.ts +370 -0
  181. package/src/domains/dispatch/receipt-integrity.ts +2 -1
  182. package/src/domains/dispatch/types.ts +14 -7
  183. package/src/domains/dispatch/validation.ts +6 -3
  184. package/src/domains/dispatch/write-boundary-enforcer.ts +45 -0
  185. package/src/domains/dispatch/write-boundary.ts +201 -22
  186. package/src/domains/eval/metrics/evidence.ts +79 -2
  187. package/src/domains/eval/runners/clio-run.ts +12 -2
  188. package/src/domains/evidence/build.ts +69 -11
  189. package/src/domains/evidence/index.ts +21 -0
  190. package/src/domains/evidence/provenance.ts +46 -11
  191. package/src/domains/evidence/trust-projection.ts +274 -0
  192. package/src/domains/evidence/trust-status.ts +145 -17
  193. package/src/domains/evidence/types.ts +4 -0
  194. package/src/domains/extensions/discovery.ts +88 -1
  195. package/src/domains/extensions/resources.ts +20 -8
  196. package/src/domains/extensions/state.ts +6 -2
  197. package/src/domains/extensions/types.ts +4 -1
  198. package/src/domains/lifecycle/doctor.ts +140 -1
  199. package/src/domains/prompts/contract.ts +3 -5
  200. package/src/domains/providers/extension.ts +30 -2
  201. package/src/domains/resources/common-loader.ts +3 -0
  202. package/src/domains/resources/prompts/loader.ts +119 -14
  203. package/src/domains/safety/policy-engine.ts +5 -5
  204. package/src/domains/safety/run-effects.ts +64 -1
  205. package/src/domains/safety/scope.ts +7 -12
  206. package/src/engine/acp/server.ts +4 -1
  207. package/src/engine/prompt-templates.ts +18 -1
  208. package/src/engine/worker-runtime.ts +6 -3
  209. package/src/interactive/dispatch-board.ts +53 -3
  210. package/src/interactive/interactive-event-projection.ts +14 -0
  211. package/src/interactive/overlays/settings.ts +141 -47
  212. package/src/interactive/slash-commands.ts +7 -2
  213. package/src/interactive/view/artifacts.ts +42 -9
  214. package/src/interactive/view/view-overlay.ts +15 -3
  215. package/src/interactive/worker-receipts.ts +14 -2
  216. package/src/interactive/worker-stream.ts +8 -0
  217. package/src/tools/dispatch-admission.ts +12 -13
  218. package/src/tools/dispatch-arguments.ts +27 -0
  219. package/src/tools/dispatch-plan.ts +29 -0
  220. package/src/tools/dispatch-runner.ts +48 -13
  221. package/src/tools/monitor.ts +13 -0
  222. package/src/tools/worker-evidence.ts +19 -13
  223. package/src/worker/spec-contract.ts +2 -1
@@ -203,12 +203,62 @@ function ruleMatchesAnyPath(rule: ProjectRule, workingPaths: ReadonlyArray<strin
203
203
  continue;
204
204
  }
205
205
  for (const path of workingPaths) {
206
- if (regex.test(normalizeGlobInput(path))) return true;
206
+ const normalized = normalizeGlobInput(path);
207
+ if (regex.test(normalized)) return true;
208
+ if (normalized.endsWith("/") && globMatchesInsideDirectory(regex, pattern, normalized)) return true;
207
209
  }
208
210
  }
209
211
  return false;
210
212
  }
211
213
 
214
+ function materializedGlobVariants(pattern: string): string[] {
215
+ let variants = [""];
216
+ const normalized = normalizeGlobInput(pattern);
217
+ for (let index = 0; index < normalized.length; index += 1) {
218
+ const character = normalized[index];
219
+ if (character === undefined) break;
220
+ if (character === "*" && normalized[index + 1] === "*" && normalized[index + 2] === "/") {
221
+ variants = variants.flatMap((prefix) => [prefix, `${prefix}x/`]).slice(0, 64);
222
+ index += 2;
223
+ continue;
224
+ }
225
+ if (character === "*" && normalized[index + 1] === "*") {
226
+ variants = variants.map((prefix) => `${prefix}x`);
227
+ index += 1;
228
+ continue;
229
+ }
230
+ if (character === "*" || character === "?") {
231
+ variants = variants.map((prefix) => `${prefix}x`);
232
+ continue;
233
+ }
234
+ if (character === "[") {
235
+ const end = normalized.indexOf("]", index + 1);
236
+ const selected = end === -1 ? "x" : (normalized[index + 1] ?? "x");
237
+ variants = variants.map((prefix) => `${prefix}${selected}`);
238
+ index = end === -1 ? index : end;
239
+ continue;
240
+ }
241
+ variants = variants.map((prefix) => `${prefix}${character}`);
242
+ }
243
+ return variants;
244
+ }
245
+
246
+ function globMatchesInsideDirectory(regex: RegExp, pattern: string, directory: string): boolean {
247
+ const candidates = new Set<string>([`${directory}x`, `${directory}x/x`]);
248
+ for (const materialized of materializedGlobVariants(pattern)) {
249
+ if (materialized.startsWith(directory)) candidates.add(materialized);
250
+ const segments = materialized.split("/");
251
+ for (let index = 0; index < segments.length; index += 1) {
252
+ const suffix = segments.slice(index).join("/");
253
+ if (suffix.length > 0) candidates.add(`${directory}${suffix}`);
254
+ }
255
+ for (let index = 0; index < materialized.length; index += 1) {
256
+ candidates.add(`${directory}${materialized.slice(index)}`);
257
+ }
258
+ }
259
+ return [...candidates].some((candidate) => regex.test(candidate));
260
+ }
261
+
212
262
  /**
213
263
  * Select the rules that should load now. Unconditional rules (no `paths`) always
214
264
  * load; a path-scoped rule loads only when one of its globs matches a path
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Startup reconciliation for durable assignments. The in-memory retry queue and
3
- * assignment registry do not survive a restart (Symphony §14.3), so every
4
- * durable record still marked `running` after a crash is orphaned: no active
5
- * attempt, no queued retry, and no attached terminal promise can ever settle it.
3
+ * assignment registry do not survive a restart (Symphony §14.3), so a durable
4
+ * record whose process owner is gone is orphaned: no active attempt, no queued
5
+ * retry, and no attached terminal promise can ever settle it. Running records
6
+ * owned by a live sibling process are deliberately left alone.
6
7
  *
7
8
  * This pass resolves each orphan deterministically against durable ledger
8
9
  * evidence so a later `wait`/`collect` can never observe a stuck `running`
@@ -11,7 +12,7 @@
11
12
  */
12
13
 
13
14
  import type { AssignmentStatus } from "./assignment.js";
14
- import type { DurableAssignmentRecord } from "./assignment-store.js";
15
+ import type { AssignmentVerdictOwner, DurableAssignmentRecord } from "./assignment-store.js";
15
16
 
16
17
  /** Minimal terminal view of one attempt's durable ledger row. */
17
18
  export interface ReconcileAttemptView {
@@ -25,13 +26,16 @@ export interface ReconcileAttemptView {
25
26
  export interface AssignmentReconcileDeps {
26
27
  /** Durable records currently marked `running`. */
27
28
  listRunning: () => ReadonlyArray<DurableAssignmentRecord>;
29
+ /** Decide liveness from the record's durable pid and process birth token. */
30
+ ownerAlive: (record: DurableAssignmentRecord) => boolean;
28
31
  /** Resolve one attempt's terminal view from the ledger, or null if pruned. */
29
32
  lookupAttempt: (runId: string) => ReconcileAttemptView | null;
30
- /** Persist the reconciled terminal status. */
33
+ /** Persist the reconciled terminal status, as the record's verdict owner when it has one. */
31
34
  settle: (
32
35
  assignmentId: string,
33
36
  terminalRunId: string,
34
37
  status: Exclude<AssignmentStatus, "running">,
38
+ owner?: AssignmentVerdictOwner,
35
39
  ) => Promise<unknown>;
36
40
  }
37
41
 
@@ -44,6 +48,10 @@ export interface AssignmentReconcileSummary {
44
48
 
45
49
  /**
46
50
  * Deterministic rule, in priority order:
51
+ * -1. a record owned by a live process is not an orphan and remains running;
52
+ * 0. a record whose verdict belongs to its opener is failed (abandoned): the
53
+ * opener died before reaching a verdict, and a green attempt under it is
54
+ * one step of a run, never the run's answer;
47
55
  * 1. a recovered succeeded terminal attempt wins (`succeeded`);
48
56
  * 2. otherwise the last terminal attempt marks the assignment `failed`;
49
57
  * 3. otherwise no attempt is recoverable and the assignment is failed
@@ -53,6 +61,15 @@ export async function reconcileOrphanAssignments(deps: AssignmentReconcileDeps):
53
61
  const summary: AssignmentReconcileSummary = { recovered: 0, abandoned: 0 };
54
62
  for (const record of deps.listRunning()) {
55
63
  if (record.status !== "running") continue;
64
+ if (deps.ownerAlive(record)) continue;
65
+ if (record.verdictOwner !== undefined) {
66
+ // Attempts under a claimed row are fleet steps, not the fleet verdict.
67
+ // Filing a green step as the terminal id of an abandoned fleet would
68
+ // contradict that step's immutable ledger result.
69
+ await deps.settle(record.assignmentId, record.assignmentId, "failed", record.verdictOwner);
70
+ summary.abandoned += 1;
71
+ continue;
72
+ }
56
73
  const views = record.attempts
57
74
  .map((runId) => deps.lookupAttempt(runId))
58
75
  .filter((view): view is ReconcileAttemptView => view !== null);
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { existsSync, readFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
+ import { BIRTH_TOKEN_SOURCE_AVAILABLE, processAlive, processBirthToken } from "../../core/process-identity.js";
8
9
  import { withStateFileLock } from "../../core/state-file-lock.js";
9
10
  import { clioStateDir } from "../../core/xdg.js";
10
11
  import { atomicWrite } from "../../engine/session.js";
@@ -12,11 +13,54 @@ import type { AssignmentStatus } from "./assignment.js";
12
13
 
13
14
  const MAX_ASSIGNMENT_RECORDS = 1_000;
14
15
 
16
+ /**
17
+ * Who decides a record's status.
18
+ *
19
+ * Absent is the ordinary case: the record is one logical dispatch, so the
20
+ * attempts filed under it are the only thing that can settle it. A fleet claims
21
+ * the verdict instead, because a fleet root gathers every step of a run under
22
+ * one id and no single step is the run's answer. Without the claim the last
23
+ * step to settle writes the row, which reports a green step of an aborted run
24
+ * as the run's outcome.
25
+ */
26
+ export type AssignmentVerdictOwner = "fleet";
27
+
28
+ /**
29
+ * Durable identity of the process executing a running assignment. A pid alone
30
+ * is not enough: the OS may recycle it after a crash, so the process birth
31
+ * token must still match before another extension treats the owner as live.
32
+ */
33
+ export interface AssignmentProcessOwner {
34
+ pid: number;
35
+ processBirthToken: string;
36
+ acquiredAt: string;
37
+ }
38
+
39
+ export interface AssignmentProcessOwnerProbe {
40
+ birthToken(pid: number): string | null;
41
+ /** True when a missing birth token proves that the process is gone. */
42
+ tokenProvesDeath?: boolean;
43
+ /** Liveness fallback for platforms whose birth token is synthetic. */
44
+ alive?(pid: number): boolean;
45
+ }
46
+
47
+ /**
48
+ * One durable logical-dispatch row.
49
+ *
50
+ * `attempts` holds terminal run ids: an agent attempt's receipt id, and for a
51
+ * fleet also each deterministic step's `code-*` run id, whose evidence lives
52
+ * under `code-steps/<assignmentId>/`. `status` answers for the whole record,
53
+ * so for a fleet it answers for the whole run rather than for its last step.
54
+ */
15
55
  export interface DurableAssignmentRecord {
16
56
  assignmentId: string;
17
57
  attempts: string[];
18
58
  terminalRunId: string | null;
19
59
  status: AssignmentStatus;
60
+ /** Set when something other than the attempts decides `status`. */
61
+ verdictOwner?: AssignmentVerdictOwner;
62
+ /** Process lease held only while this record is running. */
63
+ processOwner?: AssignmentProcessOwner;
20
64
  }
21
65
 
22
66
  interface AssignmentStoreFile {
@@ -28,6 +72,18 @@ function storePath(): string {
28
72
  return join(clioStateDir(), "assignments.json");
29
73
  }
30
74
 
75
+ function validProcessOwner(value: unknown): value is AssignmentProcessOwner {
76
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
77
+ const owner = value as Partial<AssignmentProcessOwner>;
78
+ return (
79
+ Number.isSafeInteger(owner.pid) &&
80
+ (owner.pid ?? 0) > 0 &&
81
+ typeof owner.processBirthToken === "string" &&
82
+ typeof owner.acquiredAt === "string" &&
83
+ Number.isFinite(Date.parse(owner.acquiredAt))
84
+ );
85
+ }
86
+
31
87
  function validRecord(value: unknown): value is DurableAssignmentRecord {
32
88
  if (typeof value !== "object" || value === null) return false;
33
89
  const record = value as Partial<DurableAssignmentRecord>;
@@ -36,6 +92,8 @@ function validRecord(value: unknown): value is DurableAssignmentRecord {
36
92
  Array.isArray(record.attempts) &&
37
93
  record.attempts.every((runId) => typeof runId === "string") &&
38
94
  (record.terminalRunId === null || typeof record.terminalRunId === "string") &&
95
+ (record.verdictOwner === undefined || record.verdictOwner === "fleet") &&
96
+ (record.processOwner === undefined || validProcessOwner(record.processOwner)) &&
39
97
  (record.status === "running" ||
40
98
  record.status === "succeeded" ||
41
99
  record.status === "failed" ||
@@ -57,7 +115,32 @@ function readStore(): DurableAssignmentRecord[] {
57
115
  }
58
116
 
59
117
  function copyRecord(record: DurableAssignmentRecord): DurableAssignmentRecord {
60
- return { ...record, attempts: [...record.attempts] };
118
+ return {
119
+ ...record,
120
+ attempts: [...record.attempts],
121
+ ...(record.processOwner === undefined ? {} : { processOwner: { ...record.processOwner } }),
122
+ };
123
+ }
124
+
125
+ const defaultProcessOwnerProbe: AssignmentProcessOwnerProbe = {
126
+ birthToken: processBirthToken,
127
+ tokenProvesDeath: BIRTH_TOKEN_SOURCE_AVAILABLE,
128
+ alive: processAlive,
129
+ };
130
+
131
+ /** True only when the durable identity still names the same live process. */
132
+ export function assignmentProcessOwnerAlive(
133
+ record: Pick<DurableAssignmentRecord, "processOwner">,
134
+ probe: AssignmentProcessOwnerProbe = defaultProcessOwnerProbe,
135
+ ): boolean {
136
+ const owner = record.processOwner;
137
+ if (owner === undefined) return false;
138
+ const current = probe.birthToken(owner.pid);
139
+ if (current === null || current !== owner.processBirthToken) return false;
140
+ // A synthetic `pid-N` token cannot distinguish pid reuse, so require an
141
+ // independent liveness signal on platforms without a real birth-token source.
142
+ if (probe.tokenProvesDeath === false) return probe.alive?.(owner.pid) ?? false;
143
+ return true;
61
144
  }
62
145
 
63
146
  function writeStore(assignments: ReadonlyArray<DurableAssignmentRecord>): void {
@@ -114,49 +197,103 @@ export async function renameStoredAssignment(
114
197
  return copyRecord(result);
115
198
  }
116
199
 
117
- export function registerAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
118
- return updateRecord(
200
+ function currentProcessOwner(): AssignmentProcessOwner {
201
+ const token = processBirthToken();
202
+ if (token === null) throw new Error("dispatch: cannot establish assignment owner process birth token");
203
+ return {
204
+ pid: process.pid,
205
+ processBirthToken: token,
206
+ acquiredAt: new Date().toISOString(),
207
+ };
208
+ }
209
+
210
+ function openRecord(assignmentId: string): DurableAssignmentRecord {
211
+ return {
119
212
  assignmentId,
120
- (current) => current ?? { assignmentId, attempts: [], terminalRunId: null, status: "running" },
121
- );
213
+ attempts: [],
214
+ terminalRunId: null,
215
+ status: "running",
216
+ processOwner: currentProcessOwner(),
217
+ };
218
+ }
219
+
220
+ function ensureRunningProcessOwner(record: DurableAssignmentRecord): DurableAssignmentRecord {
221
+ if (record.status !== "running" || record.processOwner !== undefined) return record;
222
+ return { ...record, processOwner: currentProcessOwner() };
223
+ }
224
+
225
+ function clearProcessOwner(record: DurableAssignmentRecord): DurableAssignmentRecord {
226
+ const result = { ...record };
227
+ delete result.processOwner;
228
+ return result;
229
+ }
230
+
231
+ export function registerAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
232
+ return updateRecord(assignmentId, (current) => ensureRunningProcessOwner(current ?? openRecord(assignmentId)));
233
+ }
234
+
235
+ /**
236
+ * Reserve this record's status for `owner`, opening the record if it is new.
237
+ * Attempts filed by anyone are still recorded; only the verdict is reserved,
238
+ * and until the owner settles it the record stays `running`.
239
+ */
240
+ export function claimAssignmentVerdict(
241
+ assignmentId: string,
242
+ owner: AssignmentVerdictOwner,
243
+ ): Promise<DurableAssignmentRecord> {
244
+ return updateRecord(assignmentId, (current) => ({
245
+ ...ensureRunningProcessOwner(current ?? openRecord(assignmentId)),
246
+ verdictOwner: owner,
247
+ }));
122
248
  }
123
249
 
124
250
  export function recordAssignmentAttempt(assignmentId: string, runId: string): Promise<DurableAssignmentRecord> {
125
251
  return updateRecord(assignmentId, (current) => {
126
- const base = current ?? { assignmentId, attempts: [], terminalRunId: null, status: "running" as const };
252
+ const base = ensureRunningProcessOwner(current ?? openRecord(assignmentId));
127
253
  return base.attempts.includes(runId) ? base : { ...base, attempts: [...base.attempts, runId] };
128
254
  });
129
255
  }
130
256
 
131
257
  export function failQueuedAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
132
258
  return updateRecord(assignmentId, (current) => {
133
- const base = current ?? { assignmentId, attempts: [], terminalRunId: null, status: "running" as const };
134
- return base.status === "running" ? { ...base, status: "failed" } : base;
259
+ const base = current ?? openRecord(assignmentId);
260
+ if (base.verdictOwner !== undefined) return base;
261
+ return base.status === "running" ? clearProcessOwner({ ...base, status: "failed" }) : base;
135
262
  });
136
263
  }
137
264
 
138
265
  export function timeoutStoredAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
139
266
  return updateRecord(assignmentId, (current) => {
140
- const base = current ?? { assignmentId, attempts: [], terminalRunId: null, status: "running" as const };
141
- return base.status === "running" ? { ...base, status: "timed_out" } : base;
267
+ const base = current ?? openRecord(assignmentId);
268
+ if (base.verdictOwner !== undefined) return base;
269
+ return base.status === "running" ? clearProcessOwner({ ...base, status: "timed_out" }) : base;
142
270
  });
143
271
  }
144
272
 
145
273
  export function cancelStoredAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
146
274
  return updateRecord(assignmentId, (current) => {
147
- const base = current ?? { assignmentId, attempts: [], terminalRunId: null, status: "running" as const };
148
- return base.status === "running" ? { ...base, status: "canceled" } : base;
275
+ const base = current ?? openRecord(assignmentId);
276
+ if (base.verdictOwner !== undefined) return base;
277
+ return base.status === "running" ? clearProcessOwner({ ...base, status: "canceled" }) : base;
149
278
  });
150
279
  }
151
280
 
281
+ /**
282
+ * File `terminalRunId` as an attempt and, when the caller owns the verdict, set
283
+ * the record's status. An attempt settling under a claimed record settles
284
+ * itself and not the record: one fleet step reaching a green receipt says
285
+ * nothing about the six steps behind it that never ran.
286
+ */
152
287
  export function settleStoredAssignment(
153
288
  assignmentId: string,
154
289
  terminalRunId: string,
155
290
  status: Exclude<AssignmentStatus, "running">,
291
+ owner?: AssignmentVerdictOwner,
156
292
  ): Promise<DurableAssignmentRecord> {
157
293
  return updateRecord(assignmentId, (current) => {
158
- const base = current ?? { assignmentId, attempts: [], terminalRunId: null, status: "running" as const };
294
+ const base = current ?? openRecord(assignmentId);
159
295
  const attempts = base.attempts.includes(terminalRunId) ? base.attempts : [...base.attempts, terminalRunId];
160
- return { ...base, attempts, terminalRunId, status };
296
+ if (base.verdictOwner !== owner) return { ...base, attempts };
297
+ return clearProcessOwner({ ...base, attempts, terminalRunId, status });
161
298
  });
162
299
  }
@@ -240,7 +240,7 @@ export interface DispatchContract {
240
240
  sealCouncilSynthesis?(input: {
241
241
  group: string;
242
242
  round: number;
243
- kind: "none" | "vote";
243
+ kind: "none" | "vote" | "judge";
244
244
  text: string;
245
245
  subjects: ReadonlyArray<{ runId: string; digest: string | null }>;
246
246
  template: RunReceipt;
@@ -252,6 +252,20 @@ export interface DispatchContract {
252
252
  /** Get a specific immutable attempt envelope. */
253
253
  getRun(runId: string): RunEnvelope | null;
254
254
 
255
+ /**
256
+ * Absolute paths this run's own successful tool calls aimed a mutation at,
257
+ * or null when the run kept no usable record: an unknown or evicted run, a
258
+ * runtime that publishes no tool telemetry, telemetry that came back
259
+ * incomplete, or a run that called a tool able to write a path its arguments
260
+ * do not name. Null is not an empty list, and a caller that cannot tell them
261
+ * apart will read "wrote nothing" off a run nobody watched.
262
+ *
263
+ * The write boundary reads this to separate what a step changed from what
264
+ * changed under it, so the answer must stay a record of observation rather
265
+ * than an inference from the checkout.
266
+ */
267
+ observedRunWrites?(runId: string): ReadonlyArray<string> | null;
268
+
255
269
  /** In-memory logical assignment state and complete finalized-attempt history. */
256
270
  readonly assignments?: {
257
271
  /** Live assignment, including its terminal promise and detailed attempt refs. */
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { pathBoundaryCovers } from "../../core/path-boundary.js";
2
3
  import { normalizeWriteBoundary } from "../agents/write-boundary.js";
3
4
 
4
5
  export type DelegationTaskMode = "sequential" | "parallel";
@@ -54,10 +55,6 @@ export function buildDelegationProposalBriefing(proposals: ReadonlyArray<{ agent
54
55
 
55
56
  const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
56
57
 
57
- function insideBoundary(path: string, boundary: ReadonlyArray<string>): boolean {
58
- return boundary.some((root) => (root.endsWith("/") ? path.startsWith(root) : path === root));
59
- }
60
-
61
58
  function malformed(detail: string): DelegationPlanValidation {
62
59
  return { ok: false, reason: "delegation_plan_malformed", detail };
63
60
  }
@@ -116,7 +113,7 @@ export function validateDelegationPlan(input: ValidateDelegationPlanInput): Dele
116
113
  } catch (error) {
117
114
  return malformed(`task '${task.id}' has invalid writes: ${error instanceof Error ? error.message : String(error)}`);
118
115
  }
119
- const outside = writes.find((path) => !insideBoundary(path, input.writes));
116
+ const outside = writes.find((path) => !pathBoundaryCovers(input.writes, path));
120
117
  if (outside !== undefined) {
121
118
  return {
122
119
  ok: false,
@@ -172,9 +172,17 @@ export function gateDeciderAgentId(requested: string | undefined): string {
172
172
  * the slot. The gate validates that answer and fails closed on a malformed one,
173
173
  * so also enforcing an inapplicable recipe postcondition would fail the run
174
174
  * before its verdict could be read.
175
+ *
176
+ * `synthesis` is the third decider and belongs here for the same reason.
177
+ * {@link deriveExecutionRole} already resolves it to the `judge` role, and
178
+ * `isBoundedGateRolePrompt` admits it only under the council judge prompt,
179
+ * which asks for `{"verdict","text"}`. A council seats the read-only
180
+ * `researcher` by default, whose recipe declares `research-report`, so leaving
181
+ * the recipe postcondition on the slot sealed a contract failure on every
182
+ * correct synthesis and burned the configured retries reproducing it.
175
183
  */
176
184
  export function appliesRecipeResultContract(gateRole: GateTopologyRole | undefined): boolean {
177
- return gateRole !== "reviewer" && gateRole !== "judge";
185
+ return gateRole !== "reviewer" && gateRole !== "judge" && gateRole !== "synthesis";
178
186
  }
179
187
 
180
188
  /** The route dimensions a gate correlation is measured across. */