@awak-app/simy-cli 0.1.3 → 0.1.4
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 +39 -5
- package/package.json +14 -4
- package/src/agent.js +204 -23
- package/src/backend-executable.js +92 -6
- package/src/console/app.js +9 -7
- package/src/console/index.js +5 -4
- package/src/desktop-executor.js +137 -0
- package/src/index.js +3 -3
- 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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SIMY CLI
|
|
2
2
|
|
|
3
|
-
Local execution agent for SIMY
|
|
3
|
+
Local execution agent for SIMY Agentic Loop runs.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx @awak-app/simy-cli
|
|
@@ -18,16 +18,50 @@ available for automation, but the command-line flag takes precedence.
|
|
|
18
18
|
|
|
19
19
|
The CLI starts a loopback HTTP agent on a random available port, keeps a
|
|
20
20
|
48-hour, origin-scoped local session, and opens a native coding chat. The first
|
|
21
|
-
chat message creates the remote audit ledger and starts the local
|
|
21
|
+
chat message creates the remote audit ledger and starts the local Agentic Loop
|
|
22
22
|
state machine; SIMY Web mirrors the lifecycle but is not required as the task
|
|
23
23
|
entry point. The CLI also accepts backend-issued launch challenges for Web-led
|
|
24
24
|
runs. It builds the requirement charter, runs Codex or Claude Code, audits
|
|
25
25
|
structured completion evidence, and re-instructs the executor within the
|
|
26
26
|
configured attempt budget.
|
|
27
27
|
|
|
28
|
+
## Executor compatibility
|
|
29
|
+
|
|
30
|
+
SIMY checks the selected executor with `--version` before an Agentic Loop can
|
|
31
|
+
start. The currently validated compatibility floors are Codex `0.144.0` and
|
|
32
|
+
Claude Code `2.1.200`. A missing, older, or unversioned executable fails the
|
|
33
|
+
preflight and returns the installed version, required version, and a recovery
|
|
34
|
+
command to SIMY Web. Update Codex with
|
|
35
|
+
`npm install -g @openai/codex@latest`; update Claude Code with `claude update`.
|
|
36
|
+
|
|
37
|
+
## Your Desktop executor
|
|
38
|
+
|
|
39
|
+
SIMY CLI is the `desktop` execution target, shown to users as **Your Desktop**.
|
|
40
|
+
It can run either Codex or Claude Code as a child process inside the verified
|
|
41
|
+
local Git checkout. Device heartbeats advertise both desktop provider
|
|
42
|
+
capabilities and their installed/required versions. Preflight and every standard
|
|
43
|
+
provider dispatch fail closed unless the selected binary is compatible; there
|
|
44
|
+
is no fallback to an unverified executable name.
|
|
45
|
+
|
|
46
|
+
Legacy local requests without `execution_target` continue to resolve to
|
|
47
|
+
`desktop`. A request marked for the hosted `simy` target is rejected by the
|
|
48
|
+
local CLI boundary so hosted and desktop execution cannot be confused. The
|
|
49
|
+
target and bound device id are preserved in the run Charter, redacted Web
|
|
50
|
+
ledger snapshot, and restored run.
|
|
51
|
+
|
|
52
|
+
## Blocked recovery contract
|
|
53
|
+
|
|
54
|
+
Every `waiting_human`, `blocked`, or `failed` lifecycle event includes a
|
|
55
|
+
versioned `detail.recovery` object. It provides a stable reason code, a
|
|
56
|
+
user-safe explanation, a primary action, executable action metadata, and
|
|
57
|
+
bounded technical details. Preflight failures use the same contract for
|
|
58
|
+
executor updates, repository selection, and CLI reconnection. Web clients must
|
|
59
|
+
validate the action type before rendering or executing it; raw local error text
|
|
60
|
+
is not copied into the recovery contract.
|
|
61
|
+
|
|
28
62
|
## Interactive console
|
|
29
63
|
|
|
30
|
-
Running `simy` in a terminal opens the
|
|
64
|
+
Running `simy` in a terminal opens the Agentic Loop chat. It discovers the
|
|
31
65
|
current GitHub repository and branch, selects an available executor, and keeps
|
|
32
66
|
the composer active so a natural-language request can start immediately. Each
|
|
33
67
|
run appears in a selectable list with its lifecycle state, process ID, and a
|
|
@@ -74,7 +108,7 @@ The local PR lifecycle is intentionally separate from deployment:
|
|
|
74
108
|
and human approval all pass.
|
|
75
109
|
|
|
76
110
|
The Web UI can refresh review and CI evidence with
|
|
77
|
-
`POST /v1/
|
|
111
|
+
`POST /v1/agentic-loop/:run_id/recheck`. Passing the local implementation gate
|
|
78
112
|
does not by itself make a PR merge-ready.
|
|
79
113
|
|
|
80
114
|
Start `simy` from the selected repository or a workspace containing it. Set
|
|
@@ -86,7 +120,7 @@ from an empty CLI chat, types repository, branch, executor, attachment, and task
|
|
|
86
120
|
input through a pseudoterminal, creates the Web ledger, spawns real child
|
|
87
121
|
processes, and exercises HIL plus lifecycle controls. It regenerates the
|
|
88
122
|
screenshots, terminal frames, raw transcript, and SHA-256 manifest under
|
|
89
|
-
`docs/pr-evidence/cli-
|
|
123
|
+
`docs/pr-evidence/cli-agentic-loop-console/`.
|
|
90
124
|
|
|
91
125
|
## Publishing
|
|
92
126
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awak-app/simy-cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Local SIMY
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Local SIMY Agentic Loop executor for Codex and Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"simy": "src/index.js"
|
|
@@ -14,11 +14,21 @@
|
|
|
14
14
|
"start": "node ./src/index.js",
|
|
15
15
|
"test": "node --test",
|
|
16
16
|
"test:e2e:console": "node ./scripts/console-e2e.js",
|
|
17
|
+
"test:e2e:audit-gate": "node ./scripts/audit-gate-e2e.js",
|
|
18
|
+
"test:e2e:retry-circuit": "node ./scripts/retry-circuit-e2e.js",
|
|
19
|
+
"test:e2e:follow-up-charter": "node ./scripts/follow-up-charter-e2e.js",
|
|
20
|
+
"test:e2e:task-routing": "node ./scripts/task-routing-e2e.js",
|
|
21
|
+
"test:e2e:executor-version-preflight": "node ./scripts/executor-version-preflight-e2e.js",
|
|
22
|
+
"test:e2e:blocked-recovery": "node ./scripts/blocked-recovery-e2e.js",
|
|
23
|
+
"test:e2e:process-localization": "node ./scripts/process-localization-e2e.js",
|
|
24
|
+
"test:e2e:hypothesis-retries": "node ./scripts/hypothesis-retries-e2e.js",
|
|
25
|
+
"test:e2e:budgets": "node ./scripts/budget-enforcement-e2e.js",
|
|
26
|
+
"test:e2e:desktop-executor": "node ./scripts/desktop-executor-e2e.js",
|
|
17
27
|
"test:e2e:console:real-provider": "node ./scripts/real-provider-console-e2e.js",
|
|
18
28
|
"test:e2e:console:fullstack": "node ./scripts/fullstack-cli-smoke.js",
|
|
19
|
-
"check": "npm run check:syntax && npm run check:real-provider-syntax && npm test && npm run check:package",
|
|
29
|
+
"check": "node --check ./src/desktop-executor.js && node --check ./scripts/desktop-executor-e2e.js && node --check ./scripts/fixtures/fake-desktop-provider.js && npm run check:syntax && npm run check:real-provider-syntax && npm test && npm run check:package",
|
|
20
30
|
"check:real-provider-syntax": "node --check ./scripts/real-codex-probe.js && node --check ./scripts/real-claude-probe.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js",
|
|
21
|
-
"check:syntax": "node --check ./src/index.js && node --check ./src/agent.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/run-registry.js && node --check ./src/backend-executable.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js",
|
|
31
|
+
"check:syntax": "node --check ./src/index.js && node --check ./src/agent.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/run-registry.js && node --check ./src/backend-executable.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js",
|
|
22
32
|
"check:package": "node ./scripts/check-package-contents.js"
|
|
23
33
|
},
|
|
24
34
|
"engines": {
|
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,
|
|
@@ -239,6 +251,7 @@ export async function startAgent({
|
|
|
239
251
|
}
|
|
240
252
|
|
|
241
253
|
const url = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
|
|
254
|
+
const agenticLoopPath = canonicalAgenticLoopPath(url.pathname);
|
|
242
255
|
if (req.method === "GET" && url.pathname === "/v1/health") {
|
|
243
256
|
json(res, 200, {
|
|
244
257
|
ok: true,
|
|
@@ -253,7 +266,7 @@ export async function startAgent({
|
|
|
253
266
|
json(res, 200, await readCapabilities(dependencies));
|
|
254
267
|
return;
|
|
255
268
|
}
|
|
256
|
-
if (req.method === "POST" &&
|
|
269
|
+
if (req.method === "POST" && agenticLoopPath === "/v1/agentic-loop/preflight") {
|
|
257
270
|
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
258
271
|
json(res, 401, { error: "simy session expired; run simy again" });
|
|
259
272
|
return;
|
|
@@ -268,11 +281,22 @@ export async function startAgent({
|
|
|
268
281
|
json(res, 400, { error: "backend must be codex or claude" });
|
|
269
282
|
return;
|
|
270
283
|
}
|
|
284
|
+
let executionTarget;
|
|
285
|
+
try {
|
|
286
|
+
executionTarget = normalizeDesktopExecutionTarget(body.execution_target);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
json(res, 400, {
|
|
289
|
+
error: error instanceof Error ? error.message : "execution_target must be desktop",
|
|
290
|
+
code: "unsupported_execution_target",
|
|
291
|
+
});
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
271
294
|
const backend = body.backend;
|
|
272
295
|
const indexedRepository = await ensureRepositoryIndexed(repository);
|
|
273
296
|
const environment = await inspectCodingLoopEnvironment({
|
|
274
297
|
repository,
|
|
275
298
|
backend,
|
|
299
|
+
executionTarget,
|
|
276
300
|
localPath:
|
|
277
301
|
typeof body.local_path === "string"
|
|
278
302
|
? body.local_path
|
|
@@ -316,7 +340,7 @@ export async function startAgent({
|
|
|
316
340
|
void synchronizeAuthorizedSession();
|
|
317
341
|
return;
|
|
318
342
|
}
|
|
319
|
-
if (req.method === "POST" &&
|
|
343
|
+
if (req.method === "POST" && agenticLoopPath === "/v1/agentic-loop/start") {
|
|
320
344
|
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
321
345
|
json(res, 401, { error: "simy session expired; run simy again" });
|
|
322
346
|
return;
|
|
@@ -339,9 +363,40 @@ export async function startAgent({
|
|
|
339
363
|
return;
|
|
340
364
|
}
|
|
341
365
|
if (registry.has(runId)) {
|
|
342
|
-
json(res, 409, { error: "
|
|
366
|
+
json(res, 409, { error: "agentic loop run already exists" });
|
|
343
367
|
return;
|
|
344
368
|
}
|
|
369
|
+
let executionTarget;
|
|
370
|
+
try {
|
|
371
|
+
executionTarget = normalizeDesktopExecutionTarget(body.execution_target);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
json(res, 400, {
|
|
374
|
+
error: error instanceof Error ? error.message : "execution_target must be desktop",
|
|
375
|
+
code: "unsupported_execution_target",
|
|
376
|
+
});
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (body.backend !== "codex" && body.backend !== "claude") {
|
|
380
|
+
json(res, 400, { error: "backend must be codex or claude" });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (body.execution_target != null) {
|
|
384
|
+
const inspection = normalizeBackendInspection(body.backend, availableCapabilities);
|
|
385
|
+
if (!inspection.compatible) {
|
|
386
|
+
const summary = backendPreflightSummary(body.backend, inspection);
|
|
387
|
+
json(res, 409, {
|
|
388
|
+
error: summary,
|
|
389
|
+
code: `desktop_executor_${inspection.status}`,
|
|
390
|
+
execution_target: executionTarget,
|
|
391
|
+
recovery: recoveryContractForPreflight({
|
|
392
|
+
key: "backend",
|
|
393
|
+
summary,
|
|
394
|
+
details: inspection,
|
|
395
|
+
}),
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
345
400
|
let stagedAttachments = [];
|
|
346
401
|
try {
|
|
347
402
|
stagedAttachments = await stageRunAttachments({
|
|
@@ -358,6 +413,8 @@ export async function startAgent({
|
|
|
358
413
|
runId,
|
|
359
414
|
request: {
|
|
360
415
|
backend: body.backend === "claude" ? "claude" : "codex",
|
|
416
|
+
execution_target: executionTarget,
|
|
417
|
+
execution_device_id: session?.device_id || null,
|
|
361
418
|
audit_backend:
|
|
362
419
|
body.audit_backend === "claude" || body.audit_backend === "codex"
|
|
363
420
|
? body.audit_backend
|
|
@@ -370,6 +427,8 @@ export async function startAgent({
|
|
|
370
427
|
: indexedRepository?.local_path || null,
|
|
371
428
|
base_branch: typeof body.base_branch === "string" ? body.base_branch : "dev",
|
|
372
429
|
max_attempts: body.max_attempts,
|
|
430
|
+
retry_budget: body.retry_budget,
|
|
431
|
+
token_budget: body.token_budget,
|
|
373
432
|
ui_evidence_root:
|
|
374
433
|
typeof body.ui_evidence_root === "string" ? body.ui_evidence_root : "",
|
|
375
434
|
acceptance_criteria: Array.isArray(body.acceptance_criteria)
|
|
@@ -391,6 +450,12 @@ export async function startAgent({
|
|
|
391
450
|
typeof body.design_review_url === "string" ? body.design_review_url : "",
|
|
392
451
|
must_not: Array.isArray(body.must_not) ? body.must_not : [],
|
|
393
452
|
proposal_id: typeof body.proposal_id === "string" ? body.proposal_id : null,
|
|
453
|
+
charter_context:
|
|
454
|
+
body.charter_context &&
|
|
455
|
+
typeof body.charter_context === "object" &&
|
|
456
|
+
!Array.isArray(body.charter_context)
|
|
457
|
+
? body.charter_context
|
|
458
|
+
: null,
|
|
394
459
|
attachments: stagedAttachments,
|
|
395
460
|
},
|
|
396
461
|
session,
|
|
@@ -407,7 +472,7 @@ export async function startAgent({
|
|
|
407
472
|
return;
|
|
408
473
|
}
|
|
409
474
|
|
|
410
|
-
const controlMatch =
|
|
475
|
+
const controlMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/control$/);
|
|
411
476
|
if (req.method === "POST" && controlMatch) {
|
|
412
477
|
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
413
478
|
json(res, 401, { error: "simy session expired; run simy again" });
|
|
@@ -444,7 +509,7 @@ export async function startAgent({
|
|
|
444
509
|
return;
|
|
445
510
|
}
|
|
446
511
|
|
|
447
|
-
const guidanceMatch =
|
|
512
|
+
const guidanceMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/guidance$/);
|
|
448
513
|
if (req.method === "POST" && guidanceMatch) {
|
|
449
514
|
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
450
515
|
json(res, 401, { error: "simy session expired; run simy again" });
|
|
@@ -474,7 +539,7 @@ export async function startAgent({
|
|
|
474
539
|
return;
|
|
475
540
|
}
|
|
476
541
|
|
|
477
|
-
const hilMatch =
|
|
542
|
+
const hilMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/hil$/);
|
|
478
543
|
if (hilMatch) {
|
|
479
544
|
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
480
545
|
json(res, 401, { error: "simy session expired; run simy again" });
|
|
@@ -556,7 +621,7 @@ export async function startAgent({
|
|
|
556
621
|
}
|
|
557
622
|
}
|
|
558
623
|
|
|
559
|
-
const recheckMatch =
|
|
624
|
+
const recheckMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/recheck$/);
|
|
560
625
|
if (req.method === "POST" && recheckMatch) {
|
|
561
626
|
const run = registry.get(decodeURIComponent(recheckMatch[1]));
|
|
562
627
|
if (!run) {
|
|
@@ -572,7 +637,7 @@ export async function startAgent({
|
|
|
572
637
|
return;
|
|
573
638
|
}
|
|
574
639
|
|
|
575
|
-
const streamMatch =
|
|
640
|
+
const streamMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/stream$/);
|
|
576
641
|
if (req.method === "GET" && streamMatch) {
|
|
577
642
|
streamRun(res, registry.get(decodeURIComponent(streamMatch[1])));
|
|
578
643
|
return;
|
|
@@ -640,6 +705,7 @@ export async function startAgent({
|
|
|
640
705
|
const requirement = String(input.requirement || "").trim();
|
|
641
706
|
const repository = String(input.repository || "").trim();
|
|
642
707
|
const backend = input.backend === "claude" ? "claude" : "codex";
|
|
708
|
+
const executionTarget = normalizeDesktopExecutionTarget(input.executionTarget);
|
|
643
709
|
if (!requirement) throw new Error("Describe the coding task first.");
|
|
644
710
|
if (!repository) throw new Error("Set a repository with /repo owner/name.");
|
|
645
711
|
if (availableCapabilities?.backends?.[backend] !== true) {
|
|
@@ -652,7 +718,11 @@ export async function startAgent({
|
|
|
652
718
|
local_path: indexedRepository?.local_path || input.localPath || workspace.localPath,
|
|
653
719
|
base_branch: String(input.baseBranch || "dev"),
|
|
654
720
|
backend,
|
|
655
|
-
|
|
721
|
+
execution_target: executionTarget,
|
|
722
|
+
execution_device_id: session?.device_id || null,
|
|
723
|
+
max_attempts: DEFAULT_RETRY_BUDGET + 1,
|
|
724
|
+
retry_budget: DEFAULT_RETRY_BUDGET,
|
|
725
|
+
token_budget: DEFAULT_TOKEN_BUDGET,
|
|
656
726
|
acceptance_criteria: [],
|
|
657
727
|
expected_tests: [],
|
|
658
728
|
expected_evidence: [],
|
|
@@ -671,7 +741,7 @@ export async function startAgent({
|
|
|
671
741
|
request,
|
|
672
742
|
});
|
|
673
743
|
const runId = String(remoteRun?.id || "");
|
|
674
|
-
if (!runId) throw new Error("SIMY Web did not return
|
|
744
|
+
if (!runId) throw new Error("SIMY Web did not return an Agentic Loop run id.");
|
|
675
745
|
const run = createRun({ runId, request, session, apiOrigin });
|
|
676
746
|
registry.create(run);
|
|
677
747
|
void startLocalCodingRun(run, runOptions);
|
|
@@ -702,6 +772,8 @@ async function createRemoteRun({ apiOrigin, apiBaseUrl, token, request }) {
|
|
|
702
772
|
backend: request.backend,
|
|
703
773
|
base_branch: request.base_branch,
|
|
704
774
|
max_attempts: request.max_attempts,
|
|
775
|
+
retry_budget: request.retry_budget,
|
|
776
|
+
token_budget: request.token_budget,
|
|
705
777
|
acceptance_criteria: request.acceptance_criteria,
|
|
706
778
|
expected_tests: request.expected_tests,
|
|
707
779
|
expected_evidence: request.expected_evidence,
|
|
@@ -812,12 +884,31 @@ function localRepositoryHilRequest(run, root, authorizedRoots) {
|
|
|
812
884
|
|
|
813
885
|
async function capabilities() {
|
|
814
886
|
const [codex, claude] = await Promise.all([
|
|
815
|
-
|
|
816
|
-
|
|
887
|
+
inspectDesktopExecutor("codex"),
|
|
888
|
+
inspectDesktopExecutor("claude"),
|
|
817
889
|
]);
|
|
818
890
|
return {
|
|
819
|
-
backends: { codex:
|
|
820
|
-
|
|
891
|
+
backends: { codex: codex.compatible, claude: claude.compatible },
|
|
892
|
+
backend_versions: {
|
|
893
|
+
codex: publicBackendInspection(codex),
|
|
894
|
+
claude: publicBackendInspection(claude),
|
|
895
|
+
},
|
|
896
|
+
execution_targets: {
|
|
897
|
+
desktop: {
|
|
898
|
+
execution_target: DESKTOP_EXECUTION_TARGET,
|
|
899
|
+
label: DESKTOP_EXECUTION_LABEL,
|
|
900
|
+
available: codex.compatible || claude.compatible,
|
|
901
|
+
backends: {
|
|
902
|
+
codex: desktopExecutorCapability(codex),
|
|
903
|
+
claude: desktopExecutorCapability(claude),
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
},
|
|
907
|
+
features: {
|
|
908
|
+
repository_scan_approval: true,
|
|
909
|
+
executor_version_preflight: true,
|
|
910
|
+
desktop_executor: true,
|
|
911
|
+
},
|
|
821
912
|
session_ttl_hours: 48,
|
|
822
913
|
};
|
|
823
914
|
}
|
|
@@ -826,13 +917,24 @@ async function readCapabilities(dependencies) {
|
|
|
826
917
|
return dependencies.capabilities ? dependencies.capabilities() : capabilities();
|
|
827
918
|
}
|
|
828
919
|
|
|
829
|
-
async function inspectCodingLoopEnvironment({
|
|
920
|
+
async function inspectCodingLoopEnvironment({
|
|
921
|
+
repository,
|
|
922
|
+
backend,
|
|
923
|
+
executionTarget,
|
|
924
|
+
localPath,
|
|
925
|
+
dependencies,
|
|
926
|
+
}) {
|
|
830
927
|
const checks = [
|
|
831
928
|
{
|
|
832
929
|
key: "session",
|
|
833
930
|
status: "passed",
|
|
834
931
|
summary: "Local CLI session is connected to this SIMY Web origin.",
|
|
835
932
|
},
|
|
933
|
+
{
|
|
934
|
+
key: "execution_target",
|
|
935
|
+
status: "passed",
|
|
936
|
+
summary: `This run will execute on ${DESKTOP_EXECUTION_LABEL} through SIMY CLI.`,
|
|
937
|
+
},
|
|
836
938
|
];
|
|
837
939
|
let repositoryPath = null;
|
|
838
940
|
try {
|
|
@@ -843,32 +945,107 @@ async function inspectCodingLoopEnvironment({ repository, backend, localPath, de
|
|
|
843
945
|
summary: `Verified ${repository} against the local Git origin.`,
|
|
844
946
|
});
|
|
845
947
|
} catch (error) {
|
|
948
|
+
const summary =
|
|
949
|
+
error instanceof Error ? error.message : "Local repository could not be resolved.";
|
|
846
950
|
checks.push({
|
|
847
951
|
key: "repository",
|
|
848
952
|
status: "failed",
|
|
849
|
-
summary
|
|
953
|
+
summary,
|
|
954
|
+
recovery: recoveryContractForPreflight({ key: "repository", summary }),
|
|
850
955
|
});
|
|
851
956
|
}
|
|
852
957
|
|
|
853
958
|
const available = await readCapabilities(dependencies);
|
|
854
|
-
const
|
|
959
|
+
const inspection = normalizeBackendInspection(backend, available);
|
|
960
|
+
const backendSummary = backendPreflightSummary(backend, inspection);
|
|
855
961
|
checks.push({
|
|
856
962
|
key: "backend",
|
|
857
|
-
status:
|
|
858
|
-
summary:
|
|
859
|
-
|
|
860
|
-
:
|
|
963
|
+
status: inspection.compatible ? "passed" : "failed",
|
|
964
|
+
summary: backendSummary,
|
|
965
|
+
details: {
|
|
966
|
+
status: inspection.status,
|
|
967
|
+
installed_version: inspection.installed_version,
|
|
968
|
+
minimum_version: inspection.minimum_version,
|
|
969
|
+
update_command: inspection.update_command,
|
|
970
|
+
},
|
|
971
|
+
...(inspection.compatible
|
|
972
|
+
? {}
|
|
973
|
+
: {
|
|
974
|
+
recovery: recoveryContractForPreflight({
|
|
975
|
+
key: "backend",
|
|
976
|
+
summary: backendSummary,
|
|
977
|
+
details: inspection,
|
|
978
|
+
}),
|
|
979
|
+
}),
|
|
861
980
|
});
|
|
862
981
|
|
|
863
982
|
return {
|
|
864
|
-
ready: Boolean(repositoryPath &&
|
|
983
|
+
ready: Boolean(repositoryPath && inspection.compatible),
|
|
865
984
|
repository,
|
|
866
985
|
backend,
|
|
986
|
+
execution_target: executionTarget,
|
|
867
987
|
local_path: repositoryPath,
|
|
868
988
|
checks,
|
|
869
989
|
};
|
|
870
990
|
}
|
|
871
991
|
|
|
992
|
+
function publicBackendInspection(inspection) {
|
|
993
|
+
return {
|
|
994
|
+
status: inspection.status,
|
|
995
|
+
available: inspection.available,
|
|
996
|
+
compatible: inspection.compatible,
|
|
997
|
+
installed_version: inspection.installed_version,
|
|
998
|
+
minimum_version: inspection.minimum_version,
|
|
999
|
+
update_command: inspection.update_command,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function normalizeBackendInspection(backend, capabilitiesValue) {
|
|
1004
|
+
const policy = BACKEND_VERSION_POLICIES[backend];
|
|
1005
|
+
const detail = capabilitiesValue?.backend_versions?.[backend];
|
|
1006
|
+
if (detail && typeof detail === "object") {
|
|
1007
|
+
return {
|
|
1008
|
+
status: String(detail.status || (detail.compatible ? "compatible" : "missing")),
|
|
1009
|
+
available: detail.available === true,
|
|
1010
|
+
compatible: detail.compatible === true,
|
|
1011
|
+
installed_version:
|
|
1012
|
+
typeof detail.installed_version === "string" ? detail.installed_version : null,
|
|
1013
|
+
minimum_version:
|
|
1014
|
+
typeof detail.minimum_version === "string"
|
|
1015
|
+
? detail.minimum_version
|
|
1016
|
+
: policy.minimumVersion,
|
|
1017
|
+
update_command:
|
|
1018
|
+
typeof detail.update_command === "string" ? detail.update_command : policy.updateCommand,
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
const compatible = capabilitiesValue?.backends?.[backend] === true;
|
|
1022
|
+
return {
|
|
1023
|
+
status: compatible ? "compatible" : "missing",
|
|
1024
|
+
available: compatible,
|
|
1025
|
+
compatible,
|
|
1026
|
+
installed_version: null,
|
|
1027
|
+
minimum_version: policy.minimumVersion,
|
|
1028
|
+
update_command: policy.updateCommand,
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function backendPreflightSummary(backend, inspection) {
|
|
1033
|
+
const label = BACKEND_VERSION_POLICIES[backend].label;
|
|
1034
|
+
if (inspection.status === "compatible" && inspection.installed_version) {
|
|
1035
|
+
return `${label} ${inspection.installed_version} is compatible (requires ${inspection.minimum_version} or newer).`;
|
|
1036
|
+
}
|
|
1037
|
+
if (inspection.status === "outdated") {
|
|
1038
|
+
return `${label} ${inspection.installed_version} is too old. Update to ${inspection.minimum_version} or newer: ${inspection.update_command}`;
|
|
1039
|
+
}
|
|
1040
|
+
if (inspection.status === "unknown_version") {
|
|
1041
|
+
return `${label} is installed, but its version could not be verified. Reinstall or update it: ${inspection.update_command}`;
|
|
1042
|
+
}
|
|
1043
|
+
if (inspection.status === "compatible") {
|
|
1044
|
+
return `${label} is available locally.`;
|
|
1045
|
+
}
|
|
1046
|
+
return `${label} is not available. Install or update it: ${inspection.update_command}`;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
872
1049
|
async function verifyLaunchChallenge({ apiOrigin, apiBaseUrl, token, runId, challenge }) {
|
|
873
1050
|
try {
|
|
874
1051
|
const response = await fetch(
|
|
@@ -890,6 +1067,10 @@ async function verifyLaunchChallenge({ apiOrigin, apiBaseUrl, token, runId, chal
|
|
|
890
1067
|
}
|
|
891
1068
|
}
|
|
892
1069
|
|
|
1070
|
+
function canonicalAgenticLoopPath(pathname) {
|
|
1071
|
+
return pathname.replace(/^\/v1\/coding-loop(?=\/|$)/, "/v1/agentic-loop");
|
|
1072
|
+
}
|
|
1073
|
+
|
|
893
1074
|
function streamRun(res, run) {
|
|
894
1075
|
if (!run) {
|
|
895
1076
|
json(res, 404, { error: "run not found" });
|
|
@@ -2,14 +2,67 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
|
|
3
3
|
const MACOS_CODEX_BINARY = "/Applications/ChatGPT.app/Contents/Resources/codex";
|
|
4
4
|
|
|
5
|
-
export
|
|
5
|
+
export const BACKEND_VERSION_POLICIES = Object.freeze({
|
|
6
|
+
codex: Object.freeze({
|
|
7
|
+
label: "Codex",
|
|
8
|
+
minimumVersion: "0.144.0",
|
|
9
|
+
updateCommand: "npm install -g @openai/codex@latest",
|
|
10
|
+
}),
|
|
11
|
+
claude: Object.freeze({
|
|
12
|
+
label: "Claude Code",
|
|
13
|
+
minimumVersion: "2.1.200",
|
|
14
|
+
updateCommand: "claude update",
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export async function resolveBackendExecutable(backend, options = {}) {
|
|
19
|
+
const inspection = await inspectBackendExecutable(backend, options);
|
|
20
|
+
return inspection.available ? inspection.executable : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function inspectBackendExecutable(
|
|
6
24
|
backend,
|
|
7
25
|
{ spawnImpl = spawn, candidates = backendCandidates(backend), timeoutMs = 1_500 } = {},
|
|
8
26
|
) {
|
|
27
|
+
const policy = versionPolicy(backend);
|
|
9
28
|
for (const candidate of [...new Set(candidates.filter(Boolean))]) {
|
|
10
|
-
|
|
29
|
+
const probe = await probeExecutable(candidate, { spawnImpl, timeoutMs });
|
|
30
|
+
if (!probe.available) continue;
|
|
31
|
+
const installedVersion = parseBackendVersion(probe.output);
|
|
32
|
+
if (!installedVersion) {
|
|
33
|
+
return {
|
|
34
|
+
backend,
|
|
35
|
+
executable: candidate,
|
|
36
|
+
available: true,
|
|
37
|
+
compatible: false,
|
|
38
|
+
status: "unknown_version",
|
|
39
|
+
installed_version: null,
|
|
40
|
+
minimum_version: policy.minimumVersion,
|
|
41
|
+
update_command: policy.updateCommand,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const compatible = compareVersions(installedVersion, policy.minimumVersion) >= 0;
|
|
45
|
+
return {
|
|
46
|
+
backend,
|
|
47
|
+
executable: candidate,
|
|
48
|
+
available: true,
|
|
49
|
+
compatible,
|
|
50
|
+
status: compatible ? "compatible" : "outdated",
|
|
51
|
+
installed_version: installedVersion,
|
|
52
|
+
minimum_version: policy.minimumVersion,
|
|
53
|
+
update_command: policy.updateCommand,
|
|
54
|
+
};
|
|
11
55
|
}
|
|
12
|
-
return
|
|
56
|
+
return {
|
|
57
|
+
backend,
|
|
58
|
+
executable: null,
|
|
59
|
+
available: false,
|
|
60
|
+
compatible: false,
|
|
61
|
+
status: "missing",
|
|
62
|
+
installed_version: null,
|
|
63
|
+
minimum_version: policy.minimumVersion,
|
|
64
|
+
update_command: policy.updateCommand,
|
|
65
|
+
};
|
|
13
66
|
}
|
|
14
67
|
|
|
15
68
|
export function backendCandidates(backend) {
|
|
@@ -23,16 +76,49 @@ export function backendCandidates(backend) {
|
|
|
23
76
|
];
|
|
24
77
|
}
|
|
25
78
|
|
|
26
|
-
function
|
|
79
|
+
export function parseBackendVersion(output) {
|
|
80
|
+
const match = String(output || "").match(/(?:^|\s|v)(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?=\s|$|\))/);
|
|
81
|
+
return match?.[1] ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function compareVersions(left, right) {
|
|
85
|
+
const leftParts = numericVersionParts(left);
|
|
86
|
+
const rightParts = numericVersionParts(right);
|
|
87
|
+
for (let index = 0; index < 3; index += 1) {
|
|
88
|
+
if (leftParts[index] !== rightParts[index]) return leftParts[index] - rightParts[index];
|
|
89
|
+
}
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function versionPolicy(backend) {
|
|
94
|
+
return backend === "claude" ? BACKEND_VERSION_POLICIES.claude : BACKEND_VERSION_POLICIES.codex;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function numericVersionParts(value) {
|
|
98
|
+
const match = String(value || "").match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
99
|
+
if (!match) return [0, 0, 0];
|
|
100
|
+
return match.slice(1).map(Number);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function probeExecutable(command, { spawnImpl, timeoutMs }) {
|
|
27
104
|
return new Promise((resolve) => {
|
|
28
105
|
let settled = false;
|
|
29
106
|
let timeout;
|
|
30
|
-
|
|
107
|
+
let output = "";
|
|
108
|
+
const child = spawnImpl(command, ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
109
|
+
child.stdout?.setEncoding?.("utf8");
|
|
110
|
+
child.stderr?.setEncoding?.("utf8");
|
|
111
|
+
child.stdout?.on?.("data", (chunk) => {
|
|
112
|
+
output += chunk;
|
|
113
|
+
});
|
|
114
|
+
child.stderr?.on?.("data", (chunk) => {
|
|
115
|
+
output += chunk;
|
|
116
|
+
});
|
|
31
117
|
const finish = (available) => {
|
|
32
118
|
if (settled) return;
|
|
33
119
|
settled = true;
|
|
34
120
|
if (timeout) clearTimeout(timeout);
|
|
35
|
-
resolve(available);
|
|
121
|
+
resolve({ available, output: output.trim() });
|
|
36
122
|
};
|
|
37
123
|
timeout = setTimeout(() => {
|
|
38
124
|
child.kill?.("SIGTERM");
|