Consolidate C4 diagram generation onto a single engine and expose it to
users via CLI and MCP, with a deterministic confidence gate and real
Knowledge-Graph-derived relationships.
- Consolidation (C1/C6): C4ModelService is the single render/analyze/store
engine; the bridge delegates and FAILS LOUD on a render error instead of
silently degrading. Duplicate inline generators removed.
- C2: real component relationships from the Knowledge Graph (AST import/call
edges) replace the naming heuristic; project-scoped via a new KG basePath
so repos outside cwd don't trip the path-traversal guard.
- C3: deterministic confidence gate (high/medium/low + reasons) on every
diagram — surfaces the detector's known limits instead of hiding them.
- C4/C5: `aqe code c4` CLI + `qe/code/c4` MCP tool (generate/search),
verified at MCP-CLI parity and through the protocol-server bridge.
- Search: generate persists embeddings (opt-in `enableC4Embeddings`, on for
MCP) so `qe/code/c4 search` returns hits in the standard flow.
- Fixed pre-existing lint in touched files (require->import, unused catches).
Docs: ADR-112 + docs/guides/c4-architecture-diagrams.md. tsc + lint clean;
C4 surface fully green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ADR-150 graceful-degradation core. AQE's native/optional deps (@ruvector/*,
hnswlib-node, rvlite) must degrade — not crash — when a platform binary or a
Dependabot-pruned lockfile is missing.
- src/shared/optional-module.ts: loadOptionalModule(name, req?) → {available}|
{degraded}. Fixes the #528 anti-pattern in the existing ad-hoc
try{require}catch{} (6+ ruvector wrappers): degrades ONLY on genuine absence
(MODULE_NOT_FOUND) and RE-THROWS real load failures (ABI mismatch / corrupt
.node) instead of masking them. 8 tests incl. the rethrow property.
- .github/workflows/degraded-mode.yml: the absent drill — npm ci --omit=optional
--ignore-scripts, then run the degraded-path tests. Pairs with the regular
install job (proves both paths, #528).
Follow-up (held, CI-verified): migrate the ad-hoc sites + the static import in
sona-wrapper.ts onto loadOptionalModule (changes a production native-load path).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The opt-in "verify before you emit" primitive a vertical/harness embeds: route
findings through the blind refuters, drop the refuted, surface every verdict for
witness, report overhead. Built in @ruvector/adversarial-verify (vendor-ready,
zero deps) so MetaHarness can embed it.
- gate.ts: verifyGate(findings, {judge, enabled, dropUncertain}) →
{emitted, blocked, all, latencyMs, judgeCalls}. Disabled ⇒ no-op, ZERO judge
calls (cost-neutral). Default-off-able per cheap-vertical posture.
- gate.test.ts (6): (a) false claim killed (b) true survives (c) no-op cost-
neutral when disabled (d) overhead measured. 31 green across the module; tsc clean.
MetaHarness-side embedding (template var + generated-harness wiring) is spec'd in
plan 05 — cross-repo (agent-harness-generator) + needs the package vendored/
published, both pending explicit OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Factor AQE's blind-refuter primitive out of qcsd-development-review.js into a
host-agnostic, ZERO-AQE-dependency module (publishable as @ruvector/* later;
not published per no-publish-without-OK). Closes G2.
- src/verification/adversarial-verify/: types (Finding/FindingVerdict/Judge),
blind refuterPrompt + lenses (ADR-074), pure k-of-n majority-kill synthesis,
adversarialVerify orchestrator (injected Judge — no agent()/Claude dep),
calibrate() (false-kill/false-keep vs ground truth, any Judge).
- 25 tests: synthesis math, blind orchestration, calibration (majority-of-3 ≤
single-refuter error), and PARITY — byte-identical to the workflow's inline
synthesis. The Workflow harness sandboxes .claude/workflows (no imports), so
the workflow inline-mirrors the package; annotated canonical + parity-guarded.
- Verification (plan A2): (a) synthesis tests ✓ (b) calibration mechanism ✓
(c) regression parity ✓ (d) zero AQE import (grep) ✓. strict-tsc clean.
Remaining (optional): empirical real-LLM false-kill on a labeled corpus (inject
a real local/OpenRouter Judge into calibrate()). Unblocks A5.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the asymmetric escalation lane corrections measured in D3 and makes
the opt-in reusable beyond test generation. All opt-in, off by default.
- executor: bestOfK diverse attempts per tier (§12 — single gated shots Goodhart,
best-of-k converts); oracleKind 'objective'|'self-authored' Goodhart guard (§10 —
a self-authored gate is withheld from routing-feedback so it can't lift confidence).
- coordinator-support: reusable buildFreeTierExecutor factory + runFreeTierTextTask;
default cheap model raised qwen3:8b -> qwen3:30b-a3b (D3: 8B below the gen floor).
- test-generation: refactored onto the factory; bestOfK=2 (the D3-validated config,
was running single-shot).
- requirements-validation: second adopter — opt-in cheap-first BDD/Gherkin gen with a
strengthened relevance oracle (rejects off-topic boilerplate, resists §10) + D9 sink.
- tests: executor (best-of-k, guard), coordinator-support factory, requirements opt-in
(incl. anti-Goodhart + D9). Full free-tier+coordinator sweep green; strict-tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes G1 of the cross-pollination plan: AQE now governs its own ~90-tool
MCP surface with a default-deny policy + CI gate ("npm audit for agent tools").
- .harness/mcp-policy.json: truthful default-deny snapshot; _enforcement labels
what is wired (tool-scoping default-deny, witness-chain audit) vs posture/
stated-intent — not theater.
- scripts/mcp-policy-gate.mjs: self-contained mirror of MetaHarness mcp-scan's
HIGH checks; also reads .mcp.json (closing the scan false-negative where AQE's
server is declared there, not in .claude/settings.json). Exit 1 on any HIGH.
- .github/workflows/mcp-policy-gate.yml + `npm run security:mcp-policy`.
- tool-scoping: fix the interface doc-comment that claimed "empty/undefined ->
allowAll" (the code DENIES by default); export ALL_AGENT_ROLES.
- tests/unit/mcp/mcp-policy-gate.test.ts: drift guard (policy roles <-> enforcement),
default-deny posture. 46/46 green; scan = 0 HIGH; fails-closed verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Self-audit of the free-tier lane found two gaps:
- Multi-file: the free-tier path generated for sourceFiles[0] only, silently
skipping the rest AND the paid path. Now it engages only for single-file
requests; multi-file falls back to the normal path so every file is covered.
(+1 regression test.)
- Exports/docs mismatch: the guide + README showed
`import ... from 'agentic-qe/routing/free-tier'`, but package.json had no
./routing export — the import would fail for npm consumers. Added the
./routing/free-tier subpath export.
Verified: project `tsc --noEmit` clean; 236 routing/coordinator/plugin tests +
18 routing-feedback tests green; no sibling breakage from the generic tracker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the cheap-first economics: local → repair → escalate the hard tail.
- coordinator: build a claudeRunner from the existing HybridRouter (tier →
complexity hint) and run the free-tier path with escalate:true. Falls back to
local-only automatically when no router is wired. patternsUsed now records the
winning tier.
- plugin (kernel layer): onInitialize constructs a live RoutingFeedbackCollector
(EMA calibration + auto-escalation, memory-only fallback) when the free tier is
opted in, and injects it as the coordinator's routingFeedback sink — so each
cheap-vs-escalated outcome drives the calibrator/escalation tracker/confidence
(lifts the stuck-at-40% routing metric). Built only when opted in.
- tests: +2 (escalation via router, D9 sink invocation); 113 green; existing
coordinator (47) and plugin (38) suites unchanged. Type-safe (MessageRole /
TaskComplexity / RoutingConfig fields all confirmed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apply the free-tier escalation work to the test-generation coordinator as an
OPT-IN, default-off capability (Ruv's cheap-first economics). Zero impact unless
enabled via `enableFreeTier` config or `AQE_FREE_TIER=1`.
- coordinator: guarded `tryFreeTierGeneration()` tries the free local model first
(reads source, prompts, verifies test+assertion), falls through to the
unchanged paid path on any miss. 47 existing coordinator tests still green.
New optional `routingFeedback?` ctor param wires D9.
- D8 repair loop: executor gains same-tier repair (`repairAttempts`, verifier
feedback fed back) + `escalate:false` repair-only mode. Coordinator path runs
local-only + repair, no paid escalation yet.
- D9 sink: createRoutingFeedbackSink() maps executor outcomes onto the existing
RoutingFeedbackCollector (calibrator + escalation + confidence); cheap wins
raise the cheap tier's confidence, lifting the stuck-at-40% routing metric.
- docs: user guide docs/guides/free-tier-local-models.md (local/cloud Ollama,
OpenRouter, OpenAI-compatible) linked from README.
- tests: +11 new (executor repair, sink, coordinator opt-in); 111 green total;
free-tier modules strict-tsc clean.
Model: qwen3:8b default. Keys read from named env vars, never stored.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Devil's-advocate review of today's branch surfaced three real gaps:
C1 (init-handler.ts): bare `aqe init --no-claude` (no --with-<platform>, no
--no-database) fell through to runStandardInit and installed the FULL Claude
surface it was meant to suppress — `noClaude` was only honored inside the
modular path. Add `options.claude === false` to the modular routing guard.
Verified: `aqe init --no-claude --minimal` now writes no .claude/, .mcp.json,
or CLAUDE.md (and the empty-install warning fires).
C2 (continuedev-installer.ts): the mergeExistingYamlConfig branch (existing
`mcpServers:` + --overwrite) appended a hardcoded AQE_MEMORY_PATH entry,
ignoring memoryBackend — re-introducing the #533 db leak in db-free installs
over an existing Continue config. Honor memoryBackend on this path too. Adds a
regression test.
H3 (entry.ts): main() ran fire-and-forget, so a startup rejection that escaped
its internal try hit the unhandledRejection handler (which intentionally keeps
the server alive at runtime) → on the in-process `agentic-qe mcp` path the CLI
wrapper would hang on its never-resolving promise with a zero exit. main() now
`.catch`es and exits non-zero on a failed startup. Re-verified #528/#535
integration tests still green (normal startup unaffected).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#533 — database-free mode now propagates AQE_MEMORY_BACKEND=memory to ALL
generated MCP-server configs, not just opencode.json. Previously `aqe init
--no-database` left .mcp.json (Claude Code) and every editor config pointed at
.agentic-qe/memory.db, so launching the server via any non-OpenCode client
recreated the db at runtime and defeated the db-free install.
- platform-config-generator.ts: generateMcpConfig(platformId, { memoryBackend })
emits AQE_MEMORY_BACKEND=memory and drops AQE_MEMORY_PATH for JSON/TOML/YAML;
covers the 8 shared-generator installers (cursor/cline/continuedev/kilocode/
roocode/windsurf/codex/copilot).
- 08-mcp.ts (.mcp.json) and kiro-installer.ts: same db-free env; learning/workers
not forced on in db-free mode (they persist to SQLite).
- 09-assets.ts threads memoryBackend into every platform installer.
Default (persistent) output is byte-for-byte unchanged.
#532 — new opt-in `--no-claude` suppresses the default Claude Code surface
(.claude/ skills+agents, .mcp.json, CLAUDE.md, governance, hooks) so
`--with-<platform>` flags become the only install targets — e.g. a clean
OpenCode-only install. Gated via shouldRun/run on phases 07-hooks, 08-mcp,
09-assets (core assets), 11-claude-md, 13-governance. Default install
(no flag) is unchanged; warns if --no-claude is passed with no platform.
Verified by real `aqe init` runs in temp dirs:
- `--no-database --with-codex --with-cursor`: .mcp.json/.cursor/.codex all emit
AQE_MEMORY_BACKEND=memory; no memory.db created.
- `--no-claude --with-opencode --no-database`: .opencode present; .claude/,
.mcp.json, CLAUDE.md all absent.
- default `--with-cursor`: full surface + persistent env intact (no regression).
Adds db-free + no-claude cases to platform-config-generator and mcp-phase tests;
all 406 init unit tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 'reworded prose' mutation test used .replace('qe_agent_definition',
'qe_agent_definition') — a no-op replacing a string with itself (CodeQL
js/identity-replacement). Replaced with a real non-invariant edit
(frontmatter description + a <notes> block after the closing tag) that
genuinely exercises 'edits outside required sections must not trip the
gate'. 9/9 tests still pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MCP-CLI parity bug found by a REAL protocol call during P5 acceptance:
the handler projected findings to a fixed 5-field shape, stripping
ADR-105's evidenceClass at the MCP boundary — unit tests passed because
they tested the projection itself. Projection now carries evidenceClass
+ evidenceArtifact per finding and the needsVerification queue; two
parity-pin tests added so the field can never be silently dropped again.
Verified: 9/9 handler tests; direct handler invocation (same code path
as the MCP tool) shows evidenceClass STATIC + needsVerification array.
Live MCP server reflects this after restart (long-running process has
pre-fix code loaded).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RUBRIC-v1.md committed first per ADR-108: ground-truth primary outcome
(hidden acceptance test), style-neutral verbatim judge instruction (no
treatment-revealing framing in either direction), two-pass cross-family
judging with INFERRED-labeled fallback, clustered-stats reporting rule.
Provably rubric-before-data: this commit precedes any results-* artifact.
2 scenarios with seeded defects + personas + hidden tests (off-by-one
pagination; swallowed async rejection). Hidden tests verified both
directions: FAIL on seeded bug with precise reasons, PASS on the fix.
lib/stats.ts encodes the three Pattern Space harness fixes as code:
isGoodRow enforced inside aggregate() (no path to a number that skips
it), seeded per-item counterbalance (their null cell was 72% position-
skewed), exact scenario-clustered sign test (validated against the
known 27/11 → p≈0.0136 re-analysis).
Verified: 14/14 stats tests; hidden-test ground truth both directions;
tsc clean. Remaining: live conversation driver + judge integration
(API-gated, same decision as ADR-106 live mode).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PatternNullStore: recordNull with ON CONFLICT consolidation,
getNullSummaries batch fetch, and the pure applyNullDiscount ranking
helper (context-matched nulls discount 5x harder than remote ones;
0.25 floor keeps patterns retrievable — nulls inform, never erase).
ExperienceCaptureService: appliedPatterns on TaskExperience (patterns
that GUIDED the task), setNullRecorder following the setWitnessChain
convention, and null capture in completeCapture — failure = negative
reward when a signal exists, else !success; recorder errors never break
capture; context fingerprint = domain:agent (v1, per ADR).
Verified: 11/11 tests run locally (London-school mocked backend — no
native deps) incl. reward-overrides-success, per-pattern fan-out,
recorder-throw survival, and discount floor/ordering; tsc clean.
Remaining for ADR-110: retrieval-time discount wiring in PatternStore
search, arena losing-strategy persistence, composition-root recorder
wiring.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Additive schema v10: qe_pattern_nulls (pattern_id FK, context_fingerprint,
failure_mode, trajectory_ref, evidence_class CHECK per ADR-105,
consolidated_count, UNIQUE(pattern_id, context_fingerprint)) + 2 indexes.
Wired into unified-memory v10 migration block; rollback refuses when
nulls exist (nulls are data — the un-committed v2.1 lesson).
Verified against a sqlite3-online-backup COPY of the real memory.db
(61MB, original never written): all 51 pre-existing table counts
byte-identical before/after (qe_patterns=298, qe_trajectories=377,
kv_store=5008), PRAGMA integrity_check ok, table+4 indexes created,
UNIQUE fires on duplicate, ON CONFLICT consolidation bumps count to 2.
tsc --noEmit clean. Unit tests (10) follow the repo's better-sqlite3
convention and run in CI — local native binding is the known macOS
binary (invalid ELF header).
Verification also caught a false-positive in its own first run: an
empty SQL pipe reported 'applied' with trivially-unchanged counts; the
new-objects query exposed it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Only EXECUTED/STATIC findings from failed blocking steps may block or
halt the pipeline. INFERRED findings queue in PipelineResult.
needsVerification (the ADR-102 adversarial-verification dispatch
surface) and downgrade overall to warn. CONJECTURE never gates. A
failed blocking step without gating evidence reads as warn, never as a
clean pass. Markdown report shows evidence class on blockers and a
'Needs Verification' section.
Verified: tsc clean; 8 new gating tests + 7 MCP handler tests, 15/15.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
aqe arena run: seeded tournaments where test strategies compete on REAL
fitness — built-in first-order operator mutants (string/comment/arrow-safe
enumeration) executed per strategy via node --test, real line coverage
from --experimental-test-coverage, and a deterministic suite-cost term.
Tournament emits a Wolfram-style pairwise competitive array; --evolve
hill-climbs the winner by seeded group flips with real re-evaluation.
Runs persist fail-soft to memory.db kv_store (namespace 'arena');
aqe arena list reads them back. Self-contained: no core changes, no new
dependencies; fixtures/arena-demo ships 4 test groups of varying kill
power over a branch-dense pricing module.
Reproducibility contract: same seed + fixture => byte-identical envelope
(verified E2E by diffing two runs), with informational.runtimesMs the
only excluded field. Honest finding during verification: the original
measured-runtime rank penalty was jitter-dominated at fixture scale and
flipped rankings between identical-seed runs — replaced with the
deterministic suite-size ratio (documented in ADR-104).
Phase 2 (documented): LLM-generated strategies frozen as artifacts,
winners distilled into qe-test-architect priors; MCP arena_run
registration (CLI and MCP share runArena() — parity by construction).
10 tests green (incl. real-engine reproducibility); tsc + eslint clean.
Part of #520 (improvement 7/7 — initiative complete).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Versioned, additive-only verdict envelopes for agent/MCP handoffs:
RiskDecision (quality gates), FindingVerdict (review findings with
adversarial refutations), CoverageGap (risk-weighted gaps).
- src/contracts/verdicts.ts: dependency-free validators (source of
truth) + mirrored draft-07 JSON Schemas; no zod/ajv runtime deps
(light-install posture preserved)
- quality_assess MCP boundary now attaches a validated riskDecision
envelope derived from the gate outcome (approve/block, escalate when
indeterminate) — additive, existing payload unchanged; direct and
wrapped handlers share the same mapToResult so MCP parity is inherent
- schemas/*.schema.json published via scripts/generate-verdict-schemas.mjs
for external ajv validation and workflow agent({schema}) enforcement
(consumed by improvement 5)
17 contract tests (goldens validate; mutations rejected with field-level
errors; builder always emits valid envelopes). E2E: envelope emitted by
the compiled boundary builder validates with stock ajv against the
published schema; mutated sample rejected. tsc + eslint clean.
Part of #520 (improvement 6/7, Tier 3).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Anthropic announced nested subagent support (depth=5) on 2026-06-09; the
runtime currently strips Task at parent->child spawn (upstream denylist,
empirically confirmed by our committed probe: level=1 status=NO_AGENT_TOOL,
matching ruflo ADR-147). This lands the readiness infrastructure that is
independently useful today and activates with zero changes when the gate
lifts:
- aqe hooks post-task gains --parent-agent-id / --depth (validated:
identifier charset, integer 0 <= d <= 32; depth=0 survives as 0);
provenance persisted to qe_trajectories.metadata_json (stage 1 — no
schema migration) and forwarded into ReasoningBank feedback so learning
can segment patterns per hierarchy level
- TaskExecution gains optional parentAgentId/depth (additive types)
- scripts/probe-nested-spawn-depth.mjs: single-shot empirical probe,
committed transcript under docs/probes/ — re-run after every
`claude update`; TASK_TOOL_LIVE is the signal for follow-up phases
- fresh-project fix found by the E2E: persistTaskOutcome assumed
captured_experiences/experience_applications exist, but they are
middleware-created — Stream B silently no-oped on fresh installs;
canonical DDL now ensured in the hook path
Deliberately deferred (documented in ADR-101 P1): tools: frontmatter
retrofit on shipped qe-*.md agents — Claude Code frontmatter is a
restrictive allowlist and would sever inherited MCP tool access;
needs maintainer sign-off + wildcard verification first.
8 unit tests green; E2E on a fresh temp project verified persistence,
rejections, and depth=0 boundary. tsc + eslint clean.
Part of #520 (improvement 4/7, Tier 2).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Breaks runaway retry loops: when the same Bash command fails consecutively
(normalized signature, cross-process via a small JSON sidecar under
.agentic-qe/), pre-command warns at 3 failures and emits a BLOCK
recommendation with a recovery hint at 5. Advisory by default;
AQE_STRICT_TOOL_LOOP=1 upgrades the block to a PreToolUse deny. A
successful run resets the breaker; an open breaker grants a half-open
probe after 2 minutes; stale entries pruned after 30 minutes.
Mirrors the ADR-064 domain-circuit-breaker semantics at command level.
Sidecar instead of memory.db: pre-command deliberately runs without a DB
open, and each hook call is a fresh process so in-process state is
impossible. Fail-open everywhere (corrupt state file => allow).
11 unit tests green; E2E verified with real aqe hooks pre/post-command:
warn after 3, BLOCK after 5, strict-mode deny envelope, reset on success.
Part of #520 (improvement 3/7, Tier 1 complete).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The enableCache flag existed since ADR-011 but was never read: every
advisor/LLM call paid full input price. Now:
- ClaudeProvider.generate() sends `system` as a content-block array with
cache_control {type: ephemeral} when enableCache (default on); plain
string when disabled. Anthropic-native provider only (ADR-092 — other
providers untouched).
- cache_creation_input_tokens / cache_read_input_tokens surfaced into
TokenUsage.cacheCreationTokens/cacheReadTokens; totalTokens now counts
the full prompt (input_tokens alone is the uncached remainder).
- CostTracker bills cache reads at 0.1x and writes at 1.25x input rate
instead of ignoring them.
5 new provider tests assert the serialized request shape (cache_control
present when enabled, absent when disabled, no system field when no
prompt) and the usage mapping. 64/64 LLM tests green, tsc clean.
Part of #520 (improvement 2/7, Tier 1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fable 5-era models emit large extended-thinking blocks inside task results
and step actions; embedding them verbatim contaminates the 384-dim pattern
vectors in memory.db and degrades HNSW retrieval.
- new scrubReasoningBlocks()/scrubReasoningDeep() (src/shared/reasoning-scrub.ts):
boundary-gated — only well-formed <think|thinking|reasoning|
REASONING_SCRATCHPAD>...</> pairs are removed; prose mentioning a tag
survives; clean input returned byte-identical
- applied at ingestion: TrajectoryTracker startTrajectory task,
recordStep action/result.data/result.error
- applied at distillation: ExperienceReplay.storeExperience task/strategy/
keyActions (insert + embedding text)
- applied at flow-forward: both init-time embedding backfills (legacy rows
cleaned as they get embedded — no memory.db rewrite) and retrieveSimilar
query embeddings (symmetric with stored vectors)
- 15 unit tests; E2E verified against a temp DB: trajectory + experience
rows persisted with all scratchpad blocks stripped
Part of #520 (improvement 1/7, Tier 1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- aqe memory/agent/task/team/pipeline CLI commands always failed with
"Fleet not initialized": autoInitialize() built kernel+queen in its own
CLIContext while the shared MCP handlers guard on the core-handlers
fleet-state singleton. New adoptExternalFleet() bridges the CLI's
components into that state (idempotent, never clobbers an existing
fleet), restoring MCP-CLI parity. Verified E2E: store/get/delete green.
- devcontainer: node_modules named-volume overlay + npm ci in postCreate
so container installs Linux natives without touching the host's macOS
binaries (takes effect on next rebuild; devcontainer users only)
- docs/setup: drop stale ruflo@3.5.18 pins (CLAUDE.md quick setup,
install-tools.sh) in favor of ruflo@3 + global `ruflo mcp start`
- docs: add Fable 5 / ruflo-parity improvement plan (tracked in #520)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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.
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>
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>
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>
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 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>
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>
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>
Two follow-up fixes after re-running the broader test suite (2064 tests):
1. tests/unit/shared/llm/router/hybrid-router.test.ts:619 — the
pre-existing test "should not retry non-retryable errors" was
asserting the OLD short-circuit-on-non-retryable behavior that
Fix#3 in the audit corrected. Updated to assert the new (correct)
semantics: non-retryable errors don't re-try the SAME provider but
DO fall through to the NEXT provider in the fallback chain.
Renamed accordingly. Both providers fail non-retryable so the
request still rejects, preserving the original "rejects" assertion.
2. README.md "LLM Providers" section was under-documented relative to
the wired implementation. Updated to:
- List all 7 ExtendedProviderType providers with their env var names
(including the GOOGLE_API_KEY alias added in Phase 1)
- Document `aqe llm config --set` persistence path
- Document the AQE_LLM_ROUTER_DISABLED kill-switch
- Mention that apiKey can't be saved via CLI (env-only)
- Drop Groq from the table since it's not actually in the supported
provider list
Verification
- npx tsc --noEmit: clean
- 2063/2063 unit + integration tests pass (1 skipped, no failures)
- Includes 38/38 hybrid-router tests after the assertion update