@iowarp/clio-coder 0.3.8 → 0.3.9

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 (333) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +7 -3
  3. package/dist/{acp-U67UHUK2.js → acp-7LOELQFP.js} +6 -6
  4. package/dist/{agents-YU6SGALZ.js → agents-FIBG2SHA.js} +27 -25
  5. package/dist/assets/codewiki.json +1 -1
  6. package/dist/{auth-5ZPJOIVG.js → auth-OI4LIH2I.js} +11 -12
  7. package/dist/{builtins-C6JMZVV6.js → builtins-AD25UL3C.js} +5 -5
  8. package/dist/{chunk-5FR74PWO.js → chunk-2JDWVJND.js} +2 -2
  9. package/dist/chunk-3DPEIQKN.js +113 -0
  10. package/dist/{chunk-4SPRNWDE.js → chunk-3DUR4WUA.js} +15 -15
  11. package/dist/{chunk-VHN4MY6O.js → chunk-3MRC2YSQ.js} +2 -2
  12. package/dist/{chunk-5DHKRSMQ.js → chunk-3UUY7R3Z.js} +11 -7
  13. package/dist/{chunk-IGWKHNIQ.js → chunk-3V5AYSEQ.js} +8 -8
  14. package/dist/{chunk-FHJEP5SW.js → chunk-465CC7FK.js} +8 -5
  15. package/dist/{chunk-5WIGXA4T.js → chunk-47CMYGET.js} +111 -4
  16. package/dist/{chunk-A3WNZD3P.js → chunk-4H6ULJ3H.js} +67 -21
  17. package/dist/{chunk-TB5666IT.js → chunk-4LJX2PUC.js} +3 -3
  18. package/dist/{chunk-XWSF374K.js → chunk-56KB5IJP.js} +2 -2
  19. package/dist/{chunk-SPULKLCF.js → chunk-5DQRIYDZ.js} +2 -2
  20. package/dist/{chunk-2HEJ2F35.js → chunk-5HFBWUMU.js} +20 -8
  21. package/dist/{chunk-WNIJTQQK.js → chunk-5PVQ4SRS.js} +78 -6
  22. package/dist/{chunk-DYIM5TJT.js → chunk-5QKCQQ3E.js} +262 -6
  23. package/dist/{chunk-TYPGUK6W.js → chunk-5T7RBWN2.js} +111 -5
  24. package/dist/{chunk-IIZWH4XA.js → chunk-774ILSRL.js} +2 -2
  25. package/dist/chunk-7C6RYZGQ.js +391 -0
  26. package/dist/{chunk-TANS5ZJS.js → chunk-AD7Y7STJ.js} +3 -3
  27. package/dist/{chunk-RWSI4YD7.js → chunk-AEYBF3TB.js} +33 -12
  28. package/dist/{chunk-DGSYXYMX.js → chunk-AMKHQW3C.js} +2 -2
  29. package/dist/{chunk-VWZOAB7K.js → chunk-B5XRQOLB.js} +7 -7
  30. package/dist/{chunk-WXY7KU3G.js → chunk-BVDVID7E.js} +2 -2
  31. package/dist/{chunk-PMDBGQSJ.js → chunk-CA42X6KT.js} +2 -2
  32. package/dist/{chunk-HLE42MG7.js → chunk-D73KXYPF.js} +3 -3
  33. package/dist/{chunk-5Q2VVUKB.js → chunk-DG4M6ZUE.js} +3 -3
  34. package/dist/{chunk-ME6CCNFO.js → chunk-EBOC7MT3.js} +6 -6
  35. package/dist/{chunk-MXKJU4JB.js → chunk-ECUO3KDP.js} +48 -7
  36. package/dist/{chunk-26LEYJZH.js → chunk-FALJGAWU.js} +2 -2
  37. package/dist/{chunk-GWS3VEIW.js → chunk-FWDFM5ZU.js} +24 -3
  38. package/dist/{chunk-7RGZWPB6.js → chunk-GAYUJ7LE.js} +67 -13
  39. package/dist/{chunk-VCBR6CU7.js → chunk-HAY4ZE2P.js} +2 -2
  40. package/dist/{chunk-FBVTI2TJ.js → chunk-HCBCAYZU.js} +11 -130
  41. package/dist/{chunk-WSB3FPX7.js → chunk-HJB5IUKP.js} +32 -136
  42. package/dist/{chunk-J3YUBZWY.js → chunk-HKO36JWF.js} +33 -5
  43. package/dist/{chunk-E77JEWSD.js → chunk-HPCTNZM2.js} +6 -36
  44. package/dist/{chunk-FJ3H4MN5.js → chunk-IHKBWSXF.js} +2 -2
  45. package/dist/{chunk-NMPKI6XL.js → chunk-JEQQR47K.js} +37 -18
  46. package/dist/{chunk-3BINW3FP.js → chunk-KV2AOLDF.js} +24 -4
  47. package/dist/{chunk-YS5VLNH5.js → chunk-LXPJXFM5.js} +7 -7
  48. package/dist/{chunk-7RFXX52T.js → chunk-MIX5N5AC.js} +271 -41
  49. package/dist/{chunk-K4XHGFR5.js → chunk-MLOK6ZOS.js} +1297 -218
  50. package/dist/{chunk-ZNLWCMVZ.js → chunk-MV2VUEJC.js} +2 -2
  51. package/dist/{chunk-MVVUPGPW.js → chunk-MXHC5QYU.js} +6 -6
  52. package/dist/{chunk-5H3GB5BO.js → chunk-N3PBVRTZ.js} +4 -382
  53. package/dist/{chunk-2HFZQUHL.js → chunk-N5XKWMDW.js} +17 -7
  54. package/dist/{chunk-5C3AQNDW.js → chunk-NNNWO6F2.js} +124 -36
  55. package/dist/{chunk-GU2UIAFZ.js → chunk-NQ6UCCOD.js} +3 -3
  56. package/dist/{chunk-N22QMJKY.js → chunk-NZU6YDNV.js} +4 -4
  57. package/dist/{chunk-ZVJ5BLO2.js → chunk-O6I4CIEU.js} +151 -13
  58. package/dist/{chunk-XN3L4EYL.js → chunk-OEDBCISO.js} +2 -2
  59. package/dist/{chunk-VAWNZU7Z.js → chunk-P3JGPQFL.js} +2 -2
  60. package/dist/{chunk-IJ7RPIYJ.js → chunk-PNY46YEY.js} +20 -3
  61. package/dist/{chunk-U6MBIEMB.js → chunk-PZ4I4JE2.js} +56 -35
  62. package/dist/{chunk-GPIEI3LY.js → chunk-QQ7EKM72.js} +2 -2
  63. package/dist/{chunk-WLFILSD5.js → chunk-R7LNVMCS.js} +66 -28
  64. package/dist/{chunk-GOXNB3AO.js → chunk-RAPCMZL4.js} +75 -4
  65. package/dist/chunk-RKKLTLYB.js +45 -0
  66. package/dist/{chunk-TT36MB5S.js → chunk-RKRLDWD3.js} +3 -1
  67. package/dist/{chunk-TTHACPOM.js → chunk-S4COXYBG.js} +456 -18
  68. package/dist/{chunk-WWCZ5F23.js → chunk-T3Z6VAAF.js} +69 -10
  69. package/dist/{chunk-GN57SG4G.js → chunk-TD7UE2L5.js} +9 -7
  70. package/dist/{chunk-TLQJPP24.js → chunk-TEO2TLVN.js} +523 -322
  71. package/dist/{chunk-FCSXB6T2.js → chunk-UOSL25KY.js} +14 -2
  72. package/dist/{chunk-KTYTFRMB.js → chunk-VKBMFOYV.js} +17 -15
  73. package/dist/{chunk-PT7HYKEM.js → chunk-VO2LKSTM.js} +2 -2
  74. package/dist/{chunk-P43ETTHK.js → chunk-VPTUJU4P.js} +2 -2
  75. package/dist/{chunk-WEH5XRJQ.js → chunk-WIE7ZOSW.js} +2 -2
  76. package/dist/{chunk-EMYUUSFG.js → chunk-WXCJ7VME.js} +5 -5
  77. package/dist/{chunk-4DGYLA73.js → chunk-XDOQXGFO.js} +22 -7
  78. package/dist/{chunk-JOZYP4GM.js → chunk-YKOFT37S.js} +5 -5
  79. package/dist/chunk-YSEHGPCT.js +127 -0
  80. package/dist/{chunk-HFSBBKSQ.js → chunk-YW7UVM5V.js} +138 -3
  81. package/dist/cli/index.js +27 -27
  82. package/dist/{clio-QVTYJ57A.js → clio-LT5V7SSZ.js} +6 -6
  83. package/dist/{code-nav-FGGFIE7L.js → code-nav-LMW275PA.js} +4 -4
  84. package/dist/{config-LW5IJFQN.js → config-RXS5T3JT.js} +71 -43
  85. package/dist/{configure-7XIZCOU4.js → configure-2WYWSCSD.js} +14 -15
  86. package/dist/{context-Y6Y7QPR6.js → context-I3BTOTCS.js} +12 -12
  87. package/dist/{context-L3WL3X7K.js → context-MVOORGMF.js} +34 -33
  88. package/dist/{context-N52ZA626.js → context-PALKKQYL.js} +20 -20
  89. package/dist/{context-clear-MBQRLSDQ.js → context-clear-N2WOYZ2K.js} +34 -33
  90. package/dist/{context-index-HVMFQHK3.js → context-index-HNG3MOME.js} +2 -2
  91. package/dist/{context-working-set-GS6DSO7F.js → context-working-set-MIEVECVZ.js} +10 -11
  92. package/dist/{dispatch-runner-22ZCNOM3.js → dispatch-runner-VVA4SRRH.js} +34 -33
  93. package/dist/doctor-TWBWFK5V.js +165 -0
  94. package/dist/{eval-BEC2WHDA.js → eval-IJ5VEZDJ.js} +2016 -142
  95. package/dist/{evidence-REJUMSKM.js → evidence-L5APPXNV.js} +29 -28
  96. package/dist/{evolve-PY5ZBA5K.js → evolve-RGNKFJ52.js} +29 -28
  97. package/dist/{extensions-HVKU65YU.js → extensions-7WYWUX5A.js} +9 -3
  98. package/dist/{fleet-7WZEWRFA.js → fleet-6CNVBZZP.js} +87 -55
  99. package/dist/{fleet-commands-UVHWM76J.js → fleet-commands-L2SXSYEI.js} +6 -6
  100. package/dist/{fleet-graph-6ULH7PES.js → fleet-graph-2J3OOIPO.js} +14 -12
  101. package/dist/{fleet-preflight-J53T6CCE.js → fleet-preflight-CZRJ4JP5.js} +3 -4
  102. package/dist/{fleet-validate-72PC4SLA.js → fleet-validate-C5RI6DP7.js} +16 -15
  103. package/dist/{init-OG3TPGQG.js → init-VBN2ACVA.js} +50 -48
  104. package/dist/{library-CNTMPLRF.js → library-JHGUMLY2.js} +13 -11
  105. package/dist/{memory-6IS7F275.js → memory-K4OQIYWG.js} +31 -30
  106. package/dist/{models-ENRJDA5W.js → models-2NCZUWDD.js} +23 -22
  107. package/dist/{monitor-XLDVO7TN.js → monitor-MMVTJABD.js} +35 -34
  108. package/dist/{orchestrator-6KSPYRHA.js → orchestrator-ZKBPCHW6.js} +1627 -294
  109. package/dist/{reset-RZ4ER727.js → reset-DD5JGOY3.js} +3 -3
  110. package/dist/{run-Y2CNK5RU.js → run-QEGNX7FL.js} +56 -55
  111. package/dist/{share-A55GYP6Z.js → share-JKD3BQMW.js} +13 -11
  112. package/dist/{skills-ALC5J6AT.js → skills-LMQIKDOZ.js} +14 -12
  113. package/dist/{skills-eval-JPBEBYQU.js → skills-eval-I7X2774U.js} +34 -32
  114. package/dist/{steer-GGWFUJUD.js → steer-CF5TDANS.js} +3 -3
  115. package/dist/{support-MIETYA5E.js → support-I7LOJLIF.js} +4 -4
  116. package/dist/{targets-VGNXIR3S.js → targets-RUSR6B5Z.js} +58 -32
  117. package/dist/{terminal-lease-WOBR64YA.js → terminal-lease-QYVORFR4.js} +6 -4
  118. package/dist/{trace-PNCASAXC.js → trace-ODOQIVIW.js} +61 -6
  119. package/dist/{upgrade-FUSUAGHR.js → upgrade-XANW3FXB.js} +17 -16
  120. package/dist/{usage-N4MKVHKD.js → usage-4H7ZRXQT.js} +88 -44
  121. package/dist/{verifiers-YAWOJ3H2.js → verifiers-UZXNBZEB.js} +6 -6
  122. package/dist/{verify-LTDHYBGY.js → verify-BVKWTNDL.js} +5 -5
  123. package/dist/{wiki-generate-6M7GHTBJ.js → wiki-generate-MY7WV2QI.js} +49 -47
  124. package/dist/worker/entry.js +29 -33
  125. package/docs/alcf-provider.md +1 -1
  126. package/docs/architecture.md +1 -1
  127. package/docs/artifact-versions.md +6 -1
  128. package/docs/built-in-agents.md +1 -1
  129. package/docs/capacity-and-scheduling.md +23 -2
  130. package/docs/commands-and-modes.md +1 -1
  131. package/docs/configuration-and-targets.md +30 -3
  132. package/docs/context-engine.md +63 -4
  133. package/docs/documentation-coverage.md +3 -3
  134. package/docs/documentation-guide.md +1 -1
  135. package/docs/environment-variables.md +2 -0
  136. package/docs/eval-runner.md +262 -11
  137. package/docs/evals-internal.md +72 -2
  138. package/docs/evidence-and-memory.md +11 -10
  139. package/docs/evolution.md +1 -1
  140. package/docs/extensions-and-sharing.md +3 -1
  141. package/docs/fleet-dispatch.md +4 -4
  142. package/docs/installation-and-lifecycle.md +1 -1
  143. package/docs/middleware-and-components.md +1 -1
  144. package/docs/model-catalog.md +1 -1
  145. package/docs/observability.md +53 -2
  146. package/docs/proactive-memory.md +127 -14
  147. package/docs/prompt-envelope-and-tools.md +19 -1
  148. package/docs/provider-adapter-cookbook.md +1 -1
  149. package/docs/release-cut-checklist.md +19 -3
  150. package/docs/safety-model.md +1 -1
  151. package/docs/scientific-validation.md +1 -1
  152. package/docs/skills-marketplace.md +1 -1
  153. package/docs/tool-usage.md +1 -1
  154. package/docs/trace-store.md +1 -1
  155. package/docs/troubleshooting.md +87 -0
  156. package/docs/tui-design.md +1 -1
  157. package/docs/worker-dispatch-mechanics.md +1 -1
  158. package/package.json +2 -1
  159. package/src/cli/agents.ts +1 -1
  160. package/src/cli/config-inspect.ts +33 -6
  161. package/src/cli/config.ts +1 -1
  162. package/src/cli/doctor-state-size.ts +82 -0
  163. package/src/cli/doctor.ts +3 -1
  164. package/src/cli/eval.ts +80 -16
  165. package/src/cli/extensions.ts +5 -1
  166. package/src/cli/fleet.ts +32 -3
  167. package/src/cli/targets.ts +44 -13
  168. package/src/cli/trace.ts +63 -4
  169. package/src/cli/usage.ts +63 -14
  170. package/src/core/bus-events.ts +29 -1
  171. package/src/core/cache-telemetry.ts +42 -0
  172. package/src/core/config.ts +18 -0
  173. package/src/core/defaults.ts +36 -6
  174. package/src/core/endpoint-key.ts +27 -0
  175. package/src/core/residency-target-key.ts +25 -0
  176. package/src/core/response-schema.ts +36 -2
  177. package/src/domains/config/classify.ts +3 -0
  178. package/src/domains/context/codewiki/coordinator.ts +12 -4
  179. package/src/domains/dispatch/admission.ts +40 -3
  180. package/src/domains/dispatch/capacity-lease.ts +98 -9
  181. package/src/domains/dispatch/contract.ts +11 -0
  182. package/src/domains/dispatch/execution-plan.ts +44 -4
  183. package/src/domains/dispatch/extension.ts +166 -42
  184. package/src/domains/dispatch/fleet-run.ts +23 -3
  185. package/src/domains/dispatch/heartbeat.ts +32 -8
  186. package/src/domains/dispatch/index.ts +3 -0
  187. package/src/domains/dispatch/orphan-recovery.ts +5 -0
  188. package/src/domains/dispatch/reservation-store.ts +116 -8
  189. package/src/domains/dispatch/state.ts +4 -0
  190. package/src/domains/dispatch/worker-spawn.ts +25 -11
  191. package/src/domains/dispatch/write-boundary-enforcer.ts +20 -3
  192. package/src/domains/dispatch/write-boundary.ts +62 -1
  193. package/src/domains/eval/artifacts/store.ts +62 -0
  194. package/src/domains/eval/compare/behavioral.ts +224 -0
  195. package/src/domains/eval/compare/compare.ts +355 -2
  196. package/src/domains/eval/compare/envelope.ts +128 -0
  197. package/src/domains/eval/compare/gates.ts +24 -6
  198. package/src/domains/eval/compare/thresholds.ts +30 -3
  199. package/src/domains/eval/execution-provenance.ts +240 -0
  200. package/src/domains/eval/metrics/aggregate.ts +136 -0
  201. package/src/domains/eval/metrics/call-ledger-stream.ts +112 -0
  202. package/src/domains/eval/metrics/tracked.ts +413 -0
  203. package/src/domains/eval/provenance.ts +117 -0
  204. package/src/domains/eval/reports/comparison.ts +128 -0
  205. package/src/domains/eval/reports/junit.ts +17 -3
  206. package/src/domains/eval/reports/markdown.ts +3 -3
  207. package/src/domains/eval/reports/text.ts +14 -0
  208. package/src/domains/eval/run-compare.ts +20 -0
  209. package/src/domains/eval/runners/clio-run.ts +127 -0
  210. package/src/domains/eval/runners/external-command.ts +28 -3
  211. package/src/domains/eval/schema/adapter.ts +111 -0
  212. package/src/domains/eval/schema/artifact.ts +20 -0
  213. package/src/domains/eval/schema/behavioral-metrics.ts +204 -0
  214. package/src/domains/eval/schema/behavioral.ts +520 -0
  215. package/src/domains/eval/schema/execution-envelope.ts +194 -0
  216. package/src/domains/eval/schema/serving.ts +74 -0
  217. package/src/domains/eval/schema/suite.ts +38 -8
  218. package/src/domains/eval/schema/validate.ts +58 -3
  219. package/src/domains/eval/schema/verdict.ts +237 -0
  220. package/src/domains/eval/suites/resolve.ts +2 -0
  221. package/src/domains/eval/suites/run.ts +264 -33
  222. package/src/domains/eval/verifiers/command.ts +2 -1
  223. package/src/domains/eval/workspaces/temp-copy.ts +145 -13
  224. package/src/domains/evidence/build.ts +2 -13
  225. package/src/domains/evidence/eval.ts +2 -12
  226. package/src/domains/evidence/findings-markdown.ts +33 -0
  227. package/src/domains/evidence/run-trust.ts +7 -113
  228. package/src/domains/evidence/trust-projection.ts +2 -2
  229. package/src/domains/extensions/compatibility.ts +285 -0
  230. package/src/domains/extensions/discovery.ts +38 -3
  231. package/src/domains/extensions/resources.ts +1 -1
  232. package/src/domains/extensions/state.ts +12 -3
  233. package/src/domains/extensions/types.ts +2 -0
  234. package/src/domains/lifecycle/doctor.ts +69 -1
  235. package/src/domains/memory/index.ts +14 -0
  236. package/src/domains/memory/task-bank-promotion.ts +64 -0
  237. package/src/domains/memory/task-memory-policy.ts +77 -8
  238. package/src/domains/memory/task-memory-spend.ts +131 -0
  239. package/src/domains/memory/task-memory-status.ts +7 -0
  240. package/src/domains/memory/task-memory-telemetry.ts +2 -0
  241. package/src/domains/middleware/index.ts +1 -0
  242. package/src/domains/middleware/memory-intervention.ts +69 -5
  243. package/src/domains/middleware/memory-step-endpoint.ts +71 -0
  244. package/src/domains/observability/background-memory-usage.ts +140 -0
  245. package/src/domains/observability/cost.ts +1 -1
  246. package/src/domains/observability/index.ts +7 -0
  247. package/src/domains/observability/out-of-turn-usage.ts +51 -2
  248. package/src/domains/observability/trace-store.ts +192 -2
  249. package/src/domains/prompts/compiler.ts +100 -13
  250. package/src/domains/providers/endpoint-capacity.ts +96 -0
  251. package/src/domains/providers/index.ts +10 -0
  252. package/src/domains/providers/models/local-models/clio-local-coding-targets.yaml +243 -1
  253. package/src/domains/providers/runtime-resolution.ts +8 -1
  254. package/src/domains/providers/runtimes/common/probe-helpers.ts +31 -9
  255. package/src/domains/providers/runtimes/local-native/llamacpp-anthropic.ts +1 -1
  256. package/src/domains/providers/runtimes/local-native/llamacpp-completion.ts +1 -1
  257. package/src/domains/providers/runtimes/local-native/llamacpp-embed.ts +1 -1
  258. package/src/domains/providers/runtimes/local-native/llamacpp-rerank.ts +1 -1
  259. package/src/domains/providers/runtimes/local-native/llamacpp.ts +4 -1
  260. package/src/domains/providers/runtimes/local-native/lmstudio.ts +4 -1
  261. package/src/domains/providers/runtimes/local-native/ollama-native.ts +6 -1
  262. package/src/domains/providers/types/capability-flags.ts +2 -0
  263. package/src/domains/providers/types/target-descriptor.ts +2 -0
  264. package/src/domains/resources/prompts/loader.ts +95 -33
  265. package/src/domains/safety/call-target.ts +52 -0
  266. package/src/domains/safety/run-effects.ts +35 -4
  267. package/src/domains/session/context-accounting.ts +52 -1
  268. package/src/domains/session/context-ledger.ts +37 -13
  269. package/src/domains/session/index.ts +6 -0
  270. package/src/domains/session/prompt-cache.ts +140 -0
  271. package/src/domains/session/prompt-manifest.ts +42 -0
  272. package/src/engine/acp/adapter.ts +18 -3
  273. package/src/engine/ai.ts +35 -0
  274. package/src/engine/apis/llamacpp-residency.ts +55 -3
  275. package/src/engine/apis/lmstudio.ts +25 -5
  276. package/src/engine/apis/ollama-native.ts +2 -1
  277. package/src/engine/apis/openai-completions.ts +80 -17
  278. package/src/engine/apis/residency-lock.ts +3 -1
  279. package/src/engine/apis/residency.ts +34 -1
  280. package/src/engine/provider-payload.ts +29 -1
  281. package/src/entry/orchestrator.ts +176 -30
  282. package/src/interactive/chat-loop-messages.ts +26 -7
  283. package/src/interactive/chat-loop.ts +318 -41
  284. package/src/interactive/chat-panel.ts +62 -8
  285. package/src/interactive/clio-editor.ts +45 -8
  286. package/src/interactive/context-activity.ts +5 -1
  287. package/src/interactive/context-meter.ts +1 -1
  288. package/src/interactive/context-overlay.ts +40 -10
  289. package/src/interactive/cost-overlay.ts +64 -6
  290. package/src/interactive/dispatch-board.ts +84 -12
  291. package/src/interactive/fleet-run-preview.ts +41 -15
  292. package/src/interactive/handoff-round.ts +41 -2
  293. package/src/interactive/interactive-application.ts +24 -1
  294. package/src/interactive/interactive-input-runtime.ts +8 -0
  295. package/src/interactive/interactive-presentation.ts +4 -0
  296. package/src/interactive/interactive-shell.ts +20 -17
  297. package/src/interactive/interactive-slash-runtime.ts +27 -4
  298. package/src/interactive/memory-overlay.ts +8 -0
  299. package/src/interactive/mutation-preview.ts +295 -0
  300. package/src/interactive/overlay-general-openers.ts +16 -0
  301. package/src/interactive/overlay-key-routing.ts +38 -0
  302. package/src/interactive/overlay-lifecycle.ts +38 -5
  303. package/src/interactive/overlay-permission-lifecycle.ts +22 -2
  304. package/src/interactive/overlay-session-lifecycle.ts +73 -9
  305. package/src/interactive/overlays/ask-user.ts +91 -19
  306. package/src/interactive/overlays/help-reference.ts +4 -0
  307. package/src/interactive/overlays/prompts.ts +11 -1
  308. package/src/interactive/overlays/settings.ts +35 -1
  309. package/src/interactive/permission-hint.ts +34 -2
  310. package/src/interactive/permission-overlay.ts +159 -9
  311. package/src/interactive/prewarm.ts +197 -0
  312. package/src/interactive/render-trace.ts +162 -15
  313. package/src/interactive/renderers/tool-execution.ts +4 -0
  314. package/src/interactive/side-question.ts +58 -1
  315. package/src/interactive/status/controller.ts +11 -0
  316. package/src/interactive/status/state-machine.ts +54 -2
  317. package/src/interactive/status/types.ts +7 -0
  318. package/src/interactive/terminal-lease.ts +2 -0
  319. package/src/interactive/turn-context.ts +299 -31
  320. package/src/interactive/turn-persistence.ts +14 -4
  321. package/src/interactive/turn-prewarm.ts +364 -0
  322. package/src/interactive/turn-queues.ts +7 -4
  323. package/src/interactive/turn-runtime.ts +8 -1
  324. package/src/interactive/turn-state.ts +23 -0
  325. package/src/interactive/view/view-overlay.ts +28 -3
  326. package/src/tools/ask-user.ts +43 -2
  327. package/src/tools/dispatch-plan.ts +17 -9
  328. package/src/tools/dispatch-scout.ts +1 -1
  329. package/src/tools/registry.ts +16 -0
  330. package/dist/chunk-AOCYTWAV.js +0 -449
  331. package/dist/chunk-HWUFFB6L.js +0 -83
  332. package/dist/chunk-R346GLFC.js +0 -31
  333. package/dist/doctor-M7YEDGAE.js +0 -91
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Local inspection of the exact mutation a parked `write` or `edit` would make.
3
+ *
4
+ * The approval card describes an unlisted argument by type and size, so a
5
+ * parked write read `content=<string 482 bytes>` and a parked edit read
6
+ * `edits=<array 3 items>`: the operator authorized bytes they had never seen,
7
+ * and the only way to read them was the external session ledger (issue #254).
8
+ *
9
+ * Two rules shape everything here. The preview is derived from the same
10
+ * arguments object the decision resumes, and it carries a digest of those
11
+ * arguments so the bytes on screen are verifiably the bytes being approved.
12
+ * And the mutation text is process-local: it is built lazily when the operator
13
+ * asks for it, it is handed only to the overlay body, and it is never placed on
14
+ * {@link import("./permission-overlay.js").ApprovalRequestView}, which is what
15
+ * crosses into the transcript, the notices, and the worker seam.
16
+ */
17
+
18
+ import { createHash } from "node:crypto";
19
+ import { readFileSync } from "node:fs";
20
+ import { ToolNames } from "../core/tool-names.js";
21
+ import { sanitizeCallTargetText, sanitizeMultilineDisplayText } from "../domains/safety/call-target.js";
22
+ import {
23
+ applyEditsToNormalizedContent,
24
+ type Edit,
25
+ generateDiffString,
26
+ normalizeToLF,
27
+ stripBom,
28
+ } from "../tools/edit-diff.js";
29
+ import { resolveToCwd } from "../tools/path-utils.js";
30
+
31
+ export { MUTATION_PREVIEW_KEY } from "./permission-hint.js";
32
+
33
+ /** Rows of mutation shown at once. Anything longer scrolls, as `/handoff` review does. */
34
+ export const MUTATION_PREVIEW_VISIBLE_ROWS = 16;
35
+
36
+ /**
37
+ * Characters of proposed content the preview renders. The diff builder has its
38
+ * own byte backstop; this bounds the write path, whose payload is a whole file.
39
+ * Reaching it prints an explicit line naming what was withheld.
40
+ */
41
+ export const MUTATION_PREVIEW_MAX_CHARS = 262_144;
42
+
43
+ /**
44
+ * Facts about a parked mutation that may cross every process boundary: what
45
+ * kind it is, how big it is, and the digest that binds a preview to it. No part
46
+ * of the mutation text appears here, which is why this is the shape the
47
+ * approval view, the transcript row, and any notice may carry.
48
+ */
49
+ export interface MutationFacts {
50
+ kind: "write" | "edit";
51
+ /** Bytes of proposed content (write), or of the replacement text across every edit. */
52
+ bytes: number;
53
+ /** Replacements in the edit list. One for a write. */
54
+ replacements: number;
55
+ /** Truncated SHA-256 over the exact call arguments this decision applies to. */
56
+ digest: string;
57
+ }
58
+
59
+ export interface MutationPreview {
60
+ facts: MutationFacts;
61
+ /** What the body is, named honestly on the overlay's first line. */
62
+ heading: string;
63
+ /** The complete rendered mutation, already sanitized and one entry per display row. */
64
+ body: ReadonlyArray<string>;
65
+ /** A control byte or escape sequence in the payload was neutralized for display. */
66
+ neutralized: boolean;
67
+ /** A tab was expanded, so columns are display columns. */
68
+ tabsExpanded: boolean;
69
+ }
70
+
71
+ /**
72
+ * Build the complete preview for the parked call, or explain why it cannot be
73
+ * built. Never throws: an unreadable file or an edit list that will not apply
74
+ * is itself something the operator needs to read before deciding.
75
+ */
76
+ export type MutationInspector = () => MutationPreview;
77
+
78
+ function canonicalJson(value: unknown): string {
79
+ if (value === null || value === undefined) return "null";
80
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
81
+ if (typeof value === "object") {
82
+ const entries = Object.entries(value as Record<string, unknown>)
83
+ .filter(([, entry]) => entry !== undefined)
84
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
85
+ return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
86
+ }
87
+ if (typeof value === "string") return JSON.stringify(value);
88
+ if (typeof value === "number") return Number.isFinite(value) ? JSON.stringify(value) : "null";
89
+ if (typeof value === "boolean") return value ? "true" : "false";
90
+ return "null";
91
+ }
92
+
93
+ /**
94
+ * The identity a decision is bound to. Key order and absent optional fields do
95
+ * not change it, so the same call digests the same way wherever it is read;
96
+ * anything else about the arguments does, which is the point: a call that
97
+ * differs from the previewed one cannot inherit its preview.
98
+ */
99
+ export function callArgumentsDigest(args: Record<string, unknown> | undefined): string {
100
+ return createHash("sha256")
101
+ .update(canonicalJson(args ?? {}), "utf8")
102
+ .digest("hex")
103
+ .slice(0, 16);
104
+ }
105
+
106
+ function readEdits(value: unknown): Edit[] | null {
107
+ if (!Array.isArray(value)) return null;
108
+ const edits: Edit[] = [];
109
+ for (const entry of value) {
110
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
111
+ const record = entry as Record<string, unknown>;
112
+ if (typeof record.oldText !== "string" || typeof record.newText !== "string") return null;
113
+ edits.push({ oldText: record.oldText, newText: record.newText });
114
+ }
115
+ return edits;
116
+ }
117
+
118
+ /**
119
+ * The size-and-digest facts for a parked call, or null when the tool is not a
120
+ * file mutation or its arguments are not the shape this can describe. The card
121
+ * keeps rendering its allowlisted target either way.
122
+ */
123
+ export function mutationFacts(tool: string, args: Record<string, unknown> | undefined): MutationFacts | null {
124
+ if (!args || typeof args.path !== "string" || args.path.length === 0) return null;
125
+ if (tool === ToolNames.Write) {
126
+ if (typeof args.content !== "string") return null;
127
+ return {
128
+ kind: "write",
129
+ bytes: Buffer.byteLength(args.content, "utf8"),
130
+ replacements: 1,
131
+ digest: callArgumentsDigest(args),
132
+ };
133
+ }
134
+ if (tool === ToolNames.Edit) {
135
+ const edits = readEdits(args.edits);
136
+ if (edits === null || edits.length === 0) return null;
137
+ const bytes = edits.reduce((total, edit) => total + Buffer.byteLength(edit.newText, "utf8"), 0);
138
+ return { kind: "edit", bytes, replacements: edits.length, digest: callArgumentsDigest(args) };
139
+ }
140
+ return null;
141
+ }
142
+
143
+ /** `write · 482 B · sha256 3f9a1c22ee04b7d1`, the collapsed card's whole mutation line. */
144
+ export function mutationFactsLine(facts: MutationFacts): string {
145
+ const shape =
146
+ facts.kind === "write"
147
+ ? `write ${facts.bytes} B`
148
+ : `edit ${facts.replacements} replacement${facts.replacements === 1 ? "" : "s"} · ${facts.bytes} B`;
149
+ return `${shape} · sha256 ${facts.digest}`;
150
+ }
151
+
152
+ function numberedLines(text: string): string[] {
153
+ const raw = text.split("\n");
154
+ const width = String(raw.length).length;
155
+ return raw.map((line, index) => `${String(index + 1).padStart(width, " ")} ${line}`);
156
+ }
157
+
158
+ function boundedContent(content: string): { text: string; withheld: number } {
159
+ if (content.length <= MUTATION_PREVIEW_MAX_CHARS) return { text: content, withheld: 0 };
160
+ return {
161
+ text: content.slice(0, MUTATION_PREVIEW_MAX_CHARS),
162
+ withheld: content.length - MUTATION_PREVIEW_MAX_CHARS,
163
+ };
164
+ }
165
+
166
+ function previewFrom(
167
+ facts: MutationFacts,
168
+ heading: string,
169
+ text: string,
170
+ options: { numbered?: boolean; trailer?: string } = {},
171
+ ): MutationPreview {
172
+ const clean = sanitizeMultilineDisplayText(text);
173
+ const body = options.numbered ? numberedLines(clean.text) : clean.text.split("\n");
174
+ if (options.trailer !== undefined) body.push(options.trailer);
175
+ return {
176
+ facts,
177
+ heading,
178
+ body,
179
+ neutralized: clean.neutralized,
180
+ tabsExpanded: clean.tabsExpanded,
181
+ };
182
+ }
183
+
184
+ function refusal(facts: MutationFacts, heading: string, reason: string): MutationPreview {
185
+ return { facts, heading, body: [reason], neutralized: false, tabsExpanded: false };
186
+ }
187
+
188
+ /**
189
+ * The requested replacements, rendered as a diff would render them, for the
190
+ * case where the effective diff cannot be computed. The operator still needs to
191
+ * read what the model asked for; the heading says this is the request rather
192
+ * than the effect.
193
+ */
194
+ function replacementListing(edits: ReadonlyArray<Edit>): string {
195
+ const blocks: string[] = [];
196
+ for (const [index, edit] of edits.entries()) {
197
+ blocks.push(`@@ replacement ${index + 1} of ${edits.length}`);
198
+ for (const line of normalizeToLF(edit.oldText).split("\n")) blocks.push(`- ${line}`);
199
+ for (const line of normalizeToLF(edit.newText).split("\n")) blocks.push(`+ ${line}`);
200
+ }
201
+ return blocks.join("\n");
202
+ }
203
+
204
+ function editPreview(facts: MutationFacts, path: string, edits: ReadonlyArray<Edit>, read: (path: string) => string) {
205
+ let current: string;
206
+ try {
207
+ current = read(resolveToCwd(path));
208
+ } catch (error) {
209
+ const message = sanitizeCallTargetText(error instanceof Error ? error.message : String(error));
210
+ return previewFrom(
211
+ facts,
212
+ `Requested replacements for ${sanitizeCallTargetText(path)} (the file could not be read: ${message})`,
213
+ replacementListing(edits),
214
+ );
215
+ }
216
+ try {
217
+ const { text } = stripBom(current);
218
+ const applied = applyEditsToNormalizedContent(normalizeToLF(text), edits, path);
219
+ const diff = generateDiffString(applied.baseContent, applied.newContent);
220
+ return previewFrom(facts, `Effective diff for ${sanitizeCallTargetText(path)}`, diff.diff);
221
+ } catch (error) {
222
+ const message = sanitizeCallTargetText(error instanceof Error ? error.message : String(error));
223
+ return previewFrom(
224
+ facts,
225
+ `Requested replacements for ${sanitizeCallTargetText(path)} (they do not apply: ${message})`,
226
+ replacementListing(edits),
227
+ );
228
+ }
229
+ }
230
+
231
+ export interface MutationInspectorOptions {
232
+ /** Reads the current file bytes for an edit's effective diff. Injected so the contract test owns the filesystem. */
233
+ readFile?: (path: string) => string;
234
+ }
235
+
236
+ /**
237
+ * Bind an inspector to one parked call. The returned closure reads the file and
238
+ * computes the diff only when the operator presses the inspect key, and it
239
+ * re-derives the digest from the same arguments it renders: a mismatch means
240
+ * the arguments changed under the decision and the preview refuses rather than
241
+ * showing bytes that are no longer the ones being approved.
242
+ */
243
+ export function createMutationInspector(
244
+ tool: string,
245
+ args: Record<string, unknown> | undefined,
246
+ facts: MutationFacts,
247
+ options: MutationInspectorOptions = {},
248
+ ): MutationInspector {
249
+ const read = options.readFile ?? ((path: string) => readFileSync(path, "utf8"));
250
+ return () => {
251
+ const current = mutationFacts(tool, args);
252
+ if (current === null || current.digest !== facts.digest) {
253
+ return refusal(
254
+ facts,
255
+ "Preview unavailable",
256
+ `These arguments no longer digest to sha256 ${facts.digest}. Deny this call rather than approving bytes that cannot be shown.`,
257
+ );
258
+ }
259
+ const path = typeof args?.path === "string" ? args.path : "";
260
+ if (facts.kind === "write") {
261
+ const content = typeof args?.content === "string" ? args.content : "";
262
+ const bounded = boundedContent(content);
263
+ return previewFrom(facts, `Proposed content for ${sanitizeCallTargetText(path)}`, bounded.text, {
264
+ numbered: true,
265
+ ...(bounded.withheld > 0
266
+ ? { trailer: `… ${bounded.withheld} more characters not shown (preview capped at ${MUTATION_PREVIEW_MAX_CHARS})` }
267
+ : {}),
268
+ });
269
+ }
270
+ const edits = readEdits(args?.edits) ?? [];
271
+ return editPreview(facts, path, edits, read);
272
+ };
273
+ }
274
+
275
+ /**
276
+ * The scrolled window of a preview, plus the position line that makes the cut
277
+ * visible. Mirrors the `/handoff` review overlay so both scrollable inspection
278
+ * surfaces read the same way.
279
+ */
280
+ export function mutationPreviewWindow(
281
+ lines: ReadonlyArray<string>,
282
+ scroll: number,
283
+ rows = MUTATION_PREVIEW_VISIBLE_ROWS,
284
+ ): { window: ReadonlyArray<string>; position: string | null; maxScroll: number } {
285
+ const visible = Math.max(1, rows);
286
+ const maxScroll = Math.max(0, lines.length - visible);
287
+ const start = Math.max(0, Math.min(scroll, maxScroll));
288
+ const window = lines.slice(start, start + visible);
289
+ if (lines.length <= visible) return { window, position: null, maxScroll };
290
+ return {
291
+ window,
292
+ position: `(${start + 1}-${start + window.length} of ${lines.length} lines)`,
293
+ maxScroll,
294
+ };
295
+ }
@@ -12,6 +12,7 @@ import {
12
12
  proposeMemoryPromotion,
13
13
  } from "../domains/memory/index.js";
14
14
  import type { ObservabilityContract } from "../domains/observability/index.js";
15
+ import { foregroundStreamUsage } from "../domains/providers/index.js";
15
16
  import type { ContextLedger } from "../domains/session/context-ledger.js";
16
17
  import type { SessionMeta } from "../domains/session/index.js";
17
18
  import { foldSessionArtifacts } from "../domains/session/session-artifacts.js";
@@ -147,6 +148,11 @@ export function createOverlayGeneralOpeners(deps: OverlayGeneralOpenersDeps): Ov
147
148
  deps.transitions.state = "cost";
148
149
  deps.transitions.handle = openCostOverlayFactory(deps.tui, deps.observability, {
149
150
  sessionId: deps.getSessionId?.() ?? null,
151
+ ...(deps.readSessionEntries
152
+ ? {
153
+ getSessionEntries: () => deps.readSessionEntries?.() ?? [],
154
+ }
155
+ : {}),
150
156
  });
151
157
  deps.requestRender();
152
158
  };
