`write-scope.mjs` has existed since M-BUG-41, but only one writer ever called
it. Measured 2026-08-12: 9 files under `scanners/` write to disk, 1 imported
the gate; 21 command templates, 17 mention a write, 5 call `write-scope-cli`.
Five templates paraphrasing one policy is the shape that put the lever table in
five copies (#61) — one level up.
The defect was never "8 ungated writers = 8 bugs". Four of them write the
plugin's own bookkeeping and must STAY ungated: a gate that fires on every run
gets switched off, and then it guards nothing. The defect is that nothing
declared WHICH, so the question was answered by reading, and answered
differently each time it was asked.
`tests/lib/write-gate-coverage.test.mjs` makes the answer structural: every
writer either imports the gate or holds an EXEMPT entry naming where the bytes
land. Seen RED against today's tree before the fix (4 ungated writers), and
each of its four assertions was separately seen red against its own defect.
Two premises in the plan text were falsified by measuring them first:
- `scan-orchestrator` was carried as "plugin-managed, legitimately exempt".
`--save-baseline` derives its path from the SCAN TARGET, so `--global`
lands `~/.claude/.config-audit-baseline.json` — user-scope, require-ok. It
is gated. `lib/baseline.mjs` is the genuinely exempt one.
- the first sweep scored 9 writers with a regex that could not match
`writeFileSync(`, so `lib/backup.mjs` — a real writer — read as clean. The
guard covers sync and async forms, strips comments before matching, and
asserts non-emptiness so a regex that stops matching cannot make every
other assertion vacuously green (#63, #64).
Gated: fix-engine, rollback-engine, campaign-export-cli, scan-orchestrator.
All five call sites share ONE reduction, `evaluateWriteTargets` — four copies
of classify/strongestGate/dedup is the drift this exists to prevent.
`campaign export` still DISCLOSES rather than refuses: cross-repo is by design
there, and tightening it into a refusal would break the feature. A dry run is
still not a write, so it is never gated (#63). A refusal is a verdict about a
config that WAS examined, so it rides in the payload and keeps the 0/1/2 exit
contract (#62) — and the verdict now reaches the success payload too, since
stderr is discarded by `2>/dev/null` (F3's class).
commands/fix.md carries `--approve-scope` from the answer the user gives, with
the rule stated where it can be read: classifying is not approving.
Dogfooded end to end: a target outside the session root refuses with zero bytes
written, then applies under `--approve-scope`.
Suite 1703 -> 1707/0. Frozen v5.0.0 + default-output snapshots: 0 changed files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pkn22uGCgk6QZA738zNmHL
Dogfooding `/config-audit fix` against a throwaway repo copy. All eight
predictions registered in the fasit before the run were confirmed, and three
further defects surfaced that were not predicted.
- M-BUG-21, third arm: the argument loop ended in `!arg.startsWith('-') =>
targetPath`, so an unknown flag was dropped and its value became the target.
In `fix` that is the WRITE target under `--apply`. Unknown options and a
value-less `--output-file` now exit 3.
- `--dry-run` was documented in the command's argument-hint and never
implemented; `--output-file` did not exist, so `commands/fix.md` told the
agent to Read a file nothing produced. Both now exist.
- M-BUG-31: `file-rename` was excluded from the backup set, so a renamed rule
file had no backup entry while the command promised one and returned a
backupId that could not restore it.
- M-BUG-32: `verifyFixes` hardcoded `includeGlobal: false`, so after a
`--global` run every untouched user-scope finding was reported as verified.
Reproduced against an unmodified ~/.claude/CLAUDE.md.
- M-BUG-29: a rename was applied before other fixes on the same file, which
then failed with ENOENT while the run still exited 0. Renames sort last.
- M-BUG-30: `severityOrder[s] || 4` maps critical (0) to 4, so critical fixes
sorted last. The old test used the same falsy fallback and agreed with the
bug. Now `?? 4`.
- A failed fix exits 2 instead of 0, matching the other scanners' convention.
Frozen tests/snapshots/v5.0.0/ untouched; --json/--raw stdout byte-identical.
Suite 1420/0 (+10).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ3MCDCnyw7wZSPnUXVhYS
Pipeline step 4 dogfood. `/config-audit rollback` could not see a single one of
the four real backups on this machine, and reported "Backup not found" for one
that was sitting right there.
Four defects, one root: nothing agreed on where a backup lives or what its
manifest looks like.
- M-BUG-22 `lib/backup.mjs` resolved `~/.config-audit/backups` (pre-v2.2.0)
while every command, agent and doc uses `~/.claude/config-audit/backups`.
The auto-backup hook and fix-cli wrote to the first, implement to the second,
rollback read only the first. Canonical root now, with the legacy root kept
readable so older backups stay listable and restorable (`legacy: true`).
- M-BUG-25 `parseManifest` understood only the engine's quoted `original_path:`
spelling, but implement hand-builds its manifest with `- backup:`/`original:`/
`sha256:`. Every implement-made backup parsed to zero files and restoreBackup
returned `{restored: [], failed: []}` — a success-shaped no-op. Both formats
parse now, and a manifest with unparseable entries throws instead of
pretending to succeed.
- M-BUG-23 both session hooks watched `~/.config-audit/sessions`, which does not
exist; sessions live under `~/.claude/`. "Check for active sessions" had never
fired once. It fires now.
- M-BUG-24 the suite called createBackup() against the developer's real home —
it had left nine stray backups there, and cleanupOldBackups() deletes past ten.
Root is overridable via CONFIG_AUDIT_BACKUP_ROOT; both test files use it.
Rollback still cannot delete files implement CREATED — no backup can hold a file
that never existed. It no longer does so silently: manifests carry a `created:`
list, restoreBackup returns `createdNotRemoved`, and rollback.md requires the
report. Automatic deletion is a destructive action and needs its own design.
Verified against backup 20260717_032636 on a throwaway copy: all three files
restore byte-exact (sha256 match), zero writes outside the copy, backup dir
unmodified. Suite 1382 -> 1398/0; frozen v5.0.0 snapshots untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SejM9RQAa1Hfuq7Ek2WfFr
Devil's advocate gap-verification (read-only Workflow, 9 skeptics) refuted the
blanket "all closed" claim by finding fix-cli.test.mjs was the one CLI-spawning
test still reading the real ~/.claude. fix-cli runs the SKL skill-listing
scanner (HOME-scoped) even with includeGlobal:false, so its manual findings
include CA-SKL-001 on a dev machine but not in clean CI.
This directly corrects 325182d, which listed fix-cli.test.mjs as "Proven safe,
left as-is (output byte-identical real vs empty HOME — fixable findings are
project-local HKV/RUL/SET, never SKL/COL)". That reasoning predated SKL being
wired into scan-orchestrator (7bb2547/66433fe) and was false: real HOME yields
manual=6 (incl. CA-SKL-001), hermetic manual=5 (5230 vs 4798 bytes).
- wrap all 5 fix-cli spawns in hermeticEnv() (matches the other 11 CLI tests)
- add a regression lock: a project-scoped run must surface no CA-SKL/CA-COL
- redirect the --apply backup check to HERMETIC_HOME — the test was also
writing backups into the real ~/.config-audit/backups on every run
- docs: stale "26 hook events" -> 28 (README:528, scanner-internals:73);
hook-validator.mjs comment April -> June 2026 (functional count already 28)
Re-audited all 12 CLI-spawning test files: 11 hermetic-helper, manifest custom
HOME-env, post-edit-verify safe-by-construction (early-exit only, never reaches
scanners). HOME-leak class now actually closed. Suite 875/875, no snapshot drift.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
Add /ultraresearch-local for structured research combining local codebase
analysis with external knowledge via parallel agent swarms. Produces research
briefs with triangulation, confidence ratings, and source quality assessment.
New command: /ultraresearch-local with modes --quick, --local, --external, --fg.
New agents: research-orchestrator (opus), docs-researcher, community-researcher,
security-researcher, contrarian-researcher, gemini-bridge (all sonnet).
New template: research-brief-template.md.
Integration: --research flag in /ultraplan-local accepts pre-built research
briefs (up to 3), enriches the interview and exploration phases. Planning
orchestrator cross-references brief findings during synthesis.
Design principle: Context Engineering — right information to right agent at
right time. Research briefs are structured artifacts in the pipeline:
ultraresearch → brief → ultraplan --research → plan → ultraexecute.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>