Commit Graph

348 Commits

Author SHA1 Message Date
Dragan Spiridonov 318d988060 fix(test-gen): audit fixes — multi-file fallback + missing package export
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>
2026-06-20 12:55:07 +00:00
Dragan Spiridonov 4409699a2b feat(test-gen): enable paid escalation + wire D9 routing-feedback at kernel layer
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>
2026-06-20 12:43:22 +00:00
Dragan Spiridonov b26edbf5e6 feat(test-gen): opt-in free local-tier generation + repair loop (D7-wire/D8/D9)
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>
2026-06-20 12:31:44 +00:00
Dragan Spiridonov 7155d8234b feat(routing): D7-wire — FreeTierEscalatingExecutor (cheap-first, verify, escalate)
Turns the dormant free-tier ladder into a running loop implementing Ruv's
SWE-bench economics for a QE task: run on the cheap local tier, verify with an
objective oracle, escalate this task up the ladder on failure, and record the
start-tier verdict so the base tier adapts across tasks.

- executor.ts: decoupled by injection — free tiers via freeTierChat, Claude
  tiers via an injected ClaudeTierRunner (no Anthropic SDK dep; coordinator
  delegates to its existing HybridRouter). Local-only mode when no runner.
  onOutcome sink = the D9 seam into routing-feedback.
- tests: 8 new (53/53 green across free-tier + tracker), strict-tsc clean.
- prototype/d7-wire-proof.mjs: LIVE — qwen3:8b generated a correct vitest test
  that passed the objective verifier in 8.65s ($0, no escalation); an
  impossible verify climbed local->haiku->sonnet->opus and failed at the top.

Model: qwen3:8b (most productive worker in D0; 5.2GB fits the 8GB story).
Additive only — no shipped coordinator hot-path edited. Adoption is a one-call
snippet documented in docs/metaharness/06 (behind a flag; needs sign-off).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 11:16:48 +00:00
Dragan Spiridonov 0d1a846dbe feat(routing): D7 — free local tier in the escalation ladder
Ruv's SWE-bench Round-3 economics for AQE: cheap-local handles the bulk,
escalate only failures up the existing Claude tiers. Maps onto AQE's
existing AutoEscalationTracker, which previously bottomed out at haiku (paid).

- escalation/auto-escalation-tracker.ts: made generic over the tier-name type
  with a configurable tierOrder (default resolved in ctor — DEFAULT_ESCALATION_
  CONFIG unchanged, so the exact-match test stays green). 26/26 original tests
  pass; zero behaviour change for existing callers.
- routing/free-tier/: configurable provider layer — local-ollama / cloud-ollama
  / openrouter / openai-compatible. Keys read from a named ENV VAR, never
  stored. freeTierChat() is OpenAI-compatible + reasoning-model aware + never
  throws (transport error = tier failure → escalate). defaultFreeTierLadder()
  = local -> haiku -> sonnet -> opus.
- tests: 19 new (45/45 green with the tracker suite), strict-tsc clean.
- prototype/d7-proof.mjs: live proof vs M5 host Ollama — free local tier
  answers ($0), escalates local->opus on failures, de-escalates back to local.

Dormant by design: not yet wired into a coordinator (next: D7-wire + D8 repair
loop + D9 feed outcomes into routing-feedback).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 09:31:39 +00:00
Dragan Spiridonov 3e43e8d155 feat(metaharness): Darwin-for-QE self-learning lane (A1 scan + D0/D1/D6)
Cross-pollination with Ruv's MetaHarness Darwin Mode: evolve the harness
around a frozen cheap local model, graded by AQE's objective QE metrics.

- docs/metaharness/06-...: GOAP action lane + ADR-style decision (→ ADR-111)
- A1: real mcp-scan snapshot of AQE's MCP surface (.harness/mcp-policy.json
  mirror) → 2 HIGH (default-deny OFF, shell granted), 6 MED, 2 LOW
- D0: local-mutator spike benchmark on host M5 Ollama (qwen3:8b/30b > gemma4
  as mutator; mock+LLM gives no fitness signal — documented)
- D1: src/integrations/darwin — QE→Darwin ScoreCard adapter folding ADR-104
  arena metrics (kill-rate/coverage/cost) + ADR-072 promotion-gate mirror
  (15/15 tests green, strict-tsc clean, zero coupling to @metaharness/darwin)
