config-audit/commands/campaign.md
Kjell Tore Guttormsen 49833aded8 feat(campaign): cross-repo prioritized backlog (v5.7 Fase 2 Block 4b)
buildBacklog(ledger) pure transform + read-only campaign-cli payload field
+ command rendering. The single machine-wide pick-list: per-repo (the ledger
tracks severity counts, not individual findings), severity-weighted
(SEVERITY_WEIGHTS c1000/h100/m10/l1), deterministic tie-break, excludes
implemented/pending/zero-finding repos.

No schema change, no new scanner -> scanner count stays 15, snapshot/backcompat
byte-stable. suite 1138->1150 (lib +9, campaign-cli +3). README badge 1091+->1150+.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 02:44:21 +02:00

9.3 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.jsonoutside 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). Execution is a later block — this command does not run audits or apply fixes itself; it tracks where each repo stands and shows what to tackle next.

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.mjsinit / 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 reportreport (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), 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.
    find "<root>" -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):

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:

  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 <id> too.
  2. 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 ~/repos to enroll your repos."
  • Pending repos exist: "Run /config-audit in a pending repo to audit it, then /config-audit campaign set-status <path> 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 + a read-only cross-repo prioritized backlog. The backlog is a derived pick-list (buildBacklog, severity-weighted); execution is a later block — this command tracks state and shows what to tackle next, it does not run audits or apply fixes.