@iowarp/clio-coder 0.3.6 → 0.3.7

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 (258) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +16 -5
  3. package/dist/{acp-2BEHC4DL.js → acp-SK4MD6MM.js} +10 -10
  4. package/dist/{agents-LNNFTM53.js → agents-2FN2K6ME.js} +30 -25
  5. package/dist/assets/codewiki.json +1 -1
  6. package/dist/{auth-KXXFI2VS.js → auth-QIYZWM5I.js} +13 -13
  7. package/dist/{chunk-KHSFENX2.js → chunk-3HAPLH5M.js} +10 -10
  8. package/dist/{chunk-24I7BN55.js → chunk-465YSENW.js} +2 -2
  9. package/dist/{chunk-4OC57DA6.js → chunk-4DGYLA73.js} +53 -2
  10. package/dist/{chunk-CYQKWTG3.js → chunk-4DWFMQDR.js} +4 -4
  11. package/dist/{chunk-E2ER4LJF.js → chunk-5C3AQNDW.js} +25 -1
  12. package/dist/{chunk-22NAGB7X.js → chunk-5C77SEEY.js} +5 -94
  13. package/dist/{chunk-43AOLP7E.js → chunk-5FR74PWO.js} +2 -1
  14. package/dist/{chunk-K7T3E2SR.js → chunk-5UJ6ECTS.js} +10 -9
  15. package/dist/{chunk-6US73PDB.js → chunk-6M7VS3J3.js} +5 -5
  16. package/dist/{chunk-CJUB2JJ2.js → chunk-6TUKSZVF.js} +5 -5
  17. package/dist/{chunk-5JGRAMKL.js → chunk-AB4XIIVB.js} +8 -6
  18. package/dist/{chunk-R46L2BIR.js → chunk-BMWK7ZIZ.js} +14 -20
  19. package/dist/{chunk-4BPJXDWC.js → chunk-C4JBQ5SR.js} +30 -14
  20. package/dist/{chunk-XXQNGV4M.js → chunk-CEYBNUGC.js} +243 -63
  21. package/dist/{chunk-VEZEGCGW.js → chunk-D4MDIG46.js} +20 -18
  22. package/dist/chunk-DJNLUABN.js +843 -0
  23. package/dist/{chunk-RY3LY4J5.js → chunk-DMD2AGVS.js} +5 -4
  24. package/dist/{chunk-KOHPCX4K.js → chunk-DOOEX22V.js} +2 -2
  25. package/dist/chunk-DQA7QLMD.js +123 -0
  26. package/dist/chunk-DR52UMZW.js +21 -0
  27. package/dist/{chunk-XF5N4U5A.js → chunk-EBEFWSGL.js} +6 -5
  28. package/dist/{chunk-EYPA3EGJ.js → chunk-EELBMBT6.js} +120 -13
  29. package/dist/{chunk-CKXWIANG.js → chunk-EOOQZZDE.js} +16 -14
  30. package/dist/{chunk-WR67VIZY.js → chunk-FOT2FX5J.js} +63 -5
  31. package/dist/{chunk-FYYLNIL5.js → chunk-GH5622CP.js} +2 -2
  32. package/dist/chunk-GWS3VEIW.js +195 -0
  33. package/dist/{chunk-LYF7OHWH.js → chunk-J7PIKKWC.js} +8 -463
  34. package/dist/{chunk-NILBFAPG.js → chunk-JNXPYBB4.js} +2 -2
  35. package/dist/{chunk-4VP4KH3K.js → chunk-JRIO5UD2.js} +4 -4
  36. package/dist/{chunk-6XXKFVSN.js → chunk-JTSEDYVQ.js} +7 -7
  37. package/dist/{chunk-QKMUKYO7.js → chunk-KCMKRQX4.js} +236 -84
  38. package/dist/chunk-KZ2H5X4G.js +1026 -0
  39. package/dist/{chunk-QNQHSOLF.js → chunk-LADCF22A.js} +12 -12
  40. package/dist/chunk-M4AKACEO.js +382 -0
  41. package/dist/{chunk-XYDYPRZI.js → chunk-MXI6J5JF.js} +7 -7
  42. package/dist/{chunk-G7MUEIGA.js → chunk-OB5HIGJY.js} +1 -1
  43. package/dist/{chunk-EKY57CSP.js → chunk-OBMAI2DP.js} +61 -767
  44. package/dist/chunk-PD3MESLB.js +242 -0
  45. package/dist/{chunk-ZRGEBJ4T.js → chunk-QCTRSGHQ.js} +21 -21
  46. package/dist/chunk-RVG5JXAL.js +41 -0
  47. package/dist/{chunk-RD5U66HV.js → chunk-SROCI7ZU.js} +7 -7
  48. package/dist/{chunk-MFFY33HR.js → chunk-THKY7CD7.js} +466 -205
  49. package/dist/{chunk-PCZJO5TI.js → chunk-UFQ3F4FW.js} +13 -178
  50. package/dist/{chunk-AD2SYQYC.js → chunk-UHXRNZ2J.js} +121 -3
  51. package/dist/chunk-UND3GU2L.js +103 -0
  52. package/dist/{chunk-QM3F2GKX.js → chunk-UUANF5CR.js} +2247 -2096
  53. package/dist/chunk-UVDSQ6LW.js +472 -0
  54. package/dist/{chunk-DJVECN66.js → chunk-VQNODYQ4.js} +14 -14
  55. package/dist/{chunk-3BPUFZDL.js → chunk-VREKEFLL.js} +3 -3
  56. package/dist/{chunk-PBTHKCPN.js → chunk-WJHBC77E.js} +6 -6
  57. package/dist/{chunk-XE2VEJHX.js → chunk-X2KV5FXT.js} +2 -2
  58. package/dist/{chunk-ZXF4XRKW.js → chunk-XEGB6BCN.js} +157 -7
  59. package/dist/{chunk-E25LMLRW.js → chunk-YD734TPH.js} +2 -2
  60. package/dist/{verifiers-NCBTHHN2.js → chunk-YTYFXUI3.js} +65 -322
  61. package/dist/{chunk-OH3TOQTB.js → chunk-ZGH7FGS5.js} +13 -7
  62. package/dist/cli/index.js +32 -30
  63. package/dist/{clio-M2KGYUFZ.js → clio-WBVQEBKO.js} +7 -7
  64. package/dist/{code-nav-GQNL7XA6.js → code-nav-FGGFIE7L.js} +3 -3
  65. package/dist/{components-5TTYYX6G.js → components-F7OEATSO.js} +4 -4
  66. package/dist/{config-XUUYQIWO.js → config-TRBL3RCF.js} +34 -29
  67. package/dist/{configure-IHJ7YOMV.js → configure-OLCVPHNM.js} +15 -15
  68. package/dist/{context-74JLXAWD.js → context-MJIJ6GOX.js} +11 -11
  69. package/dist/{context-ZQ7SIFJV.js → context-WFPKQSM6.js} +19 -3
  70. package/dist/{context-75MIWW3U.js → context-XEWE3MOJ.js} +31 -26
  71. package/dist/{context-clear-GYKWNUML.js → context-clear-KNOS2JPB.js} +31 -26
  72. package/dist/{context-working-set-UX5KEP4J.js → context-working-set-EUXAZI6N.js} +8 -8
  73. package/dist/{dispatch-runner-GIJBHNFL.js → dispatch-runner-B7MTOVKL.js} +313 -53
  74. package/dist/{docs-6FZSCG5B.js → docs-FLJTIDSE.js} +4 -4
  75. package/dist/{doctor-SVJ5BZCW.js → doctor-RN4YKO2X.js} +14 -14
  76. package/dist/{eval-CG6LLBLD.js → eval-RUBJVSNQ.js} +8 -7
  77. package/dist/{evidence-ZYFIEN42.js → evidence-JZNBUOQZ.js} +30 -25
  78. package/dist/{evolve-QGEXEMDW.js → evolve-FJVC4KKI.js} +30 -25
  79. package/dist/{extensions-ADGNCJJD.js → extensions-IQL36S7K.js} +4 -4
  80. package/dist/{fleet-S5R4ZOQY.js → fleet-BDKYJFCP.js} +214 -360
  81. package/dist/fleet-commands-ZFIWZSB3.js +70 -0
  82. package/dist/fleet-graph-Y6HPXIVF.js +125 -0
  83. package/dist/fleet-new-RDVJLHHH.js +48 -0
  84. package/dist/fleet-validate-BIYREGIK.js +79 -0
  85. package/dist/{init-5DRU55YR.js → init-LQUB5COQ.js} +44 -37
  86. package/dist/library-NJAHIGG4.js +217 -0
  87. package/dist/{memory-7YKKR6UC.js → memory-OG6HOYKM.js} +31 -26
  88. package/dist/{models-ZPOLRU2C.js → models-5ZG5XY7J.js} +21 -20
  89. package/dist/{monitor-US5F5YGZ.js → monitor-TJ7AMTGB.js} +49 -30
  90. package/dist/{orchestrator-E2AL4T5N.js → orchestrator-WZYB54DM.js} +3827 -581
  91. package/dist/{paths-E7KYAQWE.js → paths-XUC7GS6E.js} +4 -4
  92. package/dist/{reset-KZ652EK6.js → reset-PXQT45IY.js} +7 -7
  93. package/dist/{run-SRNBKDWD.js → run-FQ74YF62.js} +53 -45
  94. package/dist/{share-CGZE33UP.js → share-FW7SVCL3.js} +33 -9
  95. package/dist/{skills-S2X4DLY5.js → skills-7E7IRB3R.js} +24 -8
  96. package/dist/{skills-eval-W2GGIC4R.js → skills-eval-LI75W6OK.js} +34 -27
  97. package/dist/{targets-54SWINWB.js → targets-4CIFKCTW.js} +23 -22
  98. package/dist/{terminal-lease-SAIF2OGY.js → terminal-lease-WUZY7ZV5.js} +4 -4
  99. package/dist/{uninstall-BVLWXKBT.js → uninstall-7FV7IP4E.js} +4 -4
  100. package/dist/{upgrade-JKAR27XC.js → upgrade-K2HVIVMQ.js} +20 -19
  101. package/dist/{usage-MSAWCLX4.js → usage-GTZELZQX.js} +116 -49
  102. package/dist/verifiers-RLAHT27O.js +336 -0
  103. package/dist/{verify-X5HDROLA.js → verify-BX3BRKH5.js} +7 -6
  104. package/dist/{wiki-generate-GUSOQ6ZP.js → wiki-generate-ASIFASCN.js} +45 -37
  105. package/dist/worker/entry.js +38 -35
  106. package/docs/README.md +3 -2
  107. package/docs/acp.md +1 -1
  108. package/docs/alcf-provider.md +1 -1
  109. package/docs/architecture.md +2 -2
  110. package/docs/artifact-versions.md +10 -6
  111. package/docs/built-in-agents.md +26 -2
  112. package/docs/capacity-and-scheduling.md +1 -1
  113. package/docs/commands-and-modes.md +82 -2
  114. package/docs/configuration-and-targets.md +79 -2
  115. package/docs/context-engine.md +1 -1
  116. package/docs/development-pipeline.md +1 -1
  117. package/docs/dispatch-architecture-rationale.md +1 -1
  118. package/docs/documentation-coverage.md +3 -3
  119. package/docs/documentation-guide.md +3 -3
  120. package/docs/eval-runner.md +1 -1
  121. package/docs/evals-internal.md +1 -1
  122. package/docs/evidence-and-memory.md +5 -5
  123. package/docs/evolution.md +1 -1
  124. package/docs/exit-codes-and-output.md +4 -1
  125. package/docs/extensions-and-sharing.md +6 -2
  126. package/docs/fleet-demo-runbook.md +2 -2
  127. package/docs/fleet-dispatch.md +197 -10
  128. package/docs/git-commit-provenance.md +2 -2
  129. package/docs/glossary.md +1 -1
  130. package/docs/installation-and-lifecycle.md +2 -2
  131. package/docs/middleware-and-components.md +2 -1
  132. package/docs/model-catalog.md +1 -1
  133. package/docs/observability.md +55 -8
  134. package/docs/proactive-memory.md +1 -1
  135. package/docs/prompt-envelope-and-tools.md +1 -1
  136. package/docs/provider-adapter-cookbook.md +1 -1
  137. package/docs/release-cut-checklist.md +79 -64
  138. package/docs/resource-library.md +59 -0
  139. package/docs/safety-model.md +2 -2
  140. package/docs/scientific-validation.md +3 -3
  141. package/docs/session-lifecycle.md +37 -1
  142. package/docs/skills-marketplace.md +16 -3
  143. package/docs/tool-usage.md +14 -7
  144. package/docs/trace-store.md +1 -1
  145. package/docs/troubleshooting.md +1 -1
  146. package/docs/tui-design.md +1 -1
  147. package/docs/worker-dispatch-mechanics.md +3 -3
  148. package/package.json +1 -1
  149. package/src/cli/fleet-commands.ts +37 -0
  150. package/src/cli/fleet-graph.ts +102 -0
  151. package/src/cli/fleet-new.ts +36 -0
  152. package/src/cli/fleet-preflight.ts +121 -0
  153. package/src/cli/fleet-validate.ts +30 -0
  154. package/src/cli/fleet.ts +173 -335
  155. package/src/cli/index.ts +3 -1
  156. package/src/cli/library.ts +190 -0
  157. package/src/cli/share.ts +13 -1
  158. package/src/cli/usage.ts +111 -19
  159. package/src/core/bus-events.ts +4 -0
  160. package/src/core/commit-attribution.ts +4 -4
  161. package/src/core/config.ts +130 -0
  162. package/src/core/defaults.ts +81 -0
  163. package/src/domains/agents/builtins/architect.md +1 -0
  164. package/src/domains/agents/builtins/oracle.md +33 -0
  165. package/src/domains/agents/catalog.ts +13 -1
  166. package/src/domains/agents/fleet-contract.ts +278 -16
  167. package/src/domains/agents/index.ts +14 -0
  168. package/src/domains/agents/result-contract.ts +235 -1
  169. package/src/domains/config/classify.ts +4 -0
  170. package/src/domains/dispatch/active-route-planner.ts +14 -0
  171. package/src/domains/dispatch/backoff.ts +2 -1
  172. package/src/domains/dispatch/capability-match.ts +1 -0
  173. package/src/domains/dispatch/checkout-writer-lease.ts +175 -0
  174. package/src/domains/dispatch/contract.ts +34 -0
  175. package/src/domains/dispatch/delegation-plan.ts +167 -0
  176. package/src/domains/dispatch/execution-plan.ts +76 -5
  177. package/src/domains/dispatch/execution-role.ts +3 -1
  178. package/src/domains/dispatch/execution-scheduler.ts +183 -67
  179. package/src/domains/dispatch/extension.ts +258 -9
  180. package/src/domains/dispatch/fleet-gate.ts +14 -0
  181. package/src/domains/dispatch/fleet-plan.ts +63 -3
  182. package/src/domains/dispatch/fleet-run.ts +737 -0
  183. package/src/domains/dispatch/gate-role-prompts.ts +9 -0
  184. package/src/domains/dispatch/host-verification.ts +178 -0
  185. package/src/domains/dispatch/index.ts +38 -0
  186. package/src/domains/dispatch/intent.ts +159 -0
  187. package/src/domains/dispatch/receipt-integrity.ts +8 -4
  188. package/src/domains/dispatch/state.ts +36 -3
  189. package/src/domains/dispatch/types.ts +51 -6
  190. package/src/domains/dispatch/validation.ts +66 -6
  191. package/src/domains/evidence/trust-status.ts +10 -1
  192. package/src/domains/middleware/index.ts +15 -0
  193. package/src/domains/middleware/watchdog.ts +281 -0
  194. package/src/domains/observability/contract.ts +3 -1
  195. package/src/domains/observability/cost.ts +12 -1
  196. package/src/domains/observability/extension.ts +2 -2
  197. package/src/domains/observability/index.ts +10 -0
  198. package/src/domains/observability/out-of-turn-usage.ts +223 -0
  199. package/src/domains/resources/index.ts +20 -0
  200. package/src/domains/resources/library.ts +326 -0
  201. package/src/domains/resources/skills/marketplace.ts +37 -12
  202. package/src/domains/session/handoff.ts +629 -0
  203. package/src/domains/share/archive.ts +67 -2
  204. package/src/entry/orchestrator.ts +37 -0
  205. package/src/interactive/bus-notices.ts +26 -0
  206. package/src/interactive/chat-loop.ts +235 -1
  207. package/src/interactive/chat-renderer.ts +22 -0
  208. package/src/interactive/cost-overlay.ts +31 -3
  209. package/src/interactive/council-dispatch.ts +30 -0
  210. package/src/interactive/council-grid.ts +213 -0
  211. package/src/interactive/council.ts +99 -0
  212. package/src/interactive/dispatch-board.ts +260 -16
  213. package/src/interactive/fleet-run-preview.ts +307 -0
  214. package/src/interactive/footer/notifications.ts +219 -0
  215. package/src/interactive/handoff-round.ts +56 -0
  216. package/src/interactive/interactive-application.ts +43 -1
  217. package/src/interactive/interactive-event-projection.ts +9 -1
  218. package/src/interactive/interactive-slash-runtime.ts +52 -2
  219. package/src/interactive/interactive-subscriptions.ts +14 -2
  220. package/src/interactive/oracle.ts +179 -0
  221. package/src/interactive/overlay-ask-user-lifecycle.ts +6 -0
  222. package/src/interactive/overlay-general-openers.ts +190 -1
  223. package/src/interactive/overlay-key-routing.ts +17 -1
  224. package/src/interactive/overlay-lifecycle.ts +41 -1
  225. package/src/interactive/overlay-permission-lifecycle.ts +10 -0
  226. package/src/interactive/overlay-resource-openers.ts +11 -3
  227. package/src/interactive/overlay-session-lifecycle.ts +234 -2
  228. package/src/interactive/overlays/fleet-run-approval.ts +208 -0
  229. package/src/interactive/overlays/handoff-review.ts +185 -0
  230. package/src/interactive/overlays/library-install-confirm.ts +151 -0
  231. package/src/interactive/overlays/list-overlay.ts +168 -2
  232. package/src/interactive/overlays/settings.ts +101 -4
  233. package/src/interactive/overlays/side-question.ts +139 -0
  234. package/src/interactive/overlays/skills-hub.ts +401 -15
  235. package/src/interactive/side-question.ts +171 -0
  236. package/src/interactive/slash-commands.ts +432 -5
  237. package/src/interactive/slash-spec.ts +19 -6
  238. package/src/interactive/theme/tokens.ts +30 -0
  239. package/src/interactive/turn-middleware.ts +15 -1
  240. package/src/interactive/watchdog-run.ts +75 -0
  241. package/src/interactive/worker-share.ts +56 -1
  242. package/src/interactive/worker-stream.ts +7 -0
  243. package/src/tools/bootstrap.ts +3 -0
  244. package/src/tools/compete-worktrees.ts +13 -79
  245. package/src/tools/dispatch-admission.ts +242 -8
  246. package/src/tools/dispatch-arguments.ts +57 -1
  247. package/src/tools/dispatch-plan.ts +136 -6
  248. package/src/tools/dispatch-runner.ts +319 -13
  249. package/src/tools/dispatch-types.ts +20 -1
  250. package/src/tools/dispatch.ts +72 -2
  251. package/src/tools/monitor.ts +16 -0
  252. package/src/tools/profiles.ts +18 -4
  253. package/src/tools/task-worktree.ts +238 -0
  254. package/src/tools/verify/authoring.ts +61 -1
  255. package/src/tools/verify/scripts.ts +62 -0
  256. package/src/tools/worker-evidence.ts +2 -1
  257. package/src/worker/spec-contract.ts +1 -0
  258. package/dist/chunk-HC4CLZ2Y.js +0 -68
