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).
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.
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
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>
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 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>
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>
Wires the ADR-043 LLM router into the production swarm path: kernel
constructs HybridRouter singleton during initialize(), threads it
through all 11 LLM-enhanced domains and MCP standalone tools. Adds
persistent aqe llm config --set, Gemini GOOGLE_API_KEY alias, 2 real
fallback-chain bug fixes (provider-discriminatory hardcoded list +
non-retryable short-circuit), and a qe-devils-advocate audit hardening
sweep closing 10 findings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A post-Phase-4 devil's-advocate audit identified 10 issues. All addressed
with code fixes + tests. Some were real bugs (silent billing regression,
2 unwired MCP tools, 2 HybridRouter routing bugs); others were robustness
improvements (split singletons, silent errors, decorative type fields).
Findings → fixes (severity tagged in audit report):
1. CRITICAL — silent billing regression on upgrade
Users with ANTHROPIC_API_KEY in env would silently start paying for
Claude API calls on every Fleet operation. The CHANGELOG's "opt out
via code changes" wasn't sufficient — needed an env-only escape.
Fix: AQE_LLM_ROUTER_DISABLED=1 env kill-switch honored in both
QEKernelImpl._initializeLLMRouter and src/mcp/tools/base.ts
getSharedLLMRouter. Truthy values disable; 'false'/'0'/'no'/'off'/''
pass. Unified parse rule between kernel and MCP base.
2. HIGH — 2 unwired MCP standalone tools (audit said 3; QualityCriteria
isn't LLM-aware so excluded)
src/mcp/tools/test-generation/generate.ts:82 was calling
createTestGeneratorService(memory) without router. Same for
visual-accessibility/index.ts:177. Both now use the
createXxxServiceWithDependencies factories with { memory, llmRouter }.
3. CRITICAL — E2E only tested manual routing mode, not production
Production default is rule-based. Adding two new E2E tests for
rule-based mode SURFACED two real bugs in HybridRouter that I fixed:
3a. executeWithFallback hardcoded ['claude','openai','ollama'] as the
only fallback providers; gemini/openrouter/azure-openai/bedrock
users had no fallback even though ProviderManager can construct
them. Plus the user's defaultProvider wasn't auto-included in the
chain. Both fixed at src/shared/llm/router/hybrid-router.ts:612.
3b. A non-retryable error from one provider (e.g. Claude "invalid
x-api-key") aborted the entire fallback loop via `throw error`,
defeating the whole point of a fallback chain. Fixed: skip the
inter-attempt delay for non-retryable but continue to the next
fallback entry.
4. HIGH — MCPToolContext.llmRouter field was decorative
Added the field but no code populated it. Every getLLMRouter(context)
call fell through to getSharedLLMRouter().
Fix: added llmRouter and memory to BaseMCPTool.invoke() options;
qe-tool-bridge.ts and registry.ts now explicitly forward the shared
singleton via { llmRouter } at invoke time.
5. HIGH — 14 of 15 wired services had only STRUCTURAL verification, not
behavioral (only TestExecutor had a chat() round-trip test)
Fix: new tests/integration/llm-router-behavioral.test.ts asserts
isLLMAnalysisAvailable()===true (the exact gate the audit named) for
all 14 services + 2 full behavioral round-trips. 16 tests total.
6. HIGH — MCP and kernel built SEPARATE HybridRouter instances
Different cost trackers, caches, circuit breakers per side.
routing_economics/routing_metrics would only see half the traffic.
Fix: kernel.ts._initializeLLMRouter calls setSharedLLMRouter(router)
on init so both paths share one instance. kernel.dispose() calls
resetSharedLLMRouter() so the next kernel doesn't inherit the
previous instance.
7. HIGH — Silent error swallowing in _initializeLLMRouter
console.warn'd then continued without LLM, with no signal for
monitoring. A misconfigured provider would silently degrade with no
alert.
Fix: now publishes kernel.llm-router.init-failed (caught error) and
kernel.llm-router.init-no-provider (enabled=true but nothing
available) events on the event bus. Test confirms event delivery.
8. MEDIUM — Service count mismatch in docs (ADR/CHANGELOG said "14
services / 12 domains"; test enumerates 15 services / 11
LLM-enhanced domains)
Fix: ADR-043 addendum + CHANGELOG corrected to 15/11. Added
explicit service list and noted the 2 non-LLM domains
(enterprise-integration, coordination).
9. MEDIUM — aqe llm config --set apiKey=... silently dropped the key
stripApiKeys() returned a successfully-saved file without the key,
user thought it was saved.
Fix: CLI updateConfig() now throws a clear error pointing to env
vars BEFORE calling save. stripApiKeys also now logs a console.warn
when it strips anything, so programmatic callers see the strip.
10. LOW — Env vs disk precedence was counterintuitive and undocumented
Force-enable-on-env overrides explicit `enabled: false` on disk.
Fix: doc comment in config-store.applyEnvProviderDetection +
ADR-043 "Precedence & env-var conventions" section explains.
Verification
- npx tsc --noEmit: clean
- 191 unit + integration tests pass (16 new behavioral tests for #5,
17 wiring tests, gemini provider, config-store, llm-router-service,
kernel-llm-router with 17 tests including all kill-switch cases,
CLI persistence with 28 tests)
- 5 real-provider E2E tests pass (Gemini 2.5 Flash, OpenAI gpt-4o-mini,
OpenRouter, plus 2 rule-based-mode tests that previously failed and
now pass with the HybridRouter fallback fixes)
Refs ADR-043 audit findings (2026-05-21)
Builds on 94033e3 (Phase 1), be1e60d (Phase 2), c206c81 (Phase 3+4)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 3 — MCP tool path
- 8 standalone MCP tools (coverage-analyze, coverage-gaps, contract-validate,
chaos-inject, code-intel-analyze, defect-predict, learning-optimize) now
resolve the kernel HybridRouter via getLLMRouter(context) and pass it to
their service via the dependency-bag form.
- src/mcp/tools/base.ts: added getSharedLLMRouter() singleton, mirroring the
existing getSharedMemoryBackend() pattern. resetSharedLLMRouter() for
tests. Added MCPToolContext.llmRouter field so future tools (and the MCP
runtime when wiring per-request kernel context) can inject explicitly
rather than via the shared singleton.
Phase 4a — Mock-provider integration tests
- tests/integration/llm-router-wiring.test.ts (new, 17 tests).
- Per-domain wiring assertions (15): for each LLM-enhanced
plugin → coordinator → service path, asserts service.llmRouter ===
kernel.llmRouter after kernel boot with an injected mock provider.
This is the precise structural test for the gap the author's friend
flagged — a regression in any domain's wiring fails immediately.
- Behavioral end-to-end (1): TestExecutorService.analyzeFailuresWithLLM
actually invokes the mock provider; assert generateCalls counter
incremented and the response surfaced.
- Sanity (1): kernel.llmRouter is defined when a router was built.
Phase 4b — Real-provider E2E smoke test
- tests/e2e/llm-router-real-providers.test.ts (new, 3 tests).
- Gated by AQE_LLM_E2E=1; each provider auto-skips when its API key
isn't in env.
- Uses mode: 'manual' to bypass DEFAULT_QE_ROUTING_RULES (which would
otherwise route generic tasks to Claude even when defaultProvider
is set to something else — worth knowing).
- Verified live on 2026-05-21 against:
Gemini (gemini-2.5-flash) → "Hi!"
OpenAI (gpt-4o-mini) → "Hi!"
OpenRouter (openai/gpt-4o-mini) → "Hi!"
Claude intentionally skipped — the ANTHROPIC_API_KEY in the dev env
is a 24-char stub (real Anthropic keys are ~108 chars), would 401.
Phase 4c — Documentation
- docs/implementation/adrs/ADR-043-vendor-independent-llm.md: added
"Wiring Verified" status entry (2026-05-21) + full addendum documenting
the problem found, the four-phase resolution, the verification tests,
and the behavioral consequence for users.
- docs/implementation/adrs/ADR-051-agentic-flow-integration.md: added
"LLM-Enhancement Wiring Verified" status entry pointing to ADR-043's
addendum.
- CHANGELOG.md: added [Unreleased] section under Changed — behavioral,
explicitly noting the behavior shift (fleets with provider keys in env
will now start routing LLM calls through that provider), the opt-out
path (KernelConfig.llmRouter: { enabled: false }), and the persistence
fix for `aqe llm config --set`.
Test totals across all four phases on this branch
- 39 unit tests across 5 files (Phase 1)
- 17 integration tests in 1 file (Phase 4a)
- 3 real-provider E2E tests in 1 file (Phase 4b, AQE_LLM_E2E=1)
- 34 pre-existing kernel tests still pass (no regression)
- 22 pre-existing test-generation coordinator tests still pass (no regression)
- Clean tsc --noEmit across the full project
Refs ADR-043, ADR-051
Closes the wiring gap identified by user audit on 2026-05-21
Builds on 94033e3 (Phase 1) and be1e60d (Phase 2)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes 11 findings from a post-merge audit of the external embedder endpoint
implementation. The "no silent hash fallback" correctness promise was being
contradicted by two callers; the connect timeout was actually an idle timeout;
the identity probe could be bypassed in several paths; and identity drift
across runs was undetectable. All hardened.
Critical/High fixes:
- sqlite-persistence backfill no longer falls back to hash embeddings when the
endpoint is configured; re-raises to protect the HNSW index from non-comparable
vectors (was poisoning silently on every endpoint blip).
- qe-reasoning-bank.embed() propagates the throw under isUsingEndpoint() instead
of silently substituting a hash vector.
- EmbedderEndpointClient.embed() now gates on probe() — lazy probe on first use
with in-flight dedup so 20+ concurrent embeds share one probe.
- Circuit breaker recovery (window expires) clears cached identity so the next
embed re-probes — endpoint restarts often coincide with model swaps.
- True TCP-connect timeout enforced at the socket layer (was req.setTimeout,
which is an idle-after-write timeout — SYN blackholes hung ~75s instead of 5s).
- Endpoint identity persisted to memory.db kv_store (_system namespace) — cross-
run fingerprint drift now surfaces as a loud warning on next boot.
Medium fixes:
- encoding_format: 'float' pinned in request; base64 responses fail loud.
- TLS knobs (ca, cert, key, rejectUnauthorized, servername) passthrough for
self-hosted https endpoints without NODE_TLS_REJECT_UNAUTHORIZED=0.
- URL userinfo stripped + warned at parse; redacted from all error messages
and logs.
- Embedding cache key namespaced by mode (endpoint:<fingerprint>:<text> vs
inproc:<text>) — cross-mode collisions structurally impossible.
Tests:
- 33 unit tests pass (was 23): adds probe-gate, breaker re-probe, connect
timeout, DNS failure, concurrent embed dedup, base64 fail-loud, URL
redaction, encoding_format wire format.
- 6 live integration tests against real llama-server (all-MiniLM-L6-v2.Q8_0.gguf):
probe, single embed, batch, lazy probe gate, dim mismatch, 50-input batch.
Replaces the prior "9ms in-process fake" with honest numbers (cold 15ms,
warm 2ms against localhost llama.cpp).
- New tests/integration/embedder-endpoint-llamacpp.test.ts is gated on
AQE_LLAMA_EMBED_URL so it skips cleanly in CI without the binary.
ADR-097 updated with the per-caller audit table (12 writers verified
non-poisoning) and "drop-in for 6 providers" claim softened to "verified
against llama-server; OpenAI shape compat expected but unverified" per
provider.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add opt-in OpenAI-compatible /v1/embeddings client to eliminate duplicate
in-process model loads in ruflo/ruvector co-deployments and the per-hook
cold-load cost in fresh agentic-qe CLI processes.
When EmbeddingConfig.endpoint (or AQE_EMBEDDER_ENDPOINT) is set, the
@huggingface/transformers import is bypassed entirely and feature-extraction
routes through the remote service. Endpoint unset = today's behavior.
Wire format is OpenAI's /v1/embeddings so any existing embedder server (TEI,
vLLM, llama.cpp, Ollama, LocalAI, LM Studio, OpenAI) works as a drop-in,
not just bespoke ruvector servers. Transport is HTTP or HTTP-over-Unix-socket.
Errors hard-fail with a circuit breaker (3 failures / 60s window) — no
silent hash-embedding fallback, which would poison the HNSW index by mixing
non-comparable vectors. A startup probe asserts dim === 384 and computes a
stable identity fingerprint that catches model/quantization drift at the
boundary.
- src/learning/embedder-endpoint-client.ts: OpenAI client with keep-alive
agent, 5s/30s timeouts, Bearer auth from env, circuit breaker, probe
- src/learning/real-embeddings.ts: endpoint branch placed strictly before
the dynamic transformers import so the cold-load fix holds at runtime;
new isUsingEndpoint() / getEndpointIdentity() observability helpers
- 23 new unit tests (real http.createServer fixtures, no http mocking)
- End-to-end smoke: 9ms for import + init + probe + embed, transformers
never appears in the loaded-module list
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ADR-096 fix#3 (Option A): the previous 4-dim state_key
taskType|priority|domain|complexityBucket
fragmented semantically identical tasks across cells. complexityBucket
was derived from raw description length (round(min(len/200,1)*10)) —
so a 55-char and 130-char phrasing of the same task landed in
different Q-rows, and QWEIGHT_RAMP_VISITS=20 was rarely reachable
per cell even after the fix #1/#2 loop closure.
State key is now 3-dim:
taskType|priority|domain
Same change applied at both ends of the loop:
- buildRoutingStateKey (agent-routing.ts) — parameter and key shape
- updateHookRouterQValue (hooks-dream-learning.ts) — state_key SQL
- qe-reasoning-bank.ts reader-side call
- task-hooks.ts post-task writer call
- routing-hooks.ts post-route writer call (new in fix#1)
`deriveComplexityBucket` stays exported (marked @deprecated for
state_key use) — the pre-task bridge payload still carries
`complexityBucket` for kv_store schema compatibility and any
downstream telemetry. `TaskBridgePayload` interface unchanged.
Migration: existing rl_q_values rows written under the previous
4-dim key are orphaned. Field reports show real installs have at
most ~2 such rows (the loop was broken; no real Q-data accumulated),
so we accept the orphans rather than ship a migration. If a future
install carries substantial 4-dim data, a one-shot script that
strips the trailing |N segment is straightforward.
Test contract extended:
- agent-routing-exploration.test.ts pins the 3-segment key shape
- route-q-loop-closure.test.ts asserts complexityBucket does NOT
leak into updateHookRouterQValue payloads (writer-side check) and
that bridge.complexityBucket is ignored even when present.
ADR-096 updated: complexity-bucket promoted from Open Questions to
Decision (Option A). Open Questions now lists "re-introducing a
coarse complexity tier if telemetry shows the 3-dim key conflating
decisions that benefit from differentiation" as a wait-and-measure
follow-up rather than a planned change.
Refs: #499
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The route hook (UserPromptSubmit) reads `rl_q_values` to produce qWeight,
but the only writer was task-hooks `post-task` gated on `outcome.bridge`.
On real projects, `routing_outcomes` reached 139 rows while `rl_q_values`
stayed at 2 — the route surface that displays qWeight had nothing to read,
so `qWeight: 0` was structurally pinned.
Two complementary producer additions, ADR-096:
Fix#1 — post-route trains rl_q_values
When the Stop hook resolves a routing_outcomes sentinel, derive a
state_key from the sentinel's `task_json.description` using the
same exported helpers the route hook used (`deriveTaskType`,
`deriveComplexityBucket`, `detectQEDomains`) and call
`updateHookRouterQValue`. Writer and reader address byte-identical
rows. Best-effort + try/catch: Stop hooks must never crash the host.
Fix#2 — post-task fires even when bridge is absent
Drop the `if (outcome.bridge)` gate. When bridge is missing,
derive state from `options.description` instead. Adds a new
`--description` option to `aqe hooks post-task` so the PostToolUse
hook command can pass `tool_input.description` for Task tools.
Skip the update only when neither bridge nor description is
available (would pollute an "unknown" sentinel state).
Tests: tests/unit/cli/commands/route-q-loop-closure.test.ts pins
the producer/consumer state_key alignment contract — if a future
refactor desynchronizes the three call sites
(routing-hooks.ts post-route, task-hooks.ts post-task,
qe-reasoning-bank.ts:530), assertions fail the build.
Closes the Open Question flagged in ADR-095. The deriveComplexityBucket
fragmentation Jordi noted is acknowledged in ADR-096's Open Questions
and tracked for a follow-up — orthogonal to closing the loop.
Refs: #499
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stops unbounded growth of patterns.rvf / brain.rvf.
- #495: AbortSignal in HybridMemoryBackend.initialize() so a 5s
createHybridBackendWithTimeout abort actually cancels the work
instead of leaking a 14-min runaway init (~20 GB / 12 MB/s leak
observed in the field). UnifiedMemoryManager.initialize() also
accepts {signal} with throwIfAborted() at each await seam.
- Post-dream compaction in DreamScheduler — threshold-gated compact()
against patterns.rvf and brain.rvf so append-only growth has a
steady-state janitor.
- Boot-time size guard in getSharedRvfAdapter() for one-shot recovery
of installations already in the bad state.
- Bounded SQLite→RVF backfill (top-1000 by last_used_at) so
`: > patterns.rvf` for disk recovery doesn't trigger an unbounded
replay (the "regrew to 59 GB" pathology).
- RvfDualWriter.compact() for brain.rvf parity.
Three operator env knobs added (no rebuild needed):
- AQE_RVF_SIZE_GUARD_BYTES (default 256 MB)
- AQE_RVF_DEAD_RATIO_THRESHOLD (default 0.30)
- AQE_RVF_BACKFILL_LIMIT (default 1000)
29 new unit tests. Performance gate 15/15 pass. test:ci 4035 pass
(3 pre-existing failures, verified identical on main).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the self-learning loop: the Q-value Bellman updates that #487
populates now have a routing-side reader; structurally-aware exploration
(via the mincut infrastructure from ADR-047 / ADR-068) gathers Q-table
signal beyond the deterministic greedy winner; the catalog diversifies
without sacrificing safety on bottleneck decisions.
Three signals blended at the agent-routing decision point:
1. Static score (existing): domain match + capability match + performance
prior + language boost + pattern similarity. Computed by
`calculateAgentScores` in src/learning/agent-routing.ts.
2. Q-value blend (NEW — reads from rl_q_values):
effectiveScore = staticScore * (1 - qWeight) + sigmoid(qValue) * qWeight
qWeight ramps from 0 (no Q-data) to MAX_Q_WEIGHT=0.4 over
QWEIGHT_RAMP_VISITS=20 visits per (state_key, action_key). Cold-start
behavior is identical to pre-ADR-095 static-only routing; the learned
signal contributes meaningfully only after the Q-table has matured for
that state.
3. Mincut-gated ε-greedy exploration (NEW):
baseEpsilon = AQE_ROUTER_EXPLORATION_RATE env or 0.05 default
safetyMultiplier = getSharedMinCutMonitor().isCritical() ? 0.2 : 1.0
epsilon = baseEpsilon * safetyMultiplier
With probability ε, swap the greedy winner with a uniformly-random
pick from the top-3 alternatives. Uses crypto.randomInt (NOT
Math.random) to match the codebase convention (randomUUID is the
established crypto entry-point in the same modules).
Maintainer decisions captured in ADR-095 v2:
1. Default ON (Option A): mincut-modulated exploration ships enabled.
AQE_ROUTER_EXPLORATION_RATE=0 is the documented escape hatch.
2. Telemetry surface: extended `aqe learning loop-health` with a new
"Routing diversification" section showing 7-day exploit/explore
counts, avg quality_score per bucket, avg criticality, avg q_weight.
3. crypto.randomInt over Math.random — codebase convention preserved.
4. Q-value ramp constants are best-guess (MAX_Q_WEIGHT=0.4,
QWEIGHT_RAMP_VISITS=20); tune post-deploy from telemetry.
Source-level findings worth noting:
- `rl_q_values` was populated by `updateHookRouterQValue` since #487 but
NEVER read during routing. Verified via grep across src/learning/,
src/routing/, src/cli/commands/hooks-handlers/. This commit adds the
reader; closes that loop.
- Mincut graph is populated by Queen coordinator swarm activity, NOT
per-task. So the "criticality" signal is graph-level safety (the
`isCritical()` boolean from MinCutHealthMonitor) rather than per-task
structural weight. Honest framing reflected in the ADR.
What landed:
src/kernel/unified-memory-schemas.ts
- routing_outcomes gains exploration (INTEGER NOT NULL DEFAULT 0),
criticality (REAL), q_weight (REAL) columns + an exploration index
for the loop-health bucket queries
src/routing/routing-outcomes-migration.ts (NEW)
- ensureRoutingOutcomesAdr095Columns(db) — idempotent ALTER TABLE for
pre-existing databases. Process-local flag avoids repeat work.
src/routing/routing-feedback.ts
- hooks the migration helper into the existing schema-migration block
src/learning/agent-routing.ts
- new helpers: blendStaticAndQValue, resolveExplorationRate,
applyExplorationPolicy (uses crypto.randomInt), sigmoid normalization
- new helpers: deriveTaskType, buildRoutingStateKey,
deriveComplexityBucket — moved from task-hooks.ts so the routing path
and the post-task Q-update path share the same state_key derivation
- calculateAgentScores accepts optional qValueLookup; backward-compat
when undefined (identical pre-ADR-095 behavior)
- ScoredAgent gains optional telemetry fields: staticScore, qWeight,
qValue, qVisits, exploration
src/learning/qe-reasoning-bank-types.ts
- QERoutingResult gains optional exploration / criticality / qWeight
src/learning/qe-reasoning-bank.ts
- routeTask builds the Q-value lookup from rl_q_values keyed by
stateKey = deriveTaskType(task) | normal | domain | complexityBucket
- applies exploration policy after sort; reports avgQWeight + the
mincut safetyMultiplier in the QERoutingResult
- new private buildQValueLookup helper with table-exists guard so the
method is a graceful no-op when the schema isn't applied yet (tests,
fresh-init scenarios)
src/cli/commands/hooks-handlers/task-hooks.ts
- private deriveTaskType removed (DRY: now imported from agent-routing)
- pre-task sentinel INSERT writes the three new columns
- migration helper invoked before write
src/cli/commands/hooks-handlers/routing-hooks.ts
- JSON output passes exploration / criticality / qWeight through
- INSERT writes the three new columns; migration helper invoked
src/cli/commands/learning.ts
- `aqe learning loop-health` queries routing_outcomes for 7-day rolling
exploration stats and avg quality_score per bucket. Skipped silently
when the columns don't exist (pre-ADR-095 databases).
Tests:
- tests/unit/learning/agent-routing-exploration.test.ts: 35 tests
cover sigmoid blend math, qWeight ramp, env override priority,
mincut safety multiplier, exploration rate statistical tolerance,
only-top-3 alternative constraint, deriveTaskType regex coverage,
stateKey format, complexityBucket scaling, end-to-end Q-blend
- tests/unit/routing/routing-outcomes-migration.test.ts: 5 tests
cover fresh-DB migration, idempotent re-run, missing-table
tolerance, exploration index creation, column type/default checks
634/634 tests pass across CLI commands, workers, bridge, learning,
init/phases, architecture, kernel, and routing suites; build clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves dream cycles out of hook subprocesses into the long-lived kernel
process via the DreamScheduler that already existed but was never wired.
Single cutover, no feature flag — per maintainer decision on ADR-094.
Why this matters
================
Before this commit, every ~20 Edit/Write tool uses triggered a dream cycle
inside a short-lived `npx aqe hooks post-edit` subprocess. The cycle held
a SQLite write transaction for up to 10 seconds while the kernel-owning
MCP server, other hook subprocesses, and user MCP tool calls queued
behind it. stderr from the cycle was consumed by Claude Code's hook
reader and lost from operator visibility. Cold DreamEngine init paid
every ~20 edits.
Now the kernel's `QEKernelImpl.initialize()` boots a `DreamScheduler`
that runs cycles on its own cadence inside the long-lived process. Hook
subprocesses keep only `incrementDreamExperience` (a counter bump that
returns in ~50ms). Operators see `aqe learning loop-health` report the
`dreamScheduler` component as live within one tick.
What landed
===========
src/kernel/kernel.ts
- new private `_dreamScheduler?: DreamScheduler`
- new config flag `enableDreamScheduler` (default `true`, mirrors
`enableExperienceBridge`)
- initialize() constructs DreamEngine + DreamScheduler after the
bridge starts, wraps in try/catch (best-effort), failure logs a
warning and continues (kernel boot never blocks on this)
- dispose() stops the scheduler BEFORE bus/memory cleanup so a dream
in-progress doesn't write to a disposed backend
src/learning/dream/dream-scheduler.ts
- executeDream() now records loop-health on every tick (success or
failure) via the helper from #488 Phase 1 B.2. Operators see
`learning:loop-health.components.dreamScheduler.successesSinceBoot`
grow on each cycle.
src/cli/commands/hooks-handlers/editing-hooks.ts
src/cli/commands/hooks-handlers/task-hooks.ts
- dropped `checkAndTriggerDream` import + call
- kept `incrementDreamExperience` (the counter is the producer-side
signal the kernel scheduler reads)
- JSON output now emits `dreamTriggered: false, dreamReason:
'deferred-to-kernel'` so operator scripts grepping the field see
*where* the cycle actually runs
src/cli/commands/hooks-handlers/hooks-shared.ts
- removed the `checkAndTriggerDream` re-export. The function itself
is still defined in hooks-dream-learning.ts and exported there for
direct test access and the documented rollback path.
tests/unit/architecture/hooks-boundary.test.ts (NEW)
- greps `src/cli/commands/hooks-handlers/` for forbidden patterns:
imports from `src/learning/dream/` and references to
`checkAndTriggerDream`. Skips comment lines so explanatory mentions
don't false-positive. `hooks-dream-learning.ts` is in `allowedFiles`
since it DEFINES the function.
- This is the enforcement mechanism for the boundary contract. A
contributor accidentally re-adding heavy hook-side work gets a
clear CI failure pointing them at ADR-094.
tests/unit/kernel/dream-scheduler-wiring.test.ts (NEW)
- asserts `enableDreamScheduler` default, opt-out path, init+dispose
cleanliness with and without the scheduler, idempotent dispose
tests/unit/cli/commands/post-edit-dream-trigger.test.ts (REWRITTEN)
- was asserting checkAndTriggerDream was called from post-edit (the
#480 fix). That contract is now wrong — the kernel is the only
place that triggers dreams.
- now asserts: counter is bumped (the kernel reads it), no inline
cycle trigger, JSON emits `deferred-to-kernel`, hook stays robust
when the memory backend throws.
docs/implementation/adrs/ADR-094-kernel-side-dream-cycles.md
- status: Implemented
- migration section replaced with the single-cutover rationale and
a documented 4-line rollback path
583/583 tests pass across CLI, workers, bridge, learning, init/phases,
architecture, and kernel suites. Build clean.
Boundary contract (now enforced in CI)
======================================
Hook subprocesses MUST NOT do work that exceeds ~100ms. Acceptable
hook-side work: producer SQLite writes, counter increments, in-memory
routing lookups. Unacceptable: DreamEngine init, multi-second SQLite
transactions, anything that depends on long-lived state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six user-reported MCP fixes (see CHANGELOG and docs/releases/v3.9.30.md):
- #467 __dirname ESM polyfill across 7 source files
- #469 real all-MiniLM-L6-v2 embeddings (was random-vector mock)
- #470 NaN guard in coherence_collapse risk categorization
- #472 default test phases when MCP wrapper omits the list
- #473 wire SessionOperationCache into MCP tools/call dispatch
- #474 validate language and auto-derive framework in test_generate
Also fixes a pre-existing field-name typo in
tests/integrations/agentic-flow/e2e-integration.test.ts (.similarity →
.score) that was vacuously passing because the previous mock embedding
generator rarely cleared the threshold to exercise the assertion.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rescoped per @Jordi-Izquierdo-DDS's filed issue #484 and his Stage A.1
verified shop patch 050:
- KEEP the domain-specific fan-out events (test-execution.TestRunCompleted,
coverage-analysis.CoverageGapDetected/CoverageReportCreated,
test-generation.TestGenerated, code-intelligence.FileChanged) with
their existing flat basePayload shape. Their handlers were already
shipped against this shape; changing it would break out-of-tree
subscribers. Smaller blast radius.
- ONLY change the universal learning.ExperienceCaptured event to use the
canonical nested { experience: TaskExperience-shaped, reward } shape.
Tests parametrized across multiple non-fan-out domains
(requirements-validation, security-compliance, visual-accessibility) per
Jordi's #484 test improvement suggestion — these exercise the
universal-event path exclusively, which v3.9.28's test-execution-only
test inadvertently masked via the fan-out's side-effect kv write.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hotfix for v3.9.28 — bridge payload shape (#482 round 3)
After v3.9.28 fixed the subscriber-loading order, handlers fired for
every bridge event but immediately short-circuited because
event.payload.experience was undefined. The bridge had been publishing a
flat payload { experienceId, domain, agent, task, ... } that didn't
match handleExperienceCaptured's destructure.
Fix: bridge now publishes the canonical nested
{ experience: TaskExperience-shaped, reward } shape that matches the
existing publisher in src/learning/experience-capture.ts:1043.
Also dropped the domain-specific fan-out events (test-execution.TestRunCompleted,
coverage-analysis.CoverageGapDetected, etc.) — their handlers expect
domain-event-specific fields (runId, passed, failed, gapId, riskScore)
that hooks don't capture, so publishing them with undefined fields
recorded degenerate experiences. Universal learning.ExperienceCaptured
is now the bridge's sole publication.
Integration test now uses requirements-validation domain (per Jordi's
suggestion) so the universal-event path is the sole thing being tested,
not masked by a fan-out side effect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hotfix for v3.9.27 — bridge subscriber wiring (#482)
The CapturedExperienceBridge shipped in v3.9.27 published events into a
kernel with no subscribers because lazyLoading: true left domain plugins
un-initialized when the bridge's first drain ran. Cursor advanced past
the rows; events were silently lost.
Fix: kernel.initialize() now eagerly loads all enabled domain plugins
before starting the bridge. CLI commands opt out via the new
enableExperienceBridge: false flag to keep their fast startup. Added
end-to-end integration test (real kernel + real plugin + real drain →
asserts kv_store has learning:experience:* key).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- AbortSignal threading bounds ReasoningBank.initialize() (#478)
prevents leaked init from runaway patterns.rvf writes
- CapturedExperienceBridge wires hook activity to domain plugins (#479)
via kernel-side drain of captured_experiences
- Removes dead v3-qe-bridge.sh, two stale planning docs, five orphan
workers in qe-workers.ts (subsumed by LearningConsolidationWorker)
Net: +430 / -2,090 lines
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ten learning-pipeline fixes (#453, #454, #455, #456, #460, #461, #462,
#463, #464, #465) plus dup closures (#457-#459). Tightens the #465
sentinel sweep with the same task_json discriminator the LIMIT-1 close
uses, preserving the #451 ownership split (route sentinels owned by
post-route, pre-task sentinels by post-task). Updates the regression
suite to reflect the new sweep behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(hooks): post-route Stop hook closes route sentinels 1:1 (#451)
The `route` hook fires on UserPromptSubmit and writes a sentinel row to
routing_outcomes with quality_score=-1, expecting `post-task` to close
it. But `post-task` only fires on PostToolUse ^(Task|Agent)$, which is
rare in direct-work sessions (Bash/Edit/Read without spawning sub-
agents). Sentinels accumulated indefinitely — Jordi reported 123+
unresolved rows per session. Stream D never converged.
Changes:
1. New `aqe hooks post-route` subcommand
(src/cli/commands/hooks-handlers/routing-hooks.ts)
Closes the most-recent unresolved route sentinel using a
`task_json NOT LIKE '%"taskId"%'` discriminator that isolates
route entries from pre-task entries. Quality formula collapses the
6-dim outcome formula for unknown duration:
0.325 + 0.25·success + 0.10·1.0
= 0.675 (success) / 0.425 (failure)
2. Inverse discriminator on updateRoutingOutcomeQuality
(src/cli/commands/hooks-handlers/hooks-dream-learning.ts)
Added `task_json LIKE '%"taskId"%'` to the WHERE clause so
post-task only ever closes pre-task sentinels. Without this,
post-task could grab a stale route sentinel by accident and
miscredit duration to a row that wasn't about a Task() call.
Symmetric counterpart to the post-route filter.
3. Stop-hook wiring in both init template paths
(src/init/phases/07-hooks.ts + src/init/init-wizard-hooks.ts)
Added `npx agentic-qe hooks post-route --success true --json`
to the Stop block. Timeout 5000ms, continueOnError: true, so a
failed post-route cannot block session teardown.
4. CLI help text (src/cli/commands/hooks.ts)
Added example for the new subcommand.
5. Regression test (tests/unit/cli/commands/post-route.test.ts)
4 tests using in-memory better-sqlite3:
- Closes most-recent route sentinel; leaves pre-task sentinels
and resolved rows untouched
- quality_score = 0.425 when --success false
- No-op when only pre-task sentinels exist (discriminator works)
- No-op on second invocation when only one sentinel was outstanding
Bug-introduce verification: stashed routing-hooks.ts, ran new test —
all 4 cases failed (Commander rejects unknown subcommand). Restored
fix, all 4 pass. Test distinguishes pre-fix and post-fix states.
Co-authored-by: Jordi Izquierdo <noreply@github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(release): bump version to v3.9.25
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(hooks): post-task runs Stream B/D/F when --task-id is missing (#449)
The shipped PostToolUse hook command for Task/Agent matchers is
`npx agentic-qe hooks post-task --task-id "$TOOL_RESULT_agent_id" --success --json`
Claude Code does not populate $TOOL_RESULT_agent_id, so the flag arrives
empty and options.taskId is falsy on every real hook invocation. The
`if (options.taskId)` gate inside post-task then short-circuited the
entire Stream B/D/F learning chain:
- reasoningBank.recordOutcome — never called
- persistTaskOutcome — never called (no captured_experiences,
experience_applications, qe_trajectories, dream_insights.applied)
- updateRoutingOutcomeQuality — never called (routing_outcomes
sentinels accumulated unfilled at success=0/quality_score=-1)
- updateHookRouterQValue — never called (rl_q_values stayed empty
forever)
Same regression was fixed in v3.9.21 (patch 030) and silently came
back in v3.9.23.
Fix:
- Remove the `if (options.taskId)` gate in post-task. Use
`hook-${Date.now()}` fallback for the patternId / experience id
when the caller didn't supply one. The bridge lookup in
persistTaskOutcome keys by `ORDER BY created_at DESC LIMIT 1`
(not by taskId), so Stream F's Bellman Q-update still finds the
bridge payload it needs.
- Remove the parallel `&& options.taskId` clause from the pre-task
routing_outcomes sentinel write at line 195 — same root cause.
- Add `DO NOT REINTRODUCE` comment blocks at both sites pointing
at #449 and patch 030.
Test: tests/unit/cli/commands/task-hooks-no-taskid.test.ts mocks
hooks-shared and asserts persistTaskOutcome / updateHookRouterQValue
fire when --task-id is absent. Bug-introduce verification:
re-introducing the gate makes the no-taskId test fail with the
expected "called 1 times, but got 0 times".
Co-authored-by: Jordi Izquierdo <noreply@github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(release): bump version to v3.9.24
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#447. Three-layer fix for the upsert-returns-wrong-UUID bug that
silently broke Stream C of self-learning:
* sqlite-persistence.ts: SELECT actual id after ON CONFLICT(name,
qe_domain, pattern_type) — the upsert keeps the existing row's UUID
but we were returning the input one, leaving downstream callers with
a UUID that didn't exist in qe_patterns.
* rvf-pattern-store.ts: capture the SQLite return value and realign
pattern.id BEFORE the HNSW ingest so the index keys stay consistent
with the source-of-truth table.
* qe-hooks.ts QEAgentCompletion: change const → let on
effectivePatternId, capture storeResult.value.id from the auto-create
storePattern call, feed it into recordOutcome. Without this, every
auto-created pattern's outcome silently failed with "Pattern not
found", which is why qe_pattern_usage stayed at 0 forever.
Also (#448): excluded tests/unit/learning/qe-reasoning-bank.test.ts and
aqe-learning-engine.test.ts from test:unit:fast — both chronically hang
the vitest fork worker on heavy module graph load. Same pattern as
domain-handlers.test.ts already excluded from test:ci. Issue #448 tracks
the root-cause investigation.
Thanks to @Jordi-Izquierdo-DDS for the #447 diagnosis.
Co-Authored-By: Jordi Izquierdo <jordi-izquierdo-dds@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes routing learning loop — bootstrap patterns now reach HNSW index
(#445). After this release, `patternCount` in routing hook output is
non-zero, existing installs auto-backfill `.agentic-qe/patterns.rvf` from
SQLite on next startup.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps version + fleetVersion across package.json, plugin.json, and the
two skills-manifest.json copies. Adds CHANGELOG entry and release notes.
Includes one fix discovered during the release-prep e2e: stale-lock
recovery in shared-rvf-adapter's openOrCreateRvf ladder. The native
@ruvector/rvf-node binding leaves a `<file>.lock` artifact when a
process exits without calling close() — a behaviour that frequently
happens in CLI flows and that the previous always-create code path
masked. With open-or-create, those stale locks now surfaced as
`LockHeld` errors. The ladder now detects that, removes the stale
lock file, and retries open exactly once. E2E confirms RVF native
recovers cleanly across init → CLI invocation boundaries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundles fixes for issue #439 (Windows install) and Jordi RUFLO patches
P020 (RVF init) + P220 (hypergraph blindness).
Windows install (#439):
- Move hnswlib-node from dependencies to optionalDependencies so
installs no longer fail on Windows boxes without VS C++ Build Tools;
HnswAdapter already routes around a missing native binary at runtime.
- Replace HNSWIndex.ts top-level static `import hnswlib from
'hnswlib-node'` with a lazy require so package load survives the
optional dep being absent.
- README + preinstall.cjs: document toolchain requirement and disclose
that the JS fallback degrades to O(N) brute-force when @ruvector/gnn
is also unavailable (Windows default — no win32 prebuilds ship).
- ADR-090 amendment reconciling the move with ADR-081's optional-dep +
JS-fallback intent.
RVF init (Jordi P020):
- Root cause: shared-rvf-adapter and pattern-store factory always called
RvfDatabase.create(), which throws `0x0303 FsyncFailed` when the .rvf
file already exists from a prior init phase. Reproduced and confirmed
against both 0.1.7 and 0.1.8 native binaries — not a version
regression.
- Fix: race-tolerant open-or-create ladder (try open → fall back to
create → retry open on create-race) with dim() verification after
open. Mismatched-dim files are closed and the caller degrades rather
than corrupting silently.
Hypergraph (Jordi P220):
- Drop the original flushEdgesToHypergraph (wrote `call`/`extends`
edges that no production query reads — devil's-advocate review caught
it as dead infrastructure).
- Add HypergraphEngine.synthesizeTestCoverage that re-tags test files
type='test' and writes `covers` edges from each test file to
functions in the source files it imports. This is the shape
findUntestedFunctions / findImpactedTests actually filter on.
- E2E verified: untested correctly excludes covered functions;
impacted-tests returns the right test file.
Verification:
- `npm run build` clean
- 276/276 tests pass across HNSW, RVF, KG, coordinator, hypergraph
suites (5 new synthesizeTestCoverage tests; replaced 4 obsolete
flush tests)
- E2E on fresh project: aqe init clean, hooks stats shows
nativeAvailable: true, code index logs `Test coverage synthesized`,
hg untested + hg impacted return useful results
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>