Skip to content

Activate ruvector self-learning, agentic-qe, security + activation status line - #1

Merged
pacphi merged 20 commits into
mainfrom
explore/ruvector-self-learning-aqe
May 29, 2026
Merged

pacphi merged 20 commits into
mainfrom
explore/ruvector-self-learning-aqe

Conversation

@pacphi

@pacphi pacphi commented May 29, 2026

Copy link
Copy Markdown
Owner

Summary

Extends the machine kit beyond memory persistence to activate and verify the features that silently stay dormant on Node 24/26, and adds a one-command re-apply for upgrades.

  • 🧠 Self-learning — ruflo-enable-learning (native SQLite + 5 real capability probes) and ruflo-learning-verify (train → patterns persist 0→N). Verified live: 5/5 active, 0→7 patterns.
  • 🛡️ Security — ruflo-security-verify confirms @claude-flow/security + @claude-flow/aidefence load, prompt-injection defense fires (exit-code based, robust to an upstream render crash), and flags the CVE-DB gap.
  • 🎓 Agentic-QE (opt-in) — ruflo-setup-aqe. New finding: agentic-qe carries the same Node-≥24 native-SQLite bug as ruflo (aqe init fails at persistence-db init); the helper repairs it first, then handles half-init.
  • 📟 Status line — append-only, upgrade-safe two-line footer: 🧠 SONA … 🛡 aidefence on / 🎓 Agentic QE …. Renders only what's active.
  • 🔁 ruflo-resync — one command to re-apply everything an upgrade wipes.

Corrected diagnosis

The colleague gist's controller-registry.js patches are already upstream in ruflo 3.10.5 (agentdb v3, ESM fix, ReasoningBank embedder). The real lever is the missing native better-sqlite3 binary; "HNSW: Not loaded" in neural status is a cosmetic lazy-display, not real dormancy. Full story in docs/BACKGROUND.md. Prior art credited: Ciprian Melian's gist + the agentic-qe repo.

Test plan

All verified live on ruflo 3.10.5 / Node 26 (darwin-arm64):

  • ruflo-enable-learning → 5/5 capability probes green
  • ruflo-learning-verify → patterns 0→7, 50 learned, 55 trajectories on disk
  • ruflo-security-verify → security + aidefence load, injection flagged, scan/secrets run
  • ruflo-setup-aqe → 118 skills, both markers, idempotent
  • status-line footer renders; idempotent + upgrade-safe (1 marker after 3x apply)
  • ruflo-resync → full chain green
  • bash -n clean on all shell artifacts; --help on all bins
  • security review fix: statusline sqlite3 uses execFileSync (no shell)

Docs

Design spec + implementation plan under docs/superpowers/; docs/BACKGROUND.md, docs/TROUBLESHOOTING.md, the machine-wide CLAUDE.md reference, and a fully rewritten README all updated.

🤖 Generated with Claude Code

pacphi added 20 commits May 28, 2026 19:06
… self-learning

Patches native better-sqlite3 (6 agentdb dirs), guarded controller-registry
compat check (no-op on >=3.10), guarded @ruvector native repair, then asserts
real capability (core/VectorDb, sona, gnn, agentdb v3) rather than the lazy
'neural status' display strings. 5/5 green on ruflo 3.10.5 / Node 26.
… disk

Trains in an isolated temp dir and asserts patterns 0->>0 read directly from
.claude-flow/neural/patterns.json + stats.json (on-disk truth), not the lazy
neural-status display. Verified 0->7 patterns, 50 learned, 55 trajectories.
… note CVE gap

Uses 'security defend' exit code (1=threat, 0=clean) to assert proactive defense,
robust to an upstream stdout render crash. Documents the cve --list no-database gap
with npm audit fallback. All green on ruflo 3.10.5.
…segments

Extends ruflo-fix-statusline-version to inject a fast fs-only helper (no subprocess)
that appends 🧠 N (trained patterns) / 🛡 on (aidefence present) / 🎓 qe (.agentic-qe
db) — each rendered only when active. Shebang-safe insertion, marker-guarded idempotent.
…alf-init repair

Discovers and fixes a NEW bug beyond the gist: agentic-qe depends on
better-sqlite3@^12 directly and ships without the prebuilt .node on Node >=24,
so 'aqe init' fails at persistence-db init. setup-aqe installs the native binary
into the global agentic-qe first, then runs aqe init --auto with half-init repair
(re-run --upgrade if the .claude/skills/agentic-quality-engineering marker is missing).
Verified: 86 skills, both markers, idempotent.
…pply

Statusline footer upgraded from minimal (🧠 N 🛡 on 🎓 qe) to a two-line labeled
render: '🧠 SONA <patterns>·<traj>[·⚡HNSW]  🛡 aidefence on' and
'🎓 Agentic QE <patterns>[·traj][·vec]·<size>'. Append-only (never rewrites ruflo's
lines), upgrade-safe (strips legacy or BEGIN/END block then re-injects), fs-only +
one guarded sqlite3 for the QE line.

Adds ruflo-resync: one command to re-apply everything an upgrade wipes
(enable-learning + agentic-qe native repair + statusline). Extracts shared
_ruflo_aqe_ensure_native helper. Verified live: 5/5 learning, footer renders.
…write README

- Security: the agentic-qe footer's sqlite3 read now uses execFileSync('sqlite3',
  [db, sql]) instead of a shell-interpolated execSync, so the cwd-derived db path is
  never shell-evaluated (flagged by automated review, MEDIUM command-injection).
- README: full rewrite — decomposed, dual-audience (developer + non-technical),
  emoji section headers, friendly tone, and citations to ruflo, agentic-qe,
  Ciprian Melian's gist, ruflo#2219, better-sqlite3, and Claude Code.
