@iowarp/clio-coder 0.3.6 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +22 -6
  3. package/dist/{acp-2BEHC4DL.js → acp-U67UHUK2.js} +14 -14
  4. package/dist/{agents-LNNFTM53.js → agents-YU6SGALZ.js} +42 -35
  5. package/dist/assets/codewiki.json +1 -1
  6. package/dist/{auth-KXXFI2VS.js → auth-5ZPJOIVG.js} +32 -24
  7. package/dist/builtins-C6JMZVV6.js +17 -0
  8. package/dist/{chunk-E25LMLRW.js → chunk-26LEYJZH.js} +2 -2
  9. package/dist/{chunk-TSHXZTOQ.js → chunk-2HEJ2F35.js} +5 -5
  10. package/dist/{chunk-PBTHKCPN.js → chunk-2HFZQUHL.js} +7 -7
  11. package/dist/{chunk-FO5ZOVUY.js → chunk-3BINW3FP.js} +5 -5
  12. package/dist/{chunk-4OC57DA6.js → chunk-4DGYLA73.js} +53 -2
  13. package/dist/{chunk-CKXWIANG.js → chunk-4SPRNWDE.js} +18 -16
  14. package/dist/{chunk-E2ER4LJF.js → chunk-5C3AQNDW.js} +25 -1
  15. package/dist/{chunk-XF5N4U5A.js → chunk-5DHKRSMQ.js} +9 -8
  16. package/dist/{chunk-43AOLP7E.js → chunk-5FR74PWO.js} +2 -1
  17. package/dist/{chunk-EKY57CSP.js → chunk-5H3GB5BO.js} +68 -772
  18. package/dist/{chunk-6XXKFVSN.js → chunk-5Q2VVUKB.js} +4 -4
  19. package/dist/{chunk-DJVECN66.js → chunk-7RFXX52T.js} +295 -3705
  20. package/dist/{chunk-ZXF4XRKW.js → chunk-7RGZWPB6.js} +158 -8
  21. package/dist/{chunk-FYYLNIL5.js → chunk-A2NJGIB3.js} +2 -2
  22. package/dist/{chunk-XXQNGV4M.js → chunk-A3WNZD3P.js} +548 -72
  23. package/dist/{chunk-VEZEGCGW.js → chunk-B5CSFE7B.js} +23 -21
  24. package/dist/{chunk-2SFS6XQE.js → chunk-DGSYXYMX.js} +3 -2
  25. package/dist/chunk-DR52UMZW.js +21 -0
  26. package/dist/{chunk-ZWLZP4ZT.js → chunk-DYIM5TJT.js} +98 -12
  27. package/dist/{chunk-QKMUKYO7.js → chunk-E77JEWSD.js} +270 -125
  28. package/dist/{chunk-LYF7OHWH.js → chunk-EMYUUSFG.js} +12 -467
  29. package/dist/{chunk-4VP4KH3K.js → chunk-EQ63NRB7.js} +8 -8
  30. package/dist/chunk-FBVTI2TJ.js +518 -0
  31. package/dist/{chunk-R46L2BIR.js → chunk-FHJEP5SW.js} +19 -25
  32. package/dist/{chunk-WR67VIZY.js → chunk-GN57SG4G.js} +66 -8
  33. package/dist/{chunk-RD5U66HV.js → chunk-GPIEI3LY.js} +9 -9
  34. package/dist/{chunk-PCZJO5TI.js → chunk-GU2UIAFZ.js} +13 -178
  35. package/dist/chunk-GWS3VEIW.js +195 -0
  36. package/dist/chunk-H7IXIC72.js +103 -0
  37. package/dist/{chunk-3BPUFZDL.js → chunk-HLE42MG7.js} +3 -3
  38. package/dist/{chunk-24I7BN55.js → chunk-IFBNV6H6.js} +3 -3
  39. package/dist/{chunk-QNQHSOLF.js → chunk-IGWKHNIQ.js} +114 -55
  40. package/dist/chunk-IIZWH4XA.js +172 -0
  41. package/dist/{chunk-AD2SYQYC.js → chunk-IJ7RPIYJ.js} +124 -6
  42. package/dist/chunk-J3YUBZWY.js +382 -0
  43. package/dist/{chunk-5JGRAMKL.js → chunk-JOZYP4GM.js} +8 -6
  44. package/dist/{chunk-OH3TOQTB.js → chunk-K4XHGFR5.js} +751 -15
  45. package/dist/{chunk-EYPA3EGJ.js → chunk-KTYTFRMB.js} +178 -14
  46. package/dist/chunk-LU7P4LHA.js +33 -0
  47. package/dist/chunk-ME6CCNFO.js +108 -0
  48. package/dist/chunk-MXKJU4JB.js +1100 -0
  49. package/dist/{chunk-RY3LY4J5.js → chunk-N22QMJKY.js} +21 -18
  50. package/dist/chunk-NMPKI6XL.js +3006 -0
  51. package/dist/chunk-NUGM5KR6.js +165 -0
  52. package/dist/{chunk-22NAGB7X.js → chunk-P43ETTHK.js} +5 -94
  53. package/dist/{chunk-NILBFAPG.js → chunk-PMDBGQSJ.js} +2 -2
  54. package/dist/chunk-PT7HYKEM.js +165 -0
  55. package/dist/chunk-RVG5JXAL.js +41 -0
  56. package/dist/{chunk-GEYXPTRF.js → chunk-RWSI4YD7.js} +2 -2
  57. package/dist/{chunk-4BPJXDWC.js → chunk-TANS5ZJS.js} +35 -19
  58. package/dist/{verifiers-NCBTHHN2.js → chunk-TB5666IT.js} +67 -324
  59. package/dist/{chunk-QM3F2GKX.js → chunk-TLQJPP24.js} +7927 -8088
  60. package/dist/{chunk-G7MUEIGA.js → chunk-TT36MB5S.js} +2 -1
  61. package/dist/chunk-TTHACPOM.js +961 -0
  62. package/dist/{chunk-6US73PDB.js → chunk-TYPGUK6W.js} +7 -7
  63. package/dist/{chunk-MFFY33HR.js → chunk-U6MBIEMB.js} +554 -209
  64. package/dist/chunk-VAWNZU7Z.js +242 -0
  65. package/dist/{chunk-IR4CFBFN.js → chunk-VCBR6CU7.js} +12 -12
  66. package/dist/{chunk-WHJYKASB.js → chunk-VHN4MY6O.js} +2 -2
  67. package/dist/{chunk-XYDYPRZI.js → chunk-VWZOAB7K.js} +10 -10
  68. package/dist/{chunk-ZRGEBJ4T.js → chunk-WLFILSD5.js} +48 -48
  69. package/dist/{chunk-K7T3E2SR.js → chunk-WNIJTQQK.js} +12 -11
  70. package/dist/{chunk-CYQKWTG3.js → chunk-WSB3FPX7.js} +68 -20
  71. package/dist/{chunk-KHSFENX2.js → chunk-WWCZ5F23.js} +116 -14
  72. package/dist/{chunk-XE2VEJHX.js → chunk-WXY7KU3G.js} +2 -2
  73. package/dist/{chunk-PPAMZ32Z.js → chunk-XK56QHLX.js} +6 -1
  74. package/dist/{chunk-KOHPCX4K.js → chunk-XWSF374K.js} +5 -5
  75. package/dist/{chunk-CJUB2JJ2.js → chunk-YS5VLNH5.js} +10 -10
  76. package/dist/{chunk-ZZMN5OM4.js → chunk-ZNLWCMVZ.js} +2 -2
  77. package/dist/{chunk-WHGPSPT5.js → chunk-ZVJ5BLO2.js} +2 -2
  78. package/dist/cli/index.js +33 -31
  79. package/dist/{clio-M2KGYUFZ.js → clio-QVTYJ57A.js} +10 -10
  80. package/dist/{code-nav-GQNL7XA6.js → code-nav-FGGFIE7L.js} +5 -5
  81. package/dist/codewiki/build-worker.js +4 -4
  82. package/dist/{components-5TTYYX6G.js → components-ZFA3SAER.js} +9 -9
  83. package/dist/{config-XUUYQIWO.js → config-LW5IJFQN.js} +65 -55
  84. package/dist/{configure-IHJ7YOMV.js → configure-7XIZCOU4.js} +29 -24
  85. package/dist/{context-75MIWW3U.js → context-L3WL3X7K.js} +54 -43
  86. package/dist/{context-ZQ7SIFJV.js → context-N52ZA626.js} +30 -14
  87. package/dist/{context-74JLXAWD.js → context-Y6Y7QPR6.js} +12 -12
  88. package/dist/{context-clear-GYKWNUML.js → context-clear-MBQRLSDQ.js} +54 -43
  89. package/dist/{context-index-SSR5ECNE.js → context-index-HVMFQHK3.js} +5 -5
  90. package/dist/{context-working-set-UX5KEP4J.js → context-working-set-GS6DSO7F.js} +18 -18
  91. package/dist/{dispatch-runner-GIJBHNFL.js → dispatch-runner-22ZCNOM3.js} +367 -79
  92. package/dist/{docs-6FZSCG5B.js → docs-7LQ23DLM.js} +9 -9
  93. package/dist/{doctor-SVJ5BZCW.js → doctor-M7YEDGAE.js} +27 -23
  94. package/dist/{eval-CG6LLBLD.js → eval-BEC2WHDA.js} +73 -20
  95. package/dist/{evidence-ZYFIEN42.js → evidence-REJUMSKM.js} +70 -59
  96. package/dist/{evolve-QGEXEMDW.js → evolve-PY5ZBA5K.js} +50 -39
  97. package/dist/{extensions-ADGNCJJD.js → extensions-HVKU65YU.js} +7 -7
  98. package/dist/{fleet-S5R4ZOQY.js → fleet-7WZEWRFA.js} +236 -377
  99. package/dist/fleet-commands-UVHWM76J.js +70 -0
  100. package/dist/fleet-graph-6ULH7PES.js +125 -0
  101. package/dist/fleet-new-RDVJLHHH.js +48 -0
  102. package/dist/{fleet-preflight-BHSNPBMH.js → fleet-preflight-J53T6CCE.js} +5 -5
  103. package/dist/fleet-validate-72PC4SLA.js +79 -0
  104. package/dist/{init-5DRU55YR.js → init-OG3TPGQG.js} +69 -57
  105. package/dist/library-CNTMPLRF.js +217 -0
  106. package/dist/{memory-7YKKR6UC.js → memory-6IS7F275.js} +52 -41
  107. package/dist/{models-ZPOLRU2C.js → models-ENRJDA5W.js} +37 -31
  108. package/dist/{monitor-US5F5YGZ.js → monitor-XLDVO7TN.js} +79 -42
  109. package/dist/{orchestrator-E2AL4T5N.js → orchestrator-6KSPYRHA.js} +4126 -762
  110. package/dist/{paths-E7KYAQWE.js → paths-DBXMZMDU.js} +6 -6
  111. package/dist/registry-LG64LTF4.js +11 -0
  112. package/dist/{reset-KZ652EK6.js → reset-RZ4ER727.js} +12 -12
  113. package/dist/{run-SRNBKDWD.js → run-Y2CNK5RU.js} +89 -76
  114. package/dist/{share-CGZE33UP.js → share-A55GYP6Z.js} +37 -13
  115. package/dist/{skills-S2X4DLY5.js → skills-ALC5J6AT.js} +29 -13
  116. package/dist/{skills-eval-W2GGIC4R.js → skills-eval-JPBEBYQU.js} +60 -48
  117. package/dist/support-MIETYA5E.js +38 -0
  118. package/dist/{targets-54SWINWB.js → targets-VGNXIR3S.js} +46 -36
  119. package/dist/{terminal-lease-SAIF2OGY.js → terminal-lease-WOBR64YA.js} +4 -4
  120. package/dist/{uninstall-BVLWXKBT.js → uninstall-ZJF5H5ZN.js} +9 -9
  121. package/dist/{upgrade-JKAR27XC.js → upgrade-FUSUAGHR.js} +31 -28
  122. package/dist/{usage-MSAWCLX4.js → usage-N4MKVHKD.js} +140 -68
  123. package/dist/verifiers-YAWOJ3H2.js +336 -0
  124. package/dist/{verify-X5HDROLA.js → verify-LTDHYBGY.js} +10 -9
  125. package/dist/{wiki-generate-GUSOQ6ZP.js → wiki-generate-6M7GHTBJ.js} +75 -62
  126. package/dist/worker/entry.js +67 -57
  127. package/docs/README.md +3 -2
  128. package/docs/acp.md +1 -1
  129. package/docs/alcf-provider.md +1 -1
  130. package/docs/architecture.md +2 -2
  131. package/docs/artifact-versions.md +11 -6
  132. package/docs/built-in-agents.md +26 -2
  133. package/docs/capacity-and-scheduling.md +1 -1
  134. package/docs/commands-and-modes.md +83 -3
  135. package/docs/configuration-and-targets.md +86 -4
  136. package/docs/context-engine.md +1 -1
  137. package/docs/development-pipeline.md +1 -1
  138. package/docs/dispatch-architecture-rationale.md +1 -1
  139. package/docs/documentation-coverage.md +3 -3
  140. package/docs/documentation-guide.md +3 -3
  141. package/docs/eval-runner.md +1 -1
  142. package/docs/evals-internal.md +1 -1
  143. package/docs/evidence-and-memory.md +74 -10
  144. package/docs/evolution.md +1 -1
  145. package/docs/exit-codes-and-output.md +4 -1
  146. package/docs/extensions-and-sharing.md +6 -2
  147. package/docs/fleet-demo-runbook.md +2 -2
  148. package/docs/fleet-dispatch.md +228 -16
  149. package/docs/git-commit-provenance.md +2 -2
  150. package/docs/glossary.md +22 -2
  151. package/docs/installation-and-lifecycle.md +2 -2
  152. package/docs/middleware-and-components.md +2 -1
  153. package/docs/model-catalog.md +1 -1
  154. package/docs/observability.md +56 -9
  155. package/docs/proactive-memory.md +1 -1
  156. package/docs/prompt-envelope-and-tools.md +4 -2
  157. package/docs/provider-adapter-cookbook.md +1 -1
  158. package/docs/release-cut-checklist.md +83 -65
  159. package/docs/resource-library.md +59 -0
  160. package/docs/safety-model.md +2 -2
  161. package/docs/scientific-validation.md +3 -3
  162. package/docs/session-lifecycle.md +37 -1
  163. package/docs/skills-marketplace.md +16 -3
  164. package/docs/tool-usage.md +14 -7
  165. package/docs/trace-store.md +1 -1
  166. package/docs/troubleshooting.md +1 -1
  167. package/docs/tui-design.md +1 -1
  168. package/docs/worker-dispatch-mechanics.md +3 -3
  169. package/package.json +1 -2
  170. package/src/cli/argv.ts +5 -0
  171. package/src/cli/configure.ts +107 -23
  172. package/src/cli/doctor.ts +5 -1
  173. package/src/cli/evidence.ts +30 -25
  174. package/src/cli/fleet-commands.ts +37 -0
  175. package/src/cli/fleet-graph.ts +102 -0
  176. package/src/cli/fleet-new.ts +36 -0
  177. package/src/cli/fleet-preflight.ts +111 -0
  178. package/src/cli/fleet-validate.ts +30 -0
  179. package/src/cli/fleet.ts +173 -335
  180. package/src/cli/index.ts +3 -1
  181. package/src/cli/library.ts +190 -0
  182. package/src/cli/share.ts +13 -1
  183. package/src/cli/shared.ts +1 -0
  184. package/src/cli/targets.ts +4 -1
  185. package/src/cli/usage.ts +111 -19
  186. package/src/cli/validate-model.ts +60 -5
  187. package/src/core/bus-events.ts +29 -0
  188. package/src/core/commit-attribution.ts +4 -4
  189. package/src/core/config.ts +130 -0
  190. package/src/core/defaults.ts +81 -0
  191. package/src/core/path-boundary.ts +100 -0
  192. package/src/domains/agents/builtins/architect.md +1 -0
  193. package/src/domains/agents/builtins/oracle.md +33 -0
  194. package/src/domains/agents/catalog.ts +13 -1
  195. package/src/domains/agents/extension.ts +2 -11
  196. package/src/domains/agents/fleet-contract.ts +304 -24
  197. package/src/domains/agents/index.ts +14 -0
  198. package/src/domains/agents/recipe.ts +7 -1
  199. package/src/domains/agents/registry.ts +73 -5
  200. package/src/domains/agents/result-contract.ts +360 -15
  201. package/src/domains/agents/write-boundary.ts +15 -50
  202. package/src/domains/config/classify.ts +4 -0
  203. package/src/domains/context/project-rules.ts +51 -1
  204. package/src/domains/dispatch/active-route-planner.ts +14 -0
  205. package/src/domains/dispatch/assignment-reconcile.ts +22 -5
  206. package/src/domains/dispatch/assignment-store.ts +151 -14
  207. package/src/domains/dispatch/backoff.ts +2 -1
  208. package/src/domains/dispatch/capability-match.ts +1 -0
  209. package/src/domains/dispatch/checkout-writer-lease.ts +175 -0
  210. package/src/domains/dispatch/contract.ts +48 -0
  211. package/src/domains/dispatch/delegation-plan.ts +164 -0
  212. package/src/domains/dispatch/execution-plan.ts +76 -5
  213. package/src/domains/dispatch/execution-role.ts +12 -2
  214. package/src/domains/dispatch/execution-scheduler.ts +183 -67
  215. package/src/domains/dispatch/extension.ts +412 -74
  216. package/src/domains/dispatch/fleet-gate.ts +14 -0
  217. package/src/domains/dispatch/fleet-plan.ts +63 -3
  218. package/src/domains/dispatch/fleet-run.ts +791 -0
  219. package/src/domains/dispatch/gate-role-prompts.ts +47 -0
  220. package/src/domains/dispatch/host-verification.ts +178 -0
  221. package/src/domains/dispatch/index.ts +41 -1
  222. package/src/domains/dispatch/intent-requirements.ts +40 -0
  223. package/src/domains/dispatch/intent.ts +235 -0
  224. package/src/domains/dispatch/path-scope.ts +370 -0
  225. package/src/domains/dispatch/receipt-integrity.ts +9 -4
  226. package/src/domains/dispatch/state.ts +36 -3
  227. package/src/domains/dispatch/types.ts +64 -12
  228. package/src/domains/dispatch/validation.ts +69 -6
  229. package/src/domains/dispatch/write-boundary-enforcer.ts +45 -0
  230. package/src/domains/dispatch/write-boundary.ts +201 -22
  231. package/src/domains/eval/metrics/evidence.ts +79 -2
  232. package/src/domains/eval/runners/clio-run.ts +12 -2
  233. package/src/domains/evidence/build.ts +69 -11
  234. package/src/domains/evidence/index.ts +21 -0
  235. package/src/domains/evidence/provenance.ts +46 -11
  236. package/src/domains/evidence/trust-projection.ts +274 -0
  237. package/src/domains/evidence/trust-status.ts +155 -18
  238. package/src/domains/evidence/types.ts +4 -0
  239. package/src/domains/extensions/discovery.ts +88 -1
  240. package/src/domains/extensions/resources.ts +20 -8
  241. package/src/domains/extensions/state.ts +6 -2
  242. package/src/domains/extensions/types.ts +4 -1
  243. package/src/domains/lifecycle/doctor.ts +140 -1
  244. package/src/domains/middleware/index.ts +15 -0
  245. package/src/domains/middleware/watchdog.ts +281 -0
  246. package/src/domains/observability/contract.ts +3 -1
  247. package/src/domains/observability/cost.ts +12 -1
  248. package/src/domains/observability/extension.ts +2 -2
  249. package/src/domains/observability/index.ts +10 -0
  250. package/src/domains/observability/out-of-turn-usage.ts +223 -0
  251. package/src/domains/prompts/contract.ts +3 -5
  252. package/src/domains/providers/extension.ts +30 -2
  253. package/src/domains/resources/common-loader.ts +3 -0
  254. package/src/domains/resources/index.ts +20 -0
  255. package/src/domains/resources/library.ts +326 -0
  256. package/src/domains/resources/prompts/loader.ts +119 -14
  257. package/src/domains/resources/skills/marketplace.ts +37 -12
  258. package/src/domains/safety/policy-engine.ts +5 -5
  259. package/src/domains/safety/run-effects.ts +64 -1
  260. package/src/domains/safety/scope.ts +7 -12
  261. package/src/domains/session/handoff.ts +629 -0
  262. package/src/domains/share/archive.ts +67 -2
  263. package/src/engine/acp/server.ts +4 -1
  264. package/src/engine/prompt-templates.ts +18 -1
  265. package/src/engine/worker-runtime.ts +6 -3
  266. package/src/entry/orchestrator.ts +37 -0
  267. package/src/interactive/bus-notices.ts +26 -0
  268. package/src/interactive/chat-loop.ts +235 -1
  269. package/src/interactive/chat-renderer.ts +22 -0
  270. package/src/interactive/cost-overlay.ts +31 -3
  271. package/src/interactive/council-dispatch.ts +30 -0
  272. package/src/interactive/council-grid.ts +213 -0
  273. package/src/interactive/council.ts +99 -0
  274. package/src/interactive/dispatch-board.ts +311 -17
  275. package/src/interactive/fleet-run-preview.ts +307 -0
  276. package/src/interactive/footer/notifications.ts +219 -0
  277. package/src/interactive/handoff-round.ts +56 -0
  278. package/src/interactive/interactive-application.ts +43 -1
  279. package/src/interactive/interactive-event-projection.ts +23 -1
  280. package/src/interactive/interactive-slash-runtime.ts +52 -2
  281. package/src/interactive/interactive-subscriptions.ts +14 -2
  282. package/src/interactive/oracle.ts +179 -0
  283. package/src/interactive/overlay-ask-user-lifecycle.ts +6 -0
  284. package/src/interactive/overlay-general-openers.ts +190 -1
  285. package/src/interactive/overlay-key-routing.ts +17 -1
  286. package/src/interactive/overlay-lifecycle.ts +41 -1
  287. package/src/interactive/overlay-permission-lifecycle.ts +10 -0
  288. package/src/interactive/overlay-resource-openers.ts +11 -3
  289. package/src/interactive/overlay-session-lifecycle.ts +234 -2
  290. package/src/interactive/overlays/fleet-run-approval.ts +208 -0
  291. package/src/interactive/overlays/handoff-review.ts +185 -0
  292. package/src/interactive/overlays/library-install-confirm.ts +151 -0
  293. package/src/interactive/overlays/list-overlay.ts +168 -2
  294. package/src/interactive/overlays/settings.ts +221 -30
  295. package/src/interactive/overlays/side-question.ts +139 -0
  296. package/src/interactive/overlays/skills-hub.ts +401 -15
  297. package/src/interactive/side-question.ts +171 -0
  298. package/src/interactive/slash-commands.ts +439 -7
  299. package/src/interactive/slash-spec.ts +19 -6
  300. package/src/interactive/theme/tokens.ts +30 -0
  301. package/src/interactive/turn-middleware.ts +15 -1
  302. package/src/interactive/view/artifacts.ts +42 -9
  303. package/src/interactive/view/view-overlay.ts +15 -3
  304. package/src/interactive/watchdog-run.ts +75 -0
  305. package/src/interactive/worker-receipts.ts +14 -2
  306. package/src/interactive/worker-share.ts +56 -1
  307. package/src/interactive/worker-stream.ts +15 -0
  308. package/src/tools/bootstrap.ts +3 -0
  309. package/src/tools/compete-worktrees.ts +13 -79
  310. package/src/tools/dispatch-admission.ts +251 -18
  311. package/src/tools/dispatch-arguments.ts +84 -1
  312. package/src/tools/dispatch-plan.ts +165 -6
  313. package/src/tools/dispatch-runner.ts +364 -23
  314. package/src/tools/dispatch-types.ts +20 -1
  315. package/src/tools/dispatch.ts +72 -2
  316. package/src/tools/monitor.ts +29 -0
  317. package/src/tools/profiles.ts +18 -4
  318. package/src/tools/task-worktree.ts +238 -0
  319. package/src/tools/verify/authoring.ts +61 -1
  320. package/src/tools/verify/scripts.ts +62 -0
  321. package/src/tools/worker-evidence.ts +21 -14
  322. package/src/worker/spec-contract.ts +3 -1
  323. package/dist/chunk-HC4CLZ2Y.js +0 -68
