badgr-cli 1.1.5 → 1.1.7

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.
@@ -5,10 +5,14 @@ import { ensureLoggedIn } from '../onboarding.js';
5
5
  import { webBaseUrl } from '../config.js';
6
6
 
7
7
  const DIAGNOSE_HELP = `
8
- Paste anything. This IS the Badgr Smoke Test: free, no GPU, no login.
8
+ Paste anything. This IS Badgr Preflight: free, no GPU, no login.
9
9
  Badgr detects the input, resolves a command, and automatically runs every
10
10
  applicable free structural check on it. No GPU launches without explicit
11
- --approve, which starts a real Badgr Run.
11
+ --approve, which starts a real Badgr Run (the Smoke Test step below).
12
+
13
+ Badgr Preflight
14
+ -> static checks -> resource fit -> compatibility checks
15
+ -> optional Smoke Test (a tiny, capped, real GPU run) -> Verdict
12
16
 
13
17
  Usage:
14
18
  badgr diagnose "<input>"
@@ -31,6 +35,12 @@ Flags:
31
35
  browser sign-in automatically if not logged in).
32
36
  This is a real, billable GPU-provisioned execution
33
37
  -- distinct from the free diagnosis above.
38
+ --paid-ok Explicit consent to run as a normal paid job when a
39
+ free Smoke Test entitlement exists but can't cover
40
+ this proof (over the $0.50 cap, or the daily free-
41
+ verification budget is exhausted). Without this,
42
+ --approve stops and shows the reason instead of
43
+ silently charging you.
34
44
  --smoke Deprecated, no-op. The free mechanical checks this
35
45
  used to gate are now always run automatically as
36
46
  part of plain "badgr diagnose" -- kept only so
@@ -44,7 +54,7 @@ Flags:
44
54
  --json Machine-readable JSON output
45
55
  --help, -h Show this help
46
56
 
47
- Status: every run prints one of NEEDS INFO / READY / SMOKE CHECKED / INVALID / VERIFIED.
57
+ Verdict: every run prints one of NEEDS INFO / READY / SMOKE CHECKED / INVALID / VERIFIED.
48
58
  NEEDS INFO -- cannot yet produce a complete command
49
59
  READY -- complete evidence-backed command, no applicable checks to run
50
60
  SMOKE CHECKED -- applicable checks ran; all PASS or SKIPPED
@@ -179,11 +189,11 @@ function detectInput(raw, flags) {
179
189
  return { kind: 'text', label: 'Text / conversation', body: classified.body };
180
190
  }
181
191
 
182
- // backend/run_issue_routes.py's /extract now returns the canonical
192
+ // the backend's own /extract now returns the canonical
183
193
  // "service" (persistent, badgr serve) vs "finite_job" (bounded, badgr
184
194
  // run/train/comfyui) classification directly on the extraction as
185
- // `workload_shape` (_workload_shape, the same predicate
186
- // _canonical_badgr_command itself uses to pick a subcommand) -- read that
195
+ // `workload_shape` (the same predicate the backend's own
196
+ // command-canonicalization logic uses to pick a subcommand) -- read that
187
197
  // instead of re-deriving an approximation here, so the CLI's wording can
188
198
  // never drift from what the backend actually decided. The local fallback
189
199
  // only matters for a response predating this field (e.g. a cached fixture).
@@ -274,7 +284,7 @@ function _printResult(result, chalk) {
274
284
  // verification step below is still capped (it's just a readiness
275
285
  // check), but the resulting `badgr serve` endpoint keeps running and
276
286
  // billing until stopped, or an explicit --max-cost/--max-runtime is
277
- // supplied. See backend/run_issue_routes.py's _canonical_badgr_command.
287
+ // supplied. See the backend's own command-canonicalization logic.
278
288
  console.log(` ${chalk.bold('Recommended action:')} Open the Run link below to verify readiness and start the service`);
279
289
  console.log(` ${chalk.dim('The service keeps running after verification -- stop it with `badgr down`, or launch it with --max-cost/--persistent.')}`);
280
290
  } else {
@@ -332,12 +342,13 @@ function _localPathCandidates(e) {
332
342
  // Pure presentation over data /run-issue/extract already returns -- no new
333
343
  // endpoint, no new GPU behaviour. Diagnosis alone can reach SMOKE CHECKED
334
344
  // or INVALID (never VERIFIED, see AGENTS.md §12 and _printCopySummary
335
- // below) -- `badgr diagnose` IS the Badgr Smoke Test, so applicable free
336
- // checks always run and always count toward the printed status; there is
337
- // no separate opt-in step for them.
345
+ // below) -- `badgr diagnose` IS Badgr Preflight, so applicable free checks
346
+ // always run and always count toward the printed verdict; there is no
347
+ // separate opt-in step for them. The real, bounded GPU run is the distinct
348
+ // "Smoke Test" step (--approve / run_smoke_test), never this free path.
338
349
  //
339
350
  // The evidence-only judgment (needs_info/invalid/smoke_checked) is computed
340
- // once, server-side, in `_compute_smoke_status` (backend/run_issue_routes.py)
351
+ // once, server-side, in the backend's own smoke-status computation
341
352
  // and read here via `e.smoke_status` -- this and /run-issue's `smokeStatus()`
342
353
  // (frontend/app/run-issue/page.tsx) both just render that one backend
343
354
  // verdict rather than each re-deriving pass/fail from mechanical_checks, so
@@ -355,7 +366,7 @@ function _localPathCandidates(e) {
355
366
  function _statusWord(result) {
356
367
  const e = result.extraction || {};
357
368
  if (result.static_incompatibilities?.length || e.missing_information?.length) return 'NEEDS INFO';
358
- // Mirrors backend/run_issue_routes.py's _compute_smoke_status exactly: a
369
+ // Mirrors the backend's own smoke-status computation exactly: a
359
370
  // mechanical FAIL is invalid; otherwise only real command-scope evidence
360
371
  // (smoke_check.proof_level === "smoke_checked" -- the repository/image/
361
372
  // service's own documented default, not just a syntax-only pass) earns
@@ -373,24 +384,29 @@ function _statusWord(result) {
373
384
  }
374
385
 
375
386
  const _CHECK_LABELS = {
376
- command_syntax: 'command syntax',
377
- supported_flags: 'known CLI flags',
378
- cli_entrypoint: 'CLI entrypoint',
379
- referenced_files: 'referenced repo files',
380
- image_availability: 'image exists / accessible',
381
- docker_entrypoint: 'Docker metadata',
382
- required_env_vars: 'required env vars',
387
+ command_syntax: 'command syntax',
388
+ subcommand_exists: 'known subcommand',
389
+ required_positional_args: 'required positional args',
390
+ supported_flags: 'known CLI flags',
391
+ cli_entrypoint: 'CLI entrypoint',
392
+ referenced_files: 'referenced repo files',
393
+ dockerfile_expected: 'Dockerfile present',
394
+ image_availability: 'image exists / accessible',
395
+ docker_entrypoint: 'Docker metadata',
396
+ required_env_vars: 'required env vars',
397
+ required_ports: 'required ports',
398
+ runtime_workload_compatibility: 'runtime supports workload type',
383
399
  };
384
400
 
385
- // Compact, copy-pasteable result -- the literal STATUS word plus just enough
386
- // to forward to someone else. Additive to the detailed report above it;
387
- // does not replace it. Never prints "Verified" -- SMOKE CHECKED is the
401
+ // Compact, copy-pasteable result -- the literal Verdict word plus just
402
+ // enough to forward to someone else. Additive to the detailed report above
403
+ // it; does not replace it. Never prints "Verified" -- SMOKE CHECKED is the
388
404
  // ceiling for this free path.
389
405
  function _printCopySummary(result, chalk, caseData, config) {
390
406
  const e = result.extraction || {};
391
407
  const status = _statusWord(result);
392
408
 
393
- console.log(` ${chalk.bold(`STATUS: ${status}`)}`);
409
+ console.log(` ${chalk.bold(`Verdict: ${status}`)}`);
394
410
  console.log();
395
411
 
396
412
  if (status === 'NEEDS INFO') {
@@ -409,7 +425,7 @@ function _printCopySummary(result, chalk, caseData, config) {
409
425
  // --approve would run) is preferred over the raw source command --
410
426
  // /confirm can only compute it once a case exists (READY/SMOKE CHECKED),
411
427
  // and only when there's enough evidence to convert (see
412
- // _canonical_badgr_command's docstring, backend/run_issue_routes.py, for
428
+ // the backend's own command-canonicalization logic for
413
429
  // what it can't yet convert, e.g. training/ComfyUI). Falls back to the
414
430
  // raw extracted command otherwise -- still true and still runnable input,
415
431
  // just not guaranteed to already be `badgr`-shaped.
@@ -441,9 +457,12 @@ function _printCopySummary(result, chalk, caseData, config) {
441
457
  if (status === 'INVALID') {
442
458
  console.log(' A check above actually failed -- this command is not runnable as-is.');
443
459
  } else {
444
- console.log(' Not GPU-verified.');
460
+ console.log(` ${chalk.bold('Smoke test:')} Not run yet.`);
445
461
  }
446
462
  console.log();
463
+ } else if (status === 'READY') {
464
+ console.log(` ${chalk.bold('Smoke test:')} Not run yet.`);
465
+ console.log();
447
466
  }
448
467
  }
449
468
 
@@ -463,9 +482,10 @@ function _printSmokeChecks(result, chalk) {
463
482
  // final verdict (_statusWord can land on READY, NEEDS INFO, or INVALID
464
483
  // just as easily as SMOKE CHECKED; see its own comment for why a
465
484
  // syntax-only pass isn't proof of scope). A fixed "Smoke Checked" header
466
- // here previously disagreed with a `STATUS: READY` line printed right
467
- // after it -- this section is the free checks themselves, not the verdict.
468
- console.log(` ${chalk.bold('Free checks (no GPU, no login):')}`);
485
+ // here previously disagreed with a `Verdict: READY` line printed right
486
+ // after it -- this section is Preflight itself (static + resource checks),
487
+ // not the verdict, and not the (separate, real-GPU) Smoke Test step.
488
+ console.log(` ${chalk.bold('Preflight (no GPU, no login):')}`);
469
489
 
470
490
  const icon = (status) => (
471
491
  status === 'pass' ? chalk.green('✓') : status === 'fail' ? chalk.red('✗') : chalk.dim('-')
@@ -569,7 +589,7 @@ async function _createRunPage(config, e, chalk) {
569
589
  }
570
590
  }
571
591
 
572
- async function _doApprove(config, caseData, chalk) {
592
+ async function _doApprove(config, caseData, chalk, paidFallbackConfirmed) {
573
593
  const { case_id: caseId, status, test_plan: plan, workload_plan: workloadPlan, missing_information: missing } = caseData;
574
594
 
575
595
  if (status === 'incompatible') {
@@ -594,9 +614,9 @@ async function _doApprove(config, caseData, chalk) {
594
614
  return;
595
615
  }
596
616
 
597
- // Display the canonical WorkloadPlan (backend/reliability_engine.py) --
598
- // the same object creation -> routing -> Jobs -> watchdog all consume --
599
- // rather than a second, independently-derived estimate. Falls back to the
617
+ // Display the canonical WorkloadPlan -- the same object creation ->
618
+ // routing -> Jobs -> watchdog all consume -- rather than a second,
619
+ // independently-derived estimate. Falls back to the
600
620
  // raw test_plan only for fields the canonical plan doesn't carry
601
621
  // (requires_reduced_test / requires_funding_approval are smoke-check-only
602
622
  // concerns, never part of WorkloadPlan).
@@ -609,16 +629,16 @@ async function _doApprove(config, caseData, chalk) {
609
629
  if (plan?.requires_funding_approval) console.log(` ${chalk.yellow('!')} Large download — funding required`);
610
630
  }
611
631
 
612
- await _approveAndRun(config, caseId, plan, chalk);
632
+ await _approveAndRun(config, caseId, plan, chalk, paidFallbackConfirmed);
613
633
  }
614
634
 
615
635
  // Claim → approve → run for a case that already exists (just created via
616
636
  // /confirm above, or resumed via an existing_case input). Auth is required
617
637
  // from here on — diagnosis and case creation stay anonymous and free, but a
618
638
  // real GPU test needs a signed-in account. The CLI never grants a free-run
619
- // entitlement (web admin only, see grant_free_run in run_issue_routes.py);
639
+ // entitlement (web admin only, granted server-side);
620
640
  // it only redeems one a case already carries, same as normal billing.
621
- async function _approveAndRun(config, caseId, plan, chalk) {
641
+ async function _approveAndRun(config, caseId, plan, chalk, paidFallbackConfirmed) {
622
642
  const printResumeHint = () =>
623
643
  console.error(chalk.dim(` Resume this case: ${webBaseUrl(config)}/run-issue?case_id=${caseId}\n`));
624
644
 
@@ -706,7 +726,11 @@ async function _approveAndRun(config, caseId, plan, chalk) {
706
726
  let ran;
707
727
  try {
708
728
  ran = await callApi(`/run-issue/cases/${caseId}/run`, {
709
- method: 'POST', apiKey: authConfig.apiKey, baseUrl: authConfig.baseUrl, timeoutMs: 30_000,
729
+ method: 'POST',
730
+ apiKey: authConfig.apiKey,
731
+ baseUrl: authConfig.baseUrl,
732
+ body: { paid_fallback_confirmed: Boolean(paidFallbackConfirmed) },
733
+ timeoutMs: 30_000,
710
734
  });
711
735
  } catch (err) {
712
736
  if (err.isPaymentRequired) {
@@ -719,6 +743,29 @@ async function _approveAndRun(config, caseId, plan, chalk) {
719
743
  return;
720
744
  }
721
745
 
746
+ // The free entitlement existed but couldn't cover this proof (over the
747
+ // per-proof cap, or the daily subsidy budget exhausted) -- nothing ran
748
+ // and nothing was charged (the backend's own smoke-test execution
749
+ // stop-and-ask branch). Never auto-continue as paid: require the caller
750
+ // to explicitly re-run with --paid-ok once they've seen this and the
751
+ // real cap, exactly like the reduced-test block above.
752
+ if (ran?.paid_execution_required && !ran?.free_run_used) {
753
+ console.log();
754
+ console.log(chalk.yellow(` Smoke test not run.`));
755
+ console.log();
756
+ console.log(` ${chalk.bold('Free verification unavailable:')} ${
757
+ ran.free_run_skipped_reason === 'daily_budget_exhausted'
758
+ ? 'Daily free-verification budget exhausted.'
759
+ : 'The smallest useful proof exceeds the free verification limit.'
760
+ }`);
761
+ console.log();
762
+ if (ran.max_cost_usd != null) console.log(` ${chalk.bold('Max cost:')} $${Number(ran.max_cost_usd).toFixed(2)}`);
763
+ console.log(chalk.dim(` Continue with a paid Smoke Test: badgr diagnose ${caseId} --approve --paid-ok`));
764
+ console.log();
765
+ process.exitCode = 1;
766
+ return;
767
+ }
768
+
722
769
  console.log();
723
770
  console.log(chalk.green(' Run started.'));
724
771
  console.log(` ${chalk.bold('Case:')} ${caseId}`);
@@ -746,7 +793,7 @@ async function _diagnoseExistingCase(config, caseId, flags, chalk) {
746
793
 
747
794
  if (flags.json) {
748
795
  console.log(JSON.stringify(caseData, null, 2));
749
- if (flags.approve) await _approveAndRun(config, caseId, caseData.test_plan, chalk);
796
+ if (flags.approve) await _approveAndRun(config, caseId, caseData.test_plan, chalk, flags.paidOk);
750
797
  return;
751
798
  }
752
799
 
@@ -770,7 +817,7 @@ async function _diagnoseExistingCase(config, caseId, flags, chalk) {
770
817
  || caseData.status === 'verified' || caseData.status === 'completed'
771
818
  ? 'VERIFIED'
772
819
  : (caseData.missing_information || []).length ? 'NEEDS INFO' : 'READY';
773
- console.log(` ${chalk.bold(`STATUS: ${existingStatus}`)}`);
820
+ console.log(` ${chalk.bold(`Verdict: ${existingStatus}`)}`);
774
821
  if (existingStatus === 'VERIFIED') {
775
822
  console.log();
776
823
  console.log(` ${chalk.bold('Command:')}`);
@@ -798,7 +845,7 @@ async function _diagnoseExistingCase(config, caseId, flags, chalk) {
798
845
  console.log();
799
846
 
800
847
  if (flags.approve) {
801
- await _approveAndRun(config, caseId, caseData.test_plan, chalk);
848
+ await _approveAndRun(config, caseId, caseData.test_plan, chalk, flags.paidOk);
802
849
  } else {
803
850
  console.log(` ${chalk.bold('Resume:')} badgr diagnose ${caseId} --approve`);
804
851
  console.log();
@@ -829,6 +876,7 @@ export async function diagnoseCommand(config, args, chalk) {
829
876
  if (a === '--comfyui') { flags.comfyui = args[++i]; i++; continue; }
830
877
  if (a === '--github') { flags.github = args[++i]; i++; continue; }
831
878
  if (a === '--approve') { flags.approve = true; i++; continue; }
879
+ if (a === '--paid-ok') { flags.paidOk = true; i++; continue; }
832
880
  if (a === '--smoke') { flags.smoke = true; i++; continue; }
833
881
  if (a === '--json') { flags.json = true; i++; continue; }
834
882
  if (!a.startsWith('-')) positionalParts.push(a);
@@ -952,7 +1000,7 @@ export async function diagnoseCommand(config, args, chalk) {
952
1000
  console.error(chalk.yellow('\n Cannot approve — missing information above.\n'));
953
1001
  return;
954
1002
  }
955
- await _doApprove(config, caseData, chalk);
1003
+ await _doApprove(config, caseData, chalk, flags.paidOk);
956
1004
  }
957
1005
  return;
958
1006
  }
@@ -982,6 +1030,6 @@ export async function diagnoseCommand(config, args, chalk) {
982
1030
  console.log(chalk.yellow('\n Cannot approve — missing information above.\n'));
983
1031
  return;
984
1032
  }
985
- await _doApprove(config, caseData, chalk);
1033
+ await _doApprove(config, caseData, chalk, flags.paidOk);
986
1034
  }
987
1035
  }
@@ -1,6 +1,6 @@
1
1
  import { ensureLoggedInReady, withReauthRetry } from '../onboarding.js';
2
2
  import { findDeployment, removeDeployment, addReceipt, generateReceiptId } from '../store.js';
3
- import { terminateDeployment, listDeployments, adminTerminateDeployment } from '../api.js';
3
+ import { terminateDeployment, listDeployments } from '../api.js';
4
4
 
5
5
  function formatRuntime(minutes) {
6
6
  if (minutes < 60) return `${minutes}m`;
@@ -32,13 +32,22 @@ export async function downCommand(config, args, chalk) {
32
32
 
33
33
  config = await ensureLoggedInReady(config, chalk);
34
34
 
35
- // --admin bypasses per-user ownership (michaelhireitem@gmail.com only --
36
- // see deployment_routes.py's DELETE /v1/admin/deployments/{id}) so a
37
- // deployment found via `badgr status --admin` (owned by a different
38
- // account) can actually be stopped. Local name lookup only applies to the
39
- // caller's own deployments, so it's skipped here -- an admin shutdown
40
- // always targets a real deployment_id from `badgr status --admin`.
35
+ // --admin bypasses per-user ownership (gated server-side by an internal
36
+ // admin allowlist) so a deployment found via `badgr status --admin`
37
+ // (owned by a different account) can actually be stopped. Local name
38
+ // lookup only applies to the caller's own deployments, so it's skipped
39
+ // here -- an admin shutdown always targets a real deployment_id from
40
+ // `badgr status --admin`. See ../admin.js for why this is a separate,
41
+ // never-publicly-exported module.
41
42
  if (isAdmin) {
43
+ let adminTerminateDeployment;
44
+ try {
45
+ ({ adminTerminateDeployment } = await import('../admin.js')); // oss-safe: guarded by try/catch, no-ops when admin.js is absent from a public export
46
+ } catch {
47
+ console.error(chalk.red('\n Admin mode is not available in this build.\n'));
48
+ process.exitCode = 1;
49
+ return;
50
+ }
42
51
  process.stdout.write(chalk.dim(` [admin] Stopping ${idOrName}...`));
43
52
  try {
44
53
  const refreshed = await withReauthRetry(config, chalk, cfg => adminTerminateDeployment(cfg, idOrName));
@@ -8,7 +8,7 @@ import { ensureBadgrReady } from '../onboarding.js';
8
8
  * Submits a bounded coding-agent job via POST /v1/jobs (type: "agent"),
9
9
  * which creates a Job record and runs it on a GPU/CPU VM.
10
10
  * All three interfaces (website, CLI, POST /v1/jobs) share the same
11
- * execution path on the backend (backend/jobs_routes.py).
11
+ * execution path on the backend.
12
12
  *
13
13
  * Usage:
14
14
  * badgr job cline "Fix the checkout bug" --check "npm test"
@@ -46,15 +46,15 @@ function parseJobArgs(rawArgs) {
46
46
  return { flags, positional };
47
47
  }
48
48
 
49
- // Providers the agent Job type accepts (backend jobs_routes._VALID_AGENT_PROVIDERS).
49
+ // Providers the agent Job type accepts (server-side allowlist).
50
50
  // The BYOK/OpenAI-compatible providers `badgr launch --provider` supports
51
51
  // (openrouter, deepseek, glm, custom) are NOT among them: the Jobs API
52
52
  // resolves credentials server-side from stored provider credentials and has
53
53
  // nowhere to put a custom base URL, so those must be rejected in the CLI
54
54
  // rather than sent on to a 400 from the server.
55
- export const JOB_API_PROVIDERS = ['badgr', 'openai', 'anthropic'];
55
+ export const JOB_API_PROVIDERS = ['badgr', 'openai', 'anthropic', 'commandcode'];
56
56
 
57
- export const JOB_AGENTS = ['cline', 'claude', 'claude-code', 'codex', 'playwright'];
57
+ export const JOB_AGENTS = ['cline', 'claude', 'claude-code', 'codex', 'opencode', 'commandcode', 'playwright'];
58
58
 
59
59
  export async function jobCommand(config, args, chalk) {
60
60
  // Support both:
@@ -77,7 +77,7 @@ export async function jobCommand(config, args, chalk) {
77
77
 
78
78
  if (!instructionText || !agentName) {
79
79
  console.error(chalk.red('\nUsage: badgr job <agent> "<instruction>" --check "<command>"\n'));
80
- console.error(chalk.dim(' Agents: cline (default), claude-code, codex, playwright'));
80
+ console.error(chalk.dim(' Agents: cline (default), claude-code, codex, opencode, commandcode, playwright'));
81
81
  console.error(chalk.dim(' Example: badgr job cline "Fix the checkout bug" --check "npm test"'));
82
82
  console.error(chalk.dim(' Example: badgr job claude-code "Add pagination" --check "pytest tests/" --max-cost 3\n'));
83
83
  process.exitCode = 1;
@@ -9,9 +9,16 @@ import { VM_CLASSES, LAUNCH_VM_SIZES, vmClassForWorkload } from '../spec.js';
9
9
  // explicit `badgr launch <source> -- <command>` form (kept as the advanced
10
10
  // escape hatch), including --image for a fully custom runtime.
11
11
  //
12
+ // `opencode` rejoins this list (it was dropped from the bare-shorthand set
13
+ // in Phase 1's third revision — see docs/badgr-execution-roadmap.md's
14
+ // history note) now that it has a real image and a wrapper script verified
15
+ // against the live CLI (images/badgr-agent-opencode/badgr-opencode-run). It
16
+ // is a BYOK-only lane like claude/codex, not a Badgr-native default like
17
+ // cline — opencode has no Badgr-hosted model-token integration.
18
+ //
12
19
  // `cline` is the Badgr-native default: no user credential required, since
13
20
  // Badgr mints a short-lived, job-scoped token pointing at its own
14
- // OpenAI-compatible coding-model endpoint (see backend jobs_routes.py's
21
+ // OpenAI-compatible coding-model endpoint (see the backend's own
15
22
  // `_cline_model_token` / `_build_worker_command`). `claude`/`codex` are
16
23
  // bring-your-own-provider lanes — if no credential is stored yet, an
17
24
  // interactive terminal prompts for one inline and stores it via `badgr
