1598 Commits

Author SHA1 Message Date
Dragan Spiridonov 50936bebbe Merge pull request #518 from proffesor-for-testing/fix/rvf-project-root-resolution-vitest
fix(rvf): resolve project root via static import, not require() (#516 follow-up)
v3.10.4
2026-06-08 18:11:13 +02:00
Dragan Spiridonov dc0ac612e2 fix(rvf): resolve project root via static import, not require() (#516 follow-up)
The v3.10.4 publish failed: rvf-pattern-store.test.ts expected RvfPatternStore
but got PatternStore. createPatternStore's #516 change used
`require('../kernel/unified-memory.js')` to get findProjectRoot; esbuild inlines
that in the shipped bundle, but under vitest (the CI publish gate) it throws
"Cannot find module", so the whole RVF block fell through to the plain store —
i.e. RVF pattern store was silently disabled under the test runner.

Fix: extract findProjectRoot/clearProjectRootCache into a lightweight,
sqlite-free module (src/kernel/project-root.ts) and import it STATICALLY from
the RVF hot paths. unified-memory.ts now imports + re-exports them, so existing
`import { findProjectRoot } from '.../unified-memory.js'` sites are unchanged.
A static import avoids both the vitest require-resolution failure and dragging
unified-memory's eager better-sqlite3 load into these modules.

- src/kernel/project-root.ts: new module (fs/path only)
- src/kernel/unified-memory.ts: import + re-export (internal callers keep a binding)
- src/learning/pattern-store.ts: static import; drop require()
- src/integrations/ruvector/shared-rvf-adapter.ts: same (latent same bug)
- rvf-pattern-store.test.ts: pin AQE_PROJECT_ROOT to a hermetic temp root with
  .agentic-qe so the RVF branch is taken deterministically (was relying on a
  prior test creating ./.agentic-qe in cwd)

Verified locally (native RVF available): rvf-pattern-store 16/16,
find-project-root 4/4, 181 non-sqlite ruvector tests pass, typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:10:16 +00:00
Dragan Spiridonov 584fb7efab Merge pull request #517 from proffesor-for-testing/fix/issues-513-516-and-local-dev-env
chore(release): v3.10.4
2026-06-08 17:48:26 +02:00
Dragan Spiridonov d4fba4b9fc chore(release): bump version to v3.10.4
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:49:13 +00:00
Dragan Spiridonov 07873d8c6b fix(mcp): exit orphaned stdio server on stdin EOF instead of busy-looping (#513)
When the parent (e.g. a Claude Code session) exited, aqe-mcp was reparented
to PID 1 and busy-looped at 70-98% CPU, ignoring SIGTERM (only kill -9 worked).

Root cause: on stdin EOF the stdio transport fired its ERROR handler, which
the protocol server routes to its reconnect path. Reconnect re-attached
readline to the already-ended stdin, immediately re-emitted 'close', and
span close → error → reconnect → close at high CPU. The async SIGTERM
handler, starved by the spin and able to hang on an unresolved await, never
reached process.exit — hence "SIGTERM ignored". Non-unref'd worker/daemon
timers meant nothing made the orphan exit on its own either.

Fix (defense-in-depth):
- stdio transport: stdin EOF is now TERMINAL — fire a new onClose handler,
  never the error handler, so the reconnect spin can't start. reconnect()
  is also a no-op on an ended stream.
- entry.ts: single idempotent shutdown() shared by SIGINT/SIGTERM and stdin
  EOF, with an unref()'d 3s watchdog guaranteeing exit even if a graceful
  step hangs. A non-TTY stdin 'end'/'close' now triggers shutdown so an
  orphan exits when its parent dies.
- unref() the daemon health-check, worker tick, quality-daemon tick, and
  git-watcher poll timers so they can't keep an orphaned event loop alive.

Verified: real spawned aqe-mcp now exits ~31ms after stdin EOF (code 0),
vs. lingering indefinitely before. Adds stdio-eof.test.ts covering the
terminal-close behavior and no-reconnect-on-ended-stream guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:00:28 +00:00
Dragan Spiridonov 83e8f762a6 fix(memory): resolve project learning to its own .agentic-qe/ (#516)
Two root-resolution defects caused a project's learning to be written
outside its own .agentic-qe/.

Defect 1 — findProjectRoot() preferred the TOPMOST .agentic-qe, so an
ancestor store (e.g. ~/.agentic-qe, created by any `aqe` run from $HOME)
hijacked every descendant project. Now prefers the NEAREST .agentic-qe,
mirroring the existing .git nearest-wins logic in the same walk.

Defect 2 — the RVF substrate hardcoded a cwd-relative '.agentic-qe',
ignoring findProjectRoot(). A subfolder cwd (vendored builds, background
workers, `aqe code index docs/`) scattered stray patterns.rvf/brain.rvf
stores. All three sites now resolve via AQE_PROJECT_ROOT ?? findProjectRoot():
- shared-rvf-adapter.ts (relative dataDir anchored to project root;
  explicit absolute args still honored)
- shared-rvf-dual-writer.ts (brain.rvf)
- pattern-store.ts (patterns.rvf + existence gate)

Adds find-project-root.test.ts: nearest-wins over ancestor store, env
override precedence, .git fallback, and .agentic-qe over ancestor .git.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:48:02 +00:00
Dragan Spiridonov dc5dc14ebd feat(learning): use local Nagual instance + add local LLM judge
Switch the cross-project knowledge hub from the cloud endpoint to a
locally-runnable Nagual instance, and add an optional local LLM judge
for scoring pattern quality.

- nagual-client.ts: REST client for a local `nagual serve` instance
  (NAGUAL_URL, default http://localhost:3333); search/recordOutcome/
  promotePattern are all fire-and-forget safe.
- local-judge-client.ts: Ollama-compatible judge (NAGUAL_JUDGE_URL/MODEL)
  blending 0.7*outcome + 0.3*judge_score, with an ambiguous-zone review flag.
- task-hooks.ts: pre-task surfaces Nagual cross-project context for
  complex tasks in relevant QE domains (best-effort, gated on env + length).
- settings.json: add NAGUAL_JUDGE_URL/MODEL; fix invalid mcp allow-rule
  globs (mcp__ruflo__* / mcp__claude-flow__*) and add Bash(rm -rf:*) deny.
- docs: de-reference the cloud hub in favor of a local instance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:44:22 +00:00
Dragan Spiridonov 6ddb4b75be Merge pull request #515 from proffesor-for-testing/dependabot/npm_and_yarn/npm_and_yarn-0a9c170602
chore(deps-dev): bump hono from 4.12.18 to 4.12.23 in the npm_and_yarn group across 1 directory
2026-06-05 08:14:14 +02:00
dependabot[bot] 812a5a6a83 chore(deps-dev): bump hono in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [hono](https://github.com/honojs/hono).


Updates `hono` from 4.12.18 to 4.12.23
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.18...v4.12.23)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.23
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 20:51:33 +00:00
Dragan Spiridonov c8e6f3bc2c test(init-gate): recalibrate self-dogfood snapshot for working TS code-intelligence
The #511 fix makes init's code-intelligence pre-scan actually parse AQE's own
TypeScript source (tree-sitter, no typescript dep) and persist the graph, so the
self-dogfood init now produces ~35k KG entries (was ~27k) and takes ~182s (was
~60s). Bumped expectedKgEntries 27000->34000, expectedElapsedSec 60->80 (soft cap
240s), timeoutSec 240->300. Follow-up: batch KG persistence writes to cut init time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v3.10.3
2026-06-03 13:59:29 +00:00
Dragan Spiridonov 7fb48160f3 Merge pull request #514 from proffesor-for-testing/fix/issue-511-code-deps-persistence
chore(release): v3.10.3
2026-06-03 15:44:25 +02:00
Dragan Spiridonov 7790a8c001 fix(deps): bump eslint-plugin-unused-imports to v4 (supports @typescript-eslint 8)
CI npm ci failed with ERESOLVE — v3.2.0 peers @typescript-eslint/eslint-plugin
6-7, but the project is on v8. v4.x peers ^8 (and keeps ESLint 8 support), so it
resolves cleanly. Worked locally only because it was installed with
--legacy-peer-deps; npm ci is strict.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:22:35 +00:00
Dragan Spiridonov 0f09a06363 chore(release): bump version to v3.10.3
Code-intelligence TS/JS via bundled tree-sitter WASM (no typescript dep, #511) +
fix for MCP domain tools hanging on fresh install. Also fixes the release skill's
repo path (agentic-qe-new -> agentic-qe).
2026-06-03 13:16:48 +00:00
Dragan Spiridonov b9df2f9a58 fix(parsers): rebuild python/java/c#/rust/swift WASM grammars for web-tree-sitter 0.26
The committed grammars for these 5 languages were built with the pre-0.25
emscripten "side module" format and fail to load with web-tree-sitter 0.26
(getDylinkMetadata error) — the multi-language parser was silently falling back
to regex for ALL of them. Rebuilt with tree-sitter-cli 0.26.9 (wasi-sdk) via
scripts/build-grammars.sh.

Verified: the WASM parser test suite now loads every grammar with ZERO
"Failed to load WASM grammar" fallbacks (was one per parse); 117 parser tests pass.

Also: build-grammars.sh installs grammar source packages with --legacy-peer-deps
(they declare conflicting tree-sitter binding peers, irrelevant since we only
consume their C source) so `build-grammars.sh all` resolves cleanly.
2026-06-03 13:01:43 +00:00
Dragan Spiridonov c3173a88ad feat(code-intelligence): parse TS/JS via bundled tree-sitter WASM — no typescript dependency (#511)
Code-intelligence (index/deps/impact/search) required the multi-megabyte
`typescript` compiler at runtime; without it every TS/JS file errored
("TypeScript is required for code analysis") and produced 0 nodes/edges — i.e.
the feature was broken out of the box for the entire JS/TS ecosystem.

Bundle tree-sitter WASM grammars for typescript/tsx/javascript and extract
entities/imports from them instead:

- assets/grammars/tree-sitter-{typescript,tsx,javascript}.wasm (shipped via
  package `files: assets/**`).
- treesitter-ts-extractor.ts: extracts functions (incl. async arrows), classes +
  methods (with visibility), interfaces, and imports (incl. require()).
- knowledge-graph.ts: TS/JS extraction now prefers tree-sitter (typescript-free),
  falling back to the TypeScript compiler API only if tree-sitter is unavailable
  AND `typescript` happens to be installed.
- tree-sitter-wasm-parser.ts: export loadWasmGrammar() reusing the proven WASM
  init + grammar-path resolution.

Built with tree-sitter-cli 0.26.9 (matching web-tree-sitter ~0.26) via the new
scripts/build-grammars.sh (docker + emscripten/emsdk; reproducible — byte
identical across runs). Grammars built with the pre-0.25 emscripten dylink format
do NOT load with web-tree-sitter 0.26 (getDylinkMetadata error), which is why the
build pins the toolchain.

Verified on a real fresh project with NO typescript installed: code index → 8
nodes / 7 edges, deps → 2 connecting edges, search → 3 results. 232
code-intelligence tests + new extractor unit tests pass.

Note: the pre-existing assets/grammars/*.wasm for python/java/c#/rust/swift were
built with the old toolchain and currently fail to load with web-tree-sitter
0.26 (silently falling back to regex). They can be rebuilt with
`scripts/build-grammars.sh all` — tracked as a follow-up.
2026-06-03 12:51:10 +00:00
Dragan Spiridonov c981d84f5d fix(mcp): bound advisory routing with a timeout so a stalled learning engine can't hang domain tools
Defense-in-depth after the QEReasoningBank init recursion. Pattern search + model
routing in createDomainHandler are optional hints, so they must never block a
tool call. Wrap routeDomainTask() in withTimeout (ROUTE_ADVISORY_TIMEOUT_MS,
default 5s, env-overridable): on timeout the tool proceeds with null routing
(mapToPayload already tolerates null). Also cache the learning engine only AFTER
a successful init, so a failed/slow init no longer serves a half-initialized
engine on the next call.

Adds unit tests for withTimeout (fallback on stall, real value when fast, fallback
on reject). MCP code_index still returns normally.
2026-06-03 12:10:20 +00:00
Dragan Spiridonov d6b19d087c fix(learning): stop QEReasoningBank.initialize() recursing → MCP domain tools hung on fresh install
Root cause: initialize() seeds pretrained patterns via loadPretrainedPatterns() →
storePattern(), and storePattern() ensure-inits with
`if (!this.initialized) await this.initialize()`. Because `this.initialized = true`
was set AFTER seeding, the seed re-entered initialize() without bound — an async
self-recursion that pinned a CPU core and never returned.

Impact: EVERY MCP domain tool (code_index, quality_assess, defect_predict, …)
routes through the advisory pattern-search path (createDomainHandler →
routeDomainTask → searchPatternsForTask → getLearningEngine → AQELearningEngine →
QEReasoningBank.initialize). On a fresh install (patterns seeded for the first
time) that path recursed forever, so all MCP domain tools hung indefinitely while
the CLI — which calls domain services directly — worked. The repo and in-memory
unit tests never hit it (already seeded / bulk-load path).

Fix: set `this.initialized = true` BEFORE loadPretrainedPatterns()/seeding. The
core stores (patternStore + SQLite) are already up by then, so storePattern()
short-circuits instead of recursing.

Verified against a real tarball install: MCP code_index ∞-hang → 0.5s
(6 symbols / 5 relations); quality_assess / defect_predict 0.8s; returns cleanly
(0/0/0) when typescript is absent too. Adds a red-green regression test pinning
the init-order invariant.
2026-06-03 12:04:37 +00:00
Dragan Spiridonov 80522aa460 chore(lint): clear remaining mechanical lint + document intentional violations
Real fixes:
- prefer-as-const (3): provider `type` literal fields use `as const`
- prefer-const (1): merge split let-declaration in work-stealing timer
- no-empty (2): annotate best-effort cleanup catch blocks
- no-unused-expressions (1): convert side-effecting ternary to if/else

Documented-intentional (eslint-disable-next-line with rationale):
- no-this-alias (4): self captured for returned handler context
- no-constant-condition (4): heap sift-down / polling / tokenizer loops break internally
- no-unused-expressions (1): chaos-engineer CPU busy-loop
- no-empty-object-type (1): marker config interface

Lint problems: 489 -> 472. Typecheck, build, and 2154 coordination/routing tests pass.

Remaining 472 (406 err + 66 warn) are judgment-heavy, intentionally left for a
reviewed pass: 327 dead locals/assigned vars (may be WIP/exports), 66 explicit-any
(typing work), 42 require-imports (some intentional dynamic), 37 useless-escape
(eslint declined to autofix — regex-semantics risk).
2026-06-03 09:39:02 +00:00
Dragan Spiridonov b53f0fd520 chore(lint): wrap case-clause declarations in blocks (no-case-declarations)
AST-positioned codemod (TypeScript compiler API): wrapped 46 case/default clauses
that declare const/let/function/class in `{ }` blocks across 20 files, clearing
all 60 no-case-declarations errors. Pure block-scoping — semantically identical.
Verified: typecheck, build, and 4319 unit/integration tests pass.

Lint problems: 549 -> 489.
2026-06-03 09:32:32 +00:00
Dragan Spiridonov 447ea1df24 chore(lint): document 4 intentional rule violations (control-regex, async-executor)
Inspected the genuine "bug-smell" lint hits — none are defects:
- postgres-writer NUL stripping (2x no-control-regex): required, PostgreSQL JSONB
  rejects the NUL code point.
- http-server / websocket-transport async Promise executors (2x): both already
  handle errors (server.on('error', reject) / try-catch -> reject). Refactoring
  critical startup/upgrade paths for the rule isn't worth the risk.
Added eslint-disable-next-line with rationale on each.
2026-06-03 09:26:42 +00:00
Dragan Spiridonov 94b026e2d6 chore(lint): auto-remove dead imports via eslint-plugin-unused-imports
Add eslint-plugin-unused-imports (devDependency) and switch unused-vars handling:
- unused-imports/no-unused-imports (error, autofixable) removes dead imports
- unused-imports/no-unused-vars (error, report-only) keeps dead locals visible
- @typescript-eslint/no-unused-vars disabled to avoid double-reporting

Ran the autofix across the tree: removed 478 unused imports in 251 files.
Lint problems: 1031 -> 553. No bare side-effect imports (`import 'x'`) were
removed. Verified: typecheck passes, build passes, 4507 unit/integration tests
pass. No behavioral change — purely dead-import removal.
2026-06-03 09:21:38 +00:00
Dragan Spiridonov bec5a57f1c chore(lint): ignore src/_archived, align bounded-context count, apply safe autofixes
Repo-health cleanup from an external code audit:

- eslint: ignore src/_archived (already excluded by tsconfig, so linting it only
  produced 6 parserOptions.project errors).
- Align the architecture claim: src/index.ts and docs said "12"/"6" bounded
  contexts; there are 13 domain dirs (matching package.json). Updated src/index.ts
  (comment + ARCHITECTURE const) and the docs list to the actual 13.
- Apply stock `eslint --fix` (prefer-const, prefer-as-const, no-useless-escape,
  no-regex-spaces) across 24 files.

Lint problems: 1072 -> 1031. Typecheck and build pass; no behavioral changes.
2026-06-03 09:13:16 +00:00
Dragan Spiridonov cc5220c5a0 fix(code-intelligence): pin KG to a fixed namespace + evict stale file cache (#511)
Resolves the two remaining gaps from the audit.

Namespace divergence (CLI↔MCP parity):
KnowledgeGraphService persisted nodes/edges under the backend's DEFAULT
namespace — 'qe-kernel' for the CLI kernel, 'mcp-tools' for the MCP shared
backend — so a graph indexed by one tool was invisible to the other, and the
init existence check (`SELECT ... FROM kv_store WHERE namespace =
'code-intelligence:kg'`) always returned 0 because the data was elsewhere.

Now every KG kv op passes an explicit `{ namespace: 'code-intelligence:kg' }`
(via a single `nsOpts` getter so reads and writes can't drift apart — issue
#491 Bug 2). The two external readers that searched the default namespace by key
prefix (impact-analyzer test discovery, test-generator KG-context) now read that
namespace too. Verified: both `aqe code index` and the MCP `code_index` tool
land KG rows in `code-intelligence:kg`; deps/search still work; the init
`hasCodeIntelligenceIndex` check now finds the index.

FileReader stale content on incremental re-index:
indexFile() now evicts the file from the FileReader content cache before reading
it. The reader caches content for 5 min, so a re-index in a long-lived process
(the MCP server) could otherwise extract from pre-edit text. The two reads
within indexFile (entities + imports) still share one cache fill, so they stay
consistent. The incremental regression test now re-indexes on the SAME instance,
exercising both removeFileFromGraph and the cache eviction.

Adds a regression test asserting KG data is stored under 'code-intelligence:kg'
regardless of the backend's default namespace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 08:59:47 +00:00
Dragan Spiridonov 26e724b24e fix(code-intelligence): warn on unsupported langs, prune incremental stale edges, fix MCP code_index counts (#511)
Addresses the gaps a devil's-advocate audit surfaced in the #511 fixes:

1. Fail-loud warning (issue request #2): `index` now records how many files
   were indexed under a language with no dependency extractor and returns them
   as `IndexResult.warnings` (logged + printed by the CLI), so a downstream
   0-edge `deps` result is explained rather than silently misleading.

2. Incremental stale state: `indexFile(..., incremental)` now calls
   removeFileFromGraph() to delete a file's prior node/entity/outgoing-edge
   keys before re-indexing, so `index --incremental` prunes removed imports and
   renamed entities instead of leaving phantom dependencies. (Note: within a
   single long-lived process the FileReader content cache can still serve stale
   text; the CLI re-indexes in a fresh process so this is not user-visible.)

3. MCP parity: the `index-code` task handler emitted `nodesCreated`/`edgesCreated`
   but the MCP result mapper reads `symbolsExtracted`/`relationsFound`, so
   `code_index` always reported 0/0. Map the KG counts to those fields and pass
   `warnings` through. Verified via a real MCP protocol call: filesIndexed 2,
   symbolsExtracted 5, relationsFound 4 (was 0/0).

Adds regression tests: unsupported-language warning, incremental stale-edge
removal (fresh-instance, mirroring the CLI), and the MCP field mapping.

Known limitation (documented, not changed here): CLI persists the graph under
the 'qe-kernel' namespace and MCP under 'mcp-tools', so a graph indexed by one
tool is not read by the other. MCP exposes only `code_index` (no deps/search),
so this has no MCP-internal reader; reconciling the namespaces is entangled with
contradictory init/test-generation expectations and needs its own change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 08:47:57 +00:00
Dragan Spiridonov d7b82ff6e2 fix(code-intelligence): make aqe code search return results — content namespace round-trip (#511)
indexCode() stored retrievable content with a {namespace} store option, but
the key is already fully namespace-qualified and every read path
(semanticSearch/findSimilar/exact/fuzzy) plus the storeVector() embedding write
use the default namespace. On a real backend that honors the option, the
content written with {namespace} could not be read back without it (issue #491
Bug 2), so each matched vector's content fetch returned undefined and the result
was silently dropped — search returned 0 results for obviously-present terms.

Fix: drop the redundant {namespace} store option on the content write so it
round-trips with the reads.

Verified via CLI: `code search "calculator compute"` now returns the indexed
files (was 0). Adds a regression test using a namespace-honoring mock backend
(the existing mock ignored the option, which is why it never caught this); the
test fails with the option reintroduced and passes without it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 08:08:36 +00:00
Dragan Spiridonov 8a0217e9aa fix(code-intelligence): persist KG so aqe code deps returns edges on TS/JS repos (#511)
`aqe code index` and `aqe code deps`/`search` run as separate CLI processes,
but the knowledge graph stored nodes/edges in-memory only ("cache-only"), so a
fresh `deps` process always saw an empty graph and reported 0 edges / degree-0
nodes — even though `index` reported edges created. This affected every
language; TS was incidental (the issue's "missing TS parser" theory is a red
herring — the TS Compiler API path is wired and does extract).

Fixes:
- Persist nodes/edges to memory.db in storeNode/createEdge; getEdges hydrates a
  correct flat edge cache (the old source-keyed index could not serve
  incoming/both lookups); clear() deletes persisted KG :node:/:edge: keys only.
- resolveImportPath(): resolve relative import specifiers to real files with
  extension probing so file->file import edges connect (were dangling at
  pathToNodeId('./math')).
- traverseDependencies: restrict to true dependency edge types
  (import/call/extends/implements) and recurse directed-forward only, which
  removes phantom A->B->A cycles surfaced once edges actually existed.

Verified via CLI: index -> 6 edges; fresh-process deps -> 2 connecting edges,
0 cycles (was 0 edges). Adds cross-instance regression tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 08:01:03 +00:00
Dragan Spiridonov b46b1cf855 Merge pull request #512 from proffesor-for-testing/working-april
chore(release): v3.10.2
v3.10.2
2026-06-02 16:02:10 +02:00
Dragan Spiridonov f96159683f feat(init): ship the hook shim and wire aqe init to install + use it
Per request: aqe init now installs the cross-platform Node hook shim into each
user project and routes hooks through it (instead of `npx agentic-qe hooks`).

- package.json: add `.claude/hooks` to `files` so aqe-hook.cjs ships in the npm
  tarball (verified via `npm pack --dry-run`).
- 07-hooks.ts: new installHookShim() copies aqe-hook.cjs into the project's
  .claude/hooks/ (source: node_modules/agentic-qe, then the package's own copy
  resolved relative to the compiled module — works for npx too).
- 07-hooks.ts + init-wizard-hooks.ts: generated hooks now run
  `node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" <cmd> ...`
  (README/help examples left as `npx agentic-qe hooks` for manual use).
- aqe-hook.cjs: added an npx fallback (`npx -y --prefer-offline agentic-qe`)
  so npx-only installs with no project-local bundle still work; disable with
  AQE_HOOK_NPX=0 (tests use this for determinism). Header/scope updated (now a
  shipped, init-copied file, not repo-internal).

Migration: isAqeHookEntry already matches the new `aqe-hook.cjs` command (via
the `\baqe\b` pattern) AND the old `npx agentic-qe hooks` form, so re-running
`aqe init` replaces a user's existing hooks with the shim form — no duplicates.

Verified end-to-end (real `aqe init --auto` in a temp project):
  - aqe-hook.cjs copied into .claude/hooks/ ("Installed aqe-hook.cjs (copied)")
  - all 11 generated hooks use `node .../aqe-hook.cjs <cmd>` (0 npx commands)
  - post-task retains the #508 fix (--agent/--success true/--description)
  - isAqeHookEntry matches new+old forms, not unrelated user hooks
  - shim 7/7, settings-merge 17/17, task-hooks 5/5 tests pass; typecheck clean
2026-06-02 13:45:19 +00:00
Dragan Spiridonov 1d0cbd626e refactor(hooks): rewrite hook shim as cross-platform Node (.cjs), drop POSIX .sh
A .sh shim is the least portable option — it forces a .cjs Windows twin and a
node_modules/.bin shell-wrapper spawn. Node is already a hard dependency of AQE
(CLI is Node >=20), so a single .cjs runs identically on Windows/macOS/Linux.

aqe-hook.cjs resolves a project-local bundle and runs it via process.execPath
(this node) — no shell, no .bin/.cmd wrapper:
  1. node_modules/agentic-qe/dist/cli/bundle.js  (installed dep — user case)
  2. dist/cli/bundle.js                            (this source repo)
  else no-op (exit 0). Swallows stderr; emits only the top-level JSON object.

Repo .claude/settings.json rewired: all 10 bundle hooks now invoke
. Deleted
aqe-hook.sh. Tests rewritten to run the .cjs via node; 7/7 pass incl. installed-
dep precedence + stderr-swallow. This removes the Windows blocker that stood
against ever shipping the shim to users.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:36:16 +00:00
Dragan Spiridonov e81b56d411 fix(hooks): make aqe-hook.sh portable (resolve project-local AQE, not hardcoded dist)
The shim hardcoded ${PROJECT}/dist/cli/bundle.js, which only exists in this
source repo — if ever copied into an installed project it would silently no-op.
Now resolves a PROJECT-LOCAL runner in priority order:
  1. node_modules/.bin/aqe   (installed project — the user case)
  2. dist/cli/bundle.js       (this source repo)
  else no-op (exit 0, never block a turn)
No global-PATH / npx fallback by design (version drift + npx-per-fire crash).

Clarified in the header that the shim is REPO-INTERNAL: it is not in the npm
`files` list and `aqe init` does not copy it — user hooks are generated as
`npx agentic-qe hooks <cmd>` (cross-platform, npx-friendly). Portability here is
defense-in-depth, not a shipping decision.

Adds a regression test for branch-1 resolution + precedence (node_modules/.bin/aqe
preferred over dist). 6/6 shim tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:26:14 +00:00
Dragan Spiridonov e84a911d1f chore(release): bump version to v3.10.2
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 12:27:08 +00:00
Dragan Spiridonov c61f19ab8b fix(hooks): dogfood the #508 fix in the repo's own .claude/settings.json
The repo's own PostToolUse post-task hook still carried the broken
'--success --json' (no --agent) form (the #5 shim rewired it but kept the
args). The parser fix makes bare --success parse true, but it still recorded
agent='unknown'. Updated post-task to pass --agent/--success true/--description
and post-command to explicit --success true, matching the corrected init
templates. (post-edit's bare --success is correct — that option is a flag.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 11:38:27 +00:00
Dragan Spiridonov 2ecc377241 fix(dream): kernel DreamScheduler reconciles dream-scheduler:hook-state per cycle (#509)
Reported by @ciprianmelian. ADR-094 moved dream cycles to the kernel-side
DreamScheduler, but the dream-scheduler:hook-state kv row that SessionStart
reads is only INCREMENTED by the post-task hook (experienceCount++) and was
never reset by the entity that actually dreams. Confirmed in source: the kernel
scheduler had ZERO references to that key (it persists its own
'dream-scheduler:state' instead). Result: SessionStart perpetually showed
lastDreamTime:null and a pendingExperiences counter that only grew (e.g.
11 -> 1747 -> 6489), reading like a stuck scheduler while the daemon dreamt fine.

Fix: after a successful executeDream cycle, reconcileHookState() stamps the
truth on the SAME key + DEFAULT namespace the hook path uses:
{ lastDreamTime: now, experienceCount: 0, totalDreamsThisSession: prev+1,
  sessionStartTime: preserved }. Best-effort (never breaks a dream cycle). Key
is a local literal mirroring DREAM_STATE_KEY to avoid a learning->cli layering
import.

Verified: new unit test seeds the stale row (experienceCount: 2720,
lastDreamTime: null), triggers a dream, asserts the row becomes
experienceCount:0, lastDreamTime: <iso string>, totalDreamsThisSession:1,
sessionStartTime preserved. 39 dream-scheduler tests pass; typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 11:34:40 +00:00
Dragan Spiridonov 201f5e8a95 fix(hooks): post-task --success eats --json -> every trajectory success=0/agent=unknown (#508)
Reported by @ciprianmelian. The aqe-init-generated PostToolUse hook ran
`post-task --task-id ... --success --json`, but --success was declared
`<bool>` (value REQUIRED), so Commander consumed --json as the success value:
options.success='--json' -> parsed false -> EVERY qe_trajectories /
captured_experiences row got success=0, the --json branch was swallowed, and no
--agent meant agent='unknown'. Net: Task trajectories were unpromotable
(distillation groups by agent/domain/success), so pattern count stayed pinned
near zero. (Confirmed in source; same class as the learning gaps #510 hardened.)

Fix (two layers):
1. Parser hardening — `--success <bool>` -> `--success [bool]` (optional value)
   in task-hooks/command-hooks/routing-hooks, so a bare --success yields boolean
   true and never eats the next token. Backward compatible: --success true/false
   and the 'true' default all still parse identically.
2. Generated templates (07-hooks.ts + init-wizard-hooks.ts): post-task now emits
   `--agent "$TOOL_INPUT_subagent_type" --success true --description
   "$TOOL_INPUT_prompt" --json` (real agent attribution + explicit success +
   pre-task bridge), and post-command emits explicit `--success true`.

Verified: isolated Commander proof — OLD <bool>: success="--json" (false);
NEW [bool]: success=true, --json fires. Plus 3 regression tests driving the EXACT
broken argv through registerTaskHooks: bare --success -> success=true, explicit
--success false -> false, --agent flows through. 5/5 task-hook tests pass
(incl. existing #449); typecheck clean. (Full post-task CLI run OOMs in Codespace
per CLAUDE.md — the parsing layer is the bug and is proven.)

NOTE for existing installs: re-run `aqe init` (or hand-edit the PostToolUse hook
in .claude/settings.json) to pick up the corrected command.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 11:30:59 +00:00
Dragan Spiridonov 84fb9ca65d docs(adr): ADR-098 learning-store & routing hardening + mark 2 side-findings fixed
ADR-098 documents the durable behavioural decisions from the ruflo adoption
pass (#510): domain-score de-dilution (#2), EWC++ importance-weighted forgetting
(#6), contradiction suppression (#7), the two pre-existing bug fixes
(consecutive_failures, MCP -32601), and the explicit NEGLECTED decisions —
ruflo's Beta-priors (#2) and RaBitQ speedup (#8) did not transfer, and
multi-bundle packaging is deferred. AQE WH(Y) format, Accepted.

Marks the consecutive_failures (ae7c8b71) and MCP error-code (9b189115)
side-findings FIXED in the plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 11:06:24 +00:00
Dragan Spiridonov 9b1891157b fix(mcp): return proper JSON-RPC error code for unknown tool/method (#510 side-finding)
Pre-existing bug surfaced by the qe-devils-advocate audit and live-reproduced:
handleToolsCall (and unknown-method / double-initialize) threw a PLAIN OBJECT
{ code: -32601, message }. The server safety net checked err instanceof Error
(false for a plain object) and surfaced it as a content payload
'Internal error: [object Object]', losing the -32601 code/message an MCP client
expects. The tool was correctly never executed; only the error shape was wrong.

Fix (matches existing OAuthProviderError convention): add McpError extends Error
{ code } + isJsonRpcCodedError guard in transport/stdio.ts (guard accepts both
McpError and the legacy plain {code,message} so nothing regresses). Convert the
three plain-object throws in protocol-server.ts to McpError; the safety net
re-throws coded errors (real protocol errors) while still swallowing genuinely
unexpected errors to keep the connection alive; the transport catch honors
err.code, falling back to INTERNAL_ERROR. Successful/known-tool calls unaffected.

Verified (real server boot via scripts/audit-mcp-tool-parity.mjs):
  before: unknown tool rejected with 'Internal error: [object Object]'
  after:  unknown tool rejected with 'JSON-RPC error code=-32601'
Parity PASS (86 tools), typecheck clean, real .agentic-qe/aqe.rvf unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 11:05:25 +00:00
Dragan Spiridonov ae7c8b7105 fix(learning): surface consecutive_failures so the 'failures' deprecation branch fires (#510 side-finding)
Pre-existing bug surfaced by the qe-devils-advocate audit and confirmed:
checkDeprecation() reads (pattern as PatternWithDeprecation).consecutive_failures
to trigger the 'failures' deprecation reason, but rowToPattern() built the
QEPattern WITHOUT that column (and without deprecated_at), so getPattern()
always surfaced undefined->0 and the 'failures' branch NEVER fired — a pattern
that kept failing was never deprecated for failures.

Fix (surgical): rowToPattern now surfaces consecutive_failures and deprecated_at
(added to the PatternRow interface; returns a typed PatternWithDeprecation). No
thresholds or deprecation logic changed.

Verified: new tests assert a pattern with consecutive_failures==3 (== default
threshold), not stale, confidence above min, deprecates with reason 'failures';
below threshold does not. Regression-proven (reverting rowToPattern fails the
new test). 12 EWC tests pass; typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 11:04:57 +00:00
Dragan Spiridonov 2969bb387c fix(test)+docs: address qe-devils-advocate audit of #510 loop
Audit (qe-devils-advocate) verdict: 10/10 items have real committed code + real
passing tests at claimed counts; production changes (#2/#6/#7) genuinely wired
into live paths; no real-store mutation; #8 honestly UNWIRED. Three fair
discrepancies, now corrected:

1. #2 had one TAUTOLOGICAL test (asserted (1/3)*0.4 > (1/11)*0.4 — arithmetic on
   literals, not a code call). Rewritten to score a matching vs non-matching
   agent through calculateAgentScores and assert the delta equals the CAPPED
   domain term (~0.1333), which fails if the cap is removed. 4/4 still pass.
2. #2 status row softened to VERIFIED (PARTIAL): +5.6pp shipped, but the
   structural Q-maturity / fixed-perf-term ceiling on confidence remains.
3. #10 status row corrected to PARTIAL: only the smoke contract was delivered;
   the original multi-bundle 'subset packaging' scope is deferred.

No production code changed by this commit. Side-findings re-confirmed real
(dead consecutive_failures branch, MCP [object Object] error, RVF/SQLite drift).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:54:19 +00:00
Dragan Spiridonov 89caafea16 docs: mark Ruflo adoption item 10 VERIFIED (98a6b19c) + log multi-bundle follow-up
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:45:39 +00:00
Dragan Spiridonov 98a6b19c88 feat(plugin): machine-checkable smoke contract for agentic-qe-fleet bundle (#510 item 10)
Ports ruflo's 'smoke.sh as a plugin contract' discipline. AQE already ships
the marketplace.json + a lean auto-discovery plugin (plugins/agentic-qe-fleet:
11 agents, 9 commands, 9 skills, lean plugin.json), but had no machine-checkable
contract — so silent drift could ship a broken bundle.

plugins/agentic-qe-fleet/scripts/smoke.sh (npm run plugin:smoke) asserts:
  - plugin.json valid JSON, name == agentic-qe-fleet, has a version;
  - plugin.json does NOT declare skills/commands/agents arrays (Claude Code
    auto-discovers them — declaring them is an error, per ruflo);
  - marketplace.json indexes the plugin at ./plugins/agentic-qe-fleet;
  - exact counts (11 agents / 9 commands / 9 skills), each with name+description
    frontmatter / SKILL.md (bump the constants deliberately on change);
  - README present; top-level plugin.json registers the agentic-qe MCP server.
Exit 0 = contract holds, 1 = any violation.

Verified: passes 10/10 checks (exit 0), and correctly FAILS (exit 1) when a
skill is removed ('skills: 8 (expected 9)').

Scope note: AQE ships ONE bundle today. Splitting the 93 QE skills into multiple
lean subset bundles (aqe-security, aqe-browser, ...) is a larger restructure of
shipped artifacts — deferred as a follow-up (needs a distribution decision); the
skills-manifest.json already has the category data to drive it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:45:10 +00:00
Dragan Spiridonov 31745ce919 docs: mark Ruflo adoption item 9 VERIFIED (3196c05e) + log embedding-latency finding
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:42:02 +00:00
Dragan Spiridonov 3196c05edd feat(learning): pretrain-from-history bootstrap (#510 item 9)
Ports ruflo ADR-077: seed the pattern store from the repo's OWN git history
(commits = success trajectories) through the SAME live-learning code path, so
a fresh project starts with a non-empty, project-specific pattern store instead
of only the generic PRETRAINED_PATTERNS.

scripts/pretrain-from-history.ts (npm run pretrain:history):
  - git log --no-merges -> per commit: domain = deriveTaskType(subject),
    success = subject !startsWith 'revert' (a revert is a failure signal);
  - records each via persistTaskOutcome (the live captured_experiences +
    experience_applications + Q-update path, honoring AQE_PROJECT_ROOT);
  - then consolidateExperiencesToPatterns() distills them into qe_patterns.
  - flags: --limit N (default 500), --dry-run (classify+histogram, write nothing),
    --verbose. Extracted a pure classifyCommit() helper (19 unit tests).

Correctness fix found during verification (in the script, not prod): a brand-new
store only has core schema, so persistTaskOutcome failed with
'no such table: experience_applications'. The script now calls the canonical
ensureAllBrainTables() + initializeExperienceCapture() before recording — the
same tables production creates lazily.

Verified against a THROWAWAY AQE_PROJECT_ROOT temp store (data protection):
  --limit 150 -> captured_experiences 0->150, qe_patterns 0->4, exit 0;
  --dry-run   -> nothing recorded (captured_experiences stays 0);
  real .agentic-qe/*.rvf byte-identical (sha256) before/after; 19 tests pass.

Side-finding: AQE's real embedding (ONNX) costs ~2.3s/commit here vs ruflo's
~3ms/trajectory, so bootstrapping 500 commits takes minutes — logged as a perf
follow-up (embedding backend / batching).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:41:35 +00:00
Dragan Spiridonov 406f3c6f37 docs: mark Ruflo adoption item 8 VERIFIED (0d80eb56) — RaBitQ utility, honest finding
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:30:33 +00:00
Dragan Spiridonov 0d80eb56ec feat(perf): RaBitQ 1-bit signature utility + benchmark (#510 item 8)
Ports ruflo's perf-M4 (sign-random-projection + Hamming popcount). Adds
src/shared/utils/rabitq.ts: signSignature (384-bit packed into 12 Uint32
words, 48 bytes), hammingDistance (SWAR popcount), hammingSimilarity, and
topKBySignature (Hamming prefilter gated at N>=100 + exact-cosine rerank of a
recall-aware pool, so output equals exact topK when the true top-k fall in the
pool). 16 tests + scripts/benchmark-rabitq.ts (npm run benchmark:rabitq).

HONEST FINDING (verified, does NOT match ruflo's optimistic 2.7x/10.9x):
  N=1000: speedup 1.31x, recall@10 100%, index 1.46MB -> 47KB (32x smaller)
  N=5000: speedup 1.18x, recall@10 99.9%, 7.32MB -> 234KB (32x smaller)
The 32x INDEX-MEMORY reduction is real and unconditional. The SPEEDUP is only
~1.2-1.5x in JS, not ruflo's figure: a 1-bit sign signature in 384-dim is
coarse, so preserving recall@10 >= 0.95 needs a large exact-rerank pool
(~40-55% of N), which eats most of the prefilter win. ruflo's big win came
from replacing a LINEAR scan; AQE's main search is already native HNSW
(sublinear), so that win doesn't transfer. Tests pin this tradeoff (recall==1.0
with a sufficient pool; small pool sacrifices recall) so it can't be
mis-sold later.

NOT INTEGRATED — no zero-regression consumer exists:
  - unified-memory-hnsw search() IS the ranking (no separate exact rerank to
    prefilter), so Hamming-prefiltering its beam would drop true neighbors;
  - coherence-gate-energy O(N^2) SUM and k-means clustering are numerically
    sensitive (Hamming would change results).
Committed as a tested, benchmarked, ready-to-use utility (real memory lever for
patterns.rvf and a future prefilter->exact-rerank consumer), not wired into any
production path. No production code or .agentic-qe stores touched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:30:10 +00:00
Dragan Spiridonov 378ee2795c docs: mark Ruflo adoption item 5 VERIFIED (1a695f56) + log hook-latency/.cjs follow-ups
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:51:29 +00:00
Dragan Spiridonov 1a695f5651 feat(hooks): resilient hook shim — clean transcript + never block a turn (#510 item 5)
Ports ruflo's hook-shim discipline. AQE's project hooks ran
`node dist/cli/bundle.js hooks <cmd> --json` directly, which printed init
diagnostics to STDOUT before the JSON on every SessionStart/UserPromptSubmit:
  [hooks] CoherenceService initialized with WASM engines
  [RvfPatternStore] Initialized: .agentic-qe/patterns.rvf (dim=384)
  [RVF] Migration adapter active at stage 2 (dual-sqlite)
  [hooks] System initialized
polluting the Claude Code transcript on every turn, and a crash/slow init could
surface a hook error mid-turn.

New .claude/hooks/aqe-hook.sh wraps every bundle hook with the ruflo contract:
  * prefer the LOCAL built bundle (no npx / network);
  * no-op if the bundle isn't built (never block a turn);
  * swallow stderr;
  * emit ONLY the top-level JSON object on stdout (sed '/^{/,/^}/p' drops both
    leading init noise AND async daemon log lines printed after the JSON, so the
    --json contract stays parseable);
  * ALWAYS exit 0.
All 10 bundle hook commands in .claude/settings.json rewired to the shim
(diff touches only those 10 command strings).

Verified with real commands: route/session-start/guard via the exact settings
invocation -> exit 0, 0 noise lines, valid JSON; missing-bundle and crashing-
bundle -> exit 0, empty stdout, no stderr leak. 5 regression tests (resilience +
deterministic noise-stripping via a fake bundle) pass in ~120ms.

Note: Windows .cjs parity (ruflo also ships one) is a separate follow-up — the
entire AQE hook config is POSIX `sh -c`, so a .cjs twin needs the settings
rewrite too. Observed side concern: the route/session-start hooks cold-start the
full system (~30-60s) on each fire — logged for a perf follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:50:59 +00:00
Dragan Spiridonov 62d8a451ec docs: mark Ruflo adoption item 4 VERIFIED (b1a462fb) + log MCP error-code side-finding
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:41:21 +00:00
Dragan Spiridonov b1a462fba2 feat(mcp): protocol-compliance smoke + tool-parity audit (#510 item 4)
Ports ruflo's #1874 discipline. Replaces the FAKE mcp:validate theater
(echo 'All tools registered' && exit 0) with a real smoke that boots the
actual MCP server (tsx src/mcp/entry.ts) over stdio and validates the
initialize + tools/list handshake against the MCP SPEC, not the code:
  - protocolVersion is a YYYY-MM-DD string (the core ruflo #1874 guard — an
    object here is what Claude Code's Zod rejects)
  - capabilities is an object; serverInfo.name/version are non-empty strings
  - tools/list returns a non-empty array, each entry name + object inputSchema

scripts/audit-mcp-tool-parity.mjs is the AQE-equivalent of ruflo's CLI<->MCP
parity audit (the CLI has no callMCPTool('name') string pattern, so the
meaningful surface is advertisement<->handler parity). Drives the advertised
set from a live tools/list, diffs a monotone baseline
(verification/mcp-tool-parity-baseline.json, 86 tools), fails only on NEW
discrepancies (removed tool = regression), and probes that unknown-tool
tools/call is rejected. --update-baseline to re-baseline.

Both scripts spawn the server against an isolated AQE_PROJECT_ROOT temp dir
(data protection: real .agentic-qe/*.rvf proven byte-identical before/after)
and kill the whole detached process group on exit (no leaked servers).

Verified: npm run mcp:validate -> PASS (protocolVersion 2025-11-25, 86 tools);
mcp:parity -> PASS (0 removed, 0 added). Both exit 0, real stores untouched.

Side-finding (pre-existing, not fixed here): handleToolsCall throws a plain
object for unknown tools, which escapes the request-handler safety net as
'Internal error: [object Object]' instead of a proper JSON-RPC -32601. Logged
for a dedicated fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:40:51 +00:00
Dragan Spiridonov 19e782afc1 docs: mark Ruflo adoption item 7 VERIFIED (43918b3b) — contradiction detection
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:23:10 +00:00
Dragan Spiridonov 43918b3ba8 feat(learning): contradiction detection in consolidation (#510 item 7)
AQE's Phase-1 consolidation merged near-duplicate experiences by cosine
similarity, but two experiences with the SAME context and OPPOSITE outcomes
(large quality delta) are a conflict, not a duplicate. Merging silently
absorbed the conflict; worse, a *used* low-quality contradictory experience was
never merged (merge requires application_count===0) and stayed in retrieval,
poisoning recall.

Now, when similarity >= contradictionSimilarityThreshold (0.85) AND the quality
delta >= contradictionQualityDelta (0.4), the pair is treated as a contradiction:
the lower-quality loser is SUPPRESSED from retrieval (consolidated_into =
'contradicted', excluded by the IS NULL filter) instead of merged — even if it
was applied. The survivor is NOT boosted (a conflict is not a duplicate). The
action is logged distinctly ('contradiction'). New count surfaced as
ConsolidationResult.contradicted. Both thresholds are configurable.

PRODUCTION IMPACT: affects the dream/consolidation merge phase. Genuine
near-duplicates still merge exactly as before; only same-context/opposite-outcome
pairs are newly suppressed (soft, reversible — consolidated_into can be cleared).

Verified with 6 tests against an in-memory DB matching the production
captured_experiences schema (suppress loser, suppress even-when-applied, no
survivor boost, near-duplicate still merges, dissimilar pair untouched, distinct
log action). 13 total consolidation tests pass; typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:22:51 +00:00