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
124 lines
4.1 KiB
JavaScript
124 lines
4.1 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* campaign-cli — read-only reporter for the durable machine-wide campaign ledger
|
|
* (v5.7 Fase 2, Block 3b).
|
|
*
|
|
* Mirrors the knowledge-refresh-cli precedent: it is the DETERMINISTIC, READ-ONLY half
|
|
* of the hybrid motor. It loads the campaign ledger (the durable file that sits ABOVE
|
|
* individual config-audit sessions), validates it, and emits the repo list + a
|
|
* machine-wide roll-up as JSON. It NEVER writes the ledger — initialization and every
|
|
* status transition belong to the command layer (Block 3c `/config-audit campaign`),
|
|
* which calls the lib's pure transforms + saveLedger only on explicit, human-approved
|
|
* action. A missing ledger file is reported gracefully (initialized:false), NEVER created.
|
|
*
|
|
* Naming: `-cli` suffix → NOT an orchestrated scanner (the scan-orchestrator only loads
|
|
* scanner modules), so the scanner count is unchanged and the snapshot suite stays
|
|
* byte-stable.
|
|
*
|
|
* Usage:
|
|
* node campaign-cli.mjs [--ledger-file <path>] [--output-file <path>]
|
|
*
|
|
* Exit codes: 0 = initialized & valid, 1 = not initialized yet (advisory), 3 = error.
|
|
*/
|
|
|
|
import { resolve } from 'node:path';
|
|
import { writeOutputFile } from './lib/write-output.mjs';
|
|
import {
|
|
loadLedger,
|
|
validateLedger,
|
|
rollUp,
|
|
buildBacklog,
|
|
defaultLedgerPath,
|
|
} from './lib/campaign-ledger.mjs';
|
|
|
|
/**
|
|
* Usage error. Throws rather than calling process.exit(): exit() discards
|
|
* unflushed stdout when stdout is a pipe. The top-level catch prints the same
|
|
* `Error: ` text and sets the same exit code 3, so callers see no difference.
|
|
*/
|
|
class CliUsageError extends Error {}
|
|
|
|
function fail(message) {
|
|
throw new CliUsageError(message);
|
|
}
|
|
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
let ledgerFile = null;
|
|
let outputFile = null;
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const a = args[i];
|
|
if (a === '--ledger-file' && args[i + 1]) ledgerFile = args[++i];
|
|
else if (a === '--output-file' && args[i + 1]) outputFile = args[++i];
|
|
// A flag we do not understand must fail loudly. Silently dropping it lets a
|
|
// caller-side mistake — a typo'd `--ledger-file`, or a shell that did not
|
|
// word-split "--flag value" into two argv entries — produce a confident
|
|
// report about the wrong ledger.
|
|
else if (a.startsWith('--')) fail(`unknown flag "${a}"`);
|
|
}
|
|
|
|
const ledgerPath = resolve(ledgerFile || defaultLedgerPath());
|
|
|
|
let ledger;
|
|
try {
|
|
// loadLedger returns null on ENOENT (graceful first run) and throws on parse error.
|
|
ledger = await loadLedger(ledgerPath);
|
|
} catch (err) {
|
|
fail(`could not read ledger at ${ledgerPath}: ${err.message}`);
|
|
}
|
|
|
|
let payload;
|
|
let exitCode;
|
|
|
|
if (ledger === null) {
|
|
// Graceful first run — the ledger does not exist yet. We DO NOT create it; that is
|
|
// the command layer's job (Block 3c), on explicit human-approved action.
|
|
payload = {
|
|
status: 'ok',
|
|
initialized: false,
|
|
ledgerPath,
|
|
schemaVersion: null,
|
|
createdDate: null,
|
|
updatedDate: null,
|
|
repos: [],
|
|
rollUp: rollUp({ repos: [] }),
|
|
backlog: buildBacklog({ repos: [] }),
|
|
};
|
|
exitCode = 1; // advisory: there is no campaign to report yet
|
|
} else {
|
|
const { valid, errors } = validateLedger(ledger);
|
|
if (!valid) {
|
|
fail(`ledger at ${ledgerPath} is invalid:\n - ${errors.join('\n - ')}`);
|
|
}
|
|
payload = {
|
|
status: 'ok',
|
|
initialized: true,
|
|
ledgerPath,
|
|
schemaVersion: ledger.schemaVersion,
|
|
createdDate: ledger.createdDate,
|
|
updatedDate: ledger.updatedDate,
|
|
repos: ledger.repos,
|
|
rollUp: rollUp(ledger),
|
|
backlog: buildBacklog(ledger),
|
|
};
|
|
exitCode = 0;
|
|
}
|
|
|
|
const json = JSON.stringify(payload, null, 2);
|
|
if (outputFile) await writeOutputFile(outputFile, json, 'utf-8');
|
|
else process.stdout.write(json + '\n');
|
|
|
|
process.exitCode = exitCode;
|
|
}
|
|
|
|
const isDirectRun =
|
|
process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname);
|
|
if (isDirectRun) {
|
|
main().catch((err) => {
|
|
const prefix = err instanceof CliUsageError ? 'Error' : 'Fatal';
|
|
process.stderr.write(`${prefix}: ${err.message}\n`);
|
|
process.exitCode = 3;
|
|
});
|
|
}
|