uninstall.sh removed only the two original bins, leaving ruflo-enable-learning,
ruflo-learning-verify, and ruflo-security-verify behind. Both scripts now iterate
bin/* so they stay in sync automatically as bins are added/removed. uninstall.sh
header also clarifies that sourced functions (ruflo-resync/-setup-aqe) go away with
the rc source line, and that per-project artifacts are intentionally left untouched.
…s in the current repo

Strips the activation footer (ruflo-seg block), the console.log wrap, and the
version-probe injection, restoring ruflo's native render. Backs up first; leaves the
statusline file and all ruflo/agentic-qe data intact (points to 'ruflo cleanup --force'
for data). Flag parsing now loops so --dry-run and --this-project compose.
…hijacks it)

aqe init repoints .claude/settings.json statusLine.command at its minimal
statusline-v3.cjs, hiding the activation footer even though statusline.cjs is patched.
ruflo-fix-statusline-version now also makes statusline.cjs primary (idempotent;
falls back to v3 then a literal), so ruflo-resync self-heals it. Adds a TROUBLESHOOTING
entry.

Docs accuracy pass: corrected the README status-line mockup (it implied ruflo's native
'DDD Domains' line is relabeled 'Learning' — we don't; the footer is append-only),
documented uninstall --this-project, and softened 'reverses everything' claims. Verified
all cited specifics (controller-registry.js:313-315/:655, agentdb 3.0.0-alpha.14,
security 3.0.0-alpha.8, aidefence 3.0.3).
… + ruflo-neural-train

Matches the additional fields in Ciprian's statusline, append-only (no relabeling of
ruflo's native lines):
- SONA line: volume bar + Δ LoRA (cached) + ⚡HNSW
- Agentic QE line: git branch (⎇), icon-tagged 🎓 patterns / 🧭 traj / 🧬 vec⚡ / 💾 size
- ruflo-neural-train: wraps 'ruflo neural train' and caches MicroLoRA Delta Norm to
  .claude-flow/neural/lora-delta.json. Source finding: deltaNorm is a transient
  last-step metric (ruvector-training.js), not persisted and not derivable from the
  lora-checkpoint, so capture-at-train is the only faithful way to surface it.
…dy in ruflo header)

Also: docs for the enriched footer (SONA bar + Δ LoRA + AQE icons), ruflo-neural-train,
and the Δ LoRA source finding (BACKGROUND.md, spec R16/R16a).
…not empty 'vectors'

This aqe version stores per-pattern embeddings in qe_pattern_embeddings; the 'vectors'
table is empty, so 🧬 vec was omitted. Now tries qe_pattern_embeddings → vectors →
embeddings and uses the first with data (robust across aqe schema versions).
…README bullet

Updates the 'one guarded sqlite3 read' wording (it's now a few reads) and documents the
vec table fallback (qe_pattern_embeddings → vectors → embeddings) in README, the
reference block, BACKGROUND.md, and the spec. Also removes a stale duplicate
SONA/aidefence/Agentic-QE bullet block left in the README status-line section.
@pacphi
pacphi merged commit 29799dc into main May 29, 2026
@pacphi
pacphi deleted the explore/ruvector-self-learning-aqe branch May 29, 2026 04:04
pacphi added a commit that referenced this pull request Aug 7, 2026
…reclaimables

The collectors from the previous run were built and tested but never called —
index.mjs had no owner, so nothing composed them into the payload. Wiring them
up is most of this change; the rest is what wiring them exposed.

Now live (verified against GET /api/system, not asserted):
  - projects: 50 ever seen / 25 on disk / 21 git repos, de-duped across hosts by
    resolved real path. Was 4, because discovery reused discoverRuvfloProjects()
    — which requires .claude-flow/neural/ state and answers a different question
    that Intelligence still depends on, so it is left alone.
  - consumers: 80 roots, top 20 ranked, grouped by ecosystem. The panel called
    npx cache the #1 consumer at 6 GB; it is #12. The real leaders were entirely
    unscanned — Ollama 141 GB, LM Studio 49 GB, Hugging Face 36 GB, npm's
    _cacache 22 GB. Overlapping roots collapse by path so the list cannot go
    self-similar (~/.npm never appears beside its own _cacache).
  - snapshot persists consumers; runtime is still absent, and that allow-list is
    what structurally enforces the ephemeral-census invariant.
  - refreshDeep finally takes includeProjectTrees, which the caller was already
    passing into a zero-arity function. Sticky across rescans; default off,
    because one repository here is 175 GB and flattens every other row.

A HARD HANG, found only because the wider discovery reached it: the deep scan
parked forever at 0% CPU inside a Dropbox tree. Cloud providers leave evicted
placeholders — stat returns instantly, read blocks in the kernel until the
provider materializes the bytes, which never happens while it is signed out.
There is no timeout, so the scan never completed and the dashboard served a
17-hour-old snapshot. Worse, reading them silently pulls the file down. Guarded
by the allocated-blocks basis the DDD already names (blocks === 0 && size > 0):
placeholders are stat-ed, never opened. Zero false positives across 3,667 files
here, including sub-2KB files that APFS stores inline. The tree that hung
forever now returns in 50ms; a full scan takes 204s.

Also: Playwright was invisible on macOS (the scan looked only at the XDG and
Windows paths, so 1.86 GB read as a measured zero), and the RuvNet Brain was
under-reported by 85% because only kb/ was measured — the other 11 GB is five
dated kb.bak snapshots, now broken out rather than silently folded in.

Reclaimables grew from two detectors to cover those backups, npm's regenerable
cache, orphaned transcripts, and browser downloads — split into two safety
tiers that never sum: 'regenerable' for caches a tool refetches on demand, and
'review' for things like mise's 8 node versions, where recommending deletion of
a live runtime would be worse than saying nothing.
pacphi added a commit that referenced this pull request Aug 8, 2026
…rent model catalog (#123)

* fix(usage): catch the "<synthetic>" placeholder even without isApiErrorMessage

Some builds emit Claude Code's dropped-connection placeholder turn
(model: "<synthetic>", zero usage) without setting isApiErrorMessage,
so it slipped past the exception filter and surfaced as a real \$0
"model in play" on the scorecard. The literal model marker is now
checked alongside the flag, SCHEMA_VERSION bumps to 9 so cached
sessions re-derive, and a regression test covers the flagless shape.

Also re-anchors the usage-doc file:line citations shifted by this
file's line movement (doc-citations gate).

* feat(dashboard): date-windowed Observability History browsing

Observability's History scope was limited to the live tailer's moving
window (256 newest transcript files, 100-session projection). It now
browses retained sessions over an explicit calendar window — 1d, 7d,
14d (default), 1mo, 3mo, 6mo, 1y, all — like Usage's day chips.

- discoverJsonl() gains an optional sinceMs mtime cutoff
- LiveSessionsService.historySnapshot({sinceMs}): a one-shot scan with
  its own projection (never touches live tailer state), swept with
  all-zero windows so unterminated sessions read as stale, not live
- GET /api/live/history?window=<token>, same publicLivePayload
  scrubbing as /api/live; 501 when the service lacks historySnapshot
- window chips in the History sub-nav; History renders from a separate
  state.historySnapshot bucket so live SSE deltas can never clobber it

The UI harness also allowlists /api/live/intelligence EventSource
teardown aborts (pre-existing flake — Chromium reports deliberate
stream closes as ERR_ABORTED, same as the existing events/transcripts
entries) and gives LIVE_STUB a historySnapshot.

* docs: propose the System area — ADR-0025, machine-footprint domain, design mock

Drafts for review; nothing implemented yet.

- ADR-0025 (Proposed): a Machine footprint bounded context and a fourth
  System primary area (Summary / Storage / Runtime / Catalog /
  Projects), tiered honest collection with a persisted asOf snapshot,
  the initial metric taxonomy (install, runtime, storage, catalog,
  per-project LOC/disk, git-remote links), GET /api/system + ak
  footprint delivery, advisory-only reclaimables, and a documented
  absolute-path exception
- docs/ddd/machine-footprint.md: purpose, boundaries against Usage /
  Observability / Project intelligence / Integration management, the
  FootprintSnapshot model, measurement semantics, 12 invariants, and
  proposed ubiquitous-language terms
- docs/assets/system-tab-mock.html: self-contained both-theme mock of
  the System area on the dashboard's own tokens, every card annotated
  with its chart-form rationale; illustrative data only

* docs: propose the About area — ADR-0026, component-directory domain, design mock

Drafts for review; nothing implemented yet.

- ADR-0026 (Proposed): a leftmost About primary area introducing every
  component ak installs or configures — curated editorial copy joined
  with existing detection facts (no new endpoint, no probing), a
  registry↔directory parity gate so a managed tool cannot ship without
  its About card, official host marks + honest monogram tiles, and
  outbound user-initiated links inside the zero-egress contract
- docs/ddd/component-directory.md: the editorial/detection split as
  the load-bearing boundary, the new-user register contract (~50-word
  plain-language paragraphs, no runtime claims in prose), 10
  invariants, and proposed ubiquitous-language terms
- docs/assets/about-tab-mock.html: self-contained both-theme mock on
  the dashboard's own tokens — hero orientation strip with a
  how-it-fits map, category card grid (hosts first, honest
  not-installed state shown, configured surfaces with manage:
  commands), per-section design notes, and an annotated card anatomy

* feat(about): component directory, ak about, and a registry parity gate

Implements ADR-0026. A curated directory module carries each component's
editorial identity — tagline, one plain-language paragraph, source/npm/docs
links, icon, category — rewritten from verified upstream sources rather than
assumption. It collects nothing: state chips join client-side against the
status payload the dashboard already polls, so prose never claims runtime
state and a failed join degrades chips to unknown while content still renders.

The parity test is the point: every managed tool must have exactly one entry
and no entry may exist for something ak neither installs nor configures, so a
future tool cannot ship without its card.

* feat(system): machine-footprint collectors incl. first-class Windows census

Implements ADR-0025's collectors: a bounded walker (never follows symlinks,
one bad subtree degrades to unknown), install/storage/catalog/projects, and
an ephemeral runtime census. Unknown is never rendered as zero — every figure
carries measured/partial/unknown provenance, and lower bounds print as ">= N".

Windows is real rather than unsupported. A shipped PowerShell script gives the
guaranteed floor via Get-CimInstance (pid/ppid/CPU/RSS/uptime, argv
deliberately excluded), and a best-effort inline P/Invoke walks
NtQueryInformationProcess -> PEB -> RTL_USER_PROCESS_PARAMETERS for true cwd.
A bitness mismatch is detected rather than read through with wrong offsets,
and any probe failure degrades to an honest "not attributable" — never a
fabricated path, and never taking the census down with it. No dependency
added; package.json still declares none.

The script lives beside its consumer under src/ because `files` ships src/
wholesale — under scripts/ it would never have reached an npm-installed
Windows user at all. A guard test asserts that placement on every platform,
and three live tests execute the real PowerShell on windows-latest, checking
the census and the PEB walk against this process's own pid and cwd.

* feat(system): GET /api/system, snapshot persistence, and the ak system CLI

Cheap tier (census + known-file stats + snapshot carry-forward, TTL-cached)
on every read; deep tier explicit and single-flight so concurrent refreshes
attach to the in-flight scan. The deep result persists with an asOf; a missing
or corrupt snapshot reads as "never measured", never as zeros. Rescan is
manual only — nothing scans on dashboard open.

The payload deliberately carries absolute paths, unlike /api/live's leaf-only
reduction, because in this domain the path is the answer; file contents are
never read, so nothing sensitive can travel with them.

dashboard.test.cjs's self-contained assertion is replaced with a shared
assertSelfContained() helper. The old regex conflated "no external fetch" with
"no https string" and so failed on About's curated link pills; the replacement
pins the invariant to the directory itself — every external URL must be one
about-directory.mjs declares — and still bans external script/stylesheet/img.
That is strictly stronger, and the browser suite independently asserts the run
requests nothing off the loopback origin.

* feat(dashboard): About and System primary areas

About lands leftmost as the reading-order entry point; Overview remains the
default landing view, with a dismissible first-run nudge rather than a
hijacked view. System adds Summary/Storage/Runtime/Catalog/Projects with the
charted treatments from the design mock, a freshness label that nudges once a
snapshot goes stale, and honest empty states — "not measured yet" is never
rendered as a zero.

This makes five primary areas. ADR-0005's "exactly three stable primary areas"
assertion is updated to state the new contract, which both ADR-0025 and
ADR-0026 record as a deliberate amendment.

* docs: mark ADR-0025/0026 implemented and document About and System

Both ADRs flip Proposed -> Implemented, and their "open points for review"
sections are rewritten as resolved decisions with rationale: tab and CLI both
"System" (ak system), Projects stays its own sub-view, manual-only rescan with
a staleness nudge, Windows gets a guaranteed census plus best-effort P/Invoke
cwd, About gets a dismissible nudge, ak about ships, six configured cards.

Both bounded contexts join the context map and ubiquitous language, the ADR
index gains rows and narrative, and DASHBOARD.md documents the two new areas
for users — including how to read ">= N" lower bounds, "not measured yet",
approximate LOC, and the platform differences. README's command list gains
ak about and ak system.

The DDD docs lose their draft banners and record what shipped rather than what
was planned, including limits stated honestly: daemon budget state, the
statusline two-row join, and unattributable Codex bytes.

* fix(dashboard): strip mock annotations, report every version, unbreak Windows CI

Three fixes from reviewing the running dashboard.

Mock leakage. The design mocks were written as reviewer-facing artifacts with
their rationale inline, then handed to the UI agent as the visual spec, so it
faithfully reproduced the annotations along with the design. Removed 4 About
"Design note" blocks, 11 System "Why …" blocks, the "Two bars, two colour jobs"
note, all 13 chart-type pills ("radial gauge", "ranked bars", …), the About
footer, and the page-wide read-only footer, plus the CSS those left orphaned.
Kept every genuine data caveat — "line counts are approximate", "durations are
session span", "secrets masked server-side" — because those qualify the numbers
rather than explaining the design. Also dropped the hero's installed tally,
which restated each card's own chip less precisely.

Version chips: 6 of 9 components reported a version, for three different
reasons. driftReport() walks npm globals only, so a host installed by
mise/brew/native (Claude Code) was invisible to it; agentdb is a real global
but pinned to ruflo's bundled version, so it is deliberately excluded from the
update banner and fell out of the array with it; aidefence ships inside ruflo
and has no global install at all. foldKnownVersions() adds all three from
structured probes — never scraped from a status row's prose — as outdated:false
so noticeHtml (which filters on outdated) still shows an empty banner. The host
probe costs ~300ms, so it sits behind a 5-minute in-process TTL rather than
riding every 30s poll. Now 9 of 9.

Windows CI: the new absolute-paths assertion compared a fixture path against
the raw JSON body. A Windows path carries backslashes, which JSON escapes on
the wire, so the check passed on POSIX and failed on all three windows-latest
legs for a payload that was correct — the sibling assertions that compare
parsed values were green throughout. Compares the encoded form now.

* fix(dashboard): correct foldKnownVersions' JSDoc so tsc --checkJs passes

The CI quality gate went red on the previous commit: the param was annotated
Array<{pkg:string}> while the function pushes {pkg, installed, latest,
outdated}, so tsc rejected the object literal. The annotation was simply
narrower than the array driftReport() and the selfDrift/brain/ruvector folds
have always produced.

The return type keeps those fields OPTIONAL rather than required, because
incoming entries are passed through untouched — promising them as present
would be a second wrong annotation in the other direction.

* test(ui): defuse the dated fixture corpus, and match the hero's new contract

The UI suite went from 241/0 to 183/25 with no code change between the runs.
Cause: the fixture corpus is pinned to 2026-07-24 and the panel requests a
14-day window, so at 00:00 on 2026-08-07 the whole corpus aged out of its own
window. Proof, straight from the index: days=14 -> 0 sessions, days=30 -> 3.
One data-fixture check failed and 23 session-view assertions cascaded off it.

The kit suites avoid this by pinning `now` (usage-index.test.mjs says so in its
header), but this harness drives a REAL server against the real clock, so it
cannot. extendedCorpus() already copies the fixtures into a temp dir, so the
copy is shifted forward instead — by a WHOLE number of days, which preserves
every relative fact the assertions rest on: the 85-minute idle gap separating
the three time tiers, the worktree session nested inside another's span, and
each turn's local time-of-day for the punchcard's hour buckets. The checked-in
fixtures keep their literal dates, because the kit suites pin `now` against
exactly those.

The remaining failure was a real contract change, not a bomb: the hero no
longer counts detections, so asserting it says "unknown" tested behaviour that
was deliberately removed. It now asserts the actual contract — the hero states
only what ak MANAGES, making no detection claim in either direction, because
each card's own chip already carries per-component state and an aggregate could
only restate it less precisely.

* feat(system): wire the footprint collectors, fix a hard hang, expand reclaimables

The collectors from the previous run were built and tested but never called —
index.mjs had no owner, so nothing composed them into the payload. Wiring them
up is most of this change; the rest is what wiring them exposed.

Now live (verified against GET /api/system, not asserted):
  - projects: 50 ever seen / 25 on disk / 21 git repos, de-duped across hosts by
    resolved real path. Was 4, because discovery reused discoverRuvfloProjects()
    — which requires .claude-flow/neural/ state and answers a different question
    that Intelligence still depends on, so it is left alone.
  - consumers: 80 roots, top 20 ranked, grouped by ecosystem. The panel called
    npx cache the #1 consumer at 6 GB; it is #12. The real leaders were entirely
    unscanned — Ollama 141 GB, LM Studio 49 GB, Hugging Face 36 GB, npm's
    _cacache 22 GB. Overlapping roots collapse by path so the list cannot go
    self-similar (~/.npm never appears beside its own _cacache).
  - snapshot persists consumers; runtime is still absent, and that allow-list is
    what structurally enforces the ephemeral-census invariant.
  - refreshDeep finally takes includeProjectTrees, which the caller was already
    passing into a zero-arity function. Sticky across rescans; default off,
    because one repository here is 175 GB and flattens every other row.

A HARD HANG, found only because the wider discovery reached it: the deep scan
parked forever at 0% CPU inside a Dropbox tree. Cloud providers leave evicted
placeholders — stat returns instantly, read blocks in the kernel until the
provider materializes the bytes, which never happens while it is signed out.
There is no timeout, so the scan never completed and the dashboard served a
17-hour-old snapshot. Worse, reading them silently pulls the file down. Guarded
by the allocated-blocks basis the DDD already names (blocks === 0 && size > 0):
placeholders are stat-ed, never opened. Zero false positives across 3,667 files
here, including sub-2KB files that APFS stores inline. The tree that hung
forever now returns in 50ms; a full scan takes 204s.

Also: Playwright was invisible on macOS (the scan looked only at the XDG and
Windows paths, so 1.86 GB read as a measured zero), and the RuvNet Brain was
under-reported by 85% because only kb/ was measured — the other 11 GB is five
dated kb.bak snapshots, now broken out rather than silently folded in.

Reclaimables grew from two detectors to cover those backups, npm's regenerable
cache, orphaned transcripts, and browser downloads — split into two safety
tiers that never sum: 'regenerable' for caches a tool refetches on demand, and
'review' for things like mise's 8 node versions, where recommending deletion of
a live runtime would be worse than saying nothing.

* feat(dashboard): one project census, current models, reworked System area

Addresses a review of the local diagnostic panel. Four themes.

Project counting. Overview/Usage/Observability/System each discovered
projects their own way and reported four different numbers for the same
machine (4, 14, another 14, ~48). ADR-0027 makes discoverProjectSources()
the single census with four named scopes, and no surface may render a
project count without the sentence explaining what it counted. The
Intelligence panel now asks whether memory/intelligence has been
ACTIVATED (.claude-flow, .agentic-qe or .swarm, any host) rather than
whether ruflo has trained: 4 projects becomes 17. The learning scope
folds a repo's sub-directories and agent worktrees onto one identity —
without that, keying the picker off identity left 7 of 24 rows
unreachable. Retires project-discovery.mjs; registryWorkspaces() is
module-private again.

Models. gpt-5.4/gpt-5.4-mini retire from Codex on 2026-08-31 and
gpt-5.3-codex is already withdrawn, so execution routes to gpt-5.6-terra
and mechanical work to gpt-5.6-luna. RETIRED_MODELS substitutes a
withdrawn model at the read boundary — the one place a user pin is
overridden, because honoring a pin into a dead model fails the run — and
ak sync rewrites seeded routes naming one. claude-opus-4-8 is
deliberately NOT listed: it carries no deprecation notice, so it is
divergence, not retirement.

System area. Seven sub-views: Advisory and Sessions split out of Storage,
Advisory because it is the only part of System that suggests an action.
Storage lifts learning stores (99% of retained bytes) onto their own card
so the donut is legible, restricts the per-host split to real hosts, and
gives growth five axed sparklines. Catalog covers project scope across
every project on disk and gains kind/host filters. Projects lists only
repositories with a remote that a host has recorded a session in.

Honest degradation. Removes the AI-worker budget tile: no code path could
ever populate it, so it was a permanent "unavailable" rather than a
degradation. ADR-0023 gains §9 (a permanently unmeasurable quantity is
deleted, not degraded) and §10 (an excluded figure is still stated).

Also fixes eslint linting gitignored .ui-artifacts/, which failed
`pnpm run check` for anyone who had run `pnpm test:ui`.

* fix(dashboard): loosen the System layout and plainen the Advisory copy

Follow-up to fdd7052, all presentation — no collector or payload change.

Advisory copy. "Two safety tiers that are reported separately and never
added" stated an accounting rule before the reader had met the tiers, so
they now introduce themselves by what they mean to you. On the card,
"no total — pointers, not a sum" told you a number was MISSING without
saying why you should be glad it is; it now reads as a deliberate choice.

Catalog KPI. "232" and "skills" were on separate lines, so the tile's one
fact read as two. "skills" moves up beside the number as a unit — the
same treatment GB already gets on the byte tiles — at 12px, so it does
not inherit the 27px odometer size.

Summary band. A margin-top:-5px was actively pulling the projects liner
up into the KPI cards (5px of separation), and the disk strip carried
5px of padding around a 12px meter. Now 16px, 18px and 13px. That liner
is also capped at 120ch: two lines of dense accounting prose run the full
1440px, well past a readable measure.

Largest consumers. 110px showed about four rows, too few to read as a
ranking — you compared the top of the list against nothing. Sized for the
denser by-ecosystem mode where every row also carries a note: 9 full rows
there, 13 ranked.

Reclaimable rows. The removal hint was a bare .why div flush against the
rationale, reading as its next clause. Its own class now, with real
separation and the command in high-contrast mono, so the thing you could
run is distinct from the thing that was found.

* feat(dashboard): sortable headers on the System project footprints table

Every column sorts, one at a time, announced through aria-sort — exactly one
<th> is ever anything but "none". Clicking the active column reverses it;
clicking another takes over. Opens on project name, ascending.

First click uses each column's NATURAL direction rather than always
ascending: nobody opens a size column wanting the smallest project first, or
a recency column wanting the stalest. Project and language open ascending;
lines, disk and last-active open descending.

An unmeasured figure sorts LAST in both directions. These cells are
Measurement wrappers, so a row can carry `unknown` rather than a number, and
letting it rank would present an absent figure as a small one — the same rule
ADR-0023 applies everywhere else in the System area. A deliberately
unmeasured fixture row pins it.

The whole header is the button, so the target is the column's width rather
than a glyph, and the arrows always render: a control that only appears on
hover is invisible to anyone who never hovers, and a column that changes
width when pointed at is worse than no affordance.

* fix(test): canonicalise fixture roots the way the collectors do, and stop
asserting a POSIX-only encoding on Windows

Windows CI has failed since 2387a35 with 13 failures across three footprint
test files. Two distinct causes, both in the harness rather than the product.

realpathSync vs realpathSync.native. The fixtures canonicalised with the JS
realpath; every collector canonicalises with the native one. On POSIX these
agree, so it passed everywhere else. On Windows the JS realpath leaves an 8.3
short name alone (C:\Users\RUNNER~1\...) while the native one resolves it to
the long form the code under test returns (C:\Users\runneradmin\...) — the
same directory in two spellings, compared against each other. Fixtures now use
the native variant with the same `?? realpathSync` fallback the product uses.
footprint-collectors did not canonicalise at all.

A POSIX-rooted encoding asserted on Windows. Two tests build a Claude
transcript-directory name with `path.sep`, which yields `-a-b-c` on POSIX and
`C:-Users-...` on Windows. decodeClaudeProjectDir documents a drive prefix as
undecodable and returns null, so those tests were asserting the platform, not
the decoder. They are POSIX-only now, and a new test asserts the refusal
contract — including the Windows shape — on every platform.

Also fixes a real over-claim this surfaced. Because nothing decodes on
Windows, System > Sessions would have labelled EVERY row "deleted project"
there. labelSessions now reports which reason applies — `gone` for a
POSIX-rooted name that no longer resolves, `encoding` for a name that was
never decodable — and the panel says "name not decodable" rather than
asserting a deletion that did not happen.

* fix(footprint): the cloud-placeholder rule condemned every file on Windows

`fs.Stats.blocks` is a POSIX field. On win32 Node reports it as 0 for every
file, and `isCloudPlaceholder` read that as "a provider has evicted this" — so
every file was treated as dataless. Manifests were never queued, every source
file was skipped, and a scan returned no lines, no dependencies and no stack.
Eight of the thirteen Windows CI failures were this one gate; two independent
symptoms pinned it (`manifestsRead` 0 where 2 were present, and a skip count
one higher than the fixture's only binary file).

win32 is excluded from the heuristic. The cost is real and stated in the code:
Windows is where OneDrive Files On-Demand actually lives, so it is the platform
that most needs this check and the one platform that cannot have it — detecting
a placeholder there means FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS, which fs.Stats
does not surface. A Windows placeholder is therefore opened and may block,
exactly as before the heuristic existed. Reading one file slowly is
recoverable; measuring nothing at all is not.

`platform` is a parameter rather than a direct process.platform read, and the
predicate is exported, so the win32 branch is testable from any machine. The
end-to-end test shims lstatSync to report zero blocks — without that it would
pass with or without the fix, since real POSIX files have blocks — and an
anti-vacuity test asserts the damage is still reproducible on POSIX, so the
guard cannot quietly stop being load-bearing.

* fix(footprint): the same POSIX-blocks assumption in two more places

The previous commit fixed the cloud-placeholder rule in stack-detect but
missed a hand-rolled DUPLICATE of it in projectRemote, which is why Windows
CI went 13 → 8 → 2 rather than to zero. Both remaining failures were that
copy: `st.blocks === 0 && st.size > 0` on a .git/config, which on win32 is
true for every file, so every project reported status 'unknown' with no
remote URL. It now calls the shared exported predicate instead of restating
the rule — the duplication is what let the first fix look complete.

A third instance was latent rather than failing. measureAllocated derives
allocated bytes from per-file `blocks` and falls back to apparent size only
when the count is not finite. On win32 the count IS finite — it is 0 — so an
entire machine's allocated total came out as zero bytes beside a correct
apparent total. Its own doc comment already promised the Windows fallback
this commit actually implements. No test covered it, so CI was green on a
figure that would have been wrong on every Windows machine.

Each fix carries an anti-vacuity assertion: the identical zero-blocks input
on POSIX must still be read as a placeholder, and must still allocate zero.
Without those the platform argument could stop being load-bearing and the
tests would keep passing.
pacphi added a commit that referenced this pull request Aug 27, 2026
…lication bugs (#184)

* test+ci+lint: complexity-program P0 safety nets

Nets under the refactor tracks that follow, no behavior change:
- golden snapshot of `ak status` collect() for the offline fixture
  (row order, messages, and fix strings are load-bearing for sync's plan)
- dashboard Playwright UI suite wired into CI (was manual-only; the only
  rendering verification the dashboard has)
- complexity/max-depth/max-lines ESLint warnings over src+bin (visibility
  only; ratchets to errors per-directory as tracks land)

Claude-Session: https://claude.ai/code/session_01WMKwDpp14PPjj8Bn1g7Uza

* chore: sync worktree to refactor/complexity-program P0 safety nets

Cherry-pick e5f3d53 (golden snapshot test + fixture for status collect(),
CI wiring, complexity ESLint visibility warnings) — this worktree's branch
point predated it on refactor/complexity-program. No behavior change;
establishes the baseline Track D's task depends on.

* fix(live): recognize newer item_completed Codex message generation

The live adapter only handled the legacy user_message/agent_message event
pair, while the batch usage scanner (usage-index.mjs's codexEvent) already
decodes the newer item_completed envelope wrapping UserMessage/AgentMessage
items. A Codex rollout written in the newer generation therefore emitted no
session.input/agent.output live events and looked dead in the live view even
though the batch scan counted its prompts/responses correctly.

Teach adaptCodexRecord the same generation-detection the batch parser uses,
via a small codexMessageKind() helper mirroring codexEvent()'s dispatch.

* fix(live): reuse event-schema's inferredSignal instead of a duplicate guess

projection.mjs's signalKind() re-implemented its own action→kind mapping as a
fallback for events lacking event.signal.kind, and disagreed with
event-schema.mjs's inferredSignal(): the projection copy only recognized
presence/operation and defaulted everything else to 'metadata', missing the
'relationship' (agent.spawned/planned) and 'activity'
(session.input/agent.output/session.started) cases inferredSignal knows about.

createLiveEvent always stamps signal.kind today, so this fallback is a
defensive no-op in current code paths, but it was a second, independently
maintained answer to the same question and a wrong one if it were ever
exercised. Export inferredSignal and reuse it instead of the duplicate.

* refactor(status): extract host-detail rendering to its own module

Moves opencodeDetailRows, HOST_DETAIL_RENDERERS, renderHostDetailRows, and
admittedLifecycleFallbackRows out of status.mjs into a new
src/commands/status/host-detail.mjs, and extracts a shared row() helper into
src/commands/status/row.mjs.

opencodeDetailRows (CC 86) is also decomposed: the plugin/gateway/skill
artifacts shared a near-identical adoptable->foreign->absent->stale ladder,
each condition re-prefixed with !receiptState.adoptionBlocked (12
repetitions). Extracted a single artifactRow(subsystem, label, state, opts)
helper plus one early return on adoptionBlocked; the wiring-convergence and
agents ladders (which don't fit that shape) become their own small
functions. Result: opencodeDetailRows CC 86 -> 19, all extracted helpers
under CC 20.

Pure decomposition: no messages, ordering, or logic changed. Byte-for-byte
identical collect() output, verified against the golden snapshot.

* fix(providers): heal retired routes from ak host pick and ak setup

applyHosts -> seedActivityRoutesIfMultiHost -> applyAqeRouter ->
retireCodexMcp -> ensureRufloMcpInCodex -> applyProviders is duplicated
across host.mjs, sync.mjs, and setup.mjs, but only sync.mjs called
migrateRetiredRoutesInConfig — so `ak host pick` and `ak setup --project`
could persist a per-activity route naming a model the host has withdrawn,
left for the next `ak sync` to repair. Call it from both paths too, in the
same seed-then-migrate order sync.mjs already uses.

pick() and run_project() take an injectable `migrateRoutes` (defaulting to
the real migrateRetiredRoutesInConfig) purely as a test seam, since
routing.mjs's RETIRED_MODELS table is currently empty (no cited withdrawal)
and so cannot demonstrate a real rewrite end-to-end.

* refactor: decompose rufloActivationSegments into per-segment functions

rufloActivationSegments (statusline-footer.cjs) rendered nine independent
statusline segments (quota tee, SONA, LoRA, route-RL, proof, aidefence,
daemon, brain, QE) in one 437-line body with CC 193. Lift each segment
into its own top-level function taking an explicit ctx (fs/path/cp/os/
colors/cwd/stdin), split the LoRA block (session id, staleness, weight
recompute, pattern replay, formatting) into single-purpose helpers, and
split the RuvNet Brain and Agentic QE blocks into version/size/query
sub-helpers, since those two also exceeded the CC budget as single units.
The one real coupling (LoRA appends onto SONA's line) is now explicit:
rufloLoraSegment(ctx, learn) takes SONA's rendered string and returns the
combined line. rufloActivationSegments itself reduces to an ordered
segment-provider array plus a small assembler (CC 193 -> 9).

Normalizes the DIM/G/Y/C/R color constants from embedded raw ESC bytes to
\x1b escape notation (matching RED's existing style) — identical runtime
strings, safer to read and diff.

Behavior is unchanged: tests/statusline-segments.test.cjs (46) and
tests/statusline-brain.test.cjs (10) pass unmodified. All functions in
the file are now well under the repo's CC-25 lint warning threshold
(worst case 20, in the untouched rufloStatuslineDebug); the file no
longer appears in `pnpm run lint` output at all.

The emitted template remains one self-contained file within the
ruflo-seg:BEGIN/END markers, with no imports from outside the block.

* refactor(status): split collectDejaVuRows into per-concern functions

collectDejaVuRows (CC 95) mixed five concerns in one function: error
mapping, the install ladder, doctor health, the 6-way per-host target
ladder, and the derived-index ladder. Extracted dejaErrorRow,
dejaInstallRows, dejaDoctorRows, dejaTargetRows (via a dejaTargetContext
guard-clause helper to keep both under the CC budget), and dejaIndexRows
into src/commands/status/deja-vu.mjs; collectDejaVuRows is now a ~20-line
orchestrator that assembles their rows in the same order and keeps its
existing try/catch and exact exported signature.

Result: collectDejaVuRows CC 95 -> 21, every extracted helper under CC 25.
Pure decomposition: no messages, ordering, or logic changed. status.mjs
re-exports collectDejaVuRows unchanged for existing test imports.

* refactor(usage): share blankSession/addUsage between transcript sources

usage-opencode.mjs hand-mirrored the per-session record shape and the
(day, model) usage-row accumulator that parseClaude/parseCodex already
define in usage-index.mjs — its own comment admitted it was "mirroring
parseClaude/parseCodex exactly". Export both and have opencode's parser
build on them instead of a separate hand-written copy, so the three
transcript sources share one definition of "what a session record looks
like" and "how a usage row accumulates".

addUsage now returns the row it touched so a source with a per-source extra
field (opencode's observed costObserved) can set it without a second find().

Also re-anchors the usage-index.mjs file:line citations in
docs/USAGE-SCORECARD-METRICS.md and docs/TRANSCRIPTS.md that this shift
rendered stale (doc-citations.test.mjs).

* refactor(providers): extract convergeProviderStack shared pipeline

applyHosts -> seedActivityRoutesIfMultiHost -> migrateRetiredRoutesInConfig
-> applyAqeRouter -> retireCodexMcp -> ensureRufloMcpInCodex ->
applyProviders was pasted across host.mjs (pick), sync.mjs (run), and
setup.mjs (run_project). Extract the ONE pipeline into
providers.mjs's convergeProviderStack(cfg, cwd, options); each call site now
supplies only its own report/save policy via an injected `reporter`
callback (fired once per step, in order) plus a couple of per-site knobs
(`seedRoutes` — pick already seeded earlier in its own flow; `codexMcp` —
setup only runs the legacy/reverse Codex MCP steps while codex is enabled,
matching its pre-existing behavior; `runProviders` — sync wraps the
terminal call with its progress ticker).

Output strings, config-write ordering, and save-on-change gating are
unchanged at every call site; only the pipeline definition itself is no
longer triplicated.

* refactor(usage): decompose detectInsights into 13 independent detectors

detectInsights (CC=71) inlined 13 numbered, independent heuristics in one
~480-line body sharing only a windowCost/sessions prelude. Extract each into
its own detectX(ctx) function returning zero or one insight, collect them in
a DETECTORS registry, and rebuild detectInsights as prelude + flatMap +
the existing ranking sort. Output is identical: same firing conditions, same
text, same ranking (DETECTORS keeps the original numbered order, and sort is
stable).

detectInsights's own complexity drops from 71 to 7 (dominated by its
defensive-guard ternaries); each extracted detector sits well under the
project's CC 25 threshold.

Adds a direct unit test per detector via a new `_detectors` test-only export,
including first-time coverage for parallel-sessions, subagent-share and
long-session-share, which previously had no dedicated fixtures.

* refactor(status): generalize the section-registry pattern to all of collect()

collect() (CC 250, the worst function in the repo) inlined ~25 subsystem
concerns as ad-hoc try/catch + branch ladders. The file already had the
right pattern for exactly one concern (HOST_DETAIL_RENDERERS +
renderHostDetailRows); this generalizes it to the rest.

Each concern becomes its own module under src/commands/status/sections/,
exporting { id, collect: async (ctx) => Row[] } with
ctx = { cfg, cwd, pkgRoot, integrationFacts }. collect() is now two ordered
walks over SECTIONS_BEFORE_HOST_DETAIL / SECTIONS_AFTER_HOST_DETAIL (split
only because three existing calls -- collectDejaVuRows,
renderHostDetailRows, admittedLifecycleFallbackRows -- keep their own
bespoke signatures and error contracts between them, unchanged), each row
wrapped in a uniform try/catch that falls back to a generic
'<id> check unavailable' warn row. Sections that already had their own
try/catch (most of them) keep it verbatim for their exact original
message; the uniform wrapper is a backstop, and for the handful of
concerns that had NO try/catch before (security, learning, aqe, agentdb,
mcp, statusline, qe-court), it's a strict improvement: an unexpected throw
there now degrades one row instead of crashing all of collect().

The providers section (~8 sub-concerns under one try/catch, per audit) is
split into five sections -- providers-status, providers-external-intent,
providers-external-projection, providers-ruflo-models,
providers-local-bindings -- sharing a small computeProviderExternalState
helper (_providers-external.mjs) that each calls and catches
independently, so one probe failing no longer collapses all eight rows
into a single warn. Its drift-comparison logic duplicates write-side logic
in src/lib/providers.mjs by design for now; carries a
"TODO(complexity-program)" marker for a later cross-track re-homing.
The three-block codex-mcp concern and the four-block statusline concern
each become one section file with independently-caught inner functions,
preserving their existing per-probe error isolation.

Result: collect() CC 250 -> ~5 (a loop + a try/catch), status.mjs
1216 -> 118 lines. Every new section under CC 25 (worst is 21). Pure
decomposition: no messages, row order, or logic changed -- verified
byte-for-byte against the golden snapshot and all 48 status-command
behavior tests, plus the full status-aqe-drift and status-viability
suites.

* refactor(dashboard): split the 15-route request handler into a route table

dashboard-server.mjs's http.createServer callback was one if-chain closure
spanning ~680 lines (CC=194): 15 routes including two SSE state machines
whose reserve-slot/early-close/channel-open lifecycle was copy-pasted
verbatim three times. Split each route into its own named handler, dispatch
via an exact-path lookup table plus a small parametrized-route list, and
extract that shared SSE lifecycle into sse.mjs's new sseRoute() helper
(reserve-before-await, early-close forwarding, header/channel setup, and a
route-controlled activate()/setOnClose() for the parts that genuinely differ
per route). handleLiveEvents' own snapshot/replay reconciliation is further
pulled into a pure deliverLiveInit() helper.

Every route's behavior, security header set, and concurrency/TOCTOU
handling is unchanged — same 401/403/404 shapes, same SSE resumption and
dedup guarantees, same client-cap semantics. dashboard.test.cjs's 77 cases
(including the snapshot/replay race and TOCTOU regression tests) and the
Playwright dashboard-ui suite pass unmodified.

* docs: archive PR-131 consistency dossier and issue-110 swarm prompt

Both are implemented history: the Host & Provider Consistency master
review's decisions live in ADRs 0028-0031 (and its structural citations
predate the complexity-program refactor); the issue-110 session prompt
drove PR #179, recorded durably in ADR-0032. Renamed per the archive's
date-origin-topic convention and indexed in its README.

Claude-Session: https://claude.ai/code/session_01WMKwDpp14PPjj8Bn1g7Uza

* refactor(providers): applyAqeRouter as ordered surface reconcilers

applyAqeRouter (CC 111) braided five reconcilers (externalProviders,
managed fallbackChain, defaultProvider + its two ownership-receipt kinds,
agentOverrides, and the stale-override recompute) together via shared
mutable accumulators with implicit cross-surface feedback (externalActive
constrained what the later surfaces could reference).

Split into four (draft, ctx) => {detail, error, changed, ctx?} surface
functions folded over one draft via a small foldSurfaces helper; the one
real cross-surface dependency (externalActive -> refined
projected/staleOverrides) is now an explicit ctx patch instead of a loose
outer-scope `let`. Extracted the "nothing to apply" gate and the
stale-ownership-receipt pre-clear into named helpers, and the
externalProviders detail-line formatting into its own function, to keep
each surface's own branch count legible.

Also: change detection stringified `existing` twice for the same
never-mutated object (once before tagging `_managedBy`, once after) -
compute that snapshot once and reuse it for both compares.

CC: applyAqeRouter 111 -> 21; new surfaces land at 21-24. Output strings,
file-write conditions, and ordering are unchanged — the full suite (2265
tests, extensively covering this function's branches) passes unmodified.

* refactor: extract loopback-server.mjs for shared dashboard/admin plumbing

dashboard-server.mjs and admin-server.mjs each defined their own identical
readJsonSafe, minted their session token the same way, wrote the same
401/404 JSON response headers, and repeated the same
server.listen(...).then(resolve {url, urlWithToken, port, token, close})
boilerplate. dashboard-server.mjs also imported tokenMatches FROM
admin-server.mjs — a security primitive with no business being homed in one
specific server.

New src/lib/loopback-server.mjs owns all of it: mintToken/tokenMatches,
readJsonSafe, sendJson/sendUnauthorized/sendNotFound, and listenLoopback()
for the bind-to-127.0.0.1-and-resolve lifecycle (each server still supplies
its own close(), since dashboard's also tears down SSE clients and
background services). admin-server.mjs re-exports tokenMatches so its
existing public surface and tests/admin.test.cjs are unaffected.

Every security behavior is unchanged byte-for-byte: 127.0.0.1 binding,
token-in-fragment URL shape, DNS-rebinding Host guard, Sec-Fetch-Site/Origin
enforcement, CSP, and the 401/404 response shapes. dashboard.test.cjs and
admin.test.cjs pass unmodified.

* refactor(sync): ordered step registry replaces inline subsystem branches

run() (CC 110) was ~20 `if (subsystems.has(X)) { ... }` blocks inlined in
one function, with real ordering invariants (natives last among npm-tree
mutations, statusline after providers, kit self-update last of all) proven
only by source order and explained only in comments.

Replace with SYNC_STEPS: an ordered [{id, when(subsystems, flags, cfg),
run(ctx)}] registry. Array position is now the ordering invariant instead
of prose; `when` is a pure, explicitly-parameterized predicate so it can be
reasoned about independent of `run`'s side effects. `run(ctx)` receives the
per-invocation context (cfg, cwd, pkgRoot, flags, dejaVuAdapter,
subsystems, report, step, state) — `state` carries the two cross-step
signals (dejaVuApplyFailed, aqeRouterApplyFailure) the final convergence
check needs.

Output strings, config writes, and step ordering are byte-identical to
before; the full suite (2265 tests) passes unmodified.

CC: run() 110 -> 20; every step lands at 1-8 (the 'providers' step's
reporter callback, unavoidably multi-branch, lands at 22).

* refactor(telemetry): share wire-record decode between batch scan and live adapters

Batch usage scanning and live session adaptation each decoded the same
Codex and Claude transcript wire formats separately, and the copies had
diverged (the item_completed generation the previous commit fixed in the
live adapter is exactly this class of drift): session_meta/turn_context
extraction, the model_provider-vs-legacy-provider tolerance, Claude role
discrimination and content block-walking, and the tool call/result callId
tolerance were each implemented twice.

Add src/lib/telemetry-records.mjs with decodeCodexRecord/decodeClaudeRecord
as the one place each vendor's wire shape gets interpreted, plus the
resolveCodexProvider tolerant lookup, claudeText flattening and the
artifactName helper (previously duplicated verbatim in both live adapters).
usage-index.mjs's parseClaude/parseCodex and the live codex-adapter.mjs/
claude-adapter.mjs now all decode through these functions; aggregation vs.
event emission stay separate, reading whichever decoded fields they need.

Behavior-preserving with one deliberate widening: parseCodex now also
tolerates a bare legacy `provider` field on session_meta/turn_context
(previously only the live adapter did), unifying the "spelled two ways"
duplication the audit flagged. No existing fixture or test exercises that
field shape without model_provider also present, so this is not observable
as a regression; it makes batch and live agree instead of quietly disagreeing.

Re-anchors the usage-index.mjs file:line citations in
docs/USAGE-SCORECARD-METRICS.md and docs/TRANSCRIPTS.md that this move
rendered stale, including two that now correctly point at
telemetry-records.mjs instead.

* refactor(setup): decompose run/run_machine/run_project into named steps

run_machine (CC 44), run_project (CC 42), and the top-level run() (CC 56)
were each one long function walking a numbered-comment sequence of
install/heal/wire steps, several with early-return gates threaded through.

Extract each numbered step into its own named function (e.g.
installMachinePackages, applyMachineHostLifecycles, rufloProjectInit,
initProjectAgenticQe, resolveSetupTrust, finalizeSetupGuidanceAndMcp);
the three entry points become short linear call sequences with the same
early-return gates. Also extract providers.mjs's guidanceContext(cfg) —
the exact {flags:{dualMode, opencodeEnabled}} shape both `ak sync`'s
`blocks` step and setup's finalizeSetupGuidanceAndMcp build for
blocks.mjs's reconcileGuidance — so that shared shape is defined once.

Output strings, config writes, and step ordering are unchanged; the full
suite (2265 tests) passes unmodified.

CC: run_machine 44 -> 4, run_project 42 -> 7, run() 56 -> 21; every
extracted helper lands at 2-16.

* refactor(usage): extract scan()'s per-source concerns into named functions

scan() (CC=73) inlined provider-specific logic straight into the generic
scan loop: opencode discovery+health (coupled, since a SQLite read can fail
in ways a directory walk cannot), codex-only per-file diagnostics, opencode's
pseudo-key carry-forward with its mid-loop health mutation, and the codex
ledger resolution — three hand-built health objects and a comment elsewhere
in the file already conceding the hardcoded source triple as a known smell.

Extract each concern into its own function: discoverOpencodeSource,
processCandidate (the per-candidate parse+diagnostics step),
carryForwardCachedEntries (+ carryForwardOpencodeEntry, split out to keep
both under the complexity threshold), and resolveCodexLedger. scan() itself
is now the orchestration: discover, loop candidates, carry forward, write
cache, resolve the ledger, aggregate, assemble health.

Not a fully generic per-source descriptor array as literally suggested:
opencode's discovery is coupled to its health in a way the claude/codex
directory-walk sources aren't, and forcing a uniform {list, parse, health,
carryForward} shape over that asymmetry risked obscuring the real behavior
difference (opencode's carry-forward re-queues into `records`; claude/codex's
does not) rather than clarifying it. Named-function extraction gets the same
complexity reduction with lower risk of a subtle regression in a function
this load-bearing.

Behavior-preserving: same candidates, same cache entries, same aggregate,
same sourceHealth shape — verified against the full existing test suite,
including the scan-level cache/health/carry-forward tests. Complexity:
scan() 73 -> 13; extracted functions each land under 25 (discoverOpencodeSource
8, processCandidate 20, carryForwardCachedEntries 16, carryForwardOpencodeEntry
11, resolveCodexLedger 9).

Re-anchors two more usage-index.mjs file:line citations this shift moved.

* refactor(live): decompose reduceLiveEvent into phase functions

reduceLiveEvent (CC=81) was not a switch-on-type, but a monolithic merge
touching actor-node identity, session status, target node/edge, updatedAt,
and lifecycle all in one body. Decompose into cloneOrCreateSession,
mergeActorNode, applyStatus, applyTarget, resolveUpdatedAt, and
applyLifecycle, each owning a disjoint slice of the session it mutates.

Reordering is safe because the phases are largely independent: applyStatus
(presence/activity/workspace/project/evidence/session.status) reads only
`event` and the session's own prior fields, so its relative position versus
mergeActorNode does not change the result — verified against the full
existing projection test suite, which pins exact output shapes. applyTarget
still runs after mergeActorNode, matching the original's inline order, in
case a target id ever collides with the actor's own id.

The source.adapter==='codex-state' string-matching this function relies on
in three places is left as string-matching, not promoted to an authority
field: that would be a semantic change to how source authority is modeled,
and the fix's own rule is "prove byte-identical output or defer" — deferred,
noted in the track's final report.

Complexity: reduceLiveEvent 81 -> 12; extracted functions each land under 25
(cloneOrCreateSession 5, mergeActorNode 24, applyStatus 14, applyTarget 18,
resolveUpdatedAt 9, applyLifecycle 5).

* refactor(host): split pick() into parse/decide/apply stages

pick() (CC 144 pre-refactor; 121 after the earlier convergeProviderStack
extraction) welded flags-vs-readline input parsing, host/primary-host/aqe
validation, and the install/wire/converge apply step into one function
with a stdin dependency that made the decision logic untestable in
isolation.

Split into three stages: parsePickInput (delegates to
parsePickInputFromFlags / promptPickInputInteractively),
resolvePickDecision (host validation, primary-host resolution, admission
refresh, aqe selection validation via the extracted
validatePickAqeSelections, routing-policy construction — mutates cfg,
returns the resolved decision or an abort code), and the apply stage
(retireCodexOnDisable, installPickAbsentHosts,
applyPickOpencodeLifecycle split into enable/disable halves,
applyPickProviderStack using convergeProviderStack). pick() itself is now
the sequencing of these plus the handful of side effects between them.

Output strings, config writes, and step ordering are unchanged; the full
suite (2265 tests, including the real-spawn pick() integration tests and
the routing-retirement regression tests added for the earlier bug fix)
passes unmodified.

CC: pick() 121 -> 24; every extracted stage/helper lands at 1-25 (only
the pre-existing, untouched `status()` still exceeds 25 in this file).

* docs(providers): note pick/setup also heal retired routes

The retired-Codex-models section described only the retirement-rule
citation policy, not which commands apply the resulting route rewrite.
ak host pick and ak setup now run the same heal ak sync always has
(audit #1 fix) — state that plainly next to the existing citation note.

* fix(live): recognize item_completed in the Codex content-plane adapter too

adaptCodexTranscriptRecord (src/lib/live/transcript-adapter.mjs) had the same
legacy-only gap codex-adapter.mjs's item_completed fix addressed for the
status plane: it recognized only the legacy user_message/agent_message
event_msg pair, so a newer-generation Codex rollout surfaced no message
content in the transcript/playback view even though the status-plane adapter
(after the earlier fix) and the batch scanner both handle it.

Add an item_completed branch that decodes through decodeCodexRecord
(telemetry-records.mjs) rather than re-deriving the UserMessage/AgentMessage
item-type dispatch locally, keeping that wire knowledge single-sourced.
decodeCodexRecord joins multi-block content into one string, so this new
branch yields at most one message per item_completed event; the existing
legacy branch's per-content-block splitting (codexMessageText) is untouched
and unaffected.

Regression test mirrors the one written for codex-adapter.mjs: UserMessage,
AgentMessage (multi-block Text content), and an unrecognized item type
(which must yield no message, matching the "no encrypted reasoning or tool
bodies" contract this adapter already upholds for other unrecognized shapes).

* refactor(providers): share the routing-retirement report line, trim setup's reporter

Extract providers.mjs's reportRetiredRouteChanges(changes) — the identical
per-change print loop that ak sync's, ak host pick's, and ak setup's
convergeProviderStack 'routing-retired' reporters each carried inline
(same detail-string construction, same reportOutcome call) — so the
wording can never drift between the three, matching #2's "one shared
pipeline" goal for the report side too.

Also split setup.mjs's applyProjectProviderStack reporter (CC 30, over
the repo's complexity budget) into reportProjectAqeRouterStep and
reportProjectRufloCodexMcpStep, mirroring the same split already applied
to host.mjs's pick reporter.

Output strings and ordering are unchanged; the full suite (2265 tests)
passes unmodified.

* refactor(dashboard): split client.mjs's 4,066-line template literal into real modules

client.mjs's entire browser bundle lived as ONE template literal string
(export const JS = `...4044 lines...`) — invisible to node --check, ESLint,
and tsc alike (Finding 2 of the 2026-08 complexity audit). Split it along its
own section markers into 11 real, individually lintable/typecheckable
browser modules under src/lib/dashboard/client/ (bootstrap, overview,
intelligence, poll, usage, model-lifecycle, usage-orchestrators, about,
system-readout, system-projects, boot), each declaring real import/export
for its actual cross-file dependencies (wiring verified mechanically via
ESLint's own no-undef output, not hand-traced).

client.mjs is now a ~90-line COLLECTOR: it reads each split file's source,
strips the never-really-resolved cross-file import/export lines (concatenation
collapses the module graph into one flat scope, exactly as the pre-split
bundle already was), splices in the same Node-computed values the bundle
always carried (groups.mjs's functions/tables via .toString(), the About
directory via JSON.stringify — unchanged interpolation mechanism, just
relocated), and reassembles the exact same single IIFE. The serving contract
is byte-for-byte unchanged: page.mjs still does `import { JS } from
'./client.mjs'` and embeds one `<script>${JS}</script>` — same HTML response,
same CSP, no new routes.

Verified against a captured snapshot of the pre-refactor bundle's own
resolved output: the only diffs are harmless inter-file blank lines and two
deliberate `_`-prefixed renames of pre-existing dead locals (about.mjs's
joined/detected, system-projects.mjs's diskBar) that ESLint's first-ever pass
over this code surfaced. dashboard.test.cjs and the full Playwright
dashboard-ui suite (331 cases) pass unmodified.

Cross-file MUTABLE state (~26 names reassigned from more than one file, e.g.
usageView, SYSTEM) is declared as shared globals in eslint.config.mjs's new
client override rather than imported — real ES import bindings are read-only
from the importing side, which real-import would have made illegal. Each
split file also carries @ts-nocheck (stripped from the served bundle by the
collector): this code is never node-imported, so nothing in it should be
typechecked against node's lib, the same reasoning tsconfig.json already
applies to admin-view.mjs.

* refactor(dashboard): split styles.mjs's 1,309-line stylesheet per area

styles.mjs's inline CSS lived as one 1,309-line template literal, flagged by
max-lines (item #4 of the 2026-08 complexity audit — asset, not logic, so
lower priority than the client.mjs split it rides alongside). Split it into
four plain data modules under src/lib/dashboard/styles/ (base, usage, about,
system), each a pure `export const X_CSS = \`...\`` with no interpolation —
unlike client.mjs's browser modules these are real Node-imported modules, so
no placeholder/import-stripping mechanism is needed.

styles.mjs is now a small collector: it imports the four pieces and
concatenates them in the exact order the pre-split stylesheet always declared
them in, so cascade order and selector specificity are unchanged. Verified
against the pre-refactor CSS string: the only diffs are harmless blank lines
at the concatenation seams. Serving contract unchanged (page.mjs still does
`<style>${CSS}</style>`).

* docs(adr): add ADR-0036 for the dashboard client/loopback-server refactor

Records the sseRoute() lifecycle contract, loopback-server.mjs as the home
for loopback security primitives, and the readFileSync-concat module pattern
(generalized from ADR-0007's admin page precedent) now used to split
client.mjs and styles.mjs into real, lintable modules. Indexed in
docs/adr/README.md alongside the existing ADR narrative.

No other docs needed updates: DASHBOARD.md and the other cross-referencing
docs describe user-facing behavior, which this refactor does not change.

* docs: resolve ADR-pending references now that ADR-0036 is written

Two comments I wrote during the dashboard-server.mjs route-table split and
the client/ eslint override said "(ADR pending)"; point them at ADR-0036
now that it exists. No code change.

* fix(providers): kill the status/writer drift-comparator duplication (#129-shaped)

status/sections re-implemented the env-drift, aqe-router chain-order-drift,
and external-provider-intent comparisons whose write-side twins live in
providers.mjs (applyHosts, applyAqeRouter, aqeExternalProviderState) — the
exact failure shape issue #129 already shipped once. Move the comparison
logic into providers.mjs as read-only exports derived from the writer's own
code path:

- providerEnvDrift(cfg, env) — the same predicate applyHosts uses to decide
  whether to write, now shared instead of restated.
- aqeRouterDrift(cfg, cwd) — runs applyAqeRouter's own dry-run fold
  (buildAqeRouterContext + runAqeRouterFold, factored out of applyAqeRouter
  itself) and reads the fallback-chain slice of the result, replacing a
  hand-rolled approximation of chain validity.
- configuredAdapterIds / externalProviderIntent / providerExternalState —
  the external-AQE-provider intent-vs-live derivation, relocated verbatim
  from status/sections/_providers-external.mjs (now deleted) onto the
  library that owns the rest of this domain.

status/sections/providers-status.mjs, providers-external-intent.mjs, and
providers-external-projection.mjs now consume these exports instead of
recomputing their own view. Adds a parity test
(tests/kit/providers-drift-parity.test.mjs) that imports both the writer's
dry-run comparator and the live status row for a fixture with induced
drift, asserting they agree — so a future edit that reintroduces a second,
independently-derived comparison fails immediately instead of shipping a
silent divergence.

Zero behavior change: full suite (2289 tests), lint (0 errors), and
typecheck all pass; the status-golden snapshot is byte-identical.

* docs(adr): ADR-0037 — complexity program structural patterns and gates

Program-level record of the 2026-08-26 audit and refactor: sanctioned
structures (section registry, one provider pipeline, writer-owned drift
comparators, telemetry decode layer, segment providers), the lint gates
and their ratchet policy, and the residual backlog.

Claude-Session: https://claude.ai/code/session_01WMKwDpp14PPjj8Bn1g7Uza

* docs: archive the adapter-contract dossier and host-extensibility explainer

Both are artifact snapshots whose design shipped via ADRs 0028-0031;
renamed per the archive's date-origin-topic convention, indexed in its
README, and ADR-0031's companion links repointed.

Claude-Session: https://claude.ai/code/session_01WMKwDpp14PPjj8Bn1g7Uza

* test: fix Windows shim spawning and expected intelligence-503 in CI

withProjectCli hand-wrote a POSIX-only sh shim, so run_project() aborted
at `ruflo init` on Windows before the step under test — delegate to
withFakePath, whose shims carry .cmd/.ps1 twins. The UI suite's console
gate now ignores the intelligence endpoint's 503 on machines with no
ruflo-initialized project (CI runners), following its 404 precedent.

Claude-Session: https://claude.ai/code/session_01WMKwDpp14PPjj8Bn1g7Uza

* fix(dashboard): tolerate CRLF checkouts in the client bundle import-strip

A Windows checkout without eol pinning leaves `;\r\n` line ends, the
collector's import-strip required `;\n`, and a surviving import broke the
served classic-script bundle. Match admin-server's \r tolerance, and pin
source files to LF via .gitattributes so text-read/concat paths exercise
the same bytes on every platform.

Claude-Session: https://claude.ai/code/session_01WMKwDpp14PPjj8Bn1g7Uza
pacphi added a commit that referenced this pull request Sep 27, 2026
…eam watch) (#241)

* docs(audits): record verification and decisions for #237, #238 and #239

Adversarial verification of the reported sync, dashboard and tracker claims
against 847486c, the maintainer's decisions with the options offered, the
AgentDB and Ruflo memory alignment addendum, and the remediation plan those
decisions authorize.

* docs(audits): record the memory-location and install-transparency addendum

Three problems the upstream-reporting track returned as agentic-kit's own:
provider registration relocating Ruflo's memory root, Codex's Ruflo launcher
writing stores into system and tool folders, and the native-binding repair
editing Ruflo's install without a receipt. Records each decision with the
options offered and adds Stage 5 to the plan.

* test(telemetry): keep the hermetic export from reading real host sessions

The hermetic `ak telemetry export` test pinned HOME, XDG_CONFIG_HOME and
XDG_STATE_HOME but inherited XDG_DATA_HOME and XDG_CACHE_HOME from the
developer shell. OpenCode keeps its store at $XDG_DATA_HOME/opencode/
opencode.db (usage-opencode.defaultOpencodeDbPath), so on a machine that
exports XDG_DATA_HOME the export read the developer's real OpenCode session
and the test saw 1 session instead of 0. Pinning XDG_DATA_HOME alone takes
the leak from 1 to 0; running with every XDG variable unset passes on main.

The product is right to follow XDG_DATA_HOME, as OpenCode does. The gap was
the test sandbox, so no product code changes and Ruflo is not involved.

The test now plants a one-session OpenCode store under a hostile
XDG_DATA_HOME, so it fails on every machine without the fix (not only on
shells that export the variable), and the sandbox pins every XDG base and
drops CODEX_HOME/HERMES_HOME, as sandboxHome() already does.

(cherry picked from commit b9452a6c6ec7db24c67f2082d737da03aad657c0)

* test(isolation): run lifecycle sync and setup tests in a sandbox project

The baseline `pnpm test` rewrote the real repository's
.claude/helpers/statusline.cjs to the fixture's ruflo 9.9.9 and released
the real project's CLAUDE_FLOW_DB_PATH pin (leaving .ak-*-backup files).
HOME was sandboxed, but external-lifecycle's sync.run, setup.run_machine
and uninstall.run write relative to process.cwd(), which was the
repository root. uninstall-command and deja-vu-teardown-verify had the
same exposure: releaseRufloComponents adds rufloProjectRoot(cwd) to its
release set, and stepThisProject rewrites the cwd project's statusline.

isolateProject() moves each command-driving test file into a throwaway
git project and registers a tripwire that fails the file if the real
repository's guarded project files (settings.local.json, settings.json,
statusline.cjs, the helper stamp, llm-config.json, CLAUDE.md, AGENTS.md)
or their .ak-*/.agentic-kit-* siblings change. The #137 provider-cli
tripwire now uses the same guard. A census test fails when a test file
calls sync/setup/uninstall .run* without isolateProject().

