Completes Block 4 (4b backlog + 4c export/execution). Asymmetric: plan
export is new testable code; execution is pure reuse of the existing
per-repo implement/rollback (no new execution machinery), per the plan's
"reuse existing backup/rollback".
Plan export ("planer følger arbeidsstedet"):
- scanners/lib/campaign-export.mjs (pure, now injected, 8 tests):
planExportPath(repo,sessionId) -> <repo>/docs/config-audit-plan-<sessionId>.md
(sessionId-keyed so same-day re-audits never collide);
buildPlanExportDocument({...,now}) -> provenance header + verbatim plan.
- scanners/campaign-export-cli.mjs (-cli, read-only by default, 10 tests):
--repo resolves the repo's linked session, reads its action-plan.md,
assembles the doc, emits {exportable,problems,targetPath,document}. Two
gates -> exit 1 advisory: no-session-linked / no-action-plan. Writes the
file ONLY under opt-in --write (byte-faithful copy; the LLM never re-types
a 200-line plan). --sessions-dir override for hermetic tests; exit 0/1/3.
Command: commands/campaign.md gains an `export <path>` mode (Step 6:
preview -> approve -> --write), then routes the user to the existing
/config-audit implement (backup + verify) + rollback + set-status
implemented. Nothing auto-written (Verifiseringsplikt).
Byte-stable: lib + -cli + command-doc only -> scanner count stays 15,
agents 7, commands 21 (export is a mode, not a new command), SC-5 +
backcompat suite untouched. suite 1150->1168. Block 4a (migrateLedger)
still deferred to the first breaking schema change.
Docs: CLAUDE.md section + badge 1150->1168/65->67 files; README badge +
campaign row + Testing prose (fixed stale 1055/59 -> true 1168/67).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
13 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>... | set-status <path> <status> | export <path>] | 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 + a cross-repo prioritized
backlog to pick from + plan export). It does not run audits or apply fixes itself: it tracks
where each repo stands, shows what to tackle next, exports a planned repo's plan into that repo's
own docs/, and points at the existing per-repo /config-audit implement (backup + apply +
verify) and /config-audit rollback for execution — Block 4c reuses that machinery, it does not
reinvent it.
Three CLIs back this command
- Read (report):
scanners/campaign-cli.mjs— loads + validates the ledger, emits the repo list + roll-up + backlog. 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. - Export:
scanners/campaign-export-cli.mjs—--repo <path>resolves the repo's linked session, reads itsaction-plan.md, and assembles adocs/config-audit-plan-<sessionId>.md. Read-only (a preview) by default; it writes the file only under--write, which is invoked only after the user approves. The CLI writes the file byte-faithfully — the plan is never re-typed.
All take --ledger-file <path> (defaults to the durable path) and --output-file <path>; the
write-CLI + export-CLI also take --reference-date <YYYY-MM-DD> (the date stamp), and the
export-CLI takes --sessions-dir <path> (defaults to ~/.claude/config-audit/sessions).
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).export <path>→ export a planned repo's action plan into that repo's owndocs/.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), rollUp {totalRepos, byStatus, bySeverity, reposWithFindings}, and backlog[] — the single cross-repo prioritized work list (each:
path, name, status, findingsBySeverity, totalFindings, weightedScore, rank), already sorted
DESC by severity (most critical work first).
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 |
|---|
Prioritized backlog — the one cross-repo list to pick from, highest-severity work first.
Render backlog[] in rank order (it is already sorted); omit this table entirely when the
backlog is empty (nothing outstanding — say "Backlog clear — no outstanding findings across
tracked repos."). Implemented repos and repos with no known findings are deliberately absent.
| # | Repo | Status | Findings (C/H/M/L) | Total |
|---|
After it, point the user at the top item: "Highest priority: <name> (<status>) — pick it
with /config-audit (audit), /config-audit plan, or /config-audit implement in that repo,
then record progress here with set-status." The backlog is a pick-list, not an executor —
this command does not run audits or fixes (that is the later execution block).
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 (mode export): Export a repo's plan to its own docs/ — preview, approve, write
"Planer følger arbeidsstedet": a planned repo's action plan belongs in that repo's docs/,
not buried in a session dir. This step copies it there, byte-faithfully.
Preview first (read-only — never writes). The repo must be tracked and have a linked session
that carries an action-plan.md (i.e. /config-audit plan has run there). Run without --write:
node ${CLAUDE_PLUGIN_ROOT}/scanners/campaign-export-cli.mjs --repo "<path>" \
--reference-date "$TODAY" \
--output-file ~/.claude/config-audit/sessions/campaign-export.json 2>/dev/null; echo $?
Exit 0 = previewable, 1 = tracked but not exportable yet, 3 = error (untracked repo,
no/corrupt ledger). Read ~/.claude/config-audit/sessions/campaign-export.json with the Read tool.
- Exit 1 — read
problemsand guide, then stop (nothing to export):no-session-linked→ "<name>has no linked audit session. Link one with/config-audit campaign set-status <path> <status> --session <id>, or audit + plan it first."no-action-plan→ "<name>'s session has no plan yet. Run/config-audit planin that repo first, mark itplanned, then export."
- Exit 0 — show, then ask. Tell the user the destination (
targetPath) and a short preview — the first ~12 lines ofdocumentonly, never the whole file, never the raw JSON (UX rules). Ask for explicit approval to write it.
On approval, write it (the CLI does the faithful copy — do NOT hand-write the file):
node ${CLAUDE_PLUGIN_ROOT}/scanners/campaign-export-cli.mjs --repo "<path>" --write \
--reference-date "$TODAY" \
--output-file ~/.claude/config-audit/sessions/campaign-export.json 2>/dev/null; echo $?
Confirm: "Plan exported to <targetPath>." Then hand off to the existing execution machinery
(Block 4c reuses it — this command does not run it for you):
To execute: run
/config-audit implementin<path>— it backs up every changed file, applies the plan, and verifies. To undo:/config-audit rollback. When done, record it:/config-audit campaign set-status <path> implemented.
Step 7: 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." - Planned repos exist: "
/config-audit campaign export <path>to drop the plan into that repo's owndocs/, then/config-audit implementthere to execute it (backup + verify)." - Backlog has items: point at the top backlog repo and the natural next verb for its status (audit → plan → export → implement).
- 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 — ledger changes viacampaign-write-cli, plan exports viacampaign-export-cli --write— happens only after explicit approval, exactly mirroring how/config-audit knowledge-refreshgates register writes. The export-CLI's default (no--write) is a read-only preview. - Deterministic core, not byte-stable command. The lib transforms + all three 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 all three CLIs out of the scan-orchestrator, so the scanner count and the byte-stable snapshot suite are unaffected. - THIN scope: ledger + roll-up + status + a cross-repo prioritized backlog + plan export.
Execution is not reinvented here —
exportdrops a planned repo's plan into its owndocs/(a durable record), and the user runs the existing/config-audit implement(backup + apply + verify) +/config-audit rollbackto execute and undo. This command tracks state and routes the work; it does not run audits or apply fixes itself.