Learning capture froze for six days (2026-07-23 .. 2026-07-29) while every hook
kept reporting success: true. Root cause chain:
AQE_DISABLE_WAL=1
-> safe-db ran `journal_mode = DELETE` on EVERY writable open
-> that switch needs an EXCLUSIVE lock (busy_timeout does not help)
-> a stale `npm exec agentic-qe mcp` held memory.db + -wal open
-> SQLITE_BUSY -> openDatabase threw -> UnifiedMemoryManager init
aborted -> ALL persistence died, silently
It could not self-heal: the switch can never succeed while any reader exists.
safe-db.ts now:
- reads the current journal_mode first and attempts the switch ONLY if it
differs, so the steady state takes no lock at all and the one-time migration
leaves the hot path;
- catches ONLY SQLITE_BUSY / SQLITE_LOCKED and rethrows everything else
unchanged, so I/O errors and corruption are no longer misreported as contention;
- FAILS CLOSED on contention rather than writing anyway. AQE_DISABLE_WAL=1 is the
operator declaring WAL unsafe on this mount, so continuing in WAL would trade a
loud outage for silent corruption;
- verifies the switch actually took effect before allowing writes;
- names the real database file and the command that finds the actual holder.
The hook shim's health-log guidance named "concurrent AQE processes (MCP server /
daemon)". Operators checked exactly those and found nothing, because the culprit
was an npx-cache process matching neither "aqe" nor .mcp.json — that wrong hint is
most of why this ran six days. It now prints a /proc scan that finds ANY holder,
and only claims contention when the failure really was a lock error.
Also raises the statusline refresh from 5s to 60s; it reopened the 158MB DB on
every tick, which was constant added pressure on the same lock.
11 tests covering fail-closed, the non-lock rethrow, the skip-when-already-correct
path, durable write across reopen, and the diagnostic contents. Verified live:
journal_mode=delete, integrity_check=ok, captured_experiences growing again after
freezing at 21157.
The shipped qe-court config violated the court's own anti-collusion invariant
the moment it was copied into a project: `defense: cognitum-low` and
`jury: cognitum-high` are different tiers but the SAME coarse vendor, which is
exactly what `writerIsNeverJuror` forbids.
The deeper defect was that the rule could never have fired. `validatePanel` had
one caller in the whole repo — its own unit test. Nothing converted
`config.routing` into a panel, and the `options` block was inert: `PanelPolicy`
accepted only `minVendors`, while the config declares `minDistinctVendors` and
`writerIsNeverJuror`. An invariant with no caller is not enforced.
- move `defense` to `claude-code` rather than the jury to `codex`: the
deeperReviewer is already `codex`, so re-seating the jury there would put the
jury and the overturn reviewer on one vendor and weaken the escalation the
court rests on. `claude-code` was already a documented defense option.
- add `panelFromRouting()` and `validateCourtConfig()` — the missing config to
panel link, and the symbol downstream tooling already expected.
- widen `PanelPolicy` so `minDistinctVendors` / `writerIsNeverJuror` actually bind.
- treat a jury-less routing map as `missing-jury` instead of silently valid.
- SKILL.md: validation is now a mandatory, abort-on-failure step before seating.
Regression guard validates the real shipped config files on disk (both copies,
plus a drift check between them), so a future routing edit fails in CI rather
than in a user's court. Mutation-verified: restoring the old provider turns it
red. Suite 6 -> 15 tests.
Reported by @pacphi against agentic-qe@3.13.2.
Closes#576
Version bumped in all four places that carry it: package.json,
.claude-plugin/plugin.json, and the fleetVersion in both the .claude/ and
assets/ skills manifests (the assets mirror drifts easily and is shipped).
Adds the v3.13.1 changelog entry and release notes covering the four fixed
issues (#565, #567, #568, #569) plus the Codex QE skills and lifecycle hooks
from a parallel session (763ea385), and draft issue-closing comments to post
once the release is published.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cross-vendor (Codex/GPT) review of the finished work. Three of its five findings
reproduced; one did not, and is recorded below so the review stays calibrated.
CONFIRMED — a report containing only test files reported them as production
coverage. `productionOnly` filtering fell back to the UNFILTERED file list when
no production files survived, so an LCOV whose only entry was
`tests/verifier_matrix.rs` came back as a measured 79% coverage target. That is
issue #569's third contradiction, verbatim, on a path the original fix did not
touch. An empty production set now yields no production coverage and says why.
CONFIRMED — the aggregate was carried over from the unfiltered set. Filtering
test files out of `files` while keeping `existing.summary` meant the headline
figure still blended test coverage in. In practice `CoverageAnalyzerService.analyze`
recomputes the summary from `files`, so the emitted number was already correct —
but the handler was relying on a downstream accident. The summary is now
recomputed where the filtering happens.
CONFIRMED — saved LCOV presented estimates as instrumented output. LCOV has
nowhere to record provenance, so `generateLcov` turned a `null` into
`DA:1,0 / LF:100 / LH:0` — a measured 0% over 100 lines that nothing measured —
and a 78% estimate into an apparently instrumented 78%. LCOV is now written only
for `measured: true`; the JSON artifact continues to carry the full result with
its provenance. This was the reviewer's stated highest-value fix and its reason
for a do-not-ship verdict.
NOT CONFIRMED — the reviewer reported the headline figure as 55% for a
10%-production/100%-test fixture. Reproduced and measured: it returns 10. The
finding was derived statically and missed the downstream recomputation. Fixed the
underlying fragility anyway, but the defect as described did not exist.
Also reverts three `.claude/helpers/` files (`.helpers-version`,
`helpers.manifest.json`, `statusline.cjs`) that a hook swept into 9b6de397. They
are unrelated to all four issues, and they shipped internally inconsistent
metadata — `.helpers-version` said 3.32.7 while the manifest said 3.32.4 — plus a
whole-file CRLF rewrite of the statusline. Restored to the 001c042a baseline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The nullable coverage metrics were a visible contract change with nothing a user
would ever read to discover it. Adds an Unreleased section covering all four
issues, the two new opt-ins (`agentOverrides`, `AQE_COVERAGE_NO_EXEC`), the
session-cache staleness fix, and — under Changed — the BREAKING note that
`lineCoverage`/`statementCoverage`/`branchCoverage`/`functionCoverage` are now
`number | null`, with `0` meaning a measured zero and `null` meaning unknown.
Written outcome-first: what the tool used to tell you that wasn't true, and what
it does now.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Found by the qe-devils-advocate session review, AFTER #569 was already committed.
The handler was producing honest provenance; `coverageAnalyzeConfig.mapToResult`
was converting it back into fabricated certainty on three paths.
1. Absent provenance was treated as "collected".
`data.branchDataCollected !== false` — and `undefined !== false` is true. Any
producer that supplies no provenance (notably the coverage-analysis domain
plugin, whose handler returns a nested-`summary` CoverageReport) therefore
surfaced issue #569's EXACT reported symptom: 100% branch coverage alongside
0% function coverage, labelled `measured: true`, with `confidence: 0.7` on its
gaps. Now requires an explicit `true`; absence means unknown, so an
unprovenanced result reports `null`.
2. `measured` was derived as `!estimated`.
These are not complements — there is a third state where nothing was collected
at all. An empty project reported `lineCoverage: 0, measured: true`: "we
measured 0%" when the truth was "we measured nothing". Verified through the
real MCP server before and after. `measured` now requires an explicit claim.
3. `null` was coerced to `0`, and risk was graded off it.
`?? 0` turned "no data" into a plottable 0%, and `null < 70` is `false` in JS,
so an unguarded comparison graded "no data" as 'low' risk — the most dangerous
possible default. Nulls now survive; risk grading requires a real number.
Also fixes the stale-result mystery that dogged verification all session.
`src/optimization/session-cache.ts` fingerprints on domain+action+input only, and
persists to kv_store with a 1-hour TTL, so entries survive package upgrades: a
project analyzed before a fix keeps replaying the old answer, making the fix look
broken. I twice mis-diagnosed this (first as a cache, then wrongly retracted that;
it WAS the cache). The fingerprint now includes the build identity, so an upgrade
misses cleanly and old entries age out. `AQE_SESSION_CACHE_SALT` forces a cold
cache without deleting anything.
This matters beyond #569: every fix in this release would have appeared not to
work for any user with a warm cache.
`CoverageAnalyzeResult.lineCoverage`/`statementCoverage` are now `number | null`,
joining `branchCoverage`/`functionCoverage`. `0` means a measured zero; `null`
means unknown.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the two security questions raised by the adversarial review that the
prosecutors never returned a ruling on, so I settled them with evidence.
1. Coverage measurement executes the analyzed repository.
Collecting real coverage means running the project's own tests: the JS/TS path
runs `npx vitest|jest|nyc` (pre-existing), and the Rust path added in this
release runs `cargo llvm-cov`, which compiles and runs test binaries, executes
`build.rs`, and honors any `runner`/linker directive in that repo's
`.cargo/config.toml`. For a trusted project that is exactly what the caller
wants; it is surprising for an operation that reads like a static query, and an
agent can be pointed at an untrusted checkout.
Execution stays the default — requiring opt-in would make measured coverage the
exceptional case and estimation the norm, inverting ADR-126's whole point — but
is now opt-out via `AQE_COVERAGE_NO_EXEC=1`, which disables ALL build-tool
execution. Existing coverage reports are still parsed; everything else degrades
to clearly-labelled static estimation with guidance explaining why.
Verified end-to-end through the real MCP server with a fake `cargo` shim on
PATH: switch off -> cargo invoked; switch on -> not invoked. The unit test
includes the same control, so it cannot pass vacuously.
2. Credential stripping in agentOverrides was already complete, but under-reported.
`sanitizeAgentOverrides` builds its result as an ALLOW-LIST, so any field that
is not provider/model/temperature/maxTokens/priority never survives — apiKey,
token, password and friends were all dropped already. Only the *warning* was
apiKey-specific, so a user who pasted a token got no feedback. The warning now
covers all credential-shaped keys, and a test asserts no warning ever echoes a
secret value.
ADR-126 gains a section recording the execution decision and what was rejected
(opt-in-by-default, sandboxing), plus the requirement that future language
delegations honor the same switch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up court charge, confirmed by reproduction.
`isTestPath()` checked every segment of the ABSOLUTE path against the test-directory
set. A project that merely *lives* under a directory called `tests`, `spec`, or
`examples` — a CI workspace at `/build/spec/proj`, `~/examples/myapp`, a monorepo
`packages/test-utils/` subtree — therefore had every one of its source files
classified as test code. `buildEstimatedCoverage()` then returned `null` and
coverage analysis reported nothing at all for the whole project.
Only directories *inside* the analyzed tree are meaningful, so classification now
takes the analysis root and evaluates the relative path. Threaded through all four
call sites (source walk, cargo LCOV filter, and both instrumented-report filters).
Verified: a crate at `<tmp>/tests/myapp` now analyzes its `src/lib.rs` instead of
returning null, while a `tests/` directory *inside* the project is still excluded.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>