Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 41 additions & 12 deletions MAINTAINER.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,28 +67,57 @@ docs/
`bin/agentic-kit.mjs`, `src/`, `claude/`, `docs/TROUBLESHOOTING.md`. Nothing else
ships β€” verify with `npm pack --dry-run` before a release if you touch `files`.

> The consistency contract all managed tools share β€” install/update/version/display
> invariants, the per-tool table, and the add-a-tool checklist β€” lives in
> [docs/MANAGED-TOOLS.md](docs/MANAGED-TOOLS.md). The notes below are the
> brain-specific details behind that contract.

**RuvNet Brain is the odd one out.** ruflo/agentic-qe/the host CLIs are global npm
packages: detected via `installedVersion` (npm global root) and drift-checked with
`npm view`. The RuvNet Brain is *not* β€” `npx github:stuinfla/ruvnet-brain` installs a
`npm view`. The RuvNet Brain is *not* β€” `npx ruvnet-brain@latest` (the **published**
installer; never `github:`, which runs the unreleased default-branch HEAD) installs a
~512 MB offline KB to `~/.cache/ruvnet-brain/kb` (override `$RUVNET_BRAIN_KB`) and a
user-scope Claude Code plugin (the `search_ruvnet` MCP + hooks + a skill). So it gets a
*parallel* lifecycle in `src/lib/ruvnet-brain.mjs`: `present()` probes disk,
`latestVersion()`/`drift()` hit the GitHub releases API (TTL-cached in kit.json like
`selfDrift`). setup/sync install via `heal.installRuvnetBrain()` with `--no-stack --no-enhance`
(ak already manages ruflo/RuVector and owns the `ruvnet-brain-reference` CLAUDE.md block).
Toggle with the `ruvnetBrain` kit.json flag / `--no-ruvnet-brain`. Note: the installer's
`--enable-nightly` does **not** exist (nightly is a separate, unmanaged LaunchAgent).
`selfDrift`). setup/sync install via `heal.installRuvnetBrain()`, which resolves the
latest release tag FIRST and pins the installer to it (`--version v<tag>`), so the
bundle on disk is exactly the release ak stamps β€” no install-then-stamp race.
Toggle with the `ruvnetBrain` kit.json flag / `--no-ruvnet-brain`.

