Skip to content

Commit f642d03

Browse files
committed
fix: aqe fallback-chain credential checks, seeded-routing divergence detection, ruvector drift management
Closes #54 — ak now checks every aqe fallback rung against an actual credential before writing/reporting it: applyAqeRouter warns (never refuses) on a keyless rung, ak x provider pick warns interactively, ak status reports a WARN providers row when any rung lacks a credential, and openrouter is now visible in ak x provider status's provider table. Closes #55 — seeded per-activity routing pins that diverge from current DEFAULT_ROUTES are now surfaced (info severity, never "stale"/"outdated" per the issue's explicit framing) via divergedRoutes(), with a new `ak x provider refresh` to re-seed per-activity on demand. ak sync never auto-refreshes. MODEL_CATALOG notes now distinguish per-token price from per-task cost. aqeFallback entries carry source provenance. Also adds standalone ruvector (global npm CLI) drift detection/upgrade, opt-in and separate from the nested ruflo/aqe copies already managed indirectly, wired into ak status/sync and the dashboard. 824+ tests added/passing; full suite green.
1 parent b722980 commit f642d03

17 files changed

Lines changed: 1947 additions & 28 deletions

docs/PROVIDERS.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,22 +178,28 @@ Defaults (all overridable; your edits are marked `custom` and never re-seeded):
178178

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

181+
> **Per-token price ≠ per-task cost.** A model that needs more agentic turns costs more
182+
> per task at the same per-token price. On subscription (`claude-code` oauth) billing the
183+
> marginal dollar cost is $0 either way, and the extra turns are paid in wall-clock and
184+
> quota instead — so read every note below on the turns axis, not only the price axis.
185+
181186
| Host | Model | When to use |
182187
|---|---|---|
183-
| claude | `claude-opus-5` | new top Opus — ~2× Opus 4.8 at the same price; premium reasoning default |
184-
| claude | `claude-sonnet-5` | near-Opus at lower cost — review, spec, release |
188+
| 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 |
189+
| claude | `claude-sonnet-5` | near-Opus capability at a lower per-token price — review, spec, release |
185190
| claude | `claude-fable-5` | top capability (Mythos-class, above Opus 5) — hardest problems |
186-
| claude | `claude-haiku-4-5-20251001` | cheap/fast — high-volume mechanical |
187-
| claude | `claude-opus-4-8` | prior Opus generation — same price as opus-5, kept for pinned configs |
191+
| claude | `claude-haiku-4-5-20251001` | cheap/fast — high-volume mechanical work |
192+
| claude | `claude-opus-4-8` | prior Opus generation — same per-token price, roughly half the turns on routine work |
188193
| codex | `gpt-5.4` | coding + reasoning + agentic — recommended execution default |
189194
| codex | `gpt-5.6-sol` | newest line; first-class max reasoning effort |
190195
| codex | `gpt-5.3-codex` | pure coding-tuned — mechanical implementation & docs |
191196
| codex | `gpt-5-codex-mini` | smallest/cheapest — escalation floor, high volume |
192197

193198
> **Where Opus 5 sits** ([announcement](https://www.anthropic.com/news/claude-opus-5), July 2026):
194199
> same $5/$25 per-Mtok pricing as Opus 4.8 with roughly double the Frontier-Bench
195-
> performance, so it strictly supersedes 4.8 as the reasoning-tier default — a capability
196-
> tier above Opus 4.8 at no added cost. It is **not** Mythos-class: `claude-fable-5`
200+
> performance, which is why it is the reasoning-tier default. That parity is **per token**:
201+
> measured end-to-end it takes 2–3.4× the agentic turns on routine work, so per task it is
202+
> the more expensive arm there and 4.8 remains a defensible pin. It is **not** Mythos-class: `claude-fable-5`
197203
> remains the flagship tier. Opus 5 lands within ~0.5% of Fable on coding/agentic
198204
> benchmarks at about half the cost per task, but stays behind the Mythos-class models on
199205
> frontier domains. Rule of thumb: `claude-opus-5` is the premium default;

src/commands/status.mjs

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { loadRing, detectRegression } from '../lib/health-history.mjs';
88
import * as paths from '../lib/paths.mjs';
99
import { nativesStatus, rufloRuntimeNatives, dbPathPinStatus, aidefencePresent, securityPresent } from '../lib/natives.mjs';
1010
import { scanNpxStale } from '../lib/npx.mjs';
11-
import { registrationStatus, codexMcpStatus, rufloCodexMcpStatus } from '../lib/mcp.mjs';
11+
import { registrationStatus, codexMcpStatus, rufloCodexMcpStatus, ruvectorRegistered } from '../lib/mcp.mjs';
1212
import { listDaemons, staleDaemons } from '../lib/daemons.mjs';
1313
import { scanRvf } from '../lib/rvf.mjs';
1414
import { registry, syncBlocks, blocksForTarget, retiredForTarget, guidanceTargets } from '../lib/blocks.mjs';
@@ -19,8 +19,9 @@ import { drift as ruvnetBrainDrift, nightlyAgentPresent as rbNightlyPresent, NIG
1919
import { coherence as adbCoherence } from '../lib/agentdb.mjs';
2020
import { readJson } from '../lib/settings.mjs';
2121
import { have } from '../lib/exec.mjs';
22-
import { HOSTS, settingsTarget, isDefault, managedEnv, MANAGED_ENV_KEYS, hostInstallState, hostAuthState, bothHostsEnabled, aqeRouterFile, aqeSupportsAgentOverrides } from '../lib/providers.mjs';
23-
import { policyToAgentOverrides, routingSummary } from '../lib/routing.mjs';
22+
import { HOSTS, settingsTarget, isDefault, managedEnv, MANAGED_ENV_KEYS, hostInstallState, hostAuthState, bothHostsEnabled, aqeRouterFile, aqeSupportsAgentOverrides, credentialGaps } from '../lib/providers.mjs';
23+
import { policyToAgentOverrides, routingSummary, divergedRoutes } from '../lib/routing.mjs';
24+
import { drift as ruvectorDrift } from '../lib/ruvector.mjs';
2425

2526
export const options = {
2627
json: { type: 'boolean', default: false },
@@ -97,6 +98,35 @@ export async function collect({ pkgRoot, cwd = process.cwd() }) {
9798
}
9899
}
99100

101+
// ruvector — a global CLI users register as an MCP server BY HAND. ak manages
102+
// its drift, never its presence or its registration. Unregistered → no row at
103+
// all (same silence as codex-not-enabled): nudging a tool nobody opted into
104+
// would be management by ambush. Registered but kit.json ruvector:false → an
105+
// info row with NO fix, so sync never plans an upgrade the user turned off.
106+
//
107+
// Wording is deliberately "CLI": the registered command is typically
108+
// `npx -y ruvector mcp start`, so upgrading the global package does not
109+
// necessarily change what the MCP server executes. Claim only what is true.
110+
if (ruvectorRegistered()) {
111+
if (cfg.ruvector === false) {
112+
rows.push(row('ruvector', 'info', 'ruvector MCP registered — CLI updates disabled (kit.json ruvector:false)'));
113+
} else {
114+
try {
115+
const rv = await ruvectorDrift();
116+
if (rv.present && rv.outdated) {
117+
rows.push(row('ruvector', 'warn',
118+
`ruvector CLI ${rv.installed} installed, ${rv.latest} available`, 'sync upgrades the ruvector CLI'));
119+
} else if (rv.present) {
120+
rows.push(row('ruvector', 'ok', `ruvector CLI ${rv.installed}${rv.latest ? ' (latest)' : ''} (MCP registered, user scope)`));
121+
} else {
122+
rows.push(row('ruvector', 'info', 'ruvector MCP registered but no global CLI installed (server runs via npx)'));
123+
}
124+
} catch (e) {
125+
rows.push(row('ruvector', 'warn', `ruvector check unavailable: ${e.message}`));
126+
}
127+
}
128+
}
129+
100130
// self (the kit's own version — prerelease installs track the `next` tag)
101131
try {
102132
const s = await selfDrift({ pkgRoot });
@@ -348,6 +378,20 @@ export async function collect({ pkgRoot, cwd = process.cwd() }) {
348378
} else {
349379
rows.push(row('providers', 'ok', `wired: ${on}${chainStr} (${scope})`));
350380
}
381+
// Chain VIABILITY, separate from chain ORDER above: a chain in the right
382+
// order whose rungs have no credential fails over into nothing (#54). Warn,
383+
// not fail — the primary rung still works — and no `fix`, since only the
384+
// user can supply a key.
385+
if (chain.length) {
386+
const gaps = credentialGaps(chain);
387+
if (gaps.length) {
388+
rows.push(row('providers', 'warn',
389+
`aqe chain: ${chain.length - gaps.length}/${chain.length} rungs have credentials `
390+
+ `(${gaps.map((g) => `${g.provider}: needs ${g.missing.join(', ')}`).join('; ')})`));
391+
} else {
392+
rows.push(row('providers', 'ok', `aqe chain: ${chain.length}/${chain.length} rungs have credentials`));
393+
}
394+
}
351395
}
352396
} catch (e) {
353397
rows.push(row('providers', 'warn', `provider check unavailable: ${e.message}`));
@@ -371,6 +415,20 @@ export async function collect({ pkgRoot, cwd = process.cwd() }) {
371415
if (drift) rows.push(row('routing', 'warn', `${base} — llm-config.json out of sync`, 'sync re-applies agentOverrides'));
372416
else rows.push(row('routing', 'ok', base));
373417
}
418+
// Seeded pins vs today's defaults. Deliberately `info` and deliberately
419+
// "diverges from": which side wins is activity-dependent (a newer default
420+
// can cost 2-3× the agentic turns on routine work), so a `warn` would push
421+
// users to spend turns clearing a lint. No `fix` — sync must never
422+
// auto-refresh a pin; `ak x provider refresh` is the opt-in path (#55).
423+
const diverged = divergedRoutes(policy);
424+
if (diverged.length) {
425+
const pairs = [...new Set(diverged.flatMap((d) => [
426+
...(d.modelDiverged ? [`${d.model} vs ${d.defaultModel}`] : []),
427+
...d.escalate.map((e) => `${e.model} vs ${e.defaultModel} (escalation)`),
428+
]))].join(', ');
429+
rows.push(row('routing', 'info',
430+
`${diverged.length} seeded route(s) diverge from current defaults (${pairs}) — ak x provider refresh`));
431+
}
374432
}
375433
} catch (e) {
376434
rows.push(row('routing', 'warn', `routing check unavailable: ${e.message}`));

src/commands/sync.mjs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { listDaemons, staleDaemons, reap } from '../lib/daemons.mjs';
1111
import { loadKitConfig, saveKitConfig } from '../lib/config.mjs';
1212
import { HOSTS, applyHosts, applyProviders, hostInstallState, installHost, applyAqeRouter, seedDualRoutingIfDualHost, ensureCodexMcp, ensureRufloMcpInCodex, bothHostsEnabled } from '../lib/providers.mjs';
1313
import { driftReport, selfDrift } from '../lib/versions.mjs';
14+
import { RUVECTOR_PKG, managed as ruvectorManaged } from '../lib/ruvector.mjs';
1415
import { pruneNpxStale } from '../lib/npx.mjs';
1516
import { nativesStatus, securityPresent } from '../lib/natives.mjs';
1617
import { readJson } from '../lib/settings.mjs';
@@ -46,7 +47,7 @@ export async function run({ flags, pkgRoot }) {
4647
const cwd = process.cwd();
4748
const rows = await collect({ pkgRoot, cwd });
4849
const plan = rows.filter((r) => r.fix)
49-
.filter((r) => !(flags['no-upgrade'] && (r.subsystem === 'versions' || r.subsystem === 'self' || r.subsystem === 'ruvnet-brain')));
50+
.filter((r) => !(flags['no-upgrade'] && ['versions', 'self', 'ruvnet-brain', 'ruvector'].includes(r.subsystem)));
5051

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

@@ -75,6 +76,16 @@ export async function run({ flags, pkgRoot }) {
7576
if (subsystems.has('ruvnet-brain') && !flags['no-upgrade']) {
7677
await step('ruvnet-brain', () => heal.installRuvnetBrain({ force: true }));
7778
}
79+
// ruvector: an unmanaged global users wire up as an MCP server by hand. Only
80+
// ever UPGRADED — status emits no row (and so no plan entry) when it is absent,
81+
// so this branch can never install it for someone who didn't opt in.
82+
// The status row already gates on registration + opt-in (an unregistered or
83+
// opted-out ruvector emits no `fix`, so it cannot reach this plan) — but this
84+
// branch installs software globally, so it re-checks rather than trusting the
85+
// plan to be the only guard.
86+
if (subsystems.has('ruvector') && !flags['no-upgrade'] && ruvectorManaged(cfg)) {
87+
await step('ruvector', () => heal.upgradePackage(RUVECTOR_PKG));
88+
}
7889
// The brain installer's own nightly self-updater (macOS LaunchAgent) bypasses
7990
// ak-managed updates — disabling it is a heal, not an upgrade, so it runs even
8091
// under --no-upgrade. Reversible: `npx ruvnet-brain --enable-nightly`.

0 commit comments

Comments
 (0)