@@ -0,0 +1,629 @@
1
+ /**
2
+ * `/handoff <goal>`: carry one session's working state into a fresh session.
3
+ *
4
+ * A handoff is a session operation and nothing else. It never writes a memory
5
+ * promotion candidate, never touches the memory domain, and never calls the
6
+ * task-memory bank. What it produces is one reviewed Markdown document, seeded
7
+ * into a newly minted session as bounded data, plus a terminal note in the old
8
+ * session naming where the work continued.
9
+ *
10
+ * Everything in this module is pure. The model round, the operator review, and
11
+ * the session writes live in the interactive layer; the rules that decide what
12
+ * a handoff may contain live here so they can be asserted without a provider,
13
+ * a terminal, or a session directory.
14
+ *
15
+ * The one rule worth stating twice is path validation. Every file the model
16
+ * names is checked against this session's read ledger, meaning the set of
17
+ * workspace-relative paths its own persisted tool calls read, edited, wrote,
18
+ * listed, or grepped. It is never checked against the filesystem: a path that
19
+ * exists on disk but that this session never touched is still an invention, and
20
+ * the point of the check is to show the operator what the model invented rather
21
+ * than to confirm that the repository has files in it.
22
+ */
23
+
24
+ import { isAbsolute, normalize, relative, resolve } from "node:path";
25
+ import type { DecisionLedgerEntry, SessionEntry } from "./entries.js";
26
+ import { filterEntriesToActivePath } from "./tree/active-path.js";
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Goal gate
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /** Shortest goal a handoff accepts. Anything shorter cannot name what to continue. */
33
+ export const HANDOFF_MIN_GOAL_LENGTH = 12;
34
+
35
+ /**
36
+ * Goals that name no goal. Each of these says "carry on" without saying what
37
+ * to carry on with, which is exactly the state a handoff exists to end.
38
+ */
39
+ export const HANDOFF_GOAL_STOPLIST: ReadonlyArray<string> = [
40
+ "continue",
41
+ "keep going",
42
+ "same",
43
+ "next",
44
+ "go on",
45
+ "proceed",
46
+ "resume",
47
+ ];
48
+
49
+ export type HandoffGoalVerdict = { ok: true; goal: string } | { ok: false; reason: string };
50
+
51
+ /**
52
+ * The gate, stated as the rule it enforces. A refusal names the rule so the
53
+ * operator can see which of the two conditions rejected the line rather than
54
+ * guessing at a usage string.
55
+ *
56
+ * The stoplist is checked before the length, because every stoplist entry is
57
+ * also shorter than the minimum and the length message would be the less useful
58
+ * of the two. An operator who typed "continue" wants to hear that it names no
59
+ * goal, not that it is four characters short.
60
+ */
61
+ export function validateHandoffGoal(raw: string): HandoffGoalVerdict {
62
+ const goal = raw.trim();
63
+ if (HANDOFF_GOAL_STOPLIST.includes(goal.toLowerCase())) {
64
+ return {
65
+ ok: false,
66
+ reason: `/handoff refuses "${goal}" because it names no goal; say what the next session should accomplish`,
67
+ };
68
+ }
69
+ if (goal.length < HANDOFF_MIN_GOAL_LENGTH) {
70
+ return {
71
+ ok: false,
72
+ reason: `/handoff needs a goal of at least ${HANDOFF_MIN_GOAL_LENGTH} characters; "${goal}" is ${goal.length}`,
73
+ };
74
+ }
75
+ return { ok: true, goal };
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Extraction shape and bounds
80
+ // ---------------------------------------------------------------------------
81
+
82
+ export interface HandoffDecision {
83
+ summary: string;
84
+ rationale?: string;
85
+ }
86
+
87
+ export interface HandoffFile {
88
+ path: string;
89
+ why: string;
90
+ }
91
+
92
+ export interface HandoffCommand {
93
+ argv: string;
94
+ why: string;
95
+ }
96
+
97
+ export interface HandoffExtraction {
98
+ decisions: HandoffDecision[];
99
+ facts: string[];
100
+ files: HandoffFile[];
101
+ commands: HandoffCommand[];
102
+ openQuestions: string[];
103
+ }
104
+
105
+ /** Per-list item ceilings. Over-bound output is truncated, never refused. */
106
+ export const HANDOFF_LIST_BOUNDS = {
107
+ decisions: 24,
108
+ facts: 32,
109
+ files: 48,
110
+ commands: 16,
111
+ openQuestions: 16,
112
+ } as const;
113
+
114
+ /** Ceiling for every string in the extraction, marker included. */
115
+ export const HANDOFF_MAX_STRING_BYTES = 512;
116
+
117
+ /** Visible flag appended to a string the byte bound cut. */
118
+ export const HANDOFF_TRUNCATION_MARKER = "…[truncated]";
119
+
120
+ /**
121
+ * The output contract for the extraction round, in the JSON-schema subset
122
+ * `src/core/response-schema.ts` validates. A runtime that speaks the response
123
+ * schema dialect can enforce it on the wire; every other runtime receives it as
124
+ * the round's stated contract and has its answer validated here on the way
125
+ * back. Either way the parse below is the authority.
126
+ */
127
+ export const HANDOFF_RESPONSE_SCHEMA: Record<string, unknown> = {
128
+ type: "object",
129
+ additionalProperties: false,
130
+ required: ["decisions", "facts", "files", "commands", "openQuestions"],
131
+ properties: {
132
+ decisions: {
133
+ type: "array",
134
+ items: {
135
+ type: "object",
136
+ additionalProperties: false,
137
+ required: ["summary"],
138
+ properties: { summary: { type: "string" }, rationale: { type: "string" } },
139
+ },
140
+ },
141
+ facts: { type: "array", items: { type: "string" } },
142
+ files: {
143
+ type: "array",
144
+ items: {
145
+ type: "object",
146
+ additionalProperties: false,
147
+ required: ["path", "why"],
148
+ properties: { path: { type: "string" }, why: { type: "string" } },
149
+ },
150
+ },
151
+ commands: {
152
+ type: "array",
153
+ items: {
154
+ type: "object",
155
+ additionalProperties: false,
156
+ required: ["argv", "why"],
157
+ properties: { argv: { type: "string" }, why: { type: "string" } },
158
+ },
159
+ },
160
+ openQuestions: { type: "array", items: { type: "string" } },
161
+ },
162
+ };
163
+
164
+ export interface HandoffParseResult {
165
+ extraction: HandoffExtraction;
166
+ /** One line per bound that fired, rendered into the document so nothing is cut silently. */
167
+ truncations: string[];
168
+ }
169
+
170
+ function isRecord(value: unknown): value is Record<string, unknown> {
171
+ return typeof value === "object" && value !== null && !Array.isArray(value);
172
+ }
173
+
174
+ /**
175
+ * Cut a string to the byte ceiling and flag it. The marker counts against the
176
+ * ceiling, so the returned string is never longer than the bound, and the cut
177
+ * lands on a whole code point rather than mid-sequence.
178
+ */
179
+ export function boundHandoffString(value: string): { text: string; truncated: boolean } {
180
+ const text = value.trim();
181
+ const bytes = Buffer.byteLength(text, "utf8");
182
+ if (bytes <= HANDOFF_MAX_STRING_BYTES) return { text, truncated: false };
183
+ const markerBytes = Buffer.byteLength(HANDOFF_TRUNCATION_MARKER, "utf8");
184
+ const room = Math.max(0, HANDOFF_MAX_STRING_BYTES - markerBytes);
185
+ // `Buffer.toString` on a cut that lands mid-sequence yields a replacement
186
+ // character; dropping trailing replacement characters keeps the result a
187
+ // prefix of the original rather than a prefix plus a glyph the model never
188
+ // produced.
189
+ const head = Buffer.from(text, "utf8").subarray(0, room).toString("utf8").replace(/�+$/u, "");
190
+ return { text: `${head}${HANDOFF_TRUNCATION_MARKER}`, truncated: true };
191
+ }
192
+
193
+ function boundList<T>(items: ReadonlyArray<T>, limit: number, label: string, truncations: string[]): ReadonlyArray<T> {
194
+ if (items.length <= limit) return items;
195
+ truncations.push(`${label}: kept ${limit} of ${items.length}; the rest were dropped by the ${label} bound`);
196
+ return items.slice(0, limit);
197
+ }
198
+
199
+ function stringsFrom(value: unknown): string[] {
200
+ if (!Array.isArray(value)) return [];
201
+ return value.flatMap((item) => (typeof item === "string" && item.trim().length > 0 ? [item.trim()] : []));
202
+ }
203
+
204
+ function recordsFrom(value: unknown): Record<string, unknown>[] {
205
+ if (!Array.isArray(value)) return [];
206
+ return value.filter(isRecord);
207
+ }
208
+
209
+ function requiredField(record: Record<string, unknown>, key: string): string | null {
210
+ const value = record[key];
211
+ if (typeof value !== "string") return null;
212
+ const trimmed = value.trim();
213
+ return trimmed.length > 0 ? trimmed : null;
214
+ }
215
+
216
+ /**
217
+ * The first JSON object in the round's answer. Providers that honor the schema
218
+ * return the object alone; providers that do not often wrap it in a fenced
219
+ * block or a sentence, and refusing those would throw away a usable answer.
220
+ */
221
+ function extractJsonObject(text: string): string | null {
222
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
223
+ const candidate = (fenced?.[1] ?? text).trim();
224
+ const start = candidate.indexOf("{");
225
+ if (start < 0) return null;
226
+ let depth = 0;
227
+ let inString = false;
228
+ let escaped = false;
229
+ for (let index = start; index < candidate.length; index += 1) {
230
+ const char = candidate[index];
231
+ if (inString) {
232
+ if (escaped) escaped = false;
233
+ else if (char === "\\") escaped = true;
234
+ else if (char === '"') inString = false;
235
+ continue;
236
+ }
237
+ if (char === '"') inString = true;
238
+ else if (char === "{") depth += 1;
239
+ else if (char === "}") {
240
+ depth -= 1;
241
+ if (depth === 0) return candidate.slice(start, index + 1);
242
+ }
243
+ }
244
+ return null;
245
+ }
246
+
247
+ export type HandoffParseOutcome = { ok: true; result: HandoffParseResult } | { ok: false; reason: string };
248
+
249
+ /**
250
+ * Validate the round's answer against the response schema and bound it.
251
+ *
252
+ * Bounds never refuse. A model that answers with two hundred facts gets the
253
+ * first thirty-two and a line in the document saying so, because an operator
254
+ * reviewing a truncated handoff is strictly better off than one holding a
255
+ * refusal.
256
+ */
257
+ export function parseHandoffExtraction(text: string): HandoffParseOutcome {
258
+ const json = extractJsonObject(text);
259
+ if (json === null) return { ok: false, reason: "the extraction round returned no JSON object" };
260
+ let parsed: unknown;
261
+ try {
262
+ parsed = JSON.parse(json);
263
+ } catch (error) {
264
+ return { ok: false, reason: `the extraction round returned invalid JSON: ${(error as Error).message}` };
265
+ }
266
+ if (!isRecord(parsed)) return { ok: false, reason: "the extraction round returned a non-object" };
267
+
268
+ const truncations: string[] = [];
269
+ let stringsCut = 0;
270
+ const bound = (value: string): string => {
271
+ const result = boundHandoffString(value);
272
+ if (result.truncated) stringsCut += 1;
273
+ return result.text;
274
+ };
275
+
276
+ const decisions = boundList(
277
+ recordsFrom(parsed.decisions),
278
+ HANDOFF_LIST_BOUNDS.decisions,
279
+ "decisions",
280
+ truncations,
281
+ ).flatMap((record) => {
282
+ const summary = requiredField(record, "summary");
283
+ if (summary === null) return [];
284
+ const rationale = requiredField(record, "rationale");
285
+ return [{ summary: bound(summary), ...(rationale === null ? {} : { rationale: bound(rationale) }) }];
286
+ });
287
+ const facts = boundList(stringsFrom(parsed.facts), HANDOFF_LIST_BOUNDS.facts, "facts", truncations).map(bound);
288
+ const files = boundList(recordsFrom(parsed.files), HANDOFF_LIST_BOUNDS.files, "files", truncations).flatMap(
289
+ (record) => {
290
+ const path = requiredField(record, "path");
291
+ const why = requiredField(record, "why");
292
+ return path === null || why === null ? [] : [{ path: bound(path), why: bound(why) }];
293
+ },
294
+ );
295
+ const commands = boundList(
296
+ recordsFrom(parsed.commands),
297
+ HANDOFF_LIST_BOUNDS.commands,
298
+ "commands",
299
+ truncations,
300
+ ).flatMap((record) => {
301
+ const argv = requiredField(record, "argv");
302
+ const why = requiredField(record, "why");
303
+ return argv === null || why === null ? [] : [{ argv: bound(argv), why: bound(why) }];
304
+ });
305
+ const openQuestions = boundList(
306
+ stringsFrom(parsed.openQuestions),
307
+ HANDOFF_LIST_BOUNDS.openQuestions,
308
+ "openQuestions",
309
+ truncations,
310
+ ).map(bound);
311
+
312
+ if (stringsCut > 0) {
313
+ truncations.push(
314
+ `${stringsCut} ${stringsCut === 1 ? "entry was" : "entries were"} cut to ${HANDOFF_MAX_STRING_BYTES} bytes and marked with ${HANDOFF_TRUNCATION_MARKER}`,
315
+ );
316
+ }
317
+
318
+ return { ok: true, result: { extraction: { decisions, facts, files, commands, openQuestions }, truncations } };
319
+ }
320
+
321
+ // ---------------------------------------------------------------------------
322
+ // Read ledger
323
+ // ---------------------------------------------------------------------------
324
+
325
+ /**
326
+ * Tool names whose call arguments name a path this session actually touched.
327
+ * `artifact` writes a file, so it counts as a write; `git` and `verify` are
328
+ * command runners whose path argument is a working directory rather than a
329
+ * file, so they do not.
330
+ */
331
+ const READ_LEDGER_TOOLS: ReadonlySet<string> = new Set(["read", "edit", "write", "ls", "find", "grep", "artifact"]);
332
+
333
+ export interface HandoffReadLedgerOptions {
334
+ /** Session working directory. Paths resolve against it and relativize back to it. */
335
+ cwd?: string | null;
336
+ /** Active branch leaf, so an abandoned `/tree` branch contributes nothing. */
337
+ leafTurnId?: string | null;
338
+ }
339
+
340
+ /**
341
+ * Canonical workspace-relative form. Lexical only: no realpath, no filesystem
342
+ * probe, no `process.cwd()` fallback. A path outside the workspace keeps its
343
+ * absolute form, which is what makes it fail the ledger check against paths
344
+ * recorded relative to the workspace root.
345
+ */
346
+ export function normalizeHandoffPath(value: string, cwd: string | null): string {
347
+ const trimmed = value.trim().replace(/[\\/]+$/u, "");
348
+ if (trimmed.length === 0) return "";
349
+ const absolute = isAbsolute(trimmed) ? normalize(trimmed) : cwd === null ? normalize(trimmed) : resolve(cwd, trimmed);
350
+ if (cwd === null) return absolute;
351
+ const rel = relative(cwd, absolute);
352
+ if (rel.length === 0) return ".";
353
+ return rel.startsWith("..") || isAbsolute(rel) ? absolute : rel;
354
+ }
355
+
356
+ function usableCwd(cwd: string | null | undefined): string | null {
357
+ if (typeof cwd !== "string") return null;
358
+ const trimmed = cwd.trim();
359
+ return trimmed.length > 0 && isAbsolute(trimmed) ? normalize(trimmed) : null;
360
+ }
361
+
362
+ function stringField(record: Record<string, unknown> | null, ...keys: string[]): string | null {
363
+ if (record === null) return null;
364
+ for (const key of keys) {
365
+ const value = record[key];
366
+ if (typeof value === "string" && value.trim().length > 0) return value.trim();
367
+ }
368
+ return null;
369
+ }
370
+
371
+ function pathFromCall(toolName: string, args: unknown, cwd: string | null, into: Set<string>): void {
372
+ if (!READ_LEDGER_TOOLS.has(toolName)) return;
373
+ const record = isRecord(args) ? args : null;
374
+ const named = stringField(record, "path", "file_path", "filePath");
375
+ // grep, find, and ls all default to the working directory, which is what
376
+ // they actually looked at.
377
+ const raw = named ?? (toolName === "grep" || toolName === "find" || toolName === "ls" ? "." : null);
378
+ if (raw === null) return;
379
+ const normalized = normalizeHandoffPath(raw, cwd);
380
+ if (normalized.length > 0) into.add(normalized);
381
+ }
382
+
383
+ /**
384
+ * Fold this session's persisted tool calls into the set of paths it touched.
385
+ *
386
+ * Entries pass through `filterEntriesToActivePath` first, the same way the task
387
+ * board folds its inputs, so a file read on a `/tree` branch the operator
388
+ * abandoned is not evidence that this session read it.
389
+ */
390
+ export function buildHandoffReadLedger(
391
+ entries: ReadonlyArray<SessionEntry>,
392
+ options: HandoffReadLedgerOptions = {},
393
+ ): ReadonlySet<string> {
394
+ const cwd = usableCwd(options.cwd);
395
+ const active = filterEntriesToActivePath(entries, options.leafTurnId ?? undefined);
396
+ const paths = new Set<string>();
397
+ for (const entry of active) {
398
+ if (entry.kind === "fileEntry") {
399
+ const normalized = normalizeHandoffPath(entry.path, cwd);
400
+ if (normalized.length > 0) paths.add(normalized);
401
+ continue;
402
+ }
403
+ if (entry.kind !== "message") continue;
404
+ const payload = isRecord(entry.payload) ? entry.payload : null;
405
+ if (payload === null) continue;
406
+ if (entry.role === "tool_call") {
407
+ const name = stringField(payload, "name", "toolName", "tool");
408
+ if (name !== null) pathFromCall(name, payload.args ?? payload.arguments ?? payload.input, cwd, paths);
409
+ continue;
410
+ }
411
+ if (entry.role !== "assistant" || !Array.isArray(payload.content)) continue;
412
+ for (const block of payload.content) {
413
+ if (!isRecord(block) || block.type !== "toolCall") continue;
414
+ const name = stringField(block, "name", "toolName");
415
+ if (name !== null) pathFromCall(name, block.arguments ?? block.args ?? block.input, cwd, paths);
416
+ }
417
+ }
418
+ return paths;
419
+ }
420
+
421
+ export interface HandoffPathVerdict {
422
+ kept: HandoffFile[];
423
+ dropped: HandoffFile[];
424
+ }
425
+
426
+ /**
427
+ * Split the model's file list on the read ledger. A path the session never
428
+ * touched is dropped rather than corrected, and the document lists it so the
429
+ * operator can see what the model invented.
430
+ */
431
+ export function validateHandoffFiles(
432
+ files: ReadonlyArray<HandoffFile>,
433
+ ledger: ReadonlySet<string>,
434
+ cwd: string | null = null,
435
+ ): HandoffPathVerdict {
436
+ const kept: HandoffFile[] = [];
437
+ const dropped: HandoffFile[] = [];
438
+ for (const file of files) {
439
+ const normalized = normalizeHandoffPath(file.path, usableCwd(cwd));
440
+ if (normalized.length > 0 && ledger.has(normalized)) kept.push({ ...file, path: normalized });
441
+ else dropped.push(file);
442
+ }
443
+ return { kept, dropped };
444
+ }
445
+
446
+ // ---------------------------------------------------------------------------
447
+ // Decisions
448
+ // ---------------------------------------------------------------------------
449
+
450
+ export interface MergedHandoffDecision extends HandoffDecision {
451
+ /** True for a decision the session's decision board already settled. */
452
+ settled: boolean;
453
+ }
454
+
455
+ function decisionMatchKey(text: string): string {
456
+ return text
457
+ .toLowerCase()
458
+ .replace(/[^a-z0-9]+/gu, " ")
459
+ .trim();
460
+ }
461
+
462
+ /**
463
+ * Merge the extracted decisions with the session's settled decision board.
464
+ *
465
+ * The board is the record of what an operator actually answered, so it wins on
466
+ * conflict and its entries carry a settled marker. An extracted decision
467
+ * conflicts when it restates a board decision's key or label, which is how a
468
+ * model paraphrasing an answer the operator already gave gets folded into the
469
+ * answer rather than listed beside it. Superseded board decisions are history
470
+ * and do not travel.
471
+ */
472
+ export function mergeHandoffDecisions(
473
+ extracted: ReadonlyArray<HandoffDecision>,
474
+ board: ReadonlyArray<DecisionLedgerEntry>,
475
+ ): MergedHandoffDecision[] {
476
+ const settled: MergedHandoffDecision[] = [];
477
+ const claimed: string[] = [];
478
+ for (const interview of board) {
479
+ for (const decision of interview.decisions) {
480
+ if (decision.status !== "active") continue;
481
+ const name = decision.label ?? decision.key;
482
+ const summary = boundHandoffString(`${name}: ${decision.value}`).text;
483
+ const rationale = decision.source_question ?? decision.correction;
484
+ settled.push({
485
+ summary,
486
+ ...(rationale ? { rationale: boundHandoffString(rationale).text } : {}),
487
+ settled: true,
488
+ });
489
+ claimed.push(decisionMatchKey(name), decisionMatchKey(decision.key));
490
+ }
491
+ }
492
+ const keys = claimed.filter((key) => key.length > 0);
493
+ const seen = new Set(settled.map((decision) => decisionMatchKey(decision.summary)));
494
+ const merged = [...settled];
495
+ for (const decision of extracted) {
496
+ const key = decisionMatchKey(decision.summary);
497
+ if (seen.has(key)) continue;
498
+ if (keys.some((claim) => key.includes(claim))) continue;
499
+ seen.add(key);
500
+ merged.push({ ...decision, settled: false });
501
+ }
502
+ return merged;
503
+ }
504
+
505
+ // ---------------------------------------------------------------------------
506
+ // Document
507
+ // ---------------------------------------------------------------------------
508
+
509
+ /** Exact heading the dropped paths appear under. Asserted by contract. */
510
+ export const HANDOFF_DROPPED_HEADING = "dropped (not in this session's read ledger)";
511
+
512
+ export interface HandoffDocumentInput {
513
+ goal: string;
514
+ fromSessionId: string;
515
+ decisions: ReadonlyArray<MergedHandoffDecision>;
516
+ facts: ReadonlyArray<string>;
517
+ files: ReadonlyArray<HandoffFile>;
518
+ droppedFiles: ReadonlyArray<HandoffFile>;
519
+ commands: ReadonlyArray<HandoffCommand>;
520
+ openQuestions: ReadonlyArray<string>;
521
+ truncations: ReadonlyArray<string>;
522
+ }
523
+
524
+ function section(lines: string[], title: string, body: ReadonlyArray<string>): void {
525
+ if (body.length === 0) return;
526
+ lines.push(`## ${title}`, "");
527
+ for (const line of body) lines.push(line);
528
+ lines.push("");
529
+ }
530
+
531
+ /** Render the reviewable Markdown document. This is what the operator edits. */
532
+ export function renderHandoffDocument(input: HandoffDocumentInput): string {
533
+ const lines: string[] = [];
534
+ lines.push("# Handoff", "");
535
+ lines.push(`**Goal:** ${input.goal}`, "");
536
+ lines.push(`Carried from session \`${input.fromSessionId}\`.`, "");
537
+
538
+ section(
539
+ lines,
540
+ "Decisions",
541
+ input.decisions.map((decision) => {
542
+ const marker = decision.settled ? " _(settled)_" : "";
543
+ const rationale = decision.rationale ? `\n ${decision.rationale}` : "";
544
+ return `- ${decision.summary}${marker}${rationale}`;
545
+ }),
546
+ );
547
+ section(
548
+ lines,
549
+ "Facts",
550
+ input.facts.map((fact) => `- ${fact}`),
551
+ );
552
+ section(
553
+ lines,
554
+ "Files",
555
+ input.files.map((file) => `- \`${file.path}\`: ${file.why}`),
556
+ );
557
+ if (input.droppedFiles.length > 0) {
558
+ lines.push(`### ${HANDOFF_DROPPED_HEADING}`, "");
559
+ for (const file of input.droppedFiles) lines.push(`- \`${file.path}\`: ${file.why}`);
560
+ lines.push("");
561
+ }
562
+ section(
563
+ lines,
564
+ "Commands",
565
+ input.commands.map((command) => `- \`${command.argv}\`: ${command.why}`),
566
+ );
567
+ section(
568
+ lines,
569
+ "Open questions",
570
+ input.openQuestions.map((question) => `- ${question}`),
571
+ );
572
+ section(
573
+ lines,
574
+ "Bounds applied",
575
+ input.truncations.map((note) => `- ${note}`),
576
+ );
577
+
578
+ return `${lines.join("\n").trimEnd()}\n`;
579
+ }
580
+
581
+ // ---------------------------------------------------------------------------
582
+ // Ledger entries
583
+ // ---------------------------------------------------------------------------
584
+
585
+ /** `custom.customType` of the seed entry the new session opens with. */
586
+ export const HANDOFF_SEED_CUSTOM_TYPE = "handoffSeed";
587
+ /** `custom.customType` of the terminal note the old session closes with. */
588
+ export const HANDOFF_NOTE_CUSTOM_TYPE = "handoffNote";
589
+
590
+ /**
591
+ * How the seed entry introduces itself to the model. It is labelled as carried
592
+ * data from a named session, so the model reads a handoff document rather than
593
+ * an instruction the operator never typed.
594
+ */
595
+ export const HANDOFF_SEED_PREFIX = "Handoff document carried from session";
596
+
597
+ export interface HandoffSeedData {
598
+ fromSessionId: string;
599
+ goal: string;
600
+ document: string;
601
+ }
602
+
603
+ export interface HandoffNoteData {
604
+ toSessionId: string;
605
+ goal: string;
606
+ }
607
+
608
+ /** The model-facing text of a seed entry. Data, labelled by origin, never a user turn. */
609
+ export function handoffSeedContextText(data: HandoffSeedData): string {
610
+ return [
611
+ `${HANDOFF_SEED_PREFIX} ${data.fromSessionId}.`,
612
+ "It is reference material the operator reviewed, not a message they wrote.",
613
+ "",
614
+ data.document.trim(),
615
+ ].join("\n");
616
+ }
617
+
618
+ export function isHandoffSeedData(value: unknown): value is HandoffSeedData {
619
+ return (
620
+ isRecord(value) &&
621
+ typeof value.fromSessionId === "string" &&
622
+ typeof value.goal === "string" &&
623
+ typeof value.document === "string"
624
+ );
625
+ }
626
+
627
+ export function isHandoffNoteData(value: unknown): value is HandoffNoteData {
628
+ return isRecord(value) && typeof value.toSessionId === "string" && typeof value.goal === "string";
629
+ }