Completes the THIN machine-wide campaign surface (ledger + roll-up + status). The write half of the campaign motor, mirroring how knowledge-refresh gates register writes: - scanners/campaign-write-cli.mjs (-cli → NOT a scanner; 11 tests): init/add/ set-status, each a thin wrapper over the invariant-enforcing lib transforms (createLedger/addRepo/setRepoStatus) + saveLedger — path-normalization/dedup, idempotent add, status-lifecycle guard and updatedDate bump never hand-rolled. init refuses to clobber an existing/corrupt ledger (exit 1, file untouched); add auto-inits + reports added vs skipped; set-status takes --findings/--session. Deterministic: --reference-date is the only clock read, injected as `now`. Exit 0=write, 1=advisory no-op, 3=error. - commands/campaign.md (opus, no Web): thin orchestrator — always reports first (read-only campaign-cli), then proposes init/add/set-status and invokes ONE write-CLI subcommand only on explicit human approval (Verifiseringsplikt; never hand-edits the JSON). add --discover finds git repos under a root to pick from. Not byte-stable (own command, outside snapshot suite) like /config-audit optimize + knowledge-refresh. Both CLIs are -cli → scanner count stays 15, snapshot suite untouched. commands 20→21, suite 1127→1138 (+11), test files 64→65. Docs: CLAUDE.md §campaign-write-cli + command table + Testing badge; README commands badge 20→21 + table row; help.md + router wiring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.1 KiB
| name | description | argument-hint | allowed-tools | model |
|---|---|---|---|---|
| config-audit:campaign | Machine-wide audit campaign — track which repos are pending/audited/planned/implemented across sessions, with a machine-wide roll-up. Human-approved writes only. | [init | add <path>... | add --discover <root> | set-status <path> <status>] | Read, Write, Edit, Bash, Glob | opus |
Config-Audit: Campaign
A single config-audit session audits one scope. A campaign sits above sessions: a
durable ledger of every repo you mean to bring up to standard, each repo's lifecycle status
(pending → audited → planned → implemented), and a machine-wide roll-up of findings by
severity. It persists to ~/.claude/config-audit/campaign-ledger.json — outside the
plugin dir, next to sessions/ — so it survives plugin uninstall/reinstall/upgrade and
resumes across sessions.
The Iron rule (Verifiseringsplikt): nothing is ever auto-written. Reporting is read-only. Every mutation — creating the ledger, adding a repo, changing a status — is proposed first and applied only on explicit approval, by invoking one deterministic write-CLI subcommand. The command never hand-edits the ledger JSON.
This is the THIN campaign surface (ledger + roll-up + status). Cross-repo backlog prioritization and execution are a later block — this command does not run audits or apply fixes itself; it tracks where each repo stands.
Two CLIs back this command
- Read (report):
scanners/campaign-cli.mjs— loads + validates the ledger, emits the repo list + roll-up. Never writes. - Write (mutate):
scanners/campaign-write-cli.mjs—init/add/set-status, each a thin wrapper over the invariant-enforcing lib transforms + save. Invoked only after the user approves a specific action.
Both take --ledger-file <path> (defaults to the durable path) and --output-file <path>;
the write-CLI also takes --reference-date <YYYY-MM-DD> (the audit date stamp).
Implementation
Step 1: Parse arguments
From $ARGUMENTS, pick the mode:
- (empty) or
report→ report (read-only). Default. init→ initialize the ledger.add <path>...→ add one or more repo paths.add --discover <root>→ find git repos under<root>and let the user pick which to add.set-status <path> <status>→ transition a tracked repo (status∈ pending/audited/planned/implemented).help→ show this surface and stop.
Set a shared date stamp for any write: TODAY=$(date +%F).
Step 2: Always report current state first
Whatever the mode, start by showing where the campaign stands (read-only):
node ${CLAUDE_PLUGIN_ROOT}/scanners/campaign-cli.mjs \
--output-file ~/.claude/config-audit/sessions/campaign-report.json 2>/dev/null; echo $?
Exit 0 = a campaign exists, 1 = not initialized yet (advisory — normal first run), 3 = real error → "The campaign ledger couldn't be read — it may be corrupt." (Stop; do not attempt a write over a corrupt ledger.)
Read ~/.claude/config-audit/sessions/campaign-report.json with the Read tool (per the UX
rules — never show the raw JSON). It has initialized, repos[] (each: path, name, status, sessionId, findingsBySeverity, updatedDate), and rollUp {totalRepos, byStatus, bySeverity, reposWithFindings}.
Present it as two short tables:
Campaign roll-up
| Status | Repos |
|---|---|
| pending / audited / planned / implemented | … |
…plus a one-line severity total across audited repos (e.g. "Findings so far: 3 critical, 8 high, 5 medium, 12 low across 4 audited repos").
Repos
| Repo | Status | Findings (C/H/M/L) | Last updated |
|---|
If initialized is false, say so plainly: "No campaign yet. Run /config-audit campaign init
to start one." Then — if the mode was init or add — continue to that step (those bootstrap
a campaign); for report/set-status on an uninitialized ledger, stop after this message.
Step 3 (mode init): Initialize
If already initialized, say so and stop (no clobber). Otherwise tell the user what will happen, then create it:
node ${CLAUDE_PLUGIN_ROOT}/scanners/campaign-write-cli.mjs init \
--reference-date "$TODAY" \
--output-file ~/.claude/config-audit/sessions/campaign-write.json 2>/dev/null; echo $?
Exit 0 = created, 1 = already initialized (advisory). Confirm: "Campaign ledger created
at ~/.claude/config-audit/campaign-ledger.json." Then suggest add.
Step 4 (mode add): Add repos — propose, approve, write
Gather candidates.
- Explicit paths: use the paths given after
add. --discover <root>: find git repos (depth-limited), e.g.
Present the discovered repos as a numbered list and ask which to add (and confirm any that are already tracked will be skipped). Use Glob as a fallback iffind "<root>" -maxdepth 3 -type d -name .git 2>/dev/null | sed 's:/\.git$::'findis unavailable.
Confirm, then write. Show the final list and ask for explicit approval. On approval, add them in one call (idempotent — already-tracked repos are skipped, not reset):
node ${CLAUDE_PLUGIN_ROOT}/scanners/campaign-write-cli.mjs add <path1> <path2> ... \
--reference-date "$TODAY" \
--output-file ~/.claude/config-audit/sessions/campaign-write.json 2>/dev/null; echo $?
(For a single repo with a custom display name, add --name "<name>".) Read the result file and
report what was added vs skipped, then re-show the repo table.
Step 5 (mode set-status): Transition a repo — propose, approve, write
Confirm the repo is tracked (from Step 2's report) and that status is one of
pending/audited/planned/implemented. State the transition ("<name>: pending → audited") and
ask for approval.
When marking a repo audited, optionally attach its findings-by-severity so the machine-wide roll-up stays meaningful. Two honest sources, in order of preference:
- If the repo was audited in a config-audit session, read that session's finding counts and
build
{"critical":C,"high":H,"medium":M,"low":L}— pass--session <id>too. - Otherwise, use counts the user provides. Never invent counts (Verifiseringsplikt) — if
none are available, transition the status without
--findings.
On approval:
node ${CLAUDE_PLUGIN_ROOT}/scanners/campaign-write-cli.mjs set-status <path> <status> \
--reference-date "$TODAY" \
[--findings '{"critical":0,"high":0,"medium":0,"low":0}'] [--session <id>] \
--output-file ~/.claude/config-audit/sessions/campaign-write.json 2>/dev/null; echo $?
Exit 3 = invalid status, untracked repo, or no ledger → report the message plainly and do not retry blindly. On success, read the result and re-show the updated roll-up + repo row.
Step 6: Next steps
Tailor to where the campaign stands:
- Just initialized / few repos: "
/config-audit campaign add --discover ~/reposto enroll your repos." - Pending repos exist: "Run
/config-auditin a pending repo to audit it, then/config-audit campaign set-status <path> auditedto record the result here." - Audited but not planned: "
/config-audit planin that repo, then mark itplanned." - Always: the campaign survives this session — re-run
/config-audit campaignanytime to see the machine-wide picture.
Notes
- Read-only report, human-approved writes.
campaign-clinever writes; every mutation goes throughcampaign-write-cliand only after explicit approval, exactly mirroring how/config-audit knowledge-refreshgates register writes. - Deterministic core, not byte-stable command. The lib transforms + both CLIs are
unit-tested and deterministic (
--reference-dateinjected); this command's orchestration is judgment-driven and deliberately not in the snapshot suite (like/config-audit optimizeandknowledge-refresh). - The
-clisuffix keeps both CLIs out of the scan-orchestrator, so the scanner count and the byte-stable snapshot suite are unaffected. - THIN scope: ledger + roll-up + status only. Cross-repo backlog prioritization and execution are a later block — this command tracks state, it does not run audits or apply fixes.