@aibridge/cli 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,7 +39,7 @@ npx -y @aibridge/cli subagent --model xai-grok/grok-4.6 "summarize the architect
39
39
  |---|---|
40
40
  | `aibridge plan --model xai-grok/grok-4.6 --out plan.md "<task>"` | You want a delegate model to study the repo and expand a task into a detailed, reviewable **plan file** before any code is written |
41
41
  | `aibridge implement --model google-antigravity/gemini-3.7-flash <plan.md>` | You have an approved plan file and want it executed in place — with your project's **real typecheck and tests** run until green |
42
- | `aibridge review --model xai-grok/grok-4.6 --out review.md [--plan <plan.md>]` | You want a **different model** to pressure-test the working-tree diff against the plan contract (over-reach is a finding) or to review the plan itself before implementing |
42
+ | `aibridge review --model xai-grok/grok-4.6 --out review.md [--plan <plan.md>]` | You want a **different model** to pressure-test a diff against the plan contract, where over-reach is a finding. `--base <ref>` reviews any commit range, not just the working tree. On a clean tree it reviews the plan itself |
43
43
  | `aibridge subagent --model xai-grok/grok-4.6 "<task>"` | A self-contained task deserves a concurrent delegate, a cross-model second opinion, or a red-team pass |
44
44
  | `aibridge image-gen --model openai-codex/gpt-5.6-sol --out out.png "<prompt>"` | You need a real raster image — on a Codex, Antigravity, or Grok seat, with render verification |
45
45
  | `aibridge models [--json]` | You need the exact facts for every registered model seat (accepted efforts, image format, pinned model ID) |
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { r as runCli, t as buildContext } from "./context-DJtEcg6f.mjs";
2
+ import { r as runCli, t as buildContext } from "./context-BQgclUqy.mjs";
3
3
  //#region src/cli.ts
4
4
  await runCli(buildContext(process), process.argv.slice(2));
5
5
  //#endregion
@@ -312,6 +312,123 @@ function getDriver(backend) {
312
312
  return driver;
313
313
  }
314
314
  //#endregion
315
+ //#region src/quotaPreflight.ts
316
+ function evaluateAgyPreflight(snapshot, backendModel) {
317
+ const quota = findModelQuota(snapshot, backendModel);
318
+ if (!quota) return {
319
+ ok: true,
320
+ warning: `model "${backendModel}" not in quota snapshot; proceeding`
321
+ };
322
+ if (quota.exhausted) {
323
+ let resetAt = quota.resetTime;
324
+ if (!resetAt) {
325
+ for (const group of snapshot.groups) if (group.displayName.includes("Gemini")) {
326
+ for (const bucket of group.buckets) if (bucket.resetTime) {
327
+ if (!resetAt || new Date(bucket.resetTime).getTime() < new Date(resetAt).getTime()) resetAt = bucket.resetTime;
328
+ }
329
+ }
330
+ }
331
+ return {
332
+ ok: false,
333
+ kind: "quota",
334
+ message: `agy model "${backendModel}" is quota-exhausted`,
335
+ resetAt
336
+ };
337
+ }
338
+ return { ok: true };
339
+ }
340
+ function evaluateCodexPreflight(snapshot) {
341
+ if (snapshot.limitReached) return {
342
+ ok: false,
343
+ kind: "quota",
344
+ message: "codex quota limit reached",
345
+ resetAt: snapshot.windows.find((w) => w.resetAt)?.resetAt
346
+ };
347
+ const exhaustedWindow = snapshot.windows.find((w) => w.usedPercent >= 100);
348
+ if (exhaustedWindow) return {
349
+ ok: false,
350
+ kind: "quota",
351
+ message: "codex quota limit reached",
352
+ resetAt: exhaustedWindow.resetAt
353
+ };
354
+ return { ok: true };
355
+ }
356
+ function evaluateGrokPreflight(snapshot) {
357
+ if (snapshot.usedPercent !== void 0 && snapshot.usedPercent >= 100) return {
358
+ ok: false,
359
+ kind: "quota",
360
+ message: "grok credit quota exhausted",
361
+ resetAt: snapshot.periodEnd
362
+ };
363
+ return { ok: true };
364
+ }
365
+ async function preflightModel(resolved) {
366
+ if (resolved.spec.backend === "codex") return preflightCodex();
367
+ if (resolved.spec.backend === "grok") return preflightGrok();
368
+ if (resolved.spec.backend !== "agy") return { ok: true };
369
+ try {
370
+ return evaluateAgyPreflight(await fetchAgyQuota(), backendModelId(resolved));
371
+ } catch (err) {
372
+ if (isAuthExpired(err)) return {
373
+ ok: false,
374
+ kind: "auth",
375
+ message: err.message,
376
+ resetAt: void 0
377
+ };
378
+ return {
379
+ ok: true,
380
+ warning: `quota preflight failed (${err.message}); proceeding`
381
+ };
382
+ }
383
+ }
384
+ async function preflightCodex() {
385
+ try {
386
+ return evaluateCodexPreflight(await fetchCodexQuota());
387
+ } catch (err) {
388
+ if (isAuthExpired(err)) return {
389
+ ok: false,
390
+ kind: "auth",
391
+ message: err.message,
392
+ resetAt: void 0
393
+ };
394
+ return {
395
+ ok: true,
396
+ warning: `quota preflight failed (${err.message}); proceeding`
397
+ };
398
+ }
399
+ }
400
+ async function preflightGrok() {
401
+ try {
402
+ return evaluateGrokPreflight(await fetchGrokQuota());
403
+ } catch (err) {
404
+ if (isAuthExpired(err)) return {
405
+ ok: false,
406
+ kind: "auth",
407
+ message: err.message,
408
+ resetAt: void 0
409
+ };
410
+ return {
411
+ ok: true,
412
+ warning: `quota preflight failed (${err.message}); proceeding`
413
+ };
414
+ }
415
+ }
416
+ function formatReset$1(resetTime) {
417
+ if (!resetTime) return "-";
418
+ const ms = new Date(resetTime).getTime() - Date.now();
419
+ if (Number.isNaN(ms)) return resetTime;
420
+ if (ms <= 0) return "now";
421
+ const mins = Math.round(ms / 6e4);
422
+ const rel = mins < 60 ? `${mins}m` : `${Math.floor(mins / 60)}h${mins % 60}m`;
423
+ return `${new Date(resetTime).toLocaleTimeString()} (in ${rel})`;
424
+ }
425
+ function renderPreflightRefusal(cmd, verdict) {
426
+ if (verdict.kind === "auth") return `aibridge ${cmd}: refusing — ${verdict.message}. Running with --no-preflight would only fail unauthenticated later. Or use a different --model.`;
427
+ const resetClause = verdict.resetAt ? ` Resets ${formatReset$1(verdict.resetAt)}.` : "";
428
+ const fallback = cmd === "image-gen" ? "Use --no-preflight to override, or another image seat (--model openai-codex/gpt-5.6-sol | google-antigravity/gemini-3.7-flash | xai-grok/grok-4.6)." : "Use --no-preflight to override, or a claude-backend fallback (subagent --model sonnet|opus — bills the Claude subscription).";
429
+ return `aibridge ${cmd}: refusing — ${verdict.message}.${resetClause} ${fallback}`;
430
+ }
431
+ //#endregion
315
432
  //#region src/transparency.ts
316
433
  const CHROMA_CLAUSE = "The entire background must be a perfectly flat solid #00ff00 chroma-key green. The background must be one uniform colour with no shadows, gradients, texture, reflections, or lighting variation. Keep the subject fully separated from the background with crisp edges. Do not use #00ff00 or any similar green anywhere on the subject. No cast shadow, no contact shadow, no reflection.";
317
434
  const NATIVE_ALPHA_CLAUSE = "Render the subject on a fully transparent background — PNG with a real alpha channel, no backdrop, no canvas colour, no cast shadow.";
@@ -419,6 +536,15 @@ async function imageGen$1(flags, prompt) {
419
536
  }
420
537
  const driver = getDriver(model.spec.backend);
421
538
  if (!driver.generateImage) return fail(formatImageGenModelError(inputSlug, model));
539
+ if (flags.preflight) {
540
+ const verdict = await preflightModel(model);
541
+ if (!verdict.ok) {
542
+ this.process.stderr.write(`${renderPreflightRefusal("image-gen", verdict)}\n`);
543
+ this.process.exitCode = 3;
544
+ return;
545
+ }
546
+ if (verdict.warning) this.process.stderr.write(`aibridge image-gen: ${verdict.warning}\n`);
547
+ }
422
548
  const minBytes = model.spec.backend === "codex" ? MIN_REAL_BYTES_CODEX : MIN_REAL_BYTES_TOOL;
423
549
  const work = mkdtempSync(join(tmpdir(), "aibridge-imagegen-"));
424
550
  const effectivePrompt = flags.transparent ? `${prompt} ${alpha === "chroma" ? CHROMA_CLAUSE : NATIVE_ALPHA_CLAUSE}` : prompt;
@@ -585,6 +711,11 @@ const imageGen = buildCommand({
585
711
  optional: true,
586
712
  brief: "Max seconds to wait for the render (default: 600)"
587
713
  },
714
+ preflight: {
715
+ kind: "boolean",
716
+ default: true,
717
+ brief: "Check model quota before rendering (use --no-preflight to skip)"
718
+ },
588
719
  json: {
589
720
  kind: "boolean",
590
721
  withNegated: false,
@@ -626,122 +757,6 @@ async function delegate(opts, driver = getDriver(opts.model.spec.backend)) {
626
757
  return result;
627
758
  }
628
759
  //#endregion
629
- //#region src/quotaPreflight.ts
630
- function evaluateAgyPreflight(snapshot, backendModel) {
631
- const quota = findModelQuota(snapshot, backendModel);
632
- if (!quota) return {
633
- ok: true,
634
- warning: `model "${backendModel}" not in quota snapshot; proceeding`
635
- };
636
- if (quota.exhausted) {
637
- let resetAt = quota.resetTime;
638
- if (!resetAt) {
639
- for (const group of snapshot.groups) if (group.displayName.includes("Gemini")) {
640
- for (const bucket of group.buckets) if (bucket.resetTime) {
641
- if (!resetAt || new Date(bucket.resetTime).getTime() < new Date(resetAt).getTime()) resetAt = bucket.resetTime;
642
- }
643
- }
644
- }
645
- return {
646
- ok: false,
647
- kind: "quota",
648
- message: `agy model "${backendModel}" is quota-exhausted`,
649
- resetAt
650
- };
651
- }
652
- return { ok: true };
653
- }
654
- function evaluateCodexPreflight(snapshot) {
655
- if (snapshot.limitReached) return {
656
- ok: false,
657
- kind: "quota",
658
- message: "codex quota limit reached",
659
- resetAt: snapshot.windows.find((w) => w.resetAt)?.resetAt
660
- };
661
- const exhaustedWindow = snapshot.windows.find((w) => w.usedPercent >= 100);
662
- if (exhaustedWindow) return {
663
- ok: false,
664
- kind: "quota",
665
- message: "codex quota limit reached",
666
- resetAt: exhaustedWindow.resetAt
667
- };
668
- return { ok: true };
669
- }
670
- function evaluateGrokPreflight(snapshot) {
671
- if (snapshot.usedPercent !== void 0 && snapshot.usedPercent >= 100) return {
672
- ok: false,
673
- kind: "quota",
674
- message: "grok credit quota exhausted",
675
- resetAt: snapshot.periodEnd
676
- };
677
- return { ok: true };
678
- }
679
- async function preflightModel(resolved) {
680
- if (resolved.spec.backend === "codex") return preflightCodex();
681
- if (resolved.spec.backend === "grok") return preflightGrok();
682
- if (resolved.spec.backend !== "agy") return { ok: true };
683
- try {
684
- return evaluateAgyPreflight(await fetchAgyQuota(), backendModelId(resolved));
685
- } catch (err) {
686
- if (isAuthExpired(err)) return {
687
- ok: false,
688
- kind: "auth",
689
- message: err.message,
690
- resetAt: void 0
691
- };
692
- return {
693
- ok: true,
694
- warning: `quota preflight failed (${err.message}); proceeding`
695
- };
696
- }
697
- }
698
- async function preflightCodex() {
699
- try {
700
- return evaluateCodexPreflight(await fetchCodexQuota());
701
- } catch (err) {
702
- if (isAuthExpired(err)) return {
703
- ok: false,
704
- kind: "auth",
705
- message: err.message,
706
- resetAt: void 0
707
- };
708
- return {
709
- ok: true,
710
- warning: `quota preflight failed (${err.message}); proceeding`
711
- };
712
- }
713
- }
714
- async function preflightGrok() {
715
- try {
716
- return evaluateGrokPreflight(await fetchGrokQuota());
717
- } catch (err) {
718
- if (isAuthExpired(err)) return {
719
- ok: false,
720
- kind: "auth",
721
- message: err.message,
722
- resetAt: void 0
723
- };
724
- return {
725
- ok: true,
726
- warning: `quota preflight failed (${err.message}); proceeding`
727
- };
728
- }
729
- }
730
- function formatReset$1(resetTime) {
731
- if (!resetTime) return "-";
732
- const ms = new Date(resetTime).getTime() - Date.now();
733
- if (Number.isNaN(ms)) return resetTime;
734
- if (ms <= 0) return "now";
735
- const mins = Math.round(ms / 6e4);
736
- const rel = mins < 60 ? `${mins}m` : `${Math.floor(mins / 60)}h${mins % 60}m`;
737
- return `${new Date(resetTime).toLocaleTimeString()} (in ${rel})`;
738
- }
739
- function renderPreflightRefusal(cmd, verdict) {
740
- if (verdict.kind === "auth") return `aibridge ${cmd}: refusing — ${verdict.message}. Running with --no-preflight would only send the delegate in unauthenticated. Or use a different --model.`;
741
- const resetClause = verdict.resetAt ? ` Resets ${formatReset$1(verdict.resetAt)}.` : "";
742
- return `aibridge ${cmd}: refusing — ${verdict.message}.${resetClause} Use --no-preflight to override, or a claude-backend fallback (subagent --model sonnet|opus — bills the Claude subscription).`;
743
- }
744
- //#endregion
745
760
  //#region src/runlog.ts
746
761
  function getTimestamp() {
747
762
  const d = /* @__PURE__ */ new Date();
@@ -1385,7 +1400,7 @@ async function review$1(flags) {
1385
1400
  const run = startRun("review", `${model.spec.slug}: ${modeDetail}`);
1386
1401
  const absOutPath = isAbsolute(flags.out) ? flags.out : resolve(cwd, flags.out);
1387
1402
  let reviewPrompt;
1388
- if (isDirty) reviewPrompt = `You are an expert code reviewer. Inspect the working tree diff against base '${baseRef}' and untracked files at ${cwd}.\n` + (absPlanPath ? `Compare the implementation against the plan contract at ${absPlanPath}. Any file modified or feature added outside the plan contract counts as over-reach (severity: major unless harmful, then critical).\n` : "") + `Write your detailed review report to the file ${absOutPath}. For each finding, include file:line, severity (critical|major|minor), and rationale.\nYour final answer (last message) must consist of EXACTLY ONE VERDICT LINE:\nEither: "PASS"\nOr: "FINDINGS: <c> critical, <m> major, <n> minor"`;
1403
+ if (isDirty) reviewPrompt = `You are an expert code reviewer. Inspect the diff produced by \`git diff ${baseRef}\` (this covers committed and uncommitted changes) plus any untracked files at ${cwd}.\n` + (absPlanPath ? `Compare the implementation against the plan contract at ${absPlanPath}. Any file modified or feature added outside the plan contract counts as over-reach (severity: major unless harmful, then critical).\n` : "") + `Write your detailed review report to the file ${absOutPath}. For each finding, include file:line, severity (critical|major|minor), and rationale.\nYour final answer (last message) must consist of EXACTLY ONE VERDICT LINE:\nEither: "PASS"\nOr: "FINDINGS: <c> critical, <m> major, <n> minor"`;
1389
1404
  else reviewPrompt = `You are an expert architecture reviewer. Inspect the plan contract file at ${absPlanPath}.\nReview the plan for soundness, missing edge cases, safety, and feasibility.\nWrite your detailed review report to the file ${absOutPath}. For each finding, include severity (critical|major|minor) and rationale.\nYour final answer (last message) must consist of EXACTLY ONE VERDICT LINE:\nEither: "PASS"\nOr: "FINDINGS: <c> critical, <m> major, <n> minor"`;
1390
1405
  const outcome = await delegate({
1391
1406
  model,
@@ -1447,7 +1462,7 @@ const review = buildCommand({
1447
1462
  kind: "parsed",
1448
1463
  parse: String,
1449
1464
  optional: true,
1450
- brief: "Base git ref to diff against (default: HEAD)"
1465
+ brief: "Base git ref or range to diff against, e.g. HEAD~3 or main (default: HEAD)"
1451
1466
  },
1452
1467
  out: {
1453
1468
  kind: "parsed",
@@ -1467,7 +1482,7 @@ const review = buildCommand({
1467
1482
  }
1468
1483
  } },
1469
1484
  docs: {
1470
- brief: "Review working tree diff or plan contract",
1485
+ brief: "Review a diff (working tree, or any commit range via --base) or a plan contract",
1471
1486
  fullDescription: fullDescription$2
1472
1487
  }
1473
1488
  });
@@ -1793,4 +1808,4 @@ function buildContext(process) {
1793
1808
  return { process };
1794
1809
  }
1795
1810
  //#endregion
1796
- export { supportsImageGen as S, backendModelId as _, readRunLogs as a, listModelHelpLines as b, evaluateCodexPreflight as c, renderPreflightRefusal as d, delegate as f, MODELS as g, positiveIntSeconds as h, listRuns as i, preflightCodex as l, nonEmptyPrompt as m, app as n, startRun as o, getDriver as p, runCli as r, evaluateAgyPreflight as s, buildContext as t, preflightModel as u, formatImageGenModelError as v, resolveModel as x, formatUnknownModelError as y };
1811
+ export { supportsImageGen as S, backendModelId as _, readRunLogs as a, listModelHelpLines as b, evaluateAgyPreflight as c, preflightModel as d, renderPreflightRefusal as f, MODELS as g, positiveIntSeconds as h, listRuns as i, evaluateCodexPreflight as l, nonEmptyPrompt as m, app as n, startRun as o, getDriver as p, runCli as r, delegate as s, buildContext as t, preflightCodex as u, formatImageGenModelError as v, resolveModel as x, formatUnknownModelError as y };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { S as supportsImageGen, _ as backendModelId, a as readRunLogs, b as listModelHelpLines, c as evaluateCodexPreflight, d as renderPreflightRefusal, f as delegate, g as MODELS, h as positiveIntSeconds, i as listRuns, l as preflightCodex, m as nonEmptyPrompt, n as app, o as startRun, p as getDriver, r as runCli, s as evaluateAgyPreflight, t as buildContext, u as preflightModel, v as formatImageGenModelError, x as resolveModel, y as formatUnknownModelError } from "./context-DJtEcg6f.mjs";
1
+ import { S as supportsImageGen, _ as backendModelId, a as readRunLogs, b as listModelHelpLines, c as evaluateAgyPreflight, d as preflightModel, f as renderPreflightRefusal, g as MODELS, h as positiveIntSeconds, i as listRuns, l as evaluateCodexPreflight, m as nonEmptyPrompt, n as app, o as startRun, p as getDriver, r as runCli, s as delegate, t as buildContext, u as preflightCodex, v as formatImageGenModelError, x as resolveModel, y as formatUnknownModelError } from "./context-BQgclUqy.mjs";
2
2
  export { MODELS, app, backendModelId, buildContext, delegate, evaluateAgyPreflight, evaluateCodexPreflight, formatImageGenModelError, formatUnknownModelError, getDriver, listModelHelpLines, listRuns, nonEmptyPrompt, positiveIntSeconds, preflightCodex, preflightModel, readRunLogs, renderPreflightRefusal, resolveModel, runCli, startRun, supportsImageGen };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aibridge/cli",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "CLI that bridges tasks to AI CLIs on your machine (plan / implement / review / subagent / image-gen)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,11 +36,11 @@
36
36
  "dependencies": {
37
37
  "@stricli/core": "1.3.0",
38
38
  "sharp": "^0.35.3",
39
- "@aibridge/driver-agy": "0.7.0",
40
- "@aibridge/proc": "0.7.0",
41
- "@aibridge/driver-grok": "0.7.0",
42
- "@aibridge/driver-codex": "0.7.0",
43
- "@aibridge/driver-claude": "0.7.0"
39
+ "@aibridge/proc": "0.9.0",
40
+ "@aibridge/driver-grok": "0.9.0",
41
+ "@aibridge/driver-codex": "0.9.0",
42
+ "@aibridge/driver-agy": "0.9.0",
43
+ "@aibridge/driver-claude": "0.9.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "tsdown": "0.22.14"
@@ -51,6 +51,11 @@ export const imageGen = buildCommand({
51
51
  optional: true,
52
52
  brief: 'Max seconds to wait for the render (default: 600)',
53
53
  },
54
+ preflight: {
55
+ kind: 'boolean',
56
+ default: true,
57
+ brief: 'Check model quota before rendering (use --no-preflight to skip)',
58
+ },
54
59
  json: {
55
60
  kind: 'boolean',
56
61
  withNegated: false,
@@ -30,6 +30,7 @@ const flags = (over: Partial<ImageGenFlags>): ImageGenFlags => ({
30
30
  out: '/tmp/out.jpg',
31
31
  json: false,
32
32
  transparent: false,
33
+ preflight: false,
33
34
  ...over,
34
35
  });
35
36
 
@@ -25,6 +25,7 @@ import {
25
25
  resolveModel,
26
26
  supportsImageGen,
27
27
  } from '../../models.ts';
28
+ import { preflightModel, renderPreflightRefusal } from '../../quotaPreflight.ts';
28
29
  import {
29
30
  CHROMA_CLAUSE,
30
31
  chromaKeyToPng,
@@ -38,6 +39,7 @@ export interface ImageGenFlags {
38
39
  readonly aspectRatio?: string;
39
40
  readonly image?: string;
40
41
  readonly timeout?: number;
42
+ readonly preflight: boolean;
41
43
  readonly json: boolean;
42
44
  readonly transparent: boolean;
43
45
  }
@@ -122,6 +124,18 @@ export default async function imageGen(
122
124
  return fail(formatImageGenModelError(inputSlug, model));
123
125
  }
124
126
 
127
+ // Last gate before a paid render. Every check above is local and must stay
128
+ // above it, so a bad --out or aspect ratio still fails without a network call.
129
+ if (flags.preflight) {
130
+ const verdict = await preflightModel(model);
131
+ if (!verdict.ok) {
132
+ this.process.stderr.write(`${renderPreflightRefusal('image-gen', verdict)}\n`);
133
+ this.process.exitCode = 3;
134
+ return;
135
+ }
136
+ if (verdict.warning) this.process.stderr.write(`aibridge image-gen: ${verdict.warning}\n`);
137
+ }
138
+
125
139
  const minBytes = model.spec.backend === 'codex' ? MIN_REAL_BYTES_CODEX : MIN_REAL_BYTES_TOOL;
126
140
  const work = mkdtempSync(join(tmpdir(), 'aibridge-imagegen-'));
127
141
 
@@ -29,7 +29,7 @@ export const review = buildCommand({
29
29
  kind: 'parsed',
30
30
  parse: String,
31
31
  optional: true,
32
- brief: 'Base git ref to diff against (default: HEAD)',
32
+ brief: 'Base git ref or range to diff against, e.g. HEAD~3 or main (default: HEAD)',
33
33
  },
34
34
  out: {
35
35
  kind: 'parsed',
@@ -50,7 +50,7 @@ export const review = buildCommand({
50
50
  },
51
51
  },
52
52
  docs: {
53
- brief: 'Review working tree diff or plan contract',
53
+ brief: 'Review a diff (working tree, or any commit range via --base) or a plan contract',
54
54
  fullDescription,
55
55
  },
56
56
  });
@@ -145,7 +145,7 @@ export default async function review(this: LocalContext, flags: ReviewFlags): Pr
145
145
  let reviewPrompt: string;
146
146
  if (isDirty) {
147
147
  reviewPrompt =
148
- `You are an expert code reviewer. Inspect the working tree diff against base '${baseRef}' and untracked files at ${cwd}.\n` +
148
+ `You are an expert code reviewer. Inspect the diff produced by \`git diff ${baseRef}\` (this covers committed and uncommitted changes) plus any untracked files at ${cwd}.\n` +
149
149
  (absPlanPath
150
150
  ? `Compare the implementation against the plan contract at ${absPlanPath}. Any file modified or feature added outside the plan contract counts as over-reach (severity: major unless harmful, then critical).\n`
151
151
  : '') +
@@ -234,10 +234,22 @@ test('renderPreflightRefusal: auth kind uses unauthenticated wording', () => {
234
234
  });
235
235
  assert.strictEqual(
236
236
  msg,
237
- 'aibridge plan: refusing — grok session expired (401) — run `grok login`, then retry. Running with --no-preflight would only send the delegate in unauthenticated. Or use a different --model.',
237
+ 'aibridge plan: refusing — grok session expired (401) — run `grok login`, then retry. Running with --no-preflight would only fail unauthenticated later. Or use a different --model.',
238
238
  );
239
239
  });
240
240
 
241
+ test('renderPreflightRefusal: image-gen quota refusal points at other image seats', () => {
242
+ const msg = renderPreflightRefusal('image-gen', {
243
+ kind: 'quota',
244
+ message: 'grok credit quota exhausted',
245
+ resetAt: undefined,
246
+ });
247
+ // No claude seat renders images, so the delegation fallback would be dead advice.
248
+ assert.ok(!msg.includes('claude-backend fallback'));
249
+ assert.ok(msg.includes('another image seat'));
250
+ assert.ok(msg.includes('openai-codex/gpt-5.6-sol'));
251
+ });
252
+
241
253
  test('renderPreflightRefusal: quota kind keeps override wording', () => {
242
254
  const msg = renderPreflightRefusal('subagent', {
243
255
  kind: 'quota',
@@ -141,8 +141,16 @@ export function renderPreflightRefusal(
141
141
  verdict: { kind: 'auth' | 'quota'; message: string; resetAt: string | undefined },
142
142
  ): string {
143
143
  if (verdict.kind === 'auth') {
144
- return `aibridge ${cmd}: refusing ${verdict.message}. Running with --no-preflight would only send the delegate in unauthenticated. Or use a different --model.`;
144
+ // "the delegate" was wrong for image-gen, which has no delegate the grok
145
+ // seat is a direct API call. Verified: --no-preflight there just fails at
146
+ // the render with exit 1.
147
+ return `aibridge ${cmd}: refusing — ${verdict.message}. Running with --no-preflight would only fail unauthenticated later. Or use a different --model.`;
145
148
  }
146
149
  const resetClause = verdict.resetAt ? ` Resets ${formatReset(verdict.resetAt)}.` : '';
147
- return `aibridge ${cmd}: refusing ${verdict.message}.${resetClause} Use --no-preflight to override, or a claude-backend fallback (subagent --model sonnet|opus — bills the Claude subscription).`;
150
+ // The claude fallback is delegation-only advice: no claude seat renders images.
151
+ const fallback =
152
+ cmd === 'image-gen'
153
+ ? 'Use --no-preflight to override, or another image seat (--model openai-codex/gpt-5.6-sol | google-antigravity/gemini-3.7-flash | xai-grok/grok-4.6).'
154
+ : 'Use --no-preflight to override, or a claude-backend fallback (subagent --model sonnet|opus — bills the Claude subscription).';
155
+ return `aibridge ${cmd}: refusing — ${verdict.message}.${resetClause} ${fallback}`;
148
156
  }