@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/README.md +75 -7
- package/package.json +16 -4
- package/src/agent.js +306 -33
- package/src/auto-update.js +631 -0
- package/src/backend-executable.js +92 -6
- package/src/console/app.js +46 -8
- package/src/console/index.js +5 -4
- package/src/desktop-executor.js +137 -0
- package/src/index.js +40 -4
- package/src/local-attachments.js +2 -2
- package/src/orchestrator/audit.js +101 -7
- package/src/orchestrator/budget.js +112 -0
- package/src/orchestrator/contract.js +175 -27
- package/src/orchestrator/independent-audit.js +18 -4
- package/src/orchestrator/index.js +1 -0
- package/src/orchestrator/instruction.js +41 -5
- package/src/orchestrator/loop.js +181 -7
- package/src/orchestrator/presentation.js +2 -2
- package/src/orchestrator/problem-solving.js +129 -0
- package/src/orchestrator/recovery.js +271 -0
- package/src/orchestrator/result.js +2 -0
- package/src/orchestrator/retry.js +140 -0
- package/src/orchestrator/shared.js +87 -5
- package/src/repository-inventory.js +87 -0
- package/src/runner.js +76 -94
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 {
|
|
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
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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" &&
|
|
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" &&
|
|
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: "
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
681
|
-
|
|
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
|
-
|
|
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
|
-
|
|
816
|
-
|
|
979
|
+
inspectDesktopExecutor("codex"),
|
|
980
|
+
inspectDesktopExecutor("claude"),
|
|
817
981
|
]);
|
|
818
982
|
return {
|
|
819
|
-
backends: { codex:
|
|
820
|
-
|
|
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({
|
|
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
|
|
1045
|
+
summary,
|
|
1046
|
+
recovery: recoveryContractForPreflight({ key: "repository", summary }),
|
|
850
1047
|
});
|
|
851
1048
|
}
|
|
852
1049
|
|
|
853
1050
|
const available = await readCapabilities(dependencies);
|
|
854
|
-
const
|
|
1051
|
+
const inspection = normalizeBackendInspection(backend, available);
|
|
1052
|
+
const backendSummary = backendPreflightSummary(backend, inspection);
|
|
855
1053
|
checks.push({
|
|
856
1054
|
key: "backend",
|
|
857
|
-
status:
|
|
858
|
-
summary:
|
|
859
|
-
|
|
860
|
-
:
|
|
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 &&
|
|
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" });
|