@awak-app/simy-cli 0.1.3 → 0.1.5

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/src/agent.js CHANGED
@@ -36,7 +36,19 @@ import {
36
36
  referenceLocalAttachmentPaths,
37
37
  } from "./local-attachments.js";
38
38
  import { discoverWorkspace } from "./workspace-context.js";
39
- import { resolveBackendExecutable } from "./backend-executable.js";
39
+ import { BACKEND_VERSION_POLICIES } from "./backend-executable.js";
40
+ import {
41
+ DESKTOP_EXECUTION_LABEL,
42
+ DESKTOP_EXECUTION_TARGET,
43
+ desktopExecutorCapability,
44
+ inspectDesktopExecutor,
45
+ normalizeDesktopExecutionTarget,
46
+ } from "./desktop-executor.js";
47
+ import { recoveryContractForPreflight } from "./orchestrator/recovery.js";
48
+ import {
49
+ DEFAULT_RETRY_BUDGET,
50
+ DEFAULT_TOKEN_BUDGET,
51
+ } from "./orchestrator/budget.js";
40
52
  import {
41
53
  resolveWebApiBaseUrl,
42
54
  sessionRequiresWebAuthorization,
@@ -60,6 +72,7 @@ export async function startAgent({
60
72
  daemon = false,
61
73
  webOrigin = null,
62
74
  sessionRoot,
75
+ updateManager = null,
63
76
  dependencies = {},
64
77
  quiet = false,
65
78
  } = {}) {
@@ -207,6 +220,7 @@ export async function startAgent({
207
220
  };
208
221
 
209
222
  const selectRepository = async (run, repository) => {
223
+ if (run) ensureAcceptingNewWork(updateManager);
210
224
  const selected = repository?.local_path
211
225
  ? repositoryInventory.find(
212
226
  (item) =>
@@ -219,10 +233,14 @@ export async function startAgent({
219
233
  throw new Error(`This run requires ${run.request.repository}; select that local repository.`);
220
234
  }
221
235
  if (!run) return selected;
222
- return continueLocalCodingRunAfterRepositoryApproval(
223
- run,
224
- { repository: selected.repository, localPath: selected.local_path },
225
- runOptions,
236
+ return withUpdateWork(
237
+ updateManager,
238
+ () =>
239
+ continueLocalCodingRunAfterRepositoryApproval(
240
+ run,
241
+ { repository: selected.repository, localPath: selected.local_path },
242
+ runOptions,
243
+ ),
226
244
  );
227
245
  };
228
246
 
@@ -239,6 +257,20 @@ export async function startAgent({
239
257
  }
240
258
 
241
259
  const url = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
260
+ const agenticLoopPath = canonicalAgenticLoopPath(url.pathname);
261
+ if (
262
+ req.method === "POST" &&
263
+ agenticLoopPath.startsWith("/v1/agentic-loop") &&
264
+ agenticLoopPath !== "/v1/agentic-loop/preflight"
265
+ ) {
266
+ const releaseUpdateWork = acquireUpdateWork(updateManager);
267
+ if (!releaseUpdateWork) {
268
+ json(res, 503, updateInProgressResponse(updateManager));
269
+ return;
270
+ }
271
+ res.once("finish", releaseUpdateWork);
272
+ res.once("close", releaseUpdateWork);
273
+ }
242
274
  if (req.method === "GET" && url.pathname === "/v1/health") {
243
275
  json(res, 200, {
244
276
  ok: true,
@@ -246,6 +278,7 @@ export async function startAgent({
246
278
  web_origin: apiOrigin,
247
279
  session_valid: isSessionValid(session, Date.now(), apiOrigin),
248
280
  session_expires_at: session?.expires_at ?? null,
281
+ cli_update: updateManager?.snapshot?.() ?? null,
249
282
  });
250
283
  return;
251
284
  }
@@ -253,7 +286,7 @@ export async function startAgent({
253
286
  json(res, 200, await readCapabilities(dependencies));
254
287
  return;
255
288
  }
256
- if (req.method === "POST" && url.pathname === "/v1/coding-loop/preflight") {
289
+ if (req.method === "POST" && agenticLoopPath === "/v1/agentic-loop/preflight") {
257
290
  if (!isSessionValid(session, Date.now(), apiOrigin)) {
258
291
  json(res, 401, { error: "simy session expired; run simy again" });
259
292
  return;
@@ -268,11 +301,22 @@ export async function startAgent({
268
301
  json(res, 400, { error: "backend must be codex or claude" });
269
302
  return;
270
303
  }
304
+ let executionTarget;
305
+ try {
306
+ executionTarget = normalizeDesktopExecutionTarget(body.execution_target);
307
+ } catch (error) {
308
+ json(res, 400, {
309
+ error: error instanceof Error ? error.message : "execution_target must be desktop",
310
+ code: "unsupported_execution_target",
311
+ });
312
+ return;
313
+ }
271
314
  const backend = body.backend;
272
315
  const indexedRepository = await ensureRepositoryIndexed(repository);
273
316
  const environment = await inspectCodingLoopEnvironment({
274
317
  repository,
275
318
  backend,
319
+ executionTarget,
276
320
  localPath:
277
321
  typeof body.local_path === "string"
278
322
  ? body.local_path
@@ -316,7 +360,11 @@ export async function startAgent({
316
360
  void synchronizeAuthorizedSession();
317
361
  return;
318
362
  }
319
- if (req.method === "POST" && url.pathname === "/v1/coding-loop/start") {
363
+ if (req.method === "POST" && agenticLoopPath === "/v1/agentic-loop/start") {
364
+ if (!acceptsNewWork(updateManager)) {
365
+ json(res, 503, updateInProgressResponse(updateManager));
366
+ return;
367
+ }
320
368
  if (!isSessionValid(session, Date.now(), apiOrigin)) {
321
369
  json(res, 401, { error: "simy session expired; run simy again" });
322
370
  return;
@@ -339,9 +387,40 @@ export async function startAgent({
339
387
  return;
340
388
  }
341
389
  if (registry.has(runId)) {
342
- json(res, 409, { error: "coding loop run already exists" });
390
+ json(res, 409, { error: "agentic loop run already exists" });
391
+ return;
392
+ }
393
+ let executionTarget;
394
+ try {
395
+ executionTarget = normalizeDesktopExecutionTarget(body.execution_target);
396
+ } catch (error) {
397
+ json(res, 400, {
398
+ error: error instanceof Error ? error.message : "execution_target must be desktop",
399
+ code: "unsupported_execution_target",
400
+ });
401
+ return;
402
+ }
403
+ if (body.backend !== "codex" && body.backend !== "claude") {
404
+ json(res, 400, { error: "backend must be codex or claude" });
343
405
  return;
344
406
  }
407
+ if (body.execution_target != null) {
408
+ const inspection = normalizeBackendInspection(body.backend, availableCapabilities);
409
+ if (!inspection.compatible) {
410
+ const summary = backendPreflightSummary(body.backend, inspection);
411
+ json(res, 409, {
412
+ error: summary,
413
+ code: `desktop_executor_${inspection.status}`,
414
+ execution_target: executionTarget,
415
+ recovery: recoveryContractForPreflight({
416
+ key: "backend",
417
+ summary,
418
+ details: inspection,
419
+ }),
420
+ });
421
+ return;
422
+ }
423
+ }
345
424
  let stagedAttachments = [];
346
425
  try {
347
426
  stagedAttachments = await stageRunAttachments({
@@ -358,6 +437,8 @@ export async function startAgent({
358
437
  runId,
359
438
  request: {
360
439
  backend: body.backend === "claude" ? "claude" : "codex",
440
+ execution_target: executionTarget,
441
+ execution_device_id: session?.device_id || null,
361
442
  audit_backend:
362
443
  body.audit_backend === "claude" || body.audit_backend === "codex"
363
444
  ? body.audit_backend
@@ -370,6 +451,8 @@ export async function startAgent({
370
451
  : indexedRepository?.local_path || null,
371
452
  base_branch: typeof body.base_branch === "string" ? body.base_branch : "dev",
372
453
  max_attempts: body.max_attempts,
454
+ retry_budget: body.retry_budget,
455
+ token_budget: body.token_budget,
373
456
  ui_evidence_root:
374
457
  typeof body.ui_evidence_root === "string" ? body.ui_evidence_root : "",
375
458
  acceptance_criteria: Array.isArray(body.acceptance_criteria)
@@ -391,6 +474,12 @@ export async function startAgent({
391
474
  typeof body.design_review_url === "string" ? body.design_review_url : "",
392
475
  must_not: Array.isArray(body.must_not) ? body.must_not : [],
393
476
  proposal_id: typeof body.proposal_id === "string" ? body.proposal_id : null,
477
+ charter_context:
478
+ body.charter_context &&
479
+ typeof body.charter_context === "object" &&
480
+ !Array.isArray(body.charter_context)
481
+ ? body.charter_context
482
+ : null,
394
483
  attachments: stagedAttachments,
395
484
  },
396
485
  session,
@@ -407,7 +496,7 @@ export async function startAgent({
407
496
  return;
408
497
  }
409
498
 
410
- const controlMatch = url.pathname.match(/^\/v1\/coding-loop\/([^/]+)\/control$/);
499
+ const controlMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/control$/);
411
500
  if (req.method === "POST" && controlMatch) {
412
501
  if (!isSessionValid(session, Date.now(), apiOrigin)) {
413
502
  json(res, 401, { error: "simy session expired; run simy again" });
@@ -428,6 +517,10 @@ export async function startAgent({
428
517
  } else if (body.action === "pause") {
429
518
  pauseLocalCodingRun(run);
430
519
  } else if (body.action === "resume") {
520
+ if (!acceptsNewWork(updateManager)) {
521
+ json(res, 503, updateInProgressResponse(updateManager));
522
+ return;
523
+ }
431
524
  resumeLocalCodingRun(run);
432
525
  } else {
433
526
  json(res, 400, { error: "action must be pause, resume, or stop" });
@@ -444,7 +537,7 @@ export async function startAgent({
444
537
  return;
445
538
  }
446
539
 
447
- const guidanceMatch = url.pathname.match(/^\/v1\/coding-loop\/([^/]+)\/guidance$/);
540
+ const guidanceMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/guidance$/);
448
541
  if (req.method === "POST" && guidanceMatch) {
449
542
  if (!isSessionValid(session, Date.now(), apiOrigin)) {
450
543
  json(res, 401, { error: "simy session expired; run simy again" });
@@ -474,7 +567,7 @@ export async function startAgent({
474
567
  return;
475
568
  }
476
569
 
477
- const hilMatch = url.pathname.match(/^\/v1\/coding-loop\/([^/]+)\/hil$/);
570
+ const hilMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/hil$/);
478
571
  if (hilMatch) {
479
572
  if (!isSessionValid(session, Date.now(), apiOrigin)) {
480
573
  json(res, 401, { error: "simy session expired; run simy again" });
@@ -502,6 +595,10 @@ export async function startAgent({
502
595
  }
503
596
 
504
597
  if (req.method === "POST") {
598
+ if (!acceptsNewWork(updateManager)) {
599
+ json(res, 503, updateInProgressResponse(updateManager));
600
+ return;
601
+ }
505
602
  const body = await readJson(req);
506
603
  const requestId = localRepositoryHilRequestId(run);
507
604
  if (body.request_id !== requestId) {
@@ -556,8 +653,12 @@ export async function startAgent({
556
653
  }
557
654
  }
558
655
 
559
- const recheckMatch = url.pathname.match(/^\/v1\/coding-loop\/([^/]+)\/recheck$/);
656
+ const recheckMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/recheck$/);
560
657
  if (req.method === "POST" && recheckMatch) {
658
+ if (!acceptsNewWork(updateManager)) {
659
+ json(res, 503, updateInProgressResponse(updateManager));
660
+ return;
661
+ }
561
662
  const run = registry.get(decodeURIComponent(recheckMatch[1]));
562
663
  if (!run) {
563
664
  json(res, 404, { error: "run not found" });
@@ -572,7 +673,7 @@ export async function startAgent({
572
673
  return;
573
674
  }
574
675
 
575
- const streamMatch = url.pathname.match(/^\/v1\/coding-loop\/([^/]+)\/stream$/);
676
+ const streamMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/stream$/);
576
677
  if (req.method === "GET" && streamMatch) {
577
678
  streamRun(res, registry.get(decodeURIComponent(streamMatch[1])));
578
679
  return;
@@ -616,6 +717,7 @@ export async function startAgent({
616
717
 
617
718
  server.on("close", () => {
618
719
  if (heartbeatTimer) clearInterval(heartbeatTimer);
720
+ updateManager?.stop?.();
619
721
  registry.close();
620
722
  });
621
723
  return {
@@ -629,17 +731,19 @@ export async function startAgent({
629
731
  workspace,
630
732
  repositoryScanRoot,
631
733
  capabilities: availableCapabilities,
734
+ updates: updateManager,
632
735
  controls: {
633
736
  repositoryInventory: () => [...repositoryInventory],
634
737
  scanRepositories,
635
738
  selectRepository,
636
- create: async (input) => {
739
+ create: (input) => withUpdateWork(updateManager, async () => {
637
740
  if (!isSessionValid(session, Date.now(), apiOrigin)) {
638
741
  throw new Error("Sign in to SIMY before starting a coding task.");
639
742
  }
640
743
  const requirement = String(input.requirement || "").trim();
641
744
  const repository = String(input.repository || "").trim();
642
745
  const backend = input.backend === "claude" ? "claude" : "codex";
746
+ const executionTarget = normalizeDesktopExecutionTarget(input.executionTarget);
643
747
  if (!requirement) throw new Error("Describe the coding task first.");
644
748
  if (!repository) throw new Error("Set a repository with /repo owner/name.");
645
749
  if (availableCapabilities?.backends?.[backend] !== true) {
@@ -652,7 +756,11 @@ export async function startAgent({
652
756
  local_path: indexedRepository?.local_path || input.localPath || workspace.localPath,
653
757
  base_branch: String(input.baseBranch || "dev"),
654
758
  backend,
655
- max_attempts: 3,
759
+ execution_target: executionTarget,
760
+ execution_device_id: session?.device_id || null,
761
+ max_attempts: DEFAULT_RETRY_BUDGET + 1,
762
+ retry_budget: DEFAULT_RETRY_BUDGET,
763
+ token_budget: DEFAULT_TOKEN_BUDGET,
656
764
  acceptance_criteria: [],
657
765
  expected_tests: [],
658
766
  expected_evidence: [],
@@ -671,20 +779,74 @@ export async function startAgent({
671
779
  request,
672
780
  });
673
781
  const runId = String(remoteRun?.id || "");
674
- if (!runId) throw new Error("SIMY Web did not return a coding run id.");
782
+ if (!runId) throw new Error("SIMY Web did not return an Agentic Loop run id.");
675
783
  const run = createRun({ runId, request, session, apiOrigin });
676
784
  registry.create(run);
677
785
  void startLocalCodingRun(run, runOptions);
678
786
  return run;
679
- },
680
- continue: (run, guidance) => continueLocalCodingRun(run, guidance, runOptions),
681
- applyDecision: (run, decision) => applyLocalHumanDecision(run, decision, runOptions),
787
+ }),
788
+ continue: (run, guidance) =>
789
+ withUpdateWork(updateManager, () => continueLocalCodingRun(run, guidance, runOptions)),
790
+ applyDecision: (run, decision) =>
791
+ withUpdateWork(updateManager, () => applyLocalHumanDecision(run, decision, runOptions)),
682
792
  queueGuidance: queueLocalCodingGuidance,
683
793
  pause: pauseLocalCodingRun,
684
- resume: resumeLocalCodingRun,
794
+ resume: (run) => withUpdateWorkSync(updateManager, () => resumeLocalCodingRun(run)),
685
795
  stop: stopLocalCodingRun,
686
796
  recheck: (run) =>
687
- recheckLocalCodingRun(run, { collectEvidence: runOptions.collectEvidence }),
797
+ withUpdateWork(updateManager, () =>
798
+ recheckLocalCodingRun(run, { collectEvidence: runOptions.collectEvidence }),
799
+ ),
800
+ },
801
+ };
802
+ }
803
+
804
+ function acceptsNewWork(updateManager) {
805
+ return updateManager?.acceptsNewWork?.() !== false;
806
+ }
807
+
808
+ function acquireUpdateWork(updateManager) {
809
+ if (!acceptsNewWork(updateManager)) return null;
810
+ return updateManager?.beginWork?.() ?? (() => {});
811
+ }
812
+
813
+ function ensureAcceptingNewWork(updateManager) {
814
+ if (acceptsNewWork(updateManager)) return;
815
+ throw new Error(
816
+ updateManager?.snapshot?.().message ||
817
+ "SIMY is installing an update. Wait for the daemon to restart, then try again.",
818
+ );
819
+ }
820
+
821
+ async function withUpdateWork(updateManager, action) {
822
+ const release = acquireUpdateWork(updateManager);
823
+ if (!release) ensureAcceptingNewWork(updateManager);
824
+ try {
825
+ return await action();
826
+ } finally {
827
+ release?.();
828
+ }
829
+ }
830
+
831
+ function withUpdateWorkSync(updateManager, action) {
832
+ const release = acquireUpdateWork(updateManager);
833
+ if (!release) ensureAcceptingNewWork(updateManager);
834
+ try {
835
+ return action();
836
+ } finally {
837
+ release?.();
838
+ }
839
+ }
840
+
841
+ function updateInProgressResponse(updateManager) {
842
+ return {
843
+ error:
844
+ updateManager?.snapshot?.().message ||
845
+ "SIMY is installing an update. Wait for the daemon to restart, then try again.",
846
+ code: "cli_update_in_progress",
847
+ recovery: {
848
+ title: "Wait for SIMY to restart",
849
+ description: "The CLI will reconnect automatically after the update finishes.",
688
850
  },
689
851
  };
690
852
  }
@@ -702,6 +864,8 @@ async function createRemoteRun({ apiOrigin, apiBaseUrl, token, request }) {
702
864
  backend: request.backend,
703
865
  base_branch: request.base_branch,
704
866
  max_attempts: request.max_attempts,
867
+ retry_budget: request.retry_budget,
868
+ token_budget: request.token_budget,
705
869
  acceptance_criteria: request.acceptance_criteria,
706
870
  expected_tests: request.expected_tests,
707
871
  expected_evidence: request.expected_evidence,
@@ -812,12 +976,31 @@ function localRepositoryHilRequest(run, root, authorizedRoots) {
812
976
 
813
977
  async function capabilities() {
814
978
  const [codex, claude] = await Promise.all([
815
- resolveBackendExecutable("codex"),
816
- resolveBackendExecutable("claude"),
979
+ inspectDesktopExecutor("codex"),
980
+ inspectDesktopExecutor("claude"),
817
981
  ]);
818
982
  return {
819
- backends: { codex: Boolean(codex), claude: Boolean(claude) },
820
- features: { repository_scan_approval: true },
983
+ backends: { codex: codex.compatible, claude: claude.compatible },
984
+ backend_versions: {
985
+ codex: publicBackendInspection(codex),
986
+ claude: publicBackendInspection(claude),
987
+ },
988
+ execution_targets: {
989
+ desktop: {
990
+ execution_target: DESKTOP_EXECUTION_TARGET,
991
+ label: DESKTOP_EXECUTION_LABEL,
992
+ available: codex.compatible || claude.compatible,
993
+ backends: {
994
+ codex: desktopExecutorCapability(codex),
995
+ claude: desktopExecutorCapability(claude),
996
+ },
997
+ },
998
+ },
999
+ features: {
1000
+ repository_scan_approval: true,
1001
+ executor_version_preflight: true,
1002
+ desktop_executor: true,
1003
+ },
821
1004
  session_ttl_hours: 48,
822
1005
  };
823
1006
  }
@@ -826,13 +1009,24 @@ async function readCapabilities(dependencies) {
826
1009
  return dependencies.capabilities ? dependencies.capabilities() : capabilities();
827
1010
  }
828
1011
 
829
- async function inspectCodingLoopEnvironment({ repository, backend, localPath, dependencies }) {
1012
+ async function inspectCodingLoopEnvironment({
1013
+ repository,
1014
+ backend,
1015
+ executionTarget,
1016
+ localPath,
1017
+ dependencies,
1018
+ }) {
830
1019
  const checks = [
831
1020
  {
832
1021
  key: "session",
833
1022
  status: "passed",
834
1023
  summary: "Local CLI session is connected to this SIMY Web origin.",
835
1024
  },
1025
+ {
1026
+ key: "execution_target",
1027
+ status: "passed",
1028
+ summary: `This run will execute on ${DESKTOP_EXECUTION_LABEL} through SIMY CLI.`,
1029
+ },
836
1030
  ];
837
1031
  let repositoryPath = null;
838
1032
  try {
@@ -843,32 +1037,107 @@ async function inspectCodingLoopEnvironment({ repository, backend, localPath, de
843
1037
  summary: `Verified ${repository} against the local Git origin.`,
844
1038
  });
845
1039
  } catch (error) {
1040
+ const summary =
1041
+ error instanceof Error ? error.message : "Local repository could not be resolved.";
846
1042
  checks.push({
847
1043
  key: "repository",
848
1044
  status: "failed",
849
- summary: error instanceof Error ? error.message : "Local repository could not be resolved.",
1045
+ summary,
1046
+ recovery: recoveryContractForPreflight({ key: "repository", summary }),
850
1047
  });
851
1048
  }
852
1049
 
853
1050
  const available = await readCapabilities(dependencies);
854
- const backendAvailable = available?.backends?.[backend] === true;
1051
+ const inspection = normalizeBackendInspection(backend, available);
1052
+ const backendSummary = backendPreflightSummary(backend, inspection);
855
1053
  checks.push({
856
1054
  key: "backend",
857
- status: backendAvailable ? "passed" : "failed",
858
- summary: backendAvailable
859
- ? `${backend === "claude" ? "Claude Code" : "Codex"} is available locally.`
860
- : `${backend === "claude" ? "Claude Code" : "Codex"} is not available on PATH.`,
1055
+ status: inspection.compatible ? "passed" : "failed",
1056
+ summary: backendSummary,
1057
+ details: {
1058
+ status: inspection.status,
1059
+ installed_version: inspection.installed_version,
1060
+ minimum_version: inspection.minimum_version,
1061
+ update_command: inspection.update_command,
1062
+ },
1063
+ ...(inspection.compatible
1064
+ ? {}
1065
+ : {
1066
+ recovery: recoveryContractForPreflight({
1067
+ key: "backend",
1068
+ summary: backendSummary,
1069
+ details: inspection,
1070
+ }),
1071
+ }),
861
1072
  });
862
1073
 
863
1074
  return {
864
- ready: Boolean(repositoryPath && backendAvailable),
1075
+ ready: Boolean(repositoryPath && inspection.compatible),
865
1076
  repository,
866
1077
  backend,
1078
+ execution_target: executionTarget,
867
1079
  local_path: repositoryPath,
868
1080
  checks,
869
1081
  };
870
1082
  }
871
1083
 
1084
+ function publicBackendInspection(inspection) {
1085
+ return {
1086
+ status: inspection.status,
1087
+ available: inspection.available,
1088
+ compatible: inspection.compatible,
1089
+ installed_version: inspection.installed_version,
1090
+ minimum_version: inspection.minimum_version,
1091
+ update_command: inspection.update_command,
1092
+ };
1093
+ }
1094
+
1095
+ function normalizeBackendInspection(backend, capabilitiesValue) {
1096
+ const policy = BACKEND_VERSION_POLICIES[backend];
1097
+ const detail = capabilitiesValue?.backend_versions?.[backend];
1098
+ if (detail && typeof detail === "object") {
1099
+ return {
1100
+ status: String(detail.status || (detail.compatible ? "compatible" : "missing")),
1101
+ available: detail.available === true,
1102
+ compatible: detail.compatible === true,
1103
+ installed_version:
1104
+ typeof detail.installed_version === "string" ? detail.installed_version : null,
1105
+ minimum_version:
1106
+ typeof detail.minimum_version === "string"
1107
+ ? detail.minimum_version
1108
+ : policy.minimumVersion,
1109
+ update_command:
1110
+ typeof detail.update_command === "string" ? detail.update_command : policy.updateCommand,
1111
+ };
1112
+ }
1113
+ const compatible = capabilitiesValue?.backends?.[backend] === true;
1114
+ return {
1115
+ status: compatible ? "compatible" : "missing",
1116
+ available: compatible,
1117
+ compatible,
1118
+ installed_version: null,
1119
+ minimum_version: policy.minimumVersion,
1120
+ update_command: policy.updateCommand,
1121
+ };
1122
+ }
1123
+
1124
+ function backendPreflightSummary(backend, inspection) {
1125
+ const label = BACKEND_VERSION_POLICIES[backend].label;
1126
+ if (inspection.status === "compatible" && inspection.installed_version) {
1127
+ return `${label} ${inspection.installed_version} is compatible (requires ${inspection.minimum_version} or newer).`;
1128
+ }
1129
+ if (inspection.status === "outdated") {
1130
+ return `${label} ${inspection.installed_version} is too old. Update to ${inspection.minimum_version} or newer: ${inspection.update_command}`;
1131
+ }
1132
+ if (inspection.status === "unknown_version") {
1133
+ return `${label} is installed, but its version could not be verified. Reinstall or update it: ${inspection.update_command}`;
1134
+ }
1135
+ if (inspection.status === "compatible") {
1136
+ return `${label} is available locally.`;
1137
+ }
1138
+ return `${label} is not available. Install or update it: ${inspection.update_command}`;
1139
+ }
1140
+
872
1141
  async function verifyLaunchChallenge({ apiOrigin, apiBaseUrl, token, runId, challenge }) {
873
1142
  try {
874
1143
  const response = await fetch(
@@ -890,6 +1159,10 @@ async function verifyLaunchChallenge({ apiOrigin, apiBaseUrl, token, runId, chal
890
1159
  }
891
1160
  }
892
1161
 
1162
+ function canonicalAgenticLoopPath(pathname) {
1163
+ return pathname.replace(/^\/v1\/coding-loop(?=\/|$)/, "/v1/agentic-loop");
1164
+ }
1165
+
893
1166
  function streamRun(res, run) {
894
1167
  if (!run) {
895
1168
  json(res, 404, { error: "run not found" });