Skip to content

fix: remediate #237 and #238 (sync and dashboard truth, memory, upstream watch) - #241

Merged
pacphi merged 111 commits into
mainfrom
fix/audit-237-238-remediation
Sep 27, 2026
Merged

pacphi merged 111 commits into
mainfrom
fix/audit-237-238-remediation

Conversation

@pacphi

@pacphi pacphi commented Sep 27, 2026

Copy link
Copy Markdown
Owner

Summary

Remediates the verified findings from #237 (sync) and #238 (dashboard), with the maintainer's decisions recorded point by point. It also brings in the upstream watch and the memory, install-transparency and statusline fixes found along the way. The full record is the audit: verdicts, decisions 1–10, Addenda 1–3 and the implementation status. The work that remains is planned in the remediation program.

What changes

  • Sync and status tell the truth.
    • Status rows say whether ak sync or the user repairs them (repair: sync | manual).
    • Sync reports repairs that did not converge, and adds --skip and --json.
    • Sync's exit code covers only what sync can repair; failing or warning hand-fix rows are listed under "needs your action" (decision 10).
  • Evidence and diagnostics.
    • Live-check results are remembered, and ak status --live runs the quick checks.
    • The native-module probe separates unavailable from inconclusive.
    • One recognizer for AQE's server entries is shared by every host.
    • A busy AQE store reads as busy, not failed.
    • A refused Brain refresh is held as blocked.
  • Dashboard truth.
    • Every host shows one of three states: managed by ak, found but not managed, or not installed.
    • Live correlates sessions in non-Git folders by exact folder.
    • A slim /api/system/summary feeds the System page.
    • Limits show why Codex data is unavailable.
    • Discovery shows "Not installed" sources.
  • Memory and installs.
    • The Track upstream Ruflo memory routing fix and safe integration for existing corpora #213 memory-routing work is ported.
    • The standalone AgentDB install is retired (AgentDB ships inside Ruflo).
    • Status reports backup age, stray stores and command-log growth.
    • The memory root is pinned before provider registration.
    • Codex's Ruflo memory stays out of system and tool folders.
    • ak's edits inside Ruflo's install are receipted, shown and reversible.
  • Statusline. ak no longer overwrites Ruflo's baked version (the V9.9.9 incident), and status flags a version mismatch.
  • Test isolation. Lifecycle tests run in a sandbox project, and a census test enforces it. Tests that had written real user state were fixed.
  • Upstream watch.
    • The upstream constraint registry now ships in the npm package; it did not before.
    • The registry is extended with a watch list of 83 threads (schema v5), guarded by a test.
    • scripts/upstream-watch.mjs produces reports and ledger events.
    • A maintainer-only upstream-status skill covers Claude and Codex and is not shipped.
  • Docs.
    • ADR-0060 (Proposed): session surface, initiator and official product names.
    • Updates to ADRs 0008, 0010, 0012, 0016, 0023, 0025, 0026, 0033, 0041, 0043, 0048, 0050, 0053 and 0055.
    • Glossary terms, UPGRADING entries and the program plan.

Verification

  • Full gate at 5e9af72:
    • Kit suite: 5,092 pass, 0 fail, 6 skipped (Windows-only). Coverage is 93.00% lines, 82.30% branches and 92.36% functions.
    • Other suites: all 7 .cjs suites and the UI suites pass (dashboard 493/0).
    • Static checks: tsc, eslint (0 errors), the complexity ceiling, markdownlint and the build check all pass.
  • Adversarial review of the full diff. Five reviewers covered correctness, security, hermeticity and Windows, docs, and cross-lane contracts; a refuter checked each dimension. 27 findings were fixed test-first, with scoped re-reviews and no new breakage. The rest are parked with rulings or moved to named branches, all listed in the audit record.
  • Real-machine pass. The browser check on real data and sync --dry-run found three issues, all fixed and re-reviewed.
  • Real user state was fingerprinted before and after every automated run.
  • Windows behavior has been proven only by simulation. This PR's CI is its first Windows run.

Upstream

Filed or commented upstream during this work:

The audit record lists every link.

Not in this PR

The remaining work runs as branches 1–9 in the program plan:

  • imported-rollout origins;
  • test hermeticity;
  • the Ruflo support window and 3.46 adoption;
  • the upstream watch going live;
  • AQE store integrity;
  • one --refresh flag and one evidence store;
  • session surface;
  • the usage backlog;
  • follow-ups.

