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>
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).
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.
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.
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.
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.
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.
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>
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>
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>
`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>
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
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Confidence decay (applyConfidenceDecay) and stale/age deprecation
(checkDeprecation) were uniform — a proven high-success pattern was forgotten
at the same rate as noise, so an influx of new low-value patterns could crowd
out historically valuable ones (catastrophic forgetting).
Inspired by Elastic Weight Consolidation, high-IMPORTANCE patterns now resist
forgetting, weighted by an importance proxy = clamped success_rate gated on
usage_count >= EWC_MIN_USAGE_FOR_PROTECTION (recency already implied: only
unused patterns decay/go stale). Honest framing (as in ruflo's EWC adapter):
this is a heuristic, NOT true Fisher information — no model gradients exist here.
- Decay: per-row factor shielded by EWC_DECAY_PROTECTION*importance. A
fully-proven pattern decays up to 10x slower; importance-0 patterns decay
EXACTLY as before (no regression — locked in by test).
- Stale: effective window = staleDays * (1 + importance*EWC_STALE_PROTECTION_BONUS),
so a proven pattern unused 30d isn't deprecated like noise.
Protection covers ONLY stale/age/decay forgetting. A pattern that drops below
minActiveConfidence (or starts failing) is STILL deprecated — we blend
retention, we don't permanently shield dead weight. All three EWC_* knobs are
exported constants; set to 0 to restore exact prior behavior (reversible).
PRODUCTION IMPACT: affects the dream/consolidation lifecycle. Low/zero-importance
patterns behave identically; high-success patterns persist longer. Verified with
10 tests against an in-memory DB matching the production qe_patterns schema
(decay deltas, stale window, no-regression, dead-weight still removed). 46 total
lifecycle tests pass.
Side-finding (pre-existing, NOT this change): checkDeprecation reads
consecutive_failures via a cast, but rowToPattern doesn't surface that column,
so the 'failures' deprecation branch never fires through getPattern. Logged for
a dedicated fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Routing confidence (QERoutingResult.confidence) is the top agent's combined
static score from calculateAgentScores. The domain term was
(domainMatch / detectedDomains.length) * 0.4, but AQE's domain detector emits
7-12 domains per task, so a perfect single-domain match scored (1/11)*0.4 ~ 0.04
— relevance washed out by detector breadth, leaving confidence dominated by the
fixed performance term.
Fix: cap the denominator at DOMAIN_DENOM_CAP (3), modeling the few truly-relevant
domains a task has. Precise detection (1-2 domains) is unchanged; broad detection
no longer dilutes a real match below usefulness. Proportionality preserved.
Tunable via the constant + the existing routingWeights.similarity.
PRODUCTION IMPACT: shifts every routing decision's confidence telemetry upward
for broad-domain tasks and strengthens domain relevance vs the fixed performance
term in ranking. Reversible (raise DOMAIN_DENOM_CAP toward old behavior).
Verified with real `aqe hooks pre-task` routes (not mocks):
broad multi-domain task: 27.7% -> 33.3% (+5.6pp); narrow tasks unchanged
(test 37.3%, security 30.3%); recommendations/ranking preserved.
77 existing routing tests pass; +4 regression tests lock in de-dilution.
NOTE: ruflo ADR-142 (per-bucket Beta priors) did NOT apply — AQE confidence is
not a Beta-bandit; root cause was this dilution. See docs/ruflo-adoption-plan.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Investigation (verified with real `aqe hooks pre-task` routes) shows AQE's
40% routing confidence is NOT a Beta-bandit prior, so ruflo ADR-142's
per-bucket fix does not apply. Confidence is the top agent's static combined
score from calculateAgentScores (agent-routing.ts) blended with an immature
Q-table (qWeight=0). Real root cause: domain-score dilution —
domainScore = (domainMatch / detectedDomains.length) * 0.4 — with the domain
detector returning 7-12 domains, washing out relevance. Measured easy=37.3%,
hard=20.5%. Proposed scoped fix to calculateAgentScores is a published
hot-path change → deferred pending user sign-off (no production code touched).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports ruflo's benchmark-self-learning.mjs discipline (the 'reports success
but persists nothing' detector) to AQE. Proves the learning stores actually
persist what they claim, that stat aggregators agree with the underlying
index, that nothing writes accidentally, and that data survives restart.
Sections (each isolated in a temp dir, each gating):
A store persistence — N creates => totalPatterns delta == N
B outcome learning — N outcomes => learningOutcomes delta == N, successRate matches fed ratio
C negative control — read-only ops => 0 delta (no accidental writes)
D restart survival — dispose + reopen on same temp path => counts persist
E consistency — RvfPatternStore.getStats == adapter.totalVectors == bank.totalPatterns
F audit-real — copies real stores read-only, checks banner numbers round-trip
Data protection (CLAUDE.md): synthetic mode uses fresh temp dirs; audit-real
copies stores and opens only copies. Verified real .rvf stores byte-identical
(mtime+size+sha256) before/after a run.
Audit-real finding on the live store: banner numbers corroborated
(72.5% success, 39.7% confidence, 2131 routings) BUT RVF vector count (338)
!= SQLite qe_patterns rows (345), a ~7-pattern index/table drift the banner
hides — exactly the divergence this harness exists to surface (reported as a
diagnostic note, non-gating). Synthetic sections A-E pass; exit 1 on failure.
npm run benchmark:self-learning
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audited AQE routing against two router bugs ruflo fixed 2026-05-29:
- #2229 stale route-cache: NOT PRESENT — Q-values read live from SQLite
(qe-reasoning-bank buildQValueLookup) and in-memory qTable; no cached
routing decision exists to go stale, so no invalidation needed.
- #2239 state-encoder truncation: NOT PRESENT — state keys are plain
string joins (agent-routing buildRoutingStateKey: taskType|priority|domain),
no bit-folded quantized vector, no mask. Distinct keyword tasks map to
distinct states.
Adds a guard test locking in non-collapsing state encoding so a future
refactor toward a bit-folding encoder fails here. 4 tests passing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan to port highest-value mechanisms from ruvnet/ruflo's last month
(307 commits, ADR-120 to ADR-143) into AQE: self-learning verification
harness, per-bucket bandit priors, router bug audit, MCP smoke layer,
EWC++ forgetting protection, and 9 more items across 3 tiers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>