@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
@@ -1,5 +1,5 @@
1
1
  import type { GateDecisionArtifact, GateDecisionVerification } from "../dispatch/gate-decisions.js";
2
- import { type ReceiptIntegrityResult, verifyReceiptIntegrity } from "../dispatch/receipt-integrity.js";
2
+ import { RUN_RECEIPT_INTEGRITY_VERSION, verifyReceiptIntegrity } from "../dispatch/receipt-integrity.js";
3
3
  import type {
4
4
  RunBriefingProvenance,
5
5
  RunEnvelope,
@@ -459,13 +459,89 @@ function compatibilitySource(receipt: PersistedRunReceiptTrustFacts, field: stri
459
459
  const DISPATCH_AUTHORITY: TrustStatusAuthority = { kind: "clio", id: "dispatch" };
460
460
  const COMPATIBILITY_AUTHORITY: TrustStatusAuthority = { kind: "unknown", id: "historical-persisted-format" };
461
461
 
462
+ /**
463
+ * A receipt sealed under an integrity version this build no longer verifies.
464
+ * It is not migrated and not read as evidence, on the same terms as every
465
+ * prior integrity bump, but it is not a tampered receipt either: nothing was
466
+ * checked, so nothing failed. The outcome names both versions so an operator
467
+ * with a store full of last release's runs can tell a retired seal from a
468
+ * broken one.
469
+ */
470
+ export interface RetiredReceiptIntegrity {
471
+ receiptVersion: number;
472
+ supportedVersion: number;
473
+ }
474
+
475
+ /**
476
+ * The dispatch verifier's result, widened with the retired-format diagnosis
477
+ * this module puts in front of it. A failure without `retired` is the
478
+ * verifier's own verdict: a malformed seal, a ledger mismatch, or a digest
479
+ * that does not match the receipt's contents.
480
+ */
481
+ export type ReceiptIntegrityOutcome = { ok: true } | { ok: false; reason: string; retired?: RetiredReceiptIntegrity };
482
+
483
+ /** The retired-version diagnosis for a receipt's integrity block, or null when the block is current or unreadable. */
484
+ export function retiredReceiptIntegrity(integrity: unknown): RetiredReceiptIntegrity | null {
485
+ if (!isRecord(integrity)) return null;
486
+ const version = integrity.version;
487
+ if (typeof version !== "number" || !Number.isInteger(version) || version < 1) return null;
488
+ if (version >= RUN_RECEIPT_INTEGRITY_VERSION) return null;
489
+ return { receiptVersion: version, supportedVersion: RUN_RECEIPT_INTEGRITY_VERSION };
490
+ }
491
+
492
+ /** The one sentence every surface prints for a retired seal. It never calls the receipt invalid, broken, or corrupt. */
493
+ export function retiredReceiptIntegrityReason(retired: RetiredReceiptIntegrity): string {
494
+ return `receipt integrity v${retired.receiptVersion} is retired; this build verifies v${retired.supportedVersion}; the receipt is not read as evidence`;
495
+ }
496
+
497
+ /**
498
+ * Verify a receipt against its ledger row, diagnosing a retired seal before
499
+ * the verifier can report it as a malformed one. Every trust-bearing
500
+ * verification goes through here so the CLI, the bundle, and the TUI receipt
501
+ * view agree on what a retired receipt is.
502
+ */
503
+ export function verifyReceiptIntegrityOutcome(receipt: RunReceipt, envelope: RunEnvelope): ReceiptIntegrityOutcome {
504
+ const retired = retiredReceiptIntegrity(receipt.integrity);
505
+ if (retired !== null) return { ok: false, reason: retiredReceiptIntegrityReason(retired), retired };
506
+ return verifyReceiptIntegrity(receipt, envelope);
507
+ }
508
+
509
+ const RETIRED_INTEGRITY_SOURCE = /:integrity-v(\d+)-retired$/u;
510
+
511
+ function retiredIntegritySourceId(runId: string, receiptVersion: number): string {
512
+ return `run_receipt:${runId}:integrity-v${receiptVersion}-retired`;
513
+ }
514
+
515
+ /**
516
+ * The retired integrity version a projected artifact-integrity axis names, or
517
+ * null when the axis says something else. The compatibility source id is the
518
+ * only place the persisted projection carries the version, so the human clause
519
+ * reads it back from there rather than from the receipt.
520
+ */
521
+ export function retiredIntegrityVersionOf(status: ArtifactIntegrityStatus): number | null {
522
+ if (status.state !== "unknown" || status.source.kind !== "compatibility") return null;
523
+ const match = RETIRED_INTEGRITY_SOURCE.exec(status.source.id);
524
+ return match === null ? null : Number(match[1]);
525
+ }
526
+
462
527
  /** Adapt receipt integrity verification without reading any receipt claim as self-authenticating. */
463
528
  export function adaptReceiptIntegrityStatus(
464
529
  receipt: PersistedRunReceiptTrustFacts | null | undefined,
465
- verification?: ReceiptIntegrityResult,
530
+ verification?: ReceiptIntegrityOutcome,
466
531
  ): ArtifactIntegrityStatus {
467
532
  if (receipt === null || receipt === undefined) return absentTrustStatus("artifact_missing");
468
533
  const artifacts = [receiptReference(receipt)];
534
+ // A retired seal was never checked, so the axis is unknown rather than
535
+ // failed, and the compatibility source names the version it was sealed
536
+ // under. `failed` stays reserved for a seal this build verified and rejected.
537
+ if (verification !== undefined && !verification.ok && verification.retired !== undefined) {
538
+ return attributed(
539
+ "unknown",
540
+ { kind: "compatibility", id: retiredIntegritySourceId(receipt.runId, verification.retired.receiptVersion) },
541
+ COMPATIBILITY_AUTHORITY,
542
+ artifacts,
543
+ );
544
+ }
469
545
  if (verification === undefined || (verification.ok && receipt.integrity === undefined)) {
470
546
  return attributed(
471
547
  "unknown",
@@ -576,15 +652,46 @@ function validBoundedProjectContext(context: RunProjectContextProvenance): boole
576
652
  );
577
653
  }
578
654
 
655
+ /**
656
+ * The one structured message a `none`-tier run can still receive. The tier is
657
+ * the CLIO-CODER.md handbook policy; the workspace-root message is sent
658
+ * regardless of it, and the producer records the characters and hash of
659
+ * every structured message it sent (`projectContextProvenanceFor` in the
660
+ * dispatch extension). A none-tier block naming exactly this section, with
661
+ * a well-formed count and hash, is therefore a consistent record of a real
662
+ * message, not a contradiction. Anything else under a none tier cannot be
663
+ * explained by the policy: a handbook section the policy forbids, a hash
664
+ * with no section to hash, or a malformed count.
665
+ */
666
+ const NONE_TIER_SECTIONS: ReadonlyArray<string> = ["workspace-root"];
667
+
668
+ function validNoneTierProjectContext(context: RunProjectContextProvenance): boolean {
669
+ return (
670
+ context.tier === "none" &&
671
+ context.sections !== undefined &&
672
+ context.sections.length === NONE_TIER_SECTIONS.length &&
673
+ context.sections.every((section) => NONE_TIER_SECTIONS.includes(section)) &&
674
+ Number.isSafeInteger(context.chars) &&
675
+ (context.chars ?? -1) >= 0 &&
676
+ typeof context.contentHash === "string" &&
677
+ /^[a-f0-9]{64}$/u.test(context.contentHash)
678
+ );
679
+ }
680
+
681
+ function recordedProjectContextHash(context: RunProjectContextProvenance | undefined): string | undefined {
682
+ if (context === undefined) return undefined;
683
+ if (context.tier === "bounded") return validBoundedProjectContext(context) ? context.contentHash : undefined;
684
+ return validNoneTierProjectContext(context) ? context.contentHash : undefined;
685
+ }
686
+
579
687
  function contextArtifacts(receipt: PersistedRunReceiptTrustFacts): TrustArtifactReference[] {
580
688
  const artifacts = [receiptReference(receipt)];
581
689
  if (receipt.briefing !== undefined && validBriefing(receipt.briefing)) {
582
690
  artifacts.push(sha256Reference("briefing", `${receipt.runId}:briefing`, receipt.briefing.contentHash));
583
691
  }
584
- if (receipt.projectContext?.tier === "bounded" && validBoundedProjectContext(receipt.projectContext)) {
585
- artifacts.push(
586
- sha256Reference("project_context", `${receipt.runId}:project-context`, receipt.projectContext.contentHash),
587
- );
692
+ const recordedHash = recordedProjectContextHash(receipt.projectContext);
693
+ if (recordedHash !== undefined) {
694
+ artifacts.push(sha256Reference("project_context", `${receipt.runId}:project-context`, recordedHash));
588
695
  }
589
696
  return uniqueBoundedReferences(artifacts);
590
697
  }
@@ -604,12 +711,23 @@ export function adaptRunReceiptContextStatus(
604
711
  return attributed("unknown", compatibilitySource(receipt, "projectContext"), COMPATIBILITY_AUTHORITY, artifacts);
605
712
  }
606
713
  if (context.tier === "none") {
607
- if (context.chars !== undefined || context.contentHash !== undefined || context.sections !== undefined) {
608
- return attributed("invalid", source, DISPATCH_AUTHORITY, artifacts);
714
+ const bare = context.chars === undefined && context.contentHash === undefined && context.sections === undefined;
715
+ if (bare) {
716
+ // Nothing structured was sent: the axis does not apply unless a
717
+ // briefing was recorded, which is context of its own.
718
+ return receipt.briefing === undefined
719
+ ? attributed("not_applicable", source, DISPATCH_AUTHORITY, artifacts)
720
+ : attributed("recorded", source, DISPATCH_AUTHORITY, artifacts);
609
721
  }
610
- return receipt.briefing === undefined
611
- ? attributed("not_applicable", source, DISPATCH_AUTHORITY, artifacts)
612
- : attributed("recorded", source, DISPATCH_AUTHORITY, artifacts);
722
+ // The workspace-root message is the one thing a none-tier run receives,
723
+ // and its record is valid; every other none-tier content shape is a
724
+ // record the policy cannot account for.
725
+ return attributed(
726
+ validNoneTierProjectContext(context) ? "recorded" : "invalid",
727
+ source,
728
+ DISPATCH_AUTHORITY,
729
+ artifacts,
730
+ );
613
731
  }
614
732
  return attributed(validBoundedProjectContext(context) ? "recorded" : "invalid", source, DISPATCH_AUTHORITY, artifacts);
615
733
  }
@@ -639,18 +757,19 @@ export function adaptRunReceiptAutonomyStatus(
639
757
  }
640
758
 
641
759
  export interface AdaptRunReceiptTrustOptions {
642
- integrity?: ReceiptIntegrityResult;
760
+ integrity?: ReceiptIntegrityOutcome;
643
761
  }
644
762
 
645
763
  function receiptClaimsAreAuthenticated(
646
764
  receipt: PersistedRunReceiptTrustFacts | null | undefined,
647
- integrity: ReceiptIntegrityResult | undefined,
765
+ integrity: ReceiptIntegrityOutcome | undefined,
648
766
  ): boolean {
649
767
  return receipt?.integrity !== undefined && integrity?.ok === true;
650
768
  }
651
769
 
652
770
  function unauthenticatedReceiptProjection(
653
771
  receipt: PersistedRunReceiptTrustFacts | null | undefined,
772
+ integrity: ReceiptIntegrityOutcome | undefined,
654
773
  ): TrustStatusProjection {
655
774
  if (receipt === null || receipt === undefined) {
656
775
  return {
@@ -682,6 +801,15 @@ function unauthenticatedReceiptProjection(
682
801
  ),
683
802
  };
684
803
  }
804
+ // A retired seal leaves the receipt unread rather than rejected: its axes
805
+ // are absent because the format is historical, not because a check failed.
806
+ if (integrity !== undefined && !integrity.ok && integrity.retired !== undefined) {
807
+ return {
808
+ validationGrounding: absentTrustStatus("historical_format"),
809
+ contextProvenance: absentTrustStatus("historical_format"),
810
+ autonomyEnforcement: absentTrustStatus("historical_format"),
811
+ };
812
+ }
685
813
  // The artifact-integrity axis retains the failure diagnostic. No other
686
814
  // axis reads a field from a receipt that failed authentication.
687
815
  return {
@@ -703,7 +831,7 @@ export function adaptRunReceiptTrustStatus(
703
831
  contextProvenance: adaptRunReceiptContextStatus(receipt),
704
832
  autonomyEnforcement: adaptRunReceiptAutonomyStatus(receipt),
705
833
  }
706
- : unauthenticatedReceiptProjection(receipt);
834
+ : unauthenticatedReceiptProjection(receipt, options.integrity);
707
835
  return composeTrustStatus({
708
836
  artifactIntegrity: adaptReceiptIntegrityStatus(receipt, options.integrity),
709
837
  ...receiptProjection,
@@ -715,7 +843,7 @@ export function adaptRunReceiptTrustStatus(
715
843
  }
716
844
 
717
845
  export interface RunReceiptTrustInspection {
718
- integrity: ReceiptIntegrityResult;
846
+ integrity: ReceiptIntegrityOutcome;
719
847
  status: CanonicalTrustStatus;
720
848
  }
721
849
 
@@ -728,14 +856,14 @@ export function inspectRunReceiptTrustStatus(
728
856
  receipt: RunReceipt | null | undefined,
729
857
  envelope: RunEnvelope | null | undefined,
730
858
  ): RunReceiptTrustInspection {
731
- let integrity: ReceiptIntegrityResult;
859
+ let integrity: ReceiptIntegrityOutcome;
732
860
  if (receipt === null || receipt === undefined) {
733
861
  integrity = { ok: false, reason: "receipt unavailable" };
734
862
  } else if (envelope === null || envelope === undefined) {
735
863
  integrity = { ok: false, reason: "run ledger envelope unavailable" };
736
864
  } else {
737
865
  try {
738
- integrity = verifyReceiptIntegrity(receipt, envelope);
866
+ integrity = verifyReceiptIntegrityOutcome(receipt, envelope);
739
867
  } catch (error) {
740
868
  integrity = {
741
869
  ok: false,
@@ -23,7 +23,11 @@ export const EVIDENCE_TAGS = [
23
23
  "escalation",
24
24
  "external-bypass",
25
25
  "external-approximation",
26
+ "independent-review",
27
+ "context-provenance",
28
+ "completion-evidence",
26
29
  "receipt-integrity",
30
+ "receipt-retired",
27
31
  "protected-artifact",
28
32
  "tool-loop",
29
33
  "test-failure",
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
1
+ import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
4
4
  import type {
@@ -39,13 +39,99 @@ function normalizeResources(value: unknown): ExtensionManifestResources {
39
39
  const out: ExtensionManifestResources = {};
40
40
  const skills = trimString(value.skills);
41
41
  const prompts = trimString(value.prompts);
42
+ const agents = trimString(value.agents);
43
+ const fleets = trimString(value.fleets);
42
44
  const themes = trimString(value.themes);
43
45
  if (skills) out.skills = skills;
44
46
  if (prompts) out.prompts = prompts;
47
+ if (agents) out.agents = agents;
48
+ if (fleets) out.fleets = fleets;
45
49
  if (themes) out.themes = themes;
46
50
  return out;
47
51
  }
48
52
 
53
+ function contained(root: string, candidate: string): boolean {
54
+ const relative = path.relative(root, candidate);
55
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
56
+ }
57
+
58
+ function validateResourceTree(
59
+ root: string,
60
+ resourcePath: string,
61
+ kind: string,
62
+ diagnostics: ExtensionDiagnostic[],
63
+ ): void {
64
+ if (path.isAbsolute(resourcePath)) {
65
+ diagnostics.push({ type: "error", message: `resources.${kind} must be relative`, path: resourcePath });
66
+ return;
67
+ }
68
+ const resolvedRoot = path.resolve(root);
69
+ const resolved = path.resolve(resolvedRoot, resourcePath);
70
+ if (!contained(resolvedRoot, resolved) || resolved === resolvedRoot) {
71
+ diagnostics.push({ type: "error", message: `resources.${kind} escapes the extension root`, path: resourcePath });
72
+ return;
73
+ }
74
+ let canonicalRoot: string;
75
+ let canonicalResource: string;
76
+ try {
77
+ canonicalRoot = realpathSync(resolvedRoot);
78
+ canonicalResource = realpathSync(resolved);
79
+ if (!lstatSync(resolved).isDirectory()) {
80
+ diagnostics.push({ type: "error", message: `resources.${kind} is not a directory`, path: resolved });
81
+ return;
82
+ }
83
+ } catch (error) {
84
+ diagnostics.push({
85
+ type: "error",
86
+ message: `resources.${kind} cannot be resolved: ${error instanceof Error ? error.message : String(error)}`,
87
+ path: resolved,
88
+ });
89
+ return;
90
+ }
91
+ if (!contained(canonicalRoot, canonicalResource)) {
92
+ diagnostics.push({ type: "error", message: `resources.${kind} escapes through a symbolic link`, path: resolved });
93
+ return;
94
+ }
95
+
96
+ const pending = [resolved];
97
+ while (pending.length > 0) {
98
+ const directory = pending.pop();
99
+ if (!directory) continue;
100
+ for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
101
+ const candidate = path.join(directory, entry.name);
102
+ if (entry.isSymbolicLink()) {
103
+ try {
104
+ if (!contained(canonicalRoot, realpathSync(candidate))) {
105
+ diagnostics.push({
106
+ type: "error",
107
+ message: `resources.${kind} contains an escaping symbolic link`,
108
+ path: candidate,
109
+ });
110
+ }
111
+ } catch (error) {
112
+ diagnostics.push({
113
+ type: "error",
114
+ message: `resources.${kind} contains an unresolved symbolic link: ${error instanceof Error ? error.message : String(error)}`,
115
+ path: candidate,
116
+ });
117
+ }
118
+ } else if (entry.isDirectory()) {
119
+ pending.push(candidate);
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ function validateResourceRoots(
126
+ root: string,
127
+ resources: ExtensionManifestResources,
128
+ diagnostics: ExtensionDiagnostic[],
129
+ ): void {
130
+ for (const [kind, resourcePath] of Object.entries(resources)) {
131
+ if (resourcePath) validateResourceTree(root, resourcePath, kind, diagnostics);
132
+ }
133
+ }
134
+
49
135
  function stringArray(value: unknown): string[] | undefined {
50
136
  if (!Array.isArray(value)) return undefined;
51
137
  const out = value.map((entry) => trimString(entry)).filter((entry): entry is string => entry !== undefined);
@@ -123,6 +209,7 @@ export function loadManifestFromRoot(root: string): ExtensionCandidate {
123
209
  }
124
210
  try {
125
211
  const parsed = parseExtensionManifest(readJsonOrYaml(manifestPath), manifestPath);
212
+ if (parsed.manifest) validateResourceRoots(root, parsed.manifest.resources, parsed.diagnostics);
126
213
  return {
127
214
  path: root,
128
215
  manifestPath,
@@ -1,4 +1,4 @@
1
- import { statSync } from "node:fs";
1
+ import { lstatSync, realpathSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { listInstalledExtensions } from "./state.js";
4
4
  import type { ExtensionResourceKind, ExtensionResourceRoot } from "./types.js";
@@ -7,8 +7,24 @@ export function extensionResourcePath(rootPath: string, resourcePath: string): s
7
7
  const root = path.resolve(rootPath);
8
8
  const full = path.resolve(root, resourcePath);
9
9
  const relative = path.relative(root, full);
10
- if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) return full;
11
- return null;
10
+ if (relative === "" || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
11
+ return null;
12
+ try {
13
+ const rootReal = realpathSync(root);
14
+ const fullReal = realpathSync(full);
15
+ const canonicalRelative = path.relative(rootReal, fullReal);
16
+ if (
17
+ canonicalRelative === ".." ||
18
+ canonicalRelative.startsWith(`..${path.sep}`) ||
19
+ path.isAbsolute(canonicalRelative)
20
+ ) {
21
+ return null;
22
+ }
23
+ if (!lstatSync(full).isDirectory()) return null;
24
+ return full;
25
+ } catch {
26
+ return null;
27
+ }
12
28
  }
13
29
 
14
30
  export function enabledExtensionResourceRoots(
@@ -22,15 +38,11 @@ export function enabledExtensionResourceRoots(
22
38
  if (!rel) continue;
23
39
  const full = extensionResourcePath(entry.rootPath, rel);
24
40
  if (!full) continue;
25
- try {
26
- if (!statSync(full).isDirectory()) continue;
27
- } catch {
28
- continue;
29
- }
30
41
  roots.push({
31
42
  id: entry.id,
32
43
  scope: entry.scope,
33
44
  path: full,
45
+ rootPath: entry.rootPath,
34
46
  source: `extension:${entry.scope}:${entry.id}`,
35
47
  });
36
48
  }
@@ -129,7 +129,7 @@ export function installExtension(sourcePath: string, options: ExtensionInstallOp
129
129
  const cwd = options.cwd ?? process.cwd();
130
130
  const source = path.resolve(sourcePath);
131
131
  const candidate = loadManifestFromRoot(source);
132
- if (!candidate.manifest) return { diagnostics: candidate.diagnostics };
132
+ if (!candidate.manifest || !candidate.valid) return { diagnostics: candidate.diagnostics };
133
133
  const targetRoot = path.join(extensionBaseDir(scope, cwd), candidate.manifest.id);
134
134
  if (existsSync(targetRoot)) {
135
135
  if (!options.force) {
@@ -152,7 +152,11 @@ export function installExtension(sourcePath: string, options: ExtensionInstallOp
152
152
  rmSync(backupRoot, { recursive: true, force: true });
153
153
  cpSync(source, stagingRoot, {
154
154
  recursive: true,
155
- filter: (src) => path.basename(src) !== "state.json",
155
+ // `state.json` beside the installed extension directories belongs to
156
+ // Clio's extension manager. An extension may legitimately ship nested
157
+ // resources with the same basename (for example a project-state JSON
158
+ // template), so exclude only a bookkeeping file at the package root.
159
+ filter: (src) => path.relative(source, src) !== "state.json",
156
160
  });
157
161
  if (existsSync(targetRoot)) {
158
162
  renameSync(targetRoot, backupRoot);
@@ -1,9 +1,11 @@
1
1
  export type ExtensionScope = "user" | "project";
2
- export type ExtensionResourceKind = "skills" | "prompts" | "themes";
2
+ export type ExtensionResourceKind = "skills" | "prompts" | "agents" | "fleets" | "themes";
3
3
 
4
4
  export interface ExtensionManifestResources {
5
5
  skills?: string;
6
6
  prompts?: string;
7
+ agents?: string;
8
+ fleets?: string;
7
9
  themes?: string;
8
10
  }
9
11
 
@@ -52,6 +54,7 @@ export interface ExtensionResourceRoot {
52
54
  id: string;
53
55
  scope: ExtensionScope;
54
56
  path: string;
57
+ rootPath: string;
55
58
  source: string;
56
59
  }
57
60
 
@@ -5,8 +5,11 @@ import { initializeClioHome } from "../../core/init.js";
5
5
  import { resolveClioDirs } from "../../core/xdg.js";
6
6
  import { readSessionFileEntries, type SessionJsonlWarning } from "../../engine/session.js";
7
7
  import { detectInteropAgents, interopAgentKind, resolveOnPath } from "../interop/index.js";
8
- import { openAuthStorage } from "../providers/auth/index.js";
8
+ import { openAuthStorage, resolveAuthTarget, targetRequiresAuth } from "../providers/auth/index.js";
9
+ import { credentialsPresent } from "../providers/credentials.js";
9
10
  import { fingerprintNativeRuntime } from "../providers/probe/fingerprint.js";
11
+ import type { ProbeContext, ProbeResult, RuntimeDescriptor } from "../providers/types/runtime-descriptor.js";
12
+ import type { TargetDescriptor } from "../providers/types/target-descriptor.js";
10
13
  import { loadSkills, type SkillSource } from "../resources/skills/loader.js";
11
14
  import { readStateInfoResult } from "./state.js";
12
15
  import { getVersionInfo } from "./version.js";
@@ -534,3 +537,139 @@ export async function runDoctorRuntimeChecks(): Promise<DoctorFinding[]> {
534
537
  );
535
538
  return results.filter((finding): finding is DoctorFinding => finding !== null);
536
539
  }
540
+
541
+ /** One configured model pointer at a target, named the way settings.yaml spells it. */
542
+ interface ConfiguredModelRole {
543
+ role: string;
544
+ model: string;
545
+ }
546
+
547
+ function configuredModelRoles(
548
+ settings: ReturnType<typeof readSettings>,
549
+ target: TargetDescriptor,
550
+ ): ConfiguredModelRole[] {
551
+ const roles: ConfiguredModelRole[] = [];
552
+ if (target.defaultModel) roles.push({ role: "defaultModel", model: target.defaultModel });
553
+ if (settings.orchestrator.target === target.id && settings.orchestrator.model) {
554
+ roles.push({ role: "orchestrator.model", model: settings.orchestrator.model });
555
+ }
556
+ if (settings.background.target === target.id && settings.background.model) {
557
+ roles.push({ role: "background.model", model: settings.background.model });
558
+ }
559
+ if (settings.workers.default.target === target.id && settings.workers.default.model) {
560
+ roles.push({ role: "workers.default.model", model: settings.workers.default.model });
561
+ }
562
+ return roles;
563
+ }
564
+
565
+ /**
566
+ * Per-request budget for the model sweep. A target that does not answer in
567
+ * this time falls back to the list configure recorded, so a black-holed
568
+ * remote costs doctor a bounded wait rather than the probe's full timeout.
569
+ */
570
+ const DOCTOR_MODEL_PROBE_TIMEOUT_MS = 2_500;
571
+
572
+ async function doctorProbeContext(target: TargetDescriptor, runtime: RuntimeDescriptor): Promise<ProbeContext> {
573
+ const ctx: ProbeContext = { credentialsPresent: credentialsPresent(), httpTimeoutMs: DOCTOR_MODEL_PROBE_TIMEOUT_MS };
574
+ if (!targetRequiresAuth(target, runtime)) return ctx;
575
+ try {
576
+ const resolution = await openAuthStorage().resolveForTarget(resolveAuthTarget(target, runtime), {
577
+ includeFallback: false,
578
+ });
579
+ if (resolution.apiKey) ctx.authToken = resolution.apiKey;
580
+ } catch {
581
+ // The probe reports its own missing-auth failure; the wireModels fallback covers the check.
582
+ }
583
+ return ctx;
584
+ }
585
+
586
+ /** What the target advertises now, or null when it could not be asked. */
587
+ async function probeAdvertisedModels(
588
+ target: TargetDescriptor,
589
+ runtime: RuntimeDescriptor,
590
+ ): Promise<{ advertised: string[]; resident: string[] } | null> {
591
+ if (runtime.kind !== "http" || typeof runtime.probe !== "function" || !target.url) return null;
592
+ let probe: ProbeResult;
593
+ try {
594
+ probe = await runtime.probe(target, await doctorProbeContext(target, runtime));
595
+ } catch {
596
+ return null;
597
+ }
598
+ if (!probe.ok || !probe.models || probe.models.length === 0) return null;
599
+ const advertised = [...probe.models];
600
+ const resident: string[] = [];
601
+ for (const [id, status] of Object.entries(probe.modelStates ?? {})) {
602
+ if (!advertised.includes(id)) advertised.push(id);
603
+ if (status.state === "loaded" || status.state === "loading") resident.push(id);
604
+ }
605
+ return { advertised, resident };
606
+ }
607
+
608
+ /**
609
+ * Model sweep: every model pointer settings.yaml aims at a target with no
610
+ * static catalog is checked against what that target advertises. The live
611
+ * list wins when the target answers; the `wireModels` list configure recorded
612
+ * stands in when it does not, so an unreachable server still gets the
613
+ * placeholder id it was saved with called out. A target with neither list is
614
+ * a WARN, not a pass, because nothing was verified. Network-bound like the
615
+ * runtime sweep, so it is not part of the synchronous `runDoctor()` core.
616
+ */
617
+ export async function runDoctorModelChecks(): Promise<DoctorFinding[]> {
618
+ let settings: ReturnType<typeof readSettings>;
619
+ try {
620
+ settings = readSettings();
621
+ } catch {
622
+ return [];
623
+ }
624
+ if (settings.targets.length === 0) return [];
625
+ // Every runtime descriptor and the model catalog sit behind these two
626
+ // imports. They are loaded here, not at module load, so a doctor run on a
627
+ // home with no targets (the `--fix` seed every CLI test starts from) pays
628
+ // nothing for a sweep it has nothing to do.
629
+ const [{ getRuntimeRegistry }, { registerBuiltinRuntimes }, { listKnownModelsForRuntime }] = await Promise.all([
630
+ import("../providers/registry.js"),
631
+ import("../providers/runtimes/builtins.js"),
632
+ import("../providers/support.js"),
633
+ ]);
634
+ const registry = getRuntimeRegistry();
635
+ // doctor never loads the providers domain, so the registry is empty here
636
+ // unless another command in this process filled it.
637
+ if (registry.list().length === 0) registerBuiltinRuntimes(registry);
638
+ const results = await Promise.all(
639
+ settings.targets.map(async (target): Promise<DoctorFinding | null> => {
640
+ const runtime = registry.get(target.runtime);
641
+ if (!runtime) return null;
642
+ // Cloud runtimes are validated against their catalog at configure time.
643
+ if (listKnownModelsForRuntime(runtime.id).length > 0) return null;
644
+ const roles = configuredModelRoles(settings, target);
645
+ if (roles.length === 0) return null;
646
+ const live = await probeAdvertisedModels(target, runtime);
647
+ const recorded = target.wireModels ?? [];
648
+ if (live === null && recorded.length === 0) {
649
+ return {
650
+ ok: true,
651
+ level: "warn",
652
+ name: `model ${target.id}`,
653
+ detail: `${roles.map((entry) => `${entry.role} '${entry.model}'`).join(", ")} could not be verified: the target did not answer and configure recorded no model list; run \`clio-coder targets --probe\` once it is up`,
654
+ };
655
+ }
656
+ const advertised = live ? live.advertised : recorded;
657
+ const source = live ? `advertised by ${target.url ?? target.id} now` : "recorded by configure at last save";
658
+ const missing = roles.filter((entry) => !advertised.includes(entry.model));
659
+ if (missing.length === 0) {
660
+ return {
661
+ ok: true,
662
+ name: `model ${target.id}`,
663
+ detail: `${roles.map((entry) => `${entry.role} '${entry.model}'`).join(", ")} ${source}`,
664
+ };
665
+ }
666
+ const resident = live && live.resident.length > 0 ? live.resident.join(", ") : live ? "none" : "unknown";
667
+ return {
668
+ ok: false,
669
+ name: `model ${target.id}`,
670
+ detail: `${missing.map((entry) => `${entry.role} '${entry.model}'`).join(", ")} not ${source} (${advertised.length} ids). Resident instances: ${resident}. Re-run \`clio-coder configure --id ${target.id} --model <advertised id>\`; \`clio-coder targets --probe\` lists them`,
671
+ };
672
+ }),
673
+ );
674
+ return results.filter((finding): finding is DoctorFinding => finding !== null);
675
+ }
@@ -15,11 +15,9 @@ export interface CompileWorkerPromptInput extends WorkerPromptInputs {
15
15
  /** Working directory the rule loader and repo-awareness checks run against; defaults to `process.cwd()`. */
16
16
  cwd?: string;
17
17
  /**
18
- * Best-effort paths this worker's run actually touches (from `writeRoots`
19
- * and path-like tokens in the task/briefing text), used the same way the
20
- * session's `workingContextPaths` selects path-scoped project rules. There
21
- * is no structured path field on the model-facing dispatch tool, so this is
22
- * inference, not a guarantee: a rule can be missed, never fabricated.
18
+ * Canonical paths for this worker's declared intent. Legacy requests without
19
+ * intent retain path-like task and briefing inference. The prompt compiler
20
+ * consumes the resolved set without deriving another one.
23
21
  */
24
22
  workingContextPaths?: ReadonlyArray<string>;
25
23
  }