@iowarp/clio-coder 0.3.2 → 0.3.4

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 (259) hide show
  1. package/CHANGELOG.md +269 -458
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +3 -3
  4. package/dist/{acp-BIYHVZIM.js → acp-S5R4RR5B.js} +7 -6
  5. package/dist/{agents-YT6SSRIT.js → agents-P6DMMVZY.js} +24 -21
  6. package/dist/assets/codewiki.json +1 -1
  7. package/dist/{auth-5TWEIYDN.js → auth-2XCZLPKS.js} +12 -8
  8. package/dist/{chunk-GGXXDWE4.js → chunk-22NAGB7X.js} +2 -2
  9. package/dist/{chunk-WMSVI4G2.js → chunk-2LZI5CAG.js} +133 -13
  10. package/dist/{chunk-OAO4GE4M.js → chunk-2TZWSW76.js} +2 -2
  11. package/dist/{chunk-OOJYHWRB.js → chunk-34475P3I.js} +2 -2
  12. package/dist/{chunk-WVO7V2QY.js → chunk-35MKKU5R.js} +4 -4
  13. package/dist/{chunk-LBNRH5WM.js → chunk-3HZ5RWN2.js} +5 -5
  14. package/dist/{chunk-AGYYIBLL.js → chunk-3JLKSKD7.js} +2 -2
  15. package/dist/{chunk-MBS4V7ZP.js → chunk-4JUF2NNX.js} +7 -7
  16. package/dist/{chunk-ZDOOVTXZ.js → chunk-4OC57DA6.js} +27 -4
  17. package/dist/chunk-5M54SPOL.js +926 -0
  18. package/dist/{chunk-STBPMHSX.js → chunk-7RXG6QRZ.js} +51 -11
  19. package/dist/{chunk-77VKQEHF.js → chunk-A2GZF7DC.js} +5 -5
  20. package/dist/{chunk-A3CYT5EX.js → chunk-AD2SYQYC.js} +55 -2
  21. package/dist/chunk-AOCYTWAV.js +449 -0
  22. package/dist/chunk-BEY543CS.js +258 -0
  23. package/dist/{chunk-6N5PTWMY.js → chunk-BP4OYD6A.js} +32 -13
  24. package/dist/chunk-BPGS2WCQ.js +612 -0
  25. package/dist/{chunk-J5HN4RYU.js → chunk-BRXQQJFP.js} +8 -8
  26. package/dist/chunk-CFGTUFWB.js +67 -0
  27. package/dist/{chunk-CBCAPZAA.js → chunk-E25LMLRW.js} +2 -2
  28. package/dist/{chunk-G2DE3C7R.js → chunk-EDRHSCIE.js} +4 -4
  29. package/dist/{chunk-4KLWL3UC.js → chunk-EFADSJET.js} +2 -2
  30. package/dist/{chunk-M6SHUN7Q.js → chunk-FO5ZOVUY.js} +2 -2
  31. package/dist/chunk-FYYLNIL5.js +313 -0
  32. package/dist/{chunk-EPVUXGXG.js → chunk-HV5X7OR2.js} +14 -12
  33. package/dist/{chunk-TZTZS7QK.js → chunk-HXG4IURW.js} +5 -3
  34. package/dist/{chunk-IGLFWIYI.js → chunk-K6WL7QZT.js} +3 -3
  35. package/dist/chunk-K7VKOLQQ.js +15 -0
  36. package/dist/{chunk-BMEMKKIT.js → chunk-KOHPCX4K.js} +2 -2
  37. package/dist/{chunk-V4RXGQ5Q.js → chunk-KRPY7NTG.js} +10 -7
  38. package/dist/chunk-LL4KHSZI.js +22 -0
  39. package/dist/{chunk-KJ5LWLOE.js → chunk-MEQ45TQ4.js} +15 -9
  40. package/dist/{chunk-5UUP6MWO.js → chunk-MV3K5QF2.js} +5 -436
  41. package/dist/{chunk-AO4RKG4M.js → chunk-N4CZJQRK.js} +5 -5
  42. package/dist/{chunk-ARBGF5F7.js → chunk-NILBFAPG.js} +14 -8
  43. package/dist/chunk-OZNBF4L3.js +23 -0
  44. package/dist/{verify-G6V4D2G7.js → chunk-PCZJO5TI.js} +127 -42
  45. package/dist/chunk-QQK64KLB.js +1360 -0
  46. package/dist/{chunk-6EJV5X2W.js → chunk-QQL5RT5M.js} +979 -1619
  47. package/dist/{chunk-LZSJBIVT.js → chunk-QWU7ZBO7.js} +70 -720
  48. package/dist/{chunk-2EHAIA3X.js → chunk-RD5U66HV.js} +3 -3
  49. package/dist/{chunk-OKGUZO2U.js → chunk-SPULKLCF.js} +4 -3
  50. package/dist/{chunk-OQ33BKR3.js → chunk-TTNYS3EA.js} +3 -60
  51. package/dist/chunk-TW3WDMVS.js +677 -0
  52. package/dist/chunk-TZSKNMZG.js +434 -0
  53. package/dist/{chunk-7MNJORFF.js → chunk-UL3WSD3F.js} +6 -1
  54. package/dist/{chunk-7EYHLWU7.js → chunk-UZHIZC5S.js} +7 -7
  55. package/dist/{chunk-QTYWRVRA.js → chunk-VAWWTKDP.js} +8 -8
  56. package/dist/{chunk-X75S7HFS.js → chunk-VEZEGCGW.js} +214 -20
  57. package/dist/{chunk-OHHN2SO4.js → chunk-VMNQ6OZA.js} +98 -202
  58. package/dist/chunk-VSNATDE6.js +122 -0
  59. package/dist/chunk-W6GROXXM.js +69 -0
  60. package/dist/chunk-WPQLXFOZ.js +375 -0
  61. package/dist/{chunk-ORBHGJC5.js → chunk-WR67VIZY.js} +3 -3
  62. package/dist/{chunk-3ZXDFGR5.js → chunk-X6COSD2O.js} +5 -5
  63. package/dist/chunk-ZGVHUX3M.js +66 -0
  64. package/dist/{chunk-MAW544W2.js → chunk-ZWMF7253.js} +4 -4
  65. package/dist/{chunk-MQSRRFWA.js → chunk-ZYKPLLNQ.js} +563 -546
  66. package/dist/cli/index.js +27 -23
  67. package/dist/{clio-4LY5K2AC.js → clio-J5JIOIDS.js} +7 -6
  68. package/dist/{code-nav-7AX6FYE6.js → code-nav-AXCXSBHX.js} +5 -3
  69. package/dist/{config-GTLUW2PR.js → config-OEBMIN2U.js} +37 -27
  70. package/dist/{configure-R6A64DHX.js → configure-PUQOSIXQ.js} +16 -13
  71. package/dist/{context-5VKGUVJJ.js → context-EKDCKUUZ.js} +82 -7
  72. package/dist/{context-RW5HC47S.js → context-MGSE4Z2T.js} +33 -23
  73. package/dist/{context-JFZEJ7W5.js → context-URSXPBCK.js} +17 -9
  74. package/dist/{context-clear-6ZHBAZZT.js → context-clear-KDAJRNUK.js} +33 -23
  75. package/dist/context-working-set-SBKMPPI2.js +1552 -0
  76. package/dist/{dispatch-runner-VKBRCWQC.js → dispatch-runner-MSWN72NK.js} +43 -29
  77. package/dist/{doctor-KI767GSN.js → doctor-7BSE27PJ.js} +10 -10
  78. package/dist/{eval-XSSNATB4.js → eval-IZGDOO4H.js} +9 -8
  79. package/dist/{evidence-UA6AWDQQ.js → evidence-SR7WXB5B.js} +51 -23
  80. package/dist/{evolve-QNTFGV6Z.js → evolve-K7VE2CBX.js} +30 -20
  81. package/dist/{fleet-Q7UOMUSG.js → fleet-7XMJNQNF.js} +48 -38
  82. package/dist/{fleet-preflight-DDN536IT.js → fleet-preflight-AQNAH644.js} +3 -3
  83. package/dist/{init-WBB65ZHQ.js → init-JGNPAYXT.js} +41 -31
  84. package/dist/{memory-MD3O64RI.js → memory-4ALKDJ4Q.js} +32 -22
  85. package/dist/{models-BZU34YWD.js → models-ZMMLFJNN.js} +22 -19
  86. package/dist/{monitor-MEQA5C3I.js → monitor-2F3T5KHP.js} +55 -43
  87. package/dist/{orchestrator-CGFKEP27.js → orchestrator-ORHT43JB.js} +2507 -1896
  88. package/dist/{reset-L2FQEE3E.js → reset-NXGTYNUO.js} +4 -3
  89. package/dist/{run-IV4Q6RLN.js → run-RF4WJGMT.js} +51 -41
  90. package/dist/{share-S5BZQC5I.js → share-UT3W6E4M.js} +5 -4
  91. package/dist/{skills-LQEKRDTN.js → skills-PSACKC5Q.js} +2 -2
  92. package/dist/{skills-eval-3DC4HEWS.js → skills-eval-WJSI55RZ.js} +34 -24
  93. package/dist/{targets-C4SSGQOB.js → targets-PIIRAOYS.js} +23 -20
  94. package/dist/{terminal-lease-IT5JW2NR.js → terminal-lease-ULWXWNVY.js} +5 -4
  95. package/dist/{upgrade-7TT7SQ3G.js → upgrade-346TZ6AV.js} +18 -17
  96. package/dist/{usage-GV4PKT3M.js → usage-6KKXR32N.js} +34 -24
  97. package/dist/verifiers-4UUM6TEE.js +1214 -0
  98. package/dist/verify-X5HDROLA.js +25 -0
  99. package/dist/{wiki-generate-DQF6Z66B.js → wiki-generate-7STOCIFZ.js} +42 -31
  100. package/dist/worker/entry.js +33 -24
  101. package/docs/README.md +8 -7
  102. package/docs/acp.md +1 -1
  103. package/docs/alcf-provider.md +1 -1
  104. package/docs/architecture.md +2 -2
  105. package/docs/artifact-versions.md +1 -1
  106. package/docs/built-in-agents.md +1 -1
  107. package/docs/capacity-and-scheduling.md +1 -1
  108. package/docs/commands-and-modes.md +53 -21
  109. package/docs/config-knobs-audit.md +1 -2
  110. package/docs/configuration-and-targets.md +15 -1
  111. package/docs/context-engine.md +64 -12
  112. package/docs/context-working-set.md +194 -0
  113. package/docs/development-pipeline.md +1 -1
  114. package/docs/documentation-coverage.md +5 -5
  115. package/docs/documentation-guide.md +6 -5
  116. package/docs/environment-variables.md +2 -1
  117. package/docs/eval-runner.md +1 -1
  118. package/docs/evals-internal.md +14 -1
  119. package/docs/evidence-and-memory.md +74 -2
  120. package/docs/evolution.md +1 -1
  121. package/docs/exit-codes-and-output.md +1 -1
  122. package/docs/extensions-and-sharing.md +2 -2
  123. package/docs/fleet-dispatch.md +22 -7
  124. package/docs/glossary.md +21 -1
  125. package/docs/installation-and-lifecycle.md +6 -6
  126. package/docs/middleware-and-components.md +1 -1
  127. package/docs/model-catalog.md +7 -9
  128. package/docs/observability.md +4 -4
  129. package/docs/performance-methodology.md +2 -2
  130. package/docs/proactive-memory.md +1 -1
  131. package/docs/prompt-envelope-and-tools.md +4 -4
  132. package/docs/provider-adapter-cookbook.md +1 -1
  133. package/docs/release-cut-checklist.md +35 -35
  134. package/docs/safety-model.md +23 -4
  135. package/docs/scientific-validation.md +21 -3
  136. package/docs/session-lifecycle.md +3 -3
  137. package/docs/skills-marketplace.md +1 -1
  138. package/docs/tool-usage.md +79 -12
  139. package/docs/trace-store.md +1 -1
  140. package/docs/troubleshooting.md +1 -1
  141. package/docs/tui-design.md +2 -2
  142. package/docs/worker-dispatch-mechanics.md +11 -1
  143. package/package.json +8 -11
  144. package/skills/meta/clio-test/SKILL.md +20 -17
  145. package/skills/meta/clio-test/evals.md +3 -3
  146. package/skills/meta/clio-test/references/harness.md +35 -6
  147. package/skills/meta/clio-test/references/test-map.md +20 -10
  148. package/skills/registry.yaml +2 -2
  149. package/skills/skill-marketplace.json +1 -1
  150. package/src/cli/context-working-set.ts +513 -0
  151. package/src/cli/context.ts +8 -0
  152. package/src/cli/evidence.ts +20 -2
  153. package/src/cli/index.ts +4 -0
  154. package/src/cli/verifiers.ts +325 -0
  155. package/src/core/bash-exec.ts +39 -14
  156. package/src/core/bus-events.ts +19 -4
  157. package/src/core/config.ts +54 -0
  158. package/src/core/defaults.ts +50 -3
  159. package/src/core/git-commit-attribution.ts +46 -21
  160. package/src/core/verification-scripts.ts +6 -0
  161. package/src/domains/agents/builtins/verifier.md +3 -0
  162. package/src/domains/config/classify.ts +1 -0
  163. package/src/domains/config/keybindings.ts +3 -3
  164. package/src/domains/context/working-set/contract.ts +161 -0
  165. package/src/domains/context/working-set/defaults.ts +28 -0
  166. package/src/domains/context/working-set/engine.ts +203 -0
  167. package/src/domains/context/working-set/fold.ts +62 -0
  168. package/src/domains/context/working-set/horizon.ts +38 -0
  169. package/src/domains/context/working-set/marker.ts +103 -0
  170. package/src/domains/context/working-set/path-index.ts +436 -0
  171. package/src/domains/context/working-set/payload.ts +152 -0
  172. package/src/domains/context/working-set/policies/age-horizon.ts +55 -0
  173. package/src/domains/context/working-set/policies/index.ts +21 -0
  174. package/src/domains/context/working-set/policies/structural.ts +160 -0
  175. package/src/domains/context/working-set/project.ts +132 -0
  176. package/src/domains/context/working-set/protect.ts +109 -0
  177. package/src/domains/context/working-set/recall.ts +177 -0
  178. package/src/domains/context/working-set/replay/controls.ts +112 -0
  179. package/src/domains/context/working-set/replay/load-clio.ts +199 -0
  180. package/src/domains/context/working-set/replay/metrics.ts +185 -0
  181. package/src/domains/context/working-set/replay/reference-graph.ts +79 -0
  182. package/src/domains/context/working-set/replay/report.ts +139 -0
  183. package/src/domains/context/working-set/replay/runner.ts +325 -0
  184. package/src/domains/context/working-set/replay/synthetic.ts +422 -0
  185. package/src/domains/context/working-set/replay/trace.ts +21 -0
  186. package/src/domains/context/working-set/visible.ts +54 -0
  187. package/src/domains/evidence/build.ts +112 -45
  188. package/src/domains/evidence/eval.ts +24 -7
  189. package/src/domains/evidence/index.ts +53 -0
  190. package/src/domains/evidence/ordering.ts +12 -0
  191. package/src/domains/evidence/run-trust.ts +221 -0
  192. package/src/domains/evidence/store.ts +46 -6
  193. package/src/domains/evidence/trust-status.ts +854 -0
  194. package/src/domains/evidence/types.ts +26 -0
  195. package/src/domains/middleware/memory-intervention.ts +3 -0
  196. package/src/domains/middleware/stalled-turn.ts +165 -4
  197. package/src/domains/safety/autonomy.ts +1 -1
  198. package/src/domains/safety/default-path-policy.ts +8 -0
  199. package/src/domains/safety/finish-contract.ts +4 -3
  200. package/src/domains/safety/policy-engine.ts +48 -6
  201. package/src/domains/session/compaction/compact.ts +23 -1
  202. package/src/domains/session/compaction/cut-point.ts +2 -0
  203. package/src/domains/session/compaction/tokens.ts +16 -1
  204. package/src/domains/session/context-ledger.ts +2 -0
  205. package/src/domains/session/entries.ts +107 -1
  206. package/src/domains/session/manager.ts +9 -2
  207. package/src/domains/session/migrations/index.ts +22 -3
  208. package/src/engine/acp/server.ts +3 -0
  209. package/src/engine/agent.ts +18 -1
  210. package/src/engine/session.ts +9 -3
  211. package/src/entry/orchestrator.ts +16 -4
  212. package/src/interactive/chat-loop-messages.ts +18 -6
  213. package/src/interactive/chat-panel.ts +571 -244
  214. package/src/interactive/chat-renderer.ts +79 -39
  215. package/src/interactive/context-meter.ts +10 -0
  216. package/src/interactive/context-overlay.ts +81 -6
  217. package/src/interactive/context-recall-command.ts +110 -0
  218. package/src/interactive/editor-submit.ts +26 -1
  219. package/src/interactive/footer/widgets.ts +22 -20
  220. package/src/interactive/footer-panel.ts +6 -1
  221. package/src/interactive/interactive-application.ts +2 -0
  222. package/src/interactive/interactive-event-projection.ts +12 -0
  223. package/src/interactive/interactive-slash-runtime.ts +49 -8
  224. package/src/interactive/model-session-replay.ts +21 -0
  225. package/src/interactive/overlay-general-openers.ts +6 -0
  226. package/src/interactive/overlay-session-lifecycle.ts +8 -4
  227. package/src/interactive/overlays/ask-user.ts +146 -24
  228. package/src/interactive/renderers/tool-execution.ts +167 -56
  229. package/src/interactive/session-transcript.ts +2 -2
  230. package/src/interactive/slash-commands.ts +29 -2
  231. package/src/interactive/status/index.ts +12 -1
  232. package/src/interactive/status/reasoning.ts +87 -0
  233. package/src/interactive/status/summary.ts +13 -2
  234. package/src/interactive/transcript-detail.ts +120 -0
  235. package/src/interactive/turn-context.ts +238 -88
  236. package/src/interactive/turn-middleware.ts +6 -6
  237. package/src/tools/agent-tools.ts +11 -4
  238. package/src/tools/bash.ts +144 -82
  239. package/src/tools/builtin-tool-catalog.ts +18 -6
  240. package/src/tools/context/index.ts +105 -3
  241. package/src/tools/context/surface.ts +3 -2
  242. package/src/tools/core-bootstrap.ts +21 -0
  243. package/src/tools/dispatch-runner.ts +9 -7
  244. package/src/tools/monitor.ts +28 -20
  245. package/src/tools/presentation.ts +107 -0
  246. package/src/tools/registry.ts +65 -7
  247. package/src/tools/result-disposition.ts +550 -0
  248. package/src/tools/result-shaping.ts +262 -19
  249. package/src/tools/safe-exec.ts +2 -0
  250. package/src/tools/verify/authoring.ts +1119 -0
  251. package/src/tools/verify/catalog.ts +346 -0
  252. package/src/tools/verify/index.ts +13 -3
  253. package/src/tools/verify/scripts.ts +135 -37
  254. package/src/tools/verify/surface.ts +9 -5
  255. package/src/tools/worker-evidence.ts +35 -12
  256. package/dist/chunk-MNA4JGU4.js +0 -255
  257. package/dist/chunk-SRF2PJNW.js +0 -184
  258. package/dist/chunk-T6YILFSB.js +0 -80
  259. package/dist/chunk-VAKQQHWR.js +0 -434
@@ -28,6 +28,7 @@ import { validateEngineToolArguments } from "../engine/ai.js";
28
28
  import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "../engine/types.js";
29
29
  import { applyToolProfile, type ToolProfileName } from "./profiles.js";
30
30
  import type { ToolInvokeOptions, ToolRegistry, ToolResult, ToolSpec } from "./registry.js";
31
+ import { isDispositionedToolResultError, toolResultContextText } from "./result-disposition.js";
31
32
 
32
33
  /**
33
34
  * Lightweight per-call observability hook. Default no-op so unused
@@ -99,10 +100,10 @@ type WorkerToolOkDetails = { kind: "ok" } & Record<string, unknown>;
99
100
  function projectToolResult(result: ToolResult): WorkerAgentToolResult {
100
101
  const details = { ...(result.details ?? {}), kind: result.kind } as { kind: "ok" } | { kind: "error" };
101
102
  if (result.kind === "error") {
102
- return { content: [{ type: "text", text: result.message }], details };
103
+ return { content: [{ type: "text", text: toolResultContextText(result) }], details };
103
104
  }
104
105
  return {
105
- content: [{ type: "text", text: result.output }],
106
+ content: [{ type: "text", text: toolResultContextText(result) }],
106
107
  details,
107
108
  ...(result.terminate === true ? { terminate: true } : {}),
108
109
  };
@@ -119,6 +120,8 @@ interface RunValidatedToolCallInput {
119
120
  signal?: AbortSignal;
120
121
  telemetry?: ToolTelemetry;
121
122
  invokeOptions?: Partial<ToolInvokeOptions>;
123
+ /** Agent loops can preserve canonical failure envelopes and classify them after execution. */
124
+ returnDispositionedErrors?: boolean;
122
125
  }
123
126
 
124
127
  async function runValidatedToolCall(input: RunValidatedToolCallInput): Promise<WorkerAgentToolResult> {
@@ -183,13 +186,16 @@ async function runValidatedToolCall(input: RunValidatedToolCallInput): Promise<W
183
186
  reason: verdict.result.message,
184
187
  decision: verdict.decision,
185
188
  });
186
- throw new Error(verdict.result.message);
189
+ if (input.returnDispositionedErrors === true && isDispositionedToolResultError(verdict.result)) {
190
+ return projectToolResult(verdict.result);
191
+ }
192
+ throw new Error(toolResultContextText(verdict.result));
187
193
  }
188
194
  const toolDetails = isRecord(verdict.result.details) ? verdict.result.details : {};
189
195
  const skillActivation =
190
196
  spec.name === ToolNames.Context ? skillActivationFromToolDetails(toolDetails, input.invokeOptions?.turnId) : null;
191
197
  const result: AgentToolResult<WorkerToolOkDetails> = {
192
- content: [{ type: "text", text: verdict.result.output }],
198
+ content: [{ type: "text", text: toolResultContextText(verdict.result) }],
193
199
  details: { ...toolDetails, kind: "ok" },
194
200
  };
195
201
  if (verdict.result.terminate === true) {
@@ -291,6 +297,7 @@ function toAgentTool(
291
297
  spec,
292
298
  args: params as Record<string, unknown>,
293
299
  registry,
300
+ returnDispositionedErrors: true,
294
301
  };
295
302
  if (signal) callInput.signal = signal;
296
303
  if (telemetry) callInput.telemetry = telemetry;
package/src/tools/bash.ts CHANGED
@@ -3,87 +3,124 @@ import { BASH_HARD_CAP_BYTES, combineBashOutput, runBashCommand } from "../core/
3
3
  import { resolveSafeCwd } from "../core/safe-exec.js";
4
4
  import { ToolNames } from "../core/tool-names.js";
5
5
  import { expandPath } from "./path-utils.js";
6
- import type { ToolInvokeOptions, ToolResult, ToolResultDetails, ToolSpec } from "./registry.js";
7
- import { writeToolOffload } from "./result-shaping.js";
8
- import { DEFAULT_MAX_LINES, formatSize, truncateTail } from "./truncate.js";
6
+ import { toolPresentationPolicy } from "./presentation.js";
7
+ import type { ToolResult, ToolResultDetails, ToolSpec } from "./registry.js";
8
+ import {
9
+ deterministicDiagnosticSummary,
10
+ type NormalizedToolResultDisposition,
11
+ type ToolResultContextDisposition,
12
+ type ToolResultDisposition,
13
+ } from "./result-disposition.js";
14
+ import { DEFAULT_MAX_LINES, truncateTail } from "./truncate.js";
15
+ import { byteLength } from "./truncate-utf8.js";
9
16
 
10
- // What the model sees inline. The tail is where the failing assertion, compiler
11
- // error, and exit summary live, so we keep the LAST lines/bytes. The full
12
- // output is spilled to an offload file before truncating, so nothing is lost.
17
+ // The registry's bounded model projection and operator presentation share this
18
+ // cap. Both are tail-biased for Bash because diagnostics usually land last.
13
19
  const BASH_DISPLAY_MAX_BYTES = 16 * 1024;
14
- // Slack reserved so the continuation notice appended after truncation still
15
- // fits under the registry's per-tool bash budget without a second (head-first)
16
- // re-truncation cutting the tail we just preserved.
17
- const BASH_TAIL_NOTE_RESERVE = 512;
20
+ export type BashOutputPolicy = "full" | "bounded" | "summary" | "metadata-only";
18
21
 
19
- interface ShapedBashOutput {
20
- text: string;
21
- details?: ToolResultDetails;
22
+ const BASH_OUTPUT_POLICIES = new Set<BashOutputPolicy>(["full", "bounded", "summary", "metadata-only"]);
23
+
24
+ function bashContextDisposition(policy: BashOutputPolicy): ToolResultContextDisposition & { maxBytes: number } {
25
+ if (policy === "full") {
26
+ return { mode: "full", maxBytes: BASH_DISPLAY_MAX_BYTES, downgradeExcerpt: "tail" };
27
+ }
28
+ if (policy === "summary") {
29
+ return {
30
+ mode: "summary",
31
+ maxBytes: BASH_DISPLAY_MAX_BYTES,
32
+ strategy: "diagnostic",
33
+ redact: true,
34
+ };
35
+ }
36
+ if (policy === "metadata-only") return { mode: "metadata-only", maxBytes: BASH_DISPLAY_MAX_BYTES };
37
+ return { mode: "bounded", maxBytes: BASH_DISPLAY_MAX_BYTES, excerpt: "tail" };
38
+ }
39
+
40
+ /** Canonical default attached by the builtin catalog and reused by direct registries. */
41
+ export const BASH_DEFAULT_RESULT_DISPOSITION: ToolResultDisposition = {
42
+ presentation: {
43
+ ...toolPresentationPolicy(ToolNames.Bash, undefined),
44
+ maxBytes: BASH_DISPLAY_MAX_BYTES,
45
+ overflow: "tail",
46
+ },
47
+ context: bashContextDisposition("bounded"),
48
+ };
49
+
50
+ /** Pure, idempotent normalization used by registry admission and direct calls. */
51
+ export function normalizeBashArguments(args: Record<string, unknown>): Record<string, unknown> {
52
+ return args.output_policy === undefined ? { ...args, output_policy: "bounded" } : args;
53
+ }
54
+
55
+ function bashOutputPolicy(args: Record<string, unknown>): BashOutputPolicy | null {
56
+ const value = args.output_policy;
57
+ return typeof value === "string" && BASH_OUTPUT_POLICIES.has(value as BashOutputPolicy)
58
+ ? (value as BashOutputPolicy)
59
+ : null;
60
+ }
61
+
62
+ function resolveBashResultDisposition(
63
+ args: Record<string, unknown>,
64
+ declared: ToolResultDisposition | undefined = BASH_DEFAULT_RESULT_DISPOSITION,
65
+ ): ToolResultDisposition {
66
+ const policy = bashOutputPolicy(args) ?? "bounded";
67
+ const base = declared ?? BASH_DEFAULT_RESULT_DISPOSITION;
68
+ return { presentation: base.presentation, context: bashContextDisposition(policy) };
69
+ }
70
+
71
+ function normalizedBashDisposition(policy: BashOutputPolicy): NormalizedToolResultDisposition {
72
+ return {
73
+ presentation: { ...BASH_DEFAULT_RESULT_DISPOSITION.presentation, maxBytes: BASH_DISPLAY_MAX_BYTES },
74
+ context: bashContextDisposition(policy),
75
+ };
22
76
  }
23
77
 
24
78
  /** Bound a cumulative live snapshot without writing an offload file per tick. */
25
- function shapeBashProgress(rawOutput: string): ToolResult {
26
- const truncation = truncateTail(rawOutput, {
27
- maxLines: DEFAULT_MAX_LINES,
28
- maxBytes: BASH_DISPLAY_MAX_BYTES - BASH_TAIL_NOTE_RESERVE,
29
- });
30
- const bytes = Buffer.byteLength(rawOutput, "utf8");
79
+ function shapeBashProgress(rawOutput: string, outputBytes: number, policy: BashOutputPolicy): ToolResult {
80
+ const disposition = normalizedBashDisposition(policy);
81
+ let output: string;
82
+ if (policy === "metadata-only") {
83
+ output = `bash: running; captured ${outputBytes} bytes; stdout/stderr omitted by metadata-only policy`;
84
+ } else if (policy === "summary") {
85
+ output = deterministicDiagnosticSummary(rawOutput, BASH_DISPLAY_MAX_BYTES, true).text;
86
+ } else {
87
+ output = truncateTail(rawOutput, {
88
+ maxLines: DEFAULT_MAX_LINES,
89
+ maxBytes: disposition.context.maxBytes,
90
+ }).content;
91
+ }
31
92
  return {
32
93
  kind: "ok",
33
- output: truncation.content,
94
+ output,
34
95
  details: {
35
96
  resultSize: {
36
- bytes,
37
- shownBytes: truncation.outputBytes,
97
+ bytes: outputBytes,
98
+ shownBytes: byteLength(output),
38
99
  maxBytes: BASH_DISPLAY_MAX_BYTES,
39
- truncated: truncation.truncated,
40
- policy: "tail",
100
+ truncated: byteLength(rawOutput) > byteLength(output),
101
+ policy,
41
102
  },
42
103
  },
43
104
  };
44
105
  }
45
106
 
46
- // Tail-truncate the combined output for display, spilling the full output to a
47
- // scratch file first when it overflows the display cap. Setting
48
- // `details.resultSize.offloadPath` tells the registry result-shaper to leave
49
- // this already-shaped (tail-biased) output alone instead of re-truncating it
50
- // head-first.
51
- function shapeBashOutput(
107
+ function bashResultDetails(
108
+ result: Awaited<ReturnType<typeof runBashCommand>>,
52
109
  rawOutput: string,
53
- context: Pick<ToolInvokeOptions, "sessionId" | "toolCallId"> | undefined,
54
- ): ShapedBashOutput {
55
- const truncation = truncateTail(rawOutput, {
56
- maxLines: DEFAULT_MAX_LINES,
57
- maxBytes: BASH_DISPLAY_MAX_BYTES - BASH_TAIL_NOTE_RESERVE,
58
- });
59
- if (!truncation.truncated) return { text: rawOutput };
60
-
61
- const totalBytes = Buffer.byteLength(rawOutput, "utf8");
62
- const offloadPath = writeToolOffload(rawOutput, context, BASH_HARD_CAP_BYTES);
63
- const startLine = truncation.totalLines - truncation.outputLines + 1;
64
- const scope = truncation.lastLinePartial
65
- ? `last ${formatSize(truncation.outputBytes)} of line ${truncation.totalLines} (line is large)`
66
- : `lines ${startLine}-${truncation.totalLines} of ${truncation.totalLines}`;
67
- const location = offloadPath !== null ? ` Full output saved to ${offloadPath}; read it with offset/limit.` : "";
68
- const note = `[Output tail-truncated: showing ${scope} (${formatSize(BASH_DISPLAY_MAX_BYTES)} display limit).${location}]`;
69
- const details: ToolResultDetails = {
70
- resultSize: {
71
- bytes: totalBytes,
72
- shownBytes: truncation.outputBytes,
73
- maxBytes: BASH_DISPLAY_MAX_BYTES,
74
- truncated: true,
75
- policy: "tail",
76
- followUpHint: "Read the full-output offload file with offset/limit, or re-run a narrower command.",
77
- ...(offloadPath !== null ? { offloadPath } : {}),
78
- },
110
+ outcome: "success" | "nonzero" | "timeout" | "abort" | "output-cap",
111
+ ): ToolResultDetails {
112
+ return {
113
+ outcome,
114
+ exitCode: result.exitCode,
115
+ signal: result.signal,
116
+ timedOut: result.timedOut,
117
+ aborted: result.aborted,
118
+ outputCapped: result.outputCapped,
119
+ outputBytes: result.outputBytes,
120
+ retainedBytes: byteLength(rawOutput),
121
+ stdoutBytes: byteLength(result.stdout),
122
+ stderrBytes: byteLength(result.stderr),
79
123
  };
80
- return { text: `${truncation.content}\n\n${note}`, details };
81
- }
82
-
83
- function withDetails(base: ToolResult, details: ToolResultDetails | undefined): ToolResult {
84
- if (details === undefined) return base;
85
- if (base.kind === "ok") return { ...base, details };
86
- return { ...base, details };
87
124
  }
88
125
 
89
126
  // A bash command whose first word is a plain file/dir observer has a
@@ -120,7 +157,7 @@ function observeToolsNudge(command: string, sessionId: string | undefined): stri
120
157
  export const bashTool: ToolSpec = {
121
158
  name: ToolNames.Bash,
122
159
  description:
123
- "Execute a bash command and return stdout and stderr. Runs inside the session workspace. Default timeout 300000 ms.",
160
+ "Execute a bash command inside the session workspace. output_policy controls model context: bounded keeps the diagnostic tail and is the default; summary keeps deterministic redacted diagnostics; metadata-only keeps facts and retrieval; full is admitted only within the result budget.",
124
161
  parameters: Type.Object({
125
162
  command: Type.String({ description: "Bash command to execute." }),
126
163
  cwd: Type.Optional(
@@ -130,13 +167,30 @@ export const bashTool: ToolSpec = {
130
167
  }),
131
168
  ),
132
169
  timeout_ms: Type.Optional(Type.Number({ description: "Timeout in milliseconds." })),
170
+ output_policy: Type.Optional(
171
+ Type.Union([Type.Literal("full"), Type.Literal("bounded"), Type.Literal("summary"), Type.Literal("metadata-only")], {
172
+ description:
173
+ "Model-context disposition. Omit for bounded tail output. Use summary for noisy runs, metadata-only when only status and retrieval matter, and full only for known-small output.",
174
+ }),
175
+ ),
133
176
  }),
134
177
  baseActionClass: "execute",
135
178
  executionMode: "sequential",
179
+ prepareArguments: normalizeBashArguments,
180
+ resolveResultDisposition: resolveBashResultDisposition,
136
181
  async run(args, options): Promise<ToolResult> {
182
+ args = normalizeBashArguments(args);
137
183
  if (typeof args.command !== "string" || args.command.length === 0) {
138
184
  return { kind: "error", message: "bash: missing command argument" };
139
185
  }
186
+ const outputPolicy = bashOutputPolicy(args);
187
+ if (outputPolicy === null) {
188
+ return {
189
+ kind: "error",
190
+ message: "bash: output_policy must be one of full, bounded, summary, or metadata-only (omit it for bounded)",
191
+ details: { outcome: "invalid-arguments", exitCode: null },
192
+ };
193
+ }
140
194
  const cwdArg = typeof args.cwd === "string" && args.cwd.length > 0 ? args.cwd : undefined;
141
195
  // Pin the child's cwd inside the session workspace in the tool itself.
142
196
  // The safety net blocks escaping cwd arguments at admission; this is the
@@ -161,41 +215,49 @@ export const bashTool: ToolSpec = {
161
215
  };
162
216
  if (options?.onUpdate !== undefined) {
163
217
  runOptions.onUpdate = (progress) => {
164
- options.onUpdate?.(shapeBashProgress(combineBashOutput(progress)));
218
+ options.onUpdate?.(shapeBashProgress(combineBashOutput(progress), progress.outputBytes, outputPolicy));
165
219
  };
166
220
  }
167
221
  const result = await runBashCommand(args.command, runOptions);
168
222
  const { error, aborted, timedOut, outputCapped } = result;
223
+ const rawOutput = combineBashOutput(result);
224
+ const output = rawOutput.trim();
169
225
  if (aborted) {
170
- return { kind: "error", message: "bash: command aborted" };
226
+ const status = "bash: command aborted";
227
+ return {
228
+ kind: "error",
229
+ message: output.length > 0 ? `${output}\n\n${status}` : status,
230
+ details: bashResultDetails(result, rawOutput, "abort"),
231
+ };
171
232
  }
172
- const shaped = shapeBashOutput(combineBashOutput(result), options);
173
- const output = shaped.text.trim();
174
233
  if (timedOut) {
175
234
  const status = `bash: command timed out after ${timeout}ms`;
176
- return withDetails(
177
- { kind: "error", message: output.length > 0 ? `${output}\n\n${status}` : status },
178
- shaped.details,
179
- );
235
+ return {
236
+ kind: "error",
237
+ message: output.length > 0 ? `${output}\n\n${status}` : status,
238
+ details: bashResultDetails(result, rawOutput, "timeout"),
239
+ };
180
240
  }
181
241
  if (outputCapped) {
182
242
  const status = `bash: command output exceeded ${BASH_HARD_CAP_BYTES} bytes and was stopped`;
183
- return withDetails(
184
- { kind: "error", message: output.length > 0 ? `${output}\n\n${status}` : status },
185
- shaped.details,
186
- );
243
+ return {
244
+ kind: "error",
245
+ message: output.length > 0 ? `${output}\n\n${status}` : status,
246
+ details: bashResultDetails(result, rawOutput, "output-cap"),
247
+ };
187
248
  }
188
249
  if (error) {
189
250
  const code = typeof error.code === "number" ? error.code : (error as { code?: string }).code;
190
251
  const status = `bash: command failed (exit ${code ?? "?"})`;
191
252
  const message = output.length > 0 ? `${output}\n\n${status}` : `${status}: ${error.message}`;
192
- return withDetails({ kind: "error", message }, shaped.details);
253
+ return { kind: "error", message, details: bashResultDetails(result, rawOutput, "nonzero") };
193
254
  }
194
- const body = shaped.text.length > 0 ? shaped.text : "(no output)";
195
- return withDetails(
196
- { kind: "ok", output: `${body}${observeToolsNudge(args.command, options?.sessionId)}` },
197
- shaped.details,
198
- );
255
+ const body = rawOutput.length > 0 ? rawOutput : "(no output)";
256
+ return {
257
+ kind: "ok",
258
+ output: `${body}${observeToolsNudge(args.command, options?.sessionId)}`,
259
+ details: bashResultDetails(result, rawOutput, "success"),
260
+ };
199
261
  } catch (err) {
200
262
  const msg = err instanceof Error ? err.message : String(err);
201
263
  return { kind: "error", message: `bash: ${msg}` };
@@ -1,5 +1,8 @@
1
+ import { BASH_HARD_CAP_BYTES } from "../core/bash-exec.js";
1
2
  import { type ToolName, ToolNames } from "../core/tool-names.js";
3
+ import { BASH_DEFAULT_RESULT_DISPOSITION } from "./bash.js";
2
4
  import { OBSERVATION_POLICY_SLACK_BYTES, OBSERVE_SELF_CAPS } from "./observation.js";
5
+ import { toolPresentationPolicy } from "./presentation.js";
3
6
  import { readMaxBytes } from "./read.js";
4
7
  import type { ToolMetadata, ToolSourceInfo, ToolSpec } from "./registry.js";
5
8
 
@@ -92,7 +95,7 @@ const TOOL_METADATA: Readonly<Record<string, ToolMetadata>> = {
92
95
  ),
93
96
  costLatency: "local_fast",
94
97
  promptHint:
95
- 'Call context with scope="skills" to list installed and marketplace skills; when one matches the task, or the operator names a skill or asks how one works, suggest the operator run /skill <name> (a marketplace skill is offered for install) and never load it uninvited. When the user message carries a skill request, first load that skill via context (scope="skills", name=<skill>) before doing anything else.',
98
+ 'Call context with scope="skills" to list installed and marketplace skills; when one matches the task, or the operator names a skill or asks how one works, suggest the operator run /skill <name> (a marketplace skill is offered for install) and never load it uninvited. When the user message carries a skill request, first load that skill via context (scope="skills", name=<skill>) before doing anything else. When an [evicted ...] marker names content you need, recall it with context(scope="recall", ref=...); re-read the file only when the marker says it changed.',
96
99
  },
97
100
  [ToolNames.CredentialPresent]: {
98
101
  objective: "Check whether a credential key is present without returning its value.",
@@ -120,15 +123,19 @@ const TOOL_METADATA: Readonly<Record<string, ToolMetadata>> = {
120
123
  resultSizePolicy: exactMutationPolicy,
121
124
  costLatency: "local_fast",
122
125
  },
123
- // EXECUTE: the safe-exec spine (bash offloads its own overflow).
126
+ // EXECUTE: the safe-exec spine (canonical shaping offloads Bash overflow).
124
127
  [ToolNames.Bash]: {
125
128
  objective: "Execute an explicit shell command when narrower tools are insufficient.",
126
129
  uiLabel: "Shell",
127
130
  retrySafety: "unknown",
128
- resultSizePolicy: summaryPolicy(
129
- "Use a narrower command or a dedicated verification/read/search tool to inspect omitted output.",
130
- ),
131
+ resultSizePolicy: {
132
+ ...summaryPolicy("Use a narrower command or a dedicated verification/read/search tool to inspect omitted output."),
133
+ offloadMaxBytes: BASH_HARD_CAP_BYTES,
134
+ },
135
+ resultDisposition: BASH_DEFAULT_RESULT_DISPOSITION,
131
136
  costLatency: "local_slow",
137
+ promptHint:
138
+ "Bash output_policy defaults to bounded diagnostic tail. Use summary for noisy commands, metadata-only when only outcome and retrieval matter, and full only when output is known to fit the bounded result budget.",
132
139
  },
133
140
  [ToolNames.Git]: {
134
141
  objective: "Read-only git inspection: status, diff, or log.",
@@ -255,9 +262,14 @@ export function toolPromptHintsForNames(names: ReadonlyArray<ToolName>): Readonl
255
262
  return hints;
256
263
  }
257
264
 
265
+ // Every builtin carries its transcript presentation on its metadata, resolved
266
+ // from the one declaration table in presentation.ts, so the registry and the
267
+ // live panel (which has no registry) answer the fold question identically.
258
268
  function withBuiltinMetadata<T extends ToolSpec>(spec: T): T {
259
269
  const metadata = TOOL_METADATA[spec.name];
260
- return metadata ? withMetadata(spec, metadata) : spec;
270
+ return metadata
271
+ ? withMetadata(spec, { ...metadata, presentation: toolPresentationPolicy(spec.name, undefined) })
272
+ : spec;
261
273
  }
262
274
 
263
275
  export function builtin<T extends ToolSpec>(spec: T, sourceInfo: ToolSourceInfo): T {
@@ -1,7 +1,15 @@
1
1
  import { type Dirent, readdirSync } from "node:fs";
2
2
  import path from "node:path";
3
+ import type { ContextRecalledPayload } from "../../core/bus-events.js";
3
4
  import { SKILL_SUGGESTION_ANCHOR } from "../../core/skill-activation.js";
4
5
  import { ToolNames } from "../../core/tool-names.js";
6
+ import { foldWorkingSet } from "../../domains/context/working-set/fold.js";
7
+ import {
8
+ buildRecallFields,
9
+ recallErrorMessage,
10
+ recallParentTurnId,
11
+ resolveRecall,
12
+ } from "../../domains/context/working-set/recall.js";
5
13
  import {
6
14
  checkSkillDrift,
7
15
  discoverMarketplaceSkills,
@@ -11,6 +19,8 @@ import {
11
19
  modelVisibleSkills,
12
20
  type Skill,
13
21
  } from "../../domains/resources/index.js";
22
+ import type { SessionEntryInput } from "../../domains/session/contract.js";
23
+ import type { SessionEntry } from "../../domains/session/entries.js";
14
24
  import type { WorkspaceSnapshot } from "../../domains/session/workspace/index.js";
15
25
  import {
16
26
  finalizeObservation,
@@ -30,7 +40,8 @@ import { contextToolSurface } from "./surface.js";
30
40
  * workspace snapshot, scope=docs retrieves cited sections from Clio's bundled
31
41
  * documentation, scope=skills lists available skills or loads a requested
32
42
  * skill body (the skill-activation and pending-request contracts are
33
- * unchanged from the absorbed read_skill tool).
43
+ * unchanged from the absorbed read_skill tool), scope=recall readmits an
44
+ * evicted tool-result body by ref and records the `contextRecall` entry.
34
45
  */
35
46
 
36
47
  const DEFAULT_TREE_ENTRIES = 50;
@@ -42,6 +53,17 @@ export interface ContextWorkspaceDeps {
42
53
  saveSnapshot(snapshot: WorkspaceSnapshot): void;
43
54
  }
44
55
 
56
+ /** Ledger access for scope=recall: read the full ledger, fold it at the live leaf, append the recall record. */
57
+ export interface ContextSessionDeps {
58
+ hasSession(): boolean;
59
+ readEntries(): ReadonlyArray<SessionEntry>;
60
+ /** The live append point (`/tree` pin or tree leaf); undefined lets the fold infer it. */
61
+ activeLeafTurnId(): string | undefined;
62
+ appendEntry(entry: SessionEntryInput): SessionEntry;
63
+ /** Called after the recall entry is recorded; the orchestrator publishes it as BusChannels.ContextRecalled. */
64
+ onRecalled?: (payload: ContextRecalledPayload) => void;
65
+ }
66
+
45
67
  export interface ContextToolDeps {
46
68
  getCwd?: () => string;
47
69
  getSkillLoaderOptions?: () => Pick<
@@ -50,6 +72,8 @@ export interface ContextToolDeps {
50
72
  >;
51
73
  /** Absent in worker registries without a session; scope=workspace errors cleanly. */
52
74
  workspace?: ContextWorkspaceDeps;
75
+ /** Absent in worker registries without a session; scope=recall errors cleanly. */
76
+ session?: ContextSessionDeps;
53
77
  /**
54
78
  * Whether scope=skills may list marketplace entries beside installed
55
79
  * skills. Worker registries set false: a worker can neither install a
@@ -482,13 +506,90 @@ function runSkillsScope(
482
506
  });
483
507
  }
484
508
 
509
+ /**
510
+ * scope=recall: the body goes back through the observation envelope like any
511
+ * OBSERVE result, so the per-turn pool and the self cap still apply; an
512
+ * oversize body is offloaded by the envelope and the notice carries the
513
+ * pointer. A body whose original result was itself offloaded already ends in
514
+ * that tool's own `full: <path>` pointer, which is what the model gets back;
515
+ * the file is never inlined. The `contextRecall` entry is appended before the
516
+ * result returns; it is the churn record, not an un-eviction, so the marker
517
+ * and the prefix cache stay where they are.
518
+ */
519
+ function runRecallScope(
520
+ deps: ContextToolDeps,
521
+ args: Record<string, unknown>,
522
+ reservation: ObservationReservation,
523
+ options: ToolInvokeOptions | undefined,
524
+ ): ToolResult {
525
+ const session = deps.session;
526
+ if (!session?.hasSession()) {
527
+ return { kind: "error", message: "context: recall scope requires a bound session; none is active here" };
528
+ }
529
+ const ref = typeof args.ref === "string" ? args.ref.trim() : "";
530
+ if (ref.length === 0) {
531
+ return {
532
+ kind: "error",
533
+ message: "context: recall scope requires ref=<turnId>, the ref named in the [evicted ...] marker",
534
+ };
535
+ }
536
+ const entries = session.readEntries();
537
+ const leaf = session.activeLeafTurnId();
538
+ const view = foldWorkingSet(entries, leaf);
539
+ const resolved = resolveRecall(entries, view, ref, leaf);
540
+ if (!resolved.ok) return { kind: "error", message: `context: ${recallErrorMessage(resolved.error, entries, view)}` };
541
+ const { result } = resolved;
542
+ const fields = buildRecallFields(result, {
543
+ trigger: "tool",
544
+ ...(options?.toolCallId ? { toolCallId: options.toolCallId } : {}),
545
+ });
546
+ // The recall record parents onto the live leaf so the fold sees it on
547
+ // this branch and only this branch.
548
+ const parentTurnId = recallParentTurnId(entries, leaf);
549
+ let recorded: SessionEntry;
550
+ try {
551
+ recorded = session.appendEntry({ ...fields, parentTurnId });
552
+ } catch (err) {
553
+ return {
554
+ kind: "error",
555
+ message: `context: recall of ${result.ref.entry} could not be recorded: ${err instanceof Error ? err.message : String(err)}`,
556
+ };
557
+ }
558
+ session.onRecalled?.({ ref: result.ref.entry, trigger: "tool", tokensReadmitted: result.tokens, at: Date.now() });
559
+ const evictedState = view.evicted.get(result.ref.entry);
560
+ const truncation = truncateHead(result.body, {
561
+ maxBytes: reservation.callCapBytes,
562
+ maxLines: Number.MAX_SAFE_INTEGER,
563
+ });
564
+ return finalizeObservation({
565
+ tool: ToolNames.Context,
566
+ unit: "results",
567
+ output: truncation.content,
568
+ ...(truncation.truncated ? { fullOutput: result.body } : {}),
569
+ shownCount: 1,
570
+ totalCount: 1,
571
+ truncated: truncation.truncated,
572
+ details: {
573
+ recall: {
574
+ ref: result.ref.entry,
575
+ tokensReadmitted: result.tokens,
576
+ recallTurnId: recorded.turnId,
577
+ ...(evictedState ? { reason: evictedState.reason, evictedAtTurnId: evictedState.evictedAtTurnId } : {}),
578
+ ...(result.offloadPath !== undefined ? { offloadPath: result.offloadPath } : {}),
579
+ },
580
+ },
581
+ reservation,
582
+ ...(options ? { options } : {}),
583
+ });
584
+ }
585
+
485
586
  export function createContextTool(deps: ContextToolDeps = {}): ToolSpec {
486
587
  return {
487
588
  ...contextToolSurface,
488
589
  async run(args, options): Promise<ToolResult> {
489
590
  const scope = typeof args.scope === "string" ? args.scope : "";
490
- if (scope !== "workspace" && scope !== "docs" && scope !== "skills") {
491
- return { kind: "error", message: `context: scope must be workspace, docs, or skills; got '${scope}'` };
591
+ if (scope !== "workspace" && scope !== "docs" && scope !== "skills" && scope !== "recall") {
592
+ return { kind: "error", message: `context: scope must be workspace, docs, skills, or recall; got '${scope}'` };
492
593
  }
493
594
  const selfCap =
494
595
  scope === "docs"
@@ -508,6 +609,7 @@ export function createContextTool(deps: ContextToolDeps = {}): ToolSpec {
508
609
  }
509
610
  if (scope === "workspace") return runWorkspaceScope(deps, reservation, options);
510
611
  if (scope === "docs") return runDocsScope(args, reservation, options);
612
+ if (scope === "recall") return runRecallScope(deps, args, reservation, options);
511
613
  return runSkillsScope(deps, args, reservation, options);
512
614
  },
513
615
  };
@@ -6,12 +6,13 @@ import type { ToolSurface } from "../lazy-tool.js";
6
6
  export const contextToolSurface = {
7
7
  name: ToolNames.Context,
8
8
  description:
9
- "Environment context: scope=workspace returns the git/project snapshot, scope=docs searches Clio's bundled documentation (omit query to list the corpus), scope=skills lists installed and marketplace skills or loads an installed one by name. For repository code and the repo's generated wiki use code_nav (mode=wiki).",
9
+ "Environment context: scope=workspace returns the git/project snapshot, scope=docs searches Clio's bundled documentation (omit query to list the corpus), scope=skills lists installed and marketplace skills or loads an installed one by name, scope=recall returns the exact body of an evicted tool result by ref (the turnId named in an [evicted ...] marker). For repository code and the repo's generated wiki use code_nav (mode=wiki).",
10
10
  parameters: Type.Object({
11
- scope: StringEnum(["workspace", "docs", "skills"], { description: "Context source." }),
11
+ scope: StringEnum(["workspace", "docs", "skills", "recall"], { description: "Context source." }),
12
12
  query: Type.Optional(Type.String({ description: "scope=docs: question or terms; omit to list the corpus." })),
13
13
  name: Type.Optional(Type.String({ description: "scope=skills: skill name to load; omit to list." })),
14
14
  limit: Type.Optional(Type.Number({ description: "scope=docs: max sections (default 5, max 12)." })),
15
+ ref: Type.Optional(Type.String({ description: "scope=recall: ref of the evicted item, as named in its marker." })),
15
16
  include_tree: Type.Optional(Type.Boolean({ description: "scope=skills: list files under the skill base_dir." })),
16
17
  }),
17
18
  baseActionClass: "read",
@@ -1,5 +1,7 @@
1
+ import type { ContextRecalledPayload } from "../core/bus-events.js";
1
2
  import type { LoadSkillsInput } from "../domains/resources/index.js";
2
3
  import type { SessionContract } from "../domains/session/contract.js";
4
+ import type { SessionEntry } from "../domains/session/entries.js";
3
5
  import { createTaskBoardStore, type TaskBoardStore } from "../domains/session/task-board.js";
4
6
  import type { UserTasksStore } from "../domains/user-tasks/store.js";
5
7
  import type { AgentLedgerPort } from "../worker/protocol.js";
@@ -28,6 +30,10 @@ import { writeTool } from "./write.js";
28
30
 
29
31
  export interface CoreToolBootstrapDeps {
30
32
  session?: SessionContract;
33
+ /** Full ledger of the current session; context(scope=recall) folds it. Absent in worker registries. */
34
+ readSessionEntries?: () => ReadonlyArray<SessionEntry>;
35
+ /** Publishes a successful context(scope=recall) on the bus; absent where no bus is wired. */
36
+ onContextRecalled?: (payload: ContextRecalledPayload) => void;
31
37
  askUser?: AskUserHandler;
32
38
  taskBoard?: TaskBoardStore;
33
39
  userTasks?: UserTasksStore;
@@ -108,6 +114,7 @@ export function registerCoreTools(registry: ToolRegistry, deps: CoreToolBootstra
108
114
  ...builtin(credentialPresentTool, { path: "src/tools/credential-present.ts", scope: "core" }),
109
115
  });
110
116
  const session = deps.session;
117
+ const readSessionEntries = deps.readSessionEntries;
111
118
  registry.register({
112
119
  ...builtin(
113
120
  lazyTool(contextToolSurface, async () => {
@@ -116,6 +123,20 @@ export function registerCoreTools(registry: ToolRegistry, deps: CoreToolBootstra
116
123
  const { probeWorkspace } = await import("../domains/session/workspace/index.js");
117
124
  return createContextTool({
118
125
  ...skillToolDeps,
126
+ ...(readSessionEntries
127
+ ? {
128
+ session: {
129
+ hasSession: () => session.current() !== null,
130
+ readEntries: readSessionEntries,
131
+ activeLeafTurnId: () => {
132
+ const meta = session.current();
133
+ return meta ? (session.tree(meta.id).leafId ?? undefined) : undefined;
134
+ },
135
+ appendEntry: (entry) => session.appendEntry(entry),
136
+ ...(deps.onContextRecalled ? { onRecalled: deps.onContextRecalled } : {}),
137
+ },
138
+ }
139
+ : {}),
119
140
  workspace: {
120
141
  hasSession: () => session.current() !== null,
121
142
  getSnapshot: () => session.current()?.workspace ?? null,