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
4 changes: 3 additions & 1 deletion bin/agentic-kit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const PORCELAIN = {
const PLUMBING = {
'daemon-gc': () => import('../src/commands/x/daemon-gc.mjs'),
'mcp': () => import('../src/commands/x/mcp.mjs'),
'provider': () => import('../src/commands/x/provider.mjs'),
'reference': () => import('../src/commands/x/reference.mjs'),
'verify': () => import('../src/commands/x/verify.mjs'),
};
Expand All @@ -43,8 +44,9 @@ const HELP_ALL = `${HELP}
Plumbing commands:
ak x daemon-gc [--kill] list/stop stale ruflo daemons
ak x mcp [pick|off|status] MCP registration + tool-family deny rules
ak x provider [pick|off|status] detect claude/codex CLIs; wire ruflo + aqe hosts/providers
ak x reference [diff|sync] CLAUDE.md managed-block inspection/reconcile
ak x verify [learning|security|aqe|all] deep proofs (slow, spawns real CLIs)
ak x verify [learning|security|aqe|providers|all] deep proofs (slow, spawns real CLIs)
ak x improvement-eval [...] causal self-improvement eval (route Q-learner)`;

async function main() {
Expand Down
16 changes: 11 additions & 5 deletions claude/aqe-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,17 @@ called first**, e.g. `fleet_init({ topology:"hierarchical", maxAgents:15, memory
`statusLine`, and user `AQE_*` env overrides survive re-init; a one-time
`.claude/settings.json.backup` is written first. Keep aqe ≥3.12.1 — 3.11.x init
stripped foreign hooks.
- **Run QE on a Claude subscription instead of an API key** (≥3.12.2, runtime env — init
never writes these): `AQE_LLM_PROVIDER=claude-code` routes analysis through `claude -p`;
`AQE_MAX_BUDGET_USD` (or `--max-budget-usd`) enforces a fleet-wide spend cap that aborts
over-budget requests before spending. `aqe health` shows an "LLM Billing" section saying
who pays for each call.
- **Choose which LLM runs QE** (≥3.12.2, runtime env; ADR-123): `AQE_LLM_PROVIDER=<type>`
force-selects the provider for analysis — `claude-code` (your Claude subscription, via
`claude -p`), `claude`/`openai`/`gemini`/`openrouter`/`azure-openai`/`bedrock`/`cognitum`
(metered API key), or `ollama` (local). It normalizes `anthropic`→`claude` and ignores
unknown values. `AQE_MAX_BUDGET_USD` (or `--max-budget-usd`) caps metered spend; `aqe health`
shows an "LLM Billing" section saying who pays. `aqe init` never writes these — but
**`ak x provider pick` now manages `AQE_LLM_PROVIDER` for you** (into
`.claude/settings.local.json` `env`, reversibly), and can write an ordered **fallback chain**
into `.agentic-qe/llm-config.json` from `kit.json` (`--aqe-fallback 'claude-code:claude-opus-4-8; openai:gpt-5.6'`) —
keys stay in the env, never the file. aqe is NOT limited to claude-code: codex the *CLI* isn't a
provider type, but its OpenAI models are reachable via `AQE_LLM_PROVIDER=openai`.

### QE agents via the native Task tool
QE agents live under `.claude/agents/v3/` once `aqe init` has run in the repo:
Expand Down
83 changes: 83 additions & 0 deletions claude/providers-reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<!-- BEGIN ruflo-providers-reference -->
<!-- ruflo-providers-reference: merged into ~/.claude/CLAUDE.md ONLY when the `codex` CLI
is on PATH. Managed by agentic-kit / `ak x reference sync` — stripped automatically
when codex is uninstalled. Do not hand-edit between the sentinels. -->

## Frontier hosts & LLM providers (claude / codex)

This machine has **both** frontier-agent CLIs installed. `ak` detects them and wires ruflo +
agentic-qe to use one or both. Two independent axes:

- **Host axis** — which agent CLI runs the *ruflo* loop: `claude` (Claude Code) and/or `codex`
(OpenAI Codex). ruflo can run **both at once** (dual-mode).
- **Provider axis** — which LLM the *routers* use, independent of the host:
- **agentic-qe** — `AQE_LLM_PROVIDER=<type>` selects any of `claude-code` (subscription),
`claude` / `openai` / `gemini` / `openrouter` / `azure-openai` / `bedrock` / `cognitum`
(metered API key), or `ollama` (local). codex the CLI isn't a provider type, but its OpenAI
models are reachable via `openai`.
- **ruflo** — `anthropic` / `openai` / `google` / `ollama` via `ruflo providers configure`.
- API keys live in the environment; they are never persisted to `kit.json`.

**One or several — you're never forced to pick just one.** All three surfaces run multiple
providers concurrently:
- **ruflo hosts** — enable `claude` *and* `codex` together (dual-mode); ruflo runs both.
- **ruflo LLM providers** — a list, with load-balancing + automatic failover.
- **agentic-qe** — its `HybridRouter` **auto-enables every provider that has an API key in the
env** and fails over across an ordered chain. `AQE_LLM_PROVIDER` only pins the *default* (the
primary) — the others stay enabled. So `ak x provider` sets aqe's primary; adding
`OPENAI_API_KEY` / `GEMINI_API_KEY` to the env brings those online as fallbacks automatically.

### aqe fallback chain — managed from `kit.json`

For **deterministic** ordering (rather than relying on env auto-enable), `ak` writes aqe's
`.agentic-qe/llm-config.json` from `kit.json`:

```bash
ak x provider pick --aqe-provider claude-code \
--aqe-fallback 'claude-code:claude-opus-4-8; openai:gpt-5.6; gemini:gemini-3.5-flash'
```

Each `provider:model,model` entry becomes an ordered `fallbackChain` entry (first = highest
priority; model IDs are examples current as of July 2026 — use what your provider offers).
ak writes a **complete** chain (aqe merges it shallowly, so partial chains would drop
defaults), sets each provider `enabled`, and tags the file `_managedBy: agentic-kit`. **API keys
are never written** — they stay in the env (aqe refuses to persist them anyway). `ak sync`
reapplies the chain; `ak status` flags drift; `ak x provider off` restores the pre-ak file from
its one-time `.bak` (or removes an ak-created file). Entries need populated models — aqe's router
skips an entry with none. For lower-level edits, `aqe llm-router config` still works.

### Managing it (prompts-once, reversible)

```bash
ak x provider status # detected CLIs + versions, what's enabled, what's wired
ak x provider pick # choose ruflo hosts / aqe provider / ruflo API providers → persist → apply
ak x provider off # reset to claude-only default; strip managed env keys
```

`pick` persists your choice to `kit.json` and applies it: it writes the ruflo backend flags
(`ENABLE_CLAUDE_CODE` / `ENABLE_CODEX`) and `AQE_LLM_PROVIDER` into
`.claude/settings.local.json` `env` (merge-not-clobber, backup-first), runs
`ruflo init --dual` when codex is enabled, and registers any API-key providers with ruflo.
`ak sync` reapplies the same choice idempotently; `ak status` shows **hosts** and
**providers** rows and flags drift. At the claude-only default nothing is written — behavior
is unchanged until you opt in.

### Install & update (install-method-aware)

- **First install** — `ak setup` (and `ak x provider pick`) installs any *enabled* host that
is entirely **absent**: `npm i -g @anthropic-ai/claude-code` / `@openai/codex`.
- **Updates** — `ak sync` keeps **npm-managed** hosts current (drift is detected on the same
cached TTL as ruflo/aqe, and surfaces in the bin nudge + `ak status`).
- **Externally-installed CLIs are never touched.** If a host was installed by mise, the
native installer, or Homebrew, ak reports its version and marks it *self-managed* — it will
not shadow it with an npm copy or try to update it. Update those with your own tool.

### Grounding (rUv source)

- ruflo **ADR-034 Optional MCP Backends** (accepted): Claude Code / Gemini / OpenAI Codex
backends enabled via `ENABLE_CLAUDE_CODE` / `ENABLE_GEMINI_MCP` / `ENABLE_CODEX`.
- `@claude-flow/codex` adapter + `ruflo init --dual` ("Initialize for both Claude Code and
OpenAI Codex").
- `ruflo providers list|configure|test` — the API-key provider matrix.

<!-- END ruflo-providers-reference -->
116 changes: 116 additions & 0 deletions docs/PROVIDERS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Model providers & hosts — the simple path, and how to go deeper

`ak`'s job is to make **the best default the simplest thing** — and then get out of your
way when you want to customize, exactly as you would if you drove `ruflo` and `agentic-qe`
by hand. Everything `ak` writes is *their* standard config; `ak` just converges to it,
proves it, and can undo it.

There are two independent things you can point at a model:

- **Hosts** — which agent CLI runs the *ruflo* loop: `claude` (Claude Code), `codex` (OpenAI
Codex), or **both** at once.
- **Providers** — which LLM the *routers* use: ruflo's provider router and agentic-qe's
`HybridRouter`. Independent of the host; API keys always live in your environment.

---

## Level 0 — do nothing (the point)

Install `ak`, run `ak setup`. Claude Code is the host, agentic-qe uses its own default, and
nothing about providers is written anywhere. This is the whole feature for most people:
**it already works, and `kit.json` stays at its defaults.**

```
ak setup # claude just works; codex/other providers are opt-in
ak status # shows a "hosts" + "providers" row so you can see what's true
```

If you happen to have `codex` installed, `ak` notices and *offers* — it never flips it on
for you:

```
ℹ codex CLI detected — run `ak x provider pick` to let ruflo use both claude and codex
```

## Level 1 — turn on codex (one command)

```
ak x provider pick
```

An interactive picker (or flags for scripts). Enable `codex` and `ak`:
- installs it if it's missing (`npm i -g @openai/codex`) — but leaves an existing
mise/brew/native install alone,
- runs `ruflo init --dual` (ruflo's "Claude Code + Codex hybrid" mode),
- writes `ENABLE_CLAUDE_CODE` / `ENABLE_CODEX` into `.claude/settings.local.json`.

```
ak x provider pick --host claude,codex --yes # non-interactive
```

## Level 2 — choose which LLM runs QE

agentic-qe can run its analysis on any of: `claude-code` (your Claude subscription),
`claude` / `openai` / `gemini` / `openrouter` / `azure-openai` / `bedrock` / `cognitum`
(metered API key), or `ollama` (local).

```
ak x provider pick --aqe-provider claude-code # run QE on your subscription, no API bill
```

`ak` writes `AQE_LLM_PROVIDER` for you. Add `OPENAI_API_KEY` to your env and agentic-qe's
router will **auto-enable** OpenAI as a fallback on its own — you don't have to list it.

## Level 3 — a deterministic fallback chain

When you want explicit ordering rather than env auto-enable, `ak` manages agentic-qe's
`.agentic-qe/llm-config.json` from `kit.json`:

```
ak x provider pick \
--aqe-provider claude-code \
--aqe-fallback 'claude-code:claude-opus-4-8; openai:gpt-5.6; gemini:gemini-3.5-flash'
```

Each `provider:model,model` becomes an ordered chain entry (first = highest priority). `ak`
writes a complete, schema-correct chain, tags it `_managedBy: agentic-kit`, and **never**
writes your API keys.

> Model IDs above are examples current as of July 2026 (Claude Opus 4.8, OpenAI GPT-5.6 —
> or `gpt-5.3-codex` for agentic coding — Google Gemini 3.5 Flash). Use whatever IDs your
> provider currently offers; `ak` writes the strings you give it verbatim.

## Level 4 — drop down to raw ruflo / agentic-qe

This is the part that matters: **`ak` is a facilitator, not a wall.** Every value it manages
is the tool's own native config, and you can set it by hand — or let `ak` and hand-edits
coexist. `ak` merges-not-clobbers and backs up first, mirroring how rUv itself layers config
(`mergeWithDefaults(config, defaults)` — sensible defaults, override with your partial).

| You want to… | `ak` way | The raw ruflo/aqe way it maps to |
| ------------------------------------ | --------------------------------- | --------------------------------------------------- |
| Enable claude/codex hosts | `ak x provider pick` | `ENABLE_CLAUDE_CODE` / `ENABLE_CODEX` env (ADR-034) + `ruflo init --dual` |
| Register a ruflo LLM provider | `--provider openai:gpt-5.6` | `ruflo providers configure -p openai -m gpt-5.6` |
| Set which LLM runs QE | `--aqe-provider gemini` | `AQE_LLM_PROVIDER=gemini` (env) |
| Order QE's fallback chain | `--aqe-fallback '…'` | edit `.agentic-qe/llm-config.json` / `aqe llm-router config` |
| Cap QE spend | (kit.json `maxBudgetUsd`) | `AQE_MAX_BUDGET_USD` / `--max-budget-usd` |

If you hand-edit `.agentic-qe/llm-config.json` yourself and *don't* use `ak`'s
`--aqe-fallback`, `ak` leaves your file alone — it only manages a chain it owns (the
`_managedBy` tag). Keys always stay in the environment; neither `ak` nor aqe persists them.

## Undo, always

```
ak x provider off # reset to the claude-only default, reversibly
```

Strips the managed env keys (leaving your other settings), and restores your pre-`ak`
`llm-config.json` from its one-time backup — or removes the file if `ak` created it. `ak
status` and `ak sync` keep everything converged and flag drift in between.

---

**The shape of the whole thing:** Level 0 is the 90% case and costs nothing. Each level up is
one flag, and the bottom is always the tools' own knobs — `ak` never traps your config, it
just makes the good default automatic and the customization reversible.
37 changes: 37 additions & 0 deletions src/commands/setup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { fixStatusline } from '../lib/statusline.mjs';
import { registry, syncBlocks } from '../lib/blocks.mjs';
import { register as mcpRegister, applyExclusions } from '../lib/mcp.mjs';
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 { readJson, writeJsonWithBackup } from '../lib/settings.mjs';
import { scalar, checkpoint, withDb } from '../lib/sqlite.mjs';
Expand Down Expand Up @@ -86,6 +87,26 @@ export async function run_machine({ flags, pkgRoot, cfg }) {
ok(`MCP registered${denied ? ` (${denied} tool(s) denied per kit.json)` : ''} — exclude families anytime: ak x mcp pick`);
} else warn('claude mcp add failed — run: ak x mcp pick');
}

// 6. frontier hosts — install any ENABLED host that is entirely absent (default
// enables claude only). External installs (mise/native/brew) are left alone.
for (const h of HOSTS) {
if (!cfg.providers?.hosts?.[h.id]) continue;
const st = await hostInstallState(h);
if (st.method === 'absent') {
if (await ask(`${h.id} CLI not found — install ${h.pkg} globally?`, true, flags.yes)) {
const r = await installHost(h.id);
(r.ok ? ok : warn)(`${h.id}: ${r.detail}`);
} else warn(`${h.id} not installed — enable/install later with: ak x provider pick`);
} else {
ok(`${h.id} ${st.version ?? ''} present (${st.method}${st.method === 'external' ? ' — self-managed' : ''})`);
}
}

// 7. frontier host hint — codex detected but not enabled (opt-in via `x provider pick`)
if (!cfg.providers?.hosts?.codex && await have('codex')) {
info('codex CLI detected — run `ak x provider pick` to let ruflo use both claude and codex');
}
return true;
}

Expand Down Expand Up @@ -174,6 +195,22 @@ export async function run_project({ flags, cfg }) {
(aqe.code === 0 ? ok : warn)('agentic-qe initialized');
}

// 9.5 frontier host/provider wiring — reapply kit.json prefs (no-op at the
// claude-only default, so existing repos see zero change). When codex is
// enabled: write ENABLE_* env, regenerate dual-mode agents, register providers.
const ph = applyHosts(cfg, root);
if (ph.changed) ok(`providers: ${ph.detail}`);
const rt = applyAqeRouter(cfg, root);
if (rt.changed) (rt.ok ? ok : warn)(`aqe router: ${rt.detail}`);
if (cfg.providers?.hosts?.codex) {
const dual = await ensureDualAgents(cfg, root);
(dual.ok ? ok : warn)(`dual agents: ${dual.detail}`);
const prov = await applyProviders(cfg, root);
if (prov.changed) (prov.ok ? ok : warn)(`providers: ${prov.detail}`);
} else if (await have('codex')) {
info('codex CLI detected — enable dual-host with: ak x provider pick');
}

// 10. statusline footer — LAST, after ruflo + aqe have settled the helper.
// A still-missing footer is a WARN (not silent info): it means the AQE /
// SONA segments won't render and `ak sync` is needed to heal it.
Expand Down
54 changes: 54 additions & 0 deletions src/commands/status.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { registry, syncBlocks } from '../lib/blocks.mjs';
import { loadKitConfig } from '../lib/config.mjs';
import { driftReport, selfDrift } from '../lib/versions.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';

export const options = {
json: { type: 'boolean', default: false },
Expand Down Expand Up @@ -130,6 +132,58 @@ export async function collect({ pkgRoot, cwd = process.cwd() }) {
rows.push(row('mcp', 'warn', "legacy 'ruflo'-keyed MCP registration present", 'sync migrates it to claude-flow'));
}

// hosts (install-if-missing) — cheap: file read + `which`, no network.
// An enabled host that is entirely absent is installable by sync; an external
// install (mise/native/brew) is reported but never touched.
try {
for (const h of HOSTS) {
if (!cfg.providers.hosts[h.id]) continue;
const st = await hostInstallState(h);
if (st.method === 'absent') {
rows.push(row('hosts', h.id === 'claude' ? 'fail' : 'warn',
`${h.id} enabled but not installed`, `sync installs ${h.pkg}`));
} else {
rows.push(row('hosts', 'ok', `${h.id} ${st.version ?? ''} (${st.method}${st.method === 'external' ? ' — self-managed' : ''})`));
}
}
} catch (e) {
rows.push(row('hosts', 'warn', `host check unavailable: ${e.message}`));
}

// providers (frontier host wiring) — light: `have` probe + env read, no --version
try {
const { file, scope } = settingsTarget(cwd);
const env = readJson(file, {})?.env ?? {};
if (isDefault(cfg)) {
// advisory only (no fix): opting codex in is a deliberate `x provider pick`
if (await have('codex')) {
rows.push(row('providers', 'info', 'codex CLI installed but not enabled (claude-only default)'));
} else {
rows.push(row('providers', 'info', 'claude-only (default host)'));
}
} else {
const desired = managedEnv(cfg);
const envDrift = MANAGED_ENV_KEYS.some((k) => (k in desired ? env[k] !== desired[k] : k in env));
// aqe fallback chain: on-disk llm-config.json must match kit.json order
const chain = cfg.providers.aqeFallback ?? [];
let routerDrift = false;
if (chain.length) {
const disk = readJson(aqeRouterFile(cwd));
const diskOrder = (disk?.fallbackChain?.entries ?? []).map((e) => e.provider).join('→');
routerDrift = disk?._managedBy !== 'agentic-kit' || diskOrder !== chain.map((e) => e.provider).join('→');
}
const on = HOSTS.filter((h) => cfg.providers.hosts[h.id]).map((h) => h.id).join('+') || 'none';
const chainStr = chain.length ? `; aqe chain ${chain.map((e) => e.provider).join('→')}` : '';
if (envDrift || routerDrift) {
rows.push(row('providers', 'warn', `provider config drifted (want ${on}${chainStr}, ${scope})`, 'sync re-applies provider env + aqe router'));
} else {
rows.push(row('providers', 'ok', `wired: ${on}${chainStr} (${scope})`));
}
}
} catch (e) {
rows.push(row('providers', 'warn', `provider check unavailable: ${e.message}`));
}

// daemons
try {
const daemons = await listDaemons({ cwd });
Expand Down
Loading