@@ -429,10 +435,20 @@ export function createOverlayGeneralOpeners(deps: OverlayGeneralOpenersDeps): Ov
429
435
  };
430
436
  const resolution = deps.dispatch.preview?.(request);
431
437
  if (!resolution) return null;
438
+ const foregroundHeld =
439
+ resolution.endpoint === undefined ? 0 : (foregroundStreamUsage()[resolution.endpoint.key] ?? 0);
432
440
  return {
433
441
  targetId: resolution.targetId,
434
442
  wireModelId: resolution.wireModelId,
435
443
  nodeId: resolution.node.id,
444
+ ...(resolution.endpoint !== undefined
445
+ ? {
446
+ endpoint: {
447
+ ...resolution.endpoint,
448
+ ...(foregroundHeld > 0 ? { foregroundHeld } : {}),
449
+ },
450
+ }
451
+ : {}),
436
452
  };
437
453
  },
438
454
  });
@@ -1,5 +1,6 @@
1
1
  import type { ClioKeybinding } from "../domains/config/keybindings.js";
2
2
  import { isKeyRelease, matchesKey } from "../engine/tui.js";
3
+ import { MUTATION_PREVIEW_KEY } from "./permission-hint.js";
3
4
 
4
5
  export type OverlayState =
5
6
  | "closed"
@@ -47,6 +48,11 @@ export interface PermissionOverlayKeyDeps {
47
48
  * the call. Only the deletion keys in `isDraftEditKey` arrive here.
48
49
  */
49
50
  editDraft?: (data: string) => void;
51
+ /** Whether this card has a mutation the operator can read locally (issue #254). */
52
+ canInspectMutation?: () => boolean;
53
+ isInspectingMutation?: () => boolean;
54
+ toggleMutationInspection?: () => void;
55
+ scrollMutationInspection?: (delta: number) => void;
50
56
  }
51
57
 
52
58
  export interface DispatchBoardOverlayKeyDeps {
@@ -85,6 +91,18 @@ function isDraftEditKey(data: string): boolean {
85
91
  return !isKeyRelease(data) && DRAFT_EDIT_KEYS.some((key) => matchesKey(data, key));
86
92
  }
87
93
 
94
+ /** Rows one page key moves the open mutation. Matches the window the card renders. */
95
+ const MUTATION_PAGE_ROWS = 16;
96
+
97
+ /** How far a key scrolls the open mutation, or 0 when the key is not a scroll key. */
98
+ function mutationScrollDelta(data: string): number {
99
+ if (matchesKey(data, "up")) return -1;
100
+ if (matchesKey(data, "down")) return 1;
101
+ if (matchesKey(data, "pageUp")) return -MUTATION_PAGE_ROWS;
102
+ if (matchesKey(data, "pageDown")) return MUTATION_PAGE_ROWS;
103
+ return 0;
104
+ }
105
+
88
106
  /** Pure permission overlay key router: returns true when the input was consumed. */
89
107
  export function routePermissionOverlayKey(data: string, deps: PermissionOverlayKeyDeps): boolean {
90
108
  if (matchesKey(data, "enter") && !isKeyRelease(data)) {
@@ -98,6 +116,26 @@ export function routePermissionOverlayKey(data: string, deps: PermissionOverlayK
98
116
  deps.editDraft(data);
99
117
  return true;
100
118
  }
119
+ // Reading the mutation is the one thing this dialog does that is not an
120
+ // answer, so it toggles rather than navigating away: Enter, `s`, and Esc keep
121
+ // their meanings the whole time the mutation is on screen, and `v` is what
122
+ // puts it away. A card with nothing local to inspect leaves the key inert.
123
+ if (
124
+ matchesKey(data, MUTATION_PREVIEW_KEY) &&
125
+ !isKeyRelease(data) &&
126
+ (deps.canInspectMutation?.() ?? false) &&
127
+ deps.toggleMutationInspection
128
+ ) {
129
+ deps.toggleMutationInspection();
130
+ return true;
131
+ }
132
+ if ((deps.isInspectingMutation?.() ?? false) && deps.scrollMutationInspection && !isKeyRelease(data)) {
133
+ const delta = mutationScrollDelta(data);
134
+ if (delta !== 0) {
135
+ deps.scrollMutationInspection(delta);
136
+ return true;
137
+ }
138
+ }
101
139
  if (isEscapeKey(data)) {
102
140
  deps.cancelPermission();
103
141
  return true;
@@ -12,6 +12,7 @@ import { createOverlayTransitions } from "./overlay-transitions.js";
12
12
  import {
13
13
  createPermissionOverlayBody,
14
14
  PERMISSION_OVERLAY_WIDTH,
15
+ type PermissionOverlayBodyHandle,
15
16
  permissionOverlayHint,
16
17
  permissionOverlayPlacement,
17
18
  permissionOverlayTitle,
@@ -163,6 +164,12 @@ export interface OverlayLifecycleController {
163
164
  toggleDispatchBoardOverlay(): void;
164
165
  confirmPermission(): void;
165
166
  stopTurnFromPermission(): void;
167
+ /** Whether the live permission card has a mutation the operator can read here. */
168
+ canInspectMutation(): boolean;
169
+ /** Whether that mutation is currently open. */
170
+ isInspectingMutation(): boolean;
171
+ toggleMutationInspection(): void;
172
+ scrollMutationInspection(delta: number): void;
166
173
  cancelAskUser(): void;
167
174
  dispose(): void;
168
175
  }
@@ -210,6 +217,16 @@ export function createOverlayLifecycle(deps: OverlayLifecycleRuntimeDeps): Overl
210
217
  } = deps;
211
218
  let overlayPermission: OverlayPermissionLifecycle | null = null;
212
219
  let overlayAskUser: OverlayAskUserLifecycle | null = null;
220
+ /**
221
+ * The live permission card's body, held only while its dialog is on screen.
222
+ * The mutation text it can show lives here and nowhere else, so dropping the
223
+ * reference when the dialog closes drops the text with it.
224
+ */
225
+ let permissionBody: PermissionOverlayBodyHandle | null = null;
226
+ const inspectionHint = (): import("./permission-hint.js").PermissionInspectionHint => {
227
+ if (permissionBody === null || !permissionBody.canInspect()) return "none";
228
+ return permissionBody.isInspecting() ? "open" : "closed";
229
+ };
213
230
  const overlayTransitions = createOverlayTransitions({
214
231
  stopDispatchBoardTicker: () => interactiveTickers.stopDispatchBoardTicker(),
215
232
  renderContextIsland: () => interactiveTickers.renderContextIsland(),
@@ -217,7 +234,10 @@ export function createOverlayLifecycle(deps: OverlayLifecycleRuntimeDeps): Overl
217
234
  requestRender: () => tui.requestRender(),
218
235
  cancelPendingAskUser: () => overlayAskUser?.cancelPending() ?? false,
219
236
  finishAuth: (dismiss) => overlayAuth.finish(dismiss),
220
- onPermissionOverlayClosed: () => overlayPermission?.onPermissionOverlayClosed(),
237
+ onPermissionOverlayClosed: () => {
238
+ permissionBody = null;
239
+ overlayPermission?.onPermissionOverlayClosed();
240
+ },
221
241
  onOverlayClosed: () => overlayPermission?.retryPending(),
222
242
  });
223
243
  const closeOverlay = overlayTransitions.close;
@@ -248,17 +268,20 @@ export function createOverlayLifecycle(deps: OverlayLifecycleRuntimeDeps): Overl
248
268
  dispatch: deps.app.dispatch,
249
269
  getAutonomy: () => deps.app.getSettings?.().autonomy ?? "auto-edit",
250
270
  getOverlayState: () => overlayTransitions.state,
251
- openPermissionOverlay: (view) => {
271
+ openPermissionOverlay: (view, inspect) => {
252
272
  if (overlayTransitions.state !== "closed") return false;
253
273
  overlayTransitions.state = "permission-confirm";
254
- overlayTransitions.handle = showOverlayFrame(tui, createPermissionOverlayBody(view), {
274
+ const body = createPermissionOverlayBody(view, inspect);
275
+ permissionBody = body;
276
+ overlayTransitions.handle = showOverlayFrame(tui, body, {
255
277
  ...permissionOverlayPlacement(tui, editor, footer.view),
256
278
  width: PERMISSION_OVERLAY_WIDTH,
257
279
  title: permissionOverlayTitle(view),
258
280
  tone: permissionOverlayTone(view),
259
281
  // Read per frame: the footer names what Enter does right now, and
260
- // that depends on whether the composer holds a draft.
261
- footerHint: (innerWidth) => permissionOverlayHint(innerWidth, editor.getText().length > 0),
282
+ // that depends on whether the composer holds a draft and on whether
283
+ // the mutation is open.
284
+ footerHint: (innerWidth) => permissionOverlayHint(innerWidth, editor.getText().length > 0, inspectionHint()),
262
285
  });
263
286
  tui.requestRender();
264
287
  return true;
@@ -464,6 +487,16 @@ export function createOverlayLifecycle(deps: OverlayLifecycleRuntimeDeps): Overl
464
487
  footer.refresh();
465
488
  tui.requestRender();
466
489
  },
490
+ canInspectMutation: () => permissionBody?.canInspect() ?? false,
491
+ isInspectingMutation: () => permissionBody?.isInspecting() ?? false,
492
+ toggleMutationInspection: () => {
493
+ permissionBody?.toggleInspect();
494
+ tui.requestRender();
495
+ },
496
+ scrollMutationInspection: (delta) => {
497
+ permissionBody?.scrollInspect(delta);
498
+ tui.requestRender();
499
+ },
467
500
  cancelAskUser: overlayAskUser.cancel,
468
501
  dispose: () => {
469
502
  overlayPermission?.dispose();
@@ -11,6 +11,7 @@ import type { PermissionRequiredMeta, ToolRegistry } from "../tools/registry.js"
11
11
  import { approvalParkedNotice, autonomyDeniedNotice, workerEscalationNotice } from "./bus-notices.js";
12
12
  import type { ToolApprovalStateEvent } from "./chat-loop.js";
13
13
  import type { NoticeLevel } from "./command-output.js";
14
+ import { createMutationInspector, type MutationInspector, mutationFacts } from "./mutation-preview.js";
14
15
  import type { OverlayState } from "./overlay-key-routing.js";
15
16
  import { type ApprovalRequestView, askAxis, describeCallTarget } from "./permission-overlay.js";
16
17
 
@@ -43,7 +44,12 @@ export interface OverlayPermissionLifecycleDeps {
43
44
  dispatch: Pick<DispatchContract, "resolveWorkerPermission">;
44
45
  getAutonomy(): string;
45
46
  getOverlayState(): OverlayState;
46
- openPermissionOverlay(view: ApprovalRequestView): boolean;
47
+ /**
48
+ * Open the dialog for one parked call. `inspect` carries the complete
49
+ * mutation text and is the only channel that does; it is process-local by
50
+ * construction, so it is passed here rather than folded into the view.
51
+ */
52
+ openPermissionOverlay(view: ApprovalRequestView, inspect?: MutationInspector): boolean;
47
53
  closeOverlay(): void;
48
54
  appendNotice(level: NoticeLevel, text: string): void;
49
55
  applyApprovalState(event: ToolApprovalStateEvent): void;
@@ -131,6 +137,9 @@ function mainApprovalRequestView(
131
137
  ? { kind: "net" as const, ruleId: axisFromDecision.ruleId }
132
138
  : { kind: "autonomy" as const, level: autonomy });
133
139
  const target = describeCallTarget(call.tool, call.args);
140
+ // Facts only. The mutation text stays in the inspector the overlay opener
141
+ // gets; this object reaches the transcript row and the approval-state event.
142
+ const mutation = mutationFacts(call.tool, call.args);
134
143
  return {
135
144
  requestId: meta?.requestId ?? "permission-pending",
136
145
  tool: call.tool,
@@ -144,10 +153,21 @@ function mainApprovalRequestView(
144
153
  : {}),
145
154
  ...(call.tool === ToolNames.AskUser ? { exposure: askUserExposure(call.args) } : {}),
146
155
  ...(target.length > 0 ? { target } : {}),
156
+ ...(mutation !== null ? { mutation } : {}),
147
157
  ...(queueDepth !== undefined && queueDepth > 1 ? { queueDepth } : {}),
148
158
  };
149
159
  }
150
160
 
161
+ /**
162
+ * The inspector for a main-agent ask, or undefined when there is nothing local
163
+ * to read. A worker escalation never gets one: its arguments stay in the
164
+ * worker, and the card says so rather than implying an inspection it cannot do.
165
+ */
166
+ function mainMutationInspector(call: ClassifierCall, view: ApprovalRequestView): MutationInspector | undefined {
167
+ if (view.mutation === undefined) return undefined;
168
+ return createMutationInspector(call.tool, call.args, view.mutation);
169
+ }
170
+
151
171
  function workerEscalationEntry(payload: PermissionRequestedPayload, autonomy: string): WorkerEscalationEntry | null {
152
172
  if (!isLiveWorkerEscalationRequest(payload) || typeof payload.requestId !== "string") return null;
153
173
  const origin = typeof payload.origin === "string" ? payload.origin : undefined;
@@ -240,7 +260,7 @@ export function createOverlayPermissionLifecycle(deps: OverlayPermissionLifecycl
240
260
  announceParked();
241
261
  return;
242
262
  }
243
- if (!deps.openPermissionOverlay(view)) {
263
+ if (!deps.openPermissionOverlay(view, mainMutationInspector(call, view))) {
244
264
  announceParked();
245
265
  return;
246
266
  }
@@ -403,19 +403,83 @@ export function createOverlaySessionLifecycle(deps: OverlaySessionLifecycleDeps)
403
403
  void runHandoffExtraction(session, fromSessionId, verdict.goal);
404
404
  }
405
405
 
406
+ /** Characters of the second round's answer quoted in the terminal refusal. */
407
+ const HANDOFF_REFUSAL_QUOTE_CHARS = 200;
408
+
409
+ /**
410
+ * A refusal that says what was asked for and what came back, not only that no
411
+ * JSON object arrived. Both rounds are named, so an operator reading it knows
412
+ * two model calls were spent and on what.
413
+ */
414
+ function terminalHandoffRefusal(firstReason: string, secondReason: string, secondText: string): string {
415
+ const answered = secondText.replace(/\s+/g, " ").trim().slice(0, HANDOFF_REFUSAL_QUOTE_CHARS);
416
+ return [
417
+ "the extraction round could not produce a handoff record after one repair attempt.",
418
+ `Asked for: one JSON object with decisions, facts, files, commands, and openQuestions.`,
419
+ `Round 1: ${firstReason}.`,
420
+ `Round 2: ${secondReason}.`,
421
+ `Round 2 returned: ${answered.length > 0 ? answered : "(nothing)"}`,
422
+ ].join(" ");
423
+ }
424
+
425
+ /**
426
+ * Extract, then repair once.
427
+ *
428
+ * A local model that answers with prose around the object, or with nothing
429
+ * parseable, used to end `/handoff` outright: every downstream behavior (the
430
+ * dropped-path listing, the `e` editor, accept-mints-a-session) was
431
+ * unreachable and the operator had paid for the round either way (issue
432
+ * #223). Exactly one repair round follows, quoting what came back and what
433
+ * the parser objected to, and both rounds bill through the same out-of-turn
434
+ * usage store.
435
+ */
436
+ async function extractWithOneRepair(
437
+ goal: string,
438
+ ): Promise<
439
+ | { ok: true; parsed: Extract<ReturnType<typeof parseHandoffExtraction>, { ok: true }> }
440
+ | { ok: false; level: "error" | "warn"; reason: string }
441
+ > {
442
+ const first = await deps.chat.extractHandoff(goal);
443
+ if (first.status !== "answered") {
444
+ return {
445
+ ok: false,
446
+ level: first.status === "failed" ? "error" : "warn",
447
+ reason: first.status === "aborted" ? "the extraction round was cancelled" : first.reason,
448
+ };
449
+ }
450
+ const parsedFirst = parseHandoffExtraction(first.text);
451
+ if (parsedFirst.ok) return { ok: true, parsed: parsedFirst };
452
+
453
+ const second = await deps.chat.extractHandoff(goal, {
454
+ repair: { complaint: parsedFirst.reason, previous: first.text },
455
+ });
456
+ if (second.status !== "answered") {
457
+ return {
458
+ ok: false,
459
+ level: second.status === "failed" ? "error" : "warn",
460
+ reason:
461
+ second.status === "aborted"
462
+ ? "the repair round was cancelled"
463
+ : `round 1 could not be read (${parsedFirst.reason}); the repair round then failed: ${second.reason}`,
464
+ };
465
+ }
466
+ const parsedSecond = parseHandoffExtraction(second.text);
467
+ if (parsedSecond.ok) return { ok: true, parsed: parsedSecond };
468
+ return {
469
+ ok: false,
470
+ level: "error",
471
+ reason: terminalHandoffRefusal(parsedFirst.reason, parsedSecond.reason, second.text),
472
+ };
473
+ }
474
+
406
475
  async function runHandoffExtraction(session: SessionContract, fromSessionId: string, goal: string): Promise<void> {
407
476
  const notice = deps.getSlashNotice();
408
- const outcome = await deps.chat.extractHandoff(goal);
409
- if (outcome.status !== "answered") {
410
- const reason = outcome.status === "aborted" ? "the extraction round was cancelled" : outcome.reason;
411
- emitCommandNotice(notice, outcome.status === "failed" ? "error" : "warn", "handoff", reason);
412
- return;
413
- }
414
- const parsed = parseHandoffExtraction(outcome.text);
415
- if (!parsed.ok) {
416
- emitCommandNotice(notice, "error", "handoff", parsed.reason);
477
+ const extraction = await extractWithOneRepair(goal);
478
+ if (!extraction.ok) {
479
+ emitCommandNotice(notice, extraction.level, "handoff", extraction.reason);
417
480
  return;
418
481
  }
482
+ const parsed = extraction.parsed;
419
483
  const meta = session.current();
420
484
  const cwd = typeof meta?.cwd === "string" && meta.cwd.length > 0 ? meta.cwd : null;
421
485
  const entries = deps.readStructuredEntries(fromSessionId);