(cherry picked from commit fe48cf1a3f1695cc9b7e17defcc5a4f222e7d1b5)

* fix(statusline): stop overwriting Ruflo's baked version

fixStatusline replaced the helper's baked `let ver` with the installed
ruflo version (a leftover from the v4 port). Ruflo bakes that value as a
floor and shows the highest version it finds at render time, so a value
ak wrote too high never corrects itself: a test fixture's fake ruflo
9.9.9 reached a real project and its statusline read "RuFlo V9.9.9".

ak now leaves the baked version exactly as Ruflo wrote it and only
strips its own legacy probe marker and (re)injects the footer. The
setup and sync lines drop the version they no longer write.

(cherry picked from commit 8d9003c088bd2e7c81bccb82fd552f0bf5cd05ee)

* feat(status): flag a statusline that shows a different Ruflo version than installed

Ruflo's helper bakes `let ver` as a floor and renders the highest version
it finds, so a baked value above every install (the leaked 9.9.9) pins
the statusline to a Ruflo version that is not installed and never
corrects itself.

`ak status` and the drift nudge now compare the helper's baked version
with the installed ruflo/@claude-flow/cli and warn when it is higher.
The sync statusline step repairs it through Ruflo's own refresh: it
clears the helper stamp so the forward-only refresh regenerates the
helper with Ruflo's baked value, then re-injects the footer. ak never
writes a version. A refresh that does not lower the version gets its
stamp back and sync warns with the manual edit; when the refresh cannot
run at all (.LOCKED, RUFLO_HELPERS_LOCKED, no hook-handler, no module)
the status row names the manual edit and plans no sync fix. A higher
runtime candidate (e.g. a newer marketplace checkout) is Ruflo's own
choice and is not flagged.

(cherry picked from commit 33bc698b2b91bb607039e34bb39a70e24ae732dd)

* fix(blocks): read guidance drift from the writer's dry-run in status and nudge

`ak status` and the post-command nudge rebuilt the guidance reconcile loop
with only {dualMode, opencodeEnabled}, so every `enabled` detector fell back
to a PATH, directory, or always probe, and retired rows were force-stripped
without the known-target universe. Both readers reported drift in either
direction that `ak sync` (which applies kit.json intent) would never act on,
for example "ruflo-aqe-reference→stripp" on a Claude-only machine with AQE
managed but not on PATH (#237).

reconcileGuidance now returns each target's raw per-row results, and the
status blocks section and localDrift render from its dry run with sync's
exact context. The drift label prints the writer's action verbatim, which
fixes "stripped" rendering as "stripp". The golden status fixture now shows
the AQE reference sync would upsert. ADR-0008 and UPGRADING note the shared
source.

(cherry picked from commit ee35f6e6a7707797c804449e1d96c8962baeef8f)

* fix(mcp): share one legacy-ruflo ownership predicate between status and register

Status classified every user-scope `ruflo` MCP entry as auto-migratable and
promised "sync migrates it", while register() removes only the exact
registration agentic-kit wrote (`ruflo mcp start`, env limited to
AGENT_BROWSER_CONFIG) and silently kept every other shape. Sync then printed
a success line and the legacy entry stayed, with the promise repeated on the
next status (#237).

legacyRufloDisposition(entry) is now the one predicate both use.
register() returns { ok, preserved[] } (never env values); the sync mcp step,
setup, and `ak x mcp` name each preserved entry with its manual
`claude mcp remove ruflo -s <scope>` command. The status row for preserved
scopes carries that command in its message and no fix, so sync never plans
work it will not do. ADR-0016 and TROUBLESHOOTING describe the boundary.

(cherry picked from commit 350b6a810e5a693ffc689448e38f691182688b7d)

* fix(codex-mcp): honor aqe:false in the Codex MCP topology row

The Codex MCP topology rows ignored kit.json, so a machine that opted out
of AQE (`aqe: false`) with Codex enabled was still warned that agentic-qe
is not registered in Codex and told to run
`aqe platform setup codex --overwrite --with-ruflo` for a tool it declined.

topologyRows now receives cfg and checks the Agentic-QE registration only
while AQE is managed, matching the aqe status section. The recursive-Codex
and duplicate-Ruflo checks are unchanged. ADR-0033 records the gate.

(cherry picked from commit 0123f857e8c7b718b8d1981dbe474e7b59788cfe)

* fix(status): mark manual-only remediation rows so sync never plans them

A status row's `fix` meant "sync does this" in some rows and "you must do
this" in others (run `ak x verify aqe`, repoint a memory pin, log in,
remove a registration agentic-kit does not own). Sync planned every row
with a fix, so advisory rows became sync actions no step performed, and
sync still reported convergence (#237).

Rows now carry a repair contract: row(subsystem, level, message, fix,
{ repair }) sets repair to 'sync' (default for a fix), 'manual', or null
without a fix, and rejects unknown values. Sync plans only 'sync' fixes and
counts manual ones instead of printing "all subsystems healthy". Text
status prints "→ manual:", the dashboard tags the fix `manual`, and
`ak status --json` and /api/status carry the field.

Marked manual: the preserved legacy-ruflo MCP scopes (the removal command
moves from the message into the fix), the AQE verify hint, memory-pin,
host login, model-lifecycle advice, the opencode JSONC/catalog/ledger
rows, and the Codex agentic-qe and user-owned codex mcp-server rows. The
Codex recursive and duplicate rows are 'sync' only when sync's confirmed
repair would clear them (codexMcpRepairOutcome); custom tables are manual.

A census test scans every row() call and fails when a subsystem can emit
a 'sync' fix that no SYNC_STEPS step handles. It found `statusline/cve`
planned with no step; the statusline step, whose fixStatusline injects that
overlay, now runs for it. ADR-0023 §11, the ubiquitous language, README,
DASHBOARD, UPGRADING, and the status/sync help text describe the contract.

(cherry picked from commit fe35bc5171a69b176b6de2338914e49a3b03863c)

Integration (I1): tests/kit/status-repair-contract.test.mjs now calls isolateProject() at module scope; lane H's project-isolation census (5618e972) flagged its sync.run calls. The file's own inProject() chdir is unchanged.

* refactor(agentdb): retire the standalone agentdb install and harvest's skill step

ak installed a second, standalone global agentdb for `ak x harvest`. It was a
worse duplicate of the copy Ruflo bundles (it lagged the bundled version and
ran on the WebAssembly fallback), its install ran on every sync while harvest
stayed off by default (#237 §3, an EEXIST loop when another package owns the
bin), and `agentdb skill consolidate` read ./agentdb.db, a store no Ruflo
writer uses, so it consolidated nothing. Decision A of the 2026-09-26 audit:
ak facilitates and monitors what Ruflo does; it does not run parallel copies.

- setup, sync and heal no longer install or repin agentdb; status emits no
  agentdb row; nothing is uninstalled
- a kit.json `agentdb` key is recognized as retired: preserved, ignored, and
  no unknown-key warning
- harvest runs only Ruflo verbs from the project memory root with the project
  memory pin; `--distill` runs `ruflo memory distill run --db <root>/.swarm/memory.db`
- `ak x verify harvest` seeds nothing and isolates CLAUDE_FLOW_DB_PATH,
  CLAUDE_FLOW_MEMORY_PATH and AGENTDB_PATH inside its temp dir; it fails when
  ruflo is missing instead of skipping
- About (CLI and dashboard version fold) reports Ruflo's bundled agentdb
- docs: README, MANAGED-TOOLS, INSTALLATION, TROUBLESHOOTING, MAINTAINER,
  explainer, UPGRADING entry, ADR-0026 amendment

tests/agentdb.test.cjs and tests/harvest.test.cjs are retired (their subjects
are gone); tests/kit/agentdb-retirement.test.mjs replaces them.

(cherry picked from commit 1f76eaab67dd5293d15bd3d31f2c3a960047a76b)

Manifest: package.json scripts.test drops `&& node tests/agentdb.test.cjs` and `&& node tests/harvest.test.cjs` (both files are deleted here).
Integration (I1): README status row keeps A1's `→ manual:` wording with the agentdb subsystem removed; UPGRADING keeps both 2026-09-26 entries (this one first); status-golden drops the agentdb row on top of A1's repair keys.

* fix(brain): refresh existing installs through the updater; stamp only the observed release

Sync refreshed every existing Brain with the fresh-install path plus
--force. The installer refuses that for a Brain with private stores, after
downloading the whole bundle, so each sync repeated a doomed download
(#237 §4/§C). It also stamped the release it asked for, not the one on disk.

The heal now chooses the path from disk. When the KB ships its own updater
(kb/forge-update.mjs, the installer's precondition for --update) it runs
`npx -y ruvnet-brain@latest --update --no-nightly-prompt --no-telemetry` with
RUVNET_BRAIN_NO_UPDATE_FALLBACK=1, because the installer's fallback is a fresh
--force install that drops ak's opt-out flags. A present bundle without the
updater keeps the pinned --force reinstall; nothing installed gets a pinned
fresh install. `--update` ignores --version, so every path stamps only the
release it then reads from SOURCE.json; an update that exits 0 with the
release unchanged is degraded and stamps nothing. Sync no longer passes
{force:true}. Installer failures lose ANSI codes and keep the installer's
remediation hint.

(cherry picked from commit 881ccc98782885bb6877d584c6468960cbfb1936)

* fix(brain): hold a refused refresh as blocked until the release pair changes

When the Brain installer or the bundle's own updater refuses a refresh (a
private-overlay preflight, a stale updater's walker defect) the cause is on the
Brain side, yet status kept `fix: 'sync refreshes the KB'`, so every sync ran
the refused refresh again and failed the same way (#237 comments, B-deps D3).
With commit "refresh existing installs through the updater" alone, the loop
would only have changed its message.

A deliberate refusal ("install stopped:", "[forge-update] ERROR:", "refusing
to update", a missing updater), or an updater run that leaves the installed
release unchanged, is recorded under versionCheck.ruvnetBrain.heldRefresh with
its cause and the (installed, latest) pair it was refused for. While that exact
pair stands, the status row stays a warning with the cause and the user's
options and no sync action; either release changing is a new attempt, and a
successful install clears the hold. Transient failures (network, timeout) and
first installs are never held.

Docs: TROUBLESHOOTING row for the held refresh; ADR-0033 update note and
decision 8 sentence.

(cherry picked from commit a9fdedfcf51d472389c6fcf82802c54d9a7b58ea)

Integration (I1): ADR-0033 header conflict with A1 (0123f857) resolved by keeping A1's Updated 2026-09-26 line and its Earlier-update line; this commit's 2026-09-26 update-note paragraph and decision-8 sentence are kept.

* fix(natives): keep the native probe cause; separate unavailable from inconclusive

The Ruflo memory-runtime load probe reported the last stderr line of an
uncaught throw, which is Node's own "Node.js vNN" banner, and status called
every failed probe "WASM fallback" with the fix "sync builds the native
binding" even when sync's heal would do nothing (a binding file that exists
but will not load) or when the probe merely timed out.

The probe child now reports a load failure as one tagged JSON line and
exit 2, so the probe returns native, unavailable (with the load error, paths
reduced to file names before the 160-char cap) or inconclusive (timeout,
crash, spawn error). A timeout is detected only through the probe's own
abort signal and retried once; run()'s timeout is a backstop behind it.

Status emits one row per context that is not native. Unavailable fails and
names the cause; the sync fix appears only when the binding file is
missing, which is what the heal builds. A present binding that will not
load names the manual rebuild with no sync fix. Inconclusive is a warning
with no fix, so a slow machine no longer fails sync's convergence proof.

(cherry picked from commit bdac1674539398b79c1a2151aeb18ee2a9a73926)

Integration (I1): ADR-0023 Updated line conflicted with A1 (fe35bc51, §11 repair contract); resolved as one 2026-09-26 Updated line naming both the §11 contract and this native runtime probe amendment. Both sections are kept.

* fix(natives): rebuild a binding that exists but will not load

Status load-tests the better-sqlite3 that Ruflo's memory runtime resolves,
but sync's natives heal skipped any context whose binding FILE existed. A
binding built for another Node ABI (the usual state after a Node major
upgrade) or a damaged one therefore failed status on every run while sync
reported "already native everywhere" and made no npm call.

The heal now uses the same load test for a present binding and rebuilds it
only when the probe proves it will not load (unavailable), never on an
inconclusive probe, so a slow probe cannot trigger a rebuild in Ruflo's
tree. It removes the old file first: prebuild-install extracts over an
existing file in place (tar-fs createWriteStream), and a process started
before a Node upgrade can still map it. The rebuild counts only when the
load test then passes; a rebuilt file that still will not load is a failed
heal that names the load error. The status row for this state now carries
the sync fix. A missing binding keeps the existing build ladder unchanged.

(cherry picked from commit 3290ca64c8fe5ea21990cf086e4b04170c8382a4)

Integration (I1): ADR-0023 Updated line conflicted again (A1 §11 + B2 commit 1); resolved as one 2026-09-26 line naming the §11 repair contract, the probe split, and this commit's shared load test in the natives heal.

* fix(sync): report promised repairs that did not converge

After the apply phase, sync kept only fail-level rows, a deja-vu special
case, and recorded apply failures. A planned warn row whose step returned
ok while its fix still stood was dropped, so sync printed "converged" and
exited 0 with the repair pending (#237 section F).

The post-apply verdict now reports as unresolved every planned
(subsystem, fix) that is still present after sync re-collects status, and
every planned subsystem that no SYNC_STEPS step (or the host-alignment
tail) performs. Each prints "unresolved: [subsystem] fix - reason" and
sync exits 1. The proof uses the plan's own admission test (a fix that is
not manual), so a row cannot enter the plan under one rule and escape the
proof under another. Manual rows never enter the plan and fix-less
advisories carry no fix, so neither can fail sync. The verdict moved into
convergenceVerdict/reportVerdict, which drops run()'s complexity from 32
to 22.

The new check exposed one row that promised a repair no step performs:
natives "no agentdb locations found under global ruflo" with fix
"setup/sync installs ruflo". Sync installs a missing ruflo only through
the versions row, and nothing restores agentdb inside a present ruflo. A
present ruflo now gets a manual reinstall; an absent one defers to the
versions row with no second plan item. The status golden fixture changes
only that row.

Docs: ADR-0033 decision 9, update note, consequence and verification;
UPGRADING dated section (exit code 0 -> 1 for scripts and CI);
TROUBLESHOOTING row; README sync row; DDD term "Unresolved repair";
ak sync --help.

(cherry picked from commit 6a26372db371ecf61b0f2721604b368286a02a74)

* feat(sync): add --skip for one-run subsystem exclusions

Sync had no way to leave one component out of a run (#237, #239 P1).
The kit.json opt-outs change ownership and have side effects (aqe:false
and ruvnetBrain:false also strip guidance), and --no-upgrade withholds a
whole class of upgrades, not a component.

--skip SUBSYSTEM is repeatable (or comma-separated) and validated against
the subsystems sync knows plus every lifecycle host; an unknown name
exits 2 with the accepted list before anything is collected. A skip acts
in the three places decision D5 names:
- the plan: a skipped subsystem's items leave the plan, and so does a
  fix only a skipped step performs (statusline/cve when statusline is
  skipped), so running the rest cannot report it unresolved;
- the steps: a step never runs when the subsystem it repairs is skipped,
  even when another planned subsystem triggers it (natives on versions or
  security, providers on routing or codex-mcp); removing the subsystem
  from the plan already stops the triggers it would derive, host-lifecycles
  checks each host, and the codex-mcp reconcile and host-alignment tail
  passes honor it too;
- the proof: skipped items and a skipped subsystem's failing rows print
  "skipped by request" and never count as unresolved or failing. An empty
  plan after skipping no longer claims "all subsystems healthy".

A census test proves every subsystem a step's `when` names is accepted
and every accepted name has a step that performs it. run() stays under
the complexity threshold (the tail moved into runTail).

Docs: ak sync --help; README command list and sync row; INSTALLATION
update table; UPGRADING and TROUBLESHOOTING live-session advice
(--skip versions); ADR-0033 decision 9, update note and verification;
DDD "Unresolved repair" entry.

(cherry picked from commit 3ca275ca81a88fba293ca4e7946e0d91bdd01db4)

* feat(sync): emit one JSON result with --json

`ak sync --json` was declared and documented but never read, so it printed
the ordinary human output (#237 review, decision D6).

With --json, everything sync would write to stdout (ok/warn/fail/info
lines, the plan listing, prompts, the progress ticker) goes to stderr for
the whole run, and stdout carries exactly one JSON object, pretty-printed
like `ak status --json`:
  { plan[], steps[{id, ok, detail}], unresolved[], skipped[], converged,
    exitCode }
plan and skipped items use status's row fields; each unresolved item
carries a reason (not-converged, no-step, failing, apply-failed,
declined), so the fail-level rows and apply failures that text mode prints
as "still failing" are not lost. A step is listed when it runs; it is not
ok when a result it reported failed, it printed a failed sub-surface, it
recorded a failure in the run state, or it threw, and its detail is what
it printed. converged is null when the run stopped before a verdict (a
dry run with a plan, a rejected --skip, an error); a rejected flag or an
error also sets `error`, and an error still yields the one JSON object.

run() now fills one result object on every exit path (converge() holds
the old body). Text mode is unchanged. The synthetic aqe-embedding plan
item is built with row() so every plan entry has one shape. Tests spawn
sync (a child with a stubbed collector, and the real CLI for --dry-run and
a rejected --skip) and parse stdout as a single JSON value while the human
text arrives on stderr.

Docs: ak sync --help; README command list and sync row; UPGRADING dated
section with the result shape; INSTALLATION CI guidance; ADR-0033
decision 9, update note and verification.

(cherry picked from commit af88a3c1cc5a9c8a0d2699ec079b189c2556d51d)

* fix(aqe-embedding): say Ollama is not running when it is installed

The local-provisioning catch swallowed the fetch error and always printed
"Install Ollama ... and start it", so a user with Ollama installed but
stopped could not tell the two cases apart (#237, audit item S4).

Read only the refused-connection code (TypeError('fetch failed') whose
cause carries ECONNREFUSED, including the AggregateError for localhost)
or the probe's endpoint-unreachable reason. When the selected endpoint
is kit-provisioned Ollama and the ollama command is on PATH, say it is
installed but not running at the loopback endpoint and how to start it;
otherwise keep the install guidance. The install check runs only on that
path, never for other failures or external endpoints, and raw service
text is still never reflected.

Docs: AQE-EMBEDDINGS, SETUP, TROUBLESHOOTING; ADR-0055 updated line.
(cherry picked from commit 208b1856c9d34336d2bb9caccd83a64febf98370)

* feat(status): remember the last live check and show it with its age

ak status reads configuration only, so it showed the AQE embedding
backend as "configured-unverified" information while ak sync failed the
live request (#237 S4, audit decision 9a). Status never learned what
sync found.

Add one evidence store for live results (src/lib/live-check-evidence.mjs):
a per-check file under the kit state directory holding passed, failed or
inconclusive, a bounded reason, the source and the time, keyed by a hash
of the inputs the check ran against. Sync's aqe-embedding step and the
ak x verify suites (mcp, memory, security, providers, deja-vu, and the
aqe live embedding request) record into it; a skipped proof records
nothing. The status aqe-embedding row reads it and shows the result with
its age: a failure is a warning (never fail, which sync's convergence
proof counts), a fresh pass is ok, a stale pass is information, and a
different backend selection marks the result as changed. Status still
never probes and never writes.

Verify keeps its output unchanged; a scoped output capture reads a failed
proof's first failure line as the remembered reason.

Integration (I2): ADR-0055 header conflict with lane C 208b1856 -- both
2026-09-26 Updated lines kept (C's Ollama line first). ak status --help
conflict with A1 fe35bc51 -- G1's "never runs a live check" wording and
A1's "→ manual:" / repair wording combined into one paragraph.

(cherry picked from commit 1de21fe16f3a3b9a9526d4b2042b4dec8ad1d077)

* feat(status): add opt-in --live running the quick, free verify checks

The maintainer asked for an opt-in way for ak status to test live
services (audit decision 9b). Plain status and the dashboard stay
probe-free.

ak status --live first runs the quick, free ak x verify checks, reusing
the suite functions rather than a second copy: the AQE embedding request
for a kit-managed backend (sync's gate), Codex MCP initialize/tools-list
when Codex is enabled, provider wiring, the security packages, deja-vu's
structural proof when enabled, and a memory round trip in a temp dir.
They run in parallel, each with captured output and its own timeout; a
timeout or a check that cannot run is inconclusive, never failed. A
scoped default abort signal in exec.run stops a timed-out check's child
processes so its own temp-dir cleanup still runs. Results are remembered
as status-live evidence, a new live-checks section shows every
remembered result with its age, and --live --json adds a live array.

A disposable-HOME run showed that AQE 3.14.3 `aqe health` initializes a
.agentic-qe store in its working directory. The provider check (also used
by ak x verify providers) now runs it only where .agentic-qe exists.

Integration (I2): ADR-0055 header conflict -- lane C's Ollama line kept;
G1's own 2026-09-26 line replaced by this commit's extended --live
wording (as on lane/G1). ak status --help conflict with A1 fe35bc51 --
G1's "Without --live it is read-only" wording plus A1's "→ manual:" /
repair wording in one paragraph.

(cherry picked from commit b538debfee2511e440912502a649bc1f7b0bcf8e)

* fix(aqe-embedding): accept unrelated dotted root TOML keys

Any dotted or quoted root assignment in Codex config.toml (for example
tui.status_line = ["model"]) made the AQE embedding editor refuse the
whole file, so the endpoint projection never converged (#237, audit P1).
Meanwhile an inline AQE registration under [mcp_servers] or a root
`mcp_servers = { ... }` passed the bare-key guard and read as absent.

Table headers and assignments now share one TOML key decoder that
splits dotted keys, keeps dots inside quoted segments and decodes every
TOML 1.0 escape (including 8-digit \U, which JSON.parse rejected). At
the root and under [mcp_servers], an assignment is skipped unless its
decoded key path can define the AQE registration; those forms (inline,
dotted, quoted or escaped) are refused as conflicts. Keys inside the AQE
tables keep the existing refusal. The helper keeps the editor's
complexity at 23.

Visible change: an inline AQE registration now reports a conflict
instead of absent; UPGRADING carries the note. Table-driven test covers
the 26 audit cases plus unrelated dotted servers and escaped headers.

Integration (I2): ADR-0055 header conflict -- this commit's 2026-09-26 line
appended after the Ollama (C) and live-check (G1) lines. docs/UPGRADING.md
conflict -- the four earlier 2026-09-26 sections (agentdb, status rows,
sync --json, unresolved repairs) kept; "AQE embedding edits in Codex TOML"
placed after them, directly above 2026-09-10 as on lane/C.

(cherry picked from commit 0eaa1bab969ef090301003c82fa9854c55932bc0)

* fix(aqe): share one AQE MCP transport recognizer across all hosts

ak edits AQE_EMBEDDER_ENDPOINT only inside AQE registrations it
recognizes. The #230 allow-list accepted aqe-mcp and the pinned npx form
for Claude and Codex, while OpenCode's separate check accepted only
['aqe-mcp']. A correct manual entry such as `/opt/homebrew/bin/aqe mcp`
stayed "unrecognized AQE MCP transport preserved" with no instruction,
and the same command got different verdicts per host (#237, audit P2).

Per audit decision 3, one recognizer now serves Claude, Codex and
OpenCode: aqe-mcp with no args; aqe, agentic-qe or aqe-v3 with exactly
["mcp"] (one CLI in agentic-qe's bin map whose mcp command starts the
same server); npx with exactly -y agentic-qe@latest mcp; npm .cmd shims,
case-insensitive with Windows separators on win32. Extra flags,
subcommands and wrappers stay preserved. OpenCode's array command goes
through the same rule.

Table-driven test per platform, plus a cross-host test proving Claude,
Codex and OpenCode agree for each shape. ADR-0055 amendment; AQE
embeddings and UPGRADING describe the recognized commands.

Integration (I2): ADR-0055 conflict at the end of the file -- G1's
"Amendment 2026-09-26: remembered live checks" kept, and this commit's
"Amendment — 2026-09-26: recognized AQE start commands" appended after it.
The header Updated line and the UPGRADING paragraph merged cleanly.

(cherry picked from commit 11d5e4ebc940df3ecd0efe45dea493eaaef1b301)

* fix(aqe): treat live RVF lock contention as busy in verify

On a live RVF lock, agentic-qe 3.14.3 logs the busy warning, then
falls through to a create attempt that fails with FsyncFailed; store
and lock bytes are unchanged (agentic-qe#574; PR #719 is partial).
classifyAqeStartup checked FsyncFailed first, so `ak x verify aqe`
reported AQE as failed whenever its MCP server held patterns.rvf
(#237/#239, audit U3k).

Per audit decision 7, only the exact three-line sequence is contention:
the lock warning, the live-owner quarantine refusal (emitted only from
AQE's live-owner branch) and FsyncFailed/0x0303. It skips the
FsyncFailed rule, still yields to a failed embedding initialization,
and reports busy with an honest reason. A bare FsyncFailed, or a lock
warning plus FsyncFailed without the middle line, still fails.

TEMPORARY: the rule and its test cite #240 and agentic-qe#574/#719 and
are removed once the AQE release carrying #719 is the kit floor.
Refs #240.

(cherry picked from commit 9069298d7766aea67f82054035ec10f8ff7e95a7)

* fix(status): name the unreviewed Brain hook delta and the options

Brain 4.3.28 added one automatic hook, and the plugin row only said the hooks
"differ from the exact reviewed 4.3.17/4.3.26 contracts" (#237 #6): a
permanent warning with no hint of what changed or what to do.

An unqualified hook set is now compared with the nearest reviewed contract and
the issue names each added, removed or changed hook by event and shim action.
For 4.3.28 that is `adds UserPromptSubmit capacity-aware-parallel-work`, with
the reviewed fact that it is declared offBehavior "run" (keeps running when the
Brain is off) and can inject "launch actual workers now" (plugin 4.3.28
scripts/hook-shim.mjs:89). The row lists the options: keep it until an ak
release reviews it, `claude plugin disable ruvnet-brain@ruvnet-brain`, or
"ruvnetBrain": false. Per decision 4 there is still no 4.3.28 contract, and the
row keeps fix:null because sync cannot review a hook.

Docs: TROUBLESHOOTING row for the hook-delta warning.
(cherry picked from commit 89ecdaf09e9311153da4efda15b17ce0c978eec5)

* fix(status): list options for an out-of-range external agent-browser

An external agent-browser outside Ruflo's >=0.27.0 <0.28.0 range is
preserved by design (ADR-0043 section 3), so the row has no sync fix and
stayed amber forever with no guidance (#237, audit D5).

The external branch now says Ruflo's browser tools may not work with
that version and lists the options: install a Ruflo-compatible 0.27.x
yourself, or set agentBrowser: false in kit.json, naming what that
gives up (ak's trusted browser config for Ruflo MCP and the readiness
checks). The fix stays null, so sync never plans it; the kit-owned
branch and its sync fix are unchanged.

Docs: TROUBLESHOOTING row, MANAGED-TOOLS boundary, ADR-0043 updated line.
(cherry picked from commit 311ae7a4be4522ee3a6051b20820f24d90fecee7)

* fix(status): point non-git project hints at ak setup --project

The memory and learning rows told users in a non-Git folder to "run
setup here", but plain ak setup there configures the machine only
(#237 setup item 1, audit N3). Following the hint changed nothing about
the project the row described.

Move setup's project-scope rule (a .git entry in the directory itself,
never HOME) into one shared predicate that setup now uses, and build the
status hints from it: a repository root keeps the plain hint, a folder
outside any repository names `ak setup --project`, and a repository
subdirectory names the repository root or --project. Setup's own
"not inside a project" line names --project too.

(cherry picked from commit bea8c1555de80b825deaa6c20c3cf846980b57f3)

* fix(live): parse ps paths containing spaces; classify desktop-hosted Claude CLI

The POSIX header survey ended its regex with (\S+), so every row whose
comm= path contains a space was dropped (72 of 970 rows on the maintainer's
machine), including the Claude Code CLI that the Claude desktop app runs from
~/Library/Application Support/Claude/claude-code/<ver>/claude.app. Once such
rows parse, three follow-ons had to change with it:

- hostFromCommand treats a known executable path that contains spaces as
  argv[0], so a Codex `mcp-server` under a spaced path is still excluded and
  a Node launcher under a spaced path still names its host script.
- controllerKind classifies the desktop-hosted Claude CLI as a
  project-session before the generic .app/Contents/ desktop-app rule.
- rootControllers no longer lets a desktop-app ancestor absorb a
  project-session it hosts; the app's own services (app-server) still fold
  into the app, so the census does not grow new app-service roots.

Windows parsing is unchanged. ADR-0012, the machine-footprint DDD and
OBSERVABILITY.md describe the new classification. Refs #238 (item 3).

(cherry picked from commit 3408566d128f3c397789224f537aac968eb639d6)

* fix(live): report live source health truthfully

A registered --live-source file that did not exist showed "1 file / 0 events
/ 0 errors / ok", and so did an unreadable one. The tailer returned silently
on ENOENT, #reconcile set "ok" after every tailer pass (overwriting the
"degraded" an error had set in the same pass), a file created after start
was read from its end so its first records were lost, schema-invalid
structured records vanished without a count, and the client only counted a
status ("error") the service never emits.

- JsonlTailer exposes presence (absent/readable/unreadable), probes
  readability when nothing new is pending so a mode-000 file cannot pass as
  healthy, reports an I/O failure once per transition, refuses to open a
  non-regular file, and reads a file created or recreated after tailing began
  from its first byte (this also applies to startOffset and startAtEnd).
- The service recomputes tailed-adapter health after each pass from the
  files: readable/missing/unreadable gauges plus accepted/rejected counts,
  lastAcceptedAt and a fixed lastRejection code. Status precedence is
  degraded > awaiting-file > no-events > ok; a bad latest record stays a
  fault until a later record from that file is accepted.
- structuredRecordRejection() is the one acceptance rule shared by the
  adapter and the diagnostic; it never echoes record content.
- The Sources toggle counts degraded (and legacy error) as issues and names
  sources awaiting a file. It does not count "unavailable": the Codex state
  ledger is unavailable on every Claude-only machine.

`files` keeps meaning tailed files here; the next commit recomputes it.
Docs: OBSERVABILITY (source health, --live-source reads an existing
producer's file), TROUBLESHOOTING, DDD AdapterHealth, ADR-0012, CLI help.
Refs #237 (section E).

(cherry picked from commit 5faa8cac26b72c1207c54a1df91edb07d75280cf)

* fix(live): reflect the discovery file cap in coverage; stop the files counter drifting

Live discovery tails only the newest transcripts per host (128 each by
default), but #discover called discoverJsonl, which throws away the
truncated/candidateCount facts, so /api/live reported
acquisitionCoverage {complete:true, truncated:false} while 257 Claude and
756 Codex files were capped to 256 tailed. Separately, #add incremented
health.files and close() never reset it; the dashboard reuses one service
across its 30 s idle stop, so files grew 2 -> 4 -> 6 on each restart (the
reporter's 256/256 was one such restart).

- #discover uses discoverJsonlDetailed and keeps, per host,
  {candidateFiles, returnedFiles, fileLimit (the host's share), truncated}.
  A capped window makes acquisitionCoverage complete:false and
  truncated:true, with omittedFiles and per-host sources.
- health.files is recounted from the tailed files on every pass (the
  increment in #add and the rotation decrement are gone); native adapters
  also carry candidateFiles, and Sources renders "N files (newest of M)".

Docs: OBSERVABILITY (bounds and symptom row), DDD observability,
ADR-0012. Refs #238 (item 5).

Integration (I3): tests/kit/ga-surface-guard.test.mjs failed after this
pick ("docs/adr/0012-observability.md retains historical vocabulary
without a GA amendment"): the lengthened 2026-09-26 Updated line pushed
the ADR-0020 marker and the Update note past the first 1,200 characters
the guard reads. Fixed in this commit: the Updated line is now one line
naming the items, and its detail (process survey, source health,
discovery coverage) moved verbatim into a new "Amendment — 2026-09-26:
live acquisition" section at the end of ADR-0012.

(cherry picked from commit 9fd5e444c8509231295491c05619397b77785389)

* fix(live): keep tail offsets across idle stop and restart

The dashboard reuses one LiveSessionsService and calls close() 30 s after the
last Live client leaves, then start() on the next visit. close() dropped every
tailer and context, and start() re-bootstrapped and re-tailed each file from
its end, so operations appended during the idle gap were lost and the session
was rediscovered (#238 item 4). ADR-0012 promises retained offsets and a safe
restart.

close() now stops polling but keeps tailers (offset, partial line) and
contexts; only the first start bootstraps metadata and tails from the end. A
restart resumes each file where it stopped, and a file that appeared during the
stop is read from its first byte, the same policy the moving window applies
while running. The snapshot reports acquisitionCoverage.observedSince, and the
Live map tells the user when a session has no operations drawn since Live
started watching, so pre-observation work is disclosed rather than looking
absent.

Integration (I3): ADR-0012 header conflict with the re-done pick of
9fd5e444 (which moved the 2026-09-26 detail into "Amendment —
2026-09-26: live acquisition" to keep the ga-surface-guard 1,200-char
header rule). The one-line Updated note now also names idle restarts
(#238 item 4), and this commit's Updated-line sentence moved verbatim
into that amendment as an "Idle restarts" bullet. The §6 body edits
auto-merged unchanged.

(cherry picked from commit 54ec713af7b35030dfb9e7fa23023052fbaef7dd)

* feat(live): show non-git folder sessions through an exact-folder match

A running session in a folder that is not a Git repository showed in System ->
Runtime but dropped out of Live whenever it stopped writing, because ADR-0012
only granted a runtime lease to canonical Git repositories (#238 item 2). The
rule exists because a plain folder's project key hashes only its name: the
reporter's name-based join was reproduced attaching one `scratch` folder's
process to another `scratch` folder's transcript.

Decision 2 (exact-folder match): a process in a plain folder may lease a
transcript session only when an HMAC of its real working folder equals the
HMAC of the folder the transcript records. The secret is random per collector
and the correlator lives only in memory, never in events, snapshots, replay,
the workspace store, or logs (ADR-0053's per-server-secret precedent). Project
keys are unchanged. An unmatched plain-folder process gets no runtime-only
session, a missing folder never matches, and a bound process whose folder
changes loses the lease through the normal miss count. ADR-0012 is amended.

Integration (I3): ADR-0012 header conflict with the re-done picks of
9fd5e444/54ec713a (the 2026-09-26 detail lives in "Amendment —
2026-09-26: live acquisition" so the ga-surface-guard 1,200-char header
rule holds). The one-line Updated note now also names exact-folder
leases (#238 item 2), and this commit's Updated-line sentence moved
verbatim into that amendment as an "Exact-folder leases" bullet. The
runtime identity amendment body edit auto-merged unchanged.

(cherry picked from commit 1de615513c5785cc5e0e4dc727a719a488b40470)

* fix(live): keep the exact-folder correlator across the metadata bootstrap

The first start ingests bootstrap records through a copy of each source
context and then copies its properties back. The folder learned from a Codex
session_meta was noted against that copy, so the live context had none, and
the first streamed record without a cwd (a Codex tool call or message) cleared
the session's folder correlator. The bound process then failed the folder
check and the session quiesced after three surveys: the symptom decision 2 set
out to fix, for any Codex session in a non-Git folder that was mid-turn when
Live started.

Carry the noted folder from the bootstrap copy to the live context, and only
clear a session's correlator when its source names a Git repository or a
missing folder, never when a record names no folder at all.

(cherry picked from commit 80921288827e5ff5f7c5990faeb3b1d0322a6e3d)

* feat(hosts): report managed, found-not-managed, and not-installed hosts consistently

kit.json cannot tell "left out on purpose" from "never changed the
claude-only default", yet the dashboard called every unrouted host
Disabled, skipped even its free local checks, and offered a "Check local
setup" button that could never produce evidence (stuinfla #238, audit
decision 1).

Management and health are now separate facts with one vocabulary
(src/lib/host-management.mjs): Managed by ak / Found, not managed /
Not installed. Every found host gets the bounded tool checks
automatically (codex doctor still excluded); for an unmanaged host the
wiring check is FYI, its problems are information (never Attention, no
sync fix), and the paid connection check stays managed-only. The header
badge, details dialog ("Check again"), About (dashboard and ak about),
Overview -> Providers rows, a new Hosts & Routing participation strip
and ak host status all use the same words. The enable hint is copyable
text naming the complete --host list, fixing the old hard-coded
"--host claude,opencode" hint that would have disabled an enabled codex.

ADR-0053 amended (Disabled retired); its "disabled hosts are neutral"
test is replaced.

Integration (I3): three conflicts, both intents kept. (1) ADR-0026
header: one 2026-09-26 Updated line naming B1's agentdb amendment and
this commit's host-card management words; the "Earlier updates" line is
B1's. (2) docs/DASHBOARD.md Overview: A1's Summary sentence (arrow vs
manual tag) with this commit's Hosts & Routing participation sentence.
(3) status-golden.json providers hunk: this commit's three management
rows, each with A1's "repair": null key (regenerating the golden with
STATUS_GOLDEN_UPDATE=1 is a no-op).

(cherry picked from commit b37a92e9c53dfee97f8296151c303f48b7ca44bd)

* fix(maintenance): do not scan or count absent source roots

The Hermes automatic source is enabled on every machine. When ~/.hermes did
not exist, resolveAutomaticSourceRoots marked it present:false, but
scannableSources ignored that and drove it anyway: planPartitions found no
partitions, the orchestrator recorded planReadFailure=ENOENT and
finalizeFailed mapped it to io-failure. The Inventory banner then said
"1 source stopped at a limit (io-failure). See Discovery", while Discovery,
which reads the live orchestrator, lost the failed row after any config
change or restart. With the failed row gone, the banner fell back to
"not been scanned yet" forever, so most installs could never clear it
(#238 item 6).

An absent host root is not installed, not incomplete. Now:
- scannableSources skips present:false sources, and an explicit start is
  refused with SOURCE_NOT_PRESENT (409 on the dashboard, plain message in
  the CLI);
- projectionSourceCoverage drops them from the inventory sourceCoverage,
  so the banner and "coverage gaps" never count them;
- scanProgress leaves them out of the narrative totals and the
  forbidden-claims check;
- discovery() and the coverage rows carry present, the public v2 COVERAGE
  schema keeps it (it was stripped before), and the dashboard and
  `ak maintain discovery|scans` show "Not installed" with no start hint.
Roots the user added are unchanged: a missing user root still reports its
real failure. The closed LIMITING_REASONS/SOURCE_COVERAGE_STATES enums are
untouched.

The test harness hid this because hermeticPaths() omitted hermesDir, which
production's DEFAULT_PATHS includes; it now mirrors production, and the
stale "stuck at scanning" comment is replaced. Docs: MAINTENANCE, DASHBOARD,
DDD maintenance, ADR-0048 amendment, and `ak maintain --help`.

(cherry picked from commit d3eef34871c59a7cba539ace3d1d1d1d0272bea3)

* fix(usage): rank project concentration by repository identity

The "X dominates your usage" Finding and Score -> Projects reported
different cost and session counts for the same project and window
(#238 item 7). Both read the same sessions, but the finding ranked
`byProject`, keyed by the folder or repo-name label, while Score ->
Projects ranks `gitProjects`, keyed by verified repository identity since
PR #210. The folder label merges separate clones, plain non-git folders,
repositories that no longer exist, and sessions with no evidence, so it
cannot support a "one project" claim. The subagent theory in the report
is not the cause: both paths see the same sessions.

detectProjectConcentration now ranks `gitProjects` (label, cost,
sessions) and stays silent when an older aggregate has no such field. The
share keeps the whole window's spend as its denominator and the text now
says so ("of all API-equivalent spend in this window"). The unused
entriesByCost helper is removed.

Tests: the finding reads $350 / 4 sessions from gitProjects when byProject
says $400 / 7; no gitProjects -> no finding. Existing concentration
fixtures move to gitProjects. Docs: USAGE-SCORECARD-METRICS section 11,
ADR-0050 amendment, DDD context map.

(cherry picked from commit 1c5491859c32dfdc34c241f5b504d412f10664b7)

* docs(maintenance): qualify fresh-install coverage wording for absent hosts

Follow-up to d3eef34 (doc gate). DASHBOARD and MAINTENANCE still said a
fresh installation shows "every automatic source" as Not scanned yet; a
host source whose root is not on the machine now reads Not installed and
is not counted. Adds the "Not installed (Discovery source)" term beside
SourceCoverage in the DDD ubiquitous language. Integrator may squash this
into d3eef34.

(cherry picked from commit 512533d6db5cd0accb5263190cb404c9ea1c69b6)

* fix(usage): explain an empty Claude limits panel when the user statusLine is custom

Claude limits reach ak only through the kit footer's tee, and Claude Code
runs the statusLine with the highest precedence (project local, shared
project, then ~/.claude/settings.json). A user-level statusLine that runs
another script (for example the RuvNet Brain version segment) never
tees, yet the panel told every user to "run one session, then revisit".

/api/limits now carries claudeChannel, the class of the user-level
statusLine (none, kit-footer, project-helper, custom, unknown), from a
read-only look at the settings file and the script it names. It returns
no path and writes nothing, so ADR-0010's channel is unchanged. The
empty Claude panel names the class, states the project precedence rule,
and points at `ak setup --project` / `ak sync`. The claude field keeps
its null-or-windows contract; an older server falls back to generic copy.

Refs #238 (M3).

Integration (I3): tests/kit/doc-citations.test.mjs failed after this
pick on its own ("docs/USAGE-SCORECARD-METRICS.md:1357 cites
quota.mjs:209 but ... codexAppServerRateLimits@294"), as lane F2
reported: the classifier block shifts quota.mjs. Fixed in this commit:
the quota.mjs citations in USAGE-SCORECARD-METRICS §13b are re-anchored
to this tree (69->70, 209->294, :43->:44, :264->:349, :145->:230,
:127-137->:212-222, 51->52). The next commit (8b53dead) re-anchors them
again for its own quota.mjs changes.

(cherry picked from commit f9d4db509b18ab03a4165ebfba0fa655d0ab47b7)

* fix(usage): show why Codex limits are unavailable

Every codex app-server failure collapsed to codex:null, and the Limits
panel guessed "not installed, not logged in, or did not answer". It
could not tell a missing CLI from one that rejects the read-only flags
(the case that starved the cache for 10 days), a timeout, an RPC
refusal, or an answer with no plan window.

The exchange now keeps the failure class (not-installed, spawn-failed,
exited with its exit code, timeout, rpc-error with its numeric code,
no-limit-windows) and /api/limits carries it as codexUnavailable beside
an unchanged codex field. Stderr stays ignored and vendor message text
is dropped. The empty panel names the cause and the next check, and a
stale answer served instead says its last refresh failed.

Also re-anchors the quota.mjs line citations in USAGE-SCORECARD-METRICS
that the previous commit shifted (tests/kit/doc-citations.test.mjs).

Refs #238 (P4).

Integration (I3): USAGE-SCORECARD-METRICS §13b conflict with the
fix-forward in the previous pick (which re-anchored the quota.mjs
citations for the f9d4db50-only tree). Took this commit's side: the
Codex paragraph is identical to lane F2's final text
(codexAppServerExchange :313, collectCodexLimitsDetailed :385).
doc-citations passes.

(cherry picked from commit 8b53deadcbfae836a908afa61984f54168d1c9d1)

* perf(dashboard): serve the System page from a slim summary endpoint

GET /api/system sends the footprint collector's payload verbatim, and its
catalog repeats every presence fact in item.presence,
item.consumerBindings, item.artifacts and again in top-level artifacts
and consumerBindings. It grows with items x projects x hosts (tens of MB
on a real machine), the page draws about 0.4% of the item bytes, and the
Runtime view re-fetched it every 30 seconds.

GET /api/system/summary serves the same read and the same ?refresh=deep
and &trees= parameters with the catalog projected by
dashboard/system-summary.mjs: an allow-list of catalog keys, and items
cut to key, kind, name, hosts, sourceScopes, digestCoverage and distinct
presence[].provider {ref, version}, the shape paintCatalogMatrix already
reads. loadSystem (and so the Runtime poll and Full scan) uses it.
GET /api/system and `ak system --json` are unchanged, as UPGRADING
documents. A render-parity test draws the KPI band and every catalog card
from both payloads and requires identical HTML.

The UI harness intercepts now match /api/system/summary.

Refs #237 (M4), decision 8.

(cherry picked from commit 00ca5f5ff819d13e3b1e59d69ba5d22d1a0c16c3)

* refactor(exec): share a process-tree kill helper

The MCP discovery probe carried its own copy of the process-group and
taskkill teardown, and the tool-call client the memory route proof needs
(issue #213) would have added a third. killProcessTree() is the one
helper for MCP stdio clients: POSIX reuses run()'s killGroup (group kill,
then the child itself), Windows uses taskkill /T /F with a direct-child
fallback. run() keeps its own kill path unchanged on every platform.

(cherry picked from commit 3fc6b0870d418eb557481a161b485d2baa8253a0)

* feat(verify): prove the memory route across CLI and MCP

`ak x verify memory` failed on ruflo 3.45.0 with "isolated namespace purge
did not remove the proof row" (lane G1 unresolved #1). Observed in a
disposable project with the suite's own environment: the CLI mirrors a
`memory store` into memory.db and agentdb-memory.db, and a default
`ruflo memory purge` clears memory.db only while reporting success. The
suite now clears the sibling of its throwaway project with --path and says
so.

After the CLI proof it observes, in that project only, whether a key
written through the CLI is readable through MCP and the reverse (issue
#213), naming where each landed and the MCP backend. A split is a warning
and an unusable MCP server is "not observed"; neither fails the suite.
The proof isolates CLAUDE_FLOW_MEMORY_PATH, which the native store follows
instead of the DB-path pin, so a user's memory root never receives proof
rows. `ak status --live` keeps its memory check to the CLI proof.

New: mcp-tool-call.mjs (bounded, ordered MCP tool calls on the shared
process-tree kill) and memory-route-probe.mjs (the observation and its
verdicts). Docs: ADR-0016, TROUBLESHOOTING, SETUP, DDD term, verify help,
Ruflo guidance block.

Integration (I4): ADR-0016 header conflict with A1's 2026-09-26 line
(legacyRufloDisposition, #237), which already occupies the header budget
that ga-surface-guard reads (the Update note must sit in the first 1,200
characters; it was at 924 before this pick). A1's line is kept unchanged.
This commit adds a second, one-line 2026-09-26 Updated note that points to
a new "Amendment — 2026-09-26: project memory" section at the end of the
ADR, and this commit's Updated-line sentence moved verbatim into that
section as its first bullet. A1's form of the 2026-09-23 line ("Earlier
update") is kept. The project-memory body edit auto-merged unchanged.
Update note offset after this pick: 1,070.

(cherry picked from commit d20e11b952e784e4877328fa816e136e8bdde339)

* feat(status): gate memory routing claims on observed release evidence

When both project stores exist, status said only that MCP routing needs
separate verification. It now states which interface reads which file (CLI
memory.db, MCP agentdb-memory.db with the native bridge) for an exact
@claude-flow/cli release and platform where that routing was observed, and
keeps the unverified wording for anything else: neighbours, prereleases,
build tags, other platforms, or no resolvable CLI. The gate reads the
@claude-flow/cli nested under ruflo (else a hoisted copy), not the wrapper.

Observed pairs: 3.42.4/darwin (issue #213 work) and 3.45.0/darwin, recorded
2026-09-26 in a disposable project with the verify suite's environment and
again with scripts/ruflo-memory-routing-repro.mjs (public CLI and MCP only):
CLI writes reach MCP, MCP writes are invisible to a CLI read, a default purge
leaves the sibling's rows, and MCP ignores a non-default CLAUDE_FLOW_DB_PATH.
The row stays a warning with no sync fix; both stores are preserved.

Docs: ADR-0016 (status no longer "identifies the active writer"),
TROUBLESHOOTING routing section, DDD term, Ruflo guidance blocks.

Integration (I4): ADR-0016 header conflict with the header built in the
previous pick (A1's 2026-09-26 line plus a one-line note pointing to
"Amendment — 2026-09-26: project memory"). The note now also names the
release-gated routing claims, and this commit's Updated-line addition
("status states the routing only for an observed Ruflo release and
platform") moved verbatim into the amendment as a "Status routing claims"
bullet. The project-memory body edit auto-merged unchanged. Update note
offset after this pick: 1,084.

(cherry picked from commit 208e7992703be424ad1bd8e1ea74eebdafae3f2f)

* fix(setup): remove the setup memory probe from every store

`ak setup --project` proves a memory write in the user's real project with
a `_setup/verify-*` row, then deleted it from the first store that held it
(memory.db). The Ruflo CLI mirrors that write into agentdb-memory.db
(observed on 3.45.0), so every setup run left a probe row in the MCP
corpus. removeMemoryProbe() deletes the row from every store that holds it
with bound parameters (Ruflo's `memory delete` only tombstones), opens a
store for writing only when it holds the row, skips a store with no memory
table (agentdb-memory.db right after `memory init`), and reports an
unreadable or busy store so setup names it for manual cleanup instead of
claiming "VERIFIED".

Adds the opt-in live test tests/live/ruflo-memory-routing.test.mjs, which
runs the route probe against the installed Ruflo and fails when an
observed release/platform pair stops routing as claimed. It needs the
`test:ruflo-memory-live` package script (integrator applies the manifest
change). Docs: ADR-0016 (setup cleanup; "active writer" wording corrected),
TROUBLESHOOTING.

Manifest: package.json scripts gains
"test:ruflo-memory-live": "node --test tests/live/ruflo-memory-routing.test.mjs"
after test:qe-court-live (lane G2 manifestChanges), so the opt-in live
routing test that TROUBLESHOOTING points maintainers to can run.

Integration (I4): ADR-0016 header conflict with the header built in the
two previous picks. The one-line 2026-09-26 note now also names the setup
probe cleanup, and this commit's Updated-line addition ("setup removes its
write probe from every store, correcting the earlier "active writer"
wording") moved verbatim into "Amendment — 2026-09-26: project memory" as
a "Setup probe cleanup" bullet. The project-memory body edit auto-merged
unchanged. Update note offset after this pick: 1,105.

(cherry picked from commit 7ad2045f4188f7f7adfd49e4d56c27e24f3ccc85)

* feat(status): report the canonical memory store, its size, and stray stores

Nothing in ak showed which store the hosts actually use, how large it
is, or that memory was piling up elsewhere. The audit found a 168 MB MCP
store (98% `commands` rows with no expiry), a nested
.swarm/.swarm/agentdb-memory.db, and stray agentdb.db, agentdb.rvf,
ruvector.db and .agentic-qe/ folders that no status row mentioned.

- status resolves the canonical store at <root>/.swarm for the root
  every launch contract pins (memoryProjectRoot), so a subfolder run
  reports the hosts' store rather than a folder-local one
- each file row adds size, live WAL, the largest namespace and its
  expiry, from one read-only query on Ruflo's namespace index
- a store with no memory_entries table (agentdb-memory.db right after
  `ruflo memory init`) is empty, not unreadable (lane G2 unresolved #1);
  removeMemoryProbe and findMemoryEntry skip it explicitly
- findStrayMemoryStores: a bounded, read-only walk (4 levels, 2000
  folders, never node_modules, .git or dot-folder contents, never
  .swarm/backups) listing strays by owner; rows are info with no fix,
  because ak leaves them in place and a warning with no resolution would
  stay amber forever (#237/#238 comments, audit N4)

Docs: ADR-0016 update and paragraph, DDD terms (canonical memory store,
stray memory store), TROUBLESHOOTING table row and two sections.

Integration (I4): two conflicts, both resolved keeping both intents.
(1) docs/TROUBLESHOOTING.md symptom table: lane C's external
agent-browser row and this commit's stray-memory-store row were inserted
at the same place; both rows are kept, C's first. (2) ADR-0016 header:
the one-line 2026-09-26 project-memory note now also names canonical and
stray stores, and this commit's Updated-line addition moved verbatim into
"Amendment — 2026-09-26: project memory" as a "Canonical and stray
stores" bullet. The project-memory body edit and the two new
TROUBLESHOOTING sections auto-merged unchanged. Update note offset after
this pick: 1,132.

(cherry picked from commit 8e477b522b072102fe8ad4606bb0e326b61fdf41)

* feat(status): report memory backup and distillation age

Ruflo backs up and distills project memory only inside the project's
daemon, which ends itself after 12 hours. Nothing in ak showed that the
last backup here was 16 days old, and the daemons row reported "none
running" as ok. ak also plays a part: setup keeps
claudeFlow.daemon.autoStart false, which Ruflo 3.45.0 reads as "do not
start the daemon when a ruflo command runs" (daemon-autostart.js), so
backups stop soon after setup. This commit reports that; it does not
change setup's policy (a maintainer decision).

- memory-maintenance.mjs reads Ruflo's own evidence, read-only: the
  daemon's .claude-flow/metrics/{backup,consolidation}.json and the
  newest .swarm/backups/memory-*.db (so a manual `ruflo memory backup`
  counts); ak never runs either job
- memory rows: backup age (warn when older than 48 h or absent with no
  daemon for the project, or when the latest attempt failed),
  distillation age (warn only on a failed or corrupt run; age alone
  would stay amber forever), and an info row for agentdb-memory.db,
  which neither worker covers (upstream gap), with a manual backup
  command verified in a sandbox on 3.45.0
- daemons row: info, not ok, when the project has memory.db and no live
  daemon pidfile; names `ruflo daemon start` and any setting that turns
  off Ruflo's start-on-use; never a sync fix

Docs: ADR-0016, DDD term, TROUBLESHOOTING table row and section,
SETUP.md, the Ruflo reference guidance, and the About card for the
background daemon.

Integration (I4): one conflict and one red test, both fixed in this commit.
(1) ADR-0016 header: the one-line 2026-09-26 project-memory note now
also names backup and distillation age (and was tightened to keep
headroom under ga-surface-guard's 1,200-character header rule). This
commit's Updated-line addition moved into "Amendment — 2026-09-26:
project memory" as a "Backup and distillation age" bullet, verbatim
except that its opening "It" became "Status" so the bullet stands alone.
Lane G3's "commit 2 alone fails ga-surface-guard" defect does not occur
here: the Update note offset after this pick is 1,128.
(2) tests/kit/daemons-status.test.mjs failed 2 of 6 after the pick: two
deepEqual expectations were written before lane A1's status-row contract
(commit 4), which gives every row a `repair` field (`null` when there is
no fix). Both expectations now carry `repair: null`, as A1 did for
status-command. The rows' level, message and fix are unchanged.
The body edits, SETUP, TROUBLESHOOTING, the DDD term, the guidance
paragraph and the about-directory background-daemon paragraph (separate
from B1's agentdb hunk) auto-merged unchanged.

(cherry picked from commit 51f86c024cd6fff86311ce8fa45a76732f5166d7)

* test(live): run the qe-court live test in a disposable project

The opt-in qe-court participant-transport test ran its seats in the
checkout and cleaned up with a default `ruflo memory purge`, which
clears memory.db only; the seats write through MCP to
agentdb-memory.db, so 118 rows in 30 ak-qe-court-live-* namespaces
stayed in the real store (audit H D7).

- tests/live/disposable-memory-project.mjs creates a throwaway project:
  `git init` (codex exec refuses to run outside a Git repository, and
  it makes the folder its own memoryProjectRoot for `ak x ruflo-mcp`),
  claude-flow.config.json {daemon:{autostart:false}} before any ruflo
  command (the env opt-out does not reach an MCP server Codex launches),
  `ruflo memory init`, and CLAUDE_FLOW_DB_PATH/CLAUDE_FLOW_MEMORY_PATH
  pinned inside it; cleanup stops a daemon holding its pidfile (through
  reap()'s identity check) and deletes the folder
- the live test runs every seat there, checks the proof in that
  project's store, fails on a file edit there or in the checkout, and
  runs a read-only tripwire that fails if any proof row reaches the
  checkout's memory
- tests/kit/disposable-memory-project.test.mjs covers the helper with an
  injected runner, a real keep-alive "daemon" child and a real SQLite
  store; the paid live test itself was not run

The helper lives beside the live test because tests/live/*.test.mjs
ships in the package; it needs its own `files` entry (manifest change
for the integrator). Also shortens the ADR-0016 Updated line added in
the previous commit, which pushed the update note past the 1,200
characters ga-surface-guard reads.

Docs: ADR-0033 update note and verification bullet, PROVIDERS.md,
MAINTAINER.md tarball list.

Manifest: package.json `files` gains "tests/live/disposable-memory-project.mjs"
next to "tests/live/qe-court-participant-transport.test.mjs" (lane G3
manifestChanges). The live test ships in the tarball and imports this
helper, so without the entry a packaged copy cannot resolve its import.
MAINTAINER.md already names the helper beside the live test, as
ga-surface-guard requires for every `files` entry.

Integration (I4): two conflicts, both resolved keeping both intents.
(1) ADR-0033: lanes B1 and A2 had already set a descriptive 2026-09-26
Updated line (2026-09-23 became "Earlier update") and appended two
2026-09-26 update-note entries. This commit's date change is already
satisfied; its subject is appended to that Updated line in one clause,
and its update-note entry follows A2's. Its verification-bullet change
auto-merged beside A2's sync-command bullet. (2) ADR-0016: this commit
only shortened the l…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant