@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
@@ -593,6 +593,13 @@ function delegationAgentsAffordance(
593
593
  type SubmenuTitle = string | ((width: number) => string);
594
594
 
595
595
  export class SubmenuWrapper implements Component {
596
+ /**
597
+ * Why the last submission was refused, or null while nothing is wrong. The
598
+ * editor sets it instead of closing, so the operator corrects the value with
599
+ * the reason on screen rather than back at the row list with no reason.
600
+ */
601
+ private problem: string | null = null;
602
+
596
603
  constructor(
597
604
  private readonly title: SubmenuTitle,
598
605
  private readonly child: Component,
@@ -600,6 +607,10 @@ export class SubmenuWrapper implements Component {
600
607
  private readonly note?: string,
601
608
  ) {}
602
609
 
610
+ setProblem(problem: string | null): void {
611
+ this.problem = problem;
612
+ }
613
+
603
614
  render(width: number): string[] {
604
615
  const theme = clioTheme();
605
616
  const lines: string[] = [];
@@ -619,6 +630,11 @@ export class SubmenuWrapper implements Component {
619
630
  // two columns off each one and marked the cut, so every entry wore a
620
631
  // trailing … it had not earned.
621
632
  lines.push(...this.child.render(Math.max(1, width - 2)).map((line) => ` ${line}`));
633
+ if (this.problem !== null) {
634
+ for (const line of wrapTextWithAnsi(this.problem, Math.max(1, width - 2))) {
635
+ lines.push(theme.fg("error", ` ${line}`));
636
+ }
637
+ }
622
638
  lines.push("");
623
639
  lines.push(theme.fg("dim", ` ${this.hint}`));
624
640
  return lines;
@@ -693,33 +709,108 @@ function editTextSubmenu(title: string, note?: string): SettingSubmenuBuilder {
693
709
  return textInputSubmenu(title, note);
694
710
  }
695
711
 
696
- function editNumberSubmenu(title: string, options: { min?: number; allowBlank?: boolean } = {}): SettingSubmenuBuilder {
697
- const min = options.min ?? 0;
712
+ /**
713
+ * The bound a number row enforces, shared by the editor and the apply path so
714
+ * the two cannot drift: whatever the editor forwards, the apply path stores.
715
+ * The bounds mirror the config validator's, so a value the Settings Center
716
+ * refuses is one settings.yaml would have refused too, in the same words.
717
+ */
718
+ interface NumberSettingRule {
719
+ readonly min: number;
720
+ readonly max?: number;
721
+ readonly integer: boolean;
722
+ /** A blank submission clears the key instead of being a parse failure. */
723
+ readonly allowBlank?: boolean;
724
+ }
725
+
726
+ const NUMBER_SETTING_RULES = {
727
+ "budget.sessionCeilingUsd": { min: 0, integer: false },
728
+ "watchdog.cadenceToolCalls": { min: 1, integer: true, allowBlank: true },
729
+ "delegation.defaults.connectTimeoutMs": { min: 1, max: MAX_TIMER_DELAY_MS, integer: true },
730
+ "delegation.defaults.turnTimeoutMs": { min: 1, max: MAX_TIMER_DELAY_MS, integer: true },
731
+ "delegation.defaults.permissionTimeoutMs": { min: 1, max: MAX_TIMER_DELAY_MS, integer: true },
732
+ } as const satisfies Partial<Record<EditableSettingId, NumberSettingRule>>;
733
+
734
+ export type NumberSettingId = keyof typeof NUMBER_SETTING_RULES;
735
+
736
+ export const NUMBER_SETTING_IDS = Object.keys(NUMBER_SETTING_RULES) as readonly NumberSettingId[];
737
+
738
+ type NumberSettingOutcome = { readonly value: number | null } | { readonly refusal: string };
739
+
740
+ function describeSubmittedText(value: string): string {
741
+ const trimmed = value.trim();
742
+ return trimmed.length === 0 ? "an empty string" : JSON.stringify(trimmed);
743
+ }
744
+
745
+ /**
746
+ * Parse one submitted number-row value under its rule. A null value means the
747
+ * operator cleared an optional key. The refusal is worded like the config
748
+ * validator's issue for the same key ("expected an integer >= 1, got 0"), with
749
+ * a "Not applied:" prefix so it reads as the outcome of this submission.
750
+ */
751
+ function parseNumberSetting(value: string, rule: NumberSettingRule): NumberSettingOutcome {
752
+ const trimmed = value.trim();
753
+ const noun = rule.integer ? "an integer" : "a number";
754
+ if (trimmed.length === 0 && rule.allowBlank === true) return { value: null };
755
+ const parsed = trimmed.length === 0 ? Number.NaN : Number(trimmed);
756
+ if (!Number.isFinite(parsed) || (rule.integer && !Number.isInteger(parsed))) {
757
+ const shown = Number.isFinite(parsed) ? String(parsed) : describeSubmittedText(value);
758
+ return { refusal: `Not applied: expected ${noun}, got ${shown}.` };
759
+ }
760
+ if (parsed < rule.min) return { refusal: `Not applied: expected ${noun} >= ${rule.min}, got ${parsed}.` };
761
+ if (rule.max !== undefined && parsed > rule.max) {
762
+ return { refusal: `Not applied: expected ${noun} <= ${rule.max}, got ${parsed}.` };
763
+ }
764
+ return { value: parsed };
765
+ }
766
+
767
+ /**
768
+ * The reason a number row would refuse `value`, or null when it would apply.
769
+ * This is the same check the editor and `applySettingChange` run, exposed so a
770
+ * caller that bypasses the editor can still name the bound it hit.
771
+ */
772
+ export function describeNumberSettingRefusal(id: NumberSettingId, value: string): string | null {
773
+ const outcome = parseNumberSetting(value, NUMBER_SETTING_RULES[id]);
774
+ return "refusal" in outcome ? outcome.refusal : null;
775
+ }
776
+
777
+ function numberSettingNote(rule: NumberSettingRule): string {
778
+ const noun = rule.integer ? "a whole number" : "a number";
779
+ const clears = rule.allowBlank === true ? "; blank clears it" : "";
780
+ if (rule.min > 0 && rule.max !== undefined) return `Use ${noun} from ${rule.min} to ${rule.max}${clears}.`;
781
+ if (rule.min > 0) return `Use ${noun} of at least ${rule.min}${clears}.`;
782
+ return `Use a non-negative ${rule.integer ? "whole number" : "number"}${clears}.`;
783
+ }
784
+
785
+ /**
786
+ * A number editor that stays open on a refused submission. Closing with no
787
+ * value looked like a successful edit that changed nothing, so the reason is
788
+ * rendered under the input and the operator corrects the text in place; Esc
789
+ * still leaves without applying.
790
+ */
791
+ function editNumberSubmenu(title: string, id: NumberSettingId): SettingSubmenuBuilder {
792
+ const rule = NUMBER_SETTING_RULES[id];
698
793
  return (currentValue: string, done: (val?: string) => void) => {
699
794
  const input = new Input();
700
795
  input.setValue(currentValue);
701
796
  input.focused = true;
797
+ const wrapper = new SubmenuWrapper(
798
+ title,
799
+ input,
800
+ buildHint([{ key: "Enter", verb: "confirm" }], "back"),
801
+ numberSettingNote(rule),
802
+ );
702
803
  input.onSubmit = (val) => {
703
- if (options.allowBlank === true && val.trim().length === 0) {
704
- done("");
804
+ const outcome = parseNumberSetting(val, rule);
805
+ if ("refusal" in outcome) {
806
+ wrapper.setProblem(outcome.refusal);
705
807
  return;
706
808
  }
707
- const num = Number(val);
708
- if (Number.isFinite(num) && num >= min) {
709
- done(val);
710
- } else {
711
- done();
712
- }
809
+ wrapper.setProblem(null);
810
+ done(outcome.value === null ? "" : val.trim());
713
811
  };
714
812
  input.onEscape = () => done();
715
- return new SubmenuWrapper(
716
- title,
717
- input,
718
- buildHint([{ key: "Enter", verb: "confirm" }], "back"),
719
- min > 0
720
- ? `Use a whole number of at least ${min}${options.allowBlank === true ? "; blank clears it" : ""}.`
721
- : "Use a non-negative number.",
722
- );
813
+ return wrapper;
723
814
  };
724
815
  }
725
816
 
@@ -1412,7 +1503,7 @@ export function buildSettingItems(
1412
1503
  readOnly: true,
1413
1504
  }),
1414
1505
  settingItem("budget.sessionCeilingUsd", String(settings.budget.sessionCeilingUsd), {
1415
- submenu: editNumberSubmenu("Edit session cost ceiling USD"),
1506
+ submenu: editNumberSubmenu("Edit session cost ceiling USD", "budget.sessionCeilingUsd"),
1416
1507
  affordance: "free text",
1417
1508
  }),
1418
1509
  settingItem("defaults.maxTokens", String(settings.defaults.maxTokens), {
@@ -1479,10 +1570,10 @@ export function buildSettingItems(
1479
1570
  "watchdog.cadenceToolCalls",
1480
1571
  watchdog.cadenceToolCalls === undefined ? "(turn end only)" : String(watchdog.cadenceToolCalls),
1481
1572
  {
1482
- submenu: editNumberSubmenu("Edit watchdog cadence in tool calls; blank fires at turn end only", {
1483
- min: 1,
1484
- allowBlank: true,
1485
- }),
1573
+ submenu: editNumberSubmenu(
1574
+ "Edit watchdog cadence in tool calls; blank fires at turn end only",
1575
+ "watchdog.cadenceToolCalls",
1576
+ ),
1486
1577
  affordance: "free text",
1487
1578
  editValue: watchdog.cadenceToolCalls === undefined ? "" : String(watchdog.cadenceToolCalls),
1488
1579
  },
@@ -1503,15 +1594,15 @@ export function buildSettingItems(
1503
1594
  affordance: "free text",
1504
1595
  }),
1505
1596
  settingItem("delegation.defaults.connectTimeoutMs", String(settings.delegation.defaults.connectTimeoutMs), {
1506
- submenu: editNumberSubmenu("Edit delegate connect timeout (ms)"),
1597
+ submenu: editNumberSubmenu("Edit delegate connect timeout (ms)", "delegation.defaults.connectTimeoutMs"),
1507
1598
  affordance: "free text",
1508
1599
  }),
1509
1600
  settingItem("delegation.defaults.turnTimeoutMs", String(settings.delegation.defaults.turnTimeoutMs), {
1510
- submenu: editNumberSubmenu("Edit delegate turn timeout (ms)"),
1601
+ submenu: editNumberSubmenu("Edit delegate turn timeout (ms)", "delegation.defaults.turnTimeoutMs"),
1511
1602
  affordance: "free text",
1512
1603
  }),
1513
1604
  settingItem("delegation.defaults.permissionTimeoutMs", String(settings.delegation.defaults.permissionTimeoutMs), {
1514
- submenu: editNumberSubmenu("Edit delegate permission timeout (ms)"),
1605
+ submenu: editNumberSubmenu("Edit delegate permission timeout (ms)", "delegation.defaults.permissionTimeoutMs"),
1515
1606
  affordance: "free text",
1516
1607
  }),
1517
1608
  settingItem("keybindings", keybindingCount > 0 ? `${keybindingCount} override(s)` : "(defaults)", {
@@ -1819,9 +1910,15 @@ function applyNonNegativeInteger(value: string, set: (next: number) => void): vo
1819
1910
  if (Number.isFinite(parsed) && parsed >= 0) set(Math.floor(parsed));
1820
1911
  }
1821
1912
 
1822
- function applyPositiveInteger(value: string, set: (next: number) => void): void {
1823
- const parsed = Number(value);
1824
- if (Number.isInteger(parsed) && parsed >= 1 && parsed <= MAX_TIMER_DELAY_MS) set(parsed);
1913
+ /**
1914
+ * Store a number-row submission under the row's shared rule. A refused value
1915
+ * leaves the setting alone; the editor has already shown the operator why, and
1916
+ * `describeNumberSettingRefusal` names the reason for any other caller.
1917
+ */
1918
+ function applyNumberSetting(id: NumberSettingId, value: string, set: (next: number | null) => void): void {
1919
+ const outcome = parseNumberSetting(value, NUMBER_SETTING_RULES[id]);
1920
+ if ("refusal" in outcome) return;
1921
+ set(outcome.value);
1825
1922
  }
1826
1923
 
1827
1924
  /**
@@ -1986,15 +2083,12 @@ export function applySettingChange(settings: ClioSettings, id: string, value: st
1986
2083
  else delete settings.watchdog.target;
1987
2084
  return;
1988
2085
  }
1989
- case "watchdog.cadenceToolCalls": {
1990
- if (value.trim().length === 0) {
1991
- delete settings.watchdog.cadenceToolCalls;
1992
- return;
1993
- }
1994
- const parsed = Number(value);
1995
- if (Number.isFinite(parsed) && Math.floor(parsed) >= 1) settings.watchdog.cadenceToolCalls = Math.floor(parsed);
2086
+ case "watchdog.cadenceToolCalls":
2087
+ applyNumberSetting("watchdog.cadenceToolCalls", value, (next) => {
2088
+ if (next === null) delete settings.watchdog.cadenceToolCalls;
2089
+ else settings.watchdog.cadenceToolCalls = next;
2090
+ });
1996
2091
  return;
1997
- }
1998
2092
  case "terminal.smoothStreaming":
1999
2093
  if (value === "off" || value === "auto" || value === "on") settings.terminal.smoothStreaming = value;
2000
2094
  return;
@@ -2005,18 +2099,18 @@ export function applySettingChange(settings: ClioSettings, id: string, value: st
2005
2099
  .filter(Boolean);
2006
2100
  return;
2007
2101
  case "delegation.defaults.connectTimeoutMs":
2008
- applyPositiveInteger(value, (next) => {
2009
- settings.delegation.defaults.connectTimeoutMs = next;
2102
+ applyNumberSetting("delegation.defaults.connectTimeoutMs", value, (next) => {
2103
+ if (next !== null) settings.delegation.defaults.connectTimeoutMs = next;
2010
2104
  });
2011
2105
  return;
2012
2106
  case "delegation.defaults.turnTimeoutMs":
2013
- applyPositiveInteger(value, (next) => {
2014
- settings.delegation.defaults.turnTimeoutMs = next;
2107
+ applyNumberSetting("delegation.defaults.turnTimeoutMs", value, (next) => {
2108
+ if (next !== null) settings.delegation.defaults.turnTimeoutMs = next;
2015
2109
  });
2016
2110
  return;
2017
2111
  case "delegation.defaults.permissionTimeoutMs":
2018
- applyPositiveInteger(value, (next) => {
2019
- settings.delegation.defaults.permissionTimeoutMs = next;
2112
+ applyNumberSetting("delegation.defaults.permissionTimeoutMs", value, (next) => {
2113
+ if (next !== null) settings.delegation.defaults.permissionTimeoutMs = next;
2020
2114
  });
2021
2115
  return;
2022
2116
  case "orchestrator.target": {
@@ -2067,11 +2161,11 @@ export function applySettingChange(settings: ClioSettings, id: string, value: st
2067
2161
  .map((v) => v.trim())
2068
2162
  .filter(Boolean);
2069
2163
  return;
2070
- case "budget.sessionCeilingUsd": {
2071
- const parsed = Number(value);
2072
- if (Number.isFinite(parsed) && parsed >= 0) settings.budget.sessionCeilingUsd = parsed;
2164
+ case "budget.sessionCeilingUsd":
2165
+ applyNumberSetting("budget.sessionCeilingUsd", value, (next) => {
2166
+ if (next !== null) settings.budget.sessionCeilingUsd = next;
2167
+ });
2073
2168
  return;
2074
- }
2075
2169
  }
2076
2170
  }
2077
2171
 
@@ -6,9 +6,9 @@ import type { AgentSpec } from "../domains/agents/spec.js";
6
6
  import type { ContextInitOptions } from "../domains/context/init-options.js";
7
7
  import type { DispatchContract, DispatchRequest } from "../domains/dispatch/contract.js";
8
8
  import { type AgentRoleFactsResolver, requestExecutionRole } from "../domains/dispatch/execution-role.js";
9
- import type { ReceiptIntegrityResult } from "../domains/dispatch/receipt-integrity.js";
10
9
  import type { RunReceipt } from "../domains/dispatch/types.js";
11
10
  import type { JobThinkingLevel } from "../domains/dispatch/validation.js";
11
+ import type { ReceiptIntegrityOutcome } from "../domains/evidence/trust-status.js";
12
12
  import type { InstalledExtension } from "../domains/extensions/index.js";
13
13
  import type { InteropAgentId, InteropProposal, InteropReport } from "../domains/interop/index.js";
14
14
  import type { ProvidersContract, ResolvedModelRef } from "../domains/providers/index.js";
@@ -665,7 +665,7 @@ export interface SlashCommandContext {
665
665
  * and emit a single status line. Kept on the context so the registry does
666
666
  * not import the overlay module.
667
667
  */
668
- verifyReceipt: (runId: string) => ReceiptIntegrityResult;
668
+ verifyReceipt: (runId: string) => ReceiptIntegrityOutcome;
669
669
  /**
670
670
  * Handles the "unknown" case: append the text to the chat panel as a user
671
671
  * turn, submit to the chat loop, and schedule a re-render. Handlers for
@@ -1507,6 +1507,11 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
1507
1507
  const result = ctx.verifyReceipt(command.runId);
1508
1508
  if (result.ok) {
1509
1509
  ctx.notice("success", `verify ok ${command.runId}`);
1510
+ } else if (result.retired !== undefined) {
1511
+ // A retired seal is the expected state of a run from the previous
1512
+ // release, so it is a warning that names the versions, never the
1513
+ // failure a tampered receipt gets.
1514
+ ctx.notice("warn", `verify retired ${command.runId} ${result.reason}`);
1510
1515
  } else {
1511
1516
  ctx.notice("error", `verify fail ${command.runId} ${result.reason}`);
1512
1517
  }
@@ -2,13 +2,16 @@ import { createReadStream, readFileSync, statSync } from "node:fs";
2
2
  import { readFile, realpath, stat } from "node:fs/promises";
3
3
  import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import type { DispatchContract } from "../../domains/dispatch/contract.js";
5
- import {
6
- isReceiptIntegrity,
7
- type ReceiptIntegrityResult,
8
- verifyReceiptIntegrity,
9
- } from "../../domains/dispatch/receipt-integrity.js";
5
+ import { isReceiptIntegrity, verifyReceiptIntegrity } from "../../domains/dispatch/receipt-integrity.js";
10
6
  import type { RunEnvelope, RunReceipt } from "../../domains/dispatch/types.js";
11
7
  import { evidenceDirectory, inspectEvidence, listEvidenceOverviews } from "../../domains/evidence/store.js";
8
+ import { formatTrustSummaryLine } from "../../domains/evidence/trust-projection.js";
9
+ import {
10
+ inspectRunReceiptTrustStatus,
11
+ type ReceiptIntegrityOutcome,
12
+ retiredReceiptIntegrity,
13
+ retiredReceiptIntegrityReason,
14
+ } from "../../domains/evidence/trust-status.js";
12
15
  import type { EvidenceFinding, EvidenceOverview, EvidenceSource } from "../../domains/evidence/types.js";
13
16
  import { type AccountabilitySummary, readAccountabilitySummary } from "../../domains/observability/index.js";
14
17
  import type {
@@ -72,7 +75,7 @@ export interface ViewArtifact {
72
75
  toolName?: string;
73
76
  searchText?: readonly string[];
74
77
  load(): Promise<ViewArtifactLoadResult>;
75
- verify?(): Promise<{ ok: boolean; detail: string }>;
78
+ verify?(): Promise<ViewArtifactVerification>;
76
79
  }
77
80
 
78
81
  export interface ArtifactProvider {
@@ -108,7 +111,14 @@ const ACCOUNTABILITY_TOP_CAUSES = 8;
108
111
  export const VIEW_ARTIFACT_LINE_CAP = 50_000;
109
112
  const JSON_PRETTY_MAX_BYTES = 10 * 1024 * 1024;
110
113
 
111
- export type ReceiptVerifyResult = ReceiptIntegrityResult;
114
+ export type ReceiptVerifyResult = ReceiptIntegrityOutcome;
115
+
116
+ /**
117
+ * What a verify action reports. `retired` marks a seal this build does not
118
+ * verify because its integrity version was retired: not a pass, and not the
119
+ * failure a tampered receipt reports.
120
+ */
121
+ export type ViewArtifactVerification = { ok: true; detail: string } | { ok: false; detail: string; retired?: true };
112
122
 
113
123
  const RECEIPT_REQUIRED_KEYS = [
114
124
  "runId",
@@ -286,6 +296,21 @@ function validateToolStats(value: unknown): ReceiptVerifyResult {
286
296
 
287
297
  type ReadLedgerResult = { ok: true; envelope: RunEnvelope } | { ok: false; reason: string };
288
298
 
299
+ /**
300
+ * The canonical trust line for a receipt the integrity verifier just accepted:
301
+ * the same file and ledger row, projected through the shared boundary so the
302
+ * receipt view spells its verdict exactly as the board and the CLI do.
303
+ */
304
+ export function receiptTrustDetail(stateDir: string, runId: string): string {
305
+ try {
306
+ const receipt = JSON.parse(readFileSync(receiptFilePath(stateDir, runId), "utf8")) as RunReceipt;
307
+ const ledger = readRunEnvelope(stateDir, runId);
308
+ return formatTrustSummaryLine(inspectRunReceiptTrustStatus(receipt, ledger.ok ? ledger.envelope : null).status);
309
+ } catch {
310
+ return "integrity verified; trust status unreadable";
311
+ }
312
+ }
313
+
289
314
  function readRunEnvelope(stateDir: string, runId: string): ReadLedgerResult {
290
315
  const runs = readRunLedger(stateDir);
291
316
  for (const entry of runs) {
@@ -398,6 +423,10 @@ export function verifyReceiptFile(stateDir: string, runId: string): ReceiptVerif
398
423
  if (!isNullableString(r.sessionId)) {
399
424
  return { ok: false, reason: `sessionId invalid: ${String(r.sessionId)}` };
400
425
  }
426
+ // A retired seal is diagnosed ahead of the shape check, which would
427
+ // otherwise report the older version as an invalid integrity block.
428
+ const retired = retiredReceiptIntegrity(r.integrity);
429
+ if (retired !== null) return { ok: false, reason: retiredReceiptIntegrityReason(retired), retired };
401
430
  if (!isReceiptIntegrity(r.integrity)) {
402
431
  return { ok: false, reason: "integrity invalid" };
403
432
  }
@@ -673,8 +702,12 @@ export class ReceiptArtifactProvider implements ArtifactProvider {
673
702
  load: () => loadJsonFileLines(path),
674
703
  verify: async () => {
675
704
  const result = verifyReceiptFile(this.deps.stateDir, env.id);
676
- if (!result.ok) return { ok: false, detail: result.reason };
677
- return { ok: true, detail: "integrity verified" };
705
+ if (!result.ok) {
706
+ return { ok: false, detail: result.reason, ...(result.retired !== undefined ? { retired: true } : {}) };
707
+ }
708
+ // "integrity verified" alone read as a verified result. The
709
+ // canonical line says what the seal proves and what it does not.
710
+ return { ok: true, detail: receiptTrustDetail(this.deps.stateDir, env.id) };
678
711
  },
679
712
  };
680
713
  });
@@ -51,7 +51,9 @@ export type ViewVerificationState =
51
51
  | { status: "idle" }
52
52
  | { status: "running" }
53
53
  | { status: "ok"; detail: string }
54
- | { status: "fail"; detail: string };
54
+ | { status: "fail"; detail: string }
55
+ /** A seal this build does not verify because its version was retired: neither a pass nor a tampered-receipt failure. */
56
+ | { status: "retired"; detail: string };
55
57
 
56
58
  export type ViewScrollAction =
57
59
  | "line-up"
@@ -327,6 +329,7 @@ function verificationText(state: ViewVerificationState | undefined): string {
327
329
  if (!state || state.status === "idle") return "";
328
330
  if (state.status === "running") return `${GLYPH.running} verify running`;
329
331
  if (state.status === "ok") return `${GLYPH.ok} verify ok ${state.detail}`;
332
+ if (state.status === "retired") return `${GLYPH.warn} verify retired ${state.detail}`;
330
333
  return `${GLYPH.error} verify fail ${state.detail}`;
331
334
  }
332
335
 
@@ -343,7 +346,14 @@ export function buildArtifactHeader(
343
346
  if (size.length > 0) parts.push(theme.fg("dim", size));
344
347
  const verify = verificationText(verification);
345
348
  if (verify.length > 0) {
346
- const token = verification?.status === "ok" ? "success" : verification?.status === "fail" ? "error" : "info";
349
+ const token =
350
+ verification?.status === "ok"
351
+ ? "success"
352
+ : verification?.status === "fail"
353
+ ? "error"
354
+ : verification?.status === "retired"
355
+ ? "warning"
356
+ : "info";
347
357
  parts.push(theme.fg(token, verify));
348
358
  }
349
359
  return padAnsi(parts.join(" "), width, ELLIPSIS);
@@ -689,7 +699,9 @@ export class ViewOverlayView implements Component {
689
699
  .then((result) => {
690
700
  this.verifications.set(
691
701
  key,
692
- result.ok ? { status: "ok", detail: result.detail } : { status: "fail", detail: result.detail },
702
+ result.ok
703
+ ? { status: "ok", detail: result.detail }
704
+ : { status: result.retired === true ? "retired" : "fail", detail: result.detail },
693
705
  );
694
706
  })
695
707
  .catch((err) => {
@@ -16,6 +16,8 @@
16
16
  import { existsSync, readFileSync } from "node:fs";
17
17
  import { join } from "node:path";
18
18
  import { clioStateDir } from "../core/xdg.js";
19
+ import type { RunEnvelope, RunReceipt } from "../domains/dispatch/types.js";
20
+ import { inspectRunReceiptTrustStatus } from "../domains/evidence/trust-status.js";
19
21
  import { receiptFilePath } from "./view/artifacts.js";
20
22
  import type { WorkerReceiptFacts, WorkerResultContract } from "./worker-stream.js";
21
23
 
@@ -86,12 +88,22 @@ export function workerReceiptFacts(receipt: Record<string, unknown>): WorkerRece
86
88
 
87
89
  /** Read and project `<state>/receipts/<runId>.json` alone; null when it is absent or unreadable. */
88
90
  function readReceiptFileFacts(runId: string, stateDir: string): WorkerReceiptFacts | null {
91
+ let parsed: unknown;
89
92
  try {
90
- const parsed: unknown = JSON.parse(readFileSync(receiptFilePath(stateDir, runId), "utf8"));
91
- return isRecord(parsed) ? workerReceiptFacts(parsed) : null;
93
+ parsed = JSON.parse(readFileSync(receiptFilePath(stateDir, runId), "utf8"));
92
94
  } catch {
93
95
  return null;
94
96
  }
97
+ if (!isRecord(parsed)) return null;
98
+ const facts = workerReceiptFacts(parsed);
99
+ if (facts === null) return null;
100
+ // The trust verdict is an authenticated read-back: the receipt file
101
+ // against the ledger row it was sealed from. Without the row there is no
102
+ // authentication, and no verdict is better than a guessed one.
103
+ const row = findRunRow(runId, stateDir);
104
+ if (row === null) return facts;
105
+ const trust = inspectRunReceiptTrustStatus(parsed as unknown as RunReceipt, row as unknown as RunEnvelope).status;
106
+ return { ...facts, trust };
95
107
  }
96
108
 
97
109
  /** Read and project `<state>/receipts/<runId>.json`; null when it is absent or unreadable. */
@@ -42,6 +42,7 @@ import type {
42
42
  RunAbortedPayload,
43
43
  } from "../core/bus-events.js";
44
44
  import type { RunKind } from "../domains/dispatch/types.js";
45
+ import type { CanonicalTrustStatus } from "../domains/evidence/trust-status.js";
45
46
  import type { WorkerRunOrigin, WorkerRunRuntime, WorkerRunRuntimeKind } from "../domains/session/index.js";
46
47
  import { createWorkerProgressFold, type WorkerProgressFold, type WorkerProgressSnapshot } from "./worker-progress.js";
47
48
 
@@ -88,6 +89,13 @@ export interface WorkerReceiptSummary {
88
89
  /** A receipt's projection: the summary plus the answer it sealed. */
89
90
  export interface WorkerReceiptFacts extends WorkerReceiptSummary {
90
91
  text?: string;
92
+ /**
93
+ * The canonical trust status of the persisted receipt, authenticated
94
+ * against the persisted ledger row it was sealed from. Present only when
95
+ * both could be read back, so a surface that shows it shows an
96
+ * authenticated verdict and never the sealing process's own claim.
97
+ */
98
+ trust?: CanonicalTrustStatus;
91
99
  }
92
100
 
93
101
  /** Reads `receipts/<runId>.json` and projects it; null when it is absent or unreadable. */
@@ -6,8 +6,10 @@ import {
6
6
  COUNCIL_JUDGE_PROMPT,
7
7
  JUDGE_GATE_PROMPT,
8
8
  REVIEWER_GATE_PROMPT,
9
+ renderCouncilVoteMemberTask,
9
10
  } from "../domains/dispatch/gate-role-prompts.js";
10
11
  import { normalizeDispatchIntent } from "../domains/dispatch/intent.js";
12
+ import { renderDispatchReviewerTask } from "../domains/dispatch/intent-requirements.js";
11
13
  import { approvedRouteCandidates } from "../domains/dispatch/route-approval.js";
12
14
  import { defaultRoutingIntent } from "../domains/dispatch/routing-intent.js";
13
15
  import type { DispatchFailoverMode } from "../domains/dispatch/validation.js";
@@ -139,15 +141,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
139
141
  return typeof value === "object" && value !== null && !Array.isArray(value);
140
142
  }
141
143
 
142
- function reviewerTask(originalTask: string, builderRunId: string, cycle: number): string {
143
- return [
144
- `Review the work of builder run ${builderRunId} (review cycle ${cycle}).`,
145
- "The builder's final answer is provided as input data; verify it against the workspace, do not trust it blindly.",
146
- "Original task the builder was given:",
147
- originalTask,
148
- ].join("\n\n");
149
- }
150
-
151
144
  function reviewSettingsFromArgs(
152
145
  args: Record<string, unknown>,
153
146
  ): { ok: true; review: DispatchReviewSettings | undefined } | { ok: false; message: string } {
@@ -534,9 +527,6 @@ export function createDispatchAdmissionController(deps: DispatchToolDeps): Dispa
534
527
  if (parsed.requests.length !== 1 || parsed.requests[0] === undefined) {
535
528
  return shapeRejection(args, REVIEW_SINGLE_TASK_MESSAGE);
536
529
  }
537
- if (parsed.requests.some((request) => (request.resolvedVerification?.length ?? 0) > 0)) {
538
- return shapeRejection(args, "verification_unsupported_for_mode: review requests cannot run host verification");
539
- }
540
530
  }
541
531
  if (mode === "compete" && (parsed.requests.length !== 1 || parsed.requests[0] === undefined)) {
542
532
  return shapeRejection(args, COMPETE_SINGLE_TASK_MESSAGE);
@@ -587,7 +577,7 @@ export function createDispatchAdmissionController(deps: DispatchToolDeps): Dispa
587
577
  {
588
578
  agentId: gateDeciderAgentId(reviewResult.review.reviewer),
589
579
  executionRole: "reviewer",
590
- task: reviewerTask(base.task, subject.runId, cycle),
580
+ task: renderDispatchReviewerTask(base.task, subject.runId, cycle, base.intent),
591
581
  systemPrompt: REVIEWER_GATE_PROMPT,
592
582
  autonomy: "read-only",
593
583
  gate: { role: "reviewer", group: "plan-preview", cycle, subjects: [subject] },
@@ -648,6 +638,15 @@ export function createDispatchAdmissionController(deps: DispatchToolDeps): Dispa
648
638
  executionRole: "researcher",
649
639
  autonomy: "read-only",
650
640
  toolProfile: "council-read-only",
641
+ // The runner reuses this composed task from the resolved plan, so
642
+ // the approval artifact the plan hash binds shows the operator the
643
+ // exact ballot the member is asked to cast.
644
+ ...(council.synthesis === "vote"
645
+ ? {
646
+ task: renderCouncilVoteMemberTask(base.task),
647
+ resultContractOverride: { kind: "council-ballot" as const },
648
+ }
649
+ : {}),
651
650
  target: member.target,
652
651
  ...(member.model ? { model: member.model } : {}),
653
652
  ...(member.thinking ? { thinkingLevel: member.thinking as NonNullable<DispatchRequest["thinkingLevel"]> } : {}),
@@ -1,5 +1,6 @@
1
1
  /** Pure model-argument parsing; every returned DispatchRequest has a concrete agent id. */
2
2
 
3
+ import { pathBoundaryCovers } from "../core/path-boundary.js";
3
4
  import type { AgentAutomationAuthority } from "../domains/agents/spec.js";
4
5
  import { type AgentTaskType, classifyAgentTask } from "../domains/dispatch/agent-candidates.js";
5
6
  import { cloneDispatchBudgetRequest } from "../domains/dispatch/budget-envelope.js";
@@ -261,8 +262,34 @@ export function dispatchRequestsFromArgs(
261
262
  for (let index = 0; index < tasks.length; index += 1) {
262
263
  const item = tasks[index];
263
264
  const itemArgs: Record<string, unknown> = isRecord(item) ? { ...shared, ...item } : { ...shared, task: item };
265
+ const sharedIntent = isRecord(shared.intent) ? shared.intent : null;
266
+ const itemIntent = isRecord(item) && Object.hasOwn(item, "intent") && isRecord(item.intent) ? item.intent : null;
267
+ if (sharedIntent !== null && itemIntent !== null) itemArgs.intent = { ...sharedIntent, ...itemIntent };
264
268
  const parsed = dispatchRequestFromArgs(itemArgs, options);
265
269
  if (!parsed.ok) return { ok: false, message: `dispatch: task ${index + 1}: ${parsed.message}` };
270
+ if (sharedIntent !== null && itemIntent !== null) {
271
+ if (options.resolveIntent === undefined) {
272
+ return { ok: false, message: `dispatch: task ${index + 1}: intent resolver is unavailable` };
273
+ }
274
+ const ceiling = options.resolveIntent(sharedIntent, parsed.request.cwd);
275
+ if (!ceiling.ok) return { ok: false, message: `dispatch: task ${index + 1}: ${ceiling.message}` };
276
+ const pathCeilings = {
277
+ readRoots: [...ceiling.intent.readRoots, ...ceiling.intent.writeRoots],
278
+ writeRoots: ceiling.intent.writeRoots,
279
+ relevantPaths: [...ceiling.intent.readRoots, ...ceiling.intent.writeRoots, ...ceiling.intent.relevantPaths],
280
+ };
281
+ for (const field of ["readRoots", "writeRoots", "relevantPaths"] as const) {
282
+ const outside = parsed.request.intent?.[field].find(
283
+ (candidate) => !pathBoundaryCovers(pathCeilings[field], candidate),
284
+ );
285
+ if (outside !== undefined) {
286
+ return {
287
+ ok: false,
288
+ message: `dispatch: task ${index + 1}: intent_scope_widening: ${field} entry '${outside}' is outside the top-level intent ceiling`,
289
+ };
290
+ }
291
+ }
292
+ }
266
293
  requests.push(parsed.request);
267
294
  }
268
295
  return { ok: true, requests };