## Trap Doors

- `purge-update-cache.js` — INVARIANT: purge root MUST equal validated `getExtensionRoot()` resolution (`EXTENSION_DIR` only when install-root sentinel exists, else fallback to `~/.claude/pickle-rick`); `update-check.json` and `deploy-audit.log` are written by `extension/src/bin/check-update.ts` and `install.sh` to that path. BREAKS: purge no-ops in production when raw env paths or wrong defaults miss the live cache root; stale auto-updater cache poisons the next update check. ENFORCE: extension/tests/purge-update-cache.test.js. PATTERN_SHAPE: `process\.env\.EXTENSION_DIR\s*\|\||path\.join\(os\.homedir\(\), '\.claude', 'pickle-rick'\)`; INVARIANT: purge MUST delete both `pickle-update-*` download dirs and `pickle-extract-*` extraction dirs because `check-update.ts` uses both temp roots during release inspection/install. BREAKS: interrupted upgrades leave stale extracted payloads behind and operators get a false-clean purge. ENFORCE: extension/tests/purge-update-cache.test.js. PATTERN_SHAPE: `collect(Prefix|VarFolder)Matches\(.*'pickle-update-'` without adjacent `pickle-extract-`.
- `release-gate.sh` — INVARIANT: release tag semver MUST equal `extension/package.json` version before publish and after download verification. BREAKS: updater caches a phantom latest version and re-downloads the same tarball forever. ENFORCE: extension/tests/release-gate.test.js. PATTERN_SHAPE: `--pre-tag|--post-tag` path without tag-name-vs-package-version parity check; INVARIANT: release gating MUST resolve `extension/package.json` and git tag contents from the active worktree root, not the caller's nested cwd. BREAKS: `bash ../bin/release-gate.sh` from `extension/` or another repo subdir false-fails before validating a real release tag. ENFORCE: extension/tests/release-gate.test.js. PATTERN_SHAPE: `PKG_PATH=\"extension/package\.json\"|git show \"\\$tag:\\$PKG_PATH\"` without `git rev-parse --show-toplevel` root resolution; INVARIANT: post-tag verification MUST select the unique installable tarball by archive shape and require both `extension/package.json` and `install.sh`. BREAKS: sidecar assets or partial payloads false-green the release while updater install later fails on missing runtime files. ENFORCE: extension/tests/release-gate.test.js. PATTERN_SHAPE: `tar -tzf .*extension/package\.json` without a same-archive `install\.sh` sentinel check; INVARIANT: post-tag verification MUST require exactly one shared payload root containing both `extension/package.json` and `install.sh` before reading version metadata. BREAKS: duplicate payload roots let the gate validate an arbitrary subtree and false-green a malformed release asset. ENFORCE: extension/tests/release-gate.test.js. PATTERN_SHAPE: `for \\(root in pkg\\).*print root` without counting shared roots; INVARIANT: post-tag verification MUST require `extension/package.json` and `install.sh` under the same payload root before reading version metadata. BREAKS: split-root tarballs can false-green a malformed asset by mixing package metadata from one subtree with an installer from another. ENFORCE: extension/tests/release-gate.test.js. PATTERN_SHAPE: `(^|/)extension/package\.json$` and `(^|/)install\.sh$` checked without shared-root intersection; INVARIANT: post-tag verification MUST reject any archive entry with an absolute or dot-segment path before validating payload roots. BREAKS: a tarball with one safe install payload and one traversal entry still false-greens and later extracts unsafe paths. ENFORCE: extension/tests/release-gate.test.js. PATTERN_SHAPE: `tar -tzf` archive scan without `(^|/)\\.\\.?(\\/|$)|^/` rejection for every member; INVARIANT: post-tag verification MUST download release assets via `gh release download --pattern '*.tar.gz'`, never the source archive flag. BREAKS: the gate validates a GitHub source archive instead of the shipped installer payload and can false-green a broken release asset. ENFORCE: extension/tests/release-gate.test.js. PATTERN_SHAPE: `gh release download .* (-A|--archive) '?tar\.gz'?`.
- `verify-bundle.js` — INVARIANT: every `bundle/ac-dr-*.json` producer MUST emit the canonical bundle schema (`ac_id`, `checked_at`, `checker`, `checker_version`, `evidence`, `failure_reason`, `remediation_hint`). BREAKS: `verifyBundle()` rejects real session artifacts and the deploy-reversion bundle gate goes red before evaluating behavior. ENFORCE: extension/tests/verify-bundle.test.js, extension/tests/pipeline-runner.test.js. PATTERN_SHAPE: `write(StateFile|FileSync)\(.+bundle\/ac-dr-[^']+\.json`; INVARIANT: single-AC verification MUST normalize refined AC aliases to the canonical bundle artifact id before resolving `bundle/ac-dr-*.json`. BREAKS: `--ac AC-DR-15` false-fails against missing `ac-dr-15.json` while the full-bundle path expects `ac-dr-pre-flight.json`. ENFORCE: extension/tests/verify-bundle.test.js. PATTERN_SHAPE: `options\.ac\s*\?\s*\[options\.ac\]` without refined-id normalization; INVARIANT: single-AC verification MUST reject unknown requested AC ids before reading bundle files. BREAKS: typoed or stale `--ac` values false-pass against arbitrary `bundle/ac-dr-*.json` payloads outside the contract. ENFORCE: extension/tests/verify-bundle.test.js. PATTERN_SHAPE: `options\.ac` validation path without `EXPECTED_BUNDLE_AC_IDS` membership after canonicalization; INVARIANT: exported `verifyBundle()` and the CLI wrapper MUST default to the same repo root. BREAKS: nested-cwd callers read `extension/bundle` instead of repo `bundle` and false-report missing AC artifacts. ENFORCE: extension/tests/verify-bundle.test.js. PATTERN_SHAPE: `const repoRoot = options\.repoRoot \?\? process\.cwd\(\)`; INVARIANT: bundle verification MUST promote recoverable `ac-dr-*.json.tmp.*` snapshots before treating an artifact as missing. BREAKS: interrupted atomic bundle writes false-report missing ACs even when the newest artifact payload exists. ENFORCE: extension/tests/verify-bundle.test.js. PATTERN_SHAPE: `fs\.existsSync\(filePath\)` before `readRecoverableJsonObject\(filePath\)|JSON\.parse\(fs\.readFileSync\(filePath`.
- `verify-recapture-fired.js` — INVARIANT: AC-DR-02 MUST match `baseline_recapture_attempted` only inside the latest anatomy window and MUST NOT pin a fixed iteration. BREAKS: stale runs or later-iteration recaptures false-pass or false-fail the bundle. ENFORCE: extension/tests/verify-recapture-fired.test.js. PATTERN_SHAPE: `iteration\s*===\s*1|findMatchingEvent\(activity,\s*anatomyWindows\(state\.history\)\)` without latest-window-only filtering; INVARIANT: session state reads for AC-DR-02 MUST promote recoverable `state.json.tmp.*` snapshots before any missing/unreadable verdict, then parse via `StateManager.read(statePath)`. BREAKS: missing or corrupt base state false-fails even when the orphan tmp contains the real anatomy-window recapture event. ENFORCE: `extension/tests/verify-recapture-fired.test.js`. PATTERN_SHAPE: `fs\.existsSync\(statePath\)` before `StateManager\.read\(statePath\)|JSON\.parse\(fs\.readFileSync\(statePath` in `verify-recapture-fired.js`; INVARIANT: orphan tmp recovery MUST compare tmp mtime to the live pid start time; PID liveness alone is not ownership. BREAKS: PID reuse hides the newest recoverable state and false-fails AC-DR-02. ENFORCE: `extension/tests/verify-recapture-fired.test.js`. PATTERN_SHAPE: `isProcessAlive\(tmpPid\)` on `state\.json\.tmp\.\d+` without a process-start-vs-mtime check; INVARIANT: runtime AC-DR-02 evidence MUST be written to `<session>/bundle/ac-dr-02.runtime.json` when `sessionRoot` is provided, and to `getDataRoot()/bundle/ac-dr-02.runtime.json` otherwise. BREAKS: one session's recapture proof overwrites another's evidence or a no-session probe dirties the repo tree and blocks later clean-tree gates. ENFORCE: `extension/tests/verify-recapture-fired.test.js`. PATTERN_SHAPE: `const (DEFAULT_)?RUNTIME_ARTIFACT_PATH = path\.join\(REPO_ROOT, 'bundle', 'ac-dr-02\.runtime\.json'\)|writeRuntimeArtifact\(`; INVARIANT: AC-DR-02 runtime artifacts MUST emit remediation hints that match the actual `failure_reason`, not a generic missing-recapture hint. BREAKS: missing or unreadable session state sends operators toward the wrong repair path and obscures the real verifier failure. ENFORCE: `extension/tests/verify-recapture-fired.test.js`. PATTERN_SHAPE: `remediation_hint:\s*pass \? null : 'Ensure anatomy-park records baseline_recapture_attempted`; INVARIANT: `StateError('MISSING')` from AC-DR-02 state reads must distinguish deleted paths from unreadable existing files before setting `failure_reason`. BREAKS: chmod/EACCES state files emit `state-missing`, exit 2, and the wrong remediation path. ENFORCE: `extension/tests/verify-recapture-fired.test.js`. PATTERN_SHAPE: `err\.code === 'MISSING'` without `fs\.existsSync\(statePath\)` disambiguation; INVARIANT: anatomy windows must close only on later phase-marker history entries, not timestamp-only audit rows. BREAKS: circuit-breaker or similar timestamped state.history records truncate the anatomy window and false-fail AC-DR-02. ENFORCE: `extension/tests/verify-recapture-fired.test.js`. PATTERN_SHAPE: `history\.slice\(index \+ 1\)\.find\([^)]*timestamp` without a `step|phase` guard.
- `section-c-still-needed.js` — INVARIANT: Section C symptom scans MUST read the same watcher logs as `writeWatcherLivenessArtifact` (`tmux-runner.log` and `pipeline-runner.log`). BREAKS: Section C clears while AC-DR-05 still records `FEED TERMINATED`, so the bundle can report a false-clean watcher state. ENFORCE: extension/tests/section-c-gate.test.js. PATTERN_SHAPE: `tmux-runner\.log` without adjacent `pipeline-runner\.log` in watcher-symptom readers; INVARIANT: Section C MUST detect `FEED TERMINATED` across the full watcher log; tail slicing is evidence-only. BREAKS: long logs can scroll the banner out of the tail sample and false-clear Section C while AC-DR-05 still fails. ENFORCE: extension/tests/section-c-gate.test.js. PATTERN_SHAPE: `lastLines\(fs\.readFileSync\(.*\)\)` used for banner detection; INVARIANT: default session auto-discovery MUST rank sessions with watcher logs ahead of no-log dirs, then sort by watcher-log `mtime`. BREAKS: a newer idle or empty session masks the active log stream and flips Section C against the wrong session. ENFORCE: extension/tests/section-c-gate.test.js. PATTERN_SHAPE: `readdirSync\(DEFAULT_SESSIONS_DIR.*statSync\(dir\)\.mtimeMs|sort\(.*watcherLogMtime.*\)` without `hasWatcherLogs` precedence; INVARIANT: default session auto-discovery MUST resolve sessions from `getDataRoot()` so `PICKLE_DATA_ROOT` and future runtime-root changes stay aligned with runner-created sessions. BREAKS: custom data-root runs scan the wrong tree and default Section C to the wrong session or no session at all. ENFORCE: extension/tests/section-c-gate.test.js. PATTERN_SHAPE: `os\.homedir\(\).+pickle-rick.+sessions|DEFAULT_SESSIONS_DIR\s*=\s*path\.join\([^)]*sessions`; INVARIANT: Section C artifacts scoped to a session or runtime root MUST NOT overwrite tracked repo bundle files. BREAKS: session checks leak evidence across runs and dirty the worktree, blocking later pipeline launches. ENFORCE: extension/tests/section-c-gate.test.js. PATTERN_SHAPE: `path\.join\(REPO_ROOT,\s*'bundle',\s*'section-c-still-needed\.json'\)|const ARTIFACT_PATH = .*section-c-still-needed\.json`; INVARIANT: default session discovery and watcher scans MUST skip unreadable or disappearing session/log paths instead of throwing. BREAKS: routine cleanup or chmod races abort Section C and false-fail the watcher gate. ENFORCE: extension/tests/section-c-gate.test.js. PATTERN_SHAPE: `existsSync|statSync|readFileSync` on watcher-session paths without a local failure fallback; INVARIANT: top-level session auto-discovery MUST treat unreadable `getDataRoot()/sessions` roots as no-session, not as fatal I/O. BREAKS: a permissions race on the runtime session directory aborts Section C before it can emit the fail-open artifact. ENFORCE: extension/tests/section-c-gate.test.js. PATTERN_SHAPE: `fs\.readdirSync\(DEFAULT_SESSIONS_DIR` without local failure fallback; INVARIANT: watcher-log candidates MUST be regular readable files before ranking or reads. BREAKS: directory-shaped `tmux-runner.log` paths pin auto-discovery to bogus sessions and false-fail Section C. ENFORCE: extension/tests/section-c-gate.test.js. PATTERN_SHAPE: `accessSync|readFileSync` on `tmux-runner\.log|pipeline-runner\.log` without `statSync\(...\)\.isFile\(\)`.
- `section-c-still-needed.js` — INVARIANT: a missing explicit `--session` path MUST fall back to the runtime bundle artifact path, never create `<session>/bundle` for a nonexistent session. BREAKS: typoed or cleaned-up session ids manufacture fake session dirs that later discovery can rank as real candidates. ENFORCE: extension/tests/section-c-gate.test.js. PATTERN_SHAPE: `resolveArtifactPath\(resolvedSession\)` before `fs\.existsSync\(resolvedSession\)`; INVARIANT: session-scoped artifacts MUST fall back to the runtime bundle artifact when the selected session root cannot host a writable `bundle/` path. BREAKS: unreadable explicit or auto-selected session roots crash Section C with `EACCES` instead of emitting fail-open evidence. ENFORCE: extension/tests/section-c-gate.test.js. PATTERN_SHAPE: `fs\.existsSync\(resolvedSession\)` without `accessSync`-based session artifact writability checks before `resolveArtifactPath\(resolvedSession\)`; INVARIANT: explicit or discovered session roots MUST be directories before Section C writes `<session>/bundle` artifacts. BREAKS: file-shaped session paths raise `ENOTDIR` during artifact writes instead of failing open to the runtime bundle. ENFORCE: extension/tests/section-c-gate.test.js. PATTERN_SHAPE: `accessSync\(sessionRoot` without a paired `statSync\(sessionRoot\)\.isDirectory\(\)` before `resolveArtifactPath\(resolvedSession\)`.