> **Installer flag gotcha β€” `--yes` accepts *every* optional offer.** Audited live on the
> v3.3.1 installer (2026-07-17): under `--yes` it silently enables a nightly self-update
> LaunchAgent (`com.ruvnet.brain-update`, macOS, 03:47 β€” runs the bundle's
> `forge-update.mjs --apply`, which at v3.3.1 applies downloads **without signature
> verification**), writes telemetry consent, installs a spend-watchdog agent, and
> materializes model-router files. Hence `INSTALL_ARGS` carries FOUR suppression flags:
> `--no-stack --no-enhance` (ak manages ruflo/RuVector + the CLAUDE.md block) and
> `--no-nightly-prompt --no-telemetry` (ak owns updates; consent stays the user's).
> The nightly self-updater bypasses ak-managed updates, so `ak status` flags an existing
> agent as its own subsystem (`ruvnet-brain-nightly`) and `ak sync` disables it
> (`heal.disableRuvnetBrainNightly()`: `launchctl bootout` + plist removal β€” both steps,
> mirroring the installer's own `--disable-nightly`). Deliberate re-enrollment
> (`npx ruvnet-brain --enable-nightly`) gets re-flagged; opt ak out entirely with
> `ruvnetBrain:false`. The spend watchdog and telemetry consent of already-affected
> machines are left alone on purpose β€” local-only / a recorded user answer.

> **Version gotcha β€” three unrelated namespaces.** The plugin semver (`plugin.json`, e.g.
> `0.5.0-dev`), the KB bundle's `brainVersion` (e.g. `v0.3.0-dev`), and the GitHub **release
> tags** the installer downloads by (e.g. `v3.0.1`) are all different tracks, and *none* is
> stamped on disk in the release namespace. So drift is computed against **ak's own record**
> tags** the installer downloads by (e.g. `v3.3.1`) are all different tracks. Evergreen-era
> release bundles stamp the release tag **on disk** (`SOURCE.json` β†’ `releaseTag`), so the
> installed side resolves disk-first: `installedReleaseOnDisk()`, then **ak's own record**
> of the release it last pulled (`kit.json` β†’ `versionCheck.ruvnetBrain.installedRelease`,
> written by `recordInstalledRelease()` after a successful install). `classifyDrift()` compares
> that stamp vs `releases/latest` β€” same namespace, so it converges. A present-but-unstamped
> install (manual / pre-existing) surfaces as outdated once, so `ak sync` pulls it onto the
> managed track. Do **not** compare `installedVersion()` (plugin semver) against a release tag β€”
> that was the original bug and it can never converge.
> written by `recordInstalledRelease()` after a successful install) for pre-stamping
> bundles. The statusline footer mirrors the same order, and the dashboard's update
> banner folds the brain in from the same `drift()` result (`foldBrainDrift()` in
> dashboard-server.mjs β€” driftReport only carries npm tools; the kit's own
> `selfDrift` is folded the same way), so `ak status`, the footer, and the
> dashboard can never disagree.
> `classifyDrift()` compares that resolved value vs `releases/latest` β€”
> same namespace, so it converges. A present-but-unstamped install (manual / pre-existing)
> surfaces as outdated once, so `ak sync` pulls it onto the managed track. Do **not**
> compare `installedVersion()` (plugin semver) against a release tag β€” that was the
> original bug and it can never converge.

### Dogfooding artifacts are NOT source

Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ applies (reversibly); `ak x provider off` restores the claude-only default. Full

[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) β€” symptom β†’ `agentic-kit` command.

## How tools are managed

[docs/MANAGED-TOOLS.md](docs/MANAGED-TOOLS.md) β€” the consistency contract every
managed tool follows (release-pinned installs, sync as the single updater,
disk-first version truth, one drift story across status/statusline/dashboard),
with the per-tool table and the checklist for adding a new tool.

## Credits

ruflo/claude-flow by ruvnet Β· agentic-qe by proffesor-for-testing Β· prior art:
Expand Down
10 changes: 6 additions & 4 deletions claude/ruvnet-brain-reference.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<!-- BEGIN ruvnet-brain-reference -->
<!-- ruvnet-brain-reference: merged into ~/.claude/CLAUDE.md ONLY when the RuvNet Brain
KB is present (~/.cache/ruvnet-brain/kb, or $RUVNET_BRAIN_KB). Managed by ak β€”
stripped automatically when the brain is removed. ak installs it with
`npx github:stuinfla/ruvnet-brain --no-stack --no-enhance` (ak owns this block, so
the installer's own --enhance-claude-md is disabled). Source of truth:
stripped automatically when the brain is removed. ak installs the PUBLISHED
installer pinned to the release it tracks:
`npx ruvnet-brain@latest --no-stack --no-enhance --no-nightly-prompt --no-telemetry --version v<tag>`
(ak owns this block, updates, and telemetry consent, so the installer's own
--enhance-claude-md / nightly / telemetry offers are disabled). Source of truth:
claude/ruvnet-brain-reference.md in the agentic-kit kit. -->

## RuvNet Brain β€” ground before you assert
Expand All @@ -28,7 +30,7 @@
pull the latest release. `ak status` reports presence + drift (checked against GitHub
releases, TTL-cached). Opt out with `ak setup --no-ruvnet-brain` (or set `ruvnetBrain:false`
in `~/.config/agentic-kit/kit.json`).
- **Health check:** `npx github:stuinfla/ruvnet-brain --doctor` (verifies the KB, reader, and
- **Health check:** `npx ruvnet-brain --doctor` (verifies the KB, reader, and
runs a smoke query). Not installed? that command also installs it.
- It is an npx-driven Claude Code **plugin** (MCP server + hooks + a skill) at user scope, not
a global npm package β€” so it is detected on disk, not via `npm ls`.
Expand Down
100 changes: 100 additions & 0 deletions docs/MANAGED-TOOLS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Managed tools β€” the consistency contract

Every tool ak manages follows one contract for how it is installed, updated,
version-detected, and displayed. This doc states the contract's four
invariants, maps every managed tool onto them, and gives the checklist for
adding a new tool without breaking them.

The contract exists because the failure modes it prevents were all observed
live: a version stamp disagreeing with what was actually on disk, a statusline
showing a different version than `ak status`, a third-party self-updater
rewriting managed files behind ak's back, and an "update available" banner
that stayed silent for tools it didn't know about.

## The four invariants

1. **Disk-first installed versions.** The "installed" side of every drift
check is read from what is actually on disk β€” never from a cached claim or
a side-record that can go stale. If a tool can change outside ak (manual
npm install, a hand-run updater), the reads still tell the truth.

2. **Single update owner, with honest disowning.** `ak sync` is the only
updater for everything ak claims to manage. Where ak does *not* own the
artifact, it says so instead of pretending: externally-installed hosts are
filtered out of drift entirely, and a tool that ships its own self-updater
(the RuvNet Brain's nightly LaunchAgent) is detected as drift and disabled
by sync. One owner means the release stamp, the drift check, and the thing
on disk can converge.

3. **Same-namespace comparisons.** Installed and latest are always compared
in the same version namespace: npm semver vs npm semver, GitHub release
tag vs release tag. A comparison across namespaces (the original
ruvnet-brain bug: plugin semver `0.5.0-dev` vs release tag `3.0.1`) can
never converge. "Latest" is not always npm-latest either β€” agentdb's
authority is ruflo's *bundled* version, because a latest-chasing agentdb
is the store-corruption risk its coherence guard exists to prevent.

4. **One drift story across all surfaces.** `ak status` rows, the statusline
footer chips, and the dashboard (subsystem cards *and* the update banner)
derive from the same reads, so they cannot disagree. The dashboard banner
is the easy one to miss: `driftReport()` only knows npm tools, so
non-npm-managed tools (the brain, the kit itself) are folded into the same
`{pkg, installed, latest, outdated}` array explicitly
(`foldBrainDrift()` / the `selfDrift` fold in
`src/lib/dashboard-server.mjs`).

## The tools

| Tool | Install / update spec | Update owner | Installed version read from | Drift compared against | status / statusline / dashboard |
| --- | --- | --- | --- | --- | --- |
| **ruflo** | npm `ruflo@latest` | `ak sync` | disk: global `package.json` | npm `view latest` (TTL-cached) | row βœ“ / upstream's own `RuFlo V<x>` header βœ“ / card + banner βœ“ |
| **agentic-qe** | npm `agentic-qe@latest` | `ak sync` | disk: global `package.json` (project-local fallback) | npm `view latest` (TTL-cached) | row βœ“ / `Agentic QE V<x>` chip βœ“ / card + banner βœ“ |
| **hosts** (claude, codex) | npm `@latest` β€” only when npm-managed | `ak sync` if npm-installed; **explicitly disowned** if brew/mise/native | disk: global `package.json`, else `--version` probe | npm latest for npm-managed only; external β†’ `outdated:false` | row βœ“ (version + method) / n/a / card + banner (npm-managed only) βœ“ |
| **agentdb** | npm, **pinned to ruflo's bundled version** β€” deliberately not latest | `ak sync` (repins on core skew) | disk: global `package.json` | ruflo's **bundled** copy (coherence), not npm latest β€” by design | row βœ“ / n/a / card βœ“; banner excluded (its authority isn't "latest") |
| **ruvnet-brain** | npm `ruvnet-brain@latest` + `--version v<tag>` pin (never `github:` HEAD) | `ak sync`; the installer's own nightly self-updater is suppressed at install (`--no-nightly-prompt`) and disabled by sync if found (`ruvnet-brain-nightly` subsystem) | disk: KB `SOURCE.json β†’ releaseTag`, falling back to ak's kit.json stamp for pre-stamping bundles | GitHub `releases/latest` tag (TTL-cached) | row βœ“ / `V<tag>` chip βœ“ / card + banner βœ“ |
| **kit (self)** | npm, **pinned to the exact version drift saw** (`@pacphi/agentic-kit@<v>`) | `ak sync` (runs last β€” npm replaces the running code) | disk: running copy's `package.json` | npm `latest` (+ `next` for prereleases, TTL-cached) | row βœ“ / n/a / header version + card + banner βœ“ |

Statusline "n/a" cells are by design: the footer decorates the activation rows
it renders (ruflo / Agentic QE / brain) β€” hosts, agentdb, and the kit have no
footer row to decorate, and their versions live in `ak status` and the
dashboard.

## Where each piece lives

- **npm tools** β€” `src/lib/versions.mjs` (`installedVersion`, `driftReport`,
`selfDrift`), heals in `src/lib/heal.mjs` (`upgradePackage`, `selfUpdate`).
- **hosts** β€” `src/lib/providers.mjs` (`hostInstallState`, `installHost`,
`updateHost`, `hostDrift`).
- **agentdb** β€” `src/lib/agentdb.mjs` (`coherence`), heal
`healAgentdb` (pins to the bundled version).
- **ruvnet-brain** β€” `src/lib/ruvnet-brain.mjs` (`installedReleaseOnDisk`,
`latestVersion`, `classifyDrift`, `drift`, nightly-agent detection), heals
`installRuvnetBrain` / `disableRuvnetBrainNightly`. Full background on its
three version namespaces and the installer's `--yes` gotcha: MAINTAINER.md.
- **display surfaces** β€” `src/commands/status.mjs` (rows),
`src/templates/statusline-footer.cjs` (chips),
`src/lib/dashboard-server.mjs` (cards from the same rows; banner =
`driftReport` + `selfDrift` fold + `foldBrainDrift`).

## Adding a new tool: the checklist

1. **Install a published artifact** (npm release, tagged release asset) β€”
never a moving branch head. If the installer takes a version, resolve the
target version *first*, install it pinned, and only then record it.
2. **Decide the version authority** and keep both sides of the drift check in
that one namespace. Document the authority if it isn't npm-latest.
3. **Read installed state from disk** in a way that survives out-of-band
changes. If the tool stamps its own version on disk, prefer that stamp;
keep any ak-side record as a fallback only.
4. **Make sync the only updater.** If the tool ships auto-update machinery,
suppress it at install time and detect + disable it as drift (its own
subsystem, so sync's fix is proportionate β€” never a forced reinstall).
If ak can't own updates (external install), report installed-only and
`outdated: false`.
5. **Wire all the surfaces**: a `status` row (with the fix named), the
statusline only if the tool has a footer row, a dashboard card (free β€”
cards render status rows), and the dashboard banner (fold into the drift
array if the tool isn't in `driftReport`).
6. **Lock it with tests**: the install spec + suppression flags (regression
lock), the disk-read parser's edge cases (missing / malformed / junk),
and the display resolution order.
2 changes: 1 addition & 1 deletion docs/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ fix; `sync` applies them in the right order and re-checks afterward.
| Want to change which MCP tool families are callable | Exclusions are `permissions.deny` rules, persisted in kit.json | `ak x mcp pick` (re-runnable); `x mcp status` shows the inventory; `x mcp off` unregisters |
| `ruflo memory store` says OK but reads return nothing | Absolute-DB-path pin missing, or WAL not checkpointed, or the WASM fallback above | `ak setup` in the project re-pins + verifies a real write lands on disk |
| Suspicious token burn | Background automation vs interactive usage | ask Claude to run the **ruflo-token-audit** skill (deployed by `setup`) |
| `status` shows `ruvnet-brain … not installed` | The RuvNet Brain (offline KB + `search_ruvnet` MCP) isn't on disk | `ak sync` (or `ak setup`) runs the installer; `npx github:stuinfla/ruvnet-brain --doctor` health-checks it |
| `status` shows `ruvnet-brain … not installed` | The RuvNet Brain (offline KB + `search_ruvnet` MCP) isn't on disk | `ak sync` (or `ak setup`) runs the installer; `npx ruvnet-brain --doctor` health-checks it |
| Don't want the RuvNet Brain (the ~512 MB KB download) | It's on by default | `ak setup --no-ruvnet-brain`, or set `ruvnetBrain: false` in `~/.config/agentic-kit/kit.json` |
| RuvNet Brain KB lives somewhere non-default | The installer + ak honor `$RUVNET_BRAIN_KB` (default `~/.cache/ruvnet-brain/kb`) | export `RUVNET_BRAIN_KB` so detection points at your KB |

Expand Down
11 changes: 10 additions & 1 deletion src/commands/status.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { registry, syncBlocks } from '../lib/blocks.mjs';
import { loadKitConfig } from '../lib/config.mjs';
import { driftReport, selfDrift } from '../lib/versions.mjs';
import { upstreamCveCounterFabricated, fixStatusline } from '../lib/statusline.mjs';
import { drift as ruvnetBrainDrift } from '../lib/ruvnet-brain.mjs';
import { drift as ruvnetBrainDrift, nightlyAgentPresent as rbNightlyPresent, NIGHTLY_LABEL as RB_NIGHTLY_LABEL } from '../lib/ruvnet-brain.mjs';
import { coherence as adbCoherence } from '../lib/agentdb.mjs';
import { readJson } from '../lib/settings.mjs';
import { have } from '../lib/exec.mjs';
Expand Down Expand Up @@ -85,6 +85,15 @@ export async function collect({ pkgRoot, cwd = process.cwd() }) {
} catch (e) {
rows.push(row('ruvnet-brain', 'warn', `ruvnet-brain check unavailable: ${e.message}`));
}
// The installer's own nightly self-updater (macOS LaunchAgent, 03:47) bypasses
// ak-managed updates: it rewrites the KB outside ak's release stamp, so status
// and the statusline drift from disk. Own subsystem so sync's fix is "disable
// the agent", never a needless force-reinstall of the brain itself.
if (rbNightlyPresent()) {
rows.push(row('ruvnet-brain-nightly', 'warn',
`ruvnet-brain nightly self-updater active (${RB_NIGHTLY_LABEL}) β€” bypasses ak-managed updates`,
'sync disables it (re-enable deliberately: `npx ruvnet-brain --enable-nightly`)'));
}
}

// self (the kit's own version β€” prerelease installs track the `next` tag)
Expand Down
6 changes: 6 additions & 0 deletions src/commands/sync.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ export async function run({ flags, pkgRoot }) {
if (subsystems.has('ruvnet-brain') && !flags['no-upgrade']) {
report('ruvnet-brain', await heal.installRuvnetBrain({ force: true }));
}
// The brain installer's own nightly self-updater (macOS LaunchAgent) bypasses
// ak-managed updates β€” disabling it is a heal, not an upgrade, so it runs even
// under --no-upgrade. Reversible: `npx ruvnet-brain --enable-nightly`.
if (subsystems.has('ruvnet-brain-nightly')) {
report('ruvnet-brain nightly', await heal.disableRuvnetBrainNightly());
}
if (subsystems.has('security') || subsystems.has('versions')) {
report('aidefence', await heal.healAidefence());
report('aqe solver', await heal.healAqeSolver());
Expand Down
Loading
Loading