@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.
- package/CHANGELOG.md +269 -458
- package/CONTRIBUTING.md +1 -1
- package/README.md +3 -3
- package/dist/{acp-BIYHVZIM.js → acp-S5R4RR5B.js} +7 -6
- package/dist/{agents-YT6SSRIT.js → agents-P6DMMVZY.js} +24 -21
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-5TWEIYDN.js → auth-2XCZLPKS.js} +12 -8
- package/dist/{chunk-GGXXDWE4.js → chunk-22NAGB7X.js} +2 -2
- package/dist/{chunk-WMSVI4G2.js → chunk-2LZI5CAG.js} +133 -13
- package/dist/{chunk-OAO4GE4M.js → chunk-2TZWSW76.js} +2 -2
- package/dist/{chunk-OOJYHWRB.js → chunk-34475P3I.js} +2 -2
- package/dist/{chunk-WVO7V2QY.js → chunk-35MKKU5R.js} +4 -4
- package/dist/{chunk-LBNRH5WM.js → chunk-3HZ5RWN2.js} +5 -5
- package/dist/{chunk-AGYYIBLL.js → chunk-3JLKSKD7.js} +2 -2
- package/dist/{chunk-MBS4V7ZP.js → chunk-4JUF2NNX.js} +7 -7
- package/dist/{chunk-ZDOOVTXZ.js → chunk-4OC57DA6.js} +27 -4
- package/dist/chunk-5M54SPOL.js +926 -0
- package/dist/{chunk-STBPMHSX.js → chunk-7RXG6QRZ.js} +51 -11
- package/dist/{chunk-77VKQEHF.js → chunk-A2GZF7DC.js} +5 -5
- package/dist/{chunk-A3CYT5EX.js → chunk-AD2SYQYC.js} +55 -2
- package/dist/chunk-AOCYTWAV.js +449 -0
- package/dist/chunk-BEY543CS.js +258 -0
- package/dist/{chunk-6N5PTWMY.js → chunk-BP4OYD6A.js} +32 -13
- package/dist/chunk-BPGS2WCQ.js +612 -0
- package/dist/{chunk-J5HN4RYU.js → chunk-BRXQQJFP.js} +8 -8
- package/dist/chunk-CFGTUFWB.js +67 -0
- package/dist/{chunk-CBCAPZAA.js → chunk-E25LMLRW.js} +2 -2
- package/dist/{chunk-G2DE3C7R.js → chunk-EDRHSCIE.js} +4 -4
- package/dist/{chunk-4KLWL3UC.js → chunk-EFADSJET.js} +2 -2
- package/dist/{chunk-M6SHUN7Q.js → chunk-FO5ZOVUY.js} +2 -2
- package/dist/chunk-FYYLNIL5.js +313 -0
- package/dist/{chunk-EPVUXGXG.js → chunk-HV5X7OR2.js} +14 -12
- package/dist/{chunk-TZTZS7QK.js → chunk-HXG4IURW.js} +5 -3
- package/dist/{chunk-IGLFWIYI.js → chunk-K6WL7QZT.js} +3 -3
- package/dist/chunk-K7VKOLQQ.js +15 -0
- package/dist/{chunk-BMEMKKIT.js → chunk-KOHPCX4K.js} +2 -2
- package/dist/{chunk-V4RXGQ5Q.js → chunk-KRPY7NTG.js} +10 -7
- package/dist/chunk-LL4KHSZI.js +22 -0
- package/dist/{chunk-KJ5LWLOE.js → chunk-MEQ45TQ4.js} +15 -9
- package/dist/{chunk-5UUP6MWO.js → chunk-MV3K5QF2.js} +5 -436
- package/dist/{chunk-AO4RKG4M.js → chunk-N4CZJQRK.js} +5 -5
- package/dist/{chunk-ARBGF5F7.js → chunk-NILBFAPG.js} +14 -8
- package/dist/chunk-OZNBF4L3.js +23 -0
- package/dist/{verify-G6V4D2G7.js → chunk-PCZJO5TI.js} +127 -42
- package/dist/chunk-QQK64KLB.js +1360 -0
- package/dist/{chunk-6EJV5X2W.js → chunk-QQL5RT5M.js} +979 -1619
- package/dist/{chunk-LZSJBIVT.js → chunk-QWU7ZBO7.js} +70 -720
- package/dist/{chunk-2EHAIA3X.js → chunk-RD5U66HV.js} +3 -3
- package/dist/{chunk-OKGUZO2U.js → chunk-SPULKLCF.js} +4 -3
- package/dist/{chunk-OQ33BKR3.js → chunk-TTNYS3EA.js} +3 -60
- package/dist/chunk-TW3WDMVS.js +677 -0
- package/dist/chunk-TZSKNMZG.js +434 -0
- package/dist/{chunk-7MNJORFF.js → chunk-UL3WSD3F.js} +6 -1
- package/dist/{chunk-7EYHLWU7.js → chunk-UZHIZC5S.js} +7 -7
- package/dist/{chunk-QTYWRVRA.js → chunk-VAWWTKDP.js} +8 -8
- package/dist/{chunk-X75S7HFS.js → chunk-VEZEGCGW.js} +214 -20
- package/dist/{chunk-OHHN2SO4.js → chunk-VMNQ6OZA.js} +98 -202
- package/dist/chunk-VSNATDE6.js +122 -0
- package/dist/chunk-W6GROXXM.js +69 -0
- package/dist/chunk-WPQLXFOZ.js +375 -0
- package/dist/{chunk-ORBHGJC5.js → chunk-WR67VIZY.js} +3 -3
- package/dist/{chunk-3ZXDFGR5.js → chunk-X6COSD2O.js} +5 -5
- package/dist/chunk-ZGVHUX3M.js +66 -0
- package/dist/{chunk-MAW544W2.js → chunk-ZWMF7253.js} +4 -4
- package/dist/{chunk-MQSRRFWA.js → chunk-ZYKPLLNQ.js} +563 -546
- package/dist/cli/index.js +27 -23
- package/dist/{clio-4LY5K2AC.js → clio-J5JIOIDS.js} +7 -6
- package/dist/{code-nav-7AX6FYE6.js → code-nav-AXCXSBHX.js} +5 -3
- package/dist/{config-GTLUW2PR.js → config-OEBMIN2U.js} +37 -27
- package/dist/{configure-R6A64DHX.js → configure-PUQOSIXQ.js} +16 -13
- package/dist/{context-5VKGUVJJ.js → context-EKDCKUUZ.js} +82 -7
- package/dist/{context-RW5HC47S.js → context-MGSE4Z2T.js} +33 -23
- package/dist/{context-JFZEJ7W5.js → context-URSXPBCK.js} +17 -9
- package/dist/{context-clear-6ZHBAZZT.js → context-clear-KDAJRNUK.js} +33 -23
- package/dist/context-working-set-SBKMPPI2.js +1552 -0
- package/dist/{dispatch-runner-VKBRCWQC.js → dispatch-runner-MSWN72NK.js} +43 -29
- package/dist/{doctor-KI767GSN.js → doctor-7BSE27PJ.js} +10 -10
- package/dist/{eval-XSSNATB4.js → eval-IZGDOO4H.js} +9 -8
- package/dist/{evidence-UA6AWDQQ.js → evidence-SR7WXB5B.js} +51 -23
- package/dist/{evolve-QNTFGV6Z.js → evolve-K7VE2CBX.js} +30 -20
- package/dist/{fleet-Q7UOMUSG.js → fleet-7XMJNQNF.js} +48 -38
- package/dist/{fleet-preflight-DDN536IT.js → fleet-preflight-AQNAH644.js} +3 -3
- package/dist/{init-WBB65ZHQ.js → init-JGNPAYXT.js} +41 -31
- package/dist/{memory-MD3O64RI.js → memory-4ALKDJ4Q.js} +32 -22
- package/dist/{models-BZU34YWD.js → models-ZMMLFJNN.js} +22 -19
- package/dist/{monitor-MEQA5C3I.js → monitor-2F3T5KHP.js} +55 -43
- package/dist/{orchestrator-CGFKEP27.js → orchestrator-ORHT43JB.js} +2507 -1896
- package/dist/{reset-L2FQEE3E.js → reset-NXGTYNUO.js} +4 -3
- package/dist/{run-IV4Q6RLN.js → run-RF4WJGMT.js} +51 -41
- package/dist/{share-S5BZQC5I.js → share-UT3W6E4M.js} +5 -4
- package/dist/{skills-LQEKRDTN.js → skills-PSACKC5Q.js} +2 -2
- package/dist/{skills-eval-3DC4HEWS.js → skills-eval-WJSI55RZ.js} +34 -24
- package/dist/{targets-C4SSGQOB.js → targets-PIIRAOYS.js} +23 -20
- package/dist/{terminal-lease-IT5JW2NR.js → terminal-lease-ULWXWNVY.js} +5 -4
- package/dist/{upgrade-7TT7SQ3G.js → upgrade-346TZ6AV.js} +18 -17
- package/dist/{usage-GV4PKT3M.js → usage-6KKXR32N.js} +34 -24
- package/dist/verifiers-4UUM6TEE.js +1214 -0
- package/dist/verify-X5HDROLA.js +25 -0
- package/dist/{wiki-generate-DQF6Z66B.js → wiki-generate-7STOCIFZ.js} +42 -31
- package/dist/worker/entry.js +33 -24
- package/docs/README.md +8 -7
- package/docs/acp.md +1 -1
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +2 -2
- package/docs/artifact-versions.md +1 -1
- package/docs/built-in-agents.md +1 -1
- package/docs/capacity-and-scheduling.md +1 -1
- package/docs/commands-and-modes.md +53 -21
- package/docs/config-knobs-audit.md +1 -2
- package/docs/configuration-and-targets.md +15 -1
- package/docs/context-engine.md +64 -12
- package/docs/context-working-set.md +194 -0
- package/docs/development-pipeline.md +1 -1
- package/docs/documentation-coverage.md +5 -5
- package/docs/documentation-guide.md +6 -5
- package/docs/environment-variables.md +2 -1
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +14 -1
- package/docs/evidence-and-memory.md +74 -2
- package/docs/evolution.md +1 -1
- package/docs/exit-codes-and-output.md +1 -1
- package/docs/extensions-and-sharing.md +2 -2
- package/docs/fleet-dispatch.md +22 -7
- package/docs/glossary.md +21 -1
- package/docs/installation-and-lifecycle.md +6 -6
- package/docs/middleware-and-components.md +1 -1
- package/docs/model-catalog.md +7 -9
- package/docs/observability.md +4 -4
- package/docs/performance-methodology.md +2 -2
- package/docs/proactive-memory.md +1 -1
- package/docs/prompt-envelope-and-tools.md +4 -4
- package/docs/provider-adapter-cookbook.md +1 -1
- package/docs/release-cut-checklist.md +35 -35
- package/docs/safety-model.md +23 -4
- package/docs/scientific-validation.md +21 -3
- package/docs/session-lifecycle.md +3 -3
- package/docs/skills-marketplace.md +1 -1
- package/docs/tool-usage.md +79 -12
- package/docs/trace-store.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/docs/tui-design.md +2 -2
- package/docs/worker-dispatch-mechanics.md +11 -1
- package/package.json +8 -11
- package/skills/meta/clio-test/SKILL.md +20 -17
- package/skills/meta/clio-test/evals.md +3 -3
- package/skills/meta/clio-test/references/harness.md +35 -6
- package/skills/meta/clio-test/references/test-map.md +20 -10
- package/skills/registry.yaml +2 -2
- package/skills/skill-marketplace.json +1 -1
- package/src/cli/context-working-set.ts +513 -0
- package/src/cli/context.ts +8 -0
- package/src/cli/evidence.ts +20 -2
- package/src/cli/index.ts +4 -0
- package/src/cli/verifiers.ts +325 -0
- package/src/core/bash-exec.ts +39 -14
- package/src/core/bus-events.ts +19 -4
- package/src/core/config.ts +54 -0
- package/src/core/defaults.ts +50 -3
- package/src/core/git-commit-attribution.ts +46 -21
- package/src/core/verification-scripts.ts +6 -0
- package/src/domains/agents/builtins/verifier.md +3 -0
- package/src/domains/config/classify.ts +1 -0
- package/src/domains/config/keybindings.ts +3 -3
- package/src/domains/context/working-set/contract.ts +161 -0
- package/src/domains/context/working-set/defaults.ts +28 -0
- package/src/domains/context/working-set/engine.ts +203 -0
- package/src/domains/context/working-set/fold.ts +62 -0
- package/src/domains/context/working-set/horizon.ts +38 -0
- package/src/domains/context/working-set/marker.ts +103 -0
- package/src/domains/context/working-set/path-index.ts +436 -0
- package/src/domains/context/working-set/payload.ts +152 -0
- package/src/domains/context/working-set/policies/age-horizon.ts +55 -0
- package/src/domains/context/working-set/policies/index.ts +21 -0
- package/src/domains/context/working-set/policies/structural.ts +160 -0
- package/src/domains/context/working-set/project.ts +132 -0
- package/src/domains/context/working-set/protect.ts +109 -0
- package/src/domains/context/working-set/recall.ts +177 -0
- package/src/domains/context/working-set/replay/controls.ts +112 -0
- package/src/domains/context/working-set/replay/load-clio.ts +199 -0
- package/src/domains/context/working-set/replay/metrics.ts +185 -0
- package/src/domains/context/working-set/replay/reference-graph.ts +79 -0
- package/src/domains/context/working-set/replay/report.ts +139 -0
- package/src/domains/context/working-set/replay/runner.ts +325 -0
- package/src/domains/context/working-set/replay/synthetic.ts +422 -0
- package/src/domains/context/working-set/replay/trace.ts +21 -0
- package/src/domains/context/working-set/visible.ts +54 -0
- package/src/domains/evidence/build.ts +112 -45
- package/src/domains/evidence/eval.ts +24 -7
- package/src/domains/evidence/index.ts +53 -0
- package/src/domains/evidence/ordering.ts +12 -0
- package/src/domains/evidence/run-trust.ts +221 -0
- package/src/domains/evidence/store.ts +46 -6
- package/src/domains/evidence/trust-status.ts +854 -0
- package/src/domains/evidence/types.ts +26 -0
- package/src/domains/middleware/memory-intervention.ts +3 -0
- package/src/domains/middleware/stalled-turn.ts +165 -4
- package/src/domains/safety/autonomy.ts +1 -1
- package/src/domains/safety/default-path-policy.ts +8 -0
- package/src/domains/safety/finish-contract.ts +4 -3
- package/src/domains/safety/policy-engine.ts +48 -6
- package/src/domains/session/compaction/compact.ts +23 -1
- package/src/domains/session/compaction/cut-point.ts +2 -0
- package/src/domains/session/compaction/tokens.ts +16 -1
- package/src/domains/session/context-ledger.ts +2 -0
- package/src/domains/session/entries.ts +107 -1
- package/src/domains/session/manager.ts +9 -2
- package/src/domains/session/migrations/index.ts +22 -3
- package/src/engine/acp/server.ts +3 -0
- package/src/engine/agent.ts +18 -1
- package/src/engine/session.ts +9 -3
- package/src/entry/orchestrator.ts +16 -4
- package/src/interactive/chat-loop-messages.ts +18 -6
- package/src/interactive/chat-panel.ts +571 -244
- package/src/interactive/chat-renderer.ts +79 -39
- package/src/interactive/context-meter.ts +10 -0
- package/src/interactive/context-overlay.ts +81 -6
- package/src/interactive/context-recall-command.ts +110 -0
- package/src/interactive/editor-submit.ts +26 -1
- package/src/interactive/footer/widgets.ts +22 -20
- package/src/interactive/footer-panel.ts +6 -1
- package/src/interactive/interactive-application.ts +2 -0
- package/src/interactive/interactive-event-projection.ts +12 -0
- package/src/interactive/interactive-slash-runtime.ts +49 -8
- package/src/interactive/model-session-replay.ts +21 -0
- package/src/interactive/overlay-general-openers.ts +6 -0
- package/src/interactive/overlay-session-lifecycle.ts +8 -4
- package/src/interactive/overlays/ask-user.ts +146 -24
- package/src/interactive/renderers/tool-execution.ts +167 -56
- package/src/interactive/session-transcript.ts +2 -2
- package/src/interactive/slash-commands.ts +29 -2
- package/src/interactive/status/index.ts +12 -1
- package/src/interactive/status/reasoning.ts +87 -0
- package/src/interactive/status/summary.ts +13 -2
- package/src/interactive/transcript-detail.ts +120 -0
- package/src/interactive/turn-context.ts +238 -88
- package/src/interactive/turn-middleware.ts +6 -6
- package/src/tools/agent-tools.ts +11 -4
- package/src/tools/bash.ts +144 -82
- package/src/tools/builtin-tool-catalog.ts +18 -6
- package/src/tools/context/index.ts +105 -3
- package/src/tools/context/surface.ts +3 -2
- package/src/tools/core-bootstrap.ts +21 -0
- package/src/tools/dispatch-runner.ts +9 -7
- package/src/tools/monitor.ts +28 -20
- package/src/tools/presentation.ts +107 -0
- package/src/tools/registry.ts +65 -7
- package/src/tools/result-disposition.ts +550 -0
- package/src/tools/result-shaping.ts +262 -19
- package/src/tools/safe-exec.ts +2 -0
- package/src/tools/verify/authoring.ts +1119 -0
- package/src/tools/verify/catalog.ts +346 -0
- package/src/tools/verify/index.ts +13 -3
- package/src/tools/verify/scripts.ts +135 -37
- package/src/tools/verify/surface.ts +9 -5
- package/src/tools/worker-evidence.ts +35 -12
- package/dist/chunk-MNA4JGU4.js +0 -255
- package/dist/chunk-SRF2PJNW.js +0 -184
- package/dist/chunk-T6YILFSB.js +0 -80
- 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,
|
|
4
|
-
|
|
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`
|
|
15
|
-
|
|
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
|
-
|
|
83
|
-
`tests
|
|
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
|
|
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` | `
|
|
11
|
-
| smoke | `tests/smoke/*.test.ts` | `
|
|
12
|
-
| boundaries | `tests/boundaries
|
|
13
|
-
| harness (not
|
|
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
|
-
|
|
|
37
|
-
|
|
|
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
|
-
|
|
53
|
+
npm run test:file -- 'tests/contracts/**/*.test.ts'
|
|
44
54
|
# one file
|
|
45
|
-
|
|
55
|
+
npm run test:file -- tests/contracts/skills.test.ts
|
|
46
56
|
# only it.only / describe.only within a file
|
|
47
|
-
|
|
57
|
+
npm run test:file -- --test-only tests/contracts/skills.test.ts
|
|
48
58
|
```
|
|
49
59
|
|
|
50
60
|
## Writing tests
|
package/skills/registry.yaml
CHANGED
|
@@ -61,8 +61,8 @@ skills:
|
|
|
61
61
|
sha256: 386ebfdac9379ebf5630fa7a9af259bb45f473f7d3d679b0a7ef4164f5fc4dea
|
|
62
62
|
- name: clio-test
|
|
63
63
|
path: meta/clio-test
|
|
64
|
-
version: 0.1.
|
|
65
|
-
sha256:
|
|
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.
|
|
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
|
+
}
|
package/src/cli/context.ts
CHANGED
|
@@ -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);
|