@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
@@ -11,9 +11,16 @@ import {
11
11
  safeResourceWrite,
12
12
  } from "../../core/safe-resource-write.js";
13
13
  import { clioConfigDir, resolveClioDirs } from "../../core/xdg.js";
14
+ import { parseFrontmatter } from "../agents/frontmatter.js";
15
+ import {
16
+ assertAgentSpecPolicy,
17
+ normalizeAgentSpec,
18
+ parseAgentRecipeSchema,
19
+ parseFleetContract,
20
+ } from "../agents/index.js";
14
21
 
15
22
  export type ShareScope = "project" | "user";
16
- export type ShareEntryType = "project-context" | "prompt" | "skill" | "settings" | "extension";
23
+ export type ShareEntryType = "project-context" | "prompt" | "skill" | "agent" | "fleet" | "settings" | "extension";
17
24
 
18
25
  export interface ShareArchiveFile {
19
26
  type: ShareEntryType;
@@ -61,6 +68,8 @@ export interface ShareExportOptions {
61
68
  includeContext?: boolean;
62
69
  includePrompts?: boolean;
63
70
  includeSkills?: boolean;
71
+ includeAgents?: boolean;
72
+ includeFleets?: boolean;
64
73
  includeSettings?: boolean;
65
74
  includeExtensions?: boolean;
66
75
  }
@@ -206,7 +215,13 @@ function defaultedIncludes(
206
215
  ): Required<
207
216
  Pick<
208
217
  ShareExportOptions,
209
- "includeContext" | "includePrompts" | "includeSkills" | "includeSettings" | "includeExtensions"
218
+ | "includeContext"
219
+ | "includePrompts"
220
+ | "includeSkills"
221
+ | "includeAgents"
222
+ | "includeFleets"
223
+ | "includeSettings"
224
+ | "includeExtensions"
210
225
  >
211
226
  > {
212
227
  // True when the caller named at least one include flag explicitly. In that
@@ -216,12 +231,16 @@ function defaultedIncludes(
216
231
  options.includeContext !== undefined ||
217
232
  options.includePrompts !== undefined ||
218
233
  options.includeSkills !== undefined ||
234
+ options.includeAgents !== undefined ||
235
+ options.includeFleets !== undefined ||
219
236
  options.includeSettings !== undefined ||
220
237
  options.includeExtensions !== undefined;
221
238
  return {
222
239
  includeContext: hasExplicitInclude ? options.includeContext === true : true,
223
240
  includePrompts: hasExplicitInclude ? options.includePrompts === true : true,
224
241
  includeSkills: hasExplicitInclude ? options.includeSkills === true : true,
242
+ includeAgents: hasExplicitInclude ? options.includeAgents === true : true,
243
+ includeFleets: hasExplicitInclude ? options.includeFleets === true : true,
225
244
  includeSettings: hasExplicitInclude ? options.includeSettings === true : true,
226
245
  includeExtensions: hasExplicitInclude ? options.includeExtensions === true : true,
227
246
  };
@@ -263,6 +282,18 @@ export function createShareArchive(options: ShareExportOptions = {}): ClioShareA
263
282
  addTree(files, "skill", scope, root, `${scope}/skills`);
264
283
  }
265
284
  }
285
+ if (includes.includeAgents) {
286
+ for (const scope of scopes) {
287
+ const root = scope === "user" ? path.join(clioConfigDir(), "agents") : path.join(cwd, ".clio-coder", "agents");
288
+ addTree(files, "agent", scope, root, `${scope}/agents`);
289
+ }
290
+ }
291
+ if (includes.includeFleets) {
292
+ for (const scope of scopes) {
293
+ const root = scope === "user" ? path.join(clioConfigDir(), "fleets") : path.join(cwd, ".clio-coder", "fleets");
294
+ addTree(files, "fleet", scope, root, `${scope}/fleets`);
295
+ }
296
+ }
266
297
  if (includes.includeExtensions) {
267
298
  for (const scope of scopes) {
268
299
  const root =
@@ -313,9 +344,19 @@ function parseArchive(raw: unknown): ClioShareArchive {
313
344
  throw new Error("share archive manifest is malformed");
314
345
  }
315
346
  const archive = raw as unknown as ClioShareArchive;
347
+ const allowedTypes = new Set<ShareEntryType>([
348
+ "project-context",
349
+ "prompt",
350
+ "skill",
351
+ "agent",
352
+ "fleet",
353
+ "settings",
354
+ "extension",
355
+ ]);
316
356
  for (const file of archive.files) {
317
357
  if (
318
358
  !isRecord(file) ||
359
+ !allowedTypes.has(file.type as ShareEntryType) ||
319
360
  typeof file.archivePath !== "string" ||
320
361
  typeof file.relativePath !== "string" ||
321
362
  typeof file.sha256 !== "string" ||
@@ -392,6 +433,10 @@ function targetRootForFile(entry: ShareArchiveFile, options: ShareImportOptions)
392
433
  return scope === "user"
393
434
  ? { root: path.join(config, "skills"), containmentRoot: config, scope }
394
435
  : { root: path.join(cwd, ".clio-coder", "skills"), containmentRoot: cwd, scope };
436
+ case "agent":
437
+ return { root: path.join(config, "agents"), containmentRoot: config, scope: "user" };
438
+ case "fleet":
439
+ return { root: path.join(config, "fleets"), containmentRoot: config, scope: "user" };
395
440
  case "extension":
396
441
  return scope === "user"
397
442
  ? { root: path.join(config, "extensions"), containmentRoot: config, scope }
@@ -563,6 +608,26 @@ function prepareShareImport(filePath: string, options: ShareImportOptions = {}):
563
608
  }
564
609
  for (const targetInfo of preflight.targets) {
565
610
  const { entry, target, scope, buffer } = targetInfo;
611
+ if (entry.type === "fleet") {
612
+ try {
613
+ parseFleetContract(buffer.toString("utf8"), target);
614
+ } catch (error) {
615
+ diagnostics.push({ type: "error", message: error instanceof Error ? error.message : String(error), path: target });
616
+ continue;
617
+ }
618
+ }
619
+ if (entry.type === "agent") {
620
+ try {
621
+ const parsed = parseFrontmatter(buffer.toString("utf8"), target);
622
+ const id = path.basename(target, ".md");
623
+ assertAgentSpecPolicy(
624
+ normalizeAgentSpec(parseAgentRecipeSchema({ id, source: "user", filepath: target, ...parsed })),
625
+ );
626
+ } catch (error) {
627
+ diagnostics.push({ type: "error", message: error instanceof Error ? error.message : String(error), path: target });
628
+ continue;
629
+ }
630
+ }
566
631
  if (entry.type === "settings") {
567
632
  diagnostics.push(...settingsPlan(buffer, options));
568
633
  actions.push({ action: "settings", type: entry.type, scope, path: target });
@@ -90,6 +90,7 @@ import {
90
90
  import { createMemoryInterventionRegistration } from "../domains/middleware/memory-intervention.js";
91
91
  import { createTaskBoardReminderRegistration } from "../domains/middleware/task-board-reminder.js";
92
92
  import { createTaskNudgeRegistration } from "../domains/middleware/task-nudge.js";
93
+ import { createWatchdogRegistration } from "../domains/middleware/watchdog.js";
93
94
  import type { ObservabilityContract } from "../domains/observability/index.js";
94
95
  import { ObservabilityDomainModule } from "../domains/observability/index.js";
95
96
  import type { PromptsContract } from "../domains/prompts/contract.js";
@@ -177,6 +178,7 @@ import {
177
178
  import { subscribeLoopGuardStop } from "../interactive/loop-guard-interrupt.js";
178
179
  import { BUILTIN_SLASH_COMMANDS } from "../interactive/slash-commands.js";
179
180
  import { createToolProseRegistration } from "../interactive/tool-prose-registration.js";
181
+ import { runWatchdogReview } from "../interactive/watchdog-run.js";
180
182
  import { type AskUserHandler, cancelledAskUserResult } from "../tools/ask-user.js";
181
183
  import { registerAllTools } from "../tools/bootstrap.js";
182
184
  import { isGitRepository, recoverCleanupReadyCompeteGroups } from "../tools/compete-worktrees.js";
@@ -1069,6 +1071,10 @@ export async function bootOrchestrator(options: BootOptions = {}): Promise<BootR
1069
1071
  // Bound late: the registration is built here, but the buffer a deferred
1070
1072
  // reminder lands in belongs to the chat loop that has not been composed yet.
1071
1073
  let deferredMemoryReminderSink: ((message: string) => void) | null = null;
1074
+ // The watchdog's findings are for the operator, not the model, so they take
1075
+ // the transcript-notice path rather than the reminder buffer. Bound late for
1076
+ // the same reason: the chat loop that owns the transcript is composed below.
1077
+ let deferredWatchdogNoticeSink: ((text: string) => void) | null = null;
1072
1078
  // Content-bearing, so it exists only when the operator named a file. The
1073
1079
  // telemetry row says which silence happened; this says what the model wrote.
1074
1080
  const memoryTracePath = taskMemoryTracePath();
@@ -1261,6 +1267,7 @@ export async function bootOrchestrator(options: BootOptions = {}): Promise<BootR
1261
1267
  getAutonomy: resolveEffectiveAutonomy,
1262
1268
  ...(interactive ? { dispatchBackground } : {}),
1263
1269
  getCostCeilingUsd: () => result.getContract<SchedulingContract>("scheduling")?.ceilingUsd() ?? 0,
1270
+ ...(config ? { getWorkerRosters: () => config.get().workers.rosters } : {}),
1264
1271
  getSkillLoaderOptions: () => ({
1265
1272
  trustProjectCompatRoots: config?.get().skills.trustProjectCompatRoots === true,
1266
1273
  disableDiscovery: options.noSkills === true || options.headless?.noSkills === true,
@@ -1521,6 +1528,29 @@ export async function bootOrchestrator(options: BootOptions = {}): Promise<BootR
1521
1528
  middleware.registerHook(
1522
1529
  createDetachedDispatchNudgeRegistration({ getOpenBatches: () => openDetachedBatchViews(dispatch) }),
1523
1530
  );
1531
+ // The opt-in turn-end watchdog. Headless and ACP runs pass `false` for the
1532
+ // surface: neither has an operator reading a transcript, so a notice they
1533
+ // cannot see would be a worker run spent on nothing whatever the setting says.
1534
+ middleware.registerHook(
1535
+ createWatchdogRegistration({
1536
+ firesOnThisSurface: interactive,
1537
+ getSettings: () => (effectiveSettingsForDispatch?.() ?? getCurrentSettings()).watchdog,
1538
+ getScope: () => {
1539
+ const board = taskBoard.snapshot();
1540
+ if (board === null) return null;
1541
+ const active = board.tasks.find((task) => task.status === "active");
1542
+ return active ? `${board.title}: ${active.id} ${active.title}` : board.title;
1543
+ },
1544
+ run: (trigger) =>
1545
+ runWatchdogReview(trigger, {
1546
+ dispatch,
1547
+ bus,
1548
+ ...(agents ? { getAgentRoleFacts: agentRoleFactsResolver((id: string) => agents.getSpec(id)) } : {}),
1549
+ target: (effectiveSettingsForDispatch?.() ?? getCurrentSettings()).watchdog.target,
1550
+ ...(deferredWatchdogNoticeSink ? { emitNotice: deferredWatchdogNoticeSink } : {}),
1551
+ }),
1552
+ }),
1553
+ );
1524
1554
  if (session) {
1525
1555
  middleware.registerHook(
1526
1556
  createFinishContractRegistration({
@@ -1584,6 +1614,9 @@ export async function bootOrchestrator(options: BootOptions = {}): Promise<BootR
1584
1614
  registerDeferredReminderSink: (sink) => {
1585
1615
  deferredMemoryReminderSink = sink;
1586
1616
  },
1617
+ registerDeferredNoticeSink: (sink) => {
1618
+ deferredWatchdogNoticeSink = sink;
1619
+ },
1587
1620
  onAskUserFinalized: (policy) => {
1588
1621
  decisionBoard.recordFinalizedInterview(policy);
1589
1622
  },
@@ -1822,6 +1855,10 @@ export async function bootOrchestrator(options: BootOptions = {}): Promise<BootR
1822
1855
  providers,
1823
1856
  dispatch,
1824
1857
  ...(agents ? { agents } : {}),
1858
+ ...(() => {
1859
+ const scheduling = result.getContract<SchedulingContract>("scheduling");
1860
+ return scheduling ? { scheduling } : {};
1861
+ })(),
1825
1862
  observability,
1826
1863
  chat,
1827
1864
  ...(options.terminalLease
@@ -187,6 +187,32 @@ export function middlewareHookFailedSessionNotice(payload: unknown, seenBudgetWa
187
187
  return middlewareHookFailedNotice(payload, { noteBudgetWarningSuppression: key !== null });
188
188
  }
189
189
 
190
+ /** How many failed checks a watchdog notice names before it counts the rest. */
191
+ export const WATCHDOG_NOTICE_CHECK_LIMIT = 3;
192
+
193
+ /**
194
+ * The watchdog's finding, as the one transcript line it is allowed.
195
+ *
196
+ * A passing report renders nothing: the operator asked for a warning, not a
197
+ * receipt, and a green line after every mutating turn is noise that trains
198
+ * people to stop reading the channel. Only the failed checks of the
199
+ * `verifier-report` contract reach here, and only their names: the evidence
200
+ * body belongs to the receipt, which the operator can open. Returning null
201
+ * means the turn passed or the report carried nothing usable.
202
+ */
203
+ export function watchdogBlockersNotice(checks: ReadonlyArray<{ name: string; passed: boolean }>): BusNotice | null {
204
+ const failed = checks.filter((check) => !check.passed && check.name.trim().length > 0);
205
+ if (failed.length === 0) return null;
206
+ const named = failed.slice(0, WATCHDOG_NOTICE_CHECK_LIMIT).map((check) => check.name.trim());
207
+ const rest = failed.length - named.length;
208
+ const more = rest > 0 ? `, and ${rest} more` : "";
209
+ const blockers = failed.length === 1 ? "blocker" : "blockers";
210
+ return {
211
+ level: "warn",
212
+ text: `[watchdog] ${failed.length} ${blockers} after this turn: ${named.join("; ")}${more}. Nothing was changed or queued.`,
213
+ };
214
+ }
215
+
190
216
  /**
191
217
  * A tool call parked for one-shot approval (sd-01 §3.3). The text names the
192
218
  * axis that produced the ask: a safety-net rail asks at every level, while an
@@ -17,16 +17,19 @@ import { BusChannels, type RunAbortSource } from "../core/bus-events.js";
17
17
  import type { ClioSettings } from "../core/config.js";
18
18
  import type { SafeEventBus } from "../core/event-bus.js";
19
19
  import type { PendingSkillRequest } from "../core/skill-activation.js";
20
+ import { clioStateDir } from "../core/xdg.js";
20
21
  import {
21
22
  createMiddlewareToolChoiceControl,
22
23
  type MiddlewareContract,
23
24
  type MiddlewareToolChoiceControl,
24
25
  } from "../domains/middleware/index.js";
25
26
  import type { ObservabilityContract } from "../domains/observability/contract.js";
27
+ import type { CostEntryLabel } from "../domains/observability/cost.js";
28
+ import { appendOutOfTurnUsageRow, type OutOfTurnUsageRow } from "../domains/observability/out-of-turn-usage.js";
26
29
  import type { PromptsContract } from "../domains/prompts/contract.js";
27
30
  import { toContextOverflowError } from "../domains/providers/errors.js";
28
31
  import type { ProvidersContract } from "../domains/providers/index.js";
29
- import { runtimeTargetSnapshot } from "../domains/providers/index.js";
32
+ import { normalizeCostProvenance, runtimeTargetSnapshot, targetRequiresAuth } from "../domains/providers/index.js";
30
33
  import type { ProtectedArtifactState } from "../domains/safety/protected-artifacts.js";
31
34
  import type { CompactResult } from "../domains/session/compaction/compact.js";
32
35
  import type { ContextSnapshot, ContextUsageSnapshot } from "../domains/session/context-accounting.js";
@@ -38,6 +41,7 @@ import { protectedArtifactStateFromSessionEntries } from "../domains/session/pro
38
41
  import { isRetryableErrorMessage, type RetrySettings } from "../domains/session/retry.js";
39
42
  import { createEngineAgent } from "../engine/agent.js";
40
43
  import { resolveReservedOutputTokens } from "../engine/apis/output-budget.js";
44
+ import { cwdHash } from "../engine/session.js";
41
45
  import type { AgentEvent, AgentMessage, ImageContent } from "../engine/types.js";
42
46
  import { resolveSessionTools } from "../tools/agent-tools.js";
43
47
  import { finalizeAskUserInterviewForHost } from "../tools/ask-user.js";
@@ -53,7 +57,9 @@ import {
53
57
  toolSignatureFromState,
54
58
  } from "./chat-loop-messages.js";
55
59
  import { normalizeRetrySettings } from "./chat-loop-policy.js";
60
+ import { runHandoffRound } from "./handoff-round.js";
56
61
  import type { ApprovalRequestView } from "./permission-overlay.js";
62
+ import { runSideQuestion, type SideQuestionResult } from "./side-question.js";
57
63
  import type { AgentStatusEvent } from "./status/types.js";
58
64
  import { createTurnContext } from "./turn-context.js";
59
65
  import { createTurnMiddleware } from "./turn-middleware.js";
@@ -184,6 +190,13 @@ export interface ChatSubmitOptions {
184
190
  onAdmitted?: () => void;
185
191
  }
186
192
 
193
+ /**
194
+ * Placeholder key a target that needs no auth still has to be handed. Mirrors
195
+ * the turn runtime's own local fallback, so a `/btw` round against a local
196
+ * server authenticates exactly the way a turn against it does.
197
+ */
198
+ const LOCAL_SIDE_QUESTION_API_KEY = "clio-local-target";
199
+
187
200
  /** Closing notice an operator interrupt leaves in the transcript and the ledger. */
188
201
  const INTERRUPT_CANCEL_REASON = "[Clio Coder] run interrupted by operator; delivering the new message now.";
189
202
  const ENGINE_ACTIVE_PROMPT_ERROR =
@@ -212,6 +225,24 @@ export interface ChatCancelOptions {
212
225
  auditReason?: string;
213
226
  }
214
227
 
228
+ export interface SideQuestionOptions {
229
+ /** Cancels the round. Esc and Ctrl+C in the overlay abort through it. */
230
+ signal?: AbortSignal;
231
+ /** Streamed answer text so the overlay fills as the provider produces it. */
232
+ onDelta?: (partialText: string) => void;
233
+ }
234
+
235
+ /**
236
+ * How a `/btw` round ended. `refused` is a round that never started (a turn was
237
+ * in flight, or no orchestrator target is configured); `failed` is a round that
238
+ * started and the provider rejected.
239
+ */
240
+ export type SideQuestionOutcome =
241
+ | { status: "answered"; text: string }
242
+ | { status: "aborted"; text: string }
243
+ | { status: "refused"; reason: string }
244
+ | { status: "failed"; reason: string };
245
+
215
246
  export interface ChatLoop {
216
247
  submit(text: string, options?: ChatSubmitOptions): Promise<void>;
217
248
  steer(text: string): boolean;
@@ -250,6 +281,28 @@ export interface ChatLoop {
250
281
  * notice so the `/context compact` handler does not have to mirror the logic.
251
282
  */
252
283
  compact(instructions?: string): Promise<void>;
284
+ /**
285
+ * `/btw`: answer one side question against the session's active target,
286
+ * model, and compiled message history without starting a turn.
287
+ *
288
+ * Nothing this produces reaches the session ledger, the transcript, the
289
+ * context ledger, or the footer token counters; the message history is read,
290
+ * never mutated. The round's provider usage is still reported to `/cost`,
291
+ * labeled as a side question, because money was spent. Refused outright
292
+ * while a turn is in flight rather than queued.
293
+ */
294
+ askSideQuestion(question: string, options?: SideQuestionOptions): Promise<SideQuestionOutcome>;
295
+ /**
296
+ * `/handoff`: run the extraction round for a goal against the same target,
297
+ * model, and compiled message history, and return its raw JSON answer.
298
+ *
299
+ * Like a side question this is out of turn: no tools are sent, the message
300
+ * history is read and never mutated, and nothing the round produces reaches
301
+ * the ledger. Validating, bounding, and reviewing the answer belong to the
302
+ * caller; this method only owns the provider call. Refused outright while a
303
+ * turn is in flight rather than queued.
304
+ */
305
+ extractHandoff(goal: string, options?: SideQuestionOptions): Promise<SideQuestionOutcome>;
253
306
  /**
254
307
  * Drop or replace the chat-loop's in-memory state after a session switch
255
308
  * (/resume, /fork, /new). `leafTurnId` is the id the next user turn
@@ -292,6 +345,20 @@ export interface CreateChatLoopDeps {
292
345
  */
293
346
  prompts?: PromptsContract;
294
347
  createAgent?: typeof createEngineAgent;
348
+ /**
349
+ * The `/btw` round. Defaults to the real provider call; contracts inject a
350
+ * stub so they can assert what a side question does to the session without
351
+ * standing up a provider, exactly as `createAgent` does for a turn.
352
+ */
353
+ runSideQuestion?: typeof runSideQuestion;
354
+ /** The `/handoff` extraction round. Injectable for the same reason. */
355
+ runHandoffRound?: typeof runHandoffRound;
356
+ /**
357
+ * Append one priced out-of-turn call to the durable out-of-turn usage store.
358
+ * Defaults to the real writer under the state dir. Contracts inject a spy so
359
+ * they can assert the row was written without touching a real state dir.
360
+ */
361
+ recordOutOfTurnUsageRow?: (row: OutOfTurnUsageRow) => void;
295
362
  /**
296
363
  * Return the current session's entries for token estimation. The chat-loop
297
364
  * calls this on every submit so the auto-compaction threshold sees the
@@ -369,6 +436,12 @@ export interface CreateChatLoopDeps {
369
436
  * composition; the loop owns the buffer the reminder lands in.
370
437
  */
371
438
  registerDeferredReminderSink?: (sink: (message: string) => void) => void;
439
+ /**
440
+ * The same seam for findings that are for the operator rather than the model.
441
+ * The watchdog uses it: its run settles after the turn it reviewed, and its
442
+ * blockers become one transcript notice that never enters model context.
443
+ */
444
+ registerDeferredNoticeSink?: (sink: (text: string) => void) => void;
372
445
  /**
373
446
  * Host-finalizer seam for branch-anchored interview snapshots. Called once,
374
447
  * after the ask-user host finalizer has settled the policy and its transcript.
@@ -399,6 +472,8 @@ export function reloadProtectedArtifactsForSession(
399
472
  export function createChatLoop(deps: CreateChatLoopDeps): ChatLoop {
400
473
  const listeners = new Set<(event: ChatLoopEvent) => void>();
401
474
  const createAgent = deps.createAgent ?? createEngineAgent;
475
+ const sideQuestionRound = deps.runSideQuestion ?? runSideQuestion;
476
+ const handoffRound = deps.runHandoffRound ?? runHandoffRound;
402
477
  const middlewareToolChoice = deps.middlewareToolChoice ?? createMiddlewareToolChoiceControl();
403
478
  const state = createTurnState(deps.getSettings().orchestrator.thinkingLevel ?? "off");
404
479
  const toolStartTimes = new Map<string, number>();
@@ -485,6 +560,7 @@ export function createChatLoop(deps: CreateChatLoopDeps): ChatLoop {
485
560
 
486
561
  try {
487
562
  deps.registerDeferredReminderSink?.((message) => middleware.injectDeferredReminder(message));
563
+ deps.registerDeferredNoticeSink?.((text) => middleware.emitDeferredNotice(text));
488
564
  } catch {
489
565
  // A background observer losing its delivery path must not stop the loop
490
566
  // from starting; it simply stays silent.
@@ -592,6 +668,102 @@ export function createChatLoop(deps: CreateChatLoopDeps): ChatLoop {
592
668
  return null;
593
669
  };
594
670
 
671
+ /**
672
+ * Everything an out-of-turn round needs, or the reason it cannot run.
673
+ *
674
+ * `/btw` and `/handoff` are the two callers. Both read the compiled history
675
+ * the next turn would see, both authenticate exactly the way a turn against
676
+ * the same target does, and both are refused rather than queued while a turn
677
+ * is in flight, so the admission decision is made once here.
678
+ */
679
+ type OutOfTurnPreparation =
680
+ | { ok: true; runtime: AgentRuntime; apiKey: string | undefined }
681
+ | { ok: false; reason: string };
682
+
683
+ const prepareOutOfTurnRound = async (inFlightRefusal: string, signal?: AbortSignal): Promise<OutOfTurnPreparation> => {
684
+ if (state.streaming) return { ok: false, reason: inFlightRefusal };
685
+ let agentRuntime: AgentRuntime | null;
686
+ try {
687
+ agentRuntime = turnRuntime.ensureRuntime();
688
+ } catch (err) {
689
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
690
+ }
691
+ if (!agentRuntime) return { ok: false, reason: notConfiguredNotice() };
692
+ const resolution = agentRuntime.runtimeResolution;
693
+ try {
694
+ const apiKey = targetRequiresAuth(resolution.target, resolution.runtime)
695
+ ? (
696
+ await deps.providers.auth.resolveForTarget(resolution.target, resolution.runtime, signal ? { signal } : undefined)
697
+ ).apiKey
698
+ : LOCAL_SIDE_QUESTION_API_KEY;
699
+ return { ok: true, runtime: agentRuntime, apiKey };
700
+ } catch (err) {
701
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
702
+ }
703
+ };
704
+
705
+ const writeOutOfTurnUsageRow =
706
+ deps.recordOutOfTurnUsageRow ?? ((row: OutOfTurnUsageRow): void => appendOutOfTurnUsageRow(clioStateDir(), row));
707
+
708
+ /**
709
+ * Report an out-of-turn round's provider usage. Money was spent, so `/cost`
710
+ * says so under its own label; turn persistence, the working-set ledger,
711
+ * compaction inputs, and the footer counters never see it.
712
+ *
713
+ * The same call is also appended to the out-of-turn usage store under the
714
+ * state dir. That store exists because `/cost` only knows what this process
715
+ * spent: the round appends nothing to the session JSONL by design, so an
716
+ * archive reader such as `clio-coder usage report` had no record of the
717
+ * spend at all once the process exited. The session ledger stays untouched.
718
+ */
719
+ const recordOutOfTurnUsage = (
720
+ runtime: AgentRuntime,
721
+ usage: SideQuestionResult["usage"],
722
+ label: CostEntryLabel,
723
+ ): void => {
724
+ if (!usage) return;
725
+ const costProvenance = runtime.runtimeResolution.costProvenance;
726
+ deps.observability?.recordTokens(
727
+ runtime.targetId,
728
+ runtime.wireModelId,
729
+ usage.totalTokens,
730
+ usage.costUsd,
731
+ {
732
+ input: usage.input,
733
+ output: usage.output,
734
+ cacheRead: usage.cacheRead,
735
+ cacheWrite: usage.cacheWrite,
736
+ reasoningTokens: usage.reasoning,
737
+ totalTokens: usage.totalTokens,
738
+ apiCalls: 1,
739
+ },
740
+ costProvenance,
741
+ undefined,
742
+ label,
743
+ );
744
+ const meta = deps.session?.current() ?? null;
745
+ writeOutOfTurnUsageRow({
746
+ label,
747
+ sessionId: meta?.id ?? null,
748
+ // The identity the session ledger is filed under, so `usage report
749
+ // --repo` selects these rows with the same hash it selects ledgers with.
750
+ repoIdentity: meta ? meta.cwdHash || cwdHash(meta.cwd || process.cwd()) : null,
751
+ timestamp: new Date().toISOString(),
752
+ target: runtime.targetId,
753
+ attributedModelId: runtime.wireModelId,
754
+ usage: {
755
+ input: usage.input,
756
+ output: usage.output,
757
+ cacheRead: usage.cacheRead,
758
+ cacheWrite: usage.cacheWrite,
759
+ reasoning: usage.reasoning,
760
+ totalTokens: usage.totalTokens,
761
+ costUsd: usage.costUsd,
762
+ costProvenance: normalizeCostProvenance(costProvenance),
763
+ },
764
+ });
765
+ };
766
+
595
767
  const api: ChatLoop = {
596
768
  steer: (text) => queues.steer(text),
597
769
  queueFollowUp: (text) => queues.queueFollowUp(text),
@@ -1024,6 +1196,68 @@ export function createChatLoop(deps: CreateChatLoopDeps): ChatLoop {
1024
1196
  middlewareToolChoice.reset();
1025
1197
  },
1026
1198
 
1199
+ async askSideQuestion(question: string, options: SideQuestionOptions = {}): Promise<SideQuestionOutcome> {
1200
+ const text = question.trim();
1201
+ if (text.length === 0) {
1202
+ return { status: "refused", reason: "a side question needs a question" };
1203
+ }
1204
+ // Never queued. A side question exists to be answered now, beside a run
1205
+ // the operator is watching; holding it until the run settles would
1206
+ // deliver it after the moment it was asked in had passed.
1207
+ const prepared = await prepareOutOfTurnRound(
1208
+ "a turn is in flight; /btw runs beside the session, not in its queue",
1209
+ options.signal,
1210
+ );
1211
+ if (!prepared.ok) return { status: "refused", reason: prepared.reason };
1212
+ let result: SideQuestionResult;
1213
+ try {
1214
+ result = await sideQuestionRound({
1215
+ model: prepared.runtime.agent.state.model,
1216
+ // Read-only: runSideQuestion copies before appending its own
1217
+ // message, so the live agent's history is untouched.
1218
+ messages: prepared.runtime.agent.state.messages,
1219
+ question: text,
1220
+ ...(prepared.apiKey !== undefined ? { apiKey: prepared.apiKey } : {}),
1221
+ ...(options.signal ? { signal: options.signal } : {}),
1222
+ ...(options.onDelta ? { onDelta: options.onDelta } : {}),
1223
+ });
1224
+ } catch (err) {
1225
+ return { status: "failed", reason: err instanceof Error ? err.message : String(err) };
1226
+ }
1227
+ recordOutOfTurnUsage(prepared.runtime, result.usage, "side-question");
1228
+ return result.aborted ? { status: "aborted", text: result.text } : { status: "answered", text: result.text };
1229
+ },
1230
+
1231
+ async extractHandoff(goal: string, options: SideQuestionOptions = {}): Promise<SideQuestionOutcome> {
1232
+ const text = goal.trim();
1233
+ if (text.length === 0) {
1234
+ return { status: "refused", reason: "a handoff needs a goal" };
1235
+ }
1236
+ // Refused, never queued: a handoff describes a session that has stopped
1237
+ // working, and a turn still in flight is about to change what the
1238
+ // document would say.
1239
+ const prepared = await prepareOutOfTurnRound(
1240
+ "a turn is in flight; /handoff cannot summarize a session that is still moving",
1241
+ options.signal,
1242
+ );
1243
+ if (!prepared.ok) return { status: "refused", reason: prepared.reason };
1244
+ let result: SideQuestionResult;
1245
+ try {
1246
+ result = await handoffRound({
1247
+ model: prepared.runtime.agent.state.model,
1248
+ // Read-only, exactly as the side-question round treats it.
1249
+ messages: prepared.runtime.agent.state.messages,
1250
+ goal: text,
1251
+ ...(prepared.apiKey !== undefined ? { apiKey: prepared.apiKey } : {}),
1252
+ ...(options.signal ? { signal: options.signal } : {}),
1253
+ });
1254
+ } catch (err) {
1255
+ return { status: "failed", reason: err instanceof Error ? err.message : String(err) };
1256
+ }
1257
+ recordOutOfTurnUsage(prepared.runtime, result.usage, "handoff");
1258
+ return result.aborted ? { status: "aborted", text: result.text } : { status: "answered", text: result.text };
1259
+ },
1260
+
1027
1261
  async compact(instructions?: string): Promise<void> {
1028
1262
  // Session check runs BEFORE orchestrator-configuration so a fresh
1029
1263
  // TUI with nothing configured still reports the actionable "no
@@ -31,6 +31,13 @@ import type {
31
31
  ThinkingLevelChangeEntry,
32
32
  WorkerRunEntry,
33
33
  } from "../domains/session/entries.js";
34
+ import {
35
+ HANDOFF_NOTE_CUSTOM_TYPE,
36
+ HANDOFF_SEED_CUSTOM_TYPE,
37
+ handoffSeedContextText,
38
+ isHandoffNoteData,
39
+ isHandoffSeedData,
40
+ } from "../domains/session/handoff.js";
34
41
  import { filterEntriesToActivePath } from "../domains/session/tree/active-path.js";
35
42
  import {
36
43
  type BashExecutionMessage,
@@ -795,11 +802,18 @@ function rendersCustomEntry(entry: CustomEntry): boolean {
795
802
  if (entry.display === false) return false;
796
803
  if (entry.customType === "retryStatus") return true;
797
804
  if (entry.customType === "finishContractAdvisory" || entry.customType === "middlewareReminder") return true;
805
+ if (entry.customType === HANDOFF_SEED_CUSTOM_TYPE || entry.customType === HANDOFF_NOTE_CUSTOM_TYPE) return true;
798
806
  return entry.display === true;
799
807
  }
800
808
 
801
809
  function renderCustomEntry(entry: CustomEntry, width: number): string[] {
802
810
  if (entry.customType === "retryStatus") return renderRetryStatusEntry(entry, width);
811
+ if (entry.customType === HANDOFF_SEED_CUSTOM_TYPE && isHandoffSeedData(entry.data)) {
812
+ return wrapTextWithAnsi(styleTaggedNotice(`[handoff] carried from session ${entry.data.fromSessionId}`), width);
813
+ }
814
+ if (entry.customType === HANDOFF_NOTE_CUSTOM_TYPE && isHandoffNoteData(entry.data)) {
815
+ return wrapTextWithAnsi(styleTaggedNotice(`[handoff] handed off to session ${entry.data.toSessionId}`), width);
816
+ }
803
817
  // "finishContractAdvisory" is the pre-middleware name for the same entry
804
818
  // shape; older session ledgers still carry it.
805
819
  if (entry.customType === "finishContractAdvisory" || entry.customType === "middlewareReminder") {
@@ -1039,7 +1053,15 @@ export function buildReplayAgentMessagesFromTurns(
1039
1053
  case "skillActivation":
1040
1054
  appendContextMessage(out, "user", skillActivationContextText(entry), entry.timestamp);
1041
1055
  break;
1056
+ // The one custom entry that becomes a model message. `/handoff` seeds a
1057
+ // new session with a reviewed document, and the seed is the first thing
1058
+ // the model reads there; it is labelled by its origin session and
1059
+ // carried as data, never as a user turn the operator did not write.
1042
1060
  case "custom":
1061
+ if (entry.customType === HANDOFF_SEED_CUSTOM_TYPE && isHandoffSeedData(entry.data)) {
1062
+ appendContextMessage(out, "user", handoffSeedContextText(entry.data), entry.timestamp);
1063
+ }
1064
+ break;
1043
1065
  case "modelChange":
1044
1066
  case "thinkingLevelChange":
1045
1067
  case "fileEntry":