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
18 changes: 12 additions & 6 deletions docs/PROVIDERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,22 +178,28 @@ Defaults (all overridable; your edits are marked `custom` and never re-seeded):

**Known-good model choices** (verified 2026-07; any model your host CLI accepts also works):

> **Per-token price ≠ per-task cost.** A model that needs more agentic turns costs more
> per task at the same per-token price. On subscription (`claude-code` oauth) billing the
> marginal dollar cost is $0 either way, and the extra turns are paid in wall-clock and
> quota instead — so read every note below on the turns axis, not only the price axis.

| Host | Model | When to use |
|---|---|---|
| claude | `claude-opus-5` | new top Opus — ~2× Opus 4.8 at the same price; premium reasoning default |
| claude | `claude-sonnet-5` | near-Opus at lower cost — review, spec, release |
| claude | `claude-opus-5` | new top Opus — same per-token price as 4.8, but ~2–3× the agentic turns on routine work; earns it at the hard end |
| claude | `claude-sonnet-5` | near-Opus capability at a lower per-token price — review, spec, release |
| claude | `claude-fable-5` | top capability (Mythos-class, above Opus 5) — hardest problems |
| claude | `claude-haiku-4-5-20251001` | cheap/fast — high-volume mechanical |
| claude | `claude-opus-4-8` | prior Opus generation — same price as opus-5, kept for pinned configs |
| claude | `claude-haiku-4-5-20251001` | cheap/fast — high-volume mechanical work |
| claude | `claude-opus-4-8` | prior Opus generation — same per-token price, roughly half the turns on routine work |
| codex | `gpt-5.4` | coding + reasoning + agentic — recommended execution default |
| codex | `gpt-5.6-sol` | newest line; first-class max reasoning effort |
| codex | `gpt-5.3-codex` | pure coding-tuned — mechanical implementation & docs |
| codex | `gpt-5-codex-mini` | smallest/cheapest — escalation floor, high volume |

> **Where Opus 5 sits** ([announcement](https://www.anthropic.com/news/claude-opus-5), July 2026):
> same $5/$25 per-Mtok pricing as Opus 4.8 with roughly double the Frontier-Bench
> performance, so it strictly supersedes 4.8 as the reasoning-tier default — a capability
> tier above Opus 4.8 at no added cost. It is **not** Mythos-class: `claude-fable-5`
> performance, which is why it is the reasoning-tier default. That parity is **per token**:
> measured end-to-end it takes 2–3.4× the agentic turns on routine work, so per task it is
> the more expensive arm there and 4.8 remains a defensible pin. It is **not** Mythos-class: `claude-fable-5`
> remains the flagship tier. Opus 5 lands within ~0.5% of Fable on coding/agentic
> benchmarks at about half the cost per task, but stays behind the Mythos-class models on
> frontier domains. Rule of thumb: `claude-opus-5` is the premium default;
Expand Down
87 changes: 84 additions & 3 deletions src/commands/status.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { loadRing, detectRegression } from '../lib/health-history.mjs';
import * as paths from '../lib/paths.mjs';
import { nativesStatus, rufloRuntimeNatives, dbPathPinStatus, aidefencePresent, securityPresent } from '../lib/natives.mjs';
import { scanNpxStale } from '../lib/npx.mjs';
import { registrationStatus, codexMcpStatus, rufloCodexMcpStatus } from '../lib/mcp.mjs';
import { registrationStatus, codexMcpStatus, rufloCodexMcpStatus, ruvectorRegistered } from '../lib/mcp.mjs';
import { listDaemons, staleDaemons } from '../lib/daemons.mjs';
import { scanRvf } from '../lib/rvf.mjs';
import { registry, syncBlocks, blocksForTarget, retiredForTarget, guidanceTargets } from '../lib/blocks.mjs';
Expand All @@ -19,8 +19,10 @@ import { drift as ruvnetBrainDrift, nightlyAgentPresent as rbNightlyPresent, NIG
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, hostAuthState, bothHostsEnabled, aqeRouterFile, aqeSupportsAgentOverrides } from '../lib/providers.mjs';
import { policyToAgentOverrides, routingSummary } from '../lib/routing.mjs';
import { HOSTS, settingsTarget, isDefault, managedEnv, MANAGED_ENV_KEYS, hostInstallState, hostAuthState, bothHostsEnabled, aqeRouterFile, aqeSupportsAgentOverrides, credentialGaps } from '../lib/providers.mjs';
import { policyToAgentOverrides, routingSummary, divergedRoutes } from '../lib/routing.mjs';
import { qeCourtShipped, readQeCourtConfig, panelFromRouting, validatePanel, healJuryVendorCollision, UPSTREAM_JURY_VENDOR_ISSUE } from '../lib/qeCourt.mjs';
import { drift as ruvectorDrift } from '../lib/ruvector.mjs';

export const options = {
json: { type: 'boolean', default: false },
Expand Down Expand Up @@ -97,6 +99,35 @@ export async function collect({ pkgRoot, cwd = process.cwd() }) {
}
}

// ruvector — a global CLI users register as an MCP server BY HAND. ak manages
// its drift, never its presence or its registration. Unregistered → no row at
// all (same silence as codex-not-enabled): nudging a tool nobody opted into
// would be management by ambush. Registered but kit.json ruvector:false → an
// info row with NO fix, so sync never plans an upgrade the user turned off.
//
// Wording is deliberately "CLI": the registered command is typically
// `npx -y ruvector mcp start`, so upgrading the global package does not
// necessarily change what the MCP server executes. Claim only what is true.
if (ruvectorRegistered()) {
if (cfg.ruvector === false) {
rows.push(row('ruvector', 'info', 'ruvector MCP registered — CLI updates disabled (kit.json ruvector:false)'));
} else {
try {
const rv = await ruvectorDrift();
if (rv.present && rv.outdated) {
rows.push(row('ruvector', 'warn',
`ruvector CLI ${rv.installed} installed, ${rv.latest} available`, 'sync upgrades the ruvector CLI'));
} else if (rv.present) {
rows.push(row('ruvector', 'ok', `ruvector CLI ${rv.installed}${rv.latest ? ' (latest)' : ''} (MCP registered, user scope)`));
} else {
rows.push(row('ruvector', 'info', 'ruvector MCP registered but no global CLI installed (server runs via npx)'));
}
} catch (e) {
rows.push(row('ruvector', 'warn', `ruvector check unavailable: ${e.message}`));
}
}
}

// self (the kit's own version — prerelease installs track the `next` tag)
try {
const s = await selfDrift({ pkgRoot });
Expand Down Expand Up @@ -348,6 +379,20 @@ export async function collect({ pkgRoot, cwd = process.cwd() }) {
} else {
rows.push(row('providers', 'ok', `wired: ${on}${chainStr} (${scope})`));
}
// Chain VIABILITY, separate from chain ORDER above: a chain in the right
// order whose rungs have no credential fails over into nothing (#54). Warn,
// not fail — the primary rung still works — and no `fix`, since only the
// user can supply a key.
if (chain.length) {
const gaps = credentialGaps(chain);
if (gaps.length) {
rows.push(row('providers', 'warn',
`aqe chain: ${chain.length - gaps.length}/${chain.length} rungs have credentials `
+ `(${gaps.map((g) => `${g.provider}: needs ${g.missing.join(', ')}`).join('; ')})`));
} else {
rows.push(row('providers', 'ok', `aqe chain: ${chain.length}/${chain.length} rungs have credentials`));
}
}
}
} catch (e) {
rows.push(row('providers', 'warn', `provider check unavailable: ${e.message}`));
Expand All @@ -371,6 +416,20 @@ export async function collect({ pkgRoot, cwd = process.cwd() }) {
if (drift) rows.push(row('routing', 'warn', `${base} — llm-config.json out of sync`, 'sync re-applies agentOverrides'));
else rows.push(row('routing', 'ok', base));
}
// Seeded pins vs today's defaults. Deliberately `info` and deliberately
// "diverges from": which side wins is activity-dependent (a newer default
// can cost 2-3× the agentic turns on routine work), so a `warn` would push
// users to spend turns clearing a lint. No `fix` — sync must never
// auto-refresh a pin; `ak x provider refresh` is the opt-in path (#55).
const diverged = divergedRoutes(policy);
if (diverged.length) {
const pairs = [...new Set(diverged.flatMap((d) => [
...(d.modelDiverged ? [`${d.model} vs ${d.defaultModel}`] : []),
...d.escalate.map((e) => `${e.model} vs ${e.defaultModel} (escalation)`),
]))].join(', ');
rows.push(row('routing', 'info',
`${diverged.length} seeded route(s) diverge from current defaults (${pairs}) — ak x provider refresh`));
}
}
} catch (e) {
rows.push(row('routing', 'warn', `routing check unavailable: ${e.message}`));
Expand Down Expand Up @@ -479,6 +538,28 @@ export async function collect({ pkgRoot, cwd = process.cwd() }) {
'statusline is claude-only — codex has no command-backed statusline; its guidance ships via AGENTS.md'));
}

// qe-court (ADR-124): TEMPORARY, remove once fixed upstream. agentic-qe's own
// shipped default config.json violates its own writerIsNeverJuror invariant
// (proffesor-for-testing/agentic-qe#576) — a brand-new project fails
// validation before any user touches the file. No-op unless aqe is new
// enough AND the skill has already created its config.json (ak never
// creates it) — same gate as `ak x provider status`'s read-only awareness.
if (qeCourtShipped()) {
const qcRoot = paths.repoRoot(cwd);
const qc = qcRoot ? readQeCourtConfig(qcRoot) : null;
if (qc) {
const violations = validatePanel(panelFromRouting(qc.routing), { minVendors: qc.options?.minDistinctVendors ?? 2 });
if (violations.length) {
const fix = healJuryVendorCollision(qc.routing);
rows.push(row('qe-court', 'warn',
`qe-court panel invalid: ${violations.join(', ')}`,
fix ? `sync reassigns jury ${fix.from} → ${fix.to} (temporary until upstream fix lands: ${UPSTREAM_JURY_VENDOR_ISSUE})` : null));
} else {
rows.push(row('qe-court', 'ok', 'qe-court panel valid (vendor-diverse, jury independent of writer)'));
}
}
}

return rows;
}

Expand Down
19 changes: 18 additions & 1 deletion src/commands/sync.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { listDaemons, staleDaemons, reap } from '../lib/daemons.mjs';
import { loadKitConfig, saveKitConfig } from '../lib/config.mjs';
import { HOSTS, applyHosts, applyProviders, hostInstallState, installHost, applyAqeRouter, seedDualRoutingIfDualHost, ensureCodexMcp, ensureRufloMcpInCodex, bothHostsEnabled } from '../lib/providers.mjs';
import { driftReport, selfDrift } from '../lib/versions.mjs';
import { RUVECTOR_PKG, managed as ruvectorManaged } from '../lib/ruvector.mjs';
import { pruneNpxStale } from '../lib/npx.mjs';
import { nativesStatus, securityPresent } from '../lib/natives.mjs';
import { readJson } from '../lib/settings.mjs';
Expand Down Expand Up @@ -46,7 +47,7 @@ export async function run({ flags, pkgRoot }) {
const cwd = process.cwd();
const rows = await collect({ pkgRoot, cwd });
const plan = rows.filter((r) => r.fix)
.filter((r) => !(flags['no-upgrade'] && (r.subsystem === 'versions' || r.subsystem === 'self' || r.subsystem === 'ruvnet-brain')));
.filter((r) => !(flags['no-upgrade'] && ['versions', 'self', 'ruvnet-brain', 'ruvector'].includes(r.subsystem)));

if (plan.length === 0) { ok('nothing to do — all subsystems healthy'); return 0; }

Expand Down Expand Up @@ -75,6 +76,16 @@ export async function run({ flags, pkgRoot }) {
if (subsystems.has('ruvnet-brain') && !flags['no-upgrade']) {
await step('ruvnet-brain', () => heal.installRuvnetBrain({ force: true }));
}
// ruvector: an unmanaged global users wire up as an MCP server by hand. Only
// ever UPGRADED — status emits no row (and so no plan entry) when it is absent,
// so this branch can never install it for someone who didn't opt in.
// The status row already gates on registration + opt-in (an unregistered or
// opted-out ruvector emits no `fix`, so it cannot reach this plan) — but this
// branch installs software globally, so it re-checks rather than trusting the
// plan to be the only guard.
if (subsystems.has('ruvector') && !flags['no-upgrade'] && ruvectorManaged(cfg)) {
await step('ruvector', () => heal.upgradePackage(RUVECTOR_PKG));
}
// 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`.
Expand Down Expand Up @@ -109,6 +120,12 @@ export async function run({ flags, pkgRoot }) {
if (subsystems.has('aqe')) {
report('rvf', heal.healRvf(paths.projectAqeDir(cwd)));
}
// qe-court: TEMPORARY, remove once fixed upstream (agentic-qe#576) — see
// heal.healQeCourtPanel's doc comment. Only ever fires when status already
// found a fixable violation, so this never touches a valid or unfixable panel.
if (subsystems.has('qe-court')) {
report('qe-court', heal.healQeCourtPanel(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) {
Expand Down
Loading
Loading