fix(commands): stop writing files no later step can read, and payloads nobody asked for

Dogfooding the four read commands (posture, tokens, manifest, whats-active)
surfaced four defect classes, all in the seam between what a command template
promises and what the scanner behind it actually does.

M-BUG-40, fifth arm: posture wrote four temp files it could never read back.
#49 closed the $$/cross-block class in four commands, but posture survived it —
and so did the guard written to prevent exactly this. The guard compared each
$$ path to the block that created it, so a path written once and then read via
prose had no second occurrence to flag. Measured live: written from PID 21614,
read attempted from PID 23772. The invariant is now blanket (no $$ in any temp
path), which also caught fix.md and feature-gap.md.

M-BUG-43: 6 of 7 scanners write their payload to stdout when --raw/--json is
set even when --output-file was given, and the templates redirected only
stderr. Measured: posture 255 182 B, whats-active 35 922 B, drift 28 316 B,
manifest 23 825 B, tokens 8 768 B. fix and feature-gap never read the file they
wrote, so both recovered one letter grade from a quarter-megabyte dump.

tokens swallowed --json and --with-telemetry-recipe: documented, never
threaded, so --json returned the humanized payload where the docs promise
byte-stable v5.0.0 output.

M-BUG-42: manifest's render contract asked for {load}; the payload carries
loadPattern, so the Load column rendered blank for all 96 rows.

Four new tests (1449 -> 1453), each verified red before the fix. The
render-contract test checks {field} names against a live payload from a
fixture, since a hardcoded key list would drift. Frozen v5.0.0 snapshots
untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGCk9o27eWo9uXLjkZTXEq
This commit is contained in:
Kjell Tore Guttormsen 2026-08-01 20:40:07 +02:00
commit acd1cf1248
13 changed files with 336 additions and 21 deletions

View file

@ -8,6 +8,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- **`M-BUG-40`, fifth arm — `posture` wrote four temp files it could never read back.** #49 closed the
`$$`/cross-block class in four commands, but `posture.md` survived it, and so did the guard written
to prevent exactly this. The guard compared each `$$` path against the block that created it, so a
path written **once** and then read via prose ("Read the JSON output file using the Read tool") had
no second occurrence to flag. Measured live: the scanner wrote `/tmp/config-audit-posture-21614.json`
from PID 21614 while the next Bash call ran as PID 23772, and the read step had no path to hand the
Read tool at all. The invariant is now blanket — **no `$$` in any temp path in any command file**
which also caught `fix.md` and `feature-gap.md`. All five sites now use fixed literal paths, repeated
literally in every step that needs them.
- **`M-BUG-43` — commands leaked whole JSON payloads into the transcript.** Every scanner except
`scan-orchestrator` writes its payload to stdout when `--raw`/`--json` is set, **even when
`--output-file` was given** — and the command templates redirected only stderr. Measured on a real
repo: `posture` 255 182 B, `whats-active` 35 922 B, `drift` 28 316 B, `manifest` 23 825 B, `tokens`
8 768 B. `fix` and `feature-gap` were the worst case: both ran posture with `--json`, never read the
file they wrote, and in practice recovered a single letter grade from a quarter-megabyte dump — in
the plugin that exists to cut token cost. 13 invocations across 10 command files now redirect stdout,
and the two commands that needed the data read it from their output file instead.
- **`tokens` swallowed two documented flags.** `--json` and `--with-telemetry-recipe` were listed as
recognized flags but never threaded into the CLI call, so `--json` returned the *humanized* payload
where the docs promised byte-stable v5.0.0 output (measured: 4/4 findings carried humanizer fields,
and the title read "Your file starts with content that changes between turns" instead of
"Cache-breaking volatile content at top of CLAUDE.md"), while `--with-telemetry-recipe` silently
produced no `telemetry_recipe_path` — the very flag the command's own closing tip recommends.
- **`M-BUG-42``manifest` asked for a field the scanner never emits.** The render contract used
`{load}`; the payload carries `loadPattern`. The Load column — which the command's own prose calls
the whole point of the view — would render blank for all 96 rows. `posture`'s headline had the same
shape (`{qualityAreaCount}`, never emitted) and now takes its count from the humanized scorecard
rather than `areas.length`, which counts a Feature Coverage row the table below deliberately excludes.
### Added
- Four command-template shape tests (1449 → 1453), each verified to fail before the fix: a blanket
`$$` ban, stdout-redirect discipline for any scanner invoked with `--output-file` in raw/json mode,
flag threading from prose to shell, and a render-contract test that checks every `{field}` against a
**live payload generated from a fixture** rather than a hardcoded key list, which would drift.
- **`M-BUG-40` — command templates assumed shell state survives between fenced blocks.** It does not:
every ```` ```bash ```` fence is executed as its own Bash call, in its own process. A variable
assigned in one block is empty in the next, and `$$` (the PID) differs between calls, so a