Skip to content
GitHubRSS

The plan after the sweep week (2026-09-22)

Status: the working plan. Items move from here into STATUS’s loop as they land, each with its number; a section here is struck through in place when its item is DONE, never deleted, so the next reader sees what was decided and why.

The week of 2026-09-18 → 09-22 (items 342–572, PRs #488–#681) was one campaign: a per-file mutation sweep of core. It found real stale-hit and containment defects (#667 #679 #680 #676 #661 #650 #642 #681), it produced the sweep rulebook in CLAUDE.md, and it settled the shard-9 SIGILL by measurement (Bun 1.3.11 below the repo’s own floor). It also left STATUS at 5,803 lines, the core suite 14k test lines heavier with no CI time budget, three findings it scored as real still open, and no run-path change to A/B. Item 572’s own verdict stands: the sweep list is exhausted. This document is what comes next, in order, with the seam each piece lives in, the constraint that must survive, the measurement that proves it, and what NOT to do.

  1. Fixes, one day: F4 (the gate’s Bun floor) first because every later verdict depends on it; then F1, F3; F2 last because it needs the darwin job.
  2. Hygiene, one day: F5 (STATUS trim), then I1 and I4, because the next agent reads CLAUDE.md and STATUS before anything else.
  3. Product: D3 before D1; D5 alongside; D4 whenever the owner is available; D2 when a workspace asks for it.

F1. A gitignored directory named as a literal input folds nothing (item 565) — DONE, item 576

Section titled “F1. A gitignored directory named as a literal input folds nothing (item 565) — DONE, item 576”

cache.inputs.files: ['gen'] on an ignored gen/ resolves to zero files and the ignored-input refusal never fires: the literal check goes through Bun.file(path).exists(), which answers false for a directory, so the refusal continues past every literal naming one. Two guards mask each other (settleLiterals and the existence check in cache/inputs.ts). Stale-hit class: the task caches on an input set that ignores the directory the user named.

  • Fix: stat instead of Bun.file; a literal naming a directory is judged, then enumerated or refused under the existing ignored-input rule. The reason 565 did not fix it — a stat newly refuses a literal naming a tracked-but-empty directory — is decided here: an empty tracked directory has no files to fold, so refusing it with the same message is correct and cheaper than a special case. Say so in docs/caching.md beside the ignored-input rule.
  • Acceptance: a workspace with a gitignored gen/ holding one file and a task with files: ['gen'] refuses with the ignored-input message. Control: files: ['gen/**'] on the same tree already refuses; keep that row so the fix is differential. Second row: gen-notes.txt beside the literal must not settle gen.
  • Then grep the class: every Bun.file(...).exists() in src/ (git grep '\.exists()' -- packages/vx/src); read project-loader.ts, migration.ts and workspace.ts for a directory operand.

F2. The seatbelt profile interpolates a resolved socket path unchecked (item 478) — DONE, item 582 (testable on Linux after all: the profile is text)

Section titled “F2. The seatbelt profile interpolates a resolved socket path unchecked (item 478) — DONE, item 582 (testable on Linux after all: the profile is text)”

exec/sandbox-runtime.ts, the unix-socket loop: sbplPath(sock, …) is checked, toRealPath(sock) beside it is not, so a symlink whose target carries a quote, backslash or closing paren rewrites the SBPL policy. 478 left it because wrapping the resolved path in sbplPath would refuse a space, legal on macOS.

  • Fix: a second checker for filesystem-resolved paths that refuses only what can leave a quoted SBPL string (", \, ), newline), and route toRealPath(sock) through it. Keep sbplPath for declared values; keep refuse-not-escape.
  • Acceptance: a darwin-only row with a symlink to a socket whose target path holds ")(allow default)(; the run refuses with the injection message. Control: a target path with a space is accepted.
  • Not: widen sbplPath; escape instead of refuse; ship without the darwin job green (this container cannot run seatbelt).

F3. The wildcard classifier is written six ways (item 495 follow-up) — DONE, item 577 (two alphabets, not one: the sandbox’s brace case was measured and kept)

Section titled “F3. The wildcard classifier is written six ways (item 495 follow-up) — DONE, item 577 (two alphabets, not one: the sandbox’s brace case was measured and kept)”

[*?[\]{}] in util/paths.ts, stable-keys.ts, filter.ts, watch.ts, workspace.ts; [*?[\]] (no braces) in sandbox-binds.ts, sandbox-runtime.ts (twice), sandbox-request.ts, config-schema.ts. 495 proved the brace matters for what a clean deletes; the sandbox copies answer whether a bind needs a directory walk, where a wrong answer is a task that cannot read its file. Six spellings of one predicate is how two guards come to mask each other.

  • Fix: one exported predicate in util/paths.ts used everywhere. The sandbox sites keep their semantics only if a test shows a brace bind spec must NOT walk — measure first: a bind of src/{a,b} through sandbox-binds.ts, with and without the brace in the class.
  • Acceptance: one table over the nine characters in tests/paths.test.ts, plus a pin per former call site that fails when the site stops using the shared predicate.

F4. The gate runs a Bun below the repo’s floor — DONE, item 575

Section titled “F4. The gate runs a Bun below the repo’s floor — DONE, item 575”

engines.bun is >=1.4, CI pins 1.4.2, the 2026-09-19 container shipped 1.3.11; the difference explained the shard-9 SIGILL, three “flaky” tests and three inert symlink tripwires.

  • Fix: the ci task refuses (not warns) below MIN_BUN, in under a second, with the floor message and the download URL of the release asset (github.com/oven-sh/bun/releases/download/bun-v1.4.2/bun-linux-x64.zip works through the proxy where bun upgrade does not).
  • Acceptance: 1.3.11 → the task fails before any shard starts; 1.4.2 → unchanged.

F5. STATUS is 5,803 lines with 120 items in the loop — DONE, items 573, 592 and 612 (the trim is a duty at every twenty)

Section titled “F5. STATUS is 5,803 lines with 120 items in the loop — DONE, items 573, 592 and 612 (the trim is a duty at every twenty)”

Twenty per history file is the convention; the last trim was after 452. Trim 453–572 into six history files, one line per file in Next 14, handoff 14aq. Gate the edit script on its own exit; read the formatter’s verdict line, not its last line.

F6. Owner items, one line each, never re-recorded

Section titled “F6. Owner items, one line each, never re-recorded”

Delete the NPM_TOKEN secret; cut the 0.1.0 release; decide the site’s address; optionally enable private vulnerability reporting.

I1. The sweep becomes a script, or its rules leave CLAUDE.md — DONE, item 578 (the rules left; no script, the list is exhausted)

Section titled “I1. The sweep becomes a script, or its rules leave CLAUDE.md — DONE, item 578 (the rules left; no script, the list is exhausted)”

The sweep was done by hand 230 times and CLAUDE.md carries ~25 rules for doing it right. Either scripts/mutate.ts <file> <mutations.json> (worktree, pinned Bun, type-check per mutation for declaration-heavy files, loud on no summary, CAUGHT / SURVIVED / INCONCLUSIVE with the failing test’s name), or move the rules to docs/design/mutation-sweeps-2026-09.md with a two-line pointer. The second is cheaper and probably right: the rules are only useful while sweeping, and CLAUDE.md is read every session.

I2. CI time gets a budget — DONE, item 579 (2:23–3:16 per run; weights refreshed)

Section titled “I2. CI time gets a budget — DONE, item 579 (2:23–3:16 per run; weights refreshed)”

Read the last ten CI runs’ wall time (lint · format · test and the macOS job), record them in Next 6 beside the warm-path figures, refresh tests/shard-weights.json (last refreshed 2026-09-16, before this week’s files existed). If the core job crossed six minutes, the heaviest new witness files are the first to fold onto a shared fixture.

I3. One doc-pin mechanism — DECLINED, item 583 (three questions by design; the shard dealer reads tests/ flat)

Section titled “I3. One doc-pin mechanism — DECLINED, item 583 (three questions by design; the shard dealer reads tests/ flat)”

module-shape-drift, site-samples.unsafe (1,388 lines) and doc-class-pins.unsafe (858 lines) answer “does this doc sentence still match the code” three ways. One tests/doc-pins/ directory, one helper taking (doc path, quoted sentence, extractor of the truth), so a pin is three lines.

I4. Items get shorter; corrections replace in place — DONE, items 573 and 578 (the rule; 575–577 follow it)

Section titled “I4. Items get shorter; corrections replace in place — DONE, items 573 and 578 (the rule; 575–577 follow it)”

An item is at most twelve lines: what changed, the number, the test; reasoning goes in the PR body. A correction replaces the wrong sentence (CLAUDE.md already says so) rather than appending a CORRECTED paragraph. In flight holds each item’s current state in one paragraph, not its history.

I5. A witness dies with its guard — DECLINED, item 583 (the gate type-checks tests/; an imported symbol that goes is already red)

Section titled “I5. A witness dies with its guard — DECLINED, item 583 (the gate type-checks tests/; an imported symbol that goes is already red)”

suite-coverage.unsafe.test.ts proves every test file is launched. A companion proves every witness names a symbol that still exists, so a refactor that removes a guard must remove or rewrite its pin.

I6. The gate prints its box — DONE, item 579 (check.bun’s second line)

Section titled “I6. The gate prints its box — DONE, item 579 (check.bun’s second line)”

One line in the ci task’s summary: bun --version, the core count, the cgroup quota — so a STATUS figure carries its box without a paragraph.

I7. Bun.file(<dir>).exists() gets a pin — DONE, item 576

Section titled “I7. Bun.file(<dir>).exists() gets a pin — DONE, item 576”

A grep-based row in module-shape-drift.test.ts over Bun.file\(.*\)\.exists\(\) in src/ with an allowlist of the file-path sites.

D1. Overlapping outputs, addition shape (overlapping-outputs-2026-09.md) — DONE, item 588 (gate met by survey, item 587)

Section titled “D1. Overlapping outputs, addition shape (overlapping-outputs-2026-09.md) — DONE, item 588 (gate met by survey, item 587)”

Two cached tasks, B depends on A, both declare dist; B adds files A never wrote (strapi). Today B stays uncached.

  • Seam: execute-task.ts’s clean and save path; cleanOutputs already returns what it removed. No new plugin seam.
  • Design (the note’s, unchanged): snapshot the overlap before B runs, diff after by size + mtime; B’s clean removes only that set; B’s artifact holds only that set; B restores only after A is on disk.
  • Constraint: an overlap-narrowed artifact can never be restore-tier. Today two blanket rules hold that (upstreamOutputProjects for same-project, the graph-wide workspaceFiles exclusion in local-shortcircuit.ts for cross-project). Do not narrow either in the same change; tests/local-shortcircuit.test.ts § “a workspace-output writer anywhere” is the pin that fails if you do.
  • Measurement: the matrix A hit/B miss, A miss/B hit, both hit, both miss, B adds nothing — each tree byte-identical to a cold run of both. Cost: one stat walk of the overlap per B miss, zero on a hit.
  • Not: the rewrite-in-place shape (refine’s types). Gate: a third real repository with the addition shape — MET 2026-09-22 (item 587: twenty and storybook, survey table in the note), so this arc is open.

D2. A streaming remote seam (Next 2) — DONE, item 662 (get resolves Blob | Response, put takes a file-backed Blob)

Section titled “D2. A streaming remote seam (Next 2) — DONE, item 662 (get resolves Blob | Response, put takes a file-backed Blob)”

RemoteCacheLayer.put(hash, body) / get() on whole buffers are the last in-memory artifact. Widen both to Blob in one commit with the stub layers, the plugins guide and @vzn/vx-reapi (two-pass digest, chunked upload through the existing CHUNK_BYTES downgrade path, zstd per chunk). Measure a 150 MiB round trip through the stub and through NativeLink, peak RSS via vx last. Not: a core-side Blob alone; not before a workspace uploads > 100 MiB.

D3. Narrow the workspaceFiles restore-tier exclusion — DONE, item 584 (reach test; tier 0 → 1000 on the bench with one writer)

Section titled “D3. Narrow the workspaceFiles restore-tier exclusion — DONE, item 584 (reach test; tier 0 → 1000 on the bench with one writer)”

Refined 2026-09-22 after reading the pin (tests/local-shortcircuit.test.ts § “a workspace-output writer anywhere keeps an UNRELATED project out of the tier”, item 425). The pin’s reason is exact: “a root-anchored output can land in any project’s directory, including one with no edge to it”. So the narrowing is not “the declarer and its dependants” (the pin refuses that by name) but a REACH test, the same one workspaceInputsReach already makes for inputs: a task stays out of the tier when any workspace-output glob’s static prefix (staticPrefix) reaches its project directory, or reaches the prefix of one of its own workspaceFiles inputs; and every transitive dependant of an excluded task stays out too, because its up-front key folds the excluded task’s key, which is preliminary. A glob with no literal prefix (**/*.txt) reaches everything and keeps today’s graph-wide rule. The pin is then rewritten as two rows: an unrelated project the writer’s prefix cannot reach stays IN the tier (the new behaviour), one whose directory it reaches stays OUT (the old claim, now edge-free and path-based). Both rows plus the design note’s byte-identical matrix run before the change merges.

One cache.outputs.workspaceFiles anywhere costs the whole graph its restore tier. Exclude only tasks whose resolved outputs or inputs intersect a workspace-output path, plus their dependants; path-prefix on resolved paths, no glob walk. Measure the restore-tier count on a 1,000-project bench workspace with one workspaceFiles task (0 → ~999) and warm-restore time with an A/A control. Land before D1 or with D1’s pin extended.

D4. Launch — the agent’s half DONE, item 581 (notes drafted; canary and site green); the benchmark refresh needs the dev box; three owner items stand

Section titled “D4. Launch — the agent’s half DONE, item 581 (notes drafted; canary and site green); the benchmark refresh needs the dev box; three owner items stand”

Draft 0.1.0 release notes from merged PR titles since v0.0.21 into docs/history/release-0.1.0-notes.md; run the site build and the compiled-binary canary on the current head; re-run the real-repo benchmarks on 1.4.2 and refresh the site’s numbers. Then one line to the owner: three items, all theirs.

D5. The warm path (Next 6) — DONE, item 580 (a tie: 164.7 vs 167.8 ms, A/A 170.1 vs 169.2)

Section titled “D5. The warm path (Next 6) — DONE, item 580 (a tie: 164.7 vs 167.8 ms, A/A 170.1 vs 169.2)”

No run-path change this week. Candidates: resolveFiles after #667 (the negation fix walks per glob) and git-inputs.ts’s extra spawn on the non-git check. Measure both on the 1,000-project bench with the A/A control; if either moved the warm run past the noise floor, that is the next item.

The rejected list in CLAUDE.md, a vx-agents successor, a first-party remote, another sweep of a file whose failure mode is loud, a narrowing of local-shortcircuit.ts without its pin, a core-only Blob seam. And an empty Next list with a green main and a shipped release is the goal, not a gap to fill with process items.