@@ -14,6 +14,7 @@ import type { EvidenceTag } from "../evidence/index.js";
14
14
  import type { CostProvenance, RuntimeTargetSnapshot } from "../providers/index.js";
15
15
  import type { RunToolBudgetEnvelope } from "./budget-envelope.js";
16
16
  import type { ExecutionRole, GateTopologyRole } from "./execution-role.js";
17
+ import type { DispatchIntent } from "./intent.js";
17
18
  import type { RouteDecisionV1 } from "./route-decision.js";
18
19
  import type { RoutingIntent } from "./routing-intent.js";
19
20
 
@@ -41,7 +42,8 @@ export type RunOutcomeCode =
41
42
  | "worker_tool_call_cap_exhausted"
42
43
  | "loop_guard_tools_disabled_exhausted"
43
44
  | "result_contract_exhausted"
44
- | "worker_final_output_missing";
45
+ | "worker_final_output_missing"
46
+ | "host_verification_rejected";
45
47
 
46
48
  export function isRunOutcomeCode(value: unknown): value is RunOutcomeCode {
47
49
  return (
@@ -49,7 +51,8 @@ export function isRunOutcomeCode(value: unknown): value is RunOutcomeCode {
49
51
  value === "worker_tool_call_cap_exhausted" ||
50
52
  value === "loop_guard_tools_disabled_exhausted" ||
51
53
  value === "result_contract_exhausted" ||
52
- value === "worker_final_output_missing"
54
+ value === "worker_final_output_missing" ||
55
+ value === "host_verification_rejected"
53
56
  );
54
57
  }
55
58
 
@@ -238,6 +241,13 @@ export interface RunGateProvenance {
238
241
  verdict?: "pass" | "fail" | "revise";
239
242
  }
240
243
 
244
+ export interface RunCouncilProvenance {
245
+ group: string;
246
+ label: string;
247
+ color?: string;
248
+ round: number;
249
+ }
250
+
241
251
  /**
242
252
  * Plan-approval provenance for multi-task, compete, or remote dispatch.
243
253
  * `approval: "operator"` records that a supervised autonomy level parked the
@@ -248,7 +258,7 @@ export interface RunGateProvenance {
248
258
  */
249
259
  export interface RunPlanProvenance {
250
260
  hash: string;
251
- topology: "parallel" | "sequential" | "pipeline" | "review" | "compete" | "detached" | "fleet";
261
+ topology: "parallel" | "sequential" | "pipeline" | "review" | "compete" | "council" | "detached" | "fleet";
252
262
  taskCount: number;
253
263
  approval: "operator" | "full-auto";
254
264
  source: null | {
@@ -311,7 +321,7 @@ export interface RunPhaseDurations {
311
321
  * bumping one without the other is a compile error.
312
322
  */
313
323
  export interface RunReceiptIntegrity {
314
- version: 15;
324
+ version: 19;
315
325
  algorithm: "sha256";
316
326
  digest: string;
317
327
  }
@@ -449,6 +459,7 @@ export interface RunEnvelope {
449
459
  pipeline?: RunPipelineProvenance;
450
460
  /** Review/compete gate provenance; present only on runs of a gated dispatch. */
451
461
  gate?: RunGateProvenance;
462
+ council?: RunCouncilProvenance;
452
463
  /** Plan-approval provenance; present only on runs of an approval-gated plan. */
453
464
  plan?: RunPlanProvenance;
454
465
  /** Ad-hoc specialist provenance; present only when a persona override composed the stable prompt. */
@@ -674,6 +685,25 @@ export interface RunReceiptVerification {
674
685
  | "receipt-unavailable";
675
686
  }
676
687
 
688
+ export interface RunHostVerificationCheck {
689
+ check: string;
690
+ argv: string[];
691
+ cwd: string;
692
+ exitCode: number;
693
+ durationMs: number;
694
+ memo: boolean;
695
+ outputTail: string;
696
+ artifactPath?: string;
697
+ /** Original evidence owner on a memo hit. */
698
+ evidenceRunId?: string;
699
+ }
700
+
701
+ export interface RunHostVerification {
702
+ status: "verified" | "rejected" | "skipped";
703
+ reason?: string;
704
+ checks: RunHostVerificationCheck[];
705
+ }
706
+
677
707
  export interface RunReceipt {
678
708
  runId: string;
679
709
  agentId: string;
@@ -682,6 +712,8 @@ export interface RunReceipt {
682
712
  agentAudience?: AgentAudience;
683
713
  requestOrigin?: DispatchRequestOrigin;
684
714
  task: string;
715
+ /** Normalized dispatch intent admitted before worker execution. */
716
+ intent?: DispatchIntent;
685
717
  /** Integrity-sealed recipe policy, invocation request, effective phase, and admission reasons. */
686
718
  budget?: RunToolBudgetEnvelope;
687
719
  /** Proof of briefing content without copying its prose into the receipt. */
@@ -714,8 +746,11 @@ export interface RunReceipt {
714
746
  pipeline?: RunPipelineProvenance;
715
747
  /** Review/compete gate provenance; present only on runs of a gated dispatch. */
716
748
  gate?: RunGateProvenance;
749
+ council?: RunCouncilProvenance;
717
750
  /** Plan-approval provenance; present only on runs of an approval-gated plan. */
718
751
  plan?: RunPlanProvenance;
752
+ /** Version 5 fleet gate artifact authored by this run. */
753
+ fleetGate?: { path: string; pathHash: string };
719
754
  /** Ad-hoc specialist provenance; present only when a persona override composed the stable prompt. */
720
755
  personaOverride?: RunPersonaOverride;
721
756
  /** Effective project-context tier for this run; absent on receipts written before this field landed. */
@@ -764,6 +799,17 @@ export interface RunReceipt {
764
799
  toolActivity?: ToolActivitySummary;
765
800
  /** Integrity-sealed evidence confidence. Sealed on every receipt. */
766
801
  verification: RunReceiptVerification;
802
+ /** Orchestrator-executed declared checks. Worker self-report never populates this field. */
803
+ hostVerification?: RunHostVerification;
804
+ /** Isolated writer checkout and its collect-time application result. */
805
+ worktree?: {
806
+ path: string;
807
+ branch: string;
808
+ diffHash: string;
809
+ apply: "merge" | "preserve";
810
+ applied: boolean;
811
+ reason?: string;
812
+ };
767
813
  /** Required normalized routing request, sealed without task or prompt data. */
768
814
  routingIntent: RoutingIntent;
769
815
  /** Required routing-quality facts known at receipt finalization. */
@@ -786,8 +832,7 @@ export interface RunReceipt {
786
832
  * What this run contributed to its dispatch's agent ledger. Optional and
787
833
  * absent unless the run had a ledger, following validationGrounding and
788
834
  * capabilityMismatch: receiptDigestFields skips undefined, so a receipt
789
- * without one digests exactly as it did before this landed and
790
- * RUN_RECEIPT_INTEGRITY_VERSION stays where it is.
835
+ * without one omits the field from canonical serialization.
791
836
  */
792
837
  ledgerContribution?: RunLedgerContribution;
793
838
  /**
@@ -19,6 +19,7 @@ import {
19
19
  } from "./execution-handoff.js";
20
20
  import { isExecutionRole } from "./execution-role.js";
21
21
  import { COMPETE_STANCES, type CompeteStance } from "./gate-role-prompts.js";
22
+ import { type DispatchIntent, isDispatchIntent } from "./intent.js";
22
23
  import { isRoutingIntent, type RoutingIntent } from "./routing-intent.js";
23
24
  import type {
24
25
  DispatchRequestOrigin,
@@ -121,6 +122,10 @@ export interface JobSpec {
121
122
  */
122
123
  denyTools?: ReadonlyArray<string>;
123
124
  cwd?: string;
125
+ /** Run this writer in an isolated git worktree. */
126
+ worktree?: true;
127
+ /** How a successful isolated writer is applied to its parent checkout. */
128
+ apply?: "merge" | "preserve";
124
129
  memorySection?: string;
125
130
  noSkills?: boolean;
126
131
  skillPaths?: ReadonlyArray<string>;
@@ -134,6 +139,8 @@ export interface JobSpec {
134
139
  * per-tool calls (subprocess).
135
140
  */
136
141
  writeRoots?: ReadonlyArray<string>;
142
+ /** Normalized model-declared path, output, and host verification intent. */
143
+ intent?: DispatchIntent;
137
144
  requestOrigin?: DispatchRequestOrigin;
138
145
  /**
139
146
  * Threaded output from the previous pipeline step. Set only by the dispatch
@@ -159,6 +166,8 @@ export interface JobSpec {
159
166
  autonomy?: AutonomyLevel;
160
167
  /** Review/compete gate provenance sealed into the run's receipt. */
161
168
  gate?: RunGateProvenance;
169
+ /** Council grouping projected to receipts and fleet surfaces. */
170
+ council?: { group: string; label: string; color?: string; round: number };
162
171
  /** Plan-approval provenance sealed into the run's receipt. */
163
172
  plan?: RunPlanProvenance;
164
173
  /**
@@ -195,18 +204,22 @@ const KNOWN_KEYS = new Set([
195
204
  "toolProfile",
196
205
  "denyTools",
197
206
  "cwd",
207
+ "worktree",
208
+ "apply",
198
209
  "memorySection",
199
210
  "noSkills",
200
211
  "skillPaths",
201
212
  "trustProjectCompatRoots",
202
213
  "responseSchema",
203
214
  "writeRoots",
215
+ "intent",
204
216
  "requestOrigin",
205
217
  "pipelineInput",
206
218
  "predecessorHandoffs",
207
219
  "lineage",
208
220
  "autonomy",
209
221
  "gate",
222
+ "council",
210
223
  "plan",
211
224
  "competeStance",
212
225
  ]);
@@ -399,7 +412,7 @@ export function validateJobSpec(spec: unknown): Validated {
399
412
 
400
413
  if ("toolProfile" in spec && spec.toolProfile !== undefined) {
401
414
  if (typeof spec.toolProfile !== "string" || !isToolProfileName(spec.toolProfile)) {
402
- errors.push("toolProfile must be one of: minimal-local|science-local|full-agent");
415
+ errors.push("toolProfile must be one of: minimal-local|science-local|full-agent|council-read-only");
403
416
  }
404
417
  }
405
418
 
@@ -414,6 +427,13 @@ export function validateJobSpec(spec: unknown): Validated {
414
427
  errors.push("cwd must be a non-empty string");
415
428
  }
416
429
  }
430
+ if ("worktree" in spec && spec.worktree !== undefined && spec.worktree !== true) {
431
+ errors.push("worktree must be true when present");
432
+ }
433
+ if ("apply" in spec && spec.apply !== undefined && spec.apply !== "merge" && spec.apply !== "preserve") {
434
+ errors.push("apply must be merge or preserve");
435
+ }
436
+ if (spec.apply !== undefined && spec.worktree !== true) errors.push("apply requires worktree: true");
417
437
 
418
438
  if ("memorySection" in spec && spec.memorySection !== undefined) {
419
439
  if (typeof spec.memorySection !== "string") {
@@ -457,6 +477,15 @@ export function validateJobSpec(spec: unknown): Validated {
457
477
  errors.push("writeRoots must be a non-empty array of non-empty strings");
458
478
  }
459
479
  }
480
+ if ("intent" in spec && spec.intent !== undefined && !isDispatchIntent(spec.intent)) {
481
+ errors.push("intent must be a normalized dispatch intent");
482
+ }
483
+ if (isDispatchIntent(spec.intent) && spec.intent.writeRoots.length > 0 && Array.isArray(spec.writeRoots)) {
484
+ const jobCwd = typeof spec.cwd === "string" && spec.cwd.length > 0 ? spec.cwd : process.cwd();
485
+ const legacy = [...new Set(spec.writeRoots.map((root) => path.resolve(jobCwd, String(root))))].sort();
486
+ const declared = [...new Set(spec.intent.writeRoots.map((root) => path.resolve(jobCwd, root)))].sort();
487
+ if (JSON.stringify(legacy) !== JSON.stringify(declared)) errors.push("intent_write_roots_contradiction");
488
+ }
460
489
 
461
490
  if ("requestOrigin" in spec && spec.requestOrigin !== undefined) {
462
491
  if (typeof spec.requestOrigin !== "string" || !VALID_REQUEST_ORIGINS.has(spec.requestOrigin)) {
@@ -550,18 +579,25 @@ export function validateJobSpec(spec: unknown): Validated {
550
579
  if (typeof spec.toolProfile === "string" && isToolProfileName(spec.toolProfile)) out.toolProfile = spec.toolProfile;
551
580
  if (Array.isArray(spec.denyTools)) out.denyTools = spec.denyTools.map((tool) => String(tool));
552
581
  if (typeof spec.cwd === "string") out.cwd = spec.cwd;
582
+ if (spec.worktree === true) out.worktree = true;
583
+ if (spec.apply === "merge" || spec.apply === "preserve") out.apply = spec.apply;
553
584
  if (typeof spec.memorySection === "string") out.memorySection = spec.memorySection;
554
585
  if (typeof spec.noSkills === "boolean") out.noSkills = spec.noSkills;
555
586
  if (Array.isArray(spec.skillPaths)) out.skillPaths = spec.skillPaths.map((p) => String(p));
556
587
  if (typeof spec.trustProjectCompatRoots === "boolean") out.trustProjectCompatRoots = spec.trustProjectCompatRoots;
557
588
  if (responseSchema) out.responseSchema = responseSchema;
558
- if (
589
+ if (isDispatchIntent(spec.intent)) out.intent = structuredClone(spec.intent);
590
+ const effectiveWriteRoots =
559
591
  Array.isArray(spec.writeRoots) &&
560
592
  spec.writeRoots.length > 0 &&
561
593
  spec.writeRoots.every((root) => typeof root === "string" && root.length > 0)
562
- ) {
594
+ ? spec.writeRoots
595
+ : isDispatchIntent(spec.intent) && spec.intent.writeRoots.length > 0
596
+ ? spec.intent.writeRoots
597
+ : undefined;
598
+ if (effectiveWriteRoots !== undefined) {
563
599
  const jobCwd = typeof spec.cwd === "string" && spec.cwd.length > 0 ? spec.cwd : process.cwd();
564
- out.writeRoots = spec.writeRoots.map((root) => path.resolve(jobCwd, String(root)));
600
+ out.writeRoots = effectiveWriteRoots.map((root) => path.resolve(jobCwd, String(root)));
565
601
  }
566
602
  if (typeof spec.requestOrigin === "string" && VALID_REQUEST_ORIGINS.has(spec.requestOrigin)) {
567
603
  out.requestOrigin = spec.requestOrigin as DispatchRequestOrigin;
@@ -572,6 +608,7 @@ export function validateJobSpec(spec: unknown): Validated {
572
608
  if (isValidLineage(spec.lineage)) out.lineage = spec.lineage;
573
609
  if (isAutonomyLevel(spec.autonomy)) out.autonomy = spec.autonomy;
574
610
  if (isValidGate(spec.gate)) out.gate = cloneGate(spec.gate);
611
+ if (isValidCouncil(spec.council)) out.council = { ...spec.council };
575
612
  if (typeof spec.competeStance === "string" && VALID_COMPETE_STANCES.has(spec.competeStance)) {
576
613
  out.competeStance = spec.competeStance as CompeteStance;
577
614
  }
@@ -654,9 +691,18 @@ function isValidProtectedArtifactRemap(value: unknown): value is ProtectedArtifa
654
691
  );
655
692
  }
656
693
 
657
- const VALID_GATE_ROLES = new Set(["builder", "reviewer", "candidate", "judge"]);
694
+ const VALID_GATE_ROLES = new Set(["builder", "reviewer", "candidate", "judge", "member", "synthesis"]);
658
695
  const VALID_GATE_VERDICTS = new Set(["pass", "fail", "revise"]);
659
- const VALID_PLAN_TOPOLOGIES = new Set(["parallel", "sequential", "pipeline", "review", "compete", "detached", "fleet"]);
696
+ const VALID_PLAN_TOPOLOGIES = new Set([
697
+ "parallel",
698
+ "sequential",
699
+ "pipeline",
700
+ "review",
701
+ "compete",
702
+ "council",
703
+ "detached",
704
+ "fleet",
705
+ ]);
660
706
  const VALID_PLAN_APPROVALS = new Set(["operator", "full-auto"]);
661
707
 
662
708
  function isValidGate(value: unknown): value is RunGateProvenance {
@@ -688,6 +734,20 @@ function cloneGate(gate: RunGateProvenance): RunGateProvenance {
688
734
  };
689
735
  }
690
736
 
737
+ function isValidCouncil(value: unknown): value is NonNullable<JobSpec["council"]> {
738
+ return (
739
+ isPlainObject(value) &&
740
+ typeof value.group === "string" &&
741
+ value.group.length > 0 &&
742
+ typeof value.label === "string" &&
743
+ value.label.length > 0 &&
744
+ (value.color === undefined || typeof value.color === "string") &&
745
+ typeof value.round === "number" &&
746
+ Number.isInteger(value.round) &&
747
+ value.round >= 1
748
+ );
749
+ }
750
+
691
751
  function isValidPlan(value: unknown): value is RunPlanProvenance {
692
752
  if (!isPlainObject(value)) return false;
693
753
  if (typeof value.hash !== "string" || value.hash.length === 0) return false;
@@ -418,6 +418,7 @@ export type PersistedRunReceiptTrustFacts = Pick<RunReceipt, "runId"> &
418
418
  RunReceipt,
419
419
  | "integrity"
420
420
  | "verification"
421
+ | "hostVerification"
421
422
  | "quality"
422
423
  | "validationGrounding"
423
424
  | "briefing"
@@ -536,6 +537,12 @@ export function adaptRunReceiptValidationStatus(
536
537
  ): ValidationGroundingStatus {
537
538
  if (receipt === null || receipt === undefined) return absentTrustStatus("artifact_missing");
538
539
  const artifacts = validationArtifacts(receipt);
540
+ if (receipt.hostVerification?.status === "rejected") {
541
+ return attributed("failed", receiptSource(receipt), { kind: "validator", id: "host-verification" }, artifacts);
542
+ }
543
+ if (receipt.hostVerification?.status === "verified") {
544
+ return attributed("validated", receiptSource(receipt), { kind: "validator", id: "host-verification" }, artifacts);
545
+ }
539
546
  if (qualityHasFailure(receipt.quality)) {
540
547
  return attributed("failed", receiptSource(receipt), { kind: "validator", id: "receipt-quality" }, artifacts);
541
548
  }
@@ -549,7 +556,9 @@ export function adaptRunReceiptValidationStatus(
549
556
  if (qualityHasValidation(receipt.quality)) {
550
557
  return attributed("validated", receiptSource(receipt), { kind: "validator", id: "receipt-quality" }, artifacts);
551
558
  }
552
- if (receipt.verification !== undefined) return verificationStatus(receipt.verification, receipt, artifacts);
559
+ if (receipt.verification !== undefined) {
560
+ return verificationStatus(receipt.verification, receipt, artifacts);
561
+ }
553
562
  return attributed("unknown", compatibilitySource(receipt, "verification"), COMPATIBILITY_AUTHORITY, artifacts);
554
563
  }
555
564
 
@@ -152,3 +152,18 @@ export type {
152
152
  MiddlewareValidationIssue,
153
153
  } from "./validate.js";
154
154
  export { validateMiddlewareEffect, validateMiddlewareRule } from "./validate.js";
155
+ export {
156
+ coalesceTurnDiff,
157
+ createWatchdogRegistration,
158
+ WATCHDOG_DIFF_MAX_BYTES,
159
+ WATCHDOG_PATHS_MAX,
160
+ WATCHDOG_REGISTRATION_ID,
161
+ WATCHDOG_TASK,
162
+ WATCHDOG_TRUNCATION_MARKER,
163
+ type WatchdogDeps,
164
+ type WatchdogRegistration,
165
+ type WatchdogSettingsView,
166
+ type WatchdogTrigger,
167
+ type WatchdogTriggerReason,
168
+ watchdogBriefing,
169
+ } from "./watchdog.js";
@@ -0,0 +1,281 @@
1
+ /**
2
+ * The opt-in turn-end watchdog.
3
+ *
4
+ * A long agentic turn can leave the tree in a state nobody looked at: the
5
+ * model reports success, the operator reads the report, and the drift between
6
+ * what the board asked for and what the diff actually did stays invisible
7
+ * until someone reads the diff themselves. When `watchdog.enabled` is on, a
8
+ * turn that changed the tree is handed to one read-only verifier run briefed
9
+ * with the turn's coalesced diff and the board's current scope. Its blockers
10
+ * become one transcript notice and nothing else.
11
+ *
12
+ * What it deliberately does not do: it never auto-follows, never carries the
13
+ * turn onward, never queues a turn, and never mutates. A watchdog that could
14
+ * start work would be a second agent nobody asked for; this one can only tell
15
+ * the operator what it saw.
16
+ *
17
+ * The registration itself emits no middleware effects. It owns the decision of
18
+ * when to fire and the coalesced diff it fires with; the caller owns the
19
+ * dispatch and the notice.
20
+ */
21
+
22
+ import { ToolNames } from "../../core/tool-names.js";
23
+ import type { MiddlewareHookRegistration } from "./runtime.js";
24
+ import type { MiddlewareEffect, MiddlewareHookInput } from "./types.js";
25
+
26
+ export const WATCHDOG_REGISTRATION_ID = "observer.watchdog";
27
+
28
+ /**
29
+ * Bound on the coalesced diff a watchdog briefing carries, in UTF-8 bytes. The
30
+ * run is meant to be cheap enough to spend on every mutating turn, so it is
31
+ * given the shape of the change rather than every byte of it.
32
+ */
33
+ export const WATCHDOG_DIFF_MAX_BYTES = 12 * 1024;
34
+
35
+ /** Bound on how many mutated paths the briefing names before it summarizes the rest. */
36
+ export const WATCHDOG_PATHS_MAX = 40;
37
+
38
+ export const WATCHDOG_TRUNCATION_MARKER = "\n[diff truncated]";
39
+
40
+ /** Tools whose successful results carry a file diff worth coalescing. */
41
+ const MUTATING_TOOL_NAMES = new Set<string>([ToolNames.Write, ToolNames.Edit, ToolNames.Artifact]);
42
+
43
+ const NO_EFFECTS: ReadonlyArray<MiddlewareEffect> = [];
44
+
45
+ export type WatchdogTriggerReason = "turn_end" | "cadence";
46
+
47
+ export interface WatchdogTrigger {
48
+ reason: WatchdogTriggerReason;
49
+ /** The turn's coalesced diff, one block per mutated path in first-touch order. */
50
+ diff: string;
51
+ /** Paths the turn mutated, in first-touch order. */
52
+ paths: ReadonlyArray<string>;
53
+ /** The board's current scope, or null when the session has no task board. */
54
+ scope: string | null;
55
+ /** Tool calls seen in this turn when the trigger fired. */
56
+ toolCalls: number;
57
+ }
58
+
59
+ /** The live settings view the registration reads on every boundary, so a hot reload lands immediately. */
60
+ export interface WatchdogSettingsView {
61
+ enabled: boolean;
62
+ target?: string;
63
+ cadenceToolCalls?: number;
64
+ }
65
+
66
+ export interface WatchdogDeps {
67
+ getSettings: () => Readonly<WatchdogSettingsView>;
68
+ /** The board's current scope line, or null when there is no board. */
69
+ getScope?: () => string | null;
70
+ /**
71
+ * Dispatch the watchdog run and deliver whatever it found. Resolves when the
72
+ * run has settled; the registration awaits only to know the in-flight slot is
73
+ * free again, never to hold the turn.
74
+ */
75
+ run: (trigger: WatchdogTrigger) => Promise<void>;
76
+ /**
77
+ * False on a surface that must never fire the watchdog whatever the setting
78
+ * says. Headless and ACP runs pass false: neither has an operator watching a
79
+ * transcript, so a notice they cannot read is a worker run spent on nothing.
80
+ */
81
+ firesOnThisSurface?: boolean;
82
+ }
83
+
84
+ export interface WatchdogRegistration extends MiddlewareHookRegistration {
85
+ /** Triggers dropped because a watchdog run was already in flight. */
86
+ droppedTriggers(): number;
87
+ /** True while a watchdog run is outstanding. */
88
+ runInFlight(): boolean;
89
+ /** Resolves once no watchdog run is outstanding. */
90
+ whenIdle(): Promise<void>;
91
+ }
92
+
93
+ function stringValue(value: unknown): string | null {
94
+ return typeof value === "string" && value.length > 0 ? value : null;
95
+ }
96
+
97
+ function firstPath(details: Readonly<Record<string, unknown>> | undefined): string | null {
98
+ const paths = details?.paths;
99
+ if (!Array.isArray(paths)) return null;
100
+ for (const entry of paths) {
101
+ const path = stringValue(entry);
102
+ if (path !== null) return path;
103
+ }
104
+ return null;
105
+ }
106
+
107
+ /**
108
+ * Coalesce the per-call diffs of one turn into one diff.
109
+ *
110
+ * Per-path last-write-wins, in first-touch order. A turn that edited one file
111
+ * six times produced six diffs against six different base contents; replaying
112
+ * all of them tells a reviewer the history of the edit rather than its result,
113
+ * and the result is what the reviewer is being asked about.
114
+ */
115
+ export function coalesceTurnDiff(
116
+ byPath: ReadonlyMap<string, string>,
117
+ maxBytes: number = WATCHDOG_DIFF_MAX_BYTES,
118
+ ): string {
119
+ const blocks: string[] = [];
120
+ let index = 0;
121
+ for (const [path, diff] of byPath) {
122
+ if (index >= WATCHDOG_PATHS_MAX) {
123
+ blocks.push(`[${byPath.size - WATCHDOG_PATHS_MAX} more changed path(s) omitted]`);
124
+ break;
125
+ }
126
+ blocks.push(`--- ${path}\n${diff}`);
127
+ index += 1;
128
+ }
129
+ const whole = blocks.join("\n\n");
130
+ if (Buffer.byteLength(whole, "utf8") <= maxBytes) return whole;
131
+ const room = Math.max(0, maxBytes - Buffer.byteLength(WATCHDOG_TRUNCATION_MARKER, "utf8"));
132
+ const buffer = Buffer.from(whole, "utf8");
133
+ let cut = Math.min(room, buffer.byteLength);
134
+ while (cut > 0) {
135
+ const next = buffer[cut];
136
+ if (next === undefined || (next & 0xc0) !== 0x80) break;
137
+ cut -= 1;
138
+ }
139
+ return `${buffer.subarray(0, cut).toString("utf8")}${WATCHDOG_TRUNCATION_MARKER}`;
140
+ }
141
+
142
+ /** The bounded, fixed task every watchdog run is given. It never varies with the diff. */
143
+ export const WATCHDOG_TASK =
144
+ "Review the coalesced diff in the briefing against the scope it names. Report one check per concern: " +
145
+ "a change outside the stated scope, a change the scope required that the diff does not make, or a defect " +
146
+ "visible in the diff itself. Run no commands and change nothing.";
147
+
148
+ /** The briefing body a watchdog run receives. Data, never instructions. */
149
+ export function watchdogBriefing(trigger: WatchdogTrigger): string {
150
+ const scope = trigger.scope === null ? "(no task board; scope is unstated)" : trigger.scope;
151
+ const paths = trigger.paths.length === 0 ? "(none)" : trigger.paths.join("\n");
152
+ return [
153
+ "## Current scope",
154
+ scope,
155
+ "",
156
+ "## Changed paths",
157
+ paths,
158
+ "",
159
+ "## Coalesced diff for this turn",
160
+ trigger.diff,
161
+ ].join("\n");
162
+ }
163
+
164
+ /**
165
+ * Build the registration. It is hooked on `after_tool` and `turn_end`:
166
+ * `after_tool` is where the diffs and the tool count come from, and `turn_end`
167
+ * is the boundary a mutating turn fires on.
168
+ */
169
+ export function createWatchdogRegistration(deps: WatchdogDeps): WatchdogRegistration {
170
+ const diffByPath = new Map<string, string>();
171
+ let toolCalls = 0;
172
+ let dropped = 0;
173
+ let inFlight = false;
174
+ let outstanding: Promise<void> = Promise.resolve();
175
+
176
+ const active = (): WatchdogSettingsView | null => {
177
+ if (deps.firesOnThisSurface === false) return null;
178
+ let settings: Readonly<WatchdogSettingsView>;
179
+ try {
180
+ settings = deps.getSettings();
181
+ } catch {
182
+ return null;
183
+ }
184
+ return settings.enabled ? settings : null;
185
+ };
186
+
187
+ const resetTurn = (): void => {
188
+ diffByPath.clear();
189
+ toolCalls = 0;
190
+ };
191
+
192
+ const fire = (reason: WatchdogTriggerReason): void => {
193
+ if (diffByPath.size === 0) return;
194
+ // One run at a time. A watchdog slower than the turns that trigger it must
195
+ // not queue: a queued review describes a tree that has already moved on,
196
+ // and a backlog of them would outlive the session. The drop is counted so
197
+ // a starved cadence and a quiet one stay distinguishable.
198
+ if (inFlight) {
199
+ dropped += 1;
200
+ return;
201
+ }
202
+ const trigger: WatchdogTrigger = {
203
+ reason,
204
+ diff: coalesceTurnDiff(diffByPath),
205
+ paths: [...diffByPath.keys()].slice(0, WATCHDOG_PATHS_MAX),
206
+ scope: scopeNow(),
207
+ toolCalls,
208
+ };
209
+ inFlight = true;
210
+ // Started synchronously, so the in-flight slot is genuinely taken before
211
+ // this hook returns. Deferring the start to a microtask would let two
212
+ // triggers in the same tool batch both find the slot free.
213
+ let started: Promise<void>;
214
+ try {
215
+ started = deps.run(trigger);
216
+ } catch {
217
+ inFlight = false;
218
+ return;
219
+ }
220
+ outstanding = started
221
+ .catch(() => {
222
+ // A failed watchdog run is not a failed turn. The run's own reporting
223
+ // path already told the operator whatever it could.
224
+ })
225
+ .finally(() => {
226
+ inFlight = false;
227
+ });
228
+ };
229
+
230
+ const scopeNow = (): string | null => {
231
+ try {
232
+ return deps.getScope?.() ?? null;
233
+ } catch {
234
+ return null;
235
+ }
236
+ };
237
+
238
+ return {
239
+ id: WATCHDOG_REGISTRATION_ID,
240
+ description: "review a mutating turn's coalesced diff against the board scope with a read-only verifier run",
241
+ hooks: ["after_tool", "turn_end"],
242
+ evaluate(input: MiddlewareHookInput): ReadonlyArray<MiddlewareEffect> {
243
+ const settings = active();
244
+ if (settings === null) {
245
+ // Still clear turn state so a mid-session disable cannot leave a diff
246
+ // behind that a later enable would fire on.
247
+ if (input.hook === "turn_end") resetTurn();
248
+ return NO_EFFECTS;
249
+ }
250
+ if (input.hook === "after_tool") {
251
+ toolCalls += 1;
252
+ recordMutation(input);
253
+ const cadence = settings.cadenceToolCalls;
254
+ // Mid-turn cadence keeps accumulating: turn end still reviews the whole
255
+ // turn, not just what landed after the last cadence firing.
256
+ if (cadence !== undefined && cadence >= 1 && toolCalls % cadence === 0) fire("cadence");
257
+ return NO_EFFECTS;
258
+ }
259
+ if (input.hook !== "turn_end") return NO_EFFECTS;
260
+ // A turn with no file mutations never fires. A middleware continuation
261
+ // re-evaluates turn_end with the accumulator already cleared, so it
262
+ // cannot fire a second run for the same change either.
263
+ fire("turn_end");
264
+ resetTurn();
265
+ return NO_EFFECTS;
266
+ },
267
+ droppedTriggers: () => dropped,
268
+ runInFlight: () => inFlight,
269
+ whenIdle: () => outstanding,
270
+ };
271
+
272
+ function recordMutation(input: MiddlewareHookInput): void {
273
+ if (input.metadata?.resultKind !== "ok") return;
274
+ if (input.toolName === undefined || !MUTATING_TOOL_NAMES.has(input.toolName)) return;
275
+ const diff = stringValue(input.toolResultDetails?.diff);
276
+ if (diff === null) return;
277
+ // Map.set on an existing key keeps its insertion position, so the newest
278
+ // diff for a path wins while the path keeps its first-touch order.
279
+ diffByPath.set(firstPath(input.toolResultDetails) ?? input.toolName, diff);
280
+ }
281
+ }
@@ -2,7 +2,7 @@ import type { ResponseModelIdObservationCounts } from "../../core/response-model
2
2
  import type { TargetStatus } from "../providers/contract.js";
3
3
  import type { CostProvenance } from "../providers/index.js";
4
4
  import type { AccountabilitySummary } from "./accountability.js";
5
- import type { CostAggregate, CostEntry, UsageBreakdown } from "./cost.js";
5
+ import type { CostAggregate, CostEntry, CostEntryLabel, UsageBreakdown } from "./cost.js";
6
6
  import type { MetricsView } from "./metrics.js";
7
7
  import type { TelemetrySnapshot } from "./telemetry.js";
8
8
  import type { SessionTurnTrace } from "./trace-store.js";
@@ -142,6 +142,8 @@ export interface ObservabilityContract {
142
142
  requestedModelIds: ReadonlyArray<string>;
143
143
  responseModelIdObservationCounts: Readonly<ResponseModelIdObservationCounts>;
144
144
  },
145
+ /** Marks a priced call that was not an ordinary turn, such as a `/btw` side question. */
146
+ label?: CostEntryLabel,
145
147
  ): void;
146
148
  /** Record final output token throughput for one completed assistant stream. */
147
149
  recordTokenThroughput(snapshot: TokenThroughputSnapshot): void;