@@ -40,13 +47,19 @@ import { VM_CLASSES, LAUNCH_VM_SIZES, vmClassForWorkload } from '../spec.js';
40
47
  // always exactly one array element, so it reaches the agent unchanged
41
48
  // regardless of spaces, quotes, punctuation, or shell metacharacters: there
42
49
  // is no shell-string layer left to round-trip it through (see
43
- // command_argv on the backend and BADGR_COMMAND_JSON in
44
- // images/badgr-job-runner/entrypoint.py, which subprocess.run executes
50
+ // command_argv on the backend and BADGR_COMMAND_JSON in the worker
51
+ // image's own entrypoint, which subprocess.run executes
45
52
  // directly with shell=False — no shlex.split() involved for this path).
46
53
  const AGENT_WORKLOADS = {
47
- cline: { image: process.env.BADGR_AGENT_IMAGE_CLINE || 'ghcr.io/michaelmanly/badgr-agent-cline:latest', buildCmd: task => ['badgr-cline-run', task], provider: null },
48
- claude: { image: process.env.BADGR_AGENT_IMAGE_CLAUDE || 'ghcr.io/michaelmanly/badgr-agent-claude:latest', buildCmd: task => ['badgr-claude-run', task], provider: 'anthropic' },
49
- codex: { image: process.env.BADGR_AGENT_IMAGE_CODEX || 'ghcr.io/michaelmanly/badgr-agent-codex:latest', buildCmd: task => ['badgr-codex-run', task], provider: 'openai' },
54
+ cline: { image: process.env.BADGR_AGENT_IMAGE_CLINE || 'ghcr.io/michaelmanly/badgr-agent-cline:latest', buildCmd: task => ['badgr-cline-run', task], provider: null },
55
+ claude: { image: process.env.BADGR_AGENT_IMAGE_CLAUDE || 'ghcr.io/michaelmanly/badgr-agent-claude:latest', buildCmd: task => ['badgr-claude-run', task], provider: 'anthropic' },
56
+ codex: { image: process.env.BADGR_AGENT_IMAGE_CODEX || 'ghcr.io/michaelmanly/badgr-agent-codex:latest', buildCmd: task => ['badgr-codex-run', task], provider: 'openai' },
57
+ opencode: { image: process.env.BADGR_AGENT_IMAGE_OPENCODE || 'ghcr.io/michaelmanly/badgr-agent-opencode:latest', buildCmd: task => ['badgr-opencode-run', task], provider: 'openai' },
58
+ // Command Code is a single hosted account (bundled model catalog reached
59
+ // through its own API), not an OpenAI/Anthropic-compatible endpoint —
60
+ // there is no BYOK provider-swap lane for it, unlike claude/codex/cline
61
+ // (see the flags.provider guard in launchAgentWorkload below).
62
+ commandcode: { image: process.env.BADGR_AGENT_IMAGE_COMMANDCODE || 'ghcr.io/michaelmanly/badgr-agent-commandcode:latest', buildCmd: task => ['badgr-commandcode-run', task], provider: 'commandcode' },
50
63
  };
51
64
 
52
65
  // Non-agent workloads: no LLM credential, and the task string (if given) is
@@ -101,8 +114,9 @@ const _VALUE_FLAGS = {
101
114
  '--max-runtime': 'maxRuntime', '--max-cost': 'maxCost', '--save': 'save',
102
115
  '--workspace': 'workspace', '--output': 'output', '--checkpoint': 'checkpoint',
103
116
  '--resume-cmd': 'resumeCmd', '--size': 'size',
104
- // BYOK / OpenAI-compatible model selection — `badgr launch cline` only,
105
- // see resolveModelProviderEnv below.
117
+ // BYOK / OpenAI-compatible model selection — `badgr launch cline` (BYOK
118
+ // opt-in) and `codex`/`opencode` (BYOK-only) — see resolveModelProviderEnv
119
+ // below.
106
120
  '--provider': 'provider', '--model': 'model', '--base-url': 'baseUrl',
107
121
  // When present, routes to POST /v1/jobs (type: agent) instead of the
108
122
  // plain badgr-launch GPU/CPU run path.
@@ -194,6 +208,8 @@ function warnIfTaskMayHaveSwallowedAFlag(task, chalk) {
194
208
  * badgr launch cline "<task>" — Badgr-hosted model, no credential needed
195
209
  * badgr launch claude "<task>" — prompts inline for an Anthropic key if not connected
196
210
  * badgr launch codex "<task>" — prompts inline for an OpenAI key if not connected
211
+ * badgr launch opencode "<task>" — prompts inline for an OpenAI key if not connected
212
+ * badgr launch commandcode "<task>" — prompts inline for a Command Code key if not connected
197
213
  * badgr launch playwright ["<task>"] — no credential; task is a display label only
198
214
  * badgr launch <source> -- <command> — explicit form / advanced escape hatch
199
215
  * badgr launch <source> --cmd "<command>"
@@ -210,6 +226,8 @@ export async function launchCommand(config, args, chalk) {
210
226
  console.error(chalk.dim(' badgr launch cline "Fix the checkout bug"'));
211
227
  console.error(chalk.dim(' badgr launch claude "Fix the checkout bug"'));
212
228
  console.error(chalk.dim(' badgr launch codex "Fix the checkout bug"'));
229
+ console.error(chalk.dim(' badgr launch opencode "Fix the checkout bug"'));
230
+ console.error(chalk.dim(' badgr launch commandcode "Fix the checkout bug"'));
213
231
  console.error(chalk.dim(' badgr launch playwright "Test the checkout flow"'));
214
232
  console.error(chalk.dim(' badgr launch . --max-cost 1 -- npm test (explicit escape hatch)'));
215
233
  console.error('');
@@ -282,7 +300,7 @@ async function resolveMissingCredential(provider, chalk, label) {
282
300
  /**
283
301
  * BYOK / OpenAI-compatible or Anthropic-compatible model selection.
284
302
  *
285
- * `badgr launch <cline|codex> --provider <name> --model <id> [--base-url <url>]`
303
+ * `badgr launch <cline|codex|opencode> --provider <name> --model <id> [--base-url <url>]`
286
304
  * uses apiKind='openai' (injects OPENAI_API_KEY/OPENAI_BASE_URL/MODEL).
287
305
  * `badgr launch claude --provider <name> --model <id> [--base-url <url>]`
288
306
  * uses apiKind='anthropic' (injects ANTHROPIC_API_KEY/ANTHROPIC_BASE_URL/MODEL).
@@ -376,6 +394,13 @@ async function launchAgentWorkload(config, agentName, rawArgs, chalk) {
376
394
 
377
395
  const spec = AGENT_WORKLOADS[agentName];
378
396
 
397
+ if (agentName === 'commandcode' && flags.provider) {
398
+ console.error(chalk.red(`\n ✗ --provider is not supported for \`badgr launch commandcode\` — it has no BYOK/swappable-endpoint lane, only its own hosted account.\n`));
399
+ console.error(chalk.dim(` Run \`badgr connect commandcode\` instead.\n`));
400
+ process.exitCode = 1;
401
+ return;
402
+ }
403
+
379
404
  if (spec.provider && flags.provider) {
380
405
  // BYOK override for a fixed-provider workload (claude → anthropic,
381
406
  // codex → openai). Skip the managed credential flow and inject the
@@ -422,7 +447,7 @@ async function launchAgentWorkload(config, agentName, rawArgs, chalk) {
422
447
  } else if (flags.provider) {
423
448
  // BYOK for cline (spec.provider is null) — injects OPENAI_API_KEY,
424
449
  // OPENAI_BASE_URL, MODEL. The backend skips its managed model token
425
- // when it sees an existing OPENAI_API_KEY (see jobs_routes.py).
450
+ // when it sees an existing OPENAI_API_KEY (handled server-side).
426
451
  const modelEnv = await resolveModelProviderEnv(flags, chalk, 'openai');
427
452
  if (!modelEnv) {
428
453
  process.exitCode = 1;
@@ -435,7 +460,7 @@ async function launchAgentWorkload(config, agentName, rawArgs, chalk) {
435
460
  flags.authRequired = { provider: null, status: 'none' };
436
461
  }
437
462
  // cline with no --provider: Badgr mints a short-lived job-scoped model
438
- // token server-side (see backend jobs_routes.py); the CLI never handles
463
+ // token server-side; the CLI never handles
439
464
  // or displays that token. --provider switches cline to BYOK instead.
440
465
 
441
466
  const cmdArgv = spec.buildCmd(task);
@@ -460,7 +485,7 @@ async function launchDirectWorkload(config, workloadName, rawArgs, chalk) {
460
485
 
461
486
  if (flags.provider) {
462
487
  console.error(chalk.red(`\n ✗ --provider is not supported for \`badgr launch ${workloadName}\` — it is a test runner with no LLM component.\n`));
463
- console.error(chalk.dim(` Use \`badgr launch claude/cline/codex\` for AI agent workloads.\n`));
488
+ console.error(chalk.dim(` Use \`badgr launch claude/cline/codex/opencode/commandcode\` for AI agent workloads.\n`));
464
489
  process.exitCode = 1;
465
490
  return;
466
491
  }