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
7 changes: 7 additions & 0 deletions docs/PROVIDERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,13 @@ not written to AQE `agentOverrides`, cannot become `primaryHost`, and do not cou
AQE vendor. `ak dual` is deprecated and rejects OpenCode routes because it retains a Claude/Codex
compatibility adapter; use `ak run` instead.

**QE-Court validation stays upstream-owned.** `agentic-qe` 3.13.3 corrected its shipped
QE-Court panel and now enforces the configured anti-collusion policy before convening.
`ak status` and `ak host status` surface that result read-only; `ak sync` never rewrites
`.claude/skills/qe-court/config.json`. If a config created by 3.13.2 or earlier still
seats both `defense` and `jury` on Cognitum tiers, regenerate it with 3.13.3+ or change
`defense` to `claude-code` so the jury and defense use distinct vendors.

Defaults (all overridable; your edits are marked `custom` and never re-seeded):

| Activity | Host | Default model |
Expand Down
11 changes: 11 additions & 0 deletions docs/UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ for execution-host lifecycle and selection (`ak x host` only when you specifical
plumbing spelling). This namespace correction does not rename inference providers or provider
bindings into hosts.

## QE-Court configs created before agentic-qe 3.13.3

`agentic-qe` 3.13.3 fixed its shipped QE-Court default and made configuration validation
mandatory before a court convenes. New configs seat `defense` on `claude-code`, `jury` on
`cognitum-high`, and `deeperReviewer` on `codex`, preserving three distinct vendors.

An existing `.claude/skills/qe-court/config.json` is project-owned and is not overwritten by
an agentic-qe or `ak` upgrade. If `ak status` reports `writerIsNeverJuror`, regenerate the
config with agentic-qe 3.13.3+ or change `routing.defense.provider` from `cognitum-low` to
`claude-code`. `ak` reports this state read-only; `ak sync` no longer changes QE-Court roles.

If you already have `ak` working, you almost never need `ak setup` again — it's the
installer. Enabling a shipped-but-opt-in host feature is a `host pick` (or an `x mcp pick`,
etc.), not a re-`setup`.
Expand Down
17 changes: 6 additions & 11 deletions src/commands/status.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ 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, credentialGaps, collectIntegrationFacts } 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 { qeCourtShipped, readQeCourtConfig, validateCourtConfig } from '../lib/qeCourt.mjs';
import { drift as ruvectorDrift } from '../lib/ruvector.mjs';
import { statuslineDrift } from '../lib/codex-statusline.mjs';

Expand Down Expand Up @@ -628,22 +628,17 @@ export async function collect({ pkgRoot, cwd = process.cwd() }) {
'opencode has no statusline surface; its ruflo lifecycle ships via the plugins/ bridge + 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.
// qe-court (ADR-124): read-only awareness. agentic-qe >=3.13.3 owns config
// validation and ships a valid default; ak reports existing project state
// but never rewrites the skill's config.
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 });
const violations = validateCourtConfig(qc);
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));
`qe-court panel invalid: ${violations.join(', ')} — regenerate with agentic-qe >=3.13.3 or choose different defense/jury vendors`));
} else {
rows.push(row('qe-court', 'ok', 'qe-court panel valid (vendor-diverse, jury independent of writer)'));
}
Expand Down
6 changes: 0 additions & 6 deletions src/commands/sync.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,6 @@ 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
7 changes: 3 additions & 4 deletions src/commands/x/provider.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { have } from '../../lib/exec.mjs';
import { ok, warn, fail, info, dim, bold, yellow } from '../../lib/output.mjs';
import { repoRoot } from '../../lib/paths.mjs';
import { writeJsonWithBackup } from '../../lib/settings.mjs';
import { panelFromRouting, validatePanel, readQeCourtConfig, qeCourtConfigPath, vendorOf, qeCourtShipped } from '../../lib/qeCourt.mjs';
import { panelFromRouting, validateCourtConfig, readQeCourtConfig, qeCourtConfigPath, vendorOf, qeCourtShipped } from '../../lib/qeCourt.mjs';

/** Print the dual-host guidance tips (role delegation, judge-bias, qe-court
* cross-sell) once both hosts are enabled — shared by `pick()` and
Expand Down Expand Up @@ -238,8 +238,7 @@ function printQeCourtStatus(cwd) {
const qc = readQeCourtConfig(root);
if (!qc) return;
const panel = panelFromRouting(qc.routing);
const minVendors = qc.options?.minDistinctVendors ?? 2;
const violations = validatePanel(panel, { minVendors });
const violations = validateCourtConfig(qc);
console.log(bold('\nqe-court routing') + dim(' (.claude/skills/qe-court/config.json)'));
for (const { role, provider } of panel) {
console.log(` ${role.padEnd(28)} ${provider ?? dim('(unset)')}`);
Expand Down Expand Up @@ -369,7 +368,7 @@ async function maybeWriteQeCourtDefaults({ nonInteractive, cwd, enabled, aqeProv
if (ans !== 'y' && ans !== 'yes') { info('qe-court routing left unchanged'); return; }

for (const [role, provider] of changes) routing[role] = { ...(routing[role] ?? {}), provider };
const violations = validatePanel(panelFromRouting(routing), { minVendors: qc.options?.minDistinctVendors ?? 2 });
const violations = validateCourtConfig({ ...qc, routing });
if (violations.length) { warn(`qe-court routing defaults would be invalid (${violations.join(', ')}) — not written`); return; }

writeJsonWithBackup(qeCourtConfigPath(root), { ...qc, routing });
Expand Down
24 changes: 1 addition & 23 deletions src/lib/heal.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@
import fs from 'node:fs';
import path from 'node:path';
import { run } from './exec.mjs';
import { rufloRoot, aqeRoot, repoRoot } from './paths.mjs';
import { rufloRoot, aqeRoot } from './paths.mjs';
import { agentdbLocations, bsq3IsNative, bsq3Root, deriveBsq3Spec, selfSpecConflicts, rufloMemoryContexts, aidefencePresent } from './natives.mjs';
import { KIT_PKG } from './versions.mjs';
import { scanRvf, quarantine } from './rvf.mjs';
import { INSTALL_SPEC, INSTALL_ARGS, NIGHTLY_LABEL as RB_NIGHTLY_LABEL, nightlyAgentPlist as rbNightlyPlist, present as rbPresent, latestVersion as rbLatest, recordInstalledRelease as rbRecord } from './ruvnet-brain.mjs';
import { PKG as ADB_PKG, present as adbPresent, coherence as adbCoherence } from './agentdb.mjs';
import { readQeCourtConfig, qeCourtConfigPath, healJuryVendorCollision, UPSTREAM_JURY_VENDOR_ISSUE } from './qeCourt.mjs';
import { writeJsonWithBackup } from './settings.mjs';

// Packages whose install scripts must run for natives to build (npm >=11.17
// blocks them by default). Curated on the live 3.28/3.12.2 upgrade.
Expand Down Expand Up @@ -151,26 +149,6 @@ export function healRvf(projectAqeDir) {
return { ok: true, detail: removed.length ? `quarantined: ${removed.join(', ')}` : 'healthy' };
}

/** TEMPORARY (remove once fixed upstream): agentic-qe's own shipped default
* qe-court config.json violates its own writerIsNeverJuror invariant — see
* UPSTREAM_JURY_VENDOR_ISSUE in qeCourt.mjs. Auto-corrects ONLY the `jury`
* role, ONLY when the current config actually has the vendor collision, by
* reassigning it to an already-configured distinct-vendor provider. Never
* invents a vendor the project hasn't configured, never touches any other
* key. No-op (ok:true) when there's no project root or no config yet — ak
* never creates this file. */
export function healQeCourtPanel(cwd = process.cwd()) {
const root = repoRoot(cwd);
if (!root) return { ok: true, detail: 'no project root — skipped' };
const qc = readQeCourtConfig(root);
if (!qc) return { ok: true, detail: 'no qe-court config present — skipped' };
const fix = healJuryVendorCollision(qc.routing);
if (!fix) return { ok: true, detail: 'qe-court panel already valid (or unfixable automatically)' };
const routing = { ...qc.routing, jury: { ...qc.routing.jury, provider: fix.to } };
writeJsonWithBackup(qeCourtConfigPath(root), { ...qc, routing });
return { ok: true, detail: `jury ${fix.from} → ${fix.to} (temporary until upstream fix lands: ${UPSTREAM_JURY_VENDOR_ISSUE})` };
}

/** Upgrade a global package to latest (with allow-scripts). */
export async function upgradePackage(pkg) {
const r = await run('npm', ['install', '-g', `--allow-scripts=${ALLOW_SCRIPTS}`, `${pkg}@latest`],
Expand Down
2 changes: 1 addition & 1 deletion src/lib/providers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ export const JUDGE_BIAS_TIP = 'tip: for LLM-judged scoring, use a different vend
* requires >= 2 distinct vendors seated, which a dual-host setup already
* satisfies. Only meaningful once both hosts are enabled AND aqe is new
* enough to ship the skill — callers gate on both. */
export const QE_COURT_TIP = 'agentic-qe ≥ 3.13.0 ships qe-court (adversarial review) — its jury requires ≥ 2 distinct vendors, which your dual-host setup already satisfies';
export const QE_COURT_TIP = 'agentic-qe ships qe-court (adversarial review; upgrade to ≥ 3.13.3 for enforced config validation) — its jury requires ≥ 2 distinct vendors, which your dual-host setup already satisfies';

/** Suggested aqe-fallback chain when codex is among the enabled hosts: codex's
* models are reached via the `openai` provider type (not as an aqe provider
Expand Down
43 changes: 11 additions & 32 deletions src/lib/qeCourt.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// defaulting of its per-role provider routing, a third configuration surface
// alongside ruflo's host env and aqe's global fallback chain (see issue #36).
//
// vendorOf/validatePanel are ported 1:1 from qe-court's own referee.js (the
// The pure validation helpers mirror agentic-qe >=3.13.3's referee.ts (the
// falsifiable, dependency-free core of the court's invariants) so ak can
// report pass/fail without shelling out to aqe. Do not reimplement the court
// protocol itself here — this module only reads/defaults the `routing` block
Expand Down Expand Up @@ -35,54 +35,33 @@ export function vendorOf(providerId) {
* {role, provider} panel shape validatePanel() expects. */
export function panelFromRouting(routing) {
return Object.entries(routing ?? {})
.filter(([role]) => role !== '_note')
.filter(([role]) => !role.startsWith('_'))
.filter(([, seat]) => typeof seat?.provider === 'string' && seat.provider.length > 0)
.map(([role, entry]) => ({ role, provider: entry?.provider }));
}

/** Validate a seated panel against the court's anti-collusion invariants.
* Returns a list of violation codes (empty == valid) — ported from
* qe-court's referee.js validatePanel(). */
* qe-court's referee.ts validatePanel(). */
export function validatePanel(panel, policy = {}) {
const minVendors = policy.minVendors ?? 2;
const minVendors = policy.minVendors ?? policy.minDistinctVendors ?? 2;
const violations = [];
const vendorsSeated = new Set(panel.map((s) => vendorOf(s.provider)));
if (vendorsSeated.size < minVendors) violations.push('vendor-diversity');
const jury = panel.find((s) => s.role === 'jury');
const writerLike = panel.filter((s) => s.role === 'defense' || s.role === 'writer');
if (jury) {
if (!jury) {
violations.push('missing-jury');
} else if (policy.writerIsNeverJuror !== false) {
const juryVendor = vendorOf(jury.provider);
if (writerLike.some((w) => vendorOf(w.provider) === juryVendor)) violations.push('writerIsNeverJuror');
}
return violations;
}

// TEMPORARY (remove once fixed upstream): agentic-qe's own shipped default
// config.json violates its own writerIsNeverJuror invariant — defense:
// cognitum-low and jury: cognitum-high resolve to the same vendor per
// vendorOf() above, so a brand-new project fails validation before any user
// touches the file. Filed: proffesor-for-testing/agentic-qe#576.
export const UPSTREAM_JURY_VENDOR_ISSUE = 'https://github.com/proffesor-for-testing/agentic-qe/issues/576';

/** Compute a minimal fix for a writerIsNeverJuror violation: reassign `jury`
* to an already-configured provider whose vendor differs from every
* writer/defense vendor. Prefers `deeperReviewer`'s provider (already a
* second-look role) before falling back to the first other distinct-vendor
* seat. Returns null when the panel doesn't have the collision, or when no
* distinct-vendor seat exists to borrow from — this never invents a vendor
* the project hasn't already configured. Pure; touches nothing. */
export function healJuryVendorCollision(routing) {
const panel = panelFromRouting(routing);
const writerVendors = new Set(
panel.filter((s) => s.role === 'defense' || s.role === 'writer').map((s) => vendorOf(s.provider)),
);
const jury = panel.find((s) => s.role === 'jury');
if (!jury || !writerVendors.has(vendorOf(jury.provider))) return null;

const candidates = panel.filter((s) => s.role !== 'jury' && !writerVendors.has(vendorOf(s.provider)));
const preferred = candidates.find((s) => s.role === 'deeperReviewer') ?? candidates[0];
if (!preferred) return null;

return { role: 'jury', from: jury.provider, to: preferred.provider };
/** Validate a whole qe-court config using the policy declared in its options. */
export function validateCourtConfig(config) {
return validatePanel(panelFromRouting(config?.routing ?? {}), config?.options ?? {});
}

export function qeCourtConfigPath(root) {
Expand Down
106 changes: 0 additions & 106 deletions tests/kit/heal-qe-court.test.mjs

This file was deleted.

Loading
Loading