name: corpus-remeasure
description: The iteration cycle for the build-jail corpus — change the harness or nub, push it, re-measure ONE package@version on ONE platform, and read the answer. Invoke (via the Skill tool) whenever you need to test a hypothesis about why a package got the grant it did, verify a harness or nub fix on a real runner, or re-measure specific packages without disturbing the corpus. Carries the traps that each cost a wasted run: a bare git commit that clobbers the queue, --force argument order, a watcher that reports a pass off an empty log, and the fact that a grant is not evidence until you read the failing cell's log.
Re-measuring a package in the build-jail corpus
The loop is: edit → push → dispatch a debug run → watch → read the CELL LOGS. Each step has a trap that has already cost a run.
Corpus repo: nubjs/build-jail-corpus (public). Local clone: ~/nub-corpus-staging. nub: branch sandbox/integration at ~/.cache/nub/worktrees/integ.
1. Push the harness change — the queue is the trap
⛔ ALWAYS git commit -- <explicit paths>. A bare git commit sweeps in whatever is staged, and queue.ndjson is almost always staged because reading the corpus stages it. A commit carrying a stale queue releases rows other runners are actively measuring. Measured: one such commit carried 194 lines of stale queue.
cd ~/nub-corpus-staging
git commit -q -m "…" -- harness/search.mjs
git show --stat --oneline HEAD | head -5
The push races ~12 concurrent runners, so it needs a retry loop — and the cleanup before each attempt is what makes it work:
for a in 1 2 3 4 5; do
git restore --staged . 2>/dev/null
git checkout -- records queue.ndjson 2>/dev/null
git clean -fdq -- records 2>/dev/null
git fetch -q origin main
git rebase -q origin/main >/dev/null 2>&1 || { git rebase --abort 2>/dev/null; sleep 3; continue; }
git push origin HEAD:main >/dev/null 2>&1 && { echo "PUSHED"; break; }
sleep 3
done
git fetch -q origin main
git merge-base --is-ancestor HEAD origin/main && echo "✓ LANDED" || echo "⛔ NOT LANDED"
⛔ Verify it landed. Never infer it from the push command's apparent success — read merge-base --is-ancestor, because a rejected push inside a loop is easy to miss.
Reading the corpus for analysis leaves two kinds of debris, both of which break the next rebase: git checkout origin/main -- records STAGES those files, and it leaves records your HEAD lacks as UNTRACKED. Before cleaning untracked records, confirm they exist at origin (git cat-file -e origin/main:<path>) so a clean can never destroy an unpushed measurement.
2. Dispatch the re-measure
The workflow has a packages debug input. When set, the runner SKIPS the queue entirely — no claim, no completion, no commit of results — and measures exactly those specs with --force, publishing only the ARTIFACT. That is what makes it safe to run against a live corpus.
gh workflow run corpus-queue-runner.yml -R nubjs/build-jail-corpus \
-f os=macos \
-f nub_sha=<40-char sha> \
-f chain=false \
-f slice=2 \
-f packages="husky@3.1.0 detox@20.9.1"
- ⛔
nub_sha must be PUSHED. CI clones nubjs/nub anonymously. Check with git ls-remote https://github.com/nubjs/nub sandbox/integration before dispatching, or the run dies at clone.
- Pick the sha deliberately. Using the sha the fleet already built gets a binary-cache hit (~0 min instead of a ~26 min cold Windows build) AND makes the result comparable with corpus records. Use a NEW sha only when testing a nub change.
- Platform speed differs enormously. macOS/Linux ≈ 1 min/package; Windows ≈ 9 min/package average and up to ~40 for a package that walks the full 55-cell ladder, plus ~26 min for a cold build. Prototype on macOS or Linux whenever the question is not Windows-specific.
- ⛔ In
run-batch.sh, --force must come BEFORE --file — it is parsed only as the first argument after <nub>. The workflow already does this. A run with the order wrong "succeeded" in 17 seconds having measured nothing.
3. Watch it — and gate the watcher, or it will lie to you
⛔ NEVER dispatch CI without a bounded background watcher. A stall is silent. And the watcher itself must gate, because gh run view --log returns an EMPTY string for an in-progress run — so grep -q <bad-thing> returns false and reports a PASS off nothing. That exact bug appeared inside a gate written to prevent it.
Three gates, in order, all mandatory:
- Terminal state, on a BOUNDED loop that exits non-zero on timeout (
for i in $(seq 1 240); do … sleep 45; done) so it can never hang forever.
- Log non-empty —
wc -l over 100 lines, else refuse to interpret.
- The canary RAN — assert
fixture canary OK: <n> files is PRESENT, never infer it from the absence of REFUSING TO RUN.
The canary is the harness's own guard: it installs puppeteer@25.4.0 and refuses the whole batch unless the fixture produces >5,000 files. If it refuses, nothing below it means anything — that is how a broken isolation change gets caught before it writes garbage.
4. Read the CELL LOGS, not just the grant
⛔ THE SINGLE MOST EXPENSIVE LESSON HERE. Three consecutive fixes for one Windows defect were wrong because each was theorised from a grant and a path list. The failing cell's log named the exact cause in one line, every time.
Per-cell logs are gitignored, so they exist ONLY in the run artifact — and only when the batch actually ran:
gh run download <run-id> -R nubjs/build-jail-corpus -D /tmp/art -n records-<os>-<run-id>
find /tmp/art -path '*<pkg>*' -name '*.log'
Read the cell one rung below the winner — that is where the reason lives. Read it RAW before grepping: a grep that matches nothing looks identical to a clean run. Real examples, each of which redirected an investigation:
| log line | what it actually meant |
|---|
spawnSync git EPERM | needs to EXECUTE git — an exec denial, not a write denial |
mkdir '…\Packages\nub_sbx_<pid>_<nonce>_0' | the leaf is the PROFILE dir, not Packages — a fixture cannot pre-create it |
Could not find JAVA, skipping … | a real line that was the WRONG explanation — both platforms skipped identically |
| (no error at all) | a silent partial install; the cell failed on DIGEST, not exit code |
⛔ A record's grant tells you WHAT was needed; only the log tells you WHY. And a plausible log line can still be the wrong explanation — check whether it differs across the platforms that disagree.
5. Attribute the result
corpusGitSha in provenance names the corpus commit exactly. Use it — harnessSha256 is a content hash and git rewrites line endings on Windows, so the same commit hashes differently there and cannot be compared across platforms.
- Confounds before blaming a platform: same
nubGitSha? same nodeSelection.chosenMajor? And note darwin records are arm64 while linux records are x64, so OS and ARCH move together and cannot be separated from these records alone.
- A debug run writes NOTHING to the corpus — results live only in the artifact. To make a re-measurement stick, let the fleet re-claim the row (reopen it) rather than hand-editing records.
Scope
Use it for a hypothesis about one package, verifying a harness or nub fix on a real runner, or getting cell logs for a package whose artifact expired (30-day retention).
Not for running the corpus — that is the self-dispatching queue chain (chain=true). Not for local iteration either: build nub locally and run harness/search.mjs directly against a /tmp fixture when the question does not need a specific OS.
Related: harness/divergence-scan.mjs finds packages whose grant disagrees across OSes and ranks them — the usual source of the hypothesis you are about to test.