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>
Switch the cross-project knowledge hub from the cloud endpoint to a
locally-runnable Nagual instance, and add an optional local LLM judge
for scoring pattern quality.
- nagual-client.ts: REST client for a local `nagual serve` instance
(NAGUAL_URL, default http://localhost:3333); search/recordOutcome/
promotePattern are all fire-and-forget safe.
- local-judge-client.ts: Ollama-compatible judge (NAGUAL_JUDGE_URL/MODEL)
blending 0.7*outcome + 0.3*judge_score, with an ambiguous-zone review flag.
- task-hooks.ts: pre-task surfaces Nagual cross-project context for
complex tasks in relevant QE domains (best-effort, gated on env + length).
- settings.json: add NAGUAL_JUDGE_URL/MODEL; fix invalid mcp allow-rule
globs (mcp__ruflo__* / mcp__claude-flow__*) and add Bash(rm -rf:*) deny.
- docs: de-reference the cloud hub in favor of a local instance.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The #511 fix makes init's code-intelligence pre-scan actually parse AQE's own
TypeScript source (tree-sitter, no typescript dep) and persist the graph, so the
self-dogfood init now produces ~35k KG entries (was ~27k) and takes ~182s (was
~60s). Bumped expectedKgEntries 27000->34000, expectedElapsedSec 60->80 (soft cap
240s), timeoutSec 240->300. Follow-up: batch KG persistence writes to cut init time.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI npm ci failed with ERESOLVE — v3.2.0 peers @typescript-eslint/eslint-plugin
6-7, but the project is on v8. v4.x peers ^8 (and keeps ESLint 8 support), so it
resolves cleanly. Worked locally only because it was installed with
--legacy-peer-deps; npm ci is strict.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code-intelligence TS/JS via bundled tree-sitter WASM (no typescript dep, #511) +
fix for MCP domain tools hanging on fresh install. Also fixes the release skill's
repo path (agentic-qe-new -> agentic-qe).
The committed grammars for these 5 languages were built with the pre-0.25
emscripten "side module" format and fail to load with web-tree-sitter 0.26
(getDylinkMetadata error) — the multi-language parser was silently falling back
to regex for ALL of them. Rebuilt with tree-sitter-cli 0.26.9 (wasi-sdk) via
scripts/build-grammars.sh.
Verified: the WASM parser test suite now loads every grammar with ZERO
"Failed to load WASM grammar" fallbacks (was one per parse); 117 parser tests pass.
Also: build-grammars.sh installs grammar source packages with --legacy-peer-deps
(they declare conflicting tree-sitter binding peers, irrelevant since we only
consume their C source) so `build-grammars.sh all` resolves cleanly.
Code-intelligence (index/deps/impact/search) required the multi-megabyte
`typescript` compiler at runtime; without it every TS/JS file errored
("TypeScript is required for code analysis") and produced 0 nodes/edges — i.e.
the feature was broken out of the box for the entire JS/TS ecosystem.
Bundle tree-sitter WASM grammars for typescript/tsx/javascript and extract
entities/imports from them instead:
- assets/grammars/tree-sitter-{typescript,tsx,javascript}.wasm (shipped via
package `files: assets/**`).
- treesitter-ts-extractor.ts: extracts functions (incl. async arrows), classes +
methods (with visibility), interfaces, and imports (incl. require()).
- knowledge-graph.ts: TS/JS extraction now prefers tree-sitter (typescript-free),
falling back to the TypeScript compiler API only if tree-sitter is unavailable
AND `typescript` happens to be installed.
- tree-sitter-wasm-parser.ts: export loadWasmGrammar() reusing the proven WASM
init + grammar-path resolution.
Built with tree-sitter-cli 0.26.9 (matching web-tree-sitter ~0.26) via the new
scripts/build-grammars.sh (docker + emscripten/emsdk; reproducible — byte
identical across runs). Grammars built with the pre-0.25 emscripten dylink format
do NOT load with web-tree-sitter 0.26 (getDylinkMetadata error), which is why the
build pins the toolchain.
Verified on a real fresh project with NO typescript installed: code index → 8
nodes / 7 edges, deps → 2 connecting edges, search → 3 results. 232
code-intelligence tests + new extractor unit tests pass.
Note: the pre-existing assets/grammars/*.wasm for python/java/c#/rust/swift were
built with the old toolchain and currently fail to load with web-tree-sitter
0.26 (silently falling back to regex). They can be rebuilt with
`scripts/build-grammars.sh all` — tracked as a follow-up.
Defense-in-depth after the QEReasoningBank init recursion. Pattern search + model
routing in createDomainHandler are optional hints, so they must never block a
tool call. Wrap routeDomainTask() in withTimeout (ROUTE_ADVISORY_TIMEOUT_MS,
default 5s, env-overridable): on timeout the tool proceeds with null routing
(mapToPayload already tolerates null). Also cache the learning engine only AFTER
a successful init, so a failed/slow init no longer serves a half-initialized
engine on the next call.
Adds unit tests for withTimeout (fallback on stall, real value when fast, fallback
on reject). MCP code_index still returns normally.
Root cause: initialize() seeds pretrained patterns via loadPretrainedPatterns() →
storePattern(), and storePattern() ensure-inits with
`if (!this.initialized) await this.initialize()`. Because `this.initialized = true`
was set AFTER seeding, the seed re-entered initialize() without bound — an async
self-recursion that pinned a CPU core and never returned.
Impact: EVERY MCP domain tool (code_index, quality_assess, defect_predict, …)
routes through the advisory pattern-search path (createDomainHandler →
routeDomainTask → searchPatternsForTask → getLearningEngine → AQELearningEngine →
QEReasoningBank.initialize). On a fresh install (patterns seeded for the first
time) that path recursed forever, so all MCP domain tools hung indefinitely while
the CLI — which calls domain services directly — worked. The repo and in-memory
unit tests never hit it (already seeded / bulk-load path).
Fix: set `this.initialized = true` BEFORE loadPretrainedPatterns()/seeding. The
core stores (patternStore + SQLite) are already up by then, so storePattern()
short-circuits instead of recursing.
Verified against a real tarball install: MCP code_index ∞-hang → 0.5s
(6 symbols / 5 relations); quality_assess / defect_predict 0.8s; returns cleanly
(0/0/0) when typescript is absent too. Adds a red-green regression test pinning
the init-order invariant.
Repo-health cleanup from an external code audit:
- eslint: ignore src/_archived (already excluded by tsconfig, so linting it only
produced 6 parserOptions.project errors).
- Align the architecture claim: src/index.ts and docs said "12"/"6" bounded
contexts; there are 13 domain dirs (matching package.json). Updated src/index.ts
(comment + ARCHITECTURE const) and the docs list to the actual 13.
- Apply stock `eslint --fix` (prefer-const, prefer-as-const, no-useless-escape,
no-regex-spaces) across 24 files.
Lint problems: 1072 -> 1031. Typecheck and build pass; no behavioral changes.
Resolves the two remaining gaps from the audit.
Namespace divergence (CLI↔MCP parity):
KnowledgeGraphService persisted nodes/edges under the backend's DEFAULT
namespace — 'qe-kernel' for the CLI kernel, 'mcp-tools' for the MCP shared
backend — so a graph indexed by one tool was invisible to the other, and the
init existence check (`SELECT ... FROM kv_store WHERE namespace =
'code-intelligence:kg'`) always returned 0 because the data was elsewhere.
Now every KG kv op passes an explicit `{ namespace: 'code-intelligence:kg' }`
(via a single `nsOpts` getter so reads and writes can't drift apart — issue
#491 Bug 2). The two external readers that searched the default namespace by key
prefix (impact-analyzer test discovery, test-generator KG-context) now read that
namespace too. Verified: both `aqe code index` and the MCP `code_index` tool
land KG rows in `code-intelligence:kg`; deps/search still work; the init
`hasCodeIntelligenceIndex` check now finds the index.
FileReader stale content on incremental re-index:
indexFile() now evicts the file from the FileReader content cache before reading
it. The reader caches content for 5 min, so a re-index in a long-lived process
(the MCP server) could otherwise extract from pre-edit text. The two reads
within indexFile (entities + imports) still share one cache fill, so they stay
consistent. The incremental regression test now re-indexes on the SAME instance,
exercising both removeFileFromGraph and the cache eviction.
Adds a regression test asserting KG data is stored under 'code-intelligence:kg'
regardless of the backend's default namespace.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses the gaps a devil's-advocate audit surfaced in the #511 fixes:
1. Fail-loud warning (issue request #2): `index` now records how many files
were indexed under a language with no dependency extractor and returns them
as `IndexResult.warnings` (logged + printed by the CLI), so a downstream
0-edge `deps` result is explained rather than silently misleading.
2. Incremental stale state: `indexFile(..., incremental)` now calls
removeFileFromGraph() to delete a file's prior node/entity/outgoing-edge
keys before re-indexing, so `index --incremental` prunes removed imports and
renamed entities instead of leaving phantom dependencies. (Note: within a
single long-lived process the FileReader content cache can still serve stale
text; the CLI re-indexes in a fresh process so this is not user-visible.)
3. MCP parity: the `index-code` task handler emitted `nodesCreated`/`edgesCreated`
but the MCP result mapper reads `symbolsExtracted`/`relationsFound`, so
`code_index` always reported 0/0. Map the KG counts to those fields and pass
`warnings` through. Verified via a real MCP protocol call: filesIndexed 2,
symbolsExtracted 5, relationsFound 4 (was 0/0).
Adds regression tests: unsupported-language warning, incremental stale-edge
removal (fresh-instance, mirroring the CLI), and the MCP field mapping.
Known limitation (documented, not changed here): CLI persists the graph under
the 'qe-kernel' namespace and MCP under 'mcp-tools', so a graph indexed by one
tool is not read by the other. MCP exposes only `code_index` (no deps/search),
so this has no MCP-internal reader; reconciling the namespaces is entangled with
contradictory init/test-generation expectations and needs its own change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
indexCode() stored retrievable content with a {namespace} store option, but
the key is already fully namespace-qualified and every read path
(semanticSearch/findSimilar/exact/fuzzy) plus the storeVector() embedding write
use the default namespace. On a real backend that honors the option, the
content written with {namespace} could not be read back without it (issue #491
Bug 2), so each matched vector's content fetch returned undefined and the result
was silently dropped — search returned 0 results for obviously-present terms.
Fix: drop the redundant {namespace} store option on the content write so it
round-trips with the reads.
Verified via CLI: `code search "calculator compute"` now returns the indexed
files (was 0). Adds a regression test using a namespace-honoring mock backend
(the existing mock ignored the option, which is why it never caught this); the
test fails with the option reintroduced and passes without it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`aqe code index` and `aqe code deps`/`search` run as separate CLI processes,
but the knowledge graph stored nodes/edges in-memory only ("cache-only"), so a
fresh `deps` process always saw an empty graph and reported 0 edges / degree-0
nodes — even though `index` reported edges created. This affected every
language; TS was incidental (the issue's "missing TS parser" theory is a red
herring — the TS Compiler API path is wired and does extract).
Fixes:
- Persist nodes/edges to memory.db in storeNode/createEdge; getEdges hydrates a
correct flat edge cache (the old source-keyed index could not serve
incoming/both lookups); clear() deletes persisted KG :node:/:edge: keys only.
- resolveImportPath(): resolve relative import specifiers to real files with
extension probing so file->file import edges connect (were dangling at
pathToNodeId('./math')).
- traverseDependencies: restrict to true dependency edge types
(import/call/extends/implements) and recurse directed-forward only, which
removes phantom A->B->A cycles surfaced once edges actually existed.
Verified via CLI: index -> 6 edges; fresh-process deps -> 2 connecting edges,
0 cycles (was 0 edges). Adds cross-instance regression tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per request: aqe init now installs the cross-platform Node hook shim into each
user project and routes hooks through it (instead of `npx agentic-qe hooks`).
- package.json: add `.claude/hooks` to `files` so aqe-hook.cjs ships in the npm
tarball (verified via `npm pack --dry-run`).
- 07-hooks.ts: new installHookShim() copies aqe-hook.cjs into the project's
.claude/hooks/ (source: node_modules/agentic-qe, then the package's own copy
resolved relative to the compiled module — works for npx too).
- 07-hooks.ts + init-wizard-hooks.ts: generated hooks now run
`node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" <cmd> ...`
(README/help examples left as `npx agentic-qe hooks` for manual use).
- aqe-hook.cjs: added an npx fallback (`npx -y --prefer-offline agentic-qe`)
so npx-only installs with no project-local bundle still work; disable with
AQE_HOOK_NPX=0 (tests use this for determinism). Header/scope updated (now a
shipped, init-copied file, not repo-internal).
Migration: isAqeHookEntry already matches the new `aqe-hook.cjs` command (via
the `\baqe\b` pattern) AND the old `npx agentic-qe hooks` form, so re-running
`aqe init` replaces a user's existing hooks with the shim form — no duplicates.
Verified end-to-end (real `aqe init --auto` in a temp project):
- aqe-hook.cjs copied into .claude/hooks/ ("Installed aqe-hook.cjs (copied)")
- all 11 generated hooks use `node .../aqe-hook.cjs <cmd>` (0 npx commands)
- post-task retains the #508 fix (--agent/--success true/--description)
- isAqeHookEntry matches new+old forms, not unrelated user hooks
- shim 7/7, settings-merge 17/17, task-hooks 5/5 tests pass; typecheck clean
A .sh shim is the least portable option — it forces a .cjs Windows twin and a
node_modules/.bin shell-wrapper spawn. Node is already a hard dependency of AQE
(CLI is Node >=20), so a single .cjs runs identically on Windows/macOS/Linux.
aqe-hook.cjs resolves a project-local bundle and runs it via process.execPath
(this node) — no shell, no .bin/.cmd wrapper:
1. node_modules/agentic-qe/dist/cli/bundle.js (installed dep — user case)
2. dist/cli/bundle.js (this source repo)
else no-op (exit 0). Swallows stderr; emits only the top-level JSON object.
Repo .claude/settings.json rewired: all 10 bundle hooks now invoke
. Deleted
aqe-hook.sh. Tests rewritten to run the .cjs via node; 7/7 pass incl. installed-
dep precedence + stderr-swallow. This removes the Windows blocker that stood
against ever shipping the shim to users.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shim hardcoded ${PROJECT}/dist/cli/bundle.js, which only exists in this
source repo — if ever copied into an installed project it would silently no-op.
Now resolves a PROJECT-LOCAL runner in priority order:
1. node_modules/.bin/aqe (installed project — the user case)
2. dist/cli/bundle.js (this source repo)
else no-op (exit 0, never block a turn)
No global-PATH / npx fallback by design (version drift + npx-per-fire crash).
Clarified in the header that the shim is REPO-INTERNAL: it is not in the npm
`files` list and `aqe init` does not copy it — user hooks are generated as
`npx agentic-qe hooks <cmd>` (cross-platform, npx-friendly). Portability here is
defense-in-depth, not a shipping decision.
Adds a regression test for branch-1 resolution + precedence (node_modules/.bin/aqe
preferred over dist). 6/6 shim tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The repo's own PostToolUse post-task hook still carried the broken
'--success --json' (no --agent) form (the #5 shim rewired it but kept the
args). The parser fix makes bare --success parse true, but it still recorded
agent='unknown'. Updated post-task to pass --agent/--success true/--description
and post-command to explicit --success true, matching the corrected init
templates. (post-edit's bare --success is correct — that option is a flag.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reported by @ciprianmelian. ADR-094 moved dream cycles to the kernel-side
DreamScheduler, but the dream-scheduler:hook-state kv row that SessionStart
reads is only INCREMENTED by the post-task hook (experienceCount++) and was
never reset by the entity that actually dreams. Confirmed in source: the kernel
scheduler had ZERO references to that key (it persists its own
'dream-scheduler:state' instead). Result: SessionStart perpetually showed
lastDreamTime:null and a pendingExperiences counter that only grew (e.g.
11 -> 1747 -> 6489), reading like a stuck scheduler while the daemon dreamt fine.
Fix: after a successful executeDream cycle, reconcileHookState() stamps the
truth on the SAME key + DEFAULT namespace the hook path uses:
{ lastDreamTime: now, experienceCount: 0, totalDreamsThisSession: prev+1,
sessionStartTime: preserved }. Best-effort (never breaks a dream cycle). Key
is a local literal mirroring DREAM_STATE_KEY to avoid a learning->cli layering
import.
Verified: new unit test seeds the stale row (experienceCount: 2720,
lastDreamTime: null), triggers a dream, asserts the row becomes
experienceCount:0, lastDreamTime: <iso string>, totalDreamsThisSession:1,
sessionStartTime preserved. 39 dream-scheduler tests pass; typecheck clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reported by @ciprianmelian. The aqe-init-generated PostToolUse hook ran
`post-task --task-id ... --success --json`, but --success was declared
`<bool>` (value REQUIRED), so Commander consumed --json as the success value:
options.success='--json' -> parsed false -> EVERY qe_trajectories /
captured_experiences row got success=0, the --json branch was swallowed, and no
--agent meant agent='unknown'. Net: Task trajectories were unpromotable
(distillation groups by agent/domain/success), so pattern count stayed pinned
near zero. (Confirmed in source; same class as the learning gaps #510 hardened.)
Fix (two layers):
1. Parser hardening — `--success <bool>` -> `--success [bool]` (optional value)
in task-hooks/command-hooks/routing-hooks, so a bare --success yields boolean
true and never eats the next token. Backward compatible: --success true/false
and the 'true' default all still parse identically.
2. Generated templates (07-hooks.ts + init-wizard-hooks.ts): post-task now emits
`--agent "$TOOL_INPUT_subagent_type" --success true --description
"$TOOL_INPUT_prompt" --json` (real agent attribution + explicit success +
pre-task bridge), and post-command emits explicit `--success true`.
Verified: isolated Commander proof — OLD <bool>: success="--json" (false);
NEW [bool]: success=true, --json fires. Plus 3 regression tests driving the EXACT
broken argv through registerTaskHooks: bare --success -> success=true, explicit
--success false -> false, --agent flows through. 5/5 task-hook tests pass
(incl. existing #449); typecheck clean. (Full post-task CLI run OOMs in Codespace
per CLAUDE.md — the parsing layer is the bug and is proven.)
NOTE for existing installs: re-run `aqe init` (or hand-edit the PostToolUse hook
in .claude/settings.json) to pick up the corrected command.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ADR-098 documents the durable behavioural decisions from the ruflo adoption
pass (#510): domain-score de-dilution (#2), EWC++ importance-weighted forgetting
(#6), contradiction suppression (#7), the two pre-existing bug fixes
(consecutive_failures, MCP -32601), and the explicit NEGLECTED decisions —
ruflo's Beta-priors (#2) and RaBitQ speedup (#8) did not transfer, and
multi-bundle packaging is deferred. AQE WH(Y) format, Accepted.
Marks the consecutive_failures (ae7c8b71) and MCP error-code (9b189115)
side-findings FIXED in the plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pre-existing bug surfaced by the qe-devils-advocate audit and live-reproduced:
handleToolsCall (and unknown-method / double-initialize) threw a PLAIN OBJECT
{ code: -32601, message }. The server safety net checked err instanceof Error
(false for a plain object) and surfaced it as a content payload
'Internal error: [object Object]', losing the -32601 code/message an MCP client
expects. The tool was correctly never executed; only the error shape was wrong.
Fix (matches existing OAuthProviderError convention): add McpError extends Error
{ code } + isJsonRpcCodedError guard in transport/stdio.ts (guard accepts both
McpError and the legacy plain {code,message} so nothing regresses). Convert the
three plain-object throws in protocol-server.ts to McpError; the safety net
re-throws coded errors (real protocol errors) while still swallowing genuinely
unexpected errors to keep the connection alive; the transport catch honors
err.code, falling back to INTERNAL_ERROR. Successful/known-tool calls unaffected.
Verified (real server boot via scripts/audit-mcp-tool-parity.mjs):
before: unknown tool rejected with 'Internal error: [object Object]'
after: unknown tool rejected with 'JSON-RPC error code=-32601'
Parity PASS (86 tools), typecheck clean, real .agentic-qe/aqe.rvf unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pre-existing bug surfaced by the qe-devils-advocate audit and confirmed:
checkDeprecation() reads (pattern as PatternWithDeprecation).consecutive_failures
to trigger the 'failures' deprecation reason, but rowToPattern() built the
QEPattern WITHOUT that column (and without deprecated_at), so getPattern()
always surfaced undefined->0 and the 'failures' branch NEVER fired — a pattern
that kept failing was never deprecated for failures.
Fix (surgical): rowToPattern now surfaces consecutive_failures and deprecated_at
(added to the PatternRow interface; returns a typed PatternWithDeprecation). No
thresholds or deprecation logic changed.
Verified: new tests assert a pattern with consecutive_failures==3 (== default
threshold), not stale, confidence above min, deprecates with reason 'failures';
below threshold does not. Regression-proven (reverting rowToPattern fails the
new test). 12 EWC tests pass; typecheck clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit (qe-devils-advocate) verdict: 10/10 items have real committed code + real
passing tests at claimed counts; production changes (#2/#6/#7) genuinely wired
into live paths; no real-store mutation; #8 honestly UNWIRED. Three fair
discrepancies, now corrected:
1. #2 had one TAUTOLOGICAL test (asserted (1/3)*0.4 > (1/11)*0.4 — arithmetic on
literals, not a code call). Rewritten to score a matching vs non-matching
agent through calculateAgentScores and assert the delta equals the CAPPED
domain term (~0.1333), which fails if the cap is removed. 4/4 still pass.
2. #2 status row softened to VERIFIED (PARTIAL): +5.6pp shipped, but the
structural Q-maturity / fixed-perf-term ceiling on confidence remains.
3. #10 status row corrected to PARTIAL: only the smoke contract was delivered;
the original multi-bundle 'subset packaging' scope is deferred.
No production code changed by this commit. Side-findings re-confirmed real
(dead consecutive_failures branch, MCP [object Object] error, RVF/SQLite drift).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports ruflo's 'smoke.sh as a plugin contract' discipline. AQE already ships
the marketplace.json + a lean auto-discovery plugin (plugins/agentic-qe-fleet:
11 agents, 9 commands, 9 skills, lean plugin.json), but had no machine-checkable
contract — so silent drift could ship a broken bundle.
plugins/agentic-qe-fleet/scripts/smoke.sh (npm run plugin:smoke) asserts:
- plugin.json valid JSON, name == agentic-qe-fleet, has a version;
- plugin.json does NOT declare skills/commands/agents arrays (Claude Code
auto-discovers them — declaring them is an error, per ruflo);
- marketplace.json indexes the plugin at ./plugins/agentic-qe-fleet;
- exact counts (11 agents / 9 commands / 9 skills), each with name+description
frontmatter / SKILL.md (bump the constants deliberately on change);
- README present; top-level plugin.json registers the agentic-qe MCP server.
Exit 0 = contract holds, 1 = any violation.
Verified: passes 10/10 checks (exit 0), and correctly FAILS (exit 1) when a
skill is removed ('skills: 8 (expected 9)').
Scope note: AQE ships ONE bundle today. Splitting the 93 QE skills into multiple
lean subset bundles (aqe-security, aqe-browser, ...) is a larger restructure of
shipped artifacts — deferred as a follow-up (needs a distribution decision); the
skills-manifest.json already has the category data to drive it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports ruflo ADR-077: seed the pattern store from the repo's OWN git history
(commits = success trajectories) through the SAME live-learning code path, so
a fresh project starts with a non-empty, project-specific pattern store instead
of only the generic PRETRAINED_PATTERNS.
scripts/pretrain-from-history.ts (npm run pretrain:history):
- git log --no-merges -> per commit: domain = deriveTaskType(subject),
success = subject !startsWith 'revert' (a revert is a failure signal);
- records each via persistTaskOutcome (the live captured_experiences +
experience_applications + Q-update path, honoring AQE_PROJECT_ROOT);
- then consolidateExperiencesToPatterns() distills them into qe_patterns.
- flags: --limit N (default 500), --dry-run (classify+histogram, write nothing),
--verbose. Extracted a pure classifyCommit() helper (19 unit tests).
Correctness fix found during verification (in the script, not prod): a brand-new
store only has core schema, so persistTaskOutcome failed with
'no such table: experience_applications'. The script now calls the canonical
ensureAllBrainTables() + initializeExperienceCapture() before recording — the
same tables production creates lazily.
Verified against a THROWAWAY AQE_PROJECT_ROOT temp store (data protection):
--limit 150 -> captured_experiences 0->150, qe_patterns 0->4, exit 0;
--dry-run -> nothing recorded (captured_experiences stays 0);
real .agentic-qe/*.rvf byte-identical (sha256) before/after; 19 tests pass.
Side-finding: AQE's real embedding (ONNX) costs ~2.3s/commit here vs ruflo's
~3ms/trajectory, so bootstrapping 500 commits takes minutes — logged as a perf
follow-up (embedding backend / batching).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports ruflo's perf-M4 (sign-random-projection + Hamming popcount). Adds
src/shared/utils/rabitq.ts: signSignature (384-bit packed into 12 Uint32
words, 48 bytes), hammingDistance (SWAR popcount), hammingSimilarity, and
topKBySignature (Hamming prefilter gated at N>=100 + exact-cosine rerank of a
recall-aware pool, so output equals exact topK when the true top-k fall in the
pool). 16 tests + scripts/benchmark-rabitq.ts (npm run benchmark:rabitq).
HONEST FINDING (verified, does NOT match ruflo's optimistic 2.7x/10.9x):
N=1000: speedup 1.31x, recall@10 100%, index 1.46MB -> 47KB (32x smaller)
N=5000: speedup 1.18x, recall@10 99.9%, 7.32MB -> 234KB (32x smaller)
The 32x INDEX-MEMORY reduction is real and unconditional. The SPEEDUP is only
~1.2-1.5x in JS, not ruflo's figure: a 1-bit sign signature in 384-dim is
coarse, so preserving recall@10 >= 0.95 needs a large exact-rerank pool
(~40-55% of N), which eats most of the prefilter win. ruflo's big win came
from replacing a LINEAR scan; AQE's main search is already native HNSW
(sublinear), so that win doesn't transfer. Tests pin this tradeoff (recall==1.0
with a sufficient pool; small pool sacrifices recall) so it can't be
mis-sold later.
NOT INTEGRATED — no zero-regression consumer exists:
- unified-memory-hnsw search() IS the ranking (no separate exact rerank to
prefilter), so Hamming-prefiltering its beam would drop true neighbors;
- coherence-gate-energy O(N^2) SUM and k-means clustering are numerically
sensitive (Hamming would change results).
Committed as a tested, benchmarked, ready-to-use utility (real memory lever for
patterns.rvf and a future prefilter->exact-rerank consumer), not wired into any
production path. No production code or .agentic-qe stores touched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports ruflo's hook-shim discipline. AQE's project hooks ran
`node dist/cli/bundle.js hooks <cmd> --json` directly, which printed init
diagnostics to STDOUT before the JSON on every SessionStart/UserPromptSubmit:
[hooks] CoherenceService initialized with WASM engines
[RvfPatternStore] Initialized: .agentic-qe/patterns.rvf (dim=384)
[RVF] Migration adapter active at stage 2 (dual-sqlite)
[hooks] System initialized
polluting the Claude Code transcript on every turn, and a crash/slow init could
surface a hook error mid-turn.
New .claude/hooks/aqe-hook.sh wraps every bundle hook with the ruflo contract:
* prefer the LOCAL built bundle (no npx / network);
* no-op if the bundle isn't built (never block a turn);
* swallow stderr;
* emit ONLY the top-level JSON object on stdout (sed '/^{/,/^}/p' drops both
leading init noise AND async daemon log lines printed after the JSON, so the
--json contract stays parseable);
* ALWAYS exit 0.
All 10 bundle hook commands in .claude/settings.json rewired to the shim
(diff touches only those 10 command strings).
Verified with real commands: route/session-start/guard via the exact settings
invocation -> exit 0, 0 noise lines, valid JSON; missing-bundle and crashing-
bundle -> exit 0, empty stdout, no stderr leak. 5 regression tests (resilience +
deterministic noise-stripping via a fake bundle) pass in ~120ms.
Note: Windows .cjs parity (ruflo also ships one) is a separate follow-up — the
entire AQE hook config is POSIX `sh -c`, so a .cjs twin needs the settings
rewrite too. Observed side concern: the route/session-start hooks cold-start the
full system (~30-60s) on each fire — logged for a perf follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports ruflo's #1874 discipline. Replaces the FAKE mcp:validate theater
(echo 'All tools registered' && exit 0) with a real smoke that boots the
actual MCP server (tsx src/mcp/entry.ts) over stdio and validates the
initialize + tools/list handshake against the MCP SPEC, not the code:
- protocolVersion is a YYYY-MM-DD string (the core ruflo #1874 guard — an
object here is what Claude Code's Zod rejects)
- capabilities is an object; serverInfo.name/version are non-empty strings
- tools/list returns a non-empty array, each entry name + object inputSchema
scripts/audit-mcp-tool-parity.mjs is the AQE-equivalent of ruflo's CLI<->MCP
parity audit (the CLI has no callMCPTool('name') string pattern, so the
meaningful surface is advertisement<->handler parity). Drives the advertised
set from a live tools/list, diffs a monotone baseline
(verification/mcp-tool-parity-baseline.json, 86 tools), fails only on NEW
discrepancies (removed tool = regression), and probes that unknown-tool
tools/call is rejected. --update-baseline to re-baseline.
Both scripts spawn the server against an isolated AQE_PROJECT_ROOT temp dir
(data protection: real .agentic-qe/*.rvf proven byte-identical before/after)
and kill the whole detached process group on exit (no leaked servers).
Verified: npm run mcp:validate -> PASS (protocolVersion 2025-11-25, 86 tools);
mcp:parity -> PASS (0 removed, 0 added). Both exit 0, real stores untouched.
Side-finding (pre-existing, not fixed here): handleToolsCall throws a plain
object for unknown tools, which escapes the request-handler safety net as
'Internal error: [object Object]' instead of a proper JSON-RPC -32601. Logged
for a dedicated fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AQE's Phase-1 consolidation merged near-duplicate experiences by cosine
similarity, but two experiences with the SAME context and OPPOSITE outcomes
(large quality delta) are a conflict, not a duplicate. Merging silently
absorbed the conflict; worse, a *used* low-quality contradictory experience was
never merged (merge requires application_count===0) and stayed in retrieval,
poisoning recall.
Now, when similarity >= contradictionSimilarityThreshold (0.85) AND the quality
delta >= contradictionQualityDelta (0.4), the pair is treated as a contradiction:
the lower-quality loser is SUPPRESSED from retrieval (consolidated_into =
'contradicted', excluded by the IS NULL filter) instead of merged — even if it
was applied. The survivor is NOT boosted (a conflict is not a duplicate). The
action is logged distinctly ('contradiction'). New count surfaced as
ConsolidationResult.contradicted. Both thresholds are configurable.
PRODUCTION IMPACT: affects the dream/consolidation merge phase. Genuine
near-duplicates still merge exactly as before; only same-context/opposite-outcome
pairs are newly suppressed (soft, reversible — consolidated_into can be cleared).
Verified with 6 tests against an in-memory DB matching the production
captured_experiences schema (suppress loser, suppress even-when-applied, no
survivor boost, near-duplicate still merges, dissimilar pair untouched, distinct
log action). 13 total consolidation tests pass; typecheck clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>