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.
- 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.
- Hygiene, one day: F5 (STATUS trim), then I1 and I4, because the next agent reads CLAUDE.md and STATUS before anything else.
- 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 indocs/caching.mdbeside the ignored-input rule. - Acceptance: a workspace with a gitignored
gen/holding one file and a task withfiles: ['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.txtbeside the literal must not settlegen. - Then grep the class: every
Bun.file(...).exists()insrc/(git grep '\.exists()' -- packages/vx/src); readproject-loader.ts,migration.tsandworkspace.tsfor 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 routetoRealPath(sock)through it. KeepsbplPathfor 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.tsused everywhere. The sandbox sites keep their semantics only if a test shows a brace bind spec must NOT walk — measure first: a bind ofsrc/{a,b}throughsandbox-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
citask refuses (not warns) belowMIN_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.zipworks through the proxy wherebun upgradedoes 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.
Improvements
Section titled “Improvements”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”Bun.file(<dir>).exists() gets a pinA grep-based row in module-shape-drift.test.ts over
Bun.file\(.*\)\.exists\(\) in src/ with an allowlist of the
file-path sites.
Development
Section titled “Development”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)”overlapping-outputs-2026-09.md)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;cleanOutputsalready 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 (
upstreamOutputProjectsfor same-project, the graph-wideworkspaceFilesexclusion inlocal-shortcircuit.tsfor 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)”workspaceFiles restore-tier exclusionRefined 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.
Not this quarter
Section titled “Not this quarter”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.