@awak-app/simy-cli 0.1.2 → 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 +209 -24
- 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 +185 -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/provider-stream.js +17 -0
- package/src/repository-inventory.js +117 -0
- package/src/runner.js +128 -108
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" });
|
|
367
|
+
return;
|
|
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" });
|
|
343
381
|
return;
|
|
344
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" });
|
|
@@ -421,6 +486,10 @@ export async function startAgent({
|
|
|
421
486
|
const body = await readJson(req);
|
|
422
487
|
if (body.action === "stop") {
|
|
423
488
|
await stopLocalCodingRun(run);
|
|
489
|
+
if (run.status !== "stopped" || run.controlState !== "stopped") {
|
|
490
|
+
json(res, 500, { error: "local executor stop was not confirmed" });
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
424
493
|
} else if (body.action === "pause") {
|
|
425
494
|
pauseLocalCodingRun(run);
|
|
426
495
|
} else if (body.action === "resume") {
|
|
@@ -440,7 +509,7 @@ export async function startAgent({
|
|
|
440
509
|
return;
|
|
441
510
|
}
|
|
442
511
|
|
|
443
|
-
const guidanceMatch =
|
|
512
|
+
const guidanceMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/guidance$/);
|
|
444
513
|
if (req.method === "POST" && guidanceMatch) {
|
|
445
514
|
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
446
515
|
json(res, 401, { error: "simy session expired; run simy again" });
|
|
@@ -470,7 +539,7 @@ export async function startAgent({
|
|
|
470
539
|
return;
|
|
471
540
|
}
|
|
472
541
|
|
|
473
|
-
const hilMatch =
|
|
542
|
+
const hilMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/hil$/);
|
|
474
543
|
if (hilMatch) {
|
|
475
544
|
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
476
545
|
json(res, 401, { error: "simy session expired; run simy again" });
|
|
@@ -545,14 +614,14 @@ export async function startAgent({
|
|
|
545
614
|
repository: selected.repository,
|
|
546
615
|
local_path: selected.local_path,
|
|
547
616
|
root: scan.root,
|
|
548
|
-
state: "
|
|
617
|
+
state: "resuming",
|
|
549
618
|
});
|
|
550
619
|
void continuation.catch((error) => reportRepositoryResumeError(error, quiet));
|
|
551
620
|
return;
|
|
552
621
|
}
|
|
553
622
|
}
|
|
554
623
|
|
|
555
|
-
const recheckMatch =
|
|
624
|
+
const recheckMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/recheck$/);
|
|
556
625
|
if (req.method === "POST" && recheckMatch) {
|
|
557
626
|
const run = registry.get(decodeURIComponent(recheckMatch[1]));
|
|
558
627
|
if (!run) {
|
|
@@ -568,7 +637,7 @@ export async function startAgent({
|
|
|
568
637
|
return;
|
|
569
638
|
}
|
|
570
639
|
|
|
571
|
-
const streamMatch =
|
|
640
|
+
const streamMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/stream$/);
|
|
572
641
|
if (req.method === "GET" && streamMatch) {
|
|
573
642
|
streamRun(res, registry.get(decodeURIComponent(streamMatch[1])));
|
|
574
643
|
return;
|
|
@@ -636,6 +705,7 @@ export async function startAgent({
|
|
|
636
705
|
const requirement = String(input.requirement || "").trim();
|
|
637
706
|
const repository = String(input.repository || "").trim();
|
|
638
707
|
const backend = input.backend === "claude" ? "claude" : "codex";
|
|
708
|
+
const executionTarget = normalizeDesktopExecutionTarget(input.executionTarget);
|
|
639
709
|
if (!requirement) throw new Error("Describe the coding task first.");
|
|
640
710
|
if (!repository) throw new Error("Set a repository with /repo owner/name.");
|
|
641
711
|
if (availableCapabilities?.backends?.[backend] !== true) {
|
|
@@ -648,7 +718,11 @@ export async function startAgent({
|
|
|
648
718
|
local_path: indexedRepository?.local_path || input.localPath || workspace.localPath,
|
|
649
719
|
base_branch: String(input.baseBranch || "dev"),
|
|
650
720
|
backend,
|
|
651
|
-
|
|
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,
|
|
652
726
|
acceptance_criteria: [],
|
|
653
727
|
expected_tests: [],
|
|
654
728
|
expected_evidence: [],
|
|
@@ -667,7 +741,7 @@ export async function startAgent({
|
|
|
667
741
|
request,
|
|
668
742
|
});
|
|
669
743
|
const runId = String(remoteRun?.id || "");
|
|
670
|
-
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.");
|
|
671
745
|
const run = createRun({ runId, request, session, apiOrigin });
|
|
672
746
|
registry.create(run);
|
|
673
747
|
void startLocalCodingRun(run, runOptions);
|
|
@@ -698,6 +772,8 @@ async function createRemoteRun({ apiOrigin, apiBaseUrl, token, request }) {
|
|
|
698
772
|
backend: request.backend,
|
|
699
773
|
base_branch: request.base_branch,
|
|
700
774
|
max_attempts: request.max_attempts,
|
|
775
|
+
retry_budget: request.retry_budget,
|
|
776
|
+
token_budget: request.token_budget,
|
|
701
777
|
acceptance_criteria: request.acceptance_criteria,
|
|
702
778
|
expected_tests: request.expected_tests,
|
|
703
779
|
expected_evidence: request.expected_evidence,
|
|
@@ -808,12 +884,31 @@ function localRepositoryHilRequest(run, root, authorizedRoots) {
|
|
|
808
884
|
|
|
809
885
|
async function capabilities() {
|
|
810
886
|
const [codex, claude] = await Promise.all([
|
|
811
|
-
|
|
812
|
-
|
|
887
|
+
inspectDesktopExecutor("codex"),
|
|
888
|
+
inspectDesktopExecutor("claude"),
|
|
813
889
|
]);
|
|
814
890
|
return {
|
|
815
|
-
backends: { codex:
|
|
816
|
-
|
|
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
|
+
},
|
|
817
912
|
session_ttl_hours: 48,
|
|
818
913
|
};
|
|
819
914
|
}
|
|
@@ -822,13 +917,24 @@ async function readCapabilities(dependencies) {
|
|
|
822
917
|
return dependencies.capabilities ? dependencies.capabilities() : capabilities();
|
|
823
918
|
}
|
|
824
919
|
|
|
825
|
-
async function inspectCodingLoopEnvironment({
|
|
920
|
+
async function inspectCodingLoopEnvironment({
|
|
921
|
+
repository,
|
|
922
|
+
backend,
|
|
923
|
+
executionTarget,
|
|
924
|
+
localPath,
|
|
925
|
+
dependencies,
|
|
926
|
+
}) {
|
|
826
927
|
const checks = [
|
|
827
928
|
{
|
|
828
929
|
key: "session",
|
|
829
930
|
status: "passed",
|
|
830
931
|
summary: "Local CLI session is connected to this SIMY Web origin.",
|
|
831
932
|
},
|
|
933
|
+
{
|
|
934
|
+
key: "execution_target",
|
|
935
|
+
status: "passed",
|
|
936
|
+
summary: `This run will execute on ${DESKTOP_EXECUTION_LABEL} through SIMY CLI.`,
|
|
937
|
+
},
|
|
832
938
|
];
|
|
833
939
|
let repositoryPath = null;
|
|
834
940
|
try {
|
|
@@ -839,32 +945,107 @@ async function inspectCodingLoopEnvironment({ repository, backend, localPath, de
|
|
|
839
945
|
summary: `Verified ${repository} against the local Git origin.`,
|
|
840
946
|
});
|
|
841
947
|
} catch (error) {
|
|
948
|
+
const summary =
|
|
949
|
+
error instanceof Error ? error.message : "Local repository could not be resolved.";
|
|
842
950
|
checks.push({
|
|
843
951
|
key: "repository",
|
|
844
952
|
status: "failed",
|
|
845
|
-
summary
|
|
953
|
+
summary,
|
|
954
|
+
recovery: recoveryContractForPreflight({ key: "repository", summary }),
|
|
846
955
|
});
|
|
847
956
|
}
|
|
848
957
|
|
|
849
958
|
const available = await readCapabilities(dependencies);
|
|
850
|
-
const
|
|
959
|
+
const inspection = normalizeBackendInspection(backend, available);
|
|
960
|
+
const backendSummary = backendPreflightSummary(backend, inspection);
|
|
851
961
|
checks.push({
|
|
852
962
|
key: "backend",
|
|
853
|
-
status:
|
|
854
|
-
summary:
|
|
855
|
-
|
|
856
|
-
:
|
|
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
|
+
}),
|
|
857
980
|
});
|
|
858
981
|
|
|
859
982
|
return {
|
|
860
|
-
ready: Boolean(repositoryPath &&
|
|
983
|
+
ready: Boolean(repositoryPath && inspection.compatible),
|
|
861
984
|
repository,
|
|
862
985
|
backend,
|
|
986
|
+
execution_target: executionTarget,
|
|
863
987
|
local_path: repositoryPath,
|
|
864
988
|
checks,
|
|
865
989
|
};
|
|
866
990
|
}
|
|
867
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
|
+
|
|
868
1049
|
async function verifyLaunchChallenge({ apiOrigin, apiBaseUrl, token, runId, challenge }) {
|
|
869
1050
|
try {
|
|
870
1051
|
const response = await fetch(
|
|
@@ -886,6 +1067,10 @@ async function verifyLaunchChallenge({ apiOrigin, apiBaseUrl, token, runId, chal
|
|
|
886
1067
|
}
|
|
887
1068
|
}
|
|
888
1069
|
|
|
1070
|
+
function canonicalAgenticLoopPath(pathname) {
|
|
1071
|
+
return pathname.replace(/^\/v1\/coding-loop(?=\/|$)/, "/v1/agentic-loop");
|
|
1072
|
+
}
|
|
1073
|
+
|
|
889
1074
|
function streamRun(res, run) {
|
|
890
1075
|
if (!run) {
|
|
891
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");
|