Dogfooding `campaign` + `knowledge-refresh` against a throwaway ledger. Seven
defects, all found by running the commands as written and measuring, not by
reading them.
The headline pair only existed together. `knowledge-refresh` built
`STALE_AFTER="--stale-after 30"` and expanded it unquoted, trusting the shell to
split it in two. bash does; zsh — the macOS default, and what the Bash tool runs
here — does not. The CLI got one argv entry, matched no flag, and because it had
no unknown-flag branch, silently kept the 90-day default and reported "✓ All 14
register entries were re-verified within the last 90 days": a true-sounding
sentence about a threshold the user had just overridden. Fixing either half alone
leaves a silent wrong answer or a loud one; both are fixed, and a guard now
rejects any template that packs a flag and its value into one variable.
`knowledge-refresh` also read one register and wrote another: step 6 named an
unanchored `knowledge/best-practices.json` while the CLI reads
`${CLAUDE_PLUGIN_ROOT}/…`, which for an installed plugin is the cache. The
validation gate then ran the cached test against the cached register — green no
matter what was written. The two copies were byte-identical that day, which is
exactly why it was invisible.
`campaign` vouched for repos it could not read. `add /finnes/ikke` returned
`added` + exit 0; `refresh-tokens` then put the phantom in `swept[]` with a
0-token delta and left `skipped[]` empty, so the machine-wide bill claimed
coverage of three repos on a machine with two. Paths stay tracked — an unmounted
volume is a legitimate absence — but are reported as `addedUnverified`, and the
command names them.
Two class sweeps, both measured rather than assumed. `posture` was the single
scanner (1 of 14) whose fatal catch exited 1, which ux-rules defines as a normal
WARNING grade — a crash indistinguishable from a result. And all 13 payload
writers failed on a `--output-file` whose parent did not exist, which on a fresh
machine turned `campaign`'s first run into "the ledger may be corrupt"; they now
share `scanners/lib/write-output.mjs`.
Predicted breadth was too wide for the first time in five sessions: 6 of 8 CLIs
predicted to lack unknown-flag rejection, 4 measured. `drift` and `fix` already
reject them, via a construct the grep did not recognise — a grep matches an
implementation, the invariant is a behaviour. The sweep was rewritten to run each
CLI with a bogus flag and read the exit code.
Suite 1453 → 1469/0. Frozen snapshots untouched. `optimize-lens-cli` and
`token-hotspots-cli` share the unknown-flag defect and are deferred to the v5.14
argument-handling chunk with their positional-swallow arm; the count is recorded
in the guard rather than rounded down to zero.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012NHWjN8EnoxSqRvMTLK2NE
39 lines
1.9 KiB
JavaScript
39 lines
1.9 KiB
JavaScript
/**
|
|
* write-output — the one place a scanner's `--output-file` payload is written.
|
|
*
|
|
* Every command in this plugin follows the same contract (`.claude/rules/ux-rules.md`):
|
|
* run the scanner with `--output-file <path> 2>/dev/null`, check the exit code, then Read
|
|
* the file. The path the command chooses is frequently one it has never created — e.g.
|
|
* `commands/campaign.md` writes its report to
|
|
* `~/.claude/config-audit/sessions/campaign-report.json`, which on a fresh machine does not
|
|
* exist yet. That is precisely the FIRST run, the case campaign-cli otherwise handles
|
|
* gracefully by reporting `initialized: false`.
|
|
*
|
|
* Before this helper existed, all 13 payload writers called `writeFile` directly and threw
|
|
* ENOENT there. The exit code was 3, and the command's own exit-code table reads 3 as "the
|
|
* input is missing or corrupt" — so the user was told the ledger might be corrupt and
|
|
* warned off the one action that would have fixed anything. `saveLedger` had always created
|
|
* its parent directory; the payload write simply never did. The asymmetry was accidental.
|
|
*
|
|
* Creating the parent is the honest behaviour: the caller asked for a file at a path, and
|
|
* nothing about a missing intermediate directory is an error the caller can learn from.
|
|
*/
|
|
|
|
import { writeFile, mkdir } from 'node:fs/promises';
|
|
import { dirname } from 'node:path';
|
|
|
|
/**
|
|
* Write a scanner payload, creating the parent directory if needed.
|
|
*
|
|
* Signature-compatible with `writeFile(path, contents, encoding)` so call sites are a pure
|
|
* rename — the encoding argument is kept rather than defaulted away.
|
|
*
|
|
* @param {string} path - destination file
|
|
* @param {string} contents - serialized payload
|
|
* @param {string} [encoding='utf-8']
|
|
* @returns {Promise<void>}
|
|
*/
|
|
export async function writeOutputFile(path, contents, encoding = 'utf-8') {
|
|
await mkdir(dirname(path), { recursive: true });
|
|
await writeFile(path, contents, encoding);
|
|
}
|