@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
@@ -16,6 +16,7 @@ import { probeCapabilitiesForModel } from "./model-capabilities.js";
16
16
  import { loadPluginRuntimes } from "./plugins.js";
17
17
  import { getRuntimeRegistry } from "./registry.js";
18
18
  import { registerBuiltinRuntimes } from "./runtimes/builtins.js";
19
+ import { listKnownModelsForRuntime } from "./support.js";
19
20
  import type { CapabilityFlags } from "./types/capability-flags.js";
20
21
  import { EMPTY_CAPABILITIES } from "./types/capability-flags.js";
21
22
  import {
@@ -204,6 +205,32 @@ export function mergeProbeResult(
204
205
  return merge;
205
206
  }
206
207
 
208
+ /**
209
+ * Whether a reachable target can serve the model it is configured to serve by
210
+ * default. Reachability alone was the whole of `health`, so a target whose
211
+ * `defaultModel` the server had never heard of printed `healthy` until the
212
+ * first turn. This is judged only from a live list on a runtime with no static
213
+ * catalog: the catalog is the authority for cloud runtimes, and a cached or
214
+ * descriptor-supplied list says nothing about the server in front of us.
215
+ * Returns the reason when the default is not served, null when it is or when
216
+ * there is no live evidence either way.
217
+ */
218
+ export function unservedDefaultModelReason(
219
+ desc: Pick<RuntimeDescriptor, "id">,
220
+ target: Pick<TargetDescriptor, "defaultModel">,
221
+ merge: Pick<ProbeMerge, "discoveredModels" | "discoveredModelsSource" | "discoveredModelStates">,
222
+ ): string | null {
223
+ const model = target.defaultModel;
224
+ if (!model) return null;
225
+ if (merge.discoveredModelsSource !== "probe" || merge.discoveredModels.length === 0) return null;
226
+ if (listKnownModelsForRuntime(desc.id).length > 0) return null;
227
+ if (merge.discoveredModels.includes(model)) return null;
228
+ // LM Studio lists a loaded model under its instance id and keeps the model
229
+ // key in the state map; the request path resolves either.
230
+ if (merge.discoveredModelStates && model in merge.discoveredModelStates) return null;
231
+ return `default model '${model}' is not advertised by the target`;
232
+ }
233
+
207
234
  export function createProvidersBundle(context: DomainContext): DomainBundle<ProvidersContract> {
208
235
  const registry = getRuntimeRegistry();
209
236
  const authStore = openAuthStorage();
@@ -292,13 +319,14 @@ export function createProvidersBundle(context: DomainContext): DomainBundle<Prov
292
319
  const merge = mergeProbeResult(desc, target, probe, previous);
293
320
  const { capabilities, contextWindowProvenance } = capabilitiesFor(desc, target, merge, kb);
294
321
  const healthy = probe !== null ? probe.ok : null;
322
+ const unservedDefault = probe?.ok ? unservedDefaultModelReason(desc, target, merge) : null;
295
323
  const health: TargetHealth =
296
324
  probe === null
297
325
  ? (previous?.health ?? emptyHealth())
298
326
  : {
299
- status: healthy ? "healthy" : "down",
327
+ status: healthy ? (unservedDefault === null ? "healthy" : "degraded") : "down",
300
328
  lastCheckAt: new Date().toISOString(),
301
- lastError: probe.error ?? null,
329
+ lastError: probe.error ?? unservedDefault,
302
330
  latencyMs: probe.latencyMs ?? null,
303
331
  };
304
332
  const available = availability.available && (probe === null || probe.ok);
@@ -7,6 +7,8 @@ import type { ResourceDiagnostic, ResourceScope, ResourceSourceInfo } from "./co
7
7
 
8
8
  export interface ResourceRoot {
9
9
  path: string;
10
+ /** Package root for safely resolving extension-relative prompt references. */
11
+ rootPath?: string;
10
12
  scope: ResourceScope;
11
13
  source?: string;
12
14
  /** Collision rank, higher wins. See {@link ResourceSourceInfo.precedence}. */
@@ -44,6 +46,7 @@ export function defaultScopedResourceRoots(kind: ExtensionResourceKind, cwd: str
44
46
  return [
45
47
  ...enabledExtensionResourceRoots(kind, cwd).map((root) => ({
46
48
  path: root.path,
49
+ rootPath: root.rootPath,
47
50
  scope: "package" as const,
48
51
  source: root.source,
49
52
  precedence: COMPAT_RESOURCE_PRECEDENCE.extension,
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import { lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import path from "node:path";
4
4
  import { parseCommandArgs, substituteArgs } from "../../../engine/prompt-templates.js";
@@ -13,7 +13,6 @@ import {
13
13
  import {
14
14
  COMPAT_RESOURCE_PRECEDENCE,
15
15
  defaultScopedResourceRoots,
16
- readRootEntries,
17
16
  sourceInfoForRoot,
18
17
  splitYamlFrontmatter,
19
18
  stringField,
@@ -33,6 +32,8 @@ export interface PromptTemplate {
33
32
 
34
33
  export interface PromptTemplateRoot {
35
34
  path: string;
35
+ /** Present only for an installed extension resource root. */
36
+ rootPath?: string;
36
37
  scope: ResourceScope;
37
38
  source?: string;
38
39
  precedence?: number;
@@ -139,6 +140,61 @@ function fallbackDescription(body: string): string {
139
140
  return normalized.length > 60 ? `${normalized.slice(0, 57)}...` : normalized;
140
141
  }
141
142
 
143
+ // Keep the suffix deliberately conservative. Prompt prose commonly places a
144
+ // comma, parenthesis, or full stop immediately after an @path; treating that
145
+ // punctuation as part of the filesystem reference makes a valid template look
146
+ // unresolved. Extension bundles use portable path components, so these are the
147
+ // only characters needed in a reference while still allowing `..` to be
148
+ // detected and rejected by the containment check below.
149
+ const EXTENSION_ROOT_REFERENCE = /\$\{extensionRoot\}(\/[A-Za-z0-9._~%+@/-]*)?/g;
150
+ const EXTENSION_ROOT_TOKEN = "$" + "{extensionRoot}";
151
+
152
+ function contained(root: string, candidate: string): boolean {
153
+ const relative = path.relative(root, candidate);
154
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
155
+ }
156
+
157
+ /** Resolve only existing paths contained by the declaring extension package. */
158
+ function resolveExtensionReferences(
159
+ body: string,
160
+ root: PromptTemplateRoot,
161
+ filePath: string,
162
+ diagnostics: ResourceDiagnostic[],
163
+ ): string | null {
164
+ if (root.rootPath === undefined || !body.includes(EXTENSION_ROOT_TOKEN)) return body;
165
+ let canonicalRoot: string;
166
+ try {
167
+ canonicalRoot = realpathSync(root.rootPath);
168
+ } catch (error) {
169
+ diagnostics.push({
170
+ type: "warning",
171
+ message: `extension root could not be resolved: ${error instanceof Error ? error.message : String(error)}`,
172
+ path: filePath,
173
+ });
174
+ return null;
175
+ }
176
+ let invalid: string | null = null;
177
+ const resolved = body.replace(EXTENSION_ROOT_REFERENCE, (reference, suffix: string | undefined) => {
178
+ const candidate = path.resolve(canonicalRoot, `.${suffix ?? ""}`);
179
+ try {
180
+ const canonicalCandidate = realpathSync(candidate);
181
+ if (!contained(canonicalRoot, canonicalCandidate)) invalid = reference;
182
+ } catch {
183
+ invalid = reference;
184
+ }
185
+ return candidate;
186
+ });
187
+ if (invalid !== null) {
188
+ diagnostics.push({
189
+ type: "warning",
190
+ message: `prompt template has an unresolved or escaping extension reference: ${invalid}`,
191
+ path: filePath,
192
+ });
193
+ return null;
194
+ }
195
+ return resolved;
196
+ }
197
+
142
198
  function loadPromptFile(
143
199
  filePath: string,
144
200
  root: PromptTemplateRoot,
@@ -155,16 +211,28 @@ function loadPromptFile(
155
211
  return null;
156
212
  }
157
213
 
158
- const name = path.basename(filePath, ext);
214
+ const relativePath = path.relative(root.path, filePath);
215
+ if (
216
+ relativePath === "" ||
217
+ relativePath === ".." ||
218
+ relativePath.startsWith(`..${path.sep}`) ||
219
+ path.isAbsolute(relativePath)
220
+ ) {
221
+ diagnostics.push({ type: "warning", message: "prompt template escaped its discovery root", path: filePath });
222
+ return null;
223
+ }
224
+ const name = relativePath.slice(0, -ext.length).split(path.sep).join(":");
159
225
  if (name.trim().length === 0) return null;
160
226
  const { frontmatter, body } = splitOptionalFrontmatter(raw, filePath, diagnostics);
161
- const description = stringField(frontmatter, "description") ?? fallbackDescription(body);
227
+ const resolvedBody = resolveExtensionReferences(body, root, filePath, diagnostics);
228
+ if (resolvedBody === null) return null;
229
+ const description = stringField(frontmatter, "description") ?? fallbackDescription(resolvedBody);
162
230
  const argumentHint = stringField(frontmatter, "argument-hint") ?? stringField(frontmatter, "argumentHint");
163
231
  const sourceInfo: ResourceSourceInfo = sourceInfoForRoot(root, filePath);
164
232
  const template: PromptTemplate = {
165
233
  name,
166
234
  description,
167
- content: body.trim(),
235
+ content: resolvedBody.trim(),
168
236
  filePath,
169
237
  sourceInfo,
170
238
  trusted: root.trusted !== false,
@@ -178,10 +246,42 @@ function loadPromptRoot(
178
246
  diagnostics: ResourceDiagnostic[],
179
247
  ): ResourceCandidate<PromptTemplate>[] {
180
248
  const candidates: ResourceCandidate<PromptTemplate>[] = [];
181
- for (const entry of readRootEntries(root, "prompt template", diagnostics)) {
182
- if (!entry.isFile()) continue;
183
- const candidate = loadPromptFile(path.join(root.path, entry.name), root, diagnostics);
184
- if (candidate) candidates.push(candidate);
249
+ const pending = [root.path];
250
+ while (pending.length > 0) {
251
+ const directory = pending.pop();
252
+ if (!directory) continue;
253
+ let entries: import("node:fs").Dirent[];
254
+ try {
255
+ const stat = lstatSync(directory);
256
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
257
+ diagnostics.push({
258
+ type: "warning",
259
+ message: "prompt template root contains a non-directory or symbolic link",
260
+ path: directory,
261
+ });
262
+ continue;
263
+ }
264
+ entries = readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
265
+ } catch (error) {
266
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
267
+ diagnostics.push({
268
+ type: "warning",
269
+ message: `prompt template directory could not be read: ${error instanceof Error ? error.message : String(error)}`,
270
+ path: directory,
271
+ });
272
+ continue;
273
+ }
274
+ for (const entry of [...entries].reverse()) {
275
+ const filePath = path.join(directory, entry.name);
276
+ if (entry.isSymbolicLink()) {
277
+ diagnostics.push({ type: "warning", message: "prompt template symbolic link was ignored", path: filePath });
278
+ } else if (entry.isDirectory()) {
279
+ pending.push(filePath);
280
+ } else if (entry.isFile()) {
281
+ const candidate = loadPromptFile(filePath, root, diagnostics);
282
+ if (candidate) candidates.push(candidate);
283
+ }
284
+ }
185
285
  }
186
286
  return candidates;
187
287
  }
@@ -210,13 +310,18 @@ export function loadPromptTemplates(input: LoadPromptTemplatesInput = {}): Promp
210
310
  }
211
311
 
212
312
  function parsePromptCommand(input: string): { name: string; rest: string } | null {
213
- const trimmed = input.trim();
214
- if (!trimmed.startsWith("/")) return null;
215
- const withoutSlash = trimmed.slice(1);
313
+ const framed = input.trimStart();
314
+ if (!framed.startsWith("/")) return null;
315
+ const withoutSlash = framed.slice(1);
216
316
  const separator = withoutSlash.search(/\s/);
217
317
  const name = separator === -1 ? withoutSlash : withoutSlash.slice(0, separator);
218
318
  if (name.length === 0 || name.includes("/")) return null;
219
- const rest = separator === -1 ? "" : withoutSlash.slice(separator).trim();
319
+ // Leading whitespace before `/` and the first whitespace after the command
320
+ // are framing. A CRLF pair is one delimiter. Every subsequent byte belongs
321
+ // to the argument payload, including indentation, blank lines, and trailing
322
+ // whitespace, so raw `$ARGUMENTS` substitution can be exact at its boundary.
323
+ const delimiterWidth = separator !== -1 && withoutSlash.slice(separator, separator + 2) === "\r\n" ? 2 : 1;
324
+ const rest = separator === -1 ? "" : withoutSlash.slice(separator + delimiterWidth);
220
325
  return { name, rest };
221
326
  }
222
327
 
@@ -238,7 +343,7 @@ export function expandPromptTemplateInput(input: string, templates: PromptTempla
238
343
  const args = parseCommandArgs(command.rest);
239
344
  return {
240
345
  expanded: true,
241
- text: substituteArgs(template.content, args),
346
+ text: substituteArgs(template.content, args, command.rest),
242
347
  args,
243
348
  template,
244
349
  diagnostics: templates.diagnostics,
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import path from "node:path";
3
3
  import { artifactDefaultPath } from "../../core/artifact-paths.js";
4
+ import { pathBoundaryCovers, resolvePathBoundary } from "../../core/path-boundary.js";
4
5
  import { canonicalizeExistingPath } from "../../core/path-canonical.js";
5
6
  import { ToolNames } from "../../core/tool-names.js";
6
7
  import { clioConfigDir } from "../../core/xdg.js";
@@ -89,7 +90,8 @@ export interface SafetyPolicyEngineOptions {
89
90
  rulePacks?: RulePacks;
90
91
  projectPolicy?: LoadedProjectSafetyPolicy;
91
92
  /**
92
- * Absolute directories a write-class tool call is confined to for this run.
93
+ * Absolute path boundaries a write-class tool call is confined to for this run.
94
+ * Exact files omit a trailing slash and subtrees retain one.
93
95
  * When present and non-empty, a write/edit target outside every root is a
94
96
  * final BLOCK (reason code "write-root"). Empty or absent disables the check.
95
97
  * Enforced at the worker safety seam so both the native worker registry and
@@ -182,9 +184,7 @@ function evaluateWriteRoots(roots: ReadonlyArray<string>, writeRootCwd: string,
182
184
  const target = writeRootTargetPath(call);
183
185
  if (target === null) return null;
184
186
  const resolved = path.resolve(writeRootCwd, target);
185
- for (const root of roots) {
186
- if (resolved === root || resolved.startsWith(`${root}${path.sep}`)) return null;
187
- }
187
+ if (pathBoundaryCovers(roots, resolved)) return null;
188
188
  return `write target '${target}' resolves to '${resolved}', which is outside the permitted write roots for this run: ${roots.join(", ")}`;
189
189
  }
190
190
 
@@ -193,7 +193,7 @@ export function createSafetyPolicyEngine(options: SafetyPolicyEngineOptions = {}
193
193
  // Write-root containment resolves lexically, so it keeps its own un-canonicalized
194
194
  // cwd and roots to compare like against like (the design mandates no symlink chasing).
195
195
  const writeRootCwd = path.resolve(options.cwd ?? process.cwd());
196
- const writeRoots = (options.writeRoots ?? []).map((root) => path.resolve(root));
196
+ const writeRoots = (options.writeRoots ?? []).map((root) => resolvePathBoundary(writeRootCwd, root));
197
197
  const packs = options.rulePacks ?? getCachedDefaultRulePacks();
198
198
  const projectPolicy = options.projectPolicy ?? loadProjectSafetyPolicy(cwd);
199
199
  const projectPolicyRoot =
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { ToolNames } from "../../core/tool-names.js";
3
+ import { classify } from "./action-classifier.js";
3
4
  import { typedValidationSummary } from "./finish-contract.js";
4
5
  import {
5
6
  detectValidationCommand,
@@ -25,6 +26,13 @@ import {
25
26
  * bash command that writes through a script) leaves the path absent from the
26
27
  * set and the validator falls back to the filesystem rather than to a failed
27
28
  * conformance.
29
+ *
30
+ * `writeRecordComplete` is the other half of that sentence, and it exists for
31
+ * the reader that cannot fall back to the filesystem. The write boundary uses
32
+ * this record the other way round, to decide that a path the run did *not*
33
+ * write is somebody else's and must survive. Reading an under-reporting set as
34
+ * a closed list would clear a step of a write it really made, so the recorder
35
+ * says outright whether the set is closed.
28
36
  */
29
37
  export interface RunEffects {
30
38
  /** Absolute paths the run's successful tool calls aimed a mutation at. */
@@ -56,6 +64,18 @@ export interface RunEffects {
56
64
  * wider set.
57
65
  */
58
66
  verificationCommands: ReadonlySet<string>;
67
+ /**
68
+ * False once the run made a successful call to a tool that can mutate a path
69
+ * its own arguments do not name. After such a call `mutatedPaths` is a lower
70
+ * bound rather than the whole list, so a reader that would treat an absent
71
+ * path as proof the run did not write it has to stop treating it that way.
72
+ *
73
+ * The downgrade covers the whole run rather than the one call, because an
74
+ * opaque call's effect is unbounded. A shell command, a project script, or a
75
+ * spawned worker can write anywhere the process can reach, so once one has
76
+ * run there is no path left in the checkout that the record can clear.
77
+ */
78
+ writeRecordComplete: boolean;
59
79
  }
60
80
 
61
81
  export interface RunEffectsRecorder {
@@ -70,6 +90,39 @@ interface PendingEffects {
70
90
  paths: ReadonlyArray<string>;
71
91
  validationCommand: string | null;
72
92
  verificationCommand: string | null;
93
+ opaque: boolean;
94
+ }
95
+
96
+ /**
97
+ * Action classes whose successful calls write only paths their own arguments
98
+ * name. `read` covers the observe, retrieve, interact, and orchestrate-bookkeeping
99
+ * tools, none of which touch a source-workspace file: `git` is a closed
100
+ * status/diff/log surface, and `tasks`, `ledger`, and `context` write Clio's own
101
+ * state. `write` covers `write`, `edit`, and `artifact`, whose target
102
+ * `toolMutationPaths` resolves from the call itself, including the artifact
103
+ * tool's default path.
104
+ */
105
+ const ENUMERABLE_ACTION_CLASSES: ReadonlySet<string> = new Set(["read", "write"]);
106
+
107
+ /**
108
+ * Whether a successful call to this tool can mutate a path its own recorded
109
+ * arguments do not name.
110
+ *
111
+ * Asked of the classifier rather than of a hand-written list, because
112
+ * `src/tools/policy.ts` asserts at bootstrap that every registered tool's
113
+ * `baseActionClass` equals what this classifier returns and what the plane
114
+ * table declares. A tool added to the execute or orchestrate-dispatch plane is
115
+ * therefore opaque the day it is registered, with no list to remember to update.
116
+ *
117
+ * The classifier is asked about the tool name alone. Bash's argument scan can
118
+ * escalate a single call to `system_modify` or `git_destructive`, and those are
119
+ * still bash; the question here is what the tool can do, not what one call was
120
+ * caught doing. An unregistered name (a dynamic or MCP tool) classifies as
121
+ * `unknown` and is opaque, which is the fail-closed answer for a tool whose
122
+ * arguments this process has no schema for.
123
+ */
124
+ export function toolWritesOpaquely(toolName: string): boolean {
125
+ return !ENUMERABLE_ACTION_CLASSES.has(classify({ tool: toolName }).actionClass);
73
126
  }
74
127
 
75
128
  /**
@@ -157,18 +210,23 @@ export function createRunEffectsRecorder(cwd: string): RunEffectsRecorder {
157
210
  const failedMutationPaths = new Set<string>();
158
211
  const validationCommands = new Set<string>();
159
212
  const verificationCommands = new Set<string>();
213
+ let opaqueWriteObserved = false;
160
214
  return {
161
215
  start(toolCallId, toolName, args) {
162
216
  const paths = mutationTargets(toolName, args);
163
217
  const validationCommand = validationCommandOf(toolName, args, "finish-contract");
164
218
  const verificationCommand = validationCommandOf(toolName, args, "grounding");
165
- if (paths.length === 0 && validationCommand === null && verificationCommand === null) return;
219
+ const opaque = toolWritesOpaquely(toolName);
220
+ // An opaque call is tracked even when it exposed no path and no
221
+ // command, because the fact that it ran at all is the finding.
222
+ if (!opaque && paths.length === 0 && validationCommand === null && verificationCommand === null) return;
166
223
  // A bash call may re-base its relative paths with its own cwd argument.
167
224
  const base = typeof args?.cwd === "string" && args.cwd.length > 0 ? path.resolve(cwd, args.cwd) : cwd;
168
225
  pending.set(toolCallId, {
169
226
  paths: paths.map((target) => path.resolve(base, target)),
170
227
  validationCommand,
171
228
  verificationCommand,
229
+ opaque,
172
230
  });
173
231
  },
174
232
  finish(toolCallId, failed) {
@@ -179,6 +237,10 @@ export function createRunEffectsRecorder(cwd: string): RunEffectsRecorder {
179
237
  for (const target of effects.paths) failedMutationPaths.add(target);
180
238
  return;
181
239
  }
240
+ // Success-gated on purpose. A shell command the safety policy blocked,
241
+ // or one that came back an error, never reached the filesystem, so it
242
+ // leaves the record closed.
243
+ if (effects.opaque) opaqueWriteObserved = true;
182
244
  for (const target of effects.paths) mutatedPaths.add(target);
183
245
  if (effects.validationCommand !== null) validationCommands.add(effects.validationCommand);
184
246
  if (effects.verificationCommand !== null) verificationCommands.add(effects.verificationCommand);
@@ -191,6 +253,7 @@ export function createRunEffectsRecorder(cwd: string): RunEffectsRecorder {
191
253
  failedMutationPaths: new Set([...failedMutationPaths].filter((target) => !mutatedPaths.has(target))),
192
254
  validationCommands: new Set(validationCommands),
193
255
  verificationCommands: new Set(verificationCommands),
256
+ writeRecordComplete: !opaqueWriteObserved,
194
257
  };
195
258
  },
196
259
  };
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { asDirectoryPathBoundary, pathBoundaryEntryCovers, resolvePathBoundary } from "../../core/path-boundary.js";
2
3
  import type { ActionClass } from "./action-classifier.js";
3
4
 
4
5
  /**
@@ -15,15 +16,7 @@ export interface ScopeSpec {
15
16
  }
16
17
 
17
18
  function normalizeRoot(root: string): string {
18
- return path.resolve(root);
19
- }
20
-
21
- function isUnder(child: string, parent: string): boolean {
22
- const c = normalizeRoot(child);
23
- const p = normalizeRoot(parent);
24
- if (c === p) return true;
25
- const rel = path.relative(p, c);
26
- return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
19
+ return resolvePathBoundary(process.cwd(), root);
27
20
  }
28
21
 
29
22
  export function isSubset(worker: ScopeSpec, orchestrator: ScopeSpec): boolean {
@@ -31,7 +24,9 @@ export function isSubset(worker: ScopeSpec, orchestrator: ScopeSpec): boolean {
31
24
  if (!orchestrator.allowedActions.has(action)) return false;
32
25
  }
33
26
  for (const root of worker.allowedWriteRoots) {
34
- const covered = orchestrator.allowedWriteRoots.some((outer) => isUnder(root, outer));
27
+ const covered = orchestrator.allowedWriteRoots.some((outer) =>
28
+ pathBoundaryEntryCovers(normalizeRoot(outer), normalizeRoot(root)),
29
+ );
35
30
  if (!covered) return false;
36
31
  }
37
32
  if (worker.allowNetwork && !orchestrator.allowNetwork) return false;
@@ -48,14 +43,14 @@ export const READONLY_SCOPE: ScopeSpec = {
48
43
 
49
44
  export const WORKSPACE_SCOPE: ScopeSpec = {
50
45
  allowedActions: new Set<ActionClass>(["read", "write", "execute", "dispatch"]),
51
- allowedWriteRoots: [process.cwd()],
46
+ allowedWriteRoots: [asDirectoryPathBoundary(path.resolve(process.cwd()))],
52
47
  allowNetwork: true,
53
48
  allowDispatch: true,
54
49
  };
55
50
 
56
51
  export const CONFIRMED_SCOPE: ScopeSpec = {
57
52
  allowedActions: new Set<ActionClass>(["read", "write", "execute", "dispatch", "system_modify"]),
58
- allowedWriteRoots: [process.cwd()],
53
+ allowedWriteRoots: [asDirectoryPathBoundary(path.resolve(process.cwd()))],
59
54
  allowNetwork: true,
60
55
  allowDispatch: true,
61
56
  };
@@ -1233,7 +1233,10 @@ function safeProbeReason(
1233
1233
  } catch {
1234
1234
  return "not-configured";
1235
1235
  }
1236
- return status.available && status.health.status === "healthy" ? null : "unreachable";
1236
+ // Degraded is reachable with a default the server does not serve; a client
1237
+ // that names its own model can still use the target.
1238
+ const reachable = status.health.status === "healthy" || status.health.status === "degraded";
1239
+ return status.available && reachable ? null : "unreachable";
1237
1240
  }
1238
1241
 
1239
1242
  interface AcpPermissionBridge {
@@ -1 +1,18 @@
1
- export { parseCommandArgs, substituteArgs } from "@earendil-works/pi-agent-core";
1
+ import { parseCommandArgs, substituteArgs as substituteParsedArgs } from "@earendil-works/pi-agent-core";
2
+
3
+ export { parseCommandArgs };
4
+
5
+ const RAW_ARGUMENTS_PLACEHOLDER = /\$ARGUMENTS/g;
6
+
7
+ /**
8
+ * Substitute prompt arguments while optionally retaining the exact raw text
9
+ * for `$ARGUMENTS`. Positional, slice, and `$@` placeholders keep Pi's
10
+ * shell-style parsing semantics.
11
+ */
12
+ export function substituteArgs(content: string, args: string[], rawArguments?: string): string {
13
+ if (rawArguments === undefined) return substituteParsedArgs(content, args);
14
+ return content
15
+ .split(RAW_ARGUMENTS_PLACEHOLDER)
16
+ .map((segment) => substituteParsedArgs(segment, args))
17
+ .join(rawArguments);
18
+ }
@@ -23,7 +23,7 @@ import { type ToolName, ToolNames } from "../core/tool-names.js";
23
23
  import {
24
24
  type ObservedReadRanges,
25
25
  type ObservedRunEffects,
26
- parseResultContract,
26
+ parseWorkerResultContract,
27
27
  RESULT_CONTRACT_REPAIR_LIMIT,
28
28
  type ResultContract,
29
29
  resultContractRepairMessages,
@@ -362,8 +362,11 @@ export function startWorkerRun(input: WorkerRunInput, emit: WorkerEventEmit): Wo
362
362
  assertResponseSchemaRuntime(input);
363
363
  // The wire carries the contract as data; the one strict parser owns its
364
364
  // shape. The worker subprocess may not import it (it stays slim), so the
365
- // check lands here, before any model call.
366
- if (input.resultContract !== undefined) parseResultContract(input.resultContract, "WorkerSpec.resultContract");
365
+ // check lands here, before any model call. The wire parser, not the recipe
366
+ // one: a dispatch request may override the seated recipe's postcondition
367
+ // with a kind the coordinator authors, and the recipe parser refusing those
368
+ // killed every council vote member with a fatal spec error.
369
+ if (input.resultContract !== undefined) parseWorkerResultContract(input.resultContract, "WorkerSpec.resultContract");
367
370
  if (input.runtime.id === "claude-sdk") {
368
371
  return startClaudeSdkWorkerRun(input, emit);
369
372
  }
@@ -24,6 +24,11 @@ import {
24
24
  type RunStatus,
25
25
  runKindSupportsLiveSteering,
26
26
  } from "../domains/dispatch/types.js";
27
+ import {
28
+ summarizeTrustStatus,
29
+ type TrustSummaryProjection,
30
+ type TrustVerdict,
31
+ } from "../domains/evidence/trust-projection.js";
27
32
  import {
28
33
  COST_NOT_MEASURED,
29
34
  costAggregateForAmount,
@@ -113,6 +118,13 @@ export interface DispatchBoardRow {
113
118
  council?: { group: string; label: string; color?: string; round: number };
114
119
  /** Orchestrator-executed declared verification status. */
115
120
  hostVerification?: "verified" | "rejected" | "skipped";
121
+ /**
122
+ * The canonical trust projection of the sealed receipt, read back from
123
+ * disk and authenticated against the ledger row. Absent while the run is
124
+ * live or when the receipt could not be read back, so the card never
125
+ * implies a verdict the terminal bus event cannot carry.
126
+ */
127
+ trust?: TrustSummaryProjection;
116
128
  /** Dead-node failover hops recorded on this run's chain. */
117
129
  rerouteCount?: number;
118
130
  /** Assignment retry attempts observed on the assignment event stream. */
@@ -651,7 +663,15 @@ export function renderDispatchCard(
651
663
  `${theme.fg("dim", "ttft")} ${theme.fg("muted", ttft)}`,
652
664
  `${theme.fg("dim", "cost")} ${theme.fg("muted", cost)}`,
653
665
  ];
654
- const hostVerification = theme.fg("info", `host_verification=${row.hostVerification ?? "not_requested"}`);
666
+ // The trust line is the canonical projection, styled by its verdict tier;
667
+ // a host check is folded into that projection as validation grounding, so
668
+ // it is named as a secondary fact and never as independent verification.
669
+ const trustUnits = [
670
+ row.trust === undefined
671
+ ? theme.fg("dim", isTerminalStatus(row.status) ? "trust: receipt not read back" : "trust: not sealed yet")
672
+ : theme.fg(trustVerdictToken(row.trust.verdict), `${trustVerdictGlyph(row.trust.verdict)} ${row.trust.text}`),
673
+ ...(row.hostVerification !== undefined ? [theme.fg("muted", `host checks ${row.hostVerification}`)] : []),
674
+ ];
655
675
  const contextUnit = formatWorkerContextMeter(row.lastContextTokens ?? 0, row.contextWindow, theme);
656
676
  // The phase column: a fleet step says which wave it belongs to and which
657
677
  // step it is; every other run leaves the cell empty rather than inventing a
@@ -670,7 +690,7 @@ export function renderDispatchCard(
670
690
  ? [truncateToWidth(`${cardKvKey(theme, "task")}${theme.fg("muted", row.taskSummary)}`, contentWidth, "…", false)]
671
691
  : []),
672
692
  cardUnitsLine(theme, "status", statusUnits, contentWidth),
673
- cardUnitsLine(theme, "evidence", [hostVerification], contentWidth),
693
+ cardUnitsLine(theme, "trust", trustUnits, contentWidth),
674
694
  ...(row.budget !== undefined
675
695
  ? [
676
696
  truncateToWidth(
@@ -1160,6 +1180,33 @@ function parseOptionalDetail(value: unknown): string | null {
1160
1180
  return detail.length > 0 ? detail : null;
1161
1181
  }
1162
1182
 
1183
+ /** Only an authenticated independent pass earns the success token; sealed alone is never green. */
1184
+ function trustVerdictToken(verdict: TrustVerdict): ClioToken {
1185
+ switch (verdict) {
1186
+ case "reviewed":
1187
+ return "success";
1188
+ case "grounded":
1189
+ return "info";
1190
+ case "unverified":
1191
+ return "muted";
1192
+ case "compromised":
1193
+ return "error";
1194
+ default:
1195
+ return "dim";
1196
+ }
1197
+ }
1198
+
1199
+ function trustVerdictGlyph(verdict: TrustVerdict): string {
1200
+ switch (verdict) {
1201
+ case "reviewed":
1202
+ return GLYPH.ok;
1203
+ case "compromised":
1204
+ return GLYPH.error;
1205
+ default:
1206
+ return "◇";
1207
+ }
1208
+ }
1209
+
1163
1210
  function terminalDetail(row: DispatchBoardRow): string | null {
1164
1211
  if (row.status !== "failed" && row.status !== "dead" && row.status !== "aborted") return null;
1165
1212
  return parseOptionalDetail(row.outcomeDetail);
@@ -1319,6 +1366,7 @@ function toRow(entry: DispatchBoardEntry, now: number): DispatchBoardRow {
1319
1366
  ...(entry.gate !== undefined ? { gate: { ...entry.gate } } : {}),
1320
1367
  ...(entry.council !== undefined ? { council: { ...entry.council } } : {}),
1321
1368
  ...(entry.hostVerification !== undefined ? { hostVerification: entry.hostVerification } : {}),
1369
+ ...(entry.trust !== undefined ? { trust: entry.trust } : {}),
1322
1370
  ...(entry.rerouteCount !== undefined ? { rerouteCount: entry.rerouteCount } : {}),
1323
1371
  ...(entry.failoverHops !== undefined ? { failoverHops: entry.failoverHops } : {}),
1324
1372
  ...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
@@ -1394,8 +1442,10 @@ export function createDispatchBoardStore(
1394
1442
  * before it publishes one, so this is the first moment the file exists.
1395
1443
  */
1396
1444
  const settleFromReceipt = (entry: DispatchBoardEntry): void => {
1397
- const text = readReceipt?.(entry.runId)?.text;
1445
+ const facts = readReceipt?.(entry.runId);
1446
+ const text = facts?.text;
1398
1447
  entry.progress.settle(typeof text === "string" && text.trim().length > 0 ? text : undefined);
1448
+ if (facts?.trust !== undefined) entry.trust = summarizeTrustStatus(facts.trust);
1399
1449
  };
1400
1450
 
1401
1451
  // Payloads arrive typed off the bus, but the board keeps its runtime
@@ -3,6 +3,7 @@ import {
3
3
  type ConfigReloadFailedPayload,
4
4
  type ContextPrunedPayload,
5
5
  type ContextWarningPayload,
6
+ type DispatchScopeNoticePayload,
6
7
  type LoopBlockedPayload,
7
8
  type RuntimeNoticePayload,
8
9
  type ToolBudgetExceededPayload,
@@ -271,6 +272,19 @@ export function createInteractiveEventProjection(deps: InteractiveEventProjectio
271
272
  );
272
273
 
273
274
  remainingUnsubscribers.push(
275
+ deps.bus.on(BusChannels.DispatchScopeNotice, (payload) => {
276
+ const event = payload as DispatchScopeNoticePayload | null | undefined;
277
+ if (
278
+ !event ||
279
+ typeof event !== "object" ||
280
+ !["typed_scope_replaced_inferred_paths", "legacy_scope_inferred", "legacy_scope_empty"].includes(event.code) ||
281
+ typeof event.message !== "string"
282
+ ) {
283
+ return;
284
+ }
285
+ deps.appendTranscriptNotice("warn", event.message);
286
+ deps.requestRender();
287
+ }),
274
288
  deps.bus.on(BusChannels.LoopBlocked, (payload) => {
275
289
  const event = payload as LoopBlockedPayload | null | undefined;
276
290
  if (!event || typeof event !== "object" || typeof event.tool !== "string" || typeof event.repeatCount !== "number") {