fix(commands): close the promises the command templates could not keep

DEL B chunk `interview` (+ discover/status/cleanup/help). Fasit written before
the run predicted 8 defects and refuted 4 candidates; all 8 confirmed, all 4
refutations held, and three predictions turned out too narrow.

- M-BUG-36: `drift --list` reached the command as 0 bytes. drift-cli accepted
  --output-file but list mode ignored it, and the listing goes to stderr, which
  the command discards per ux-rules rule 2. Fixing the caller alone would not
  have helped.
- M-BUG-37: feature-gap's "Create backup" step ran fix-cli without --apply.
  Dry-run is the default, so no backup existed (backupId: null) while the
  command went on to edit config believing it could roll back.
- M-BUG-38: fix-cli told users to recover with scanners/rollback-cli.mjs, which
  does not exist. Dead reference in the one message read after a bad fix.
- M-BUG-21 fourth arm: five templates carried literal [--global]/[--full-machine]
  inside executable bash blocks. A bracketed placeholder does not start with a
  dash, so every scanner's arg loop takes it as the scan target.
- interview and analyze never said which session they act on; interview could
  rewind a finished session; cleanup interpolated an unvalidated id into rm -rf
  (an empty id deletes every session); status advertised a `resume` command that
  does not exist and documented an `all` argument it never parsed.

TDD: 9 red tests first, including a machine sweep for dead /config-audit
references and for bracketed flags in bash blocks. Suite 1432 -> 1441/0.
Frozen v5.0.0 snapshots untouched; --raw/--json contracts unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGvA1uUQn2hPBPMaCKK6x3
This commit is contained in:
Kjell Tore Guttormsen 2026-07-31 21:27:07 +02:00
commit b85919f2ec
17 changed files with 318 additions and 28 deletions

View file

@ -20,9 +20,16 @@ Generate comprehensive analysis report from discovery findings.
## Implementation
### Step 1: Verify session state
### Step 1: Resolve the session and verify its state
Read `~/.claude/config-audit/sessions/{session-id}/state.yaml` using the Read tool and verify discovery phase completed. If not, tell the user: "Discovery hasn't been run yet. Start with `/config-audit discover` or just run `/config-audit` for a full audit."
Find the session first — never guess which one `{session-id}` refers to:
```
Glob: ~/.claude/config-audit/sessions/*/state.yaml
Sort by modification time — the most recently modified session wins
```
Every `{session-id}` below is that session's id. Read its `state.yaml` using the Read tool and verify discovery phase completed. If the Glob returns nothing, or discovery hasn't completed, tell the user: "Discovery hasn't been run yet. Start with `/config-audit discover` or just run `/config-audit` for a full audit."
### Step 2: Tell the user what's happening

View file

@ -75,7 +75,13 @@ Manage and clean up accumulated config-audit sessions in `~/.claude/config-audit
- Warn before deleting active sessions: "Session {id} is still active (phase: {phase}). Delete anyway?"
6. **Execute cleanup**:
- For each session to delete: `rm -rf ~/.claude/config-audit/sessions/{session-id}/`
- **Validate the id before it ever reaches `rm -rf`.** Each `{session-id}`
must match `^[0-9]{8}_[0-9]{6}$` (the id format `discover` generates) or be
an existing directory name read verbatim from the Glob in step 1. If an id
is empty or fails to match, **refuse to delete it**, report it, and continue
with the rest. An empty id expands the path to
`~/.claude/config-audit/sessions//`, which deletes *every* session.
- For each validated session: `rm -rf ~/.claude/config-audit/sessions/{session-id}/`
- Track deleted count and freed space
7. **Output summary**:

View file

@ -90,7 +90,11 @@ Run both scanners and posture in a single Bash command. Default mode runs the hu
```bash
RAW_FLAG=""
if echo "$ARGUMENTS" | grep -q -- "--raw"; then RAW_FLAG="--raw"; fi
node ${CLAUDE_PLUGIN_ROOT}/scanners/scan-orchestrator.mjs <target-path> --output-file ~/.claude/config-audit/sessions/{session-id}/findings/scan-results.json [--full-machine] [--global] $RAW_FLAG 2>/dev/null; node ${CLAUDE_PLUGIN_ROOT}/scanners/posture.mjs <target-path> --output-file ~/.claude/config-audit/sessions/{session-id}/posture.json [--full-machine] [--global] $RAW_FLAG 2>/dev/null; echo $?
# Set to the scope flags the detected scope calls for, otherwise leave empty.
# A placeholder in square brackets does not start with a dash, so both CLIs'
# arg loops would take it as the TARGET PATH instead of a flag.
SCOPE_FLAGS="" # e.g. SCOPE_FLAGS="--full-machine" or SCOPE_FLAGS="--global"
node ${CLAUDE_PLUGIN_ROOT}/scanners/scan-orchestrator.mjs <target-path> --output-file ~/.claude/config-audit/sessions/{session-id}/findings/scan-results.json $SCOPE_FLAGS $RAW_FLAG 2>/dev/null; node ${CLAUDE_PLUGIN_ROOT}/scanners/posture.mjs <target-path> --output-file ~/.claude/config-audit/sessions/{session-id}/posture.json $SCOPE_FLAGS $RAW_FLAG 2>/dev/null; echo $?
```
Use `--full-machine` for `full` scope, `--global` for `home` scope. For `repo` and `current`, pass the resolved path directly.

View file

@ -72,7 +72,12 @@ Run the scan orchestrator silently to discover and scan files. Default mode emit
```bash
RAW_FLAG=""
if echo "$ARGUMENTS" | grep -q -- "--raw"; then RAW_FLAG="--raw"; fi
node ${CLAUDE_PLUGIN_ROOT}/scanners/scan-orchestrator.mjs <target-path> --output-file ~/.claude/config-audit/sessions/{session-id}/findings/scan-results.json [--full-machine] [--global] $RAW_FLAG 2>/dev/null; echo $?
# Set to the flag itself for full/home scope, otherwise leave empty. Never pass
# a placeholder wrapped in square brackets: it does not start with a dash, so
# the orchestrator's arg loop takes it as the SCAN TARGET and silently scans a
# path that does not exist.
SCOPE_FLAGS="" # e.g. SCOPE_FLAGS="--full-machine" or SCOPE_FLAGS="--global"
node ${CLAUDE_PLUGIN_ROOT}/scanners/scan-orchestrator.mjs <target-path> --output-file ~/.claude/config-audit/sessions/{session-id}/findings/scan-results.json $SCOPE_FLAGS $RAW_FLAG 2>/dev/null; echo $?
```
Check exit code: 0/1/2 → normal. 3 → "Discovery encountered an error. Try a narrower scope."

View file

@ -102,9 +102,14 @@ When iterating new/resolved findings, prefer `userActionLanguage` over raw `seve
If `$ARGUMENTS` contains `--list`:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/drift-cli.mjs --list 2>/dev/null
node ${CLAUDE_PLUGIN_ROOT}/scanners/drift-cli.mjs --list --output-file /tmp/config-audit-baselines.json 2>/dev/null; echo $?
```
The human-readable listing goes to stderr, which `2>/dev/null` discards — read
`/tmp/config-audit-baselines.json` with the Read tool and render the `baselines`
array (`name`, `findingCount`, `savedAt`) as a table. If the array is empty, tell
the user no baselines are saved yet and point at `/config-audit drift --save`.
### What's next
After viewing drift:

View file

@ -128,15 +128,23 @@ If the user picks numbers: parse the selection and proceed to Step 6.
For each selected recommendation:
1. **Create backup** of any files that will be modified:
1. **Create backup** of any files that will be modified.
Do **not** reach for `fix-cli.mjs` here. It is dry-run by default, so calling
it without `--apply` creates no backup at all and returns `backupId: null`
and calling it *with* `--apply` would execute unrelated auto-fixes that the
user never selected. Copy the files yourself:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/fix-cli.mjs <target-path> --json 2>/dev/null
BACKUP_DIR=~/.claude/config-audit/backups/$(date +%Y%m%d_%H%M%S)/files
mkdir -p "$BACKUP_DIR" 2>/dev/null
# repeat per file that will be touched:
cp "<file-to-modify>" "$BACKUP_DIR/" 2>/dev/null; echo $?
```
Or create manual backup:
```bash
mkdir -p ~/.claude/config-audit/backups/$(date +%Y%m%d_%H%M%S)/files/ 2>/dev/null
```
Copy each file that will be touched.
Tell the user where the copies landed. These are plain file copies — they are
restored by copying them back, **not** by `/config-audit rollback`, which only
knows about backups written by `fix` and `implement`.
2. **Apply the template** from gap-closure-templates.md. Use the Write or Edit tool to create or modify the relevant configuration file.

View file

@ -39,7 +39,11 @@ Parse flags and run scanners silently. Default mode emits humanized JSON — eac
```bash
RAW_FLAG=""
if echo "$ARGUMENTS" | grep -q -- "--raw"; then RAW_FLAG="--raw"; fi
node ${CLAUDE_PLUGIN_ROOT}/scanners/scan-orchestrator.mjs <path> --output-file /tmp/config-audit-fix-scan-$$.json [--global] $RAW_FLAG 2>/dev/null; echo $?
# Set to --global when the user asked for global scope, otherwise leave empty.
# A placeholder in square brackets does not start with a dash, so the arg loop
# would take it as the scan/fix TARGET instead of a flag.
GLOBAL_FLAG=""
node ${CLAUDE_PLUGIN_ROOT}/scanners/scan-orchestrator.mjs <path> --output-file /tmp/config-audit-fix-scan-$$.json $GLOBAL_FLAG $RAW_FLAG 2>/dev/null; echo $?
```
Exit code 3 → tell user: "Scanner error. Try `/config-audit posture` to check your configuration."
@ -49,7 +53,7 @@ Exit code 3 → tell user: "Scanner error. Try `/config-audit posture` to check
Run fix planner silently. The fix-cli emits humanized prose to stderr in default mode and v5.0.0-shape JSON to stdout when `--json` is set; we use `--json` here for structured data and let the humanizer-aware rendering layer (this command's prose output below) supply the plain-language wording from the scan envelope above:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/fix-cli.mjs <path> [--global] --output-file /tmp/config-audit-fix-plan-$$.json 2>/dev/null; echo $?
node ${CLAUDE_PLUGIN_ROOT}/scanners/fix-cli.mjs <path> $GLOBAL_FLAG --output-file /tmp/config-audit-fix-plan-$$.json 2>/dev/null; echo $?
```
Exit codes: 0 = plan produced, 2 = one or more fixes failed (apply step only), 3 = argument or tool error. On 3, show the stderr message — an unknown flag is rejected by design, not silently ignored.
@ -100,7 +104,7 @@ AskUserQuestion:
If confirmed, apply:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scanners/fix-cli.mjs <path> --apply [--global] --output-file /tmp/config-audit-fix-applied-$$.json 2>/dev/null; echo $?
node ${CLAUDE_PLUGIN_ROOT}/scanners/fix-cli.mjs <path> --apply $GLOBAL_FLAG --output-file /tmp/config-audit-fix-applied-$$.json 2>/dev/null; echo $?
```
Read `/tmp/config-audit-fix-applied-$$.json` with the Read tool to get applied/failed counts and the backup ID. Exit code 2 means at least one fix failed — report it; `failed[]` carries the reason per fix.

View file

@ -32,10 +32,28 @@ AskUserQuestion requires synchronous terminal interaction and does not work when
if echo "$ARGUMENTS" | grep -q -- "--raw"; then RAW_FLAG="--raw"; fi
```
1. **Load session state**: Verify analysis phase completed, read analysis report for context
1. **Resolve the session, then load its state**:
```
Glob: ~/.claude/config-audit/sessions/*/state.yaml
Sort by modification time — the most recently modified session wins
```
Every path below substitutes that session's id for `{session-id}`. Never guess
it: if the Glob returns nothing, say "No audit session found — run
`/config-audit discover` first" and exit. Read the session's `state.yaml` and
verify `completed_phases` contains `analyze`; if it doesn't, tell the user
analysis hasn't run yet and exit. Then read the analysis report for context.
2. **Conduct interview inline**: Use AskUserQuestion tool directly (NOT via Task). Adapt questions based on analysis findings.
3. **Save interview results**: Write to `~/.claude/config-audit/sessions/{session-id}/interview.md`
4. **Update state** (see state-management rule)
4. **Update state** (see state-management rule), with one bound specific to this
command: interview is optional and can be run against a session that already
moved past it. If `completed_phases` already contains a later phase (`plan`,
`implement`, `verify`), do **not** rewind `current_phase` and do not re-add a
phase already in `completed_phases` — append `interview` only if it is absent,
leave `current_phase`/`next_phase` pointing at the furthest phase reached, and
tell the user the preferences will apply the next time `/config-audit plan`
runs. Rewinding a finished session is how its progress gets lost.
5. **Output summary**
## Interview Questions

View file

@ -37,8 +37,13 @@ When `--raw` is in `$ARGUMENTS`, render the raw `current_phase` field value verb
```bash
RAW_FLAG=""
if echo "$ARGUMENTS" | grep -q -- "--raw"; then RAW_FLAG="--raw"; fi
ALL_FLAG=""
if echo "$ARGUMENTS" | grep -qw -- "all"; then ALL_FLAG="all"; fi
```
When `ALL_FLAG` is set, skip steps 24 and render the **List All Sessions**
table below instead of a single session's status.
2. **Find active session**:
```
Glob: ~/.claude/config-audit/sessions/*/state.yaml
@ -128,11 +133,13 @@ All config-audit sessions:
| 20250120_160000 | implement | 2025-01-20 16:00 |
```
## Resume Session
## Resuming a session
If multiple sessions exist:
```
/config-audit resume {session-id}
```
There is no `resume` command. Sessions are selected by recency: every
session-aware command globs `~/.claude/config-audit/sessions/*/state.yaml` and
takes the most recently modified one.
Sets that session as active and continues from last phase.
To continue an older session, run its next phase directly — `/config-audit plan`,
`/config-audit implement`, and so on read `next_phase` from the state file. If
the wrong session keeps winning, delete the stale ones with
`/config-audit cleanup`.

View file

@ -43,7 +43,12 @@ Default mode (no `--json`, no `--raw`) emits a humanized JSON envelope: each fin
TMPFILE="/tmp/config-audit-tokens-$$.json"
RAW_FLAG=""
if echo "$ARGUMENTS" | grep -q -- "--raw"; then RAW_FLAG="--raw"; fi
node ${CLAUDE_PLUGIN_ROOT}/scanners/token-hotspots-cli.mjs <path> --output-file "$TMPFILE" [--global] [--no-exclude-cache] $RAW_FLAG 2>/dev/null; echo $?
# Set each to the flag itself when the user asked for it, otherwise leave empty.
# A placeholder in square brackets does not start with a dash, so the CLI's arg
# loop would take it as the TARGET PATH instead of a flag.
GLOBAL_FLAG="" # --global
CACHE_FLAG="" # --no-exclude-cache
node ${CLAUDE_PLUGIN_ROOT}/scanners/token-hotspots-cli.mjs <path> --output-file "$TMPFILE" $GLOBAL_FLAG $CACHE_FLAG $RAW_FLAG 2>/dev/null; echo $?
```
**Exit code handling:**

View file

@ -36,7 +36,12 @@ Tell the user: **"Reading active configuration for `<path>`..."**
TMPFILE="/tmp/ca-whats-active-$$.json"
RAW_FLAG=""
if echo "$ARGUMENTS" | grep -q -- "--raw"; then RAW_FLAG="--raw"; fi
node ${CLAUDE_PLUGIN_ROOT}/scanners/whats-active.mjs <path> --output-file "$TMPFILE" [--verbose] [--suggest-disables] $RAW_FLAG 2>/dev/null; echo $?
# Set each to the flag itself when the user asked for it, otherwise leave empty.
# A placeholder in square brackets does not start with a dash, so the scanner's
# arg loop would take it as the TARGET PATH instead of a flag.
VERBOSE_FLAG="" # --verbose
SUGGEST_FLAG="" # --suggest-disables
node ${CLAUDE_PLUGIN_ROOT}/scanners/whats-active.mjs <path> --output-file "$TMPFILE" $VERBOSE_FLAG $SUGGEST_FLAG $RAW_FLAG 2>/dev/null; echo $?
```
**Exit code handling:**