opencode-dejavu 2.7.0 → 2.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ /**
2
+ * One-off migration runner for existing dejavu stores.
3
+ * Re-tiers gates learned under older policies (probe-tool blocking → watching,
4
+ * diagnostics → reminding), backfills mechanical corrections, sanitizes
5
+ * signatures/snippets/corrections (secrets + terminal control chars), applies
6
+ * feedback-demotion catch-up, and merges stale project copies of global gates.
7
+ * Idempotent; also runs automatically at plugin init.
8
+ *
9
+ * WARNING: the log-scrub step below rewrites log.jsonl WITHOUT the log lock —
10
+ * run this while OpenCode is closed, or appends racing the rewrite are lost.
11
+ *
12
+ * Usage: bun scripts/migrate.ts <projectDir> [moreProjectDirs...]
13
+ */
14
+ import { readFile, writeFile } from "node:fs/promises"
15
+ import { homedir } from "node:os"
16
+ import { join } from "node:path"
17
+ import { scrubSecrets } from "../src/patterns"
18
+ import { GateStore, Stores } from "../src/store"
19
+
20
+ const globalStore = new GateStore(process.env.DEJAVU_HOME ?? join(homedir(), ".config", "opencode", "dejavu"))
21
+ const projects = process.argv.slice(2)
22
+ const targets = projects.length > 0 ? projects : [process.cwd()]
23
+
24
+ for (const project of targets) {
25
+ const projectStore = new GateStore(join(project, ".opencode", "dejavu"))
26
+ const stores = new Stores(globalStore, projectStore)
27
+ await stores.migrate(true)
28
+ // The script exits after this — flush deferred demotion events now, or they
29
+ // are silently lost ("every repair is logged" invariant).
30
+ await globalStore.flushDeferred()
31
+ await projectStore.flushDeferred()
32
+ console.log(`migrated: ${project}`)
33
+ }
34
+
35
+ // Historical logs are scrubbed too — secrets must not linger on disk.
36
+ const logDirs = [globalStore.dir, ...targets.map((t) => join(t, ".opencode", "dejavu"))]
37
+ for (const dir of logDirs) {
38
+ const logPath = join(dir, "log.jsonl")
39
+ try {
40
+ const raw = await readFile(logPath, "utf8")
41
+ const scrubbed = scrubSecrets(raw)
42
+ if (scrubbed !== raw) {
43
+ await writeFile(logPath, scrubbed, "utf8")
44
+ console.log(`scrubbed log: ${logPath}`)
45
+ }
46
+ } catch {
47
+ // missing log is fine
48
+ }
49
+ }
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: dejavu
3
+ description: Protocol for working with the dejavu error-gate plugin. Use when a tool call is answered with a "[dejavu] REMINDER" or "[dejavu] BLOCKED" message, when the same tool call fails repeatedly, or when you discover the root cause of a gated failure. Covers how to react to reminders, what BLOCKED means, the dejavu:proceed escape hatch, and how to improve gate corrections in .opencode/dejavu/gates.json.
4
+ ---
5
+
6
+ # dejavu — recurring-error gates
7
+
8
+ dejavu is an OpenCode plugin that watches tool calls fail, counts recurrences across sessions, and promotes frequent failures into **gates**. It is your external long-term memory for mistakes: what failed before will not silently fail again.
9
+
10
+ You do not manage dejavu's detection — it is mechanical. Your job is to react correctly and to improve gate quality when you learn something.
11
+
12
+ ## When you get `[dejavu] REMINDER`
13
+
14
+ The call you were about to make has failed repeatedly in the past. The call was aborted before execution.
15
+
16
+ 1. **Do not retry the identical call.** That is exactly the behavior the gate exists to prevent.
17
+ 2. Read `Last failure:` and `Correction:` in the message.
18
+ 3. Diagnose the root cause (read the relevant file, check the environment, run a diagnostic command).
19
+ 4. Retry with a **changed** approach — a different command, fixed arguments, or a prerequisite step first.
20
+ 5. If you are confident the situation changed (e.g. you just installed the missing dependency), retry as-is — a SUCCESS clears your session from the gate's chain. On a blocking gate, a repeated failure after a reminder escalates it to a hard block within this session; a reminding gate (diagnostics/iteration commands) never blocks, it only reminds.
21
+ 6. `dejavu:proceed` (as a trailing COMMENT inside the call — `# dejavu:proceed`) bypasses the gate. Use it ONLY when the user explicitly asked you to force the operation, or you have concrete proof the gate is stale. Every override is logged; on blocking gates it is ALSO counted on the gate, and repeated overrides demote the gate (your bypasses are feedback: a gate everyone works around retires itself). If you override and the call SUCCEEDS, say so when improving the gate — that proves it stale.
22
+
23
+ ## When you get `[dejavu] BLOCKED`
24
+
25
+ You were reminded, retried, and it failed again. The gate is now hard in this session.
26
+
27
+ - Do not attempt the same call again in any form that matches the pattern.
28
+ - Tell the user what is blocked and why (the message contains evidence and the gate file path).
29
+ - Choose a fundamentally different approach to reach the goal.
30
+
31
+ ## Improving gates (your one write privilege)
32
+
33
+ Gates live in `.opencode/dejavu/gates.json` (project) and `~/.config/opencode/dejavu/gates.json` (global agent habits). The files are human- and agent-editable.
34
+
35
+ When you discover the **root cause** of a gated failure, update the gate's `correction` field with a one-line actionable instruction (what to do instead, not what to avoid). Example: `"correction": "Use 'npm install --legacy-peer-deps' — this repo has conflicting peer deps"`.
36
+
37
+ Do NOT:
38
+ - create gates manually (promotion is mechanical: 3 failures across 2 sessions),
39
+ - weaken or delete gates without telling the user,
40
+ - stuff prose into `correction` — one actionable line only.
41
+
42
+ ## What dejavu tracks
43
+
44
+ - `bash` commands that exit non-zero or print error signatures (normalized: paths/numbers/hashes abstracted)
45
+ - `read`/`edit`/`write` failures on files (via tool-level error events)
46
+ - counts, distinct sessions, distinct projects; patterns seen in 2+ projects become global (they are your habits, not the repo's quirks)
47
+
48
+ Gates expire after 60 days without recurrence. Gates listen to behavior in both directions: a gate whose error keeps recurring under enforcement (3+ times) or that gets overridden repeatedly (5+ on blocking gates) demotes itself to `watching` + `feedbackDemoted` and stops enforcing — if you later learn it was right, a human re-enforces it by setting its `status` back to `blocking`/`reminding` AND clearing `feedbackDemoted` in gates.json (the gate gets a fresh grace window). A gate blocked 10+ times is flagged `review: true` in its file. A gate that taught its lesson (reminded 5+ times, never reoffended) retires softly too — if a retired gate starts failing again, it re-promotes on its own.
package/src/AGENTS.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## OVERVIEW
4
4
 
5
- Two dependency-free modules: `patterns.ts` (pure functions — call identity, normalization, detection, policy) and `store.ts` (stateful — gates.json/log.jsonl I/O under locks, promotion, scope escalation).
5
+ Three dependency-free modules: `patterns.ts` (pure functions — call identity, normalization, detection, policy), `store.ts` (stateful — gates.json/log.jsonl I/O under locks, promotion, scope escalation, feedback demotion) and `validate.ts` (the parse/repair boundary every persisted gate crosses).
6
6
 
7
7
  ## WHERE TO LOOK
8
8
 
@@ -16,42 +16,90 @@ Two dependency-free modules: `patterns.ts` (pure functions — call identity, no
16
16
  | Failure text scan | patterns.ts | `detectFailure` + `FAILURE_SIGNATURES` |
17
17
  | Noise filtering | patterns.ts | `isNoiseError` + `NOISE_ERRORS` (aborted/cancelled ≠ failed) |
18
18
  | Diagnostic/intended-exit logic | patterns.ts | `DIAGNOSTIC_VERBS`, `isIntendedNonzero`, `canBlock`, `canRemind` |
19
+ | Over-generic shape guard | patterns.ts | `hasResidualIdentity` — parameterized-away substance ⇒ watching only |
20
+ | Wrapper unwrapping | patterns.ts | `unwrapCmdWrapper` — `cmd /c\|/k` payload normalizes as the inner command |
21
+ | Persistence boundary | patterns.ts | `sanitizeForStore` = `stripControl` (ANSI/C0) + `scrubSecrets` |
19
22
  | Escalation scope policy | patterns.ts | `isRepoLocal` + `REPO_LOCAL_VERBS` — repo-local verbs never escalate globally |
20
- | One scope (gates.json + index.json + log.jsonl) | store.ts | `GateStore` — `load`/`save`/`loadIndex`/`saveIndex`/`log`/`expire`/`extract`/`rotateLog`/`reconcile` |
23
+ | One scope (gates.json + index.json + log.jsonl) | store.ts | `GateStore` — `load`/`save`/`loadIndex`/`saveIndex`/`log`/`expire`/`extract`/`rotateLog`/`reconcile`; deferred events `deferEvent`/`flushDeferred`/`appendBatch` + `routeSalientTo` |
21
24
  | Two-scope logic + promotion | store.ts | `Stores` — `findGate`/`recordFailure`/`migrate`/`enforcedGates`/`reconcileAll`; `mergeGate` merges duplicate keys |
25
+ | Enforcement negative feedback | store.ts | `checkFeedbackDemotion` + `DEMOTE_RECURRENCES`/`DEMOTE_OVERRIDES`; counters `overrideCount`/`recurredAfterGate`, grace via `feedbackBaseline` |
22
26
  | Gate parse/repair boundary | validate.ts | `coerceGateShape` (strict parse), `repairGate` (mechanical coercion), `hasNestedTokens` (corruption fingerprint) |
23
27
  | fs safety | store.ts | `ntPath`, `atomicWrite`, `withLock` |
24
28
 
25
29
  ## INVARIANTS (do not break)
26
30
 
27
31
  - Rule order in `PARAM_RULES` matters: quoted strings first, specific tokens (uuid/sha/ip/url/date), generic numbers last — reordering fragments signatures
28
- - Rule order in `normalizeCommand` matters too: quoted strings are parameterized BEFORE path rules — a `<str>` substitution inserts spaces that would expose an adjacent `/` to the path rule on a second pass (idempotency); interpreter payload hashing runs while the payload is still raw
29
- - `scrubSecrets()` runs on every string before it touches disk; `recordFailure` re-scrubs defensively
30
- - Three enforcement tiers: `canBlock` (bash && non-diagnostic && not a bare one-liner shape) is the ONLY path to `blocking`; `canRemind` (diagnostic bash) is the only path to `reminding`; probe tools use `PROMOTE_COUNT_PROBE` and never leave `watching`
32
+ - Rule order in `normalizeCommand` matters too: control-char strip first, then quoted strings are parameterized BEFORE path rules — a `<str>` substitution inserts spaces that would expose an adjacent `/` to the path rule on a second pass (idempotency); `unwrapCmdWrapper` runs before `hashInterpreterPayload` so a wrapped one-liner still fingerprints; interpreter payload hashing runs while the payload is still raw
33
+ - `sanitizeForStore()` (control-char strip + `scrubSecrets`) runs on every string before it touches disk; `recordFailure` re-sanitizes defensively
34
+ - Three enforcement tiers: `canBlock` (bash && non-diagnostic && residual identity) is the ONLY path to `blocking`; `canRemind` (diagnostic bash && residual identity) is the only path to `reminding`; probe tools use `PROMOTE_COUNT_PROBE` and never leave `watching`; `hasResidualIdentity` guards BOTH tiers — a fully parameterized shape may only watch
35
+ - Feedback demotion is the negative twin of healing: recurrences (`DEMOTE_RECURRENCES`) or overrides (`DEMOTE_OVERRIDES`) past threshold demote to `watching` + `feedbackDemoted`, which blocks mechanical re-promotion; `feedbackBaseline` (counter values at demotion) gives a human re-enforcement a fresh grace window; `mergeGate` sums `overrideCount` and baselines and preserves the demotion mark
36
+ - Index entries are never pruned on one project's initiative by `reconcileAll` (it rebuilds missing entries only) — a process sees one project store, so "absent here" ≠ "dead". `expireAll`'s index sweep prunes via time-decayed candidacy: a key absent from every visible scope (own project + global) is stamped `orphanCandidateSince`, cleared the moment any scope holds the gate again, and deleted only after `ORPHAN_CANDIDATE_DAYS` (7) of continuous absence — a live gate in an unopened project clears its own candidacy on that project's sweep; the plain `lastSeen` TTL still applies regardless
31
37
  - Repo-local verbs (`isRepoLocal`) never escalate to the global store — their failures are repo quirks; both escalation paths (`recordFailure` + `reconcileAll`) and doctor's MISSED-ESCALATION honor this
32
38
  - Fuzzy merging requires comparable flag sets (one a subset of the other) — disjoint switches are different operations and must never merge; subset additions still consolidate
33
39
  - `DIAGNOSTIC_VERBS` serves two callers (exit-1 allowlist + blocking policy) — one list, two uses; edit knowing both move
34
- - Lock order is always project → global, gates → index (see `recordFailure` escalation) — reversing deadlocks; the log lock is separate and leaf-level
40
+ - Lock order is always project → global, gates → index (see `recordFailure` escalation) — reversing deadlocks; the log lock is separate and leaf-level, acquired alone or outermost, NEVER while holding a gates/index lock — log-hygiene helpers (`exciseCorruptLogLines`/rotate) run OUTSIDE the scope lock, else the scope critical section stretches across a full log read+parse+rewrite at init-storm time
41
+ - A degraded lock waiter NEVER unlinks the lockfile (`withLock` tracks `acquired`) — deleting the live holder's lock destroys mutual exclusion for every subsequent acquirer (the production corrupt-log root cause)
42
+ - Incoming bash signatures without residual identity match concrete gates EXACTLY only (`findGate` fuzzy + `recordFailure` consolidation both guard it) — family noise must neither enforce nor pollute another gate's evidence
43
+ - `bashSegmentSignatures` unfolds `cmd /c` payloads recursively (depth-bounded) — inner-chain gates fire through the wrapper, and the before-hook honors `dejavu:proceed` inside a LEADING wrapper payload (unwrap before quote-strip)
44
+ - `INTERPRETER_ONELINER` alternatives run longest-first — `-c` matching inside `-command` swallowed the flag tail into the payload and fragmented keys across flag spellings
35
45
  - Cross-project evidence lives ONLY in the global `index.json` — a gate's own `projects` array sees one store and never drives escalation alone
36
46
  - Escalation writes the global gate FIRST, then removes the project copy — a crash between the two writes must leave a duplicate (healed by migrate), never a hole
37
47
  - `mergeGate` preserves session enforcement state (`remindedSessions`/`failedSessions`) — merging must never reset the remind→block chain
38
48
  - Fuzzy consolidation in `recordFailure` prefers the gate holding the session's reminded state (before/after hooks must stay in sync) and never overwrites the evidence snippet
39
49
  - Snippets and corrections are UNTRUSTED text re-injected into agent context — keep the data-label framing in messages, the 200-char correction bound, and scrub quarantine bytes
40
- - Inside `runLocked` always `load(true)`; unlocked `load()` peeks are routing hints only, never a basis for mutation
50
+ - Inside `runLocked` always `load(true)`; OUTSIDE the gates lock always non-force `load()` — `load(true)` quarantines an unparseable file (a WRITE), so forcing outside the lock is a write-without-lock (the class round 3 fixed in doctor); unlocked peeks are routing hints only, never a basis for mutation
41
51
  - Hot-path reads use the 1s TTL cache + key index (`byKey`/`enforcedOnly`); mutations inside locks use `load(true)`; `save()` refreshes the cache directly
42
52
  - The remind→block chain is persisted ON THE GATE (`remindedSessions`/`failedSessions`) and enforced under the store lock — process memory holds nothing authoritative, so several windows and restarts share one escalation
43
- - Successes heal: `recordSuccess` grows `succeededAfterGate` on an enforced gate; at `HEAL_SUCCESSES` (3) it retires to `watching` and logs `healed`. A failure resets the streak in `recordFailure`. Only bash successes heal (only bash gates enforce)
53
+ - Successes heal: `recordSuccess` grows `succeededAfterGate` on an enforced gate; at `HEAL_SUCCESSES` (3) it retires to `watching` and logs `healed`. A failure resets the streak in `recordFailure`. Only bash successes heal (only bash gates enforce). A success ALSO clears the succeeding session from `remindedSessions`/`failedSessions` — override + success must leave the session clean, otherwise the only exit from a block is permanent overriding (the arms-race engine). Heal-aware: a blocking gate with `succeededAfterGate > 0` skips the first-encounter abort (a likely-fixed command) — it arms the chain silently and blocks only a repeat failure
54
+ - Iteration verbs (`dart run`, `go run|build|test|vet`, `cargo run|build|test|clippy`) are diagnostic: their failures are the work itself — they annotate the failing output and never interrupt or block, and their exit 1 is the intended "still broken" outcome of iteration
55
+ - Overrides count toward demotion ONLY on blocking gates — a reminding gate never blocks and never interrupts, so bypassing it is not friction with the teaching (the `override` event is still logged for visibility)
56
+ - Index churn gate: the first-ever failure of a brand-new pattern (no entry yet, count 1) skips the machine-wide index rewrite; anything indexed or recurring updates as before — escalation evidence preserved, one-off noise not rewritten
57
+ - Unparseable gate dates reset to now at the parse boundary — `expire` compares `Date.parse < cutoff`, and NaN is never `<` anything (immortal gates)
58
+ - `migrate()` never re-promotes `feedbackDemoted` gates (the watching→reminding catch-up checks the flag) — "never re-promotes mechanically" must hold on EVERY mechanical path, not just `recordFailure`. The catch-up ALSO exempts `retireBaseline` gates: a healed/taught-retired gate's lifetime count already clears the catch-up bar, so re-promoting it on every migrate would re-open the promote→heal→promote oscillation the damping baseline exists to kill — retirement is evidence-based and only a fresh bar of failures (via `recordFailure`) may lift it
59
+ - `recordSuccess` heals and clears session chains on EXACT matches only — fuzzy matches are attribution convenience, never a basis for state mutation (proxy successes would heal the wrong gate)
60
+ - The override marker requires comment syntax (`# dejavu:proceed`) — quote-stripping alone let unquoted markers smuggled as data (`echo dejavu:proceed && gated`) bypass gates
61
+ - Exit-1 immunity requires every NON-transparent chain segment to be diagnostic — a diagnostic later in the chain must not hide a non-diagnostic's failure (`deploy && grep`). Pipe formatters (`select-object`/`tee-object`/`tee`/`head`/`tail`/...) are transparent ONLY in pipe-tail position (after a `|`, including bash's `|&` pipe-stdout-and-stderr, which is a pipe so its tail is a pipe tail too): they never produce the pipeline's exit code there, so `tsc | Select-Object` keeps immunity; but a formatter standing alone or as the TERMINAL producer of a sequence (`npm test && tail -5 missing.log`) IS the failing producer and still counts. Navigation (`cd`/`set-location`) is transparent only when the segment is PURE navigation — a segment pairing a nav verb with a diagnostic and no separator (`cd packages/foo npx vitest run …`) keeps the diagnostic instead of being dropped wholesale (dropping it hid the command and broke immunity); a bare `cd /bad/path` still counts. A real non-diagnostic producer still breaks immunity (`npm install | select-object` counts). Position comes from `splitChainTagged`; `||` is a sequence separator, so the segment after it is a producer, not a pipe tail. A single `&` is deliberately NOT a separator — on Windows it is the PowerShell call operator (`& "C:\…\exe"`), and splitting on it would break those invocations
62
+ - `load()` distinguishes corruption from absence: unparseable gates.json quarantines under the lock (bytes kept) instead of silently emptying — the silent path let the next save overwrite recoverable gates
63
+ - `withLock` verifies ownership (pid in the lockfile) before unlinking — after a stale-steal the original holder would otherwise delete the stealer's lock and open the critical section
64
+ - Retire-on-taught: reminded `TAUGHT_REMINDERS`+ times with zero reoffense AND zero post-gate failures → softly to watching (`retired-taught`, no feedbackDemoted — re-promotion stays possible). Healing via success is impossible there: the agent changed behavior, the gated call never runs again. Only TRUE first encounters count — raced calls (same dispatch burst) never saw the reminder
65
+ - Reminding gates NEVER abort the call: the before-hook returns early for `reminding`, and the after-hook appends a `[dejavu] NOTE` (`remindNote`) to the FAILING output once per session (repeat same-session failures only accrue `recurredAfterReminder`); a succeeding run produces no note. Reminding taught retirement fires in the after-hook at one reminder ABOVE `TAUGHT_REMINDERS` with `recurredAfterReminder === 0` — `recurredAfterGate` is no signal there (it grows structurally: every session's first failure counts and the note rides AFTER it), and the extra round keeps it from preempting anti-nag evidence
66
+ - Anti-nag retirement (the negative twin of taught) runs at two hook points: the before-hook for BLOCKING gates (first encounter) and the after-hook for REMINDING gates (a same-session failure after the note). Condition: reminded `ANTI_NAG_REMINDERS`+ times AND `recurredAfterReminder >= ANTI_NAG_REOFFENSE` → the gate NAGS instead of teaching: watching + `feedbackDemoted`, counters reset (a manual re-enforce starts fresh). `feedbackDemoted` blocks mechanical re-promotion; a human re-enforces manually. `recurredAfterReminder` accrues for blocking (failedSessions branch) and reminding (ignored-note branch); `repairGate` zeroes it only AT the blocking→non-blocking demotion transition (stale-evidence guard), since reminding gates now accrue it freshly — the plugin should help, not nag
67
+ - Retirement damping (`retireBaseline`): heal and teach-retire both capture `{count}` at the moment of retirement, and re-promotion requires `count − retireBaseline.count ≥ threshold` — a full fresh bar — which promotion then consumes (deletes). Without it a retired gate re-promotes on the VERY NEXT single failure, because its lifetime `count`/`sessions` already clear the bar (promote→heal→promote oscillation). `mergeGate` preserves an existing baseline, `repairGate` clamps it to ≤ `count` (a corrupted overshoot must keep damping active, not re-open instant re-promotion). feedbackDemoted gates are orthogonal — they never re-promote at all
68
+ - Enforcement counters are lifecycle-scoped: promotion resets remindedCount/recurrences/overrides/heal streak/baseline AND clears session chains (remindedSessions/failedSessions) — a re-promoted gate must not inherit the previous retire/heal round's evidence, and stale chains must not let a session skip its reminder
69
+ - Demotion votes count only failures the gate had a chance to prevent: recurrence demotion additionally requires `DEMOTE_REOFFENSE_SESSIONS` distinct sessions that reoffended AFTER a reminder (`reoffenseSessions`) — first-encounter failures never saw a reminder, and one bad session/model must not demote a gate for everyone
70
+ - Exit-1 immunity flattens SUBSHELL paren groups before splitting (`isIntendedNonzero` → `flattenSubshellParens`): `(deploy && grep)` is a container, not an atom — a diagnostic inside parens must not immunize a non-diagnostic failure. Flattening is brace-aware: parens inside `{}` script blocks and quotes are left alone, so method-call parens (`ForEach-Object { $_.trim() }`) don't split their segment
71
+ - Hook events are queued inside the store lock and logged AFTER release — logging under the gates lock extends the critical section and cascades contention into degrade storms
72
+ - `recordSuccess` is exact-only (byKey across scopes, no fuzzy scan): it runs on every successful bash call, and healing/chain-clearing are state mutations — fuzzy matches are attribution convenience, never a basis for mutation
44
73
  - Log appends and rotation take the log lock — every OpenCode window shares the global log; unlocked appends interleave into broken JSON
74
+ - Nothing logs under the gates lock — events are deferred (`deferEvent`) and flushed by the next `log()`/`flushDeferred()`; scripts (doctor/migrate) MUST call `flushDeferred()` before exiting or repair events are lost ("every repair is logged"). The drain happens INSIDE the log lock (draining before the lock dropped events)
75
+ - `reconcile()` preserves the migration stamp when it rewrites gates.json — dropping it forces a full `migrate()` scan on every startup (init storm)
76
+ - `logAll` scopes events: high-volume events (`detected`/`reminded`/`blocked`/`retry-allowed`/`recurred-after-gate`) stay in the project log; only machine-memory-salient events (`GLOBAL_LOG_EVENTS` = init/promoted/demoted/healed/retired-*/override) reach the global log — the global log lock is the most-contended lock
77
+ - Deferred events bypass `logAll`'s routing, so the project store mirrors them itself: `routeSalientTo` (project→global, wired in the `Stores` constructor; the global store gets NO peer) makes `log()`/`flushDeferred()` forward the salient subset of the DRAINED deferred batch via the peer's `appendBatch`, AFTER the store's own log lock releases (leaf locks, one at a time). Direct events are NEVER mirrored here — `logAll` already routes them; mirroring the direct `event` inside `log()` would double-write it to the global log
78
+ - `recordFailure` uses FLAT lock phases — the project gates lock is released before the index lock and the global gates lock (never nested); escalation is three short phases (copy → global-first write → remove-local); a crash between the two writes leaves a duplicate healed by migrate, never a hole
79
+ - The TTL sweep timer is jittered (0.75–1.25× interval) and flushes deferred events (`flushDeferredAll`) so a quiet long-lived process doesn't lose sweep events on exit
45
80
  - Every gate read from disk crosses `coerceGateShape` + `repairGate` in `load()` — enforcement never sees raw state; hopeless records are dropped, repairable ones coerced
46
81
  - Quarantine preserves bytes: unparseable files are renamed to `*.corrupt-*`, never deleted; every repair emits a `repaired`/`quarantined` log event
47
82
  - Fuzzy matching is Levenshtein-based on purpose: token Jaccard collapsed all `<str>` placeholders into one bucket; ratio ≤ 0.3 PLUS absolute distance ≥ 3 (verb-level-different commands must never merge)
83
+ - Evidence quality: `failureSnippet` is error-aware — for a non-zero exit it scans from the END for a failure-shaped line and never returns a success-shaped one (`looksLikeSuccess` rejects pass summaries); `recordFailure` never overwrites a failure-shaped snippet with a success-shaped one (latest wins among failure-shaped); `repairGate` clears success-shaped snippets and re-derives machine `Last error:` template corrections that quoted one (human edits never match the fixed template byte-for-byte, so they stay)
84
+ - Chain attribution requires exactly ONE non-transparent producer (`nonTransparentProducers`) — with several, the exit code does not say which failed, so the failure records under the WHOLE call; a diagnostic segment's gate must not be inflated by another producer's failure
85
+ - `segmentHasIdentity` treats bare flags (`-x`/`--foo`) after a wrapper head as switches, not identity — a flag-only wrapper (`cmd <path> <str> -f`) matches a command family and may only watch
86
+ - Env assignments (`$env:X=…`, `FOO=bar`) and `start-sleep` are transparent producers (they cannot be the failing producer) — like navigation, they break neither exit-1 immunity nor attribution
87
+ - `promotionCount` increments on every promotion and is never reset (`mergeGate` sums it) — the rot-proof FLAPPY measure (the log-based one rots with rotation); `save()` stamps `lastInitVersion` with the WRITER's own version — the durable drift signal (log init events rotate away)
48
88
 
49
89
  ## ANTI-PATTERNS
50
90
 
51
91
  - Do NOT add a tool to `callSignature` without deciding its class: `PROBE_TOOLS` (higher bar, never blocks) or bash-class
52
92
  - Do NOT widen `FAILURE_SIGNATURES` to cover file-tool output — that text is file content; extend the event channel instead
53
93
  - Do NOT flatten interpreter one-liner payloads to `<str>` — `hashInterpreterPayload` must run before string parameterization; a bare `-c <str>` gate would block the whole command family
94
+ - Do NOT enforce signatures without residual identity — if normalization parameterized the whole command away (`cmd <path> <str>`, `node <str> <n>`), it matches a command family; `hasResidualIdentity()` guards every tier, such shapes may only watch
95
+ - Do NOT keep `cmd /c` wrappers in signatures — `unwrapCmdWrapper()` normalizes the payload so identity and the diagnostic tier see the real verb
96
+ - Do NOT prune global index entries because the CURRENT scope lacks the key — the gate may live in another project's store; only the TTL sweep removes entries
97
+ - Do NOT persist anything before `sanitizeForStore()` — terminal control chars in signatures/snippets/corrections are a bug (PowerShell colors errors with VT sequences)
54
98
  - Do NOT count aborted/cancelled executions as failures — filter with `isNoiseError()` at the event channel
55
99
  - Do NOT widen `coerceGateShape` drop rules casually — records it deems hopeless are silently dropped on every load; a bad rule silently empties stores
56
100
  - Do NOT write gates.json directly — always `runLocked` + `save()` (atomicWrite); logs are append-only via `log()`
57
101
  - Do NOT let `withLock` throw on contention — it degrades to unlocked after `LOCK_WAIT_MS` by design (pipeline must not hang)
102
+ - Do NOT reorder `INTERPRETER_ONELINER` alternatives shortest-first and do NOT add a code-passing interpreter flag without adding it to `CODE_PASSING_FLAGS` (the guard and the fingerprint must agree on what is structure)
103
+ - Do NOT promise blocks in reminding-tier messages — tier-truthful wording only; a wrong enforcement model teaches the agent wrongly
104
+ - Do NOT read `log.jsonl` outside the log lock for rewrite-style operations (excise/rotate) — an unlocked read + locked rewrite drops concurrent appends
105
+ - Do NOT count reminding-tier overrides toward `DEMOTE_OVERRIDES` — only blocking friction demotes