Per request: aqe init now installs the cross-platform Node hook shim into each
user project and routes hooks through it (instead of `npx agentic-qe hooks`).
- package.json: add `.claude/hooks` to `files` so aqe-hook.cjs ships in the npm
tarball (verified via `npm pack --dry-run`).
- 07-hooks.ts: new installHookShim() copies aqe-hook.cjs into the project's
.claude/hooks/ (source: node_modules/agentic-qe, then the package's own copy
resolved relative to the compiled module — works for npx too).
- 07-hooks.ts + init-wizard-hooks.ts: generated hooks now run
`node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" <cmd> ...`
(README/help examples left as `npx agentic-qe hooks` for manual use).
- aqe-hook.cjs: added an npx fallback (`npx -y --prefer-offline agentic-qe`)
so npx-only installs with no project-local bundle still work; disable with
AQE_HOOK_NPX=0 (tests use this for determinism). Header/scope updated (now a
shipped, init-copied file, not repo-internal).
Migration: isAqeHookEntry already matches the new `aqe-hook.cjs` command (via
the `\baqe\b` pattern) AND the old `npx agentic-qe hooks` form, so re-running
`aqe init` replaces a user's existing hooks with the shim form — no duplicates.
Verified end-to-end (real `aqe init --auto` in a temp project):
- aqe-hook.cjs copied into .claude/hooks/ ("Installed aqe-hook.cjs (copied)")
- all 11 generated hooks use `node .../aqe-hook.cjs <cmd>` (0 npx commands)
- post-task retains the #508 fix (--agent/--success true/--description)
- isAqeHookEntry matches new+old forms, not unrelated user hooks
- shim 7/7, settings-merge 17/17, task-hooks 5/5 tests pass; typecheck clean
A .sh shim is the least portable option — it forces a .cjs Windows twin and a
node_modules/.bin shell-wrapper spawn. Node is already a hard dependency of AQE
(CLI is Node >=20), so a single .cjs runs identically on Windows/macOS/Linux.
aqe-hook.cjs resolves a project-local bundle and runs it via process.execPath
(this node) — no shell, no .bin/.cmd wrapper:
1. node_modules/agentic-qe/dist/cli/bundle.js (installed dep — user case)
2. dist/cli/bundle.js (this source repo)
else no-op (exit 0). Swallows stderr; emits only the top-level JSON object.
Repo .claude/settings.json rewired: all 10 bundle hooks now invoke
. Deleted
aqe-hook.sh. Tests rewritten to run the .cjs via node; 7/7 pass incl. installed-
dep precedence + stderr-swallow. This removes the Windows blocker that stood
against ever shipping the shim to users.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shim hardcoded ${PROJECT}/dist/cli/bundle.js, which only exists in this
source repo — if ever copied into an installed project it would silently no-op.
Now resolves a PROJECT-LOCAL runner in priority order:
1. node_modules/.bin/aqe (installed project — the user case)
2. dist/cli/bundle.js (this source repo)
else no-op (exit 0, never block a turn)
No global-PATH / npx fallback by design (version drift + npx-per-fire crash).
Clarified in the header that the shim is REPO-INTERNAL: it is not in the npm
`files` list and `aqe init` does not copy it — user hooks are generated as
`npx agentic-qe hooks <cmd>` (cross-platform, npx-friendly). Portability here is
defense-in-depth, not a shipping decision.
Adds a regression test for branch-1 resolution + precedence (node_modules/.bin/aqe
preferred over dist). 6/6 shim tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The repo's own PostToolUse post-task hook still carried the broken
'--success --json' (no --agent) form (the #5 shim rewired it but kept the
args). The parser fix makes bare --success parse true, but it still recorded
agent='unknown'. Updated post-task to pass --agent/--success true/--description
and post-command to explicit --success true, matching the corrected init
templates. (post-edit's bare --success is correct — that option is a flag.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reported by @ciprianmelian. ADR-094 moved dream cycles to the kernel-side
DreamScheduler, but the dream-scheduler:hook-state kv row that SessionStart
reads is only INCREMENTED by the post-task hook (experienceCount++) and was
never reset by the entity that actually dreams. Confirmed in source: the kernel
scheduler had ZERO references to that key (it persists its own
'dream-scheduler:state' instead). Result: SessionStart perpetually showed
lastDreamTime:null and a pendingExperiences counter that only grew (e.g.
11 -> 1747 -> 6489), reading like a stuck scheduler while the daemon dreamt fine.
Fix: after a successful executeDream cycle, reconcileHookState() stamps the
truth on the SAME key + DEFAULT namespace the hook path uses:
{ lastDreamTime: now, experienceCount: 0, totalDreamsThisSession: prev+1,
sessionStartTime: preserved }. Best-effort (never breaks a dream cycle). Key
is a local literal mirroring DREAM_STATE_KEY to avoid a learning->cli layering
import.
Verified: new unit test seeds the stale row (experienceCount: 2720,
lastDreamTime: null), triggers a dream, asserts the row becomes
experienceCount:0, lastDreamTime: <iso string>, totalDreamsThisSession:1,
sessionStartTime preserved. 39 dream-scheduler tests pass; typecheck clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reported by @ciprianmelian. The aqe-init-generated PostToolUse hook ran
`post-task --task-id ... --success --json`, but --success was declared
`<bool>` (value REQUIRED), so Commander consumed --json as the success value:
options.success='--json' -> parsed false -> EVERY qe_trajectories /
captured_experiences row got success=0, the --json branch was swallowed, and no
--agent meant agent='unknown'. Net: Task trajectories were unpromotable
(distillation groups by agent/domain/success), so pattern count stayed pinned
near zero. (Confirmed in source; same class as the learning gaps #510 hardened.)
Fix (two layers):
1. Parser hardening — `--success <bool>` -> `--success [bool]` (optional value)
in task-hooks/command-hooks/routing-hooks, so a bare --success yields boolean
true and never eats the next token. Backward compatible: --success true/false
and the 'true' default all still parse identically.
2. Generated templates (07-hooks.ts + init-wizard-hooks.ts): post-task now emits
`--agent "$TOOL_INPUT_subagent_type" --success true --description
"$TOOL_INPUT_prompt" --json` (real agent attribution + explicit success +
pre-task bridge), and post-command emits explicit `--success true`.
Verified: isolated Commander proof — OLD <bool>: success="--json" (false);
NEW [bool]: success=true, --json fires. Plus 3 regression tests driving the EXACT
broken argv through registerTaskHooks: bare --success -> success=true, explicit
--success false -> false, --agent flows through. 5/5 task-hook tests pass
(incl. existing #449); typecheck clean. (Full post-task CLI run OOMs in Codespace
per CLAUDE.md — the parsing layer is the bug and is proven.)
NOTE for existing installs: re-run `aqe init` (or hand-edit the PostToolUse hook
in .claude/settings.json) to pick up the corrected command.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ADR-098 documents the durable behavioural decisions from the ruflo adoption
pass (#510): domain-score de-dilution (#2), EWC++ importance-weighted forgetting
(#6), contradiction suppression (#7), the two pre-existing bug fixes
(consecutive_failures, MCP -32601), and the explicit NEGLECTED decisions —
ruflo's Beta-priors (#2) and RaBitQ speedup (#8) did not transfer, and
multi-bundle packaging is deferred. AQE WH(Y) format, Accepted.
Marks the consecutive_failures (ae7c8b71) and MCP error-code (9b189115)
side-findings FIXED in the plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pre-existing bug surfaced by the qe-devils-advocate audit and live-reproduced:
handleToolsCall (and unknown-method / double-initialize) threw a PLAIN OBJECT
{ code: -32601, message }. The server safety net checked err instanceof Error
(false for a plain object) and surfaced it as a content payload
'Internal error: [object Object]', losing the -32601 code/message an MCP client
expects. The tool was correctly never executed; only the error shape was wrong.
Fix (matches existing OAuthProviderError convention): add McpError extends Error
{ code } + isJsonRpcCodedError guard in transport/stdio.ts (guard accepts both
McpError and the legacy plain {code,message} so nothing regresses). Convert the
three plain-object throws in protocol-server.ts to McpError; the safety net
re-throws coded errors (real protocol errors) while still swallowing genuinely
unexpected errors to keep the connection alive; the transport catch honors
err.code, falling back to INTERNAL_ERROR. Successful/known-tool calls unaffected.
Verified (real server boot via scripts/audit-mcp-tool-parity.mjs):
before: unknown tool rejected with 'Internal error: [object Object]'
after: unknown tool rejected with 'JSON-RPC error code=-32601'
Parity PASS (86 tools), typecheck clean, real .agentic-qe/aqe.rvf unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pre-existing bug surfaced by the qe-devils-advocate audit and confirmed:
checkDeprecation() reads (pattern as PatternWithDeprecation).consecutive_failures
to trigger the 'failures' deprecation reason, but rowToPattern() built the
QEPattern WITHOUT that column (and without deprecated_at), so getPattern()
always surfaced undefined->0 and the 'failures' branch NEVER fired — a pattern
that kept failing was never deprecated for failures.
Fix (surgical): rowToPattern now surfaces consecutive_failures and deprecated_at
(added to the PatternRow interface; returns a typed PatternWithDeprecation). No
thresholds or deprecation logic changed.
Verified: new tests assert a pattern with consecutive_failures==3 (== default
threshold), not stale, confidence above min, deprecates with reason 'failures';
below threshold does not. Regression-proven (reverting rowToPattern fails the
new test). 12 EWC tests pass; typecheck clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit (qe-devils-advocate) verdict: 10/10 items have real committed code + real
passing tests at claimed counts; production changes (#2/#6/#7) genuinely wired
into live paths; no real-store mutation; #8 honestly UNWIRED. Three fair
discrepancies, now corrected:
1. #2 had one TAUTOLOGICAL test (asserted (1/3)*0.4 > (1/11)*0.4 — arithmetic on
literals, not a code call). Rewritten to score a matching vs non-matching
agent through calculateAgentScores and assert the delta equals the CAPPED
domain term (~0.1333), which fails if the cap is removed. 4/4 still pass.
2. #2 status row softened to VERIFIED (PARTIAL): +5.6pp shipped, but the
structural Q-maturity / fixed-perf-term ceiling on confidence remains.
3. #10 status row corrected to PARTIAL: only the smoke contract was delivered;
the original multi-bundle 'subset packaging' scope is deferred.
No production code changed by this commit. Side-findings re-confirmed real
(dead consecutive_failures branch, MCP [object Object] error, RVF/SQLite drift).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports ruflo's 'smoke.sh as a plugin contract' discipline. AQE already ships
the marketplace.json + a lean auto-discovery plugin (plugins/agentic-qe-fleet:
11 agents, 9 commands, 9 skills, lean plugin.json), but had no machine-checkable
contract — so silent drift could ship a broken bundle.
plugins/agentic-qe-fleet/scripts/smoke.sh (npm run plugin:smoke) asserts:
- plugin.json valid JSON, name == agentic-qe-fleet, has a version;
- plugin.json does NOT declare skills/commands/agents arrays (Claude Code
auto-discovers them — declaring them is an error, per ruflo);
- marketplace.json indexes the plugin at ./plugins/agentic-qe-fleet;
- exact counts (11 agents / 9 commands / 9 skills), each with name+description
frontmatter / SKILL.md (bump the constants deliberately on change);
- README present; top-level plugin.json registers the agentic-qe MCP server.
Exit 0 = contract holds, 1 = any violation.
Verified: passes 10/10 checks (exit 0), and correctly FAILS (exit 1) when a
skill is removed ('skills: 8 (expected 9)').
Scope note: AQE ships ONE bundle today. Splitting the 93 QE skills into multiple
lean subset bundles (aqe-security, aqe-browser, ...) is a larger restructure of
shipped artifacts — deferred as a follow-up (needs a distribution decision); the
skills-manifest.json already has the category data to drive it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports ruflo ADR-077: seed the pattern store from the repo's OWN git history
(commits = success trajectories) through the SAME live-learning code path, so
a fresh project starts with a non-empty, project-specific pattern store instead
of only the generic PRETRAINED_PATTERNS.
scripts/pretrain-from-history.ts (npm run pretrain:history):
- git log --no-merges -> per commit: domain = deriveTaskType(subject),
success = subject !startsWith 'revert' (a revert is a failure signal);
- records each via persistTaskOutcome (the live captured_experiences +
experience_applications + Q-update path, honoring AQE_PROJECT_ROOT);
- then consolidateExperiencesToPatterns() distills them into qe_patterns.
- flags: --limit N (default 500), --dry-run (classify+histogram, write nothing),
--verbose. Extracted a pure classifyCommit() helper (19 unit tests).
Correctness fix found during verification (in the script, not prod): a brand-new
store only has core schema, so persistTaskOutcome failed with
'no such table: experience_applications'. The script now calls the canonical
ensureAllBrainTables() + initializeExperienceCapture() before recording — the
same tables production creates lazily.
Verified against a THROWAWAY AQE_PROJECT_ROOT temp store (data protection):
--limit 150 -> captured_experiences 0->150, qe_patterns 0->4, exit 0;
--dry-run -> nothing recorded (captured_experiences stays 0);
real .agentic-qe/*.rvf byte-identical (sha256) before/after; 19 tests pass.
Side-finding: AQE's real embedding (ONNX) costs ~2.3s/commit here vs ruflo's
~3ms/trajectory, so bootstrapping 500 commits takes minutes — logged as a perf
follow-up (embedding backend / batching).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports ruflo's perf-M4 (sign-random-projection + Hamming popcount). Adds
src/shared/utils/rabitq.ts: signSignature (384-bit packed into 12 Uint32
words, 48 bytes), hammingDistance (SWAR popcount), hammingSimilarity, and
topKBySignature (Hamming prefilter gated at N>=100 + exact-cosine rerank of a
recall-aware pool, so output equals exact topK when the true top-k fall in the
pool). 16 tests + scripts/benchmark-rabitq.ts (npm run benchmark:rabitq).
HONEST FINDING (verified, does NOT match ruflo's optimistic 2.7x/10.9x):
N=1000: speedup 1.31x, recall@10 100%, index 1.46MB -> 47KB (32x smaller)
N=5000: speedup 1.18x, recall@10 99.9%, 7.32MB -> 234KB (32x smaller)
The 32x INDEX-MEMORY reduction is real and unconditional. The SPEEDUP is only
~1.2-1.5x in JS, not ruflo's figure: a 1-bit sign signature in 384-dim is
coarse, so preserving recall@10 >= 0.95 needs a large exact-rerank pool
(~40-55% of N), which eats most of the prefilter win. ruflo's big win came
from replacing a LINEAR scan; AQE's main search is already native HNSW
(sublinear), so that win doesn't transfer. Tests pin this tradeoff (recall==1.0
with a sufficient pool; small pool sacrifices recall) so it can't be
mis-sold later.
NOT INTEGRATED — no zero-regression consumer exists:
- unified-memory-hnsw search() IS the ranking (no separate exact rerank to
prefilter), so Hamming-prefiltering its beam would drop true neighbors;
- coherence-gate-energy O(N^2) SUM and k-means clustering are numerically
sensitive (Hamming would change results).
Committed as a tested, benchmarked, ready-to-use utility (real memory lever for
patterns.rvf and a future prefilter->exact-rerank consumer), not wired into any
production path. No production code or .agentic-qe stores touched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports ruflo's hook-shim discipline. AQE's project hooks ran
`node dist/cli/bundle.js hooks <cmd> --json` directly, which printed init
diagnostics to STDOUT before the JSON on every SessionStart/UserPromptSubmit:
[hooks] CoherenceService initialized with WASM engines
[RvfPatternStore] Initialized: .agentic-qe/patterns.rvf (dim=384)
[RVF] Migration adapter active at stage 2 (dual-sqlite)
[hooks] System initialized
polluting the Claude Code transcript on every turn, and a crash/slow init could
surface a hook error mid-turn.
New .claude/hooks/aqe-hook.sh wraps every bundle hook with the ruflo contract:
* prefer the LOCAL built bundle (no npx / network);
* no-op if the bundle isn't built (never block a turn);
* swallow stderr;
* emit ONLY the top-level JSON object on stdout (sed '/^{/,/^}/p' drops both
leading init noise AND async daemon log lines printed after the JSON, so the
--json contract stays parseable);
* ALWAYS exit 0.
All 10 bundle hook commands in .claude/settings.json rewired to the shim
(diff touches only those 10 command strings).
Verified with real commands: route/session-start/guard via the exact settings
invocation -> exit 0, 0 noise lines, valid JSON; missing-bundle and crashing-
bundle -> exit 0, empty stdout, no stderr leak. 5 regression tests (resilience +
deterministic noise-stripping via a fake bundle) pass in ~120ms.
Note: Windows .cjs parity (ruflo also ships one) is a separate follow-up — the
entire AQE hook config is POSIX `sh -c`, so a .cjs twin needs the settings
rewrite too. Observed side concern: the route/session-start hooks cold-start the
full system (~30-60s) on each fire — logged for a perf follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports ruflo's #1874 discipline. Replaces the FAKE mcp:validate theater
(echo 'All tools registered' && exit 0) with a real smoke that boots the
actual MCP server (tsx src/mcp/entry.ts) over stdio and validates the
initialize + tools/list handshake against the MCP SPEC, not the code:
- protocolVersion is a YYYY-MM-DD string (the core ruflo #1874 guard — an
object here is what Claude Code's Zod rejects)
- capabilities is an object; serverInfo.name/version are non-empty strings
- tools/list returns a non-empty array, each entry name + object inputSchema
scripts/audit-mcp-tool-parity.mjs is the AQE-equivalent of ruflo's CLI<->MCP
parity audit (the CLI has no callMCPTool('name') string pattern, so the
meaningful surface is advertisement<->handler parity). Drives the advertised
set from a live tools/list, diffs a monotone baseline
(verification/mcp-tool-parity-baseline.json, 86 tools), fails only on NEW
discrepancies (removed tool = regression), and probes that unknown-tool
tools/call is rejected. --update-baseline to re-baseline.
Both scripts spawn the server against an isolated AQE_PROJECT_ROOT temp dir
(data protection: real .agentic-qe/*.rvf proven byte-identical before/after)
and kill the whole detached process group on exit (no leaked servers).
Verified: npm run mcp:validate -> PASS (protocolVersion 2025-11-25, 86 tools);
mcp:parity -> PASS (0 removed, 0 added). Both exit 0, real stores untouched.
Side-finding (pre-existing, not fixed here): handleToolsCall throws a plain
object for unknown tools, which escapes the request-handler safety net as
'Internal error: [object Object]' instead of a proper JSON-RPC -32601. Logged
for a dedicated fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AQE's Phase-1 consolidation merged near-duplicate experiences by cosine
similarity, but two experiences with the SAME context and OPPOSITE outcomes
(large quality delta) are a conflict, not a duplicate. Merging silently
absorbed the conflict; worse, a *used* low-quality contradictory experience was
never merged (merge requires application_count===0) and stayed in retrieval,
poisoning recall.
Now, when similarity >= contradictionSimilarityThreshold (0.85) AND the quality
delta >= contradictionQualityDelta (0.4), the pair is treated as a contradiction:
the lower-quality loser is SUPPRESSED from retrieval (consolidated_into =
'contradicted', excluded by the IS NULL filter) instead of merged — even if it
was applied. The survivor is NOT boosted (a conflict is not a duplicate). The
action is logged distinctly ('contradiction'). New count surfaced as
ConsolidationResult.contradicted. Both thresholds are configurable.
PRODUCTION IMPACT: affects the dream/consolidation merge phase. Genuine
near-duplicates still merge exactly as before; only same-context/opposite-outcome
pairs are newly suppressed (soft, reversible — consolidated_into can be cleared).
Verified with 6 tests against an in-memory DB matching the production
captured_experiences schema (suppress loser, suppress even-when-applied, no
survivor boost, near-duplicate still merges, dissimilar pair untouched, distinct
log action). 13 total consolidation tests pass; typecheck clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Confidence decay (applyConfidenceDecay) and stale/age deprecation
(checkDeprecation) were uniform — a proven high-success pattern was forgotten
at the same rate as noise, so an influx of new low-value patterns could crowd
out historically valuable ones (catastrophic forgetting).
Inspired by Elastic Weight Consolidation, high-IMPORTANCE patterns now resist
forgetting, weighted by an importance proxy = clamped success_rate gated on
usage_count >= EWC_MIN_USAGE_FOR_PROTECTION (recency already implied: only
unused patterns decay/go stale). Honest framing (as in ruflo's EWC adapter):
this is a heuristic, NOT true Fisher information — no model gradients exist here.
- Decay: per-row factor shielded by EWC_DECAY_PROTECTION*importance. A
fully-proven pattern decays up to 10x slower; importance-0 patterns decay
EXACTLY as before (no regression — locked in by test).
- Stale: effective window = staleDays * (1 + importance*EWC_STALE_PROTECTION_BONUS),
so a proven pattern unused 30d isn't deprecated like noise.
Protection covers ONLY stale/age/decay forgetting. A pattern that drops below
minActiveConfidence (or starts failing) is STILL deprecated — we blend
retention, we don't permanently shield dead weight. All three EWC_* knobs are
exported constants; set to 0 to restore exact prior behavior (reversible).
PRODUCTION IMPACT: affects the dream/consolidation lifecycle. Low/zero-importance
patterns behave identically; high-success patterns persist longer. Verified with
10 tests against an in-memory DB matching the production qe_patterns schema
(decay deltas, stale window, no-regression, dead-weight still removed). 46 total
lifecycle tests pass.
Side-finding (pre-existing, NOT this change): checkDeprecation reads
consecutive_failures via a cast, but rowToPattern doesn't surface that column,
so the 'failures' deprecation branch never fires through getPattern. Logged for
a dedicated fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Routing confidence (QERoutingResult.confidence) is the top agent's combined
static score from calculateAgentScores. The domain term was
(domainMatch / detectedDomains.length) * 0.4, but AQE's domain detector emits
7-12 domains per task, so a perfect single-domain match scored (1/11)*0.4 ~ 0.04
— relevance washed out by detector breadth, leaving confidence dominated by the
fixed performance term.
Fix: cap the denominator at DOMAIN_DENOM_CAP (3), modeling the few truly-relevant
domains a task has. Precise detection (1-2 domains) is unchanged; broad detection
no longer dilutes a real match below usefulness. Proportionality preserved.
Tunable via the constant + the existing routingWeights.similarity.
PRODUCTION IMPACT: shifts every routing decision's confidence telemetry upward
for broad-domain tasks and strengthens domain relevance vs the fixed performance
term in ranking. Reversible (raise DOMAIN_DENOM_CAP toward old behavior).
Verified with real `aqe hooks pre-task` routes (not mocks):
broad multi-domain task: 27.7% -> 33.3% (+5.6pp); narrow tasks unchanged
(test 37.3%, security 30.3%); recommendations/ranking preserved.
77 existing routing tests pass; +4 regression tests lock in de-dilution.
NOTE: ruflo ADR-142 (per-bucket Beta priors) did NOT apply — AQE confidence is
not a Beta-bandit; root cause was this dilution. See docs/ruflo-adoption-plan.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Investigation (verified with real `aqe hooks pre-task` routes) shows AQE's
40% routing confidence is NOT a Beta-bandit prior, so ruflo ADR-142's
per-bucket fix does not apply. Confidence is the top agent's static combined
score from calculateAgentScores (agent-routing.ts) blended with an immature
Q-table (qWeight=0). Real root cause: domain-score dilution —
domainScore = (domainMatch / detectedDomains.length) * 0.4 — with the domain
detector returning 7-12 domains, washing out relevance. Measured easy=37.3%,
hard=20.5%. Proposed scoped fix to calculateAgentScores is a published
hot-path change → deferred pending user sign-off (no production code touched).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports ruflo's benchmark-self-learning.mjs discipline (the 'reports success
but persists nothing' detector) to AQE. Proves the learning stores actually
persist what they claim, that stat aggregators agree with the underlying
index, that nothing writes accidentally, and that data survives restart.
Sections (each isolated in a temp dir, each gating):
A store persistence — N creates => totalPatterns delta == N
B outcome learning — N outcomes => learningOutcomes delta == N, successRate matches fed ratio
C negative control — read-only ops => 0 delta (no accidental writes)
D restart survival — dispose + reopen on same temp path => counts persist
E consistency — RvfPatternStore.getStats == adapter.totalVectors == bank.totalPatterns
F audit-real — copies real stores read-only, checks banner numbers round-trip
Data protection (CLAUDE.md): synthetic mode uses fresh temp dirs; audit-real
copies stores and opens only copies. Verified real .rvf stores byte-identical
(mtime+size+sha256) before/after a run.
Audit-real finding on the live store: banner numbers corroborated
(72.5% success, 39.7% confidence, 2131 routings) BUT RVF vector count (338)
!= SQLite qe_patterns rows (345), a ~7-pattern index/table drift the banner
hides — exactly the divergence this harness exists to surface (reported as a
diagnostic note, non-gating). Synthetic sections A-E pass; exit 1 on failure.
npm run benchmark:self-learning
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audited AQE routing against two router bugs ruflo fixed 2026-05-29:
- #2229 stale route-cache: NOT PRESENT — Q-values read live from SQLite
(qe-reasoning-bank buildQValueLookup) and in-memory qTable; no cached
routing decision exists to go stale, so no invalidation needed.
- #2239 state-encoder truncation: NOT PRESENT — state keys are plain
string joins (agent-routing buildRoutingStateKey: taskType|priority|domain),
no bit-folded quantized vector, no mask. Distinct keyword tasks map to
distinct states.
Adds a guard test locking in non-collapsing state encoding so a future
refactor toward a bit-folding encoder fails here. 4 tests passing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan to port highest-value mechanisms from ruvnet/ruflo's last month
(307 commits, ADR-120 to ADR-143) into AQE: self-learning verification
harness, per-bucket bandit priors, router bug audit, MCP smoke layer,
EWC++ forgetting protection, and 9 more items across 3 tiers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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
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>
Plumbs the kernel-singleton HybridRouter (Phase 1) into every domain
plugin → coordinator → service that has an ADR-051 LLM-enhancement
branch, so isLLMAnalysisAvailable() finally returns true in production
instead of being unreachable dead code.
What changed (12 domains, same surgical pattern in each)
- plugin.ts: factory accepts `llmRouter?: HybridRouter` as a new last
arg; class constructor stores it as `private readonly`; onInitialize
forwards to the coordinator.
- coordinator.ts: constructor accepts `llmRouter?: HybridRouter`;
service construction switches from legacy `(memory)` form to the
dependency-bag `{ memory, llmRouter }` form that 14 service classes
already supported but no caller used.
- kernel.ts: each DOMAIN_FACTORIES wrapper updated from
`(eb, m, c, _llmRouter) => createXxxPlugin(eb, m, c)` to
`(eb, m, c, llmRouter) => createXxxPlugin(eb, m, c, undefined, llmRouter)`
so the kernel singleton actually reaches the plugin layer.
Domains wired
- test-execution → TestExecutorService
- test-generation → TestGeneratorService (via createTestGeneratorServiceWithDependencies)
- coverage-analysis → CoverageAnalyzerService + GapDetectorService
- quality-assessment → QualityAnalyzerService + DeploymentAdvisorService
- security-compliance → SecurityScannerService → SASTScanner (sub-component)
- contract-testing → ContractValidatorService
- chaos-resilience → ChaosEngineerService
- requirements-validation → RequirementsValidatorService
- code-intelligence → KnowledgeGraphService
- defect-intelligence → DefectPredictorService + RootCauseAnalyzerService
- learning-optimization → LearningCoordinatorService
- visual-accessibility → VisualTesterService (via createVisualTesterServiceWithDependencies)
Domains NOT wired (intentional)
- enterprise-integration: no LLM-enhanced services in its surface
- coordination: cross-cutting plugin, no LLM call sites
Their kernel wrappers stay at `_llmRouter` discard.
Behavior change
- Before this commit: a fleet booted with provider keys in env still
ran zero LLM calls from the swarm path — every `isLLMAnalysisAvailable()`
returned false because llmRouter was undefined in every service.
- After this commit: the same fleet routes LLM-enhanced branches
through Phase 1's HybridRouter. Setting GOOGLE_API_KEY +
`aqe llm config --set defaultProvider=gemini` now actually changes
which provider sees the tokens.
Verification
- `npx tsc --noEmit -p tsconfig.json`: clean
- 191 tests pass across 7 files including:
* 34/34 existing kernel.test.ts (no regression)
* 22/22 existing test-generation coordinator tests (no regression)
* 40/40 gemini.test.ts (Phase 1 alias)
* 27/27 cli/commands/llm-router.test.ts (Phase 1 persistence)
* 29/29 config-store.test.ts
* 10/10 llm-router-service.test.ts
* 4/4 kernel-llm-router.test.ts
- Phase 4 will add mock-provider integration tests per-domain proving
the analyzeXxxWithLLM branches actually fire end-to-end.
Refs ADR-043 (vendor-independent LLM), ADR-051 (LLM-enhanced services)
Builds on 94033e3 (Phase 1 — kernel singleton + persistence)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wires the foundation for ADR-043's HybridRouter into the kernel boot
sequence so domain plugins can finally receive an llmRouter dependency.
Phase 1 lands the kernel/CLI/provider changes; Phase 2 will plumb the
router through all 13 domains.
Background: the LLM-enhancement code paths added in ADR-051
(isLLMAnalysisAvailable() across 14 services) were unreachable because
no construction site ever passed an llmRouter — services accepted the
optional dependency but no caller injected it. ProviderManager +
HybridRouter were only instantiated from the `aqe llm advise` CLI
subcommand.
Changes
- src/shared/llm/providers/gemini.ts: read GOOGLE_API_KEY as a third
fallback after GOOGLE_AI_API_KEY and GEMINI_API_KEY, so users with
the conventional Google key name don't need to alias it themselves.
- src/shared/llm/router/config-store.ts (new): load/save
.agentic-qe/llm-config.json with env-detection layering
(defaults < disk < env < explicit override). Strips apiKey fields
defensively before persisting. Filters out providers the
ProviderManager doesn't yet construct (onnx) to keep boot logs
clean.
- src/shared/llm/llm-router-service.ts (new): builds a HybridRouter
from the loaded config, picks primary/fallbacks from the available
set honoring the user's defaultProvider when possible. Accepts an
injected ProviderManager for tests.
- src/kernel/interfaces.ts: extends QEKernel with llmRouter? and
KernelConfig with llmRouter.enabled ('auto'|true|false) +
configOverride + providerManager test-injection.
- src/kernel/kernel.ts: kernel-singleton llmRouter built during
initialize(), exposed via kernel.llmRouter, forwarded to all 13
DOMAIN_FACTORIES as a 4th arg. Phase 2 wires each factory through
to its coordinator/services; for now the wrappers accept-and-discard.
- src/cli/commands/llm-router.ts: replaces the
`// would be persisted in real implementation` stub with real
read/write through the config-store. `aqe llm config --set` now
survives a process restart. Adds AQE_CONFIG_ROOT env var as a test
seam so tests don't mutate the real .agentic-qe/.
Tests (110 total, all passing)
- gemini.test.ts: +3 new tests for each env-var alias, existing
tests updated to clear all three names
- config-store.test.ts (new, 29 tests): load/save/merge precedence,
env detection, secret stripping, atomicity
- llm-router-service.test.ts (new, 10 tests): pickEnabledProviders,
pickPrimaryAndFallbacks, end-to-end with injected mock provider
- kernel-llm-router.test.ts (new, 4 tests): kernel boot with router
enabled=false / auto-no-providers / injected mock, chat() round-trip
- llm-router.test.ts (CLI): +2 persistence-to-disk tests, env-isolated
- tests/mocks/index.ts: shared createMockLLMProvider() helper
Verification
- npx tsc --noEmit: clean
- 110 tests pass; no regressions in existing kernel.test.ts (34/34)
- Stderr noise from PersistentSONAEngine init is pre-existing and
unrelated to this change.
What this commit does NOT do
- Does NOT change behavior for any user yet — DOMAIN_FACTORIES
wrappers still discard the llmRouter param. Phase 2 lights up the
domain services.
Refs ADR-043 (vendor-independent LLM), ADR-051 (LLM-enhanced services)
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>