Closes #237
Closes #238
Refs #239, #213, #240

🤖 Generated with Claude Code

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.
…endum

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.
…ions

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)
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)
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)
…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)
…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)
…nd 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)
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)
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 (5618e97) flagged its sync.run calls. The file's own inProject() chdir is unchanged.
…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.
… 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)
…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.
…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.
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.
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)
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)
`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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
…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)
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)
… 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)
UPSTREAM-WATCH.md says the watch holds every upstream issue ak filed, but
ruvnet/ruflo#3473 (security defend crashes on every detected threat) was
filed seconds after the registry was committed and never added. Nothing in
source cites it yet, so the citation guard could not catch it, and the
daily watcher would never report a reply on it or its release, which plan
Branch 3 item 4 depends on.

The registry now carries a mapped 'filed' entry for it; Addendum 3's
closed-upstream bullet links it. A new guard fails when a thread the audit
record lists as filed upstream evidence (Item 6) has no watch entry.

Finding: docs-04
reconcileOpencodeGuidance (ak host pick, ak host off, setup) rebuilt the
guidance loop with its own thin context and no known-target list. The Brain
block's detector then fell back to whether a KB folder exists, while sync
reads kit.json's ruvnetBrain, so a managed Brain without its KB (or an
opted-out one with a user KB folder) made pick and sync flip the opencode
AGENTS.md block back and forth, with status and the nudge reporting drift
in between. Pick could also force-strip a custom block scoped to a target
this machine lacks, which sync leaves alone.

It now calls reconcileGuidance itself, limited to the opencode AGENTS.md
through a new `only` option that still counts every target as known, with
kit.json intent plus the opencode state the command converges to.

Finding: contracts-1
verify memory and verify harvest derived their CLAUDE_FLOW_DB_PATH through
the project-root rule, and runHarvest re-derived the root (and distill's
--db) from the temp dir. With TMPDIR inside a Git checkout (a project .tmp
set by direnv) that root is the enclosing repository, so ruflo memory
init/store/purge, the outcome hook and distillation ran against that
repository's real store, and ak status --live's memory proof with them.

Both proofs now pin CLAUDE_FLOW_DB_PATH and CLAUDE_FLOW_MEMORY_PATH inside
their temp dir explicitly, and runHarvest and planHarvest accept the root
verify passes instead of re-deriving it (ak x harvest keeps deriving it).

Finding: hermeticity-tmpdir-inside-repo-isolation
The withAbortSignal comment claimed a timed-out check's child processes
stop. Node's abort signals only the direct child, so a grandchild keeps
running; on Windows every CMD shim (ruflo, aqe, npm, ...) runs through a
PowerShell wrapper, so the real node process survives the abort. The
comment now says so. Killing the whole tree on abort is deferred: it needs
Windows CI to prove, and runWithInput's killGroup is also direct-child on
Windows, so the fix belongs with killProcessTree.

Finding: hermeticity-windows-abort-orphans (comment corrected; tree kill deferred)
Follow-up to 726df78: tsc inferred installRuvnetBrain's options from their
defaults, so the new cfg option (no default) was rejected at both call
sites; it now defaults to undefined. The new sync-driving test also runs
inside isolateProject(), as the project-isolation guard requires of every
file that drives sync.

Finding: correctness-brain-held-refresh-lost (follow-up)
Follow-up to f953146: its longer Updated line pushed ADR-0016's
"Update note" past the first 1,200 characters that the GA-surface guard
reads for living-plan metadata. The line is shortened.

Finding: security-verify-memory-user-store (follow-up)
The implementation status section stopped at 259f075 and said every SHA was
listed, while eleven later commits (ADR-0060, Addendum 3, the eight
upstream-watch commits including 24ddc42, which changes what npm ships, and
the program plan) had no rows or check totals, as Branch 0 Step 2 requires.

It now lists those eleven with the suites that cover them, the review fix
wave (one row per finding, with its focused evidence and what was left
unchanged), the full-suite results at 5764c51 and after the fix wave, and
the current markdownlint (165 files) and npm pack (522 files) totals.

Finding: docs-03
Follow-up to 539809e. The convergence verdict set aside every row of any
subsystem that had a skipped plan item. Before 539809e a subsystem was
either wholly skipped or not; now --skip providers skips a Codex MCP
registration while a recursive-table fix of the same subsystem stays
planned. That fix, had it not taken, was reported "skipped by request"
instead of unresolved.

Only subsystems --skip names are now set aside whole; a plan item --skip
took out of another subsystem is set aside by its (subsystem, fix), so the
rest of that subsystem is still judged.

Finding: correctness-skip-providers-false-unresolved (follow-up)
…e gate

Adds d0b728f to the --skip providers row of the review fix wave and moves
the fix-wave results row to the final gate run on d0b728f (5,068 passed, 6
skipped, 0 failed of 5,074; coverage 92.98 / 82.29 / 92.36).

Finding: docs-03 (follow-up)
The OpenCode agent-projection row read "1 agent projection files from
npm-nested@3.45.0, current source is npm-nested@3.45.0" on the maintainer's
machine. The projection stamp is compared on five facts (stamp present,
catalog source id, projected file set, lazy-dispatcher mode, gateway
families), and the last two are derived from whether the lazy rUv gateway is
current. With only the gateway out of date the projection goes stale as a
consequence, while the message named the one fact that had not changed.

agentsStatus now returns staleReasons, and the row names what diverged: a
source change keeps the two source ids; a projection written for a gateway
state that no longer holds says it follows the out-of-date lazy rUv gateway.
The comparison and the planned fix are unchanged.

A disposable-HOME test drives a real `ak sync` over the gateway-derived case
and shows it converges with exit 0, so this row cannot hold sync at exit 1.

Finding: P4 (Branch 0 real-machine pass)
Retiring the standalone agentdb install (5e02beb) removed the `agentdb`
status row, but the About card still joined on that subsystem, so it read
"state unknown", titled "no ak status row reported this on this machine", on
every machine. The UI harness hid the regression because its status stub
still carried a fake `agentdb` row.

The card now joins the one status fact about agentdb: the natives row about
Ruflo's bundled copy ("... agentdb location(s)": native, WASM fallback, or
missing). ABOUT_JOIN gains a `phrase` kind (one shared subsystem plus a
message phrase); the rest of natives (agentic-qe's binding, Ruflo's memory
runtime, install-edit receipts) and the ruflo version row do not join.
natives.mjs lifts that row into agentdbLocationRow so a unit test can hold
its wording and the client's phrase together.

The UI stub drops the fake row and carries realistic natives rows, with a
rendered check that the card reads installed from the natives row and is not
turned red by the memory-runtime row beside it. about-directory's comment,
docs/DASHBOARD.md's About section and the UPGRADING agentdb entry say where
the chip comes from.

Finding: P1 (Branch 0 real-machine pass)
The ruvnet-brain-plugin row warned with fix null and repair null although
its message listed what the user could do (disable the plugin, or set
"ruvnetBrain": false). Under the repair contract a row without a fix gets no
"manual" tag on Overview or About, is not among the manual steps `ak sync`
reports, and is not counted by the bare `ak` hint.

The row now keeps the fact in its message and carries the options as a
manual fix. An audit of every status section found the same class (a warning
whose message tells the user to run or edit something, with fix null) in:

- ruvnet-brain: a refused Brain refresh (fix the cause, or ruvnetBrain false)
- aqe-embedding: semantic backend missing (ak x aqe-embedding configure)
- codex-context: ownership kept while Codex is disabled (ak x codex-context off)
- codex-plugins: config inspection and compatibility issues
- models: model inventory unavailable (ak models refresh)
- memory: a Ruflo configuration pointing memory away from .swarm, and a
  stale backup with no daemon running
- providers: unavailable external AQE intent (restore, or revoke-grant)
- qe-court: invalid panel (regenerate, or choose other vendors)
- agent-browser: an out-of-range external install, and a platform without
  Chrome for Testing
- ruflo-components: applied but unverified (restart the hosts)

Each moves the instruction into `fix` with repair 'manual'; ruflo-components
rows keep ADR-0058's state, meaning and action text. All are warnings, so
sync's plan and exit code are unchanged. The Brain plugin's own warning stays
until an ak release reviews the hooks or ruvnetBrain is false: its static
check reads the payload whether the plugin is enabled or not.

agent-browser's row ladder becomes agentBrowserRow(state), codex-context
exports its row builder, and models.collect takes a readStore seam, so each
row can be asserted directly. The status golden changes by the one
aqe-embedding row. UPGRADING says which rows moved their instruction to `fix`.

Finding: P5 (Branch 0 real-machine pass)
Whether `ak sync` failed on a fail-level row with a manual fix depended
on unrelated plan items: with an empty plan it exited 0 ("nothing sync
can do"), but next to any planned fix the convergence verdict counted
the row as still failing and exited 1. On a machine with a managed AQE
embedding backend the synthetic aqe-embedding item is always planned,
so the same row failed sync there and passed elsewhere.

Maintainer decision 10 (2026-09-26): sync's exit code reflects only what
sync can repair. convergenceVerdict never counts a row whose repair is
'manual', with or without a plan. Every fail- or warn-level manual row
is listed once after the verdict (or the plan) under "needs your
action", and --json gains a needsYourAction array of { subsystem, level,
message, fix } on every path. When a failing manual row remains, the
verdict reads "converged — nothing left that sync can repair" instead
of claiming no failing subsystems. ak status is unchanged and still
reports overall health.

A manual row of a --skip'ped subsystem is now listed under needs your
action rather than "skipped by request", and no longer hides a recorded
apply failure for its subsystem. The status-viability check that the
advisory models action never enters sync's plan now reads the plan part
of the output, since the same row is listed as needing your action.
Addresses review finding correctness-manual-fail-exit-flips.
ADR-0033 section 9 and its combined 2026-09-26 Updated line now say that
sync's exit code reflects only what sync can repair: a fail- or
warn-level manual row never flips it or the converged verdict, and is
listed under "needs your action" (needsYourAction in --json), while
ak status stays the overall-health answer. Its Verification section
names the new test file.

UPGRADING gains a 2026-09-26 entry for the change, and its ak sync
--json entry shows the new field. README and INSTALLATION list it too.
The audit record's Addendum 3 gains Decision 10 in the decision format,
and the fix-wave note that left the question open points to it.
README.md and docs/INSTALLATION.md said every "→ manual:" row is
listed under "needs your action" / needsYourAction. Only failing and
warning manual rows are (info rows, such as the AQE readiness
reminder, never are), matching UPGRADING, ADR-0033 §9 and
`ak sync --help`.
ADR-0033 §9 and `ak sync --help` described --skip removing a
subsystem's rows from the plan and reporting them "skipped by
request", but left out that a manual-fix row of a skipped subsystem
is listed under "needs your action" instead: sync never performed
that fix regardless of --skip.
convergenceVerdict's counts() already excludes a manual row from
remaining, but the apply-failed guards (aqeRouterApplyFailure,
codexRepairFailure, dejaVuApplyFailed) key on the same subsystem a
manual row can also name. Cover all three: a manual fail row on that
subsystem must not suppress the synthetic apply-failed entry, and
must stay excluded when there is no recorded failure.
announcePlan's "N item(s) need a manual step" note counted every
manual row, including info-level ones (e.g. the AQE readiness
reminder), while the needs-your-action list below it holds only
failing/warning rows. The note could claim more items than the list
shows. Feed announcePlan the same needsYourAction array the heading
uses, so the count and the list can never disagree; an info-only
manual row is now invisible to both, and sync reports the machine
healthy outright.
`ak sync --help` said a "→ manual:" row "is counted but never
applied" — the same every-row claim minor 1 fixed in README and
INSTALLATION, now stale in the paragraph above it after minor 4's
fix. Only a failing or warning manual row is counted as a manual
step; an info-level one is not. Also note in UPGRADING's decision-10
entry that an info-only manual row now reads "nothing to do — all
subsystems healthy" instead of counting as a manual step.
…ettle

probeBsq3Runtime timed each attempt with AbortSignal.timeout(), whose timer
is unref'd on every Node. The probe's own deadline therefore did not keep the
process alive while an attempt waited; only the runner's handles did. The
real run() spawns a child, so production held, but Node 22's test runner
cancelled the injected hanging-runner test ("Promise resolution is still
pending but the event loop has already resolved") and every test after it.

Each attempt now owns an AbortController aborted by a ref'd setTimeout with
the same TimeoutError reason, cleared in a finally once the attempt settles,
so a fast probe leaves no timer holding the process open. run()'s own
timeout stays the backstop. Two tests pin both halves on Node 22, 24 and 26
by counting ref'd timers, since only Node 22's runner exposed the gap.
callMcpTools resolved as soon as it had asked for the server's process tree
to be killed. On Windows that kill is a separate taskkill process, so the
server was still running, holding its working directory, when the caller got
the result. ak x verify memory removes that folder right after its MCP round
trip, and on Windows the removal fails with EPERM; the tool-call tests failed
the same way on windows-latest (9 tests, Node 22, 24 and 26).

The call now does what mcp-probe.mjs already does: once the session ends it
kills the tree and waits for the child's 'close' (the process gone and its
stdout pipe released, so a child that inherited the pipe is covered too),
bounded by exitGraceMs (default 5 s). A server still held open after that
reports cleanup-timeout with the answers it gave, rather than a clean status.
A child that never started or has already exited returns at once.

Tests: every session end (ok, output-limit, timeout) is checked for an exited
server pid the moment the call returns, which failed on macOS before this
change (the child was still unreaped), and for a fixture folder removable
with no retries, which is the Windows proof. A POSIX test pins
cleanup-timeout with a child that escaped the killed process group. Fixture
cleanup keeps retries only as a backstop.
The store-size test registered its folder removal before the hook that
closes its open WAL writer. node:test runs after-hooks in the order they were
added, so the folder was removed while the writer still held the database,
-wal and -shm files open. POSIX allows that; Windows refuses with EPERM,
which failed the test on windows-latest (Node 22, 24 and 26).

One hook now closes the test's own connections and then removes the folder,
still without retries: on Windows its success also shows that
projectMemoryStatus released its own read-only connection. The product was
already correct; withDb closes in a finally, and lsof on macOS shows no
handle on the store left after projectMemoryStatus returns.
On windows-latest, "tool-internal folders use the user-level store" failed:
~/.config/opencode classified as a plain work folder, so Codex's Ruflo
launcher would create a .swarm store inside it. toolInternalDirs listed
~/.config, ~/.local, ~/.cache and the XDG overrides only off Windows, but
cross-platform CLIs keep them under %USERPROFILE% too (Claude Code's native
installer uses ~\.local\bin; xdg-basedir tools use ~\.config), and the
ubiquitous language, SETUP, UPGRADING and ADR-0016 list them for every
platform. They now count on every platform; AppData, %APPDATA% and
%LOCALAPPDATA% stay Windows-only and ~/Library darwin-only.

A second Windows defect sat beside it: the home-folder and temporary-root
checks compared strings exactly, while Windows paths are case-insensitive,
so a cwd spelled c:\users\me was not the home folder and would get its own
~\.swarm. Sameness now uses path.relative, which applies the platform's own
rules, as the inside-a-folder check already did.

The location logic takes an injectable path flavour (p, the host's by
default, as globalRootCandidates does) through repoRoot, tempRoots,
toolInternalDirs, homeRelative, userMemoryDir and projectMemoryDb, so a new
test applies path.win32 on any host: XDG-style folders, AppData, another
spelling of the home folder and of %TEMP%, a drive root, an XDG override,
and disposable folders below %TEMP% that keep their own store. Reverting
either fix fails it on macOS.
Windows runners check text out with CRLF line endings. Two tests matched
LF-only patterns against Markdown: the upstream-status SKILL.md frontmatter
(/^---\nname: .../) and the daily-routine prompt block in
docs/UPSTREAM-WATCH.md (/```text\n.../, which returned null and threw).
Both failed on windows-latest (Node 22, 24 and 26).

Following 2cce648 (make the reader tolerant; pin only what must be exact):
the two tests read these files with CRLF normalized to LF, since they check
content, not line endings. The two SKILL.md copies are also pinned to
eol=lf in .gitattributes, because Claude Code and Codex parse the skill's
frontmatter at runtime and a Windows checkout should hand them the text CI
tests; a new test checks that attribute so the normalization cannot hide
its loss. The watcher itself reads neither file, and its ledger parser
already splits on \r?\n.

Reproduced on macOS in a core.autocrlf=true clone: both tests failed there
exactly as on Windows and pass with this change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant