@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
@@ -1,18 +1,20 @@
1
1
  # Clio test harness reference
2
2
 
3
- How to drive the real Clio binary, a mock provider, and the ACP surface in
4
- tests. All of this is non-interactive there is no pty harness in v0.2.2.
3
+ How to drive the real Clio binary, a mock provider, the ACP surface, and a
4
+ real pseudo-terminal in tests. Every model here is a stub; these are machinery
5
+ tests. A run against a real model is `benchmarks/internal/SKILL.md`.
5
6
 
6
7
  ## Contents
7
8
  - The spawn harness (`runCli`, `makeScratchHome`)
8
9
  - Mocking a provider (OpenAI-compatible SSE fixture)
9
10
  - ACP over JSON-RPC/stdio
11
+ - The PTY (`openPty`, `runInPty`)
10
12
  - One-off probes (no test file)
11
13
 
12
14
  ## The spawn harness
13
15
 
14
- `tests/harness/spawn.ts` is the only harness. It spawns `node dist/cli/index.js`,
15
- so **build first** (or keep `npm run dev` running) before `test:smoke`.
16
+ `tests/harness/spawn.ts` spawns `node dist/cli/index.js` with piped stdio, so
17
+ **build first** (or keep `npm run dev` running) before running smoke.
16
18
 
17
19
  ```ts
18
20
  import { makeScratchHome, runCli } from "../harness/spawn.js";
@@ -76,11 +78,38 @@ client (see `createJsonRpcProcessClient` in the smoke test): `initialize` →
76
78
  non-spec discriminator breaks strict clients like Zed, so the smoke test asserts
77
79
  every emitted variant is in the v1 set.
78
80
 
81
+ ## The PTY
82
+
83
+ Piped stdio reports no terminal width and no TTY, so the TUI refuses to start
84
+ and every width-sensitive path collapses to 80 columns. `tests/harness/pty.ts`
85
+ opens a real pseudo-terminal through `node-pty` (a devDependency, never
86
+ shipped):
87
+
88
+ ```ts
89
+ import { openPty, runInPty, stripAnsi, visibleLines } from "../harness/pty.js";
90
+
91
+ // Scripted: type on a schedule, stop when the output matches, bounded by a timeout.
92
+ const run = await runInPty(process.execPath, [CLI], { cols: 120, rows: 40, cwd, env,
93
+ readyWhen: /ctx /, input: [{ afterMs: 200, data: "/quit\r" }], until: /bye/, timeoutMs: 20_000 });
94
+
95
+ // Controllable: write, resize, pause output, wait for a matcher, wait for exit.
96
+ const session = await openPty(process.execPath, [CLI], { cols: 140, rows: 44, cwd, env });
97
+ await session.waitForOutput((out) => /ctx /.test(stripAnsi(out)), 30_000);
98
+ session.write("/quit\r");
99
+ await session.waitForExit(10_000);
100
+ ```
101
+
102
+ Use it only for what a pipe cannot show: width, raw mode, SIGINT through a
103
+ terminal, the alternate-screen and keyboard-protocol teardown. The three
104
+ suites that need it are `tests/smoke/tui-width-matrix.test.ts`,
105
+ `instant-shell-pty.test.ts`, and `render-trace-pty.test.ts`. Anything else
106
+ belongs on `runCli`.
107
+
79
108
  ## One-off probes (no test file)
80
109
 
81
110
  To poke at Clio without writing a permanent test, drop a throwaway script in
82
- `/tmp` and run it with tsx. Delete it when done — never leave probes under
83
- `tests/` or `scripts/`.
111
+ your scratch directory and run it with tsx. Delete it when done — never leave
112
+ probes under `tests/`, `scripts/`, or `benchmarks/`.
84
113
 
85
114
  ```ts
86
115
  // /tmp/probe.ts
@@ -1,4 +1,4 @@
1
- # Where Clio's tests live (v0.2.2)
1
+ # Where Clio's tests live
2
2
 
3
3
  Three layers under `tests/`. Add a new test next to the closest existing file;
4
4
  create a new file only for a genuinely new domain cluster.
@@ -7,10 +7,17 @@ create a new file only for a genuinely new domain cluster.
7
7
 
8
8
  | Layer | Path | Runner | Build needed |
9
9
  |---|---|---|---|
10
- | contracts | `tests/contracts/*.test.ts` | `node --import tsx --test` | no (imports `src`) |
11
- | smoke | `tests/smoke/*.test.ts` | `node --import tsx --test` | **yes** (spawns `dist/`) |
12
- | boundaries | `tests/boundaries/*.test.ts` | `node --import tsx --test` | no |
13
- | harness (not a test) | `tests/harness/spawn.ts` | imported by smoke | — |
10
+ | contracts | `tests/contracts/*.test.ts` | `npm run test:file -- <glob>` (tsx + scratch root) | no (imports `src`) |
11
+ | smoke | `tests/smoke/*.test.ts` | `npm run test:file -- <glob>` | **yes** (spawns `dist/`) |
12
+ | boundaries | `tests/boundaries/check-boundaries.ts` | `npm run lint` (hygiene) | no |
13
+ | harness (not tests) | `tests/harness/*.ts` | imported by contracts and smoke | — |
14
+
15
+ The harness modules: `spawn.ts` (run the built CLI with pipes), `scratch-env.ts`
16
+ (isolated Clio home), `pty.ts` (a real pseudo-terminal), `openai-compat-fixture.ts`
17
+ and `fake-lmstudio-server.ts` (stub providers), `fake-ssh.ts` (stub fleet node),
18
+ `clock.ts` (steppable clock), plus dispatch, receipt, and module-graph helpers.
19
+ Everything under `tests/` stubs the model. Real-model runs are
20
+ `benchmarks/internal/` and never run under `npm test`.
14
21
 
15
22
  ## Contract test files
16
23
 
@@ -33,18 +40,21 @@ create a new file only for a genuinely new domain cluster.
33
40
  | Area | File |
34
41
  |---|---|
35
42
  | non-interactive CLI + ACP-over-stdio end-to-end | `tests/smoke/cli.test.ts` |
36
- | import boundary rules (rule1/2/3) | `tests/boundaries/boundaries.test.ts` |
37
- | boundary checker implementation | `tests/boundaries/check-boundaries.ts` |
43
+ | the package as installed from `npm pack` | `tests/smoke/pack-install.test.ts` |
44
+ | TUI at real terminal sizes, NO_COLOR, Ctrl-C teardown (PTY) | `tests/smoke/tui-width-matrix.test.ts` |
45
+ | instant shell before hydration, SIGTERM through the lease (PTY) | `tests/smoke/instant-shell-pty.test.ts` |
46
+ | committed-frame render trace under PTY backpressure (PTY) | `tests/smoke/render-trace-pty.test.ts` |
47
+ | import boundary rules (rule1/2/3), run under `npm run lint` | `tests/boundaries/check-boundaries.ts` |
38
48
 
39
49
  ## Running a subset
40
50
 
41
51
  ```bash
42
52
  # all contracts
43
- node --import tsx --test 'tests/contracts/**/*.test.ts'
53
+ npm run test:file -- 'tests/contracts/**/*.test.ts'
44
54
  # one file
45
- node --import tsx --test tests/contracts/skills.test.ts
55
+ npm run test:file -- tests/contracts/skills.test.ts
46
56
  # only it.only / describe.only within a file
47
- node --import tsx --test --test-only tests/contracts/skills.test.ts
57
+ npm run test:file -- --test-only tests/contracts/skills.test.ts
48
58
  ```
49
59
 
50
60
  ## Writing tests
@@ -61,8 +61,8 @@ skills:
61
61
  sha256: 386ebfdac9379ebf5630fa7a9af259bb45f473f7d3d679b0a7ef4164f5fc4dea
62
62
  - name: clio-test
63
63
  path: meta/clio-test
64
- version: 0.1.3
65
- sha256: 99683ea7d7bf5bf57788e58ee6c66e01a4e30ec8d3d7d0a1f8b2b85caff4ae5a
64
+ version: 0.1.4
65
+ sha256: 90ca1fe7660cd8c3d835801883ebd55d2d17cb652b8585b28ddb82aec642336e
66
66
  - name: credentials
67
67
  path: meta/credentials
68
68
  version: 0.1.2
@@ -109,7 +109,7 @@
109
109
  "name": "clio-test",
110
110
  "description": "Use when writing or modifying Clio Coder's own source under src/, or verifying a change end-to-end against the real test harness. Covers the three real layers (contracts / smoke / boundaries), choosing which to run for a given change, the mock-provider and ACP-over-stdio harness, and the hot-reload dev loop for picking up latest code. Activate on any src/ edit, before declaring a change verified, or when asked whether Clio still works.",
111
111
  "sourceUrl": "https://github.com/iowarp/clio-coder/tree/main/skills/meta/clio-test",
112
- "version": "0.1.3",
112
+ "version": "0.1.4",
113
113
  "audit": "pass",
114
114
  "category": "meta"
115
115
  },
@@ -0,0 +1,513 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { assertSafeId } from "../core/safe-id.js";
5
+ import { clioStatePath } from "../core/xdg.js";
6
+ import { DEFAULT_WORKING_SET_SETTINGS } from "../domains/context/working-set/defaults.js";
7
+ import { foldWorkingSet } from "../domains/context/working-set/fold.js";
8
+ import { buildPathIndex } from "../domains/context/working-set/path-index.js";
9
+ import { resolveWorkingSetPolicy } from "../domains/context/working-set/policies/index.js";
10
+ import { makeOraclePolicy, makeRandomPolicy, nonePolicy } from "../domains/context/working-set/replay/controls.js";
11
+ import { loadClioTraces, type ReplayLoadCascade } from "../domains/context/working-set/replay/load-clio.js";
12
+ import { aggregateReplayMetrics, type ReplayMeasurement } from "../domains/context/working-set/replay/metrics.js";
13
+ import { buildReferenceGraph, type ReferenceGraph } from "../domains/context/working-set/replay/reference-graph.js";
14
+ import {
15
+ type ReplayPolicyResult,
16
+ type ReplayReportInput,
17
+ renderReplayJson,
18
+ renderReplayMarkdown,
19
+ } from "../domains/context/working-set/replay/report.js";
20
+ import { replayTrace } from "../domains/context/working-set/replay/runner.js";
21
+ import { generateSyntheticCorpora, SYNTHETIC_CORPORA } from "../domains/context/working-set/replay/synthetic.js";
22
+ import type { Trace } from "../domains/context/working-set/replay/trace.js";
23
+ import { parseSessionEntries } from "../domains/session/archive-readers.js";
24
+ import { DEFAULT_KEEP_RECENT_TOKENS } from "../domains/session/compaction/defaults.js";
25
+ import { filterEntriesToActivePath } from "../domains/session/tree/active-path.js";
26
+
27
+ const REPLAY_HELP = `Usage:
28
+ clio-coder context replay (--sessions <path>... | --synthetic <corpus>...) [options]
29
+
30
+ Options:
31
+ --sessions <path>... Clio session directories, sessions roots, or ledger JSONL files
32
+ --synthetic <ids> comma-separated procedural corpora: ${SYNTHETIC_CORPORA.map((spec) => spec.id).join(",")}
33
+ --policies <ids> comma-separated none,random,age-horizon,structural-v1,oracle
34
+ --budgets <tokens> comma-separated budgets (default: 16000,32000,64000)
35
+ --threshold <ratio> pressure threshold (default: 0.8)
36
+ --target <ratio> post-eviction pressure target (default: 0.6)
37
+ --protect-last-turns <n>
38
+ protected recent turns (default: ${DEFAULT_WORKING_SET_SETTINGS.protectLastTurns})
39
+ --min-evictable-tokens <n>
40
+ minimum tool-result body tokens (default: ${DEFAULT_WORKING_SET_SETTINGS.minEvictableTokens})
41
+ --seed <integer> deterministic random-policy seed (default: 0)
42
+ --no-filter include every readable transcript
43
+ --json <path> write the stable JSON report
44
+ --md <path> write Markdown instead of printing it
45
+ `;
46
+
47
+ const WORKING_SET_HELP = `Usage:
48
+ clio-coder context working-set --session <id|path>
49
+
50
+ Print the durable working-set fold and path-index summary for one Clio session.
51
+ `;
52
+
53
+ /**
54
+ * Conservative stand-in for replay's summary paraphrase. Five recorded local
55
+ * compactions price at 405, 405, 968, 981, and 1,124 tokens; 1,500 sits above
56
+ * that observed range. The replay README records the sample and sensitivity.
57
+ */
58
+ const REPLAY_SUMMARY_TOKENS = 1_500;
59
+
60
+ const POLICY_IDS = ["none", "random", "age-horizon", "structural-v1", "oracle"] as const;
61
+ type ReplayPolicyId = (typeof POLICY_IDS)[number];
62
+
63
+ class CliUsageError extends Error {}
64
+
65
+ interface ReplayArgs {
66
+ sessions: string[];
67
+ policies: ReplayPolicyId[];
68
+ budgets: number[];
69
+ threshold: number;
70
+ target: number;
71
+ protectLastTurns: number;
72
+ minEvictableTokens: number;
73
+ seed: number;
74
+ synthetic: string[];
75
+ noFilter: boolean;
76
+ jsonPath?: string;
77
+ markdownPath?: string;
78
+ }
79
+
80
+ function commaValues(value: string, flag: string): string[] {
81
+ const values = value
82
+ .split(",")
83
+ .map((entry) => entry.trim())
84
+ .filter((entry) => entry.length > 0);
85
+ if (values.length === 0) throw new CliUsageError(`${flag} requires at least one value`);
86
+ return [...new Set(values)];
87
+ }
88
+
89
+ function numberValue(value: string, flag: string): number {
90
+ const parsed = Number(value);
91
+ if (!Number.isFinite(parsed)) throw new CliUsageError(`${flag} requires a finite number`);
92
+ return parsed;
93
+ }
94
+
95
+ function requiredValue(args: ReadonlyArray<string>, index: number, flag: string): string {
96
+ const value = args[index + 1];
97
+ if (value === undefined || value.startsWith("--")) throw new CliUsageError(`${flag} requires a value`);
98
+ return value;
99
+ }
100
+
101
+ function policyResolves(id: ReplayPolicyId): boolean {
102
+ if (id === "none" || id === "random" || id === "oracle") return true;
103
+ try {
104
+ resolveWorkingSetPolicy(id);
105
+ return true;
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ function defaultPolicies(): ReplayPolicyId[] {
112
+ return POLICY_IDS.filter(policyResolves);
113
+ }
114
+
115
+ function parseReplayArgs(args: ReadonlyArray<string>): ReplayArgs {
116
+ const parsed: ReplayArgs = {
117
+ sessions: [],
118
+ policies: defaultPolicies(),
119
+ budgets: [16_000, 32_000, 64_000],
120
+ threshold: 0.8,
121
+ target: 0.6,
122
+ protectLastTurns: DEFAULT_WORKING_SET_SETTINGS.protectLastTurns,
123
+ minEvictableTokens: DEFAULT_WORKING_SET_SETTINGS.minEvictableTokens,
124
+ seed: 0,
125
+ synthetic: [],
126
+ noFilter: false,
127
+ };
128
+ let policiesExplicit = false;
129
+ for (let index = 0; index < args.length; index += 1) {
130
+ const arg = args[index];
131
+ if (arg === "--no-filter") {
132
+ parsed.noFilter = true;
133
+ continue;
134
+ }
135
+ if (arg === "--sessions") {
136
+ let consumed = 0;
137
+ while (index + 1 < args.length && !args[index + 1]?.startsWith("--")) {
138
+ parsed.sessions.push(args[index + 1] as string);
139
+ index += 1;
140
+ consumed += 1;
141
+ }
142
+ if (consumed === 0) throw new CliUsageError("--sessions requires at least one path");
143
+ continue;
144
+ }
145
+ if (
146
+ arg === "--policies" ||
147
+ arg === "--budgets" ||
148
+ arg === "--synthetic" ||
149
+ arg === "--threshold" ||
150
+ arg === "--target" ||
151
+ arg === "--protect-last-turns" ||
152
+ arg === "--min-evictable-tokens" ||
153
+ arg === "--seed"
154
+ ) {
155
+ const value = requiredValue(args, index, arg);
156
+ index += 1;
157
+ if (arg === "--policies") {
158
+ const values = commaValues(value, arg);
159
+ const unknown = values.filter((entry) => !(POLICY_IDS as ReadonlyArray<string>).includes(entry));
160
+ if (unknown.length > 0) throw new CliUsageError(`unknown replay policy: ${unknown.join(", ")}`);
161
+ parsed.policies = values as ReplayPolicyId[];
162
+ policiesExplicit = true;
163
+ } else if (arg === "--budgets") {
164
+ parsed.budgets = commaValues(value, arg).map((entry) => {
165
+ const budget = numberValue(entry, arg);
166
+ if (!Number.isInteger(budget) || budget <= 0) {
167
+ throw new CliUsageError("--budgets values must be positive integers");
168
+ }
169
+ return budget;
170
+ });
171
+ } else if (arg === "--synthetic") {
172
+ const values = commaValues(value, arg);
173
+ const known = SYNTHETIC_CORPORA.map((spec) => spec.id);
174
+ const unknown = values.filter((entry) => !known.includes(entry));
175
+ if (unknown.length > 0) throw new CliUsageError(`unknown synthetic corpus: ${unknown.join(", ")}`);
176
+ parsed.synthetic = values;
177
+ } else if (arg === "--threshold") {
178
+ parsed.threshold = numberValue(value, arg);
179
+ if (parsed.threshold <= 0 || parsed.threshold > 1) {
180
+ throw new CliUsageError("--threshold must be greater than 0 and at most 1");
181
+ }
182
+ } else if (arg === "--target") {
183
+ parsed.target = numberValue(value, arg);
184
+ if (parsed.target <= 0 || parsed.target >= 1) {
185
+ throw new CliUsageError("--target must be greater than 0 and less than 1");
186
+ }
187
+ } else if (arg === "--protect-last-turns") {
188
+ parsed.protectLastTurns = numberValue(value, arg);
189
+ if (!Number.isInteger(parsed.protectLastTurns) || parsed.protectLastTurns < 1) {
190
+ throw new CliUsageError("--protect-last-turns must be an integer at least 1");
191
+ }
192
+ } else if (arg === "--min-evictable-tokens") {
193
+ parsed.minEvictableTokens = numberValue(value, arg);
194
+ if (!Number.isInteger(parsed.minEvictableTokens) || parsed.minEvictableTokens < 0) {
195
+ throw new CliUsageError("--min-evictable-tokens must be a non-negative integer");
196
+ }
197
+ } else {
198
+ parsed.seed = numberValue(value, arg);
199
+ if (!Number.isInteger(parsed.seed)) throw new CliUsageError("--seed must be an integer");
200
+ }
201
+ continue;
202
+ }
203
+ if (arg === "--json" || arg === "--md") {
204
+ const value = requiredValue(args, index, arg);
205
+ index += 1;
206
+ if (arg === "--json") parsed.jsonPath = value;
207
+ else parsed.markdownPath = value;
208
+ continue;
209
+ }
210
+ throw new CliUsageError(`unknown flag ${arg}`);
211
+ }
212
+ if (parsed.sessions.length === 0 && parsed.synthetic.length === 0) {
213
+ throw new CliUsageError("--sessions or --synthetic is required");
214
+ }
215
+ if (parsed.target >= parsed.threshold) throw new CliUsageError("--target must be less than --threshold");
216
+ if (policiesExplicit) {
217
+ const unavailable = parsed.policies.filter((id) => !policyResolves(id));
218
+ if (unavailable.length > 0) {
219
+ throw new CliUsageError(`replay policy is not available in this build: ${unavailable.join(", ")}`);
220
+ }
221
+ }
222
+ return parsed;
223
+ }
224
+
225
+ function policyForTrace(id: ReplayPolicyId, graph: ReferenceGraph, seed: number) {
226
+ if (id === "none") return nonePolicy;
227
+ if (id === "random") return makeRandomPolicy(seed);
228
+ if (id === "oracle") return makeOraclePolicy(graph);
229
+ return resolveWorkingSetPolicy(id);
230
+ }
231
+
232
+ function gitSha(): string | null {
233
+ try {
234
+ return execFileSync("git", ["rev-parse", "HEAD"], {
235
+ cwd: process.cwd(),
236
+ encoding: "utf8",
237
+ stdio: ["ignore", "pipe", "ignore"],
238
+ }).trim();
239
+ } catch {
240
+ return null;
241
+ }
242
+ }
243
+
244
+ function exactCommandLine(): string[] {
245
+ return [process.execPath, ...process.execArgv, ...process.argv.slice(1)];
246
+ }
247
+
248
+ async function writeOutput(path: string, contents: string): Promise<string> {
249
+ const output = resolve(path);
250
+ await mkdir(dirname(output), { recursive: true });
251
+ await writeFile(output, contents, "utf8");
252
+ return output;
253
+ }
254
+
255
+ function cascadeLine(cascade: ReplayLoadCascade): string {
256
+ const filtered = Object.entries(cascade.filtered)
257
+ .map(([stage, count]) => `${stage}=${count}`)
258
+ .join(" ");
259
+ return `cascade found=${cascade.found} unreadable=${cascade.unreadable} ${filtered} kept=${cascade.kept}`;
260
+ }
261
+
262
+ export async function runContextReplayCommand(args: string[]): Promise<number> {
263
+ if (args.includes("--help") || args.includes("-h")) {
264
+ process.stdout.write(REPLAY_HELP);
265
+ return 0;
266
+ }
267
+ let parsed: ReplayArgs;
268
+ try {
269
+ parsed = parseReplayArgs(args);
270
+ } catch (error) {
271
+ process.stderr.write(
272
+ `clio-coder context replay: ${error instanceof Error ? error.message : String(error)}\n${REPLAY_HELP}`,
273
+ );
274
+ return 2;
275
+ }
276
+ try {
277
+ const ledgers = await loadClioTraces(parsed.sessions, { filter: parsed.noFilter ? false : {} });
278
+ const synthetic = generateSyntheticCorpora(parsed.synthetic);
279
+ const loaded = {
280
+ traces: [...ledgers.traces, ...synthetic.traces],
281
+ cascade: {
282
+ found: ledgers.cascade.found + synthetic.cascade.found,
283
+ unreadable: ledgers.cascade.unreadable,
284
+ filtered: ledgers.cascade.filtered,
285
+ kept: ledgers.cascade.kept + synthetic.cascade.kept,
286
+ },
287
+ };
288
+ if (loaded.traces.length === 0) {
289
+ process.stderr.write(`clio-coder context replay: no traces to replay (${cascadeLine(loaded.cascade)})\n`);
290
+ return 1;
291
+ }
292
+ const indexed = loaded.traces.map((trace) => {
293
+ const index = buildPathIndex(trace.entries, { cwd: trace.cwd });
294
+ return { trace, index, graph: buildReferenceGraph(trace, index) };
295
+ });
296
+ const settings = {
297
+ ...DEFAULT_WORKING_SET_SETTINGS,
298
+ target: parsed.target,
299
+ protectLastTurns: parsed.protectLastTurns,
300
+ minEvictableTokens: parsed.minEvictableTokens,
301
+ };
302
+ const results: ReplayPolicyResult[] = [];
303
+ for (const budgetTokens of parsed.budgets) {
304
+ for (const policyId of parsed.policies) {
305
+ const measurements: ReplayMeasurement[] = indexed.map(({ trace, index, graph }) => ({
306
+ trace,
307
+ index,
308
+ graph,
309
+ replay: replayTrace(trace, policyForTrace(policyId, graph, parsed.seed), {
310
+ policyId,
311
+ budgetTokens,
312
+ threshold: parsed.threshold,
313
+ target: parsed.target,
314
+ settings,
315
+ seed: parsed.seed,
316
+ summaries: { keepRecentTokens: DEFAULT_KEEP_RECENT_TOKENS, summaryTokens: REPLAY_SUMMARY_TOKENS },
317
+ }),
318
+ }));
319
+ results.push({ budgetTokens, policyId, metrics: aggregateReplayMetrics(measurements) });
320
+ }
321
+ }
322
+ const report: ReplayReportInput = {
323
+ config: {
324
+ policies: parsed.policies,
325
+ budgets: parsed.budgets,
326
+ threshold: parsed.threshold,
327
+ target: parsed.target,
328
+ seed: parsed.seed,
329
+ corpus: [...(parsed.sessions.length > 0 ? ["ledgers"] : []), ...parsed.synthetic],
330
+ filter: parsed.noFilter ? "none" : "default",
331
+ settings,
332
+ },
333
+ cascade: loaded.cascade,
334
+ results,
335
+ gitSha: gitSha(),
336
+ commandLine: exactCommandLine(),
337
+ };
338
+ const markdown = renderReplayMarkdown(report);
339
+ if (parsed.markdownPath === undefined) process.stdout.write(markdown);
340
+ else {
341
+ const path = await writeOutput(parsed.markdownPath, markdown);
342
+ process.stdout.write(`${cascadeLine(loaded.cascade)}\nmarkdown ${path}\n`);
343
+ }
344
+ if (parsed.jsonPath !== undefined) {
345
+ const path = await writeOutput(parsed.jsonPath, renderReplayJson(report));
346
+ process.stdout.write(`json ${path}\n`);
347
+ }
348
+ return 0;
349
+ } catch (error) {
350
+ process.stderr.write(`clio-coder context replay failed: ${error instanceof Error ? error.message : String(error)}\n`);
351
+ return 1;
352
+ }
353
+ }
354
+
355
+ async function sessionPath(value: string): Promise<string> {
356
+ const candidate = resolve(value);
357
+ try {
358
+ const facts = await stat(candidate);
359
+ if (facts.isDirectory()) return join(candidate, "current.jsonl");
360
+ if (facts.isFile()) return candidate;
361
+ } catch {
362
+ // A non-path value is resolved as a validated session id below.
363
+ }
364
+ try {
365
+ assertSafeId(value, "session");
366
+ } catch (error) {
367
+ throw new CliUsageError(error instanceof Error ? error.message : String(error));
368
+ }
369
+ const sessionsRoot = join(clioStatePath(), "sessions");
370
+ let cwdHashes: string[];
371
+ try {
372
+ cwdHashes = await readdir(sessionsRoot);
373
+ } catch {
374
+ throw new Error(`session not found: ${value}`);
375
+ }
376
+ for (const cwdHash of cwdHashes.sort()) {
377
+ const current = join(sessionsRoot, cwdHash, value, "current.jsonl");
378
+ try {
379
+ if ((await stat(current)).isFile()) return current;
380
+ } catch {
381
+ // Continue across repositories until the id is found.
382
+ }
383
+ }
384
+ throw new Error(`session not found: ${value}`);
385
+ }
386
+
387
+ async function pinnedLeafForSession(source: string): Promise<string | undefined> {
388
+ try {
389
+ const raw = await readFile(join(dirname(source), "meta.json"), "utf8");
390
+ const value = JSON.parse(raw) as unknown;
391
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
392
+ const pinned = (value as Record<string, unknown>).pinnedLeafTurnId;
393
+ return typeof pinned === "string" && pinned.length > 0 ? pinned : undefined;
394
+ } catch {
395
+ return undefined;
396
+ }
397
+ }
398
+
399
+ async function sessionCwdForSession(source: string): Promise<string | null> {
400
+ try {
401
+ const raw = await readFile(join(dirname(source), "meta.json"), "utf8");
402
+ const value = JSON.parse(raw) as unknown;
403
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
404
+ const cwd = (value as Record<string, unknown>).cwd;
405
+ return typeof cwd === "string" && cwd.length > 0 ? cwd : null;
406
+ } catch {
407
+ return null;
408
+ }
409
+ }
410
+
411
+ function formatWorkingSet(trace: Trace): string {
412
+ const view = foldWorkingSet(trace.entries);
413
+ const index = buildPathIndex(trace.entries, { cwd: trace.cwd });
414
+ const graph = buildReferenceGraph(trace, index);
415
+ const evictedTokens = [...view.evicted.values()].reduce((sum, state) => sum + state.tokensFreed, 0);
416
+ const churn = view.itemsEvicted === 0 ? "n/a" : (view.recalls / view.itemsEvicted).toFixed(3);
417
+ const opCounts = new Map<string, number>();
418
+ for (const observation of index.observations) {
419
+ opCounts.set(observation.op, (opCounts.get(observation.op) ?? 0) + 1);
420
+ }
421
+ const rewrittenPaths = new Set<string>();
422
+ for (const edge of graph.edges) {
423
+ if (edge.kind !== "file_rewrite") continue;
424
+ const path = index.byRef.get(edge.from)?.path;
425
+ if (path) rewrittenPaths.add(path);
426
+ }
427
+ const lines = [
428
+ `session: ${trace.id}`,
429
+ `source: ${trace.source}`,
430
+ "working set:",
431
+ ` policy: ${view.lastPolicyId ?? "none"}`,
432
+ ` eviction events: ${view.evictionEvents}`,
433
+ ` evicted refs: ${view.evicted.size}`,
434
+ ` items evicted: ${view.itemsEvicted}`,
435
+ ` evicted tokens: ${evictedTokens}`,
436
+ ` recalls: ${view.recalls}`,
437
+ ` churn: ${churn}`,
438
+ " refs:",
439
+ ...([...view.evicted.entries()].length === 0
440
+ ? [" none"]
441
+ : [...view.evicted.entries()]
442
+ .sort(([a], [b]) => a.localeCompare(b))
443
+ .map(([ref, state]) => ` ${ref} reason=${state.reason} by=${state.by ?? "-"} tokens=${state.tokensFreed}`)),
444
+ "path index:",
445
+ ` observations: ${index.observations.length}`,
446
+ ` ops: ${
447
+ [...opCounts.entries()]
448
+ .sort(([a], [b]) => a.localeCompare(b))
449
+ .map(([op, count]) => `${op}=${count}`)
450
+ .join(", ") || "none"
451
+ }`,
452
+ ` paths with rewrites: ${rewrittenPaths.size}`,
453
+ ...[...rewrittenPaths].sort().map((path) => ` ${path}`),
454
+ "",
455
+ ];
456
+ return lines.join("\n");
457
+ }
458
+
459
+ export async function runContextWorkingSetCommand(args: string[]): Promise<number> {
460
+ if (args.includes("--help") || args.includes("-h")) {
461
+ process.stdout.write(WORKING_SET_HELP);
462
+ return 0;
463
+ }
464
+ let session: string | undefined;
465
+ for (let index = 0; index < args.length; index += 1) {
466
+ const arg = args[index];
467
+ if (arg === "--session") {
468
+ try {
469
+ session = requiredValue(args, index, arg);
470
+ } catch (error) {
471
+ process.stderr.write(
472
+ `clio-coder context working-set: ${error instanceof Error ? error.message : String(error)}\n${WORKING_SET_HELP}`,
473
+ );
474
+ return 2;
475
+ }
476
+ index += 1;
477
+ continue;
478
+ }
479
+ process.stderr.write(`clio-coder context working-set: unknown flag ${arg}\n${WORKING_SET_HELP}`);
480
+ return 2;
481
+ }
482
+ if (session === undefined) {
483
+ process.stderr.write(`clio-coder context working-set: --session is required\n${WORKING_SET_HELP}`);
484
+ return 2;
485
+ }
486
+ try {
487
+ const source = await sessionPath(session);
488
+ const raw = await readFile(source, "utf8");
489
+ const parsed = parseSessionEntries(raw, source);
490
+ if (parsed.errors.length > 0) throw new Error(parsed.errors.join("; "));
491
+ const entries = filterEntriesToActivePath(parsed.entries, await pinnedLeafForSession(source));
492
+ const cwd = await sessionCwdForSession(source);
493
+ process.stdout.write(
494
+ formatWorkingSet({
495
+ id: basename(dirname(source)),
496
+ source,
497
+ cwd,
498
+ entries,
499
+ turnCount: buildPathIndex(entries, { cwd }).turnCount,
500
+ }),
501
+ );
502
+ return 0;
503
+ } catch (error) {
504
+ if (error instanceof CliUsageError) {
505
+ process.stderr.write(`clio-coder context working-set: ${error.message}\n${WORKING_SET_HELP}`);
506
+ return 2;
507
+ }
508
+ process.stderr.write(
509
+ `clio-coder context working-set failed: ${error instanceof Error ? error.message : String(error)}\n`,
510
+ );
511
+ return 1;
512
+ }
513
+ }
@@ -10,6 +10,8 @@ const HELP = `Usage:
10
10
  [--target <id>] [--model <id>] [--thinking off|low|medium|high]
11
11
  clio-coder context reset [--all] [--yes]
12
12
  clio-coder context index [--json]
13
+ clio-coder context replay --sessions <path>... [options]
14
+ clio-coder context working-set --session <id|path>
13
15
 
14
16
  Project context commands:
15
17
  clio-coder context show project context status (CLIO-CODER.md, preload, codewiki)
@@ -18,6 +20,8 @@ Project context commands:
18
20
  clio-coder context wiki generate or inspect the agent-authored Markdown wiki
19
21
  clio-coder context reset clear accumulated project context artifacts
20
22
  clio-coder context index build the codewiki index without model calls
23
+ clio-coder context replay compare working-set policies over Clio ledgers
24
+ clio-coder context working-set inspect one session's working-set fold and path index
21
25
  `;
22
26
 
23
27
  function printWikiProgress(event: BootstrapProgressEvent): void {
@@ -313,6 +317,10 @@ export async function runContextCommand(args: string[]): Promise<number> {
313
317
  return (await import("./context-clear.js")).runContextClearCommand(rest);
314
318
  case "index":
315
319
  return (await import("./context-index.js")).runContextIndexCommand(rest);
320
+ case "replay":
321
+ return (await import("./context-working-set.js")).runContextReplayCommand(rest);
322
+ case "working-set":
323
+ return (await import("./context-working-set.js")).runContextWorkingSetCommand(rest);
316
324
  default:
317
325
  process.stderr.write(`clio-coder context: unknown subcommand ${verb}\n`);
318
326
  process.stdout.write(HELP);