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
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,33 @@ What the verbs cover:

| Verb | What it does |
| ------ | -------------- |
| **setup** | Installs/updates ruflo + agentic-qe globally (handling npm β‰₯11.17's `allow-scripts` so natives build), installs the **RuvNet Brain** (an offline knowledge base over the rUv stack, powering the `search_ruvnet` MCP β€” a ~512 MB one-time download, prompted; skip with `--no-ruvnet-brain`), deploys the token-audit skill, merges the managed guidance blocks into `~/.claude/CLAUDE.md`, offers one-time MCP registration (user scope, with a tool-family picker), and β€” inside a repo β€” initializes the project: sanitized `ruflo init`, absolute memory-path pin, a **verified** storeβ†’disk write, statusline footer, and a background daemon with **local-only ($0) workers** (token-spending AI workers stay opt-in behind upstream's machine-wide budget). Project scope triggers on a `.git` directory in the current folder; without one it's skipped with a note. `--project` forces it anyway (e.g. a not-yet-`git init`-ed folder), `--minimal` skips it, `--yes` accepts all prompts (non-interactive), `--no-aqe` / `--no-ruvnet-brain` / `--no-security` disable those subsystems, and `--reconfigure` re-offers MCP registration. |
| **status** | Per-subsystem βœ“/⚠/βœ— (versions, the kit's own version, **ruvnet-brain** (present + release drift, or "not installed"), natives, security, learning, aqe/RVF, MCP, **hosts** (claude/codex β€” version + install method, or "enabled but not installed"), **providers** (host wiring + aqe fallback chain, or "drifted"/claude-only default), daemons, CLAUDE.md blocks, statusline), each drift row naming what `sync` would do about it. |
| **sync** | The one convergence verb: upgrades first when a new release exists, then re-heals everything an upgrade wipes, then re-checks and reports. Included in that heal: it **installs any enabled frontier host** (claude/codex) that's entirely absent β€” never touching an external (mise/brew/native) install β€” and **re-applies provider wiring** (the `ENABLE_*` host env, the aqe fallback chain, and ruflo API providers) whenever it has drifted. It also **re-runs the RuvNet Brain installer** to pull the latest release when the on-disk KB has drifted (or installs it if absent, when enabled). It also **self-updates the kit**: when a newer `@pacphi/agentic-kit` exists it installs it as the *last* step (the new code applies from the next `ak` run, never mid-sync). Prerelease installs (`4.0.0-alpha.*`) track the `next` npm dist-tag as well as `latest`, so alphas see their successors; stable installs only ever follow `latest`. `--no-upgrade` skips the self-update along with the package upgrades. |
| **setup** | Installs/updates ruflo + agentic-qe + the **agentdb** CLI globally (handling npm β‰₯11.17's `allow-scripts` so natives build; agentdb is pinned to ruflo's bundled version so the shared learning store stays coherent), installs the **RuvNet Brain** (an offline knowledge base over the rUv stack, powering the `search_ruvnet` MCP β€” a ~512 MB one-time download, prompted; skip with `--no-ruvnet-brain`), deploys the token-audit skill, merges the managed guidance blocks into `~/.claude/CLAUDE.md`, offers one-time MCP registration (user scope, with a tool-family picker), and β€” inside a repo β€” initializes the project: sanitized `ruflo init`, absolute memory-path pin, a **verified** storeβ†’disk write, statusline footer, and a background daemon with **local-only ($0) workers** (token-spending AI workers stay opt-in behind upstream's machine-wide budget). Project scope triggers on a `.git` directory in the current folder; without one it's skipped with a note. `--project` forces it anyway (e.g. a not-yet-`git init`-ed folder), `--minimal` skips it, `--yes` accepts all prompts (non-interactive), `--no-aqe` / `--no-ruvnet-brain` / `--no-security` disable those subsystems, and `--reconfigure` re-offers MCP registration. |
| **status** | Per-subsystem βœ“/⚠/βœ— (versions, the kit's own version, **ruvnet-brain** (present + release drift, or "not installed"), natives, security, learning, aqe/RVF, **agentdb** (CLI present + coherent with ruflo's bundled version, or a store-skew warning), MCP, **hosts** (claude/codex β€” version + install method, or "enabled but not installed"), **providers** (host wiring + aqe fallback chain, or "drifted"/claude-only default), daemons, CLAUDE.md blocks, statusline), each drift row naming what `sync` would do about it β€” plus a **health-history** line that flags regressions since the last sync (learning shrank, native slots dropped, drift/security backslid). |
| **sync** | The one convergence verb: upgrades first when a new release exists, then re-heals everything an upgrade wipes, then re-checks and reports. Included in that heal: it **installs any enabled frontier host** (claude/codex) that's entirely absent β€” never touching an external (mise/brew/native) install β€” and **re-applies provider wiring** (the `ENABLE_*` host env, the aqe fallback chain, and ruflo API providers) whenever it has drifted. It also **installs/repins the standalone `agentdb` CLI** to ruflo's bundled version (keeping the shared cognitive store coherent) and appends a **health-history snapshot** so `status` can flag regressions across syncs. It also **re-runs the RuvNet Brain installer** to pull the latest release when the on-disk KB has drifted (or installs it if absent, when enabled). It also **self-updates the kit**: when a newer `@pacphi/agentic-kit` exists it installs it as the *last* step (the new code applies from the next `ak` run, never mid-sync). Prerelease installs (`4.0.0-alpha.*`) track the `next` npm dist-tag as well as `latest`, so alphas see their successors; stable installs only ever follow `latest`. `--no-upgrade` skips the self-update along with the package upgrades. |
| **uninstall** | Removes the kit's footprint (and any legacy shell-kit install); project data is never touched; `--purge` also offers to remove the global packages. |

Power-user mechanisms live under `ak x …` (`daemon-gc`, `mcp pick|off`,
`provider status|pick|off`, `reference diff|sync`, `verify learning|security|aqe`,
`improvement-eval`) β€” see `ak --help --all`.
Power-user mechanisms live under `ak x …` (`daemon-gc`, `dashboard`, `harvest`,
`mcp pick|off`, `provider status|pick|off`, `reference diff|sync`,
`verify learning|security|aqe|providers|harvest`, `improvement-eval`) β€” see `ak --help --all`.

Two of those are worth calling out:

- **`ak x dashboard`** β€” a read-only local web dashboard (`127.0.0.1:7431`, localhost-only,
never detaches) that renders the same subsystem view as `ak status`, grouped one card per
subsystem with severity triage and a learning-over-time strip. Self-contained and offline β€”
no external fetches. Stop with Ctrl-C.
- **`ak x harvest`** β€” an **opt-in** (`kit.json` `harvest:true`), foreground learning-*write*:
it records the session outcome and consolidates accumulated episodes into durable skills via
the real `ruflo hooks post-task` / `agentdb skill consolidate` verbs, reporting the actual
skills created/updated. Off and `--dry-run`-safe by default; no daemon, ever.
`ak x verify harvest` proves the whole path end-to-end against real CLIs.

## The status line

Projects set up by the kit get an append-only footer under ruflo's own status line,
each segment shown **only when genuinely active**: 🧠 SONA patterns/trajectories (+
live micro-LoRA Ξ”β€–Wβ€–), πŸ“ˆ route-RL metrics, πŸ›‘ aidefence, βš™ machine-wide daemon
count, and πŸŽ“ Agentic-QE stats.
live micro-LoRA Ξ”β€–Wβ€–), πŸ“ˆ route-RL metrics, πŸ›‘ aidefence, 🧿 RuvNet Brain KB,
βš™ machine-wide daemon count, and πŸŽ“ Agentic-QE stats.

## Requirements

Expand Down
4 changes: 4 additions & 0 deletions bin/agentic-kit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const PORCELAIN = {

const PLUMBING = {
'daemon-gc': () => import('../src/commands/x/daemon-gc.mjs'),
'dashboard': () => import('../src/commands/x/dashboard.mjs'),
'harvest': () => import('../src/commands/x/harvest.mjs'),
'mcp': () => import('../src/commands/x/mcp.mjs'),
'provider': () => import('../src/commands/x/provider.mjs'),
'reference': () => import('../src/commands/x/reference.mjs'),
Expand Down Expand Up @@ -46,6 +48,8 @@ const HELP_ALL = `${HELP}

Plumbing (power users) β€” each takes --help:
ak x daemon-gc [--kill] list/stop stale ruflo daemons
ak x dashboard [--port N] read-only local health dashboard (localhost only)
ak x harvest [--dry-run] opt-in learning-write: replay experiences into the substrate
ak x mcp [status|pick|off] MCP registration + tool-family deny rules
ak x provider [status|pick|off] detect claude/codex CLIs; wire ruflo + aqe hosts/providers
ak x reference [diff|sync] CLAUDE.md managed-block inspection/reconcile
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default [
// deliberately old-school (var, literal ANSI escape bytes, bare catch bindings)
// because the snippet runs embedded in the user's shell, not as normal source.
// Relax the idiom rules here; syntax/undef checks still apply.
files: ['src/templates/statusline-footer.cjs', 'tests/statusline-segments.test.cjs'],
files: ['src/templates/statusline-footer.cjs', 'tests/statusline-segments.test.cjs', 'tests/statusline-brain.test.cjs'],
// getStdinData is injected by the host statusline runtime (guarded with typeof).
languageOptions: { globals: { getStdinData: 'readonly' } },
rules: {
Expand Down
12 changes: 12 additions & 0 deletions src/commands/setup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { loadKitConfig, saveKitConfig } from '../lib/config.mjs';
import { HOSTS, applyHosts, applyProviders, ensureDualAgents, hostInstallState, installHost, applyAqeRouter } from '../lib/providers.mjs';
import { installedVersion } from '../lib/versions.mjs';
import * as rb from '../lib/ruvnet-brain.mjs';
import * as adb from '../lib/agentdb.mjs';
import { readJson, writeJsonWithBackup } from '../lib/settings.mjs';
import { scalar, checkpoint, withDb } from '../lib/sqlite.mjs';
import * as paths from '../lib/paths.mjs';
Expand Down Expand Up @@ -81,6 +82,17 @@ export async function run_machine({ flags, pkgRoot, cfg }) {
(r.ok ? ok : warn)(`agentic-qe: ${r.detail}`);
} else ok(`agentic-qe ${installedVersion('agentic-qe')} present`);
}
if (cfg.agentdb) {
const c = adb.coherence();
if (!c.present) {
info("installing agentdb globally (harvest write path; pinned to ruflo's bundled version)…");
const r = await heal.healAgentdb();
(r.ok ? ok : warn)(`agentdb: ${r.detail}`);
} else if (c.skew === 'core') {
const r = await heal.healAgentdb();
(r.ok ? ok : warn)(`agentdb: ${r.detail}`);
} else ok(`agentdb ${c.global} present (coherent with ruflo)`);
}
if (cfg.ruvnetBrain) {
if (!rb.present()) {
if (await ask('Install the RuvNet Brain (~512 MB offline KB, powers the search_ruvnet MCP)?', true, flags.yes)) {
Expand Down
26 changes: 25 additions & 1 deletion src/commands/status.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
// (set by bare invocation) appends exactly one suggested next action.
import fs from 'node:fs';
import path from 'node:path';
import { glyph, dim, bold } from '../lib/output.mjs';
import { glyph, dim, bold, warn } from '../lib/output.mjs';
import { loadRing, detectRegression } from '../lib/health-history.mjs';
import * as paths from '../lib/paths.mjs';
import { nativesStatus, aidefencePresent, securityPresent } from '../lib/natives.mjs';
import { scanNpxStale } from '../lib/npx.mjs';
Expand All @@ -15,6 +16,7 @@ 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 { coherence as adbCoherence } from '../lib/agentdb.mjs';
import { readJson } from '../lib/settings.mjs';
import { have } from '../lib/exec.mjs';
import { HOSTS, settingsTarget, isDefault, managedEnv, MANAGED_ENV_KEYS, hostInstallState, aqeRouterFile } from '../lib/providers.mjs';
Expand Down Expand Up @@ -174,6 +176,25 @@ export async function collect({ pkgRoot, cwd = process.cwd() }) {
rows.push(row('aqe', 'info', 'agentic-qe not initialized in this project'));
}

// agentdb (data-plane CLI `ak x harvest` drives). Pinned to ruflo's BUNDLED
// agentdb so the shared cognitive store never skews on the core version.
if (cfg.agentdb === false) {
rows.push(row('agentdb', 'info', 'agentdb management disabled in kit.json'));
} else {
const c = adbCoherence();
if (!c.present) {
rows.push(row('agentdb', 'warn', 'agentdb CLI not installed (harvest write path unavailable)',
"setup/sync installs it (pinned to ruflo's bundled agentdb)"));
} else if (c.skew === 'core') {
rows.push(row('agentdb', 'warn',
`agentdb ${c.global} skewed from ruflo-bundled ${c.bundled} β€” shared-store corruption risk`,
"sync repins agentdb to ruflo's bundled version"));
} else {
rows.push(row('agentdb', 'ok',
`agentdb ${c.global}${c.bundled ? ` (coherent with ruflo${c.skew === 'prerelease' ? ' β€” prerelease diff' : ''})` : ''}`));
}
}

// MCP
const mcp = registrationStatus();
if (mcp.claudeFlow) {
Expand Down Expand Up @@ -333,6 +354,9 @@ export async function run({ flags, pkgRoot }) {
console.log(` ${glyph(r.level)} ${label.padEnd(11)} ${r.message}${r.fix ? dim(` β†’ ${r.fix}`) : ''}`);
}

// health-history: alarm on any backslide since the previous sync snapshot.
for (const reg of detectRegression(loadRing(loadKitConfig()))) warn(`regression: ${reg.message}`);

if (flags.hint) {
const actionable = rows.filter((r) => r.fix);
console.log('');
Expand Down
25 changes: 24 additions & 1 deletion src/commands/sync.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import { fixStatusline } from '../lib/statusline.mjs';
import { registry, syncBlocks } from '../lib/blocks.mjs';
import { register as mcpRegister, applyExclusions } from '../lib/mcp.mjs';
import { listDaemons, staleDaemons, reap } from '../lib/daemons.mjs';
import { loadKitConfig } from '../lib/config.mjs';
import { loadKitConfig, saveKitConfig } from '../lib/config.mjs';
import { HOSTS, applyHosts, applyProviders, hostInstallState, installHost, applyAqeRouter } from '../lib/providers.mjs';
import { driftReport, selfDrift } from '../lib/versions.mjs';
import { pruneNpxStale } from '../lib/npx.mjs';
import { nativesStatus, securityPresent } from '../lib/natives.mjs';
import { readJson } from '../lib/settings.mjs';
import { appendToConfig } from '../lib/health-history.mjs';
import * as paths from '../lib/paths.mjs';
import { ok, warn, fail, bold, dim } from '../lib/output.mjs';

Expand Down Expand Up @@ -94,6 +97,11 @@ export async function run({ flags, pkgRoot }) {
if (subsystems.has('aqe')) {
report('rvf', heal.healRvf(paths.projectAqeDir(cwd)));
}
// agentdb: install/repin the standalone CLI to ruflo's bundled version so the
// shared cognitive store stays coherent (harvest's write path depends on it).
if (subsystems.has('agentdb') && cfg.agentdb !== false) {
report('agentdb', await heal.healAgentdb());
}
if (subsystems.has('mcp') && cfg.mcp.register) {
const okReg = await mcpRegister();
if (okReg) {
Expand Down Expand Up @@ -147,6 +155,21 @@ export async function run({ flags, pkgRoot }) {
// converge proof
console.log('');
const after = await collect({ pkgRoot, cwd });

// health-history: append one post-heal snapshot so `status` can flag backslides
// (learning shrank, native slots dropped, drift/security regressed) across syncs.
try {
const stats = readJson(path.join(paths.projectClaudeFlowDir(cwd), 'neural', 'stats.json'));
appendToConfig(cfg, {
ts: Math.floor(Date.now() / 1000),
learningRows: stats?.patternsLearned ?? 0,
nativeSlots: nativesStatus()?.locations?.length ?? 0,
driftOutdated: (await driftReport()).some((r) => !r.installed || r.outdated),
securityPresent: securityPresent(),
});
saveKitConfig(cfg);
} catch { /* health snapshot is best-effort β€” never fail a sync over it */ }

const remaining = after.filter((r) => r.level === 'fail');
if (remaining.length === 0) { ok(bold('converged β€” no failing subsystems')); return 0; }
for (const r of remaining) fail(`still failing: [${r.subsystem}] ${r.message}`);
Expand Down
70 changes: 70 additions & 0 deletions src/commands/x/dashboard.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// x dashboard β€” a read-only local web dashboard for the kit's health.
//
// Boots a loopback-only HTTP server (127.0.0.1) that serves a single
// self-contained page plus a /api/status JSON endpoint mirroring
// `ak status --json` (plus version drift, improvement.json, and the health
// ring). Runs FOREGROUND and blocks until Ctrl-C; nothing is detached and
// nothing mutates state.
import { startDashboard } from '../../lib/dashboard-server.mjs';
import { ok, info, dim, warn } from '../../lib/output.mjs';

export const options = {
port: { type: 'string' },
};

export const help = `ak x dashboard β€” read-only local health dashboard (localhost only)

Serves a self-contained web panel that visualizes the same subsystem rows
\`ak status\` reports β€” versions, natives, security, learning, providers, hosts,
mcp, ruvnet-brain, aqe β€” plus version drift and a learning-history sparkline.
Bound to 127.0.0.1; auto-refreshes every 5s. Read-only: it never changes state.

Runs in the foreground β€” press Ctrl-C to stop.

Usage: ak x dashboard [options]

Options:
--port N port to bind on 127.0.0.1 (default 7431; 0 = ephemeral)

Examples:
ak x dashboard open on http://127.0.0.1:7431
ak x dashboard --port 8080 pick a port`;

export async function run({ flags }) {
let port = 7431;
if (flags.port !== undefined) {
const p = Number(flags.port);
if (!Number.isInteger(p) || p < 0 || p > 65535) {
warn(`invalid --port ${flags.port}; using ${port}`);
} else {
port = p;
}
}

let server;
try {
server = await startDashboard({ port, cwd: process.cwd() });
} catch (e) {
warn(`could not start dashboard: ${e.message}`);
if (e.code === 'EADDRINUSE') info(`port ${port} is busy β€” try: ak x dashboard --port 0`);
return 1;
}

ok(`dashboard live at ${server.url}`);
info(dim('read-only Β· localhost only Β· Ctrl-C to stop'));

// Block foreground until an interrupt, then close cleanly.
return await new Promise((resolve) => {
let closing = false;
const shutdown = async () => {
if (closing) return;
closing = true;
await server.close();
console.log('');
ok('dashboard stopped');
resolve(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
});
}
Loading
Loading