--- name: config-audit:campaign description: Machine-wide audit campaign — track which repos are pending/audited/planned/implemented across sessions, with a machine-wide roll-up. Human-approved writes only. argument-hint: "[init | add ... | add --discover | set-status ]" allowed-tools: Read, Write, Edit, Bash, Glob model: 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 ` (defaults to the durable path) and `--output-file `; the write-CLI also takes `--reference-date ` (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 ...` → add one or more repo paths. - `add --discover ` → find git repos under `` and let the user pick which to add. - `set-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): ```bash 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: ```bash 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 `: find git repos (depth-limited), e.g. ```bash find "" -maxdepth 3 -type d -name .git 2>/dev/null | sed 's:/\.git$::' ``` 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 if `find` is 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): ```bash node ${CLAUDE_PLUGIN_ROOT}/scanners/campaign-write-cli.mjs add ... \ --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 ""`.) 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 ("``: 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: 1. 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 ` too. 2. Otherwise, use counts the user provides. **Never invent counts** (Verifiseringsplikt) — if none are available, transition the status without `--findings`. On approval: ```bash node ${CLAUDE_PLUGIN_ROOT}/scanners/campaign-write-cli.mjs set-status \ --reference-date "$TODAY" \ [--findings '{"critical":0,"high":0,"medium":0,"low":0}'] [--session ] \ --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 ~/repos` to enroll your repos." - **Pending repos exist:** "Run `/config-audit` in a pending repo to audit it, then `/config-audit campaign set-status audited` to record the result here." - **Audited but not planned:** "`/config-audit plan` in that repo, then mark it `planned`." - Always: the campaign survives this session — re-run `/config-audit campaign` anytime to see the machine-wide picture. ## Notes - **Read-only report, human-approved writes.** `campaign-cli` never writes; every mutation goes through `campaign-write-cli` and only after explicit approval, exactly mirroring how `/config-audit knowledge-refresh` gates register writes. - **Deterministic core, not byte-stable command.** The lib transforms + both CLIs are unit-tested and deterministic (`--reference-date` injected); this command's orchestration is judgment-driven and deliberately **not** in the snapshot suite (like `/config-audit optimize` and `knowledge-refresh`). - The `-cli` suffix 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.