opencode-dejavu 2.6.0 → 2.7.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.7.0 — 2026-08-26
4
+
5
+ ### Added (no more manual corrections)
6
+ - **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.
7
+ - **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.
8
+
3
9
  ## 2.6.0 — 2026-08-26
4
10
 
5
11
  ### Added (only well-grounded triggers)
package/README.md CHANGED
@@ -73,6 +73,7 @@ Restart OpenCode. Gates appear automatically as failures recur — nothing to co
73
73
  - **Migration** — gates outside the blocking policy are demoted to `watching` automatically; project copies of already-global gates are merged into the global gate (evidence is consolidated, never deleted).
74
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-scrubbed, stale blocking demoted), unparseable log lines are excised to `log.jsonl.corrupt`, and the cross-project index is reconciled. Every repair is logged as a `repaired`/`quarantined` event.
75
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.
76
+ - **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
77
 
77
78
  ## Observability (debugging aids)
78
79
 
package/index.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  bashSegmentSignatures,
6
6
  callSignature,
7
7
  detectFailure,
8
+ failureSnippet,
8
9
  isIntendedNonzero,
9
10
  isNoiseError,
10
11
  parameterizeError,
@@ -268,7 +269,7 @@ export const Dejavu: Plugin = async ({ directory, client }) => {
268
269
  return
269
270
  }
270
271
 
271
- const snippet = scrubSecrets(detection.matched ? detection.snippet : `exit code ${exitCode}`)
272
+ const snippet = scrubSecrets(detection.matched ? detection.snippet : failureSnippet(text, exitCode))
272
273
 
273
274
  const result = await stores.recordFailure({
274
275
  key,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-dejavu",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "Cross-session memory prosthesis for OpenCode: detects recurring tool-call failures and promotes them into enforced gates. Remind first, block on same-session repeat offense.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/src/patterns.ts CHANGED
@@ -460,6 +460,22 @@ export function detectFailure(outputText: string): FailureDetection {
460
460
  return { matched: false, snippet: "" }
461
461
  }
462
462
 
463
+ /**
464
+ * For exit-code failures whose output matched no signature, a bare
465
+ * "exit code N" gives a human/agent nothing to write a correction from.
466
+ * Bash output is command output (safe to surface), so keep the last non-empty
467
+ * line — compilers/test runners print their summary at the end.
468
+ */
469
+ export function failureSnippet(outputText: string, exitCode: number | null): string {
470
+ const lines = outputText
471
+ .split("\n")
472
+ .map((l) => l.trim())
473
+ .filter((l) => l !== "")
474
+ const tail = lines[lines.length - 1]
475
+ if (tail !== undefined && tail !== "") return tail.slice(0, 200)
476
+ return `exit code ${exitCode}`
477
+ }
478
+
463
479
  // --- Noise filtering ----------------------------------------------------------
464
480
 
465
481
  /**
@@ -478,3 +494,33 @@ const NOISE_ERRORS: RegExp[] = [
478
494
  export function isNoiseError(errorText: string): boolean {
479
495
  return NOISE_ERRORS.some((rule) => rule.test(errorText))
480
496
  }
497
+
498
+ // --- Default corrections ------------------------------------------------------
499
+
500
+ /**
501
+ * Mechanical, overridable default correction chosen by command family, so a
502
+ * promoted gate always ships with SOME teaching text instead of sitting
503
+ * "NOT TEACHING" until a human writes one. Rules, not an LLM — the hot path
504
+ * stays mechanical; a human/agent may refine the text later.
505
+ */
506
+ export function suggestCorrection(signature: string, snippet: string): string {
507
+ if (/(^|\s)(--check|--dry-run|verify|check)\b/i.test(signature) && /dart run|generate|sync/i.test(signature)) {
508
+ return "Generated artifacts are stale — run the same script WITHOUT the check flag to regenerate, then commit the result."
509
+ }
510
+ if (/\b(pytest|jest|vitest|mocha|cucumbertest|flutter test|npm test|gradlew\b[^\n]*test|dart test)\b/i.test(signature)) {
511
+ return "A test is failing — read the failing assertion in the output and fix the code or the expectation; do not re-run the suite blindly."
512
+ }
513
+ if (/\b(tsc|typecheck|type-check)\b/i.test(signature)) {
514
+ return "Type errors — run the compiler, read the reported file:line diagnostics, and fix the types before retrying."
515
+ }
516
+ if (/\b(curl|wget)\b/i.test(signature)) {
517
+ return "Network/endpoint failure — verify the URL is reachable, check rate limits and timeouts; retry with backoff, not immediately."
518
+ }
519
+ if (/\b(npm|yarn|pnpm|bun)\s+(install|ci)\b/i.test(signature)) {
520
+ return "Dependency install failed — inspect the resolver error; try the lockfile/legacy-peer-deps route the repo documents."
521
+ }
522
+ if (snippet !== "" && snippet !== "exit code 1") {
523
+ return `Last error: "${snippet}" — address that specific error before retrying this exact call.`
524
+ }
525
+ return "This exact call keeps failing — inspect the last output line and change approach before retrying."
526
+ }
package/src/store.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { appendFile, mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises"
2
2
  import { dirname, join } from "node:path"
3
- import { canBlock, canRemind, fuzzySimilar, FUZZY_MAX_LEN, isRepoLocal, scrubSecrets } from "./patterns"
3
+ import { canBlock, canRemind, fuzzySimilar, FUZZY_MAX_LEN, isRepoLocal, scrubSecrets, suggestCorrection } from "./patterns"
4
4
  import { coerceGateShape, repairGate } from "./validate"
5
5
 
6
6
  /** Bumped on behavior changes; stamped into init log events so stale sessions are visible. */
7
- export const PLUGIN_VERSION = "2.6.0"
7
+ export const PLUGIN_VERSION = "2.7.0"
8
8
 
9
9
  export interface Gate {
10
10
  /** sha1 signature prefix — the pattern identity */
@@ -727,6 +727,12 @@ export class Stores {
727
727
  changed = true
728
728
  }
729
729
  }
730
+ // Backfill: an enforced gate with no correction gets a mechanical
731
+ // default so it teaches immediately instead of sitting "NOT TEACHING".
732
+ if (gate.status !== "watching" && gate.correction === undefined) {
733
+ gate.correction = suggestCorrection(gate.signature, gate.snippet)
734
+ changed = true
735
+ }
730
736
  }
731
737
  if (changed) await store.save()
732
738
  })
@@ -955,6 +961,11 @@ export class Stores {
955
961
  gate.status = "reminding"
956
962
  promoted = true
957
963
  }
964
+ // A promoted gate always ships with SOME teaching text (mechanical
965
+ // default, overridable) so it never sits "NOT TEACHING" awaiting a human.
966
+ if (promoted && gate.correction === undefined) {
967
+ gate.correction = suggestCorrection(gate.signature, gate.snippet)
968
+ }
958
969
  }
959
970
 
960
971
  await store.save()