opencode-dejavu 2.6.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.
- package/CHANGELOG.md +367 -0
- package/README.md +26 -14
- package/command/dejavu.md +26 -0
- package/index.ts +416 -39
- package/package.json +6 -2
- package/scripts/analyze.ts +59 -0
- package/scripts/doctor.ts +505 -0
- package/scripts/githooks/commit-msg +46 -0
- package/scripts/migrate.ts +49 -0
- package/skills/dejavu/SKILL.md +48 -0
- package/src/AGENTS.md +56 -8
- package/src/patterns.ts +722 -32
- package/src/store.ts +805 -181
- package/src/validate.ts +58 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,372 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.27.0 — 2026-09-08
|
|
4
|
+
|
|
5
|
+
### Changed (friction + signal-to-noise)
|
|
6
|
+
- **Heal-aware blocking first encounter.** A blocking gate with recent consecutive successes (`succeededAfterGate > 0` — the command is being fixed) no longer aborts the first run of a session; it arms the remind→block chain silently and lets the call run. A success keeps healing; a repeat failure still blocks. This removes the last false-positive interrupt class (a stale blocking gate nagging a command that already works, e.g. the `cli start` case) without weakening blocking for commands that are genuinely still broken.
|
|
7
|
+
- **Doctor NOT TEACHING now flags only blocking gates.** A recurring reminding/diagnostic gate (gradlew test, flutter test, vitest) is normal iteration — the failure IS the work — not a teaching failure; flagging it was noise. A recurring blocking gate is the real "correction isn't working" signal.
|
|
8
|
+
|
|
9
|
+
## 2.26.1 — 2026-09-08
|
|
10
|
+
|
|
11
|
+
### Fixed (snippet evidence for bare-exit failures)
|
|
12
|
+
`failureSnippet` no longer falls straight to a bare `exit code N` when a failed command's output has no failure-shaped line: it now returns the last NON-success line (a success-shaped tail is still never surfaced). Gates on gradle/flutter/etc. that previously taught nothing now carry real context. Existing bare-exit gates pick up the better snippet on their next recurrence.
|
|
13
|
+
|
|
14
|
+
## 2.26.0 — 2026-09-08
|
|
15
|
+
|
|
16
|
+
### Fixed (correction quality — what dejavu actually tells agents)
|
|
17
|
+
A store audit of enforced gates surfaced three systemic advice defects; the two mechanical ones are fixed and propagate to existing gates via repair:
|
|
18
|
+
|
|
19
|
+
- **Unix-tool-in-PowerShell advice.** A large share of recurring gates were `… | head`/`tail`/`cat`/`wc` failing because those are Unix tools, not PowerShell commands. The captured snippet was the PowerShell boilerplate tail (`Check the spelling of the name…`), which the Unix rule in `suggestCorrection` never matched (it looked for "not recognized"). The rule now also matches the boilerplate tail and covers `cat`/`grep`/`less`; the correction teaches the native equivalent (`Select-Object -First/-Last`, `Get-Content`, `Select-String`, `(Get-Content f).Count`). Existing gates are re-derived on repair (6 upgraded in the audit).
|
|
20
|
+
- **Success/banner-shaped snippets are no longer stored as failure evidence** — gradle task summaries (`N actionable tasks: …`), `Configuration cache entry …`, and the `Node.js v<ver>` crash-tail banner join `looksLikeSuccess`, so they are cleared at the boundary instead of being quoted as the "error".
|
|
21
|
+
- **`repairGate` re-derives machine-made (AUTO_TEMPLATE) corrections on every repair**, not just when the quote is success-shaped — `suggestCorrection` upgrades now reach old gates. Human/agent edits never match the template byte-for-byte and are untouched.
|
|
22
|
+
|
|
23
|
+
### Known limitation (not mechanical)
|
|
24
|
+
Gates whose only evidence is a bare `exit code N` (no error line captured) still get the generic correction — there is nothing to teach from. 28 such gates in the audit; improving this needs better snippet capture or a non-mechanical step.
|
|
25
|
+
|
|
26
|
+
## 2.25.0 — 2026-09-08
|
|
27
|
+
|
|
28
|
+
### Added (self-maintenance: the remaining manual `--repair` work now runs itself)
|
|
29
|
+
- **Index orphans prune automatically (time-decayed candidacy).** The only fleet-wide operation a single plugin process could not safely do was pruning an index key whose gate lives in a project it cannot see. Now `expireAll` marks a key absent from every visible scope (own project + global) with `orphanCandidateSince`, clears it the moment any scope holds the gate again, and prunes only after `ORPHAN_CANDIDATE_DAYS` (7) of continuous absence — a live gate in another project clears its own candidacy on that project's sweep, so no cross-project evidence is lost in practice. `doctor --repair` remains the authoritative full-fleet sweep; the candidacy path removes the day-to-day need for it.
|
|
30
|
+
- **Startup health event.** Init logs a `health` event to the project log when enforced gates are NOT TEACHING (`recurredAfterGate >= 3`) or review-flagged, instead of letting them accumulate silently.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- **`git status` is a diagnostic** (joins `git show|log|ls-tree|ls-files|blame|diff`): read-only, its exit 1 is a downstream filter finding nothing, so it can never block and its exit 1 is immune. Existing blocking `git status` gates demote to reminding on repair.
|
|
34
|
+
- **`DEMOTE_OVERRIDES` 5 → 3.** An agent that bypasses a gate 3 times is fighting it — demote sooner. Gates already past the bar demote on the next repair.
|
|
35
|
+
|
|
36
|
+
## 2.24.1 — 2026-09-05
|
|
37
|
+
|
|
38
|
+
### Readiness-poll hang: port hint + multi-line wait-loop detection
|
|
39
|
+
A subagent started vite detached correctly but vite picked a FREE port (strictPort off) while the readiness poll / playwright waited on the configured port → hang. The long-running reminder now says: read the ACTUAL port from the server's startup log (don't assume the configured port), poll THAT port. Also WAIT-LOOP now catches multi-line PowerShell loops (`while ($true) { … Start-Sleep … }` across lines), which the single-line regexes missed.
|
|
40
|
+
|
|
41
|
+
## 2.24.0 — 2026-09-05
|
|
42
|
+
|
|
43
|
+
### Close the remaining subagent-hang vectors (deep-research driven)
|
|
44
|
+
Three research agents (oracle design, librarian on OpenCode bash internals, empirical gap probe) enumerated every way a bash call still hangs. Fixed the high-value, low-false-positive gaps:
|
|
45
|
+
- **More starters**: `docker compose up` / `docker run` (without `-d`, `run` constrained to `-p/-it`), `node --watch`, `bun --watch`, monorepo `yarn workspace <name> dev`, Python server entrypoints (`python app.py|server.py|main.py|wsgi.py|asgi.py`). Ambiguous `go run`/`cargo run`/`dotnet run`/`make` stay excluded by design.
|
|
46
|
+
- **`isDetached` hardened** (these were silently treated as detached but actually block): `& … wait`, `nohup X` without `&`, `Start-Process … -Wait` / `-NoNewWindow`.
|
|
47
|
+
- **New WAIT-LOOP guard**: polling loops (`while/until/for` + `sleep`/`Start-Sleep`, or `while (Test-Connection/Invoke-WebRequest) {`) with no timeout hang until the bash timeout; the reminder now pushes `curl --max-time N` / `-TimeoutSec N` / a max-iteration `break`.
|
|
48
|
+
- **Bypass visibility**: a `dejavu:proceed` bypass of the long-running guard now logs a warning, so "why did my subagent hang" is answerable after the fact.
|
|
49
|
+
- Confirmed from OpenCode source: bash stdin is `ignore` (interactive prompts fail fast on EOF, they don't hang), there is no model-facing background-job or PTY tool, and the 2-min (max 10-min) timeout with SIGTERM→SIGKILL is the only native hang cap — so proactive detection in the plugin is the right layer.
|
|
50
|
+
|
|
51
|
+
## 2.23.3 — 2026-09-05
|
|
52
|
+
|
|
53
|
+
### Long-running reminder is now actionable (stop agents giving up on e2e)
|
|
54
|
+
A subagent refused browser/e2e verification because starting the dev server seemed impossible (foreground is interrupted by the guard; a `start-dev.ps1` wrapper hangs). The guard was right to interrupt, but its reminder didn't hand the agent a working path, so it gave up instead of switching to detached. The reminder now includes a concrete detached recipe per shell (`Start-Process npm -ArgumentList 'run','dev'`, `Start-Process powershell -ArgumentList '-File','start-dev.ps1'`, `nohup … &`, `tmux new-session -d`) and the e2e workflow (start detached → poll the port → run tests → kill). Note: `.ps1`/`.sh` wrappers are deliberately NOT matched by name — a filename can't tell a detached starter (`start-backend.mjs`) from a foreground one, so name-matching would be a coin flip.
|
|
55
|
+
|
|
56
|
+
## 2.23.2 — 2026-09-05
|
|
57
|
+
|
|
58
|
+
### False-positive visibility (`doctor` OVERRIDDEN section)
|
|
59
|
+
Every `dejavu:proceed` override is the agent explicitly voting "this gate is wrong / friction." `doctor` now surfaces an **OVERRIDDEN** section listing gates with `overrideCount > 0`, sorted by overrides, and counts how many are **still enforced** (those are live false positives / friction and raise the issue count). Watching ones are history (already feedback-demoted); still-enforced ones (e.g. a `blocking` gradle pipeline the agent keeps overriding) are the actionable false positives. This makes "what is dejavu falsely nagging on?" a one-command answer, alongside the existing STALE-BLOCKING / FEEDBACK-DEMOTED / ANNOYING / NOT-TEACHING sections.
|
|
60
|
+
|
|
61
|
+
## 2.23.1 — 2026-09-05
|
|
62
|
+
|
|
63
|
+
### Long-running guard hardening (agent-driven combination sweep)
|
|
64
|
+
Two agents (a librarian survey of real starter/detach idioms across ecosystems + an empirical probe over ~60 command combinations) stress-tested the 2.23.0 guard and surfaced 14 false positives and 16 false negatives. Fixed the high-value, low-risk ones:
|
|
65
|
+
- **Starters added** (canonical, unambiguous): `npm|yarn|pnpm|bun start`, `ng serve`, `manage.py runserver` / `django-admin runserver`, `php artisan serve`, `jupyter lab|notebook`, `webpack serve|webpack-dev-server`, `http-server|live-server`, `dotnet watch`, `hugo server`, `jekyll serve`, `mkdocs serve`, `mix phx.server`, `iex -S mix`, `nodemon`, `expo|react-native start`, `ollama serve`.
|
|
66
|
+
- **False positives fixed**: `pip install uvicorn gunicorn` no longer reads as starting a server (uvicorn/gunicorn now require a module:var or flag arg); `cat vite.config.ts` / `npm run build:vite` no longer match `vite` as a filename/`build:` target; `vite build --watch` now warns (watcher).
|
|
67
|
+
- **Detach detection broadened**: `screen -dm`, `pm2`, `Start-Job`, `forever`, `daemonize`, `systemd-run`, and a standalone `&` anywhere (trailing, mid-chain, or closing a subshell) — while `&&` chains still warn.
|
|
68
|
+
- **Read-only git inspectors are diagnostics** (`git show|log|ls-tree|ls-files|blame|diff`): a live subagent was interrupted by a blocking gate on `git show … | Select-String`, whose exit 1 is just the downstream filter finding nothing. These now promote to `reminding` at most; real git errors exit ≥ 2 and still count. Existing blocking `git show | …` gates were demoted by `doctor --repair`.
|
|
69
|
+
- Known limitations remain by design: ambiguous `node <file>`, `go run`, `dotnet run` are not flagged; mention-vs-execution (`grep 'npm run dev' Makefile`) can still warn — `# dejavu:proceed` escapes.
|
|
70
|
+
|
|
71
|
+
## 2.23.0 — 2026-09-05
|
|
72
|
+
|
|
73
|
+
### Long-running command guard (interrupt BEFORE the hang)
|
|
74
|
+
Agents and subagents sometimes start dev servers / watchers in FOREGROUND bash (`npm run dev`, `node server.js`, …). OpenCode's bash tool is one-shot (default 2-min timeout), so the call blocks until timeout, burns tokens, and strands an orphan process; native background-bash was removed from V2 and PTY/tmux are behavior, not enforcement. dejavu now interrupts a foreground server start in the before-hook with a "run detached" reminder (tmux / `nohup … &` / `Start-Process` / a startup script that spawns detached and returns).
|
|
75
|
+
- **Static, bounded class** — unlike open-ended error detection, the set of server starters is small and recognizable (`npm|yarn|pnpm|bun run dev|serve|watch`, `next|nuxt|astro dev`, `vite` (not build), `flask|streamlit run`, `uvicorn|gunicorn`, `python -m http.server`, `mvn spring-boot:run`, `gradle bootRun`, `rails s`, `php -S`), so it warns on first sight rather than learning from an expensive hang.
|
|
76
|
+
- **Detached forms pass silently** — trailing `&`, `nohup`, `setsid`, `disown`, `Start-Process`, `tmux new-session`, `start /b`. One-shots and builds (`vite build`, `npm run build`) are never flagged; ambiguous `node <file>`, `go run`, `dotnet run` are deliberately excluded.
|
|
77
|
+
- **`# dejavu:proceed`** remains the escape hatch for a deliberate foreground run.
|
|
78
|
+
|
|
79
|
+
## 2.22.1 — 2026-09-05
|
|
80
|
+
|
|
81
|
+
### Noise boundary — two more infra classes, both non-bash tool errors
|
|
82
|
+
A post-release sweep of the live stores surfaced two more server-side-unavailability patterns that had accumulated as gates (the playwright one at 10x/10 sessions, the LSP one at 10x/6). Both are non-bash tool errors, so they could only ever `watch` — classifying them noise removes clutter with zero teaching lost.
|
|
83
|
+
- **Closed-browser automation errors** (`target page, context or browser has been closed`) — a transient startup/state hiccup fixed by relaunching, not an agent habit.
|
|
84
|
+
- **LSP diagnostics timeouts** (`timed out waiting for fresh diagnostics … within 3000ms`) — the LSP was slow to answer, a latency hiccup, not a mistake.
|
|
85
|
+
- Retroactive: `doctor --repair` expired the accumulated gates (both were non-bash `watching` gates in the global store).
|
|
86
|
+
|
|
87
|
+
## 2.22.0 — 2026-09-04
|
|
88
|
+
|
|
89
|
+
### Theme
|
|
90
|
+
Root-cause pass over the accumulated store data — 1481 gates across 7 stores analyzed by `analyze`+`doctor`, then reviewed by explore/oracle/kimi3-verifier/Momus. The stores had been faithfully remembering the WRONG things: success lines as errors, infrastructure noise as failures, and whole command families as specific calls. This release fixes evidence quality, the noise boundary, and attribution integrity — what we remember, not just how we remember it.
|
|
91
|
+
|
|
92
|
+
### Evidence quality (the "17 passed" bug)
|
|
93
|
+
- **`failureSnippet` is error-aware.** For a non-zero exit it now scans from the END for a failure-shaped line instead of blindly keeping the last non-empty line. A success-shaped tail ("17 passed (3.1m)", "1 passed (50.1s)") is never returned as failure evidence; when only success lines remain it falls back to `exit code N`. Chained commands and `Select-Object -Last N` pipelines put another shard's pass summary at the tail — that was teaching gates to "fix" a passing summary.
|
|
94
|
+
- **`looksLikeSuccess` / `looksLikeFailure`** are the shared evidence-quality classifiers. A line that reports failures is never success, even when it also tallies passes ("1 failed, 1780 passed").
|
|
95
|
+
- **`FAILURE_SIGNATURES` gaps closed:** the PowerShell "is not recognized as the name of a cmdlet" wording (only the cmd wording matched, so head/tail/wc gates stored the "Check the spelling" boilerplate), bare runner summaries `[1-9]\d* failed` (pytest/playwright/vitest), `no tests found/matched`, and the generic `^error:` prefix.
|
|
96
|
+
- **Evidence monotonicity:** `recordFailure` never overwrites a failure-shaped snippet with a success-shaped one (latest still wins between two failure-shaped snippets).
|
|
97
|
+
|
|
98
|
+
### Cross-language generalization (detection is an engine, not a JS/Python list)
|
|
99
|
+
An adversarial cross-ecosystem probe (librarian ground truth for 22 tools + an empirical battery + an independent verifier) showed the v2.22.0 evidence engine was tuned to the JavaScript/Python/PowerShell formats seen in production: Go `--- FAIL:` / `exit status 1`, Maven `[ERROR] BUILD FAILURE`, RSpec/Elixir/minitest `N failure(s)`, dotnet `Failed! - Failed: N` (reversed order), PHPUnit `FAILURES!`, sbt `*** TEST FAILED ***` were all undetected, so their gates degraded to a useless `exit code 1` correction. The fix is language-agnostic, not per-tool enumeration:
|
|
100
|
+
- **Failure vocabulary generalized by shape, not by tool:** count-bearing failure forms now match with a NON-ZERO count in EITHER order (`1 failed` / `Failed: 1` / `Failures: 1` / `failures=N`), covering every runner's summary; build-status words that never appear in a pass summary (`BUILD FAILURE|FAILED`, standalone uppercase `FAIL`, `TEST(S) FAILED`); compiler prefixes that carry a bracket (`error[E0308]:`, `[ERROR]`); Go `exit status N`; TAP `not ok`.
|
|
101
|
+
- **Success invariant is substring-based, not line-start:** decorated/embedded pass summaries (`==== 10 passed ====`, `test result: ok.`, `BUILD SUCCESSFUL`, `OK (N tests)`, Go `ok\tpkg`, dotnet `Passed!`/`Build succeeded.`) are all rejected as evidence. The non-zero-failure guard (`1 failed`, `Failed: 1`) runs first, so a `0 failed` pass tally can never read as a failure — the "17 passed" bug cannot recur in any ecosystem's clothing.
|
|
102
|
+
- **Leading runner decorations** (`====`, `---`, `[info]`) are stripped before matching, so a pattern need not anticipate every tool's framing.
|
|
103
|
+
- Regression battery locks it: 15 ecosystems' failing outputs must yield a real evidence line (never `exit code 1`), and every ecosystem's pass summary must be rejected.
|
|
104
|
+
|
|
105
|
+
### Correction integrity
|
|
106
|
+
- **`suggestCorrection` never quotes a success-shaped snippet as "Last error"** — it produced `Last error: "17 passed (3.1m)"`. Bare `exit code N` is no longer quoted either.
|
|
107
|
+
- **New correction families:** Unix commands in PowerShell (head/tail/wc → `Select-Object -First/-Last`, `(Get-Content).Count`), file-not-found for read/edit/write (locate via glob, don't guess path variants), and command-not-installed.
|
|
108
|
+
- **`repairGate` heals legacy evidence at the persistence boundary:** clears success-shaped snippets and re-derives a machine `Last error: "…"` template correction that quoted a success line. Human edits never match the fixed template byte-for-byte and are untouched.
|
|
109
|
+
|
|
110
|
+
### Noise boundary (server-side unavailability is not an agent mistake)
|
|
111
|
+
- **`NOISE_ERRORS` extended:** LSP daemon unreachable, MCP transport / streamable-http errors, webfetch non-2xx, and webfetch/gRPC `transport error` (the connection itself never completed). Client-side mistakes (4xx, ENOENT, syntax) stay teachable.
|
|
112
|
+
- **`isNoiseError` now guards the after-hook too** (it guarded only the event channel), so infra noise grows no gate from either channel.
|
|
113
|
+
- **Retroactive cleanup:** `migrate()` backdates already-classified noise gates to the epoch so the TTL sweep expires them (the accumulated lsp-daemon / webfetch / MCP-noise gates).
|
|
114
|
+
|
|
115
|
+
### Attribution integrity (the playwright-count-56 bug)
|
|
116
|
+
- **Chain attribution applies only with exactly ONE non-transparent producer.** With several producers the exit code does not say which one failed, so attributing the failure to a known segment fabricated evidence — a diagnostic segment's gate inflated by a non-diagnostic producer's failure. Such chains now record under the whole call. New `nonTransparentProducers` counts the producers (navigation, env assignments, start-sleep, pipe-tail formatters are transparent).
|
|
117
|
+
|
|
118
|
+
### Immunity holes closed
|
|
119
|
+
- **Env assignments (`$env:CI="true"`, `FOO=bar`) and `start-sleep` are transparent** — they cannot be the failing producer, so they no longer break a diagnostic's exit-1 immunity or attribution.
|
|
120
|
+
- **`npm run check:*` / `verify:*` are diagnostics** — like test/typecheck/lint, their exit 1 is "found issues", not an infrastructure error.
|
|
121
|
+
- **Flag-only wrapper shapes lose residual identity** — `cmd <path> <str> -f` matches a whole command family (a flag is a switch, not call identity) and may only watch.
|
|
122
|
+
|
|
123
|
+
### Lifecycle & observability
|
|
124
|
+
- **Reminding taught retirement** — a diagnostic gate reminded `TAUGHT_REMINDERS`+1 times with zero same-session reoffense retires softly to `watching` (no `feedbackDemoted`), logged `retired-taught`. The bar is one clean reminder above the blocking threshold so it never preempts anti-nag evidence. `recurredAfterGate` is no signal here (it grows structurally for reminding gates — every session's first failure counts, the note rides after it).
|
|
125
|
+
- **`promotionCount` lifetime counter** — incremented on every promotion, never reset, summed by `mergeGate`, parsed by `coerceGateShape`. Doctor's FLAPPY escalates to an issue at `promotionCount >= 3` (rot-proof oscillation evidence; the log-based FLAPPY rots with rotation). Report-only — no mechanical auto-demotion.
|
|
126
|
+
- **`lastInitVersion` drift signal** — `save()` stamps the WRITER's own version into gates.json on every save; doctor reads it first (log init events rotate away). Fixes the "version indeterminate" blind spot on busy logs.
|
|
127
|
+
- **`migrate(force)` for explicit repair** — `doctor --repair` and `bun scripts/migrate.ts` now force the full per-gate migration regardless of the version stamp. The init-storm skip is a startup optimization; an explicit repair must apply ALL healing, otherwise a same-version re-run of `--repair` silently skips newly added repair logic.
|
|
128
|
+
|
|
129
|
+
### Data
|
|
130
|
+
- Ran `doctor --repair` across all 7 stores: pruned true-orphan index keys, stamped `lastInitVersion`, expired noise gates (lsp-daemon / webfetch non-2xx / grep-app / transport-error), demoted flag-only / success-evidence gates. Post-repair invariant verified: no enforced gate carries a success-shaped snippet or a garbage template correction.
|
|
131
|
+
|
|
132
|
+
## 2.21.0 — 2026-09-03
|
|
133
|
+
|
|
134
|
+
### Changed (reminding gates never interrupt — "help, don't nag" completed)
|
|
135
|
+
Two production screenshots showed the same root problem: a reminding (diagnostic) gate aborted the call with a reminder and hid the output, nagging with stale evidence (`flutter test` after the immunity fix; a `dart analyze`+`custom_lint` chain whose "103 issues" predated a cleanup). The reminding tier existed to protect iteration, yet aborting the run was itself punishment for iterating. Now:
|
|
136
|
+
|
|
137
|
+
- **Reminding gates never abort.** The before-hook lets `reminding`-tier calls straight through; the reminder is delivered as a non-blocking `[dejavu] NOTE` appended to the FAILING output in the after-hook, once per session (a repeat same-session failure only accrues `recurredAfterReminder`, no second note). A run that succeeds produces no note at all — so the stale-evidence nag disappears entirely. Blocking gates are unchanged (remind-abort on first encounter, hard block on same-session repeat).
|
|
138
|
+
- **Anti-nag retirement for reminding gates accrues in the after-hook** — a gate reminded `ANTI_NAG_REMINDERS` (5) times whose notes are consistently ignored (`recurredAfterReminder >= ANTI_NAG_REOFFENSE` (3)) retires to `watching` + `feedbackDemoted`, logged `demoted` ("anti-nag retirement"). Taught retirement deliberately does not apply to reminding gates (a failure event cannot prove a reminder taught anything).
|
|
139
|
+
- **`repairGate` zeroes a stale `recurredAfterReminder` at the blocking→non-blocking demotion transition only** (was: on every load). The counter now accrues legitimately while reminding, so the unconditional reset would have wiped it inside every store lock; tying it to the transition keeps the "no retirement on the previous tier's evidence" guarantee.
|
|
140
|
+
|
|
141
|
+
## 2.20.0 — 2026-09-03
|
|
142
|
+
|
|
143
|
+
### Fixed (residual immunity blind spots found in post-restart production data)
|
|
144
|
+
Re-checked the stores after the 2.19.0 restart: immunity held for 27 of 30 post-restart diagnostic failures, but three residual shapes still gated ordinary iteration work. All three closed:
|
|
145
|
+
|
|
146
|
+
- **`cd` + diagnostic in one segment no longer dropped as navigation** — `cd packages/foo npx vitest run …` (no separator between the path and the command) was dropped wholesale as navigation, hiding the diagnostic and breaking immunity. Navigation is now transparent only when the segment is PURE navigation; a segment pairing a nav verb with a diagnostic keeps the diagnostic. A bare `cd /bad/path` still counts (not immune).
|
|
147
|
+
- **Subshell-paren flattening no longer breaks PowerShell script blocks** — `Select-String … | ForEach-Object { $_.line.trim() }` was gated because the blanket `()→;` flatten split the `.trim()` method-call parens inside the `{ }` script block. New `flattenSubshellParens` flattens parens only OUTSIDE `{}` braces and quotes, so method-call parens stay part of their segment while `(deploy && grep)` still splits (a diagnostic nested in parens must not blanket-immunize a non-diagnostic).
|
|
148
|
+
- **`npm/pnpm/yarn` `typecheck`/`lint` are diagnostics** — production `npm run typecheck` was reminded 20+ times because only `test` was recognized; `typecheck`/`lint` join it, including the flags-between form (`pnpm --filter <pkg> typecheck`). `npm run build` stays non-diagnostic (a build failure is a real error, not the work).
|
|
149
|
+
|
|
150
|
+
### Note (deliberate non-goal)
|
|
151
|
+
The point of these fixes is NOT to enumerate every command in every language — that is whack-a-mole. The durable design is (a) cover the common iteration commands broadly and (b) let anti-nag retirement self-correct anything that still slips through and nags. Both are now in place.
|
|
152
|
+
|
|
153
|
+
## 2.19.0 — 2026-08-31
|
|
154
|
+
|
|
155
|
+
### Fixed (closing the verifier's remaining blind spots — "help, don't nag" cleanup)
|
|
156
|
+
The 2.18.0 `kimi3-verifier` review confirmed ship-ready but listed five pre-existing blind spots. Three were safe and worth closing; two are deliberately left (see Notes).
|
|
157
|
+
|
|
158
|
+
- **Bash `|&` (pipe stdout+stderr) is now a pipe separator** — `npm test |& head -5` was counted instead of immune because the `&` glued onto the next segment (`& head -5` misses the anchored formatter regex). Both `splitChain` and `splitChainTagged` now consume `|&` as a 2-char pipe, so the tail is a pipe-tail formatter and the diagnostic keeps immunity.
|
|
159
|
+
- **Unix `tee` added to the pipe formatters** — only `tee-object` was recognized, so `npm test | tee out.log` counted. `tee` (a pass-through that exits 0) is now transparent in pipe-tail position; a standalone `tee` producer still counts.
|
|
160
|
+
- **Stale `recurredAfterReminder` cleared on tier demotion** — the counter accrues only while blocking, but a policy demotion (e.g. a legacy blocking `npm test` gate demoted to reminding when it became diagnostic) left the stale value, which blocked taught-retirement (needs it `=== 0`) and let the gate nag until TTL. `repairGate` now zeroes it once the gate is no longer blocking, letting such gates taught-retire softly. Regression tests split the two outcomes by `recurredAfterGate`.
|
|
161
|
+
|
|
162
|
+
### Notes (two verifier blind spots deliberately NOT fixed)
|
|
163
|
+
- **Single `&` is not treated as a chain separator** — on Windows `&` is the PowerShell **call operator** (`& "C:\…\exe" args`), so splitting on it would break those invocations. Bash-style backgrounding (`A & B`) is rare in agent commands here; leaving it unsplit is the correct call, not a gap.
|
|
164
|
+
- **Manually re-enforced gates keep old session chains** — re-enforcement is a human edit of `gates.json`; the mechanical path (promotion) clears chains, and a human can clear the arrays too. Documented behavior, not a defect.
|
|
165
|
+
|
|
166
|
+
## 2.18.0 — 2026-08-31
|
|
167
|
+
|
|
168
|
+
Theme: **the plugin should help, not nag** — finish closing the immunity blind spots and stop interrupting when interrupting provably does nothing.
|
|
169
|
+
|
|
170
|
+
### Fixed (production-data follow-up: the immunity fix had a formatter blind spot)
|
|
171
|
+
Re-ran doctor/analyze on the accumulated stores after 2.17.0 and found a live case the immunity still broke: `cd <path> && npx vitest run … >& <n> | head - <n>` (MidasAI). 2.17.0 made the **PowerShell** pipeline formatters (`Select-Object`, `Tee-Object`, …) transparent, but not the **unix** output shapers — so piping a diagnostic into `head`/`tail`/`column`/`uniq` still counted the ordinary test failure.
|
|
172
|
+
|
|
173
|
+
- **Unix output shapers added to the transparent formatters** — `head`, `tail`, `column`, `uniq` join the PowerShell cmdlets in `PIPE_FORMATTERS`. Piping a diagnostic into one keeps its exit-1 immunity; a non-diagnostic piped into a formatter still counts (`npm install | head -5` still gates), and a bare formatter as the producer still gates.
|
|
174
|
+
- **Formatter transparency is pipe-position only** — an independent review (`kimi3-verifier`) refuted the first cut, which was separator-blind and over-granted: `npm test && tail -5 missing.log` became immune even though `npm test` exits 0 under `&&` and the exit 1 is `tail`'s file-not-found (a real recurring mistake). Transparency now applies only to segments `splitChainTagged` marks as **pipe tails** (`|`); a formatter as a `;`/`&&`/`||` terminal producer is the failing producer and its exit still counts.
|
|
175
|
+
|
|
176
|
+
### Added (anti-nag retirement — the negative twin of taught retirement)
|
|
177
|
+
- **Anti-nag retirement** — a **blocking** gate reminded `ANTI_NAG_REMINDERS` (5) times whose advice is consistently ignored (`recurredAfterReminder >= ANTI_NAG_REOFFENSE` (3): the agent reoffends in-session right after being reminded) is nagging, not teaching. On the next first-encounter it retires to `watching` + `feedbackDemoted`, the call proceeds **without** a reminder, and the event is logged (`demoted`, "anti-nag retirement"). Mirrors taught retirement (same hook point, same lock, first-encounter only) but marks `feedbackDemoted` so it does not mechanically re-promote into the nag loop; a human can re-enforce manually. Two guards the independent review forced: (1) `status === "blocking"` in the condition — `recurredAfterReminder` accrues only while blocking but a tier demotion keeps the stale counter, so a reminding/diagnostic gate demoted from a legacy blocking one must not be retired on someone else's old evidence; (2) the counters are **reset on fire**, so a manual re-enforce gets a genuinely fresh start instead of instantly re-triggering.
|
|
178
|
+
|
|
179
|
+
### Note (independent verification)
|
|
180
|
+
This release was hardened by a read-only adversarial review (`kimi3-verifier`) that refuted two over-claims in the first cut (the separator-blind formatter over-grant; anti-nag firing on a reminding gate via a stale counter) plus a manual-re-enforce trap. All three are fixed above with regression tests; typecheck + full smoke suite green.
|
|
181
|
+
|
|
182
|
+
## 2.17.0 — 2026-08-31
|
|
183
|
+
|
|
184
|
+
### Fixed (production-data analysis: exit-1 immunity was breaking on real-world command shapes)
|
|
185
|
+
Found by reading the accumulated store data (doctor + analyze across all projects): the dominant NOT-TEACHING / REMINDERS-IGNORED / ANNOYING / FLAPPY noise was test and type-check commands being **gated on ordinary test failures** — exactly the "the failures are the work" case the immunity exists for. Three root causes:
|
|
186
|
+
|
|
187
|
+
- **Piping a diagnostic into a PowerShell formatter broke immunity** — `flutter test --no-pub 2>&1 | Select-Object -Last 5` (and `Tee-Object`, etc.): the formatter segment is not in `DIAGNOSTIC_VERBS`, so the "every chain segment must be diagnostic" rule denied immunity and the test's exit-1 became a gate. PowerShell pipeline formatters never set the exit code (`$LASTEXITCODE` stays with the producing native command), so they are now transparent to the check. A real non-diagnostic producer still gates (`npm install | select-object` still counts).
|
|
188
|
+
- **A leading `cd <path> &&` broke immunity** — `cd X && npx tsc --noEmit`: the navigation segment is non-diagnostic and denied immunity even though only the diagnostic can fail. Navigation (`cd`/`set-location`/`pushd`/`popd`) is now transparent. (`cd` alone still gates — a bare `cd` to a bad path is a real recurring mistake.)
|
|
189
|
+
- **`npm test` / `yarn test` / `pnpm test` were not recognized as diagnostics** — the test-runner list covered pytest/jest/vitest/etc. but not the npm/yarn/pnpm script runners, so their ordinary test failures gated. Added. (`npm run build` stays non-diagnostic — a build failure is a real error, not "the work".)
|
|
190
|
+
|
|
191
|
+
The `deploy --broken && grep done` hazard (a later diagnostic hiding a real failure) is unchanged and still denied.
|
|
192
|
+
|
|
193
|
+
### Systemic lesson
|
|
194
|
+
- An allowlist rule ("every segment must be diagnostic") is only as good as its segment model: segments that never produce the exit code (pipe formatters, `cd`) must be transparent to it, or the rule false-fires on the exact shapes agents use to trim noisy output (`… | Select-Object -Last 5`). Real production data was the only thing that surfaced this — the synthetic immunity tests all used bare or `&&`-chained commands.
|
|
195
|
+
|
|
196
|
+
## 2.16.0 — 2026-08-30
|
|
197
|
+
|
|
198
|
+
### Added (implementing the three deferred audit items)
|
|
199
|
+
- **Cross-channel double-count guard** — the same failure recorded by BOTH detection channels (after-hook exit/text AND the event-stream error part) within 2s for one (key, session) is now counted once. The guard keys on the WHOLE-CALL signature, not the segment-attributed key: the event channel signs the entire call, so a chained command (`x && gated`) double-firing across channels still dedups on one identity instead of slipping through on mismatched keys. The channels are disjoint by construction today (bash fails via exit/text, file tools via error-state parts), so the guard is inert until upstream ever double-fires — then it keeps counts and demotion math correct instead of inflating them. The doctor tripwire from 2.11.0 remains as the observable.
|
|
200
|
+
- **Promote→heal oscillation damping (`retireBaseline`)** — `count`/`sessions` are lifetime-cumulative, so a healed or taught-retired gate re-promoted on the VERY NEXT single failure (the FLAPPY loop the round-7 doctor report now measures). Retirement (heal or taught) now captures `retireBaseline.count`; re-promotion requires a full fresh bar (`count − retireBaseline.count ≥ threshold`), consumed on promotion. Feedback-demoted gates are untouched (they never re-promote mechanically). The invariant holds on EVERY mechanical re-promotion path: `migrate()`'s watching→reminding catch-up also exempts `retireBaseline` gates — without that, a healed diagnostic gate's lifetime count cleared the catch-up bar and re-promoted on every migrate (each version bump), re-opening the oscillation and spamming `healed` into the global log. This is the damping the audits deferred until data justified — shipped behind the same evidence model, observable via the FLAPPY report.
|
|
201
|
+
- **Deferred salient events reach the global log** — deferred events bypass `logAll`'s routing, so a project-store `demoted` (migrate) or `retired-healed` (expireAll) never reached the global forensics despite being in `GLOBAL_LOG_EVENTS`. The project store now carries `routeSalientTo` (wired by `Stores` to the global store); `log()`/`flushDeferred()` mirror the salient subset of the DRAINED deferred batch to the peer after their own log lock releases. Direct events are NOT mirrored (logAll already routes them — mirroring would double-write).
|
|
202
|
+
|
|
203
|
+
### Notes
|
|
204
|
+
- **`dejavu:learned` stays abandoned** — the audits' fourth deferred item is deliberately NOT implemented: a marker an agent (or injected content) could emit to silence gates mechanically is an adversarial vector; the proxy metrics (`REMINDERS IGNORED` / `TEACHING-WELL`) already cover the legitimate need.
|
|
205
|
+
- Re-promotion smoke tests updated to the damped semantics (a single post-heal failure no longer re-promotes; a full fresh bar does).
|
|
206
|
+
|
|
207
|
+
### Systemic lessons
|
|
208
|
+
- Two detection channels that are "disjoint by construction" still need a runtime dedup keyed on the shared identity (key, session) + channel-mismatch window — construction guarantees rot when the producer is upstream of you.
|
|
209
|
+
- Damping a lifecycle oscillation is best done with a baseline captured at the transition (like `feedbackBaseline`), not by resetting the lifetime evidence — the evidence stays truthful for display/eviction/merge while the decision is gated.
|
|
210
|
+
- Deferred-event routing and direct-event routing must stay distinct code paths: they look identical at the log lock, but only one is already routed.
|
|
211
|
+
- A mechanical-state invariant ("never re-promote without a fresh bar") is only as strong as its WEAPEST promotion path — `recordFailure` honored the baseline but `migrate`'s catch-up was a second promotion path that bypassed it. When adding a transition rule, enumerate EVERY path that performs that transition (here: recordFailure + migrate catch-up), and gate them all; a fresh-eyes audit caught the one the implementer's mental model omitted.
|
|
212
|
+
|
|
213
|
+
## 2.15.0 — 2026-08-30
|
|
214
|
+
|
|
215
|
+
### Fixed (subagent audit round 8)
|
|
216
|
+
- **`load(true)` outside the gates lock in `reconcileAll` (two sites)** — the escalation filter (project store, no lock held) and the index rebuild (holding the index lock, not the gates lock) both used the force path, which quarantines an unparseable `gates.json` — a WRITE — without the gates lock. That is exactly the write-without-lock class round 3 fixed in doctor. Both now use non-force `load()` (routing-hint reads per the project's own invariant); `reconcile()` healed both scopes a few lines earlier, so the peeks are fresh and the force path's only job (quarantine-on-corruption) already ran under the lock.
|
|
217
|
+
|
|
218
|
+
### Systemic lessons (round 8)
|
|
219
|
+
- `load()`'s two modes have different write semantics: non-force is a pure peek, force CAN WRITE (quarantine). The rule is therefore "force only under the gates lock" — not merely "prefer force under the lock". Any new read outside the gates lock must be non-force, or it silently reintroduces the write-without-lock window.
|
|
220
|
+
|
|
221
|
+
## 2.14.0 — 2026-08-30
|
|
222
|
+
|
|
223
|
+
### Fixed (subagent audit round 7)
|
|
224
|
+
- **`reconcile()` held the gates lock across the log lock (last nesting)** — `exciseCorruptLogLines()` was called inside `runLocked`, acquiring the log lock while holding the gates lock on every init. No deadlock (the log lock is a leaf), but it extended the gates critical section by a full log read+parse+rewrite exactly at init-storm time — the round-4 lesson leaking in one last place. Log hygiene now runs after the gates lock releases.
|
|
225
|
+
|
|
226
|
+
### Added
|
|
227
|
+
- **Doctor FLAPPY report** — per-key count of `promoted` vs resolved (`healed`/`retired-healed`/`retired-taught`) log transitions; flags keys promoted ≥2 AND resolved ≥2 times (promote→heal oscillation). Data-gathering only: `count`/`sessions` are lifetime-cumulative, so a healed/retired gate re-promotes on a single next failure — damping is deferred until this report shows it matters.
|
|
228
|
+
- **AGENTS.md CODE MAP is now symbol-only** — dropped the per-symbol line numbers (they rot every audit round and misled the round-7 doc check); references locate by symbol name. Header metadata refreshed.
|
|
229
|
+
|
|
230
|
+
### Systemic lessons (round 7)
|
|
231
|
+
- The log lock is a LEAF lock — it is always acquired alone or outermost, never while holding a gates/index lock. `reconcile()`'s nesting was the last survivor of the pre-`exciseCorruptLogLines` era; "nothing heavy runs under the gates lock" must be re-checked against every new log-touching helper.
|
|
232
|
+
- Doc line numbers rot on every change — a CODE MAP that carries them goes stale each round and misleads the next audit. Symbol-only references are the stable form; the map names WHAT and WHERE (file), never WHICH LINE.
|
|
233
|
+
- Measure oscillation before damping it — promote→heal→promote is a real risk, but damping changes promotion semantics; ship the FLAPPY tripwire first, act only on data.
|
|
234
|
+
|
|
235
|
+
## 2.13.0 — 2026-08-30
|
|
236
|
+
|
|
237
|
+
### Fixed (subagent audit round 6)
|
|
238
|
+
- **`recordFailure` flat lock phases (item A)** — the previous implementation held the project gates lock across the index lock + the global gates lock + two saves: the longest critical section in the system, and every other window's waiter degraded to unlocked after `LOCK_WAIT_MS` (the lost-update window the `degraded` event documents). Each phase now holds exactly one lock (project gates → index → [copy, global, remove-local] for escalation). Crash invariant preserved (global-first-then-remove-local; a crash between the two writes leaves a duplicate healed by migrate, never a hole).
|
|
239
|
+
- **`logAll` scoping (item B)** — the global log is shared by every window of every project (the most-contended lock) and was double-writing every event. High-volume events (`detected`/`reminded`/`blocked`/`retry-allowed`/`recurred-after-gate`) now stay in the project log; only machine-memory-salient events (`init`/`promoted`/`demoted`/`healed`/`retired-*`/`override`) reach the global log. ~90% fewer global log-lock acquisitions.
|
|
240
|
+
- **Deferred events drained before the log lock (N1)** — `log()`/`flushDeferred()` drained `deferredEvents` BEFORE acquiring the log lock, so an event deferred between the drain and the lock was dropped from that flush. The drain now happens inside the log lock.
|
|
241
|
+
- **Logging moved out of the index lock (N2)** — `reconcileAll` and doctor logged `repaired` events while holding the index lock, extending the index critical section at exactly init-storm time. Now logged after the lock releases.
|
|
242
|
+
- **TTL timer flushes deferred events (N3)** — `expireAll` defers `expired`/`retired-healed` events; a quiet long-lived process previously held them until the next hook log (lost on exit). The jittered TTL timer now calls `flushDeferredAll()`.
|
|
243
|
+
|
|
244
|
+
## 2.12.0 — 2026-08-30
|
|
245
|
+
|
|
246
|
+
### Fixed (subagent audit round 5)
|
|
247
|
+
- **Migration stamp was erased on every startup** — `reconcile()` parses `gates.json` directly (bypassing `load()`) and saved without the in-memory stamp, so the next `migrate()` re-ran its full per-gate scan on every startup. The init-storm killer from 2.11.0 was dead code. reconcile now preserves the stamp.
|
|
248
|
+
- **`recordSuccess` logged under the gates lock** — a heal on a hot gate while other windows waited cascaded contention. The `healed` event now logs after the lock releases.
|
|
249
|
+
- **Scripts lost deferred repair events** — doctor/migrate repair stores then exit without a subsequent `log()` call, so deferred repaired/quarantined/demoted/expired events were silently dropped ("every repair is logged" invariant). Added `GateStore.flushDeferred()`; doctor and migrate call it.
|
|
250
|
+
- **`pendingCalls` leaked entries for reminded/blocked calls** — aborted calls never reach the after-hook, so their entries leaked until FIFO eviction at the cap. Now deleted when the signal throws.
|
|
251
|
+
- **Paren sub-expressions blanket-immunized the outer verb** — `deploy (grep x)` flattened to one segment and the inner diagnostic immunized deploy's failure. Parens now flatten to segment separators (`;`), keeping command-level granularity.
|
|
252
|
+
- **`flagTokens` recomputed per pair** — the flood path re-split/sorted the same incoming signature on every gate under the gates lock. Now bounded-cached.
|
|
253
|
+
- **TTL timer had no jitter** — windows opened together all swept the shared global store at the same instant every interval. Now jittered (0.75–1.25× interval).
|
|
254
|
+
|
|
255
|
+
## 2.11.0 — 2026-08-30
|
|
256
|
+
|
|
257
|
+
### Added (subagent audit round 4 — verification + backlog triage)
|
|
258
|
+
- **Migration stamp** — `gates.json` now carries `migrated: <plugin version>`; the 2nd..Nth start of the same version skips the full per-gate `migrate()` scan. The biggest init-storm contributor removed on the common path (repairGate on load still heals hand-edits/policy violations).
|
|
259
|
+
- **Doctor capacity & corruption visibility** — per-scope gate count vs `MAX_GATES` (warning at ≥80%), flood-guard eviction count, quarantine artifact count+size, and a cross-channel double-count monitor (same failure recorded by two channels within 2s — the early-warning tripwire for the latent upstream double-count).
|
|
260
|
+
- **Stale-steal is pid-liveness-gated and visible** — a stale lock is stolen only if the recorded holder pid is dead (ESRCH); a live slow holder is waited out, and every steal is logged (`stale lock stolen`). A same-pid holder (another window/context in this process) is never stolen.
|
|
261
|
+
|
|
262
|
+
### Fixed
|
|
263
|
+
- **Escalation no longer rests on ghost dirs** — project dirs renamed/moved away (common on Windows dev machines) no longer count toward the 2-project escalation threshold (`recordFailure`, `reconcileAll`, doctor MISSED ESCALATION all filter `existsSync`). Evidence is preserved in the index; only the decision ignores ghosts.
|
|
264
|
+
- **`expireAll`/`migrate`/`reconcile`/quarantine/excise no longer log under the gates lock** — events are deferred (`deferEvent`) and flushed by the next `log()` call, batched under one log-lock acquisition. The round-3 invariant ("nothing heavy runs while holding the gates lock") was leaking in five places; all closed.
|
|
265
|
+
- **`reconcile()` reports steals/degrades** — it called `withLock` without the callbacks, so a stale-steal during init was invisible.
|
|
266
|
+
- **`fuzzySimilar` rejects cheap-first** — the O(1) length-band check now runs before the code-fingerprint regexes and `flagTokens` allocation; the flood path calls this per gate under the gates lock, and most pairs are rejected before any allocation.
|
|
267
|
+
- **Hook log flushes can no longer swallow enforcement** — the post-lock event flushes are wrapped: a logging failure is reported via `logHookError` and the GateSignal still throws.
|
|
268
|
+
|
|
269
|
+
### Systemic lessons (round 4)
|
|
270
|
+
- Lock staleness must be judged by holder LIVENESS, not lock age — a slow live holder and a dead one need opposite responses; and same-process lock holders are always "live".
|
|
271
|
+
- Deferred-event flushing must be the ONLY way to log from inside a store lock — every `await store.log(...)` inside `runLocked` is a contention cascade waiting to happen.
|
|
272
|
+
- Cross-project evidence must distinguish "dir existed" from "dir exists" at decision time; ghost evidence is kept (it may return) but never decides.
|
|
273
|
+
- Escalation of hot-path rejection order matters: free O(1) checks before any allocation.
|
|
274
|
+
|
|
275
|
+
## 2.10.0 — 2026-08-30
|
|
276
|
+
|
|
277
|
+
### Fixed (adversarial review round 3)
|
|
278
|
+
- **Paren-wrapped chains blanket-granted exit-1 immunity.** `splitChain` keeps `(deploy --broken && grep done log)` as ONE segment, so a diagnostic anywhere inside immunized the non-diagnostic part — hiding deploy's failure. `isIntendedNonzero` now flattens paren groups before splitting (immunity needs command-level granularity).
|
|
279
|
+
- **Re-promotion inherited stale session chains.** The lifecycle reset cleared counters but left `remindedSessions`/`failedSessions` — the session that triggered a taught-retirement could skip its reminder after re-promotion ("one retry allowed" on a stale entry). Promotion now clears session chains too.
|
|
280
|
+
- **Logging under the gates lock cascaded contention.** `reminded`/`blocked`/`retry-allowed`/`override`/`recurred-after-gate`/`demoted` events were logged while holding the gates lock — log-lock contention extended the critical section toward degrade storms. Hook events are queued and logged after the lock is released.
|
|
281
|
+
- **`recordSuccess` ran a wasted fuzzy scan** on every successful bash call while accepting exact matches only — now an exact-only lookup (also removes the fuzzy proxy-heal surface entirely). `recordFailure` routing uses the O(1) key index instead of a linear scan.
|
|
282
|
+
- **doctor repairs were unsafe against the live plugin.** `--repair`'s key collection used `load(true)`, which could quarantine an unparseable gates.json WITHOUT the store lock while OpenCode is running (the `/dejavu` command runs doctor in-session) — now non-force `load()`. Also: `--repair` now sweeps expired gates (reports no longer show gates that should be gone), and a missing init event after log rotation reports "version indeterminate" instead of a false VERSION DRIFT.
|
|
283
|
+
- Docs: review-flag semantics (blocked 10+ times, error persists — not "fired while error stopped"), re-enforcement wording (set `status` back AND clear `feedbackDemoted`), tunables list (`TAUGHT_REMINDERS`, `DEMOTE_REOFFENSE_SESSIONS`), blocking-only override counting.
|
|
284
|
+
|
|
285
|
+
### Fixed (adversarial review round 2 — holes found by subagent audit of 2.9.0)
|
|
286
|
+
- **Race-burst taught-retirement hole.** A parallel burst of identical calls within the reminder race window each incremented `remindedCount` — one burst of 5 could `retired-taught` a gate on its very first encounter, having taught nothing (raced calls never saw a reminder). Only true first encounters count now.
|
|
287
|
+
- **Retire↔re-promote oscillation.** Counters were lifetime-cumulative: a re-promoted gate (after heal/taught retirement) re-retired on its first reminder (stale `remindedCount ≥ 5`, stale zero recurrences), and one early recurrence locked out taught-retirement forever. Promotion now starts a fresh enforcement lifecycle (remindedCount/recurrences/overrides/heal-streak/baseline reset).
|
|
288
|
+
- **Demotion voted by failures the gate could not prevent.** `recurredAfterGate` counted first-encounter failures that never saw a reminder — 3 sessions failing once each demoted a gate that never spoke, and one bad session/model in a shared store could demote a gate for everyone. Recurrence demotion now additionally requires `DEMOTE_REOFFENSE_SESSIONS` (2) distinct sessions that reoffended AFTER a reminder (`reoffenseSessions`, capped, lifecycle-scoped).
|
|
289
|
+
- **`migrate()` re-promoted feedback-demoted gates.** The watching→reminding catch-up ignored `feedbackDemoted` — a demoted diagnostic gate silently re-enforced on every restart, directly violating "never re-promotes mechanically". The invariant now holds on EVERY mechanical path.
|
|
290
|
+
- **Proxy success healed the wrong gate.** `recordSuccess` used fuzzy matching: a success on a fuzzy-similar command grew another gate's heal streak and cleared its session chain. Healing and chain-clearing now require EXACT matches — fuzzy is attribution convenience, never a basis for state mutation.
|
|
291
|
+
- **Override marker smuggling.** The bypass check accepted an unquoted `dejavu:proceed` anywhere in actionable text — `echo dejavu:proceed && gated-cmd` or `tool --message dejavu:proceed` bypassed gates (and 5 smuggled overrides demoted them). The marker now requires comment syntax (`# dejavu:proceed`).
|
|
292
|
+
- **Chain immunity hole.** Exit-1 immunity was granted if ANY diagnostic verb appeared anywhere in the command — `deploy --broken && grep done log.txt` hid deploy's failure. Immunity now requires EVERY chain segment to be diagnostic.
|
|
293
|
+
- **Mid-session corruption → silent data loss.** `load()` conflated "file missing" with "file unparseable": a corrupted gates.json became an empty store in memory, and the next save overwrote the recoverable bytes. Parse errors now quarantine (bytes kept, fresh store started), like reconcile.
|
|
294
|
+
- **Lock ownership race.** After a stale-steal, the original holder's `unlink` deleted the STEALER's lockfile, opening the critical section to a third process. `withLock` now verifies ownership (pid in the lockfile) before releasing.
|
|
295
|
+
- **Env-prefixed one-liners escaped fingerprinting.** `PYTHONPATH=x python -c "..."` normalized to an over-generic watching shape; the interpreter regex anchor now allows leading env assignments.
|
|
296
|
+
- **`mergeGate` dropped the reminding tier** when merging a reminding source into a watching target (status merge is now rank-preserving: watching < reminding < blocking).
|
|
297
|
+
- Probe gates at count 3-4 no longer get the 60-day TTL (their promotion bar is 5); `coerceGateShape` round-trips `succeededAfterGate === 0`.
|
|
298
|
+
|
|
299
|
+
### Added
|
|
300
|
+
- **Retire-on-taught** — the positive twin of feedback demotion: a gate reminded `TAUGHT_REMINDERS` (5) times with zero in-session reoffense AND zero post-gate failures has taught its lesson — the agent changed behavior, so no success can ever heal it (the `wc -l` eternal-reminder loop). It retires softly to watching (logged `retired-taught`); re-promotion on new failures stays possible.
|
|
301
|
+
- **Flood guard prefers feedback-demoted victims** — proven-unteachable gates were the stickiest residents under the old lowest-count rule; they are evicted first now, and every eviction is logged (was invisible).
|
|
302
|
+
- **Lazy failure scan** — the full-output `detectFailure` scan is skipped on successful bash calls with exit metadata (hot-path cost).
|
|
303
|
+
- **doctor**: `REVIEW-FLAGGED` enforced-only (the flag never clears, healed gates would flag forever); `NOT TEACHING` baseline-relative (a human re-enforcement keeps its grace window); `GLOBAL_PROJECTS`/`DEMOTE_RECURRENCES` imported from store instead of hardcoded.
|
|
304
|
+
|
|
305
|
+
### Systemic lessons (how not to step on this class again)
|
|
306
|
+
- Every mechanical promotion/enforcement path must be audited against every demotion flag — a flag enforced in one path and ignored in another is a state-machine hole.
|
|
307
|
+
- Bypass markers must require unambiguous syntax (comment form); "strip quotes then regex" is not an annotation/data distinction.
|
|
308
|
+
- Policy checks over chains decide per-segment or all-segments — never "anywhere in the text".
|
|
309
|
+
- Fuzzy matching is for enforcement ATTRIBUTION; state mutations (heal, clear, consolidate) require exact identity.
|
|
310
|
+
- Locks are verified on release, not just acquired.
|
|
311
|
+
- Behavioral counters are lifecycle-scoped, not lifetime-cumulative: any retire/re-promote boundary resets them, or stale evidence from a previous lifecycle leaks into the next (oscillation, permanent lockouts). Lifecycle resets must clear ALL per-session enforcement state, not just counters — stale chains leak across lifecycle boundaries too.
|
|
312
|
+
- Feedback votes count only events the gate had a chance to influence (post-reminder failures, distinct sessions) — raw event counts let one bad session punish everyone.
|
|
313
|
+
- Chain-policy heuristics must treat paren groups as containers, not atoms — flatten whenever the decision needs command-level granularity.
|
|
314
|
+
- Nothing heavy (logging, nested locks) runs while holding the gates lock — lock hold time is contention cascade; diagnostics run outside the critical section.
|
|
315
|
+
- Diagnostic/report tooling must not mutate stores (quarantine) without the lock — it runs while the live plugin is open.
|
|
316
|
+
|
|
317
|
+
## 2.9.0 — 2026-08-30
|
|
318
|
+
|
|
319
|
+
### Added (the arms race, closed — negative-feedback loop completed)
|
|
320
|
+
- **Success clears the chain.** A success on an enforced gate now removes the succeeding session from its remind→block chain (`remindedSessions`/`failedSessions`). Before, a session that PROVED the fix (often via `dejavu:proceed`) stayed blocked forever and could only keep overriding — and the overrides then demoted the very gate the agent had just vindicated. Now: override once, succeed, the session is clean.
|
|
321
|
+
- **Iteration verbs remind-only.** `dart run`, `go run|build|test|vet`, `cargo run|build|test|clippy` join the diagnostic tier: their failures are the work itself (the agent is fixing the code they run). Blocking them produced the production arms races — dozens of overrides, zero teaching.
|
|
322
|
+
- **Doctor consumes the in-session metric.** New `REVIEW-FLAGGED` (mechanical `review: true`), `REMINDERS IGNORED` (`recurredAfterReminder >= 3` — the correction teaches nothing), and `TEACHING-WELL` notes; `recurredAfterReminder` is no longer a dead metric. `doctor --repair` now prunes TRUE-orphan index keys — safe only there, where every scope is visible at once.
|
|
323
|
+
- **analyze** shows reminding/feedback-demoted counts and discovers project stores from the global index (parity with doctor).
|
|
324
|
+
|
|
325
|
+
### Changed
|
|
326
|
+
- **Overrides count only against blocking gates.** On a reminding gate the marker merely skips one interrupting reminder — avoiding that is rational agent behavior, not friction with the teaching. The event is still logged.
|
|
327
|
+
- **Reminders are tier-truthful.** A reminding gate no longer promises to "harden into a block" (it never can) — teaching the agent a wrong model of enforcement.
|
|
328
|
+
- **Hook errors are visible.** Hook catches log rate-limited (1/min) client-log errors — a silently dead plugin was invisible before.
|
|
329
|
+
- The global index is no longer rewritten on the FIRST failure of a brand-new pattern (no escalation value) — machine-wide index churn drops while escalation evidence is preserved (anything indexed or recurring updates as before).
|
|
330
|
+
- Log rotation also runs on the TTL timer (multi-day sessions never rotated before).
|
|
331
|
+
|
|
332
|
+
### Fixed
|
|
333
|
+
- `exciseCorruptLogLines` reads the log INSIDE the log lock — the unlocked read + locked rewrite dropped every line another window appended between the two (concurrent OpenCode startups all reconcile at once).
|
|
334
|
+
- Unparseable `firstSeen`/`lastSeen` reset to now at parse time — a hand-edited garbage date made a gate immortal (`expire` compares `Date.parse < cutoff`; NaN never is).
|
|
335
|
+
|
|
336
|
+
### Release hygiene
|
|
337
|
+
- The npm package now ships `scripts/`, `command/`, `skills/` — doctor/analyze/migrate and the `/dejavu` command work on the recommended install path.
|
|
338
|
+
|
|
339
|
+
## 2.8.0 — 2026-08-30
|
|
340
|
+
|
|
341
|
+
### Added (roots, not symptoms — learned from 9 days of production data across 5 projects)
|
|
342
|
+
- **Behavioral feedback demotion.** Enforcement now has negative feedback (the twin of `healed`): an enforced gate that keeps failing after promotion (`DEMOTE_RECURRENCES` = 3) or keeps getting explicitly bypassed (`DEMOTE_OVERRIDES` = 5) is demoted to `watching` and marked `feedbackDemoted` — it never re-promotes mechanically; a human re-enforces by editing `gates.json`, and the gate gets a fresh grace window (`feedbackBaseline`) instead of re-demoting on the next failure. Overrides are now counted per gate (`overrideCount`), every demotion logs a `demoted` event, and `migrate()` catches up gates that already crossed the thresholds.
|
|
343
|
+
- **Residual-identity guard.** Signatures whose substance was entirely parameterized (`cmd <path> <str>`, `node <str> <n> >& <n>`, `& <str> -c @ <str> @`) can no longer enforce at any tier — they match whole command families. Generalizes the legacy bare-one-liner rule: any future normalization gap degrades to watching instead of blocking arbitrary calls.
|
|
344
|
+
- **PowerShell identity.** `cmd /c|/k "payload"` unwraps to the inner command — the wrapper no longer hides the real verb from the diagnostic policy, and `/c` no longer becomes `<path>`; interpreter one-liners recognize quoted exe paths and the call operator (`& "C:\...\python.exe" -c ...`) and here-string payloads (`@"..."@`) — code no longer leaks into signatures as raw tokens.
|
|
345
|
+
- **Control-character stripping.** ANSI/VT sequences and C0 junk are stripped before persistence (`stripControl`/`sanitizeForStore`) — PowerShell colored errors no longer corrupt snippets/corrections with raw `ESC[31;1m` garbage; historical gates are cleaned by `migrate()`.
|
|
346
|
+
- **mypy** joined the diagnostic verbs (remind-only tier; exit 1 is its normal "findings" outcome).
|
|
347
|
+
- **Noise filters**: grep_app "no results found" and the question tool's "user dismissed" are not failures.
|
|
348
|
+
- **doctor without arguments** discovers project stores from the global index's project list (before: cross-store checks ran global-only and reported hundreds of false INDEX ORPHANS).
|
|
349
|
+
|
|
350
|
+
### Changed
|
|
351
|
+
- `reconcileAll()` no longer prunes index orphans: one process sees ONE project store, so a key whose gate lives in another project is invisible, not dead — pruning destroyed cross-project escalation evidence. Rot is still bounded by the 60-day TTL sweep in `expireAll`; doctor reports true orphans (it now sees every scope).
|
|
352
|
+
|
|
353
|
+
### Fixed (adversarial review round)
|
|
354
|
+
- **`withLock` no longer deletes a foreign lock**: a waiter degrading to unlocked ran `unlink` unconditionally in `finally` — removing the lockfile the live holder owned and letting a third process enter the critical section concurrently. This is the root cause of the zero-byte corrupt log line seen in production.
|
|
355
|
+
- **Interpreter flag fragmentation**: regex alternatives run longest-first (`-command`/`-encodedcommand` before `-c`/`-e`) — before, `-command` matched as `-c` and swallowed `ommand` into the payload, fragmenting one call into different keys per spelling. Long flags converge to `-c` in the emitted signature.
|
|
356
|
+
- **Windows `py` launcher** one-liners are fingerprinted (before: `py - <n> -c <str>` — an enforceable over-generic shape).
|
|
357
|
+
- **Residual-identity guard bypasses closed**: `python -m <str>` (the module is the program, like `-c` — `-m`/`--module` are code-passing flags) and chains headed by `cd`/`pushd`/`popd`/`set-location`/`exit` (`cd <path> && python <path>` no longer borrows identity from the builtin).
|
|
358
|
+
- **Over-generic shapes never fuzzy-match**: an incoming signature without residual identity matches concrete gates exactly only — it no longer enforces via fuzzy or pollutes unrelated gates' evidence (`findGate` + `recordFailure` consolidation).
|
|
359
|
+
- **Chain bypass through `cmd /c`**: segment signatures recursively unfold the wrapper payload — a gate on the inner command fires even when the whole chain hides inside `cmd /c "a && gated"`; `dejavu:proceed` inside a LEADING `cmd /c "..."` payload is honored as an override (before, the wrapper quotes hid it like smuggled data).
|
|
360
|
+
|
|
361
|
+
### Data notes
|
|
362
|
+
- Production evidence (9 days, ~1000 gates, 5 projects): the global `wc -l` gate taught 10 sessions with zero recurrences — reminders work; script-runner gates (`dart run`, mypy behind wrappers, `cmd /c` gradle) produced arms races with dozens of `dejavu:proceed` overrides and zero teaching — feedback demotion ends that race mechanically.
|
|
363
|
+
|
|
364
|
+
## 2.7.0 — 2026-08-26
|
|
365
|
+
|
|
366
|
+
### Added (no more manual corrections)
|
|
367
|
+
- **Auto-corrections.** A promoted gate now always ships with a mechanical, overridable default correction (`suggestCorrection`), chosen by command family (stale `--check` artifacts, failing tests, type errors, network, installs) or from the captured error line — so a gate never sits "NOT TEACHING" awaiting a human. `migrate()` backfills existing enforced gates.
|
|
368
|
+
- **Richer snippets.** For exit-code failures whose output matched no signature, dejavu keeps the last non-empty output line (`failureSnippet`) instead of a bare "exit code N", giving corrections real context.
|
|
369
|
+
|
|
3
370
|
## 2.6.0 — 2026-08-26
|
|
4
371
|
|
|
5
372
|
### Added (only well-grounded triggers)
|
package/README.md
CHANGED
|
@@ -19,9 +19,11 @@ Cross-session **memory prosthesis with teeth** for [OpenCode](https://github.com
|
|
|
19
19
|
tool call fails → signature normalized (paths/numbers/hashes stripped)
|
|
20
20
|
→ pattern-key counted, sessions tracked
|
|
21
21
|
→ 3 failures across 2 distinct sessions → gate promoted
|
|
22
|
-
next attempt → [dejavu] REMINDER thrown (call aborted, agent sees the correction)
|
|
23
|
-
retry fails again → same-session repeat offense → hard BLOCK on further attempts
|
|
24
|
-
|
|
22
|
+
next attempt → [dejavu] REMINDER thrown (call aborted, agent sees the correction)
|
|
23
|
+
retry fails again → same-session repeat offense → hard BLOCK on further attempts
|
|
24
|
+
diagnostic cmd → gate stays remind-only: the call RUNS and the reminder rides
|
|
25
|
+
on the failing output as a [dejavu] NOTE (once per session)
|
|
26
|
+
```
|
|
25
27
|
|
|
26
28
|
Design decisions (post-mortem of existing approaches):
|
|
27
29
|
|
|
@@ -29,8 +31,10 @@ Design decisions (post-mortem of existing approaches):
|
|
|
29
31
|
- **Gate messages are teachers.** Every message carries `CORRECTION:` (what to do instead) and `EVIDENCE:` (N failures across M sessions), not just a prohibition.
|
|
30
32
|
- **Mechanical pattern-keys only.** No LLM-based error classification in the hot path — the unreliable component doesn't do reliability work.
|
|
31
33
|
- **Two scopes.** Repo-specific gotchas live in `<repo>/.opencode/dejavu/` (committable); patterns seen in 2+ project dirs are agent-level habits and move to `~/.config/opencode/dejavu/`. No single store can see all projects, so a global pattern index (`index.json`) counts distinct project dirs per key and drives the escalation.
|
|
32
|
-
- **Gates rot — so they expire.** 60 days without recurrence and a gate is dropped. A gate
|
|
34
|
+
- **Gates rot — so they expire.** 60 days without recurrence and a gate is dropped. A gate blocked 10+ times without the error going away gets `review: true` for manual inspection.
|
|
33
35
|
- **The metric is recurrence-after-gate.** Tracked per gate as `recurredAfterGate` — if gates don't reduce recurrence, the whole approach is wrong and you'll see it in the data.
|
|
36
|
+
- **Enforcement has negative feedback.** The metric acts: a gate that keeps failing after promotion (3+ recurrences across 2+ sessions that reoffended after a reminder) or keeps getting explicitly bypassed (5+ `dejavu:proceed` overrides on a blocking gate — reminding-gate overrides are logged but not counted) is friction, not teaching — it demotes itself to `watching` and never re-promotes mechanically (`feedbackDemoted`). A human can re-enforce by setting `status` back and clearing `feedbackDemoted` in `gates.json`; the gate then gets a fresh grace window.
|
|
37
|
+
- **No identity, no teeth.** A signature whose substance was entirely parameterized away (`cmd <path> <str>`, `node <str>`) matches a whole command family and can never enforce — it may only watch. Over-generic shapes degrade to evidence instead of punishing unrelated calls.
|
|
34
38
|
|
|
35
39
|
## Install
|
|
36
40
|
|
|
@@ -60,19 +64,23 @@ Restart OpenCode. Gates appear automatically as failures recur — nothing to co
|
|
|
60
64
|
|
|
61
65
|
## Robustness & safety
|
|
62
66
|
|
|
63
|
-
- **Blocking policy** — only `bash` commands that are NOT diagnostics may ever become blocking gates.
|
|
64
|
-
- **One-liner identity** — for `python -c` / `node -e` / `bun -e` and friends the code payload IS the call, so it is fingerprinted (`<code:hash>`) instead of flattened to `<str>`: different scripts never share a gate, the same script failing repeatedly still converges. Legacy bare `-c <str>` shapes
|
|
67
|
+
- **Blocking policy** — only `bash` commands that are NOT diagnostics may ever become blocking gates. Diagnostics and iteration commands (tsc/eslint/mypy/pytest/gradle-test/flutter/curl/grep, `dart run`, `go run|build|test|vet`, `cargo run|build|test|clippy`...) promote to `reminding` — they annotate the failing output with a `[dejavu] NOTE` (once per session) and never block or interrupt the run, so iterating on tests/builds is never punished. File probes (read/edit/write/glob/grep) stay `watching`: measured, visible in reports, never interrupting. `canBlock()`/`canRemind()` in `src/patterns.ts` are the single source of truth. Signatures without residual identity (see above) enforce at no tier.
|
|
68
|
+
- **One-liner identity** — for `python -c` / `node -e` / `bun -e` and friends the code payload IS the call, so it is fingerprinted (`<code:hash>`) instead of flattened to `<str>`: different scripts never share a gate, the same script failing repeatedly still converges. PowerShell shapes are covered — quoted exe paths (`& "C:\...\python.exe" -c ...`), here-string payloads, env-prefixed invocations (`PYTHONPATH=x python -c ...`). Legacy bare `-c <str>` shapes never enforce at any tier (residual-identity guard).
|
|
69
|
+
- **Wrapper unwrapping** — `cmd /c|/k "..."` normalizes to the INNER command: the gate key, identity and diagnostic tier all see the real call instead of a `cmd <path> <str>` shape matching every cmd invocation.
|
|
70
|
+
- **Clean persistence** — terminal control characters (PowerShell VT colors, NULs) are stripped before anything touches disk; signatures, snippets and corrections never carry ANSI escapes.
|
|
65
71
|
- **Secret scrubbing** — every signature and snippet passes `scrubSecrets()` (OpenAI/Anthropic/AWS/GitHub/Slack/Stripe/JWT/bearer/DB-conn-string/PEM patterns + `root@host`) before touching disk. Historical data is cleaned by `migrate()` at init or via `bun scripts/migrate.ts <dirs...>` (also scrubs logs).
|
|
66
72
|
- **Intended non-zero exits** — exit 1 from diagnostics is NOT a failure (that is their normal "found nothing / found issues" outcome). Exit ≥ 2 always counts.
|
|
67
73
|
- **Aborted ≠ failed** — cancelled/aborted tool executions ("Tool execution aborted") are infrastructure noise and are never counted as failures.
|
|
74
|
+
- **Long-running guard** — a FOREGROUND dev-server/watcher start (`npm run dev`, `next dev`, `vite`, `flask run`, `uvicorn`, `python -m http.server`, `mvn spring-boot:run`, `gradle bootRun`, …) would block the bash call until its ~2-min timeout and strand an orphan process. dejavu interrupts it in the before-hook with a "run detached" reminder (tmux / `nohup … &` / `Start-Process` / a startup script that spawns detached). Detached forms (trailing `&`, `nohup`, `tmux`, `Start-Process`) and one-shots/builds (`vite build`, `npm run build`) pass silently; `# dejavu:proceed` allows a deliberate foreground run. The starter list is deliberately conservative (ambiguous `node <file>`, `go run`, `dotnet run` are not flagged).
|
|
68
75
|
- **File content is not command output** — text failure signatures are scanned for `bash` only; `read`/`edit`/`write` failures come exclusively from the event channel (a file containing "TypeError" is not a failure).
|
|
69
76
|
- **Concurrency** — gates.json mutations run under an exclusive lockfile; log appends and rotation take their own lock (every OpenCode window shares the global log); writes are tmp+rename with EPERM/EACCES/EBUSY retry (Windows AV/indexer). NT long paths get the `\\?\` prefix. If a lock cannot be acquired within 3s the critical section degrades to unlocked (the tool pipeline must never hang) and emits a `degraded` log event — the only window where updates can be lost is visible.
|
|
70
77
|
- **Multi-window safe** — the remind→block escalation chain is persisted on the gate itself (`remindedSessions`/`failedSessions`), not in process memory: several OpenCode windows on one store — and process restarts — all see the same chain. Enforcement always reads fresh gate state under the store lock.
|
|
71
78
|
- **Near-duplicate consolidation** — new failures merge into existing patterns via normalized Levenshtein ≤ 0.3 with an absolute floor of 3 edits (replaces token Jaccard, which collapsed all `<str>` placeholders; the floor stops `git push` vs `git pull`-style merges).
|
|
72
|
-
- **Bounded memory** — per-session maps are capped (
|
|
73
|
-
- **Migration** — gates outside the blocking policy are
|
|
74
|
-
- **Self-healing** — every init reconciles the stores: an unparseable `gates.json` is quarantined (bytes preserved as `gates.json.corrupt-<ts>`), gate records are strictly parsed and mechanically repaired (inverted dates swapped, duplicate keys merged, secrets re-
|
|
75
|
-
- **Gates heal, not just accumulate** — dejavu sees successes too: a SUCCESS matching an enforced gate grows `succeededAfterGate`, and after 3 in a row the gate retires to `watching` (logged `healed`), so a command you fixed stops triggering reminders. A failure resets the streak. This kills the "ruff check passed 10 times but dejavu still reminds" false positive.
|
|
79
|
+
- **Bounded memory** — per-session maps are capped (50 entries per gate) and freed on `session.deleted`; handled part IDs evict FIFO; TTL expiry and log rotation re-run every 6 h in long-lived processes.
|
|
80
|
+
- **Migration** — gates outside the blocking policy are re-tiered automatically (diagnostics land in `reminding`, everything else in `watching`); already-proven recurring diagnostics start reminding immediately; project copies of already-global gates are merged into the global gate (evidence is consolidated, never deleted).
|
|
81
|
+
- **Self-healing** — every init reconciles the stores: an unparseable `gates.json` is quarantined (bytes preserved as `gates.json.corrupt-<ts>`), gate records are strictly parsed and mechanically repaired (inverted dates swapped, duplicate keys merged, secrets/control-chars re-sanitized, stale blocking demoted), unparseable log lines are excised to `log.jsonl.corrupt`, and the cross-project index is reconciled (missing entries rebuilt; entries are never pruned on a single project's initiative — one process cannot see other projects' gates, and rot is bounded by the TTL sweep). Every repair is logged as a `repaired`/`quarantined` event.
|
|
82
|
+
- **Gates heal, not just accumulate** — dejavu sees successes too: a SUCCESS matching an enforced gate grows `succeededAfterGate`, and after 3 in a row the gate retires to `watching` (logged `healed`), so a command you fixed stops triggering reminders. A failure resets the streak. This kills the "ruff check passed 10 times but dejavu still reminds" false positive. The negative twin: a gate the agent keeps fighting (recurrences or explicit overrides) demotes itself (logged `demoted`) — enforcement listens to behavior in both directions. Third path: a gate reminded 5+ times with zero reoffense has TAUGHT its lesson (the agent changes behavior, so no success ever heals it) — it retires softly (logged `retired-taught`), re-promotion on new failures stays possible. Heal-aware: a blocking gate with a live heal streak (`succeededAfterGate > 0`) does not interrupt the first run — it lets a likely-fixed command run and blocks only a repeat failure.
|
|
83
|
+
- **Auto-corrections, no manual work** — a promoted gate always ships with a mechanical, overridable default correction chosen by command family (stale `--check` artifacts, failing tests, type errors, network, installs) or from the captured error line, so a gate never sits "NOT TEACHING" awaiting a human. `migrate()` backfills existing gates. Snippets now keep the last output line (`failureSnippet`) instead of a bare "exit code N".
|
|
76
84
|
|
|
77
85
|
## Observability (debugging aids)
|
|
78
86
|
|
|
@@ -100,26 +108,30 @@ Not covered (by design, v1): semantically-equivalent-but-syntactically-different
|
|
|
100
108
|
| `~/.config/opencode/dejavu/gates.json` | global gates (agent habits) |
|
|
101
109
|
| `~/.config/opencode/dejavu/index.json` | cross-project pattern index: which project dirs each failure key was seen in (escalation evidence) |
|
|
102
110
|
| `<repo>/.opencode/dejavu/gates.json` | project gates (repo gotchas) |
|
|
103
|
-
| `*/dejavu/log.jsonl` | every event: detected, promoted, reminded, blocked, override, expired, recurred-after-gate, repaired, quarantined |
|
|
111
|
+
| `*/dejavu/log.jsonl` | every event: detected, promoted, reminded, retry-allowed, blocked, override, expired, recurred-after-gate, demoted, healed, retired-healed, retired-taught, repaired, quarantined, degraded, init |
|
|
104
112
|
| `*/dejavu/*.corrupt*` | quarantined corruption (unparseable gates.json, excised log lines) — bytes preserved for forensics; safe to delete after inspection |
|
|
105
113
|
|
|
106
|
-
Both are human-editable. Removing a gate object disables it. Editing `correction` improves what the agent is told.
|
|
114
|
+
Both are human-editable. Removing a gate object disables it. Editing `correction` improves what the agent is told. Clearing `feedbackDemoted` (and setting `status` back to `blocking`/`reminding`) re-enforces a gate the agent's behavior retired — it gets a fresh grace window via `feedbackBaseline`.
|
|
107
115
|
|
|
108
116
|
## Development
|
|
109
117
|
|
|
110
118
|
```bash
|
|
111
119
|
bun install
|
|
112
|
-
bun run typecheck # tsc --noEmit (index.ts
|
|
120
|
+
bun run typecheck # tsc --noEmit (index.ts, src/**, scripts/**, test/**)
|
|
113
121
|
bun test/smoke.ts # behavioral smoke test, no framework needed
|
|
114
122
|
```
|
|
115
123
|
|
|
116
|
-
Tunables are named constants at the top of `index.ts` and `src/store.ts`: `PROMOTE_COUNT` (3), `PROMOTE_COUNT_PROBE` (5), `PROMOTE_SESSIONS` (2), `GLOBAL_PROJECTS` (2), `TTL_DAYS` (60), `NOISE_TTL_DAYS` (7), `REVIEW_FIRES` (10), `MAX_GATES` (2000), `HEAL_SUCCESSES` (3).
|
|
124
|
+
Tunables are named constants at the top of `index.ts` and `src/store.ts`: `PROMOTE_COUNT` (3), `PROMOTE_COUNT_PROBE` (5), `PROMOTE_SESSIONS` (2), `GLOBAL_PROJECTS` (2), `TTL_DAYS` (60), `NOISE_TTL_DAYS` (7), `REVIEW_FIRES` (10), `MAX_GATES` (2000), `HEAL_SUCCESSES` (3), `DEMOTE_RECURRENCES` (3), `DEMOTE_REOFFENSE_SESSIONS` (2), `DEMOTE_OVERRIDES` (3), `TAUGHT_REMINDERS` (5), `ORPHAN_CANDIDATE_DAYS` (7).
|
|
117
125
|
|
|
118
126
|
## Roadmap
|
|
119
127
|
|
|
120
128
|
- v2: recurrence-after-gate reporting command; V2 plugin API error hooks — `tool.execute.error` is drafted upstream (opencode issue #27900) but unmerged; the event-stream scan remains the file-tool failure channel until it lands
|
|
121
129
|
- v3: auto-proposal of ast-grep rules for statically detectable patterns (repo-level CI gates)
|
|
122
130
|
|
|
131
|
+
## Disclaimer
|
|
132
|
+
|
|
133
|
+
dejavu is a community project. It is not built by the OpenCode team and is not affiliated with them in any way.
|
|
134
|
+
|
|
123
135
|
## License
|
|
124
136
|
|
|
125
137
|
MIT
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: dejavu status report — pathologies first, then active gates, recurrence metrics, review flags
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
First run the pathology report (it surfaces every known defect class in one pass):
|
|
6
|
+
|
|
7
|
+
bun ~/.config/opencode/vendor/dejavu/scripts/doctor.ts
|
|
8
|
+
|
|
9
|
+
No arguments needed: doctor discovers every project store from the global index.
|
|
10
|
+
For npm installs the script lives in the plugin package instead (e.g. `node_modules/opencode-dejavu/scripts/doctor.ts`); if dejavu was cloned elsewhere, use that checkout's `scripts/doctor.ts`.
|
|
11
|
+
Add `--repair` to heal first (idempotent): quarantines corrupt files, merges duplicates, excises broken log lines, reconciles the index, prunes true-orphan index keys (safe in doctor — it sees every scope), applies feedback-demotion catch-up.
|
|
12
|
+
|
|
13
|
+
Then read the state files for detail:
|
|
14
|
+
|
|
15
|
+
- Project gates: `.opencode/dejavu/gates.json` in the current directory (may not exist yet)
|
|
16
|
+
- Global gates: `~/.config/opencode/dejavu/gates.json` (plus `index.json` — cross-project evidence per key, machine-managed)
|
|
17
|
+
- Event logs: `log.jsonl` next to each gates.json (last ~30 lines; events carry `channel`, `via`, `exit`, `version` fields for forensics)
|
|
18
|
+
|
|
19
|
+
Report structure:
|
|
20
|
+
|
|
21
|
+
1. **Pathologies** — whatever doctor printed (stale blocking/reminding gates, not-teaching gates, annoying gates, review-flagged, reminders-ignored, feedback-demoted, unsanitized data, version drift). For each, propose the minimal action (migrate / write correction / delete / restart OpenCode) but do NOT edit anything without explicit confirmation.
|
|
22
|
+
2. **Active gates** — table: signature, count, sessions, remindedCount, blockedCount, overrideCount, recurredAfterGate, correction (if set)
|
|
23
|
+
3. **Health** — any gate with `recurredAfterGate > 0` is NOT stopping its error: quote its evidence (last snippet) and propose a one-line `correction` text for it. Gates with `feedbackDemoted: true` surrendered to agent behavior — re-enforcing is a human decision (set `status` back to `blocking`/`reminding` AND clear `feedbackDemoted`).
|
|
24
|
+
4. **Near promotion** — top-5 `watching` bash patterns by count (candidates for future gates)
|
|
25
|
+
|
|
26
|
+
Keep the report under 40 lines. Do not modify any files.
|