REVIEWED_TREE: 7eb34e9e9d9aaa17ee0bad89281392cd611f15f2 /workspace/gstack-evals

**Scope.** I read the full diff against 97e4796 for every source file: `src/waste/crewlog.ts`, `src/waste/README.md`, `src/cli.ts`, `src/errors/codes.ts` and `docs/errors.md`. The test files were reviewed in summary mode only, so the coverage reduction is visible: `tests/waste/crewlog.test.ts` (+378, 48 tests on shell parsing, bash traits, thread exports, receipts, per-PR data and CLI errors) and `tests/cli.test.ts` (+1/-1, adds `waste` to PLANNED). At this tree, `bun test tests/waste tests/cli.test.ts` passes (48/48), and so do `bun run typecheck` and `bun run errors:check`. I checked findings against real inputs where I could: this crew's thread export shape, 11 gstack-evals PR bodies, the 7 FOUN-64 per-PR data files, and gstack 1.93.39.0's receipt code. I left no changes in the tree.

**1. R5 drops every PR with zero fix-checks from the rounds count, which inflates the past-cap share. FIXABLE.**
crewlog.ts:618-619 and 625-626 (`receiptLoop`) and 730-731 (`scanPrs`) only compute rounds when `fix_checks` is present. gstack leaves it out when it is 0 (`if (fixChecks) out.fix_checks = fixChecks;`, review-scope.ts:458). `REVIEW_LOOP:` lines never print `rounds=` and also leave out a zero `fix_checks`. gstack's own rule is `rounds ?? passes + (fix_checks ?? 0)` (ship-receipt-depth.ts:259).
- **Real receipts:** PR #110's receipt (`passes:1`, no `fix_checks` or `rounds`) comes back with `rounds: null`.
- **FOUN-64 data:** `waste prs` measures rounds for 12 of 31 receipt PRs in gbrain, 13 of 20 in gstack-evals and 20 of 37 in truther. Overall it reports 27 of 45 PRs past the cap (60%, mean 3.71 rounds). gstack's rule over the same data gives 30 of 88 (34%, mean 2.72).

The dropped PRs are the low-round ones, so this is selection bias in the in-the-wild R5 baseline, not random noise. Fix: use gstack's fallback in all three places. If an older receipt's missing field really means "unknown", count it in a visible lower-bound bucket instead of dropping it. Severity: High. Blocks: yes (the next step publishes exactly this number from exactly this data).

**2. R1 cannot see re-runs of test runners that print timings. INVESTIGATE.**
Two back-to-back `bun test tests/waste | tail -4` runs with no change between them differ only in `[32.00ms]` vs `[25.00ms]`. Scanning them gives `r1_checks: 0, informative_reruns: 1`. README:42 labels that bucket "a flake or a real change", which is wrong for timing noise. jest, vitest, pytest, go test and cargo test all print elapsed times, so `r1_checks` sits near zero for anything but quiet tools like tsc. The GEVA-220 plan's R1 already names a sensitivity reading that runs a frozen normalizer (durations, ISO timestamps, PIDs, temp-path suffixes) before comparing; the reader doesn't implement it. The exact-match primary rule follows the plan, so whether the crew baseline needs the normalized reading is a call for whoever owns the plan. Severity: Medium. Blocks: no.

**3. Substitutions hide tree changes from the lenient R1 boundary. FIXABLE.**
`parseShell` (line 102, and the `$(` handling inside double quotes) keeps `$(...)` and backtick bodies as plain words and never parses them. A run of `bun run typecheck`, then `R=$(python3 scripts/fix_imports.py 2>&1); echo "$R"`, then `bun run typecheck` scores `r1: 1, r1_checks: 1`, even though the script edited the tree. Only `r1_strict` catches it. A related case: inside a `$(cat <<'EOF' ...)` commit message, an apostrophe in the here-doc body throws off `closeParen`'s single-quote scan, so the rest of the command (for example `&& git push`) is swallowed and `pushed_head_checks` undercounts. Smaller gaps: `env -u X git push` and `nice -n 10 cmd` take the flag's value as the program name, and `FIX_FLAGS` treats `python -u -m pytest` (unbuffered output) as an update flag. Fix: parse substitution bodies recursively and merge their traits into the segment. Severity: Low. Blocks: no.

**4. Some bad exports are accepted or misreported. FIXABLE.**
An empty export file exits 0 and adds a session with 0 items to the totals, so a failed export step is counted silently. An item with `calls: [null]` (line 490), or a non-string `input.text` reaching `.trim()`, exits through E_INTERNAL saying "a defect in src/waste/crewlog.ts, not in your input". The README promises E_WASTE_INPUT for input problems. Fix: refuse an export with zero items, and check the call shape in `readExport`. Severity: Low. Blocks: no.

**5. The output prints model ids taken from the input. FIXABLE.**
Line 486 copies each assistant item's `model` string into the output as an object key. That breaks the README's "never prints any other text from what it reads" claim. A model id that happens to be an inherited property name such as `constructor` also turns that count into a string. It is harmless for real Capy exports, but the privacy promise should either say this or the code should map the ids to a fixed set. Severity: Low. Blocks: no.

Recommendation: Fix the R5 rounds fallback at crewlog.ts:619/626/731 because it drops 43 of 88 receipt-bearing PRs in the real FOUN-64 data and reports 60% of PRs past the cap where gstack's own rule gives 34%.
