# Row 307 (#1814, Vikunja 598): move row 270's pi compaction reading behind the harness kind Owner Gizmoduck. Reviewer Duckworth. Gate owner Sage. Brief: docs/plans/2026-10-23_harness-kind-compactions.md. Base 1e629c892. Design: DESIGN.md beside this file, approved (static) by Duckworth on 67daf6403 (duckworth-0280), with C1, C2, N1 and N2 folded in at cb048b93c. Mos ruled go on the four old-test edits in mos-1459, with the bounds below. ## Commits 1. 1f6db1589, red first. The two new test files and capture.mjs. Its product files are base's: `git diff --stat 1e629c892 1f6db1589` lists only this packet, capture.mjs and the two tests. 2. 44104adb1, the build. The product files and the four old-test edits. ## What the build does - `packages/plugins/src/kinds.mjs:52-57`: the harness kind's exports end in `"compactions"`, and the comment gains its contract line. `interfaceVersion` stays 1 (DESIGN.md "interfaceVersion stays 1"). - New `packages/plugins/harness/pi/compactions.mjs`: base's `scanPi`, `readRange`, `digest`, `STREAM`, `TAIL`, the chunk size, `PI_VERSION` and `claudeHistory`, unchanged in behaviour, plus the export. `scanPi` is no longer exported and takes no defaults: its one caller passes all four arguments. - `pi/index.mjs` re-exports it. `claude-code/index.mjs` answers harness-unsupported with version null. - `packages/seat/src/compactions.mjs`: keeps the ledger. `readLedger` sets `unsupported` on `r.reason === "harness-unsupported"` only (:113). `checkAnswer` (:147-164) refuses an answer out of contract. `collectCompactions(store, { name, compactions }, opts)` (:174-205) calls the export, checks it, and writes the same lines in the same order as base. - `packages/cli/src/launcher.mjs`: the `compactions` closure takes the loaded module and calls the export directly, with the N1 comment on why it skips `call()`. Both call sites pass `harness`. - `packages/plugins/README.md`: the Harness section gains the `compactions` bullet. ## Old-test edits (mos-1459 bounds) | File | Edit | Bound | | --- | --- | --- | | plugins/tests/kinds.test.mjs | `"compactions"` appended to the pinned list | :17-21, append only | | seat/tests/compactions.test.mjs | two module imports; `collect` passes `{ name, compactions }` | :11/:21, helper only | | seat/tests/compactions-budget.test.mjs | pi import; `collect` passes `{ name, compactions }` | :13/:24-25, helper only | | cli/tests/compactions-launch.test.mjs | pi import; base :344's call (now :345) passes `{ name: "pi", compactions: pi }` | call only, `.count === 1` kept | No call or expected value in those files changed. harness.test.mjs, cli helpers.mjs:133-151, launch-routes.test.mjs:17-23 and launcher.test.mjs:694-697 are unedited. ## New tests - `packages/seat/tests/compactions-ledger.test.mjs`: - FIXTURES: seven ledgers row 270's code writes (known, store-incomplete, store-unreadable, not-collected, harness-unsupported from claude-code, harness-unsupported from Claude history, and torn), each with its streams, projects and steps. Per fixture, test A reads `compactionCount` and test B runs one collection through `{ name: "pi", compactions: pi.compactions }`. - Test B runs over the fixture's own streams, not an empty session as DESIGN.md:189 says. Over an empty session every fixture with a cursor would go store-incomplete (the stream went away), so "the others go known" could not hold. The expected answers: known stays known (1), store-unreadable goes known with 0 (the cursor is at the end), not-collected goes known with 1, and store-incomplete, torn and both harness-unsupported fixtures stay unknown. - The malformed-answer test (17 answers, plus a module that throws, over a fresh store and over the known fixture) and the argument test (exact keys; core passes a copy of the cursors). - `packages/plugins/tests/harness-compactions.test.mjs`: CASES plain, budget (100 bytes), cap (1 stream of 2), shrank and header against pi; pi's `first` with and without Claude history; claude-code; and a module re-exporting every pi export but `compactions`, refused `missing-export`. - capture.mjs reads both capture blocks from the test files. It replays each fixture with base's `collectCompactions(store, "", ...)` and compares the ledger byte for byte, and it runs base's `scanPi` per case and compares the answer under base's reason order (:301 at base) and `PI_VERSION`. It prints base's `compactionCount` per fixture: that is the before half. ## Expected red on 1f6db1589 - seat compactions-ledger: the seven test A pass (the ledger rules are base's). Test B fails for known, store-unreadable and not-collected: base takes the object as a harness that isn't pi and writes harness-unsupported. Test B passes on base for the other four, whose expected answer is unknown with the same reason. The malformed-answer and argument tests fail: base never calls the module. Correction after the run: test B fails on base for claude-history too, so the reds are known, store-unreadable, not-collected and claude-history. On base the object harness takes :304, which writes `collected` without a scan, so the count stays null. Pi on the tip scans on a later collection, finds c1 and answers 1, still unknown with harness-unsupported. The test is right and this list was wrong (Duckworth agreed, duckworth-0287). - plugins harness-compactions: the five CASES, the `first` test and the claude-code test fail with a TypeError (`compactions` isn't exported on base). The missing-export test fails: base's loader loads the module. - harness.test.mjs:192 is already red on base. That's the brief's starting point. ## Run plan (mos-1275, `timeout -k 10`) All on a CLEAR from Sage. TMPDIR comes from `mktemp -d /mnt/storage/scratch/tmp/gXXX`. Gate worktrees under /mnt/storage/scratch, node_modules linked right after `git worktree add`, pipefail and tee. | Run | Longest recorded | LIMIT | | --- | --- | --- | | capture.mjs on a 1f6db1589 worktree (base and candidate both it) | none | 120 s | | red first: the two new files on 1f6db1589, each whole | none (seat suite 7 s) | 300 s | | the touched test files whole on 44104adb1, one at a time | compactions-launch alone: none; other files 15 s | 600 s | | bus suite | 25 s (270 r5) | 900 s | | cli suite | 682 s (270 r5) | 2100 s | | plugins suite | none recorded | 900 s | | webui suite | 58.2 s (294) | 600 s | | business suite | none recorded | 900 s | | seat suite whole, `node --test tests/` (Duckworth C1) | 7 s (270 r5) | 600 s | | each scripts/test-*.sh | 42 s (test-foundation) | 900 s | | each mutant, its kill files whole | 1 s (264a probes) | 300 s | - In the cli run, compactions-launch's T3 tests (base :206, and :305 with the :344 edit; :207, :306 and :345 after the import line) must show as run, not skipped on `sandboxProblem()` (base :36, now :37). The report names every skip (Duckworth C2). - Plugins must have zero failures. - A timeout with no failed test is a hang and goes to Sage. A timeout in the shell phase goes to Mos unplaced. A first-run red in a new 307 test is placed and reported after (mos-1272). A red in an old test goes to Sage, unfixed. ## Mutants Each on an isolated copy of the file, restore checked by sha256, only on a green base. | # | Change | Kill files | | --- | --- | --- | | M1 | `readLedger`: `if (r.harness !== "pi" \|\| r.reason === "harness-unsupported")` restored | plugins harness.test.mjs (:192) | | M2 | pi `compactions` drops the `first && claudeHistory(store)` branch | seat compactions-budget.test.mjs (base :86, now :87), plugins harness-compactions.test.mjs | | M3 | `readLedger` never sets `unsupported` | seat compactions-ledger.test.mjs, seat compactions.test.mjs (base :197, now :199) | | M4 | `collectCompactions` skips `checkAnswer` | seat compactions-ledger.test.mjs | ## Runs Sage's CLEARs: sage-3455 for steps 1-3, sage-3465 for steps 4-6. Gate worktrees at /mnt/storage/scratch/g307-base (1f6db1589), g307-tip and g307-mut (1f00d4f91), with node_modules linked. TMPDIR was /mnt/storage/scratch/tmp/gTIK. Outputs went to /mnt/storage/scratch/gizmo-307-out/s1 and s2. ### Steps 1-3 (13:31:21Z-13:31:36Z, then the capture rerun at 13:32Z) - capture.mjs crashed with ENOENT on its first run. Base `sessionStore` (session.mjs:78 `privateDirs`, called from :117) makes its directories one level at a time, and :44 passed a per-fixture root that didn't exist yet. The fix is one line before :44, `` mkdirSync(join(root, `f-${name}`)) ``. A copy with that line, run on g307-base as both base and candidate, exited rc 0 and printed `all match`: 7 ledgers byte for byte, 7 base counts and 5 scans. Base counts: known 1 known; store-incomplete, store-unreadable and not-collected null, unknown with their own reason; claude-code and claude-history null, harness-unsupported; torn 1, store-incomplete. - Red first on 1f6db1589, both rc 1. Seat compactions-ledger: 9 pass, 7 fail (the corrected list above, plus the malformed-answer and argument tests). Plugins harness-compactions: 0 pass, 8 fail. Seven fail with `TypeError: pi.compactions is not a function` or `claude.compactions is not a function`. The missing-export test fails because base loads the module. - Touched files whole on 1f00d4f91, all rc 0: compactions-ledger 16/16, harness-compactions 8/8, kinds 5/5, harness 9/9 (:192 green), compactions 20/20, compactions-budget 10/10, compactions-launch 5/5 with 0 skipped. ### Steps 4-6 (14:00:10Z-14:14:00Z, on 1f00d4f91) | Run | rc | Time | Result | | --- | --- | --- | --- | | bus | 0 | 24 s | 890 pass, 0 fail, 0 skipped | | cli | 0 | 631 s | 927 pass, 0 fail, 0 skipped; T3 :207, :306, :345 ran | | plugins | 0 | 0 s | 119 pass, 0 fail, 0 skipped | | webui | 0 | 57 s | 374 pass, 0 fail, 1 skipped (below) | | business | 0 | 2 s | 231 pass, 0 fail, 0 skipped | | seat | 0 | 7 s | 218 pass, 0 fail, 0 skipped | | test-auth | 0 | 0 s | 15 passed, 0 failed | | test-conductor | 0 | 7 s | | | test-config | 0 | 3 s | | | test-discord | 0 | 7 s | | | test-extension-package | 0 | 2 s | | | test-foundation | 0 | 39 s | | | test-land-work | 0 | 12 s | | | test-queue | 0 | 20 s | one skip (below) | | test-release | 0 | 3 s | 15 passed, 0 failed | | test-task | 0 | 14 s | 92 passed, 0 failed, two skips (below) | Skips: - webui: "rebuilding from the source gives the same bytes" needs the pinned esbuild. 307 doesn't touch the editor. - test-queue: "queue verify and render --check", because the gate worktree isn't the queue's canonical root. - test-task: "live provider cases" twice (row 244, no provider call in the gate). Mutants ran in g307-mut. Each restore matched its sha256, and the tree was clean after M4. | # | Killed by | | --- | --- | | M1 | plugins harness.test.mjs:192 | | M2 | seat compactions-budget.test.mjs:87 and :104; plugins harness-compactions.test.mjs:47 | | M3 | seat compactions-ledger.test.mjs:123 (test B, claude-code and claude-history); seat compactions.test.mjs:199 | | M4 | seat compactions-ledger.test.mjs:151 (the malformed-answer test) | There were no reds, so nothing was placed under mos-1272. The sweep found no node or chromium left in my cwds.