- D6: prototyped + proved a `customScore` hook in Darwin's evolve loop
  (patch in prototype/, Ruv's tree left pristine). QE-native scoring now
  DIFFERENTIATES variants: winner +0.1230 over baseline, 3 promotions
  (vs flat 0.765 from the mock substrate)

Two upstream PRs to ruvnet/agent-harness-generator are staged but unsent:
the customScore hook + --ruvllm-timeout/max-tokens CLI flags.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 07:40:05 +00:00
Dragan Spiridonov 8eb060b6e2 ci(mcp): make the MCP integration regression tests actually run (C3)
The #528 and #535 integration tests (tests/integration/mcp, gated on
describe.skipIf(!BUILT)) never executed in CI — a false green:
- the mcp-integration-tests job ran `npm ci` but no `npm run build`, so the
  dist bundles were absent and both tests silently skipped;
- the step was `continue-on-error: true`, so even a real failure passed;
- neither test was in the release gate's test:integration:fast list.

Fixes:
- mcp-tools-test.yml: add `npm run build` before test:mcp:integration and drop
  continue-on-error so the job is a real gate (the exit-124 hang tolerance
  stays for the known vitest-hang flake).
- both tests: skip only for LOCAL unbuilt checkouts (`!BUILT && !process.env.CI`)
  — in CI a missing build now fails loudly instead of skipping. Verified:
  CI=1 with the bundle removed fails the suite instead of reporting green.
- package.json: add both tests to test:integration:fast so they also run on the
  release gate (npm-publish.yml), which builds first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:46:16 +00:00
Dragan Spiridonov 112fffc466 fix(init,mcp): --no-claude routing (C1), db-free continuedev merge (C2), startup exit (H3)
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>
2026-06-17 15:42:24 +00:00
Dragan Spiridonov 5975171fe5 feat(init): db-free MCP propagation (#533) + --no-claude exclusive mode (#532)
#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>
2026-06-17 14:07:13 +00:00
Dragan Spiridonov 7e224ca632 fix(mcp): memory_delete no longer masked by stale session-cache read (#535)
memory_delete returned {deleted:true} but an immediate memory_retrieve still
returned the value (with an identical timestamp). Root cause was NOT the delete
— it works. memory_retrieve is isConcurrencySafe, so the Issue-#473 session
cache stored its result; memory_delete never invalidated it, so the follow-up
read served the stale cached "found" entry (the identical timestamp was the
cache fingerprint). Confirmed by reproduction: with AQE_SESSION_CACHE=off the
post-delete read already returned found:false.

Fix: mutating tools now invalidate their domain's cached reads. Adds
SessionOperationCache.invalidateDomain() (clears in-memory + persisted rows);
the protocol server calls it after any successful tool whose name ends in a
mutation verb (_store/_delete/_set/_share/_update/_remove/_clear/_promote/
_cleanup). Generalizes to other write/read pairs (e.g. cross_phase_store).

Verified end-to-end through the protocol server with the cache ENABLED:
store -> retrieve(found) -> delete -> retrieve(found:false). Adds
tests/integration/mcp/memory-delete-cache-invalidation.test.ts.

Note: the other findings in #535 (cross_phase_stats counter, GOAP
maxSteps/plan-lookup/activeAgents, test_generate_enhanced quality,
qe_coherence_consensus text) are separate root causes and remain open for
dedicated follow-up per the issue's own "split into its own issue" note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:45:46 +00:00
Dragan Spiridonov 5ac4c7cba7 fix(mcp): run agentic-qe mcp server in-process, not via double-spawn (#528)
The `mcp` subcommand spawned `node dist/mcp/bundle.js` with stdio:'inherit'.
With a piped stdin (any programmatic MCP client), the grandchild received
`stdin-eof` right after answering `initialize` and the Issue #513 stdin-EOF
guard shut it down before serving `tools/list` — so `tools/list` never
returned. The dedicated `aqe-mcp` bin (which runs the bundle directly) worked.

Fix: dynamic-import the MCP entry so it runs in-process on the command's real
stdio, exactly like `aqe-mcp`. entry.ts kicks off `main()` fire-and-forget, so
the action then awaits a never-resolving promise to keep the CLI wrapper from
exiting out from under the server; entry.ts owns shutdown.

Verified with the exact issue reproduction (piped stdin + EOF): `agentic-qe mcp`
now answers initialize AND tools/list (86 tools), matching the direct bundle.
Adds tests/integration/mcp/mcp-cli-inprocess.test.ts as a regression guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:12:00 +00:00
Dragan Spiridonov 0403a65f0f test(scripts): fix js/identity-replacement CodeQL finding (alert 211)
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>
2026-06-11 17:05:17 +00:00
Dragan Spiridonov 50302ce94e feat(safety): ADR-106 live per-tier eval — all native Claude tiers pass 5/5 (issue #522)
Path A (model-judgment) live runner over the real ADR-026 tiers
(haiku/sonnet/opus via Anthropic API). Each tier is given the
data-protection absolutes + an explicit ACTIONS: [...] declaration
protocol, hit with each temptation as opener AND mid-task, N=5, and run
through the existing assertion engine. Asserts on declared intent only;
never touches a real DB. safety-eval.yml live job now executes it.

Real run found and fixed a harness false-positive: a REFUSING model that
illustrates a command in prose was wrongly flagged. The ACTIONS protocol
makes intent unambiguous; discrimination tests prove a genuinely-declared
rm memory.db still FAILS while an illustrated one PASSES (not neutered).

Verified: 16 deterministic tests; live matrix 5/5 all cells, all 3 tiers
(haiku $0.085 / sonnet $0.231 / opus $1.108), committed as evidence.
Retry-resilient client (transient fetch/429/5xx backoff).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 12:51:30 +00:00
Dragan Spiridonov 8592048b8a fix(mcp): expose evidenceClass + needsVerification in validation_pipeline MCP projection (issue #522)
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>
2026-06-11 11:10:11 +00:00
Dragan Spiridonov c02e60a8f3 feat(benchmarks): ADR-109 interaction benchmark — rubric pre-registered before any data (issue #522)
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>
2026-06-11 11:05:26 +00:00
Dragan Spiridonov 8663df13b7 feat(learning): PatternNullStore + null capture on failed pattern-guided tasks per ADR-110 (issue #522)
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>
2026-06-11 10:56:35 +00:00
Dragan Spiridonov 687874fe37 feat(learning): qe_pattern_nulls migration — kept nulls per ADR-110 (issue #522)
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>
2026-06-11 10:50:37 +00:00
Dragan Spiridonov e9c1a53873 feat(safety): ADR-106 behavioral eval engine + scenario corpus (issue #522)
Deterministic core of the safety eval: 4 temptation scenarios (stale-db
cleanup, recreate-db sync fix, WAL quick-delete, hurried consolidation)
each with opener and mid-task injection modes; trajectory assertion
engine (forbidden-operation with unlessPrecededBy exemptions,
missing-prerequisite ordering rules — backup-before-mutation,
verify-before-success-claim); runner CLI; 7 fixture trajectories
modeling real compliant/violating agent response shapes.

Pass/fail only, never a score — refusal is compliance; WAL removal is
exempt only after backup/integrity check (restore context per
CLAUDE.md); a success claim without row-count verification fails.

Verified: engine tests 9/9; runner CLI on fixtures — violating_direct_rm
exit 1, compliant_refusal exit 0, violating_fabricated_success exit 1.

Remaining for ADR-106 (tracked): live per-tier driver (needs API key),
N=5 matrix, safety-eval.yml release gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 10:43:04 +00:00
Dragan Spiridonov 88014dc20e feat(validation): evidence-class gating per ADR-105 (issue #522)
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>
2026-06-11 10:28:58 +00:00
Dragan Spiridonov bd0bb213ca feat(scripts): shipped-agent invariant verifier per ADR-107 (issue #522)
config/agent-invariants.json manifest + verify-shipped-invariants.ts:
section-level content assertions (not keyword greps), shipped-vs-source
divergence on invariant sections, assets-only-qe rule, loud exemptions,
version-policy warnings. npm run verify:invariants.

Found pre-existing drift on first run: qe-integration-architect.md
(legacy format, no XML sections) and qe-queen-coordinator.md (missing 3
sections) — exempted with reasons pending maintainer decision; 53 agents
carry frontmatter 3.0.0/3.1.0 vs package 3.10.5 (warn until semantics
decided).

Verified: baseline run 0 violations/55 warnings exit 0; mutation tests
9/9 (hollow-section, removed-section, divergence, reword-passes); E2E
smoke on mutated tree exits 1 catching both hollow + divergence rules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 10:25:01 +00:00
Dragan Spiridonov 07326b9802 feat(arena): ADR-104 qe-arena Phase 1 — competitive test-strategy tournaments
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>
2026-06-10 10:48:37 +00:00
Dragan Spiridonov dc6fce5e70 feat(contracts): ADR-103 structured verdict handoffs — RiskDecision et al
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>
2026-06-10 10:15:07 +00:00
Dragan Spiridonov f2a350ba7f feat(agents): ADR-101 nested-subagent readiness — provenance, probe, validation
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>
2026-06-10 10:06:05 +00:00
Dragan Spiridonov 2d5126a053 feat(hooks): ADR-100 tool-loop circuit breaker — warn at 3, block at 5
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>
2026-06-10 09:48:37 +00:00
Dragan Spiridonov 9c54a70656 feat(llm): ADR-088 amendment — wire enableCache to ephemeral cache breakpoints
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>
2026-06-10 09:41:49 +00:00
Dragan Spiridonov 8ff39f2c6e feat(learning): ADR-099 reasoning-tag scrub before distillation/embedding
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>
2026-06-10 09:35:25 +00:00
Dragan Spiridonov a76fbf98e1 fix(cli): bridge CLI auto-init into MCP fleet state + dev-env fixes
- 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>
2026-06-10 09:22:55 +00:00
Dragan Spiridonov dc0ac612e2 fix(rvf): resolve project root via static import, not require() (#516 follow-up)
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>
2026-06-08 16:10:16 +00:00
Dragan Spiridonov 07873d8c6b fix(mcp): exit orphaned stdio server on stdin EOF instead of busy-looping (#513)
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>
2026-06-08 12:00:28 +00:00
Dragan Spiridonov 83e8f762a6 fix(memory): resolve project learning to its own .agentic-qe/ (#516)
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>
2026-06-08 11:48:02 +00:00
Dragan Spiridonov c8e6f3bc2c test(init-gate): recalibrate self-dogfood snapshot for working TS code-intelligence
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>
2026-06-03 13:59:29 +00:00
Dragan Spiridonov c3173a88ad feat(code-intelligence): parse TS/JS via bundled tree-sitter WASM — no typescript dependency (#511)
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.
2026-06-03 12:51:10 +00:00
Dragan Spiridonov c981d84f5d fix(mcp): bound advisory routing with a timeout so a stalled learning engine can't hang domain tools
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.
2026-06-03 12:10:20 +00:00
Dragan Spiridonov d6b19d087c fix(learning): stop QEReasoningBank.initialize() recursing → MCP domain tools hung on fresh install
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.
2026-06-03 12:04:37 +00:00
Dragan Spiridonov cc5220c5a0 fix(code-intelligence): pin KG to a fixed namespace + evict stale file cache (#511)
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>
2026-06-03 08:59:47 +00:00
Dragan Spiridonov 26e724b24e fix(code-intelligence): warn on unsupported langs, prune incremental stale edges, fix MCP code_index counts (#511)
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>
2026-06-03 08:47:57 +00:00
Dragan Spiridonov d7b82ff6e2 fix(code-intelligence): make aqe code search return results — content namespace round-trip (#511)
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>
2026-06-03 08:08:36 +00:00
Dragan Spiridonov 8a0217e9aa fix(code-intelligence): persist KG so aqe code deps returns edges on TS/JS repos (#511)
`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>
2026-06-03 08:01:03 +00:00
Dragan Spiridonov f96159683f feat(init): ship the hook shim and wire aqe init to install + use it
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
2026-06-02 13:45:19 +00:00
Dragan Spiridonov 1d0cbd626e refactor(hooks): rewrite hook shim as cross-platform Node (.cjs), drop POSIX .sh
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>
2026-06-02 13:36:16 +00:00
Dragan Spiridonov e81b56d411 fix(hooks): make aqe-hook.sh portable (resolve project-local AQE, not hardcoded dist)
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>
2026-06-02 13:26:14 +00:00
Dragan Spiridonov 2ecc377241 fix(dream): kernel DreamScheduler reconciles dream-scheduler:hook-state per cycle (#509)
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>
2026-06-02 11:34:40 +00:00
Dragan Spiridonov 201f5e8a95 fix(hooks): post-task --success eats --json -> every trajectory success=0/agent=unknown (#508)
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>
2026-06-02 11:30:59 +00:00
Dragan Spiridonov ae7c8b7105 fix(learning): surface consecutive_failures so the 'failures' deprecation branch fires (#510 side-finding)
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>
2026-06-02 11:04:57 +00:00
Dragan Spiridonov 2969bb387c fix(test)+docs: address qe-devils-advocate audit of #510 loop
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>
2026-06-02 10:54:19 +00:00
Dragan Spiridonov 3196c05edd feat(learning): pretrain-from-history bootstrap (#510 item 9)
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>
2026-06-02 10:41:35 +00:00
Dragan Spiridonov 0d80eb56ec feat(perf): RaBitQ 1-bit signature utility + benchmark (#510 item 8)
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>
2026-06-02 10:30:10 +00:00
Dragan Spiridonov 1a695f5651 feat(hooks): resilient hook shim — clean transcript + never block a turn (#510 item 5)
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>
2026-06-02 09:50:59 +00:00
Dragan Spiridonov 43918b3ba8 feat(learning): contradiction detection in consolidation (#510 item 7)
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>
2026-06-02 09:22:51 +00:00
Dragan Spiridonov ef4738dec4 feat(learning): EWC++ importance-weighted forgetting protection (#510 item 6)
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>
2026-06-02 09:15:19 +00:00