@awak-app/simy-cli 0.5.21 → 0.5.22

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 CHANGED
@@ -32,8 +32,8 @@ simy --daemon
32
32
  ```
33
33
 
34
34
  When an interactive `simy --daemon` starts without managed SIMY workflows,
35
- the CLI asks once before installing the Codex plugin
36
- marketplace and Codex/Claude opt-in SQM skills. Existing unrelated plugins
35
+ the CLI asks once before installing the Codex and Claude Code plugin
36
+ sources and Codex/Claude opt-in SQM skills. Existing unrelated plugins
37
37
  and hooks are preserved. Use `--install-workflows` for explicit non-interactive
38
38
  installation or `--no-workflow-prompt` to start the daemon without asking.
39
39
 
@@ -49,10 +49,24 @@ period in each Codex session, then rearms after the CLI reconnects. The hook doe
49
49
  not start or install the CLI, read session tokens, enable SQM, or send email.
50
50
  Review and enable the plugin hooks through Codex's `/hooks` command.
51
51
 
52
+ For Claude Code, register the generated local marketplace and explicitly enable SIMY:
53
+
54
+ ```sh
55
+ claude plugin marketplace add ~/.simy/integrations/claude-marketplace
56
+ claude plugin install simy@simy-local
57
+ ```
58
+
59
+ Use `/simy:sqm-loop` or `/simy:pr-review` in Claude Code. The workflows are generated
60
+ from the same maintained source as Codex; only provider terminology and hook/manifest
61
+ format differ. Installing source files does not imply that either provider has enabled
62
+ the plugin. Existing Claude settings, unrelated marketplaces, and plugins are preserved.
63
+
52
64
  After that first installation, the daemon checks installed workflows at startup
53
65
  and every hour, including after a CLI automatic upgrade. It updates only existing
54
66
  platform installations and refreshes an already enabled SIMY Codex plugin through
55
- `codex plugin add`. Plugin versions include a content digest so changed bundles
67
+ `codex plugin add`. Enabled user-scope Claude plugins are refreshed with
68
+ `claude plugin update simy@simy-local --scope user`, after verifying that the
69
+ marketplace still points to SIMY’s generated local source. Plugin versions include a content digest so changed bundles
56
70
  receive a fresh cache. Use a new Codex task to pick up updated skills; running
57
71
  tasks are not restarted. Missing or disabled plugins are not enabled automatically.
58
72
  Failed refreshes retry on the next check without blocking daemon startup.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.5.21",
3
+ "version": "0.5.22",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,13 +39,15 @@
39
39
  "check": "npm run check:auto-update && node --check ./src/desktop-executor.js && node --check ./src/local-task.js && node --check ./scripts/desktop-executor-e2e.js && node --check ./scripts/pre-guardrail-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",
40
40
  "check:auto-update": "node --check ./src/auto-update.js && node --check ./scripts/auto-update-e2e.js && npm run check:sqm",
41
41
  "check:sqm": "node --check ./src/sqm/canonical.js && node --check ./src/sqm/validation.js && node --check ./src/sqm/reliability.js && node --check ./src/sqm/repository.js && node --check ./src/sqm/checkers.js && node --check ./src/sqm/bundle-store.js && node --check ./src/sqm/proof.js && node --check ./src/sqm/evidence-client.js && node --check ./src/sqm/session.js && node --check ./src/sqm/index.js && node --check ./src/sqm/command.js && node --check ./src/sqm/agent-skill.js && node --check ./src/sqm/workflow-onboarding.js && node --check ./src/sqm/workflow-update.js && node --check ./src/sqm/review-worker.js && node --check ./scripts/sqm-complete-loop-e2e.js && node --check ./scripts/sqm-local-review-e2e.js",
42
- "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",
42
+ "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 && node --check ./scripts/claude-parity-runtime-e2e.js && node --check ./scripts/claude-plugin-runtime-e2e.js",
43
43
  "check:syntax": "node --check ./src/index.js && node --check ./src/install-update.js && node --check ./src/agent.js && node --check ./src/connection-discovery.js && node --check ./src/codex-plugins/simy/hooks/connection-reminder.mjs && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/durable-local-task-worker.js && node --check ./src/durable-local-task-steps-contract.js && node --check ./src/bounded-local-task-subtasks-contract.js && node --check ./src/bounded-local-task-subtask-pool.js && node --check ./src/local-task-artifact-contract.js && node --check ./src/execution-capability-contract.js && node --check ./src/shutdown.js && node --check ./src/run-registry.js && node --check ./src/direct-executor.js && node --check ./src/execution-guardrail.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/completion-contract.js && node --check ./src/orchestrator/pr-preparation.js && node --check ./src/orchestrator/minimum-completion.js && node --check ./src/orchestrator/delivery/scripts/check-minimum-completion.mjs && 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/completion-contract-fixture.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/completion-contract-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/local-task-default-workspace-e2e.js && node --check ./scripts/local-task-idempotency-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/console-recovery-e2e-fixture.js && node --check ./scripts/console-recovery-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",
44
44
  "check:package": "node ./scripts/check-package-contents.js",
45
45
  "build:macos-installer:dev": "bash packaging/build-macos-arm64.sh --unsigned-dev",
46
46
  "build:macos-installer": "bash packaging/build-macos-arm64.sh --production",
47
47
  "test:macos-installer": "node --test packaging/test-macos-packaging.mjs",
48
- "build:macos-installer:production": "bash packaging/build-macos-arm64.sh --production"
48
+ "build:macos-installer:production": "bash packaging/build-macos-arm64.sh --production",
49
+ "test:e2e:claude-parity": "node ./scripts/claude-parity-runtime-e2e.js",
50
+ "test:e2e:claude-plugin": "node ./scripts/claude-plugin-runtime-e2e.js"
49
51
  },
50
52
  "engines": {
51
53
  "node": ">=20"
package/src/agent.js CHANGED
@@ -50,6 +50,7 @@ import {
50
50
  } from "./desktop-executor.js";
51
51
  import {
52
52
  createCodexStatusInspector,
53
+ createProviderStatusInspector,
53
54
  createConnectionStatusTracker,
54
55
  desktopAuthStatus,
55
56
  desktopTaskStatus,
@@ -187,46 +188,11 @@ export async function startAgent({
187
188
  : [],
188
189
  );
189
190
  const installMode = updateManager?.snapshot?.()?.install_mode ?? null;
190
- const inspectedCapabilities = await readCapabilities(dependencies);
191
- const durableControlPlane =
192
- inspectedCapabilities?.features?.local_task_control_plane === true;
193
- const hasSqmLocalReview = Object.hasOwn(
194
- inspectedCapabilities?.features || {},
195
- "sqm_local_review",
196
- );
197
- const contractSourceCapabilities =
198
- durableControlPlane || hasSqmLocalReview
199
- ? {
200
- ...inspectedCapabilities,
201
- features: {
202
- ...inspectedCapabilities?.features,
203
- ...(durableControlPlane
204
- ? {
205
- local_task_control_plane: daemon,
206
- pipeline_local_task_artifacts:
207
- daemon &&
208
- inspectedCapabilities.features.pipeline_local_task_artifacts === true,
209
- durable_local_task_attachments: daemon,
210
- durable_local_task_steps:
211
- daemon &&
212
- inspectedCapabilities.features.durable_local_task_steps === true,
213
- bounded_local_task_subtasks:
214
- daemon &&
215
- inspectedCapabilities.features.bounded_local_task_subtasks === true,
216
- }
217
- : {}),
218
- ...(hasSqmLocalReview
219
- ? {
220
- sqm_local_review:
221
- daemon && inspectedCapabilities?.features?.sqm_local_review === true,
222
- }
223
- : {}),
224
- },
225
- }
226
- : inspectedCapabilities;
191
+ let contractSourceCapabilities = daemonCapabilities(await readCapabilities(dependencies), daemon);
227
192
  let availableCapabilities;
193
+ let providerStatuses = { codex: { state: "unknown" }, claude: { state: "unknown" } };
228
194
  const refreshAvailableCapabilities = () => {
229
- availableCapabilities = withCliContract(contractSourceCapabilities, installMode, {
195
+ availableCapabilities = withCliContract({ ...contractSourceCapabilities, ...(providerStatuses ? { provider_statuses: providerStatuses } : {}) }, installMode, {
230
196
  authenticated:
231
197
  isSessionValid(session, Date.now(), apiOrigin) && Boolean(session?.device_id),
232
198
  });
@@ -255,6 +221,46 @@ export async function startAgent({
255
221
  now: dependencies.statusNowMs,
256
222
  });
257
223
 
224
+ const claudeStatus = createProviderStatusInspector("claude", {
225
+ inspect: dependencies.inspectClaudeStatus,
226
+ now: dependencies.statusNowMs,
227
+ });
228
+
229
+ const readProviderStatuses = async ({ force = false } = {}) => {
230
+ const [codex, claude] = await Promise.all([codexStatus({ force }), claudeStatus({ force })]);
231
+ const binaryStateChanged = ["codex", "claude"].some((provider) => {
232
+ const next = provider === "codex" ? codex : claude;
233
+ return providerStatuses[provider]?.version !== next.version ||
234
+ providerStatuses[provider]?.state !== next.state;
235
+ });
236
+ if (force || binaryStateChanged) {
237
+ contractSourceCapabilities = daemonCapabilities(await readCapabilities(dependencies), daemon);
238
+ }
239
+ providerStatuses = { codex, claude };
240
+ refreshAvailableCapabilities();
241
+ return providerStatuses;
242
+ };
243
+
244
+ const inspectReadyBackend = async (backend, options) => {
245
+ await readProviderStatuses(options);
246
+ const inspection = normalizeBackendInspection(backend, availableCapabilities);
247
+ if (inspection.compatible) {
248
+ const failure = providerAuthenticationFailure(backend, providerStatuses[backend]);
249
+ if (failure) throw Object.assign(new Error(failure.summary), failure);
250
+ }
251
+ return inspection;
252
+ };
253
+
254
+ const requireRunProvider = async (run) => {
255
+ const backend = run.request.backend;
256
+ const inspection = await inspectReadyBackend(backend, { force: true });
257
+ if (!inspection.compatible) {
258
+ throw Object.assign(new Error(backendPreflightSummary(backend, inspection)), {
259
+ code: `desktop_executor_${inspection.status}`,
260
+ });
261
+ }
262
+ };
263
+
258
264
  const requestDesktopLogin = () => {
259
265
  if (isSessionValid(session, Date.now(), apiOrigin)) return null;
260
266
  if (!loginUrl) {
@@ -300,6 +306,7 @@ export async function startAgent({
300
306
  if (heartbeatInFlight) return heartbeatInFlight;
301
307
  connectionStatus.attempt();
302
308
  let deviceHeartbeatSucceeded = false;
309
+ const advertisedProviders = JSON.stringify(providerStatuses);
303
310
  heartbeatInFlight = Promise.resolve()
304
311
  .then(() =>
305
312
  heartbeatDevice({
@@ -314,6 +321,13 @@ export async function startAgent({
314
321
  .then(async () => {
315
322
  deviceHeartbeatSucceeded = true;
316
323
  connectionStatus.success();
324
+ // Register the device immediately; provider subprocesses must not delay
325
+ // browser authorization. Publish verified readiness after their probe.
326
+ await readProviderStatuses();
327
+ if (JSON.stringify(providerStatuses) !== advertisedProviders) {
328
+ await heartbeatDevice({ apiOrigin, apiBaseUrl: session.api_base_url, token: session.token,
329
+ port, capabilities: refreshAvailableCapabilities(), repoInventory: repositoryInventory });
330
+ }
317
331
  const sqmResult = sqmLocalReviewWorker
318
332
  ? await sqmLocalReviewWorker.poll()
319
333
  : null;
@@ -429,7 +443,15 @@ export async function startAgent({
429
443
  storageRoot: taskStorageRoot,
430
444
  workspaceRoot: dependencies.localTaskWorkspaceRoot,
431
445
  resolveRepository: ensureRepositoryIndexed,
432
- runLocalTask: dependencies.runLocalTask || startLocalTask,
446
+ runLocalTask: async (task, options) => {
447
+ const inspection = await inspectReadyBackend(task.backend);
448
+ if (!inspection.compatible) {
449
+ throw Object.assign(new Error(backendPreflightSummary(task.backend, inspection)), {
450
+ code: `desktop_executor_${inspection.status}`,
451
+ });
452
+ }
453
+ return (dependencies.runLocalTask || startLocalTask)(task, options);
454
+ },
433
455
  attachmentStore: localTaskAttachmentStore,
434
456
  getDeviceId: () => session?.device_id || null,
435
457
  beginWork: () => acquireUpdateWork(updateManager),
@@ -437,7 +459,7 @@ export async function startAgent({
437
459
  });
438
460
  }
439
461
 
440
- if (daemon && availableCapabilities?.features?.sqm_local_review === true) {
462
+ if (daemon && Object.hasOwn(availableCapabilities?.features || {}, "sqm_local_review")) {
441
463
  const sqmApi =
442
464
  dependencies.sqmLocalReviewApi ||
443
465
  createSqmLocalReviewApi({
@@ -454,6 +476,10 @@ export async function startAgent({
454
476
  api: sqmApi,
455
477
  beginWork: () => acquireUpdateWork(updateManager),
456
478
  runReview: dependencies.runSqmLocalReview,
479
+ getProvider: () => ["codex", "claude"].find(
480
+ (provider) => availableCapabilities?.backends?.[provider] === true &&
481
+ providerStatuses[provider]?.state === "ready",
482
+ ) || null,
457
483
  onError: (error) => reportSqmLocalReviewError(error, quiet),
458
484
  });
459
485
  }
@@ -472,6 +498,7 @@ export async function startAgent({
472
498
  throw new Error(`This run requires ${run.request.repository}; select that local repository.`);
473
499
  }
474
500
  if (!run) return selected;
501
+ await requireRunProvider(run);
475
502
  return withUpdateWork(
476
503
  updateManager,
477
504
  () =>
@@ -659,6 +686,7 @@ export async function startAgent({
659
686
  ? body.local_path
660
687
  : indexedRepository?.local_path || null,
661
688
  dependencies,
689
+ providerStatus: (await readProviderStatuses())[backend],
662
690
  });
663
691
  json(res, 200, environment);
664
692
  return;
@@ -904,6 +932,7 @@ export async function startAgent({
904
932
  requestFingerprint = createLocalTaskRequestFingerprint({
905
933
  instruction,
906
934
  backend: body.backend,
935
+ model: body.model,
907
936
  operation: guardrail.operation,
908
937
  permissionMode: guardrail.permission_mode,
909
938
  workspaceMode,
@@ -965,7 +994,7 @@ export async function startAgent({
965
994
  json(res, 503, updateInProgressResponse(updateManager));
966
995
  return;
967
996
  }
968
- const inspection = normalizeBackendInspection(body.backend, availableCapabilities);
997
+ const inspection = await inspectReadyBackend(body.backend);
969
998
  if (!inspection.compatible) {
970
999
  const summary = backendPreflightSummary(body.backend, inspection);
971
1000
  json(res, 409, {
@@ -1020,6 +1049,7 @@ export async function startAgent({
1020
1049
  id: workspace.id,
1021
1050
  instruction,
1022
1051
  backend: body.backend,
1052
+ model: body.model,
1023
1053
  operation: guardrail.operation,
1024
1054
  permissionMode: guardrail.permission_mode,
1025
1055
  workspaceMode,
@@ -1127,7 +1157,7 @@ export async function startAgent({
1127
1157
  });
1128
1158
  return;
1129
1159
  }
1130
- const inspection = normalizeBackendInspection(body.backend, availableCapabilities);
1160
+ const inspection = await inspectReadyBackend(body.backend);
1131
1161
  if (!inspection.compatible) {
1132
1162
  const summary = backendPreflightSummary(body.backend, inspection);
1133
1163
  json(res, 409, {
@@ -1242,22 +1272,20 @@ export async function startAgent({
1242
1272
  json(res, 400, { error: "backend must be codex or claude" });
1243
1273
  return;
1244
1274
  }
1245
- if (body.execution_target != null) {
1246
- const inspection = normalizeBackendInspection(body.backend, availableCapabilities);
1247
- if (!inspection.compatible) {
1248
- const summary = backendPreflightSummary(body.backend, inspection);
1249
- json(res, 409, {
1250
- error: summary,
1251
- code: `desktop_executor_${inspection.status}`,
1252
- execution_target: executionTarget,
1253
- recovery: recoveryContractForPreflight({
1254
- key: "backend",
1255
- summary,
1256
- details: inspection,
1257
- }),
1258
- });
1259
- return;
1260
- }
1275
+ const inspection = await inspectReadyBackend(body.backend);
1276
+ if (!inspection.compatible) {
1277
+ const summary = backendPreflightSummary(body.backend, inspection);
1278
+ json(res, 409, {
1279
+ error: summary,
1280
+ code: `desktop_executor_${inspection.status}`,
1281
+ execution_target: executionTarget,
1282
+ recovery: recoveryContractForPreflight({
1283
+ key: "backend",
1284
+ summary,
1285
+ details: inspection,
1286
+ }),
1287
+ });
1288
+ return;
1261
1289
  }
1262
1290
  let stagedAttachments = [];
1263
1291
  try {
@@ -1359,6 +1387,7 @@ export async function startAgent({
1359
1387
  json(res, 503, updateInProgressResponse(updateManager));
1360
1388
  return;
1361
1389
  }
1390
+ await requireRunProvider(run);
1362
1391
  resumeLocalCodingRun(run);
1363
1392
  } else {
1364
1393
  json(res, 400, { error: "action must be pause, resume, or stop" });
@@ -1393,6 +1422,7 @@ export async function startAgent({
1393
1422
  return;
1394
1423
  }
1395
1424
  const body = await readJson(req);
1425
+ await requireRunProvider(run);
1396
1426
  let budget;
1397
1427
  try {
1398
1428
  budget = await increaseLocalCodingRunProviderTokenBudget(
@@ -1518,6 +1548,7 @@ export async function startAgent({
1518
1548
  return;
1519
1549
  }
1520
1550
 
1551
+ await requireRunProvider(run);
1521
1552
  const continuation = continueLocalCodingRunAfterRepositoryApproval(
1522
1553
  run,
1523
1554
  { repository: selected.repository, localPath: selected.local_path },
@@ -1566,6 +1597,11 @@ export async function startAgent({
1566
1597
 
1567
1598
  json(res, 404, { error: "not found" });
1568
1599
  } catch (err) {
1600
+ if (err?.code === "provider_login_required" || err?.code === "provider_status_unknown" ||
1601
+ (typeof err?.code === "string" && err.code.startsWith("desktop_executor_"))) {
1602
+ json(res, 409, { error: err.message, code: err.code, recovery: err.recovery });
1603
+ return;
1604
+ }
1569
1605
  if (err instanceof LocalTaskFileCapabilityError) {
1570
1606
  json(res, 400, localTaskFileErrorPayload(err));
1571
1607
  return;
@@ -1634,6 +1670,7 @@ export async function startAgent({
1634
1670
  });
1635
1671
  const getDesktopStatus = async () => {
1636
1672
  const auth = desktopAuthStatus(session, { webOrigin: apiOrigin });
1673
+ const { codex, claude } = await readProviderStatuses();
1637
1674
  return {
1638
1675
  schemaVersion: 1,
1639
1676
  sampledAt: new Date().toISOString(),
@@ -1643,7 +1680,9 @@ export async function startAgent({
1643
1680
  connection: connectionStatus.snapshot({ authenticated: auth.state === "signed_in" }),
1644
1681
  tasks: desktopTaskStatus([registry, directRegistry, localTaskRegistry]),
1645
1682
  update: updateManager?.snapshot?.() ?? null,
1646
- codex: await codexStatus(),
1683
+ codex,
1684
+ claude,
1685
+ providers: { codex, claude },
1647
1686
  loginUrl: auth.state === "signed_in" ? null : requestDesktopLogin(),
1648
1687
  };
1649
1688
  };
@@ -1670,6 +1709,7 @@ export async function startAgent({
1670
1709
  return getDesktopStatus();
1671
1710
  },
1672
1711
  refreshCodexStatus: () => codexStatus({ force: true }),
1712
+ refreshProviderStatus: () => readProviderStatuses({ force: true }),
1673
1713
  workspace,
1674
1714
  repositoryScanRoot,
1675
1715
  get capabilities() {
@@ -1690,7 +1730,7 @@ export async function startAgent({
1690
1730
  const executionTarget = normalizeDesktopExecutionTarget(input.executionTarget);
1691
1731
  if (!requirement) throw new Error("Describe the coding task first.");
1692
1732
  if (!repository) throw new Error("Set a repository with /repo owner/name.");
1693
- if (availableCapabilities?.backends?.[backend] !== true) {
1733
+ if (!(await inspectReadyBackend(backend)).compatible) {
1694
1734
  throw new Error(`${backend === "claude" ? "Claude Code" : "Codex"} is not available on PATH.`);
1695
1735
  }
1696
1736
  const indexedRepository = await ensureRepositoryIndexed(repository);
@@ -1730,12 +1770,21 @@ export async function startAgent({
1730
1770
  return run;
1731
1771
  }),
1732
1772
  continue: (run, guidance) =>
1733
- withUpdateWork(updateManager, () => continueLocalCodingRun(run, guidance, runOptions)),
1773
+ withUpdateWork(updateManager, async () => {
1774
+ await requireRunProvider(run);
1775
+ return continueLocalCodingRun(run, guidance, runOptions);
1776
+ }),
1734
1777
  applyDecision: (run, decision) =>
1735
- withUpdateWork(updateManager, () => applyLocalHumanDecision(run, decision, runOptions)),
1778
+ withUpdateWork(updateManager, async () => {
1779
+ await requireRunProvider(run);
1780
+ return applyLocalHumanDecision(run, decision, runOptions);
1781
+ }),
1736
1782
  queueGuidance: queueLocalCodingGuidance,
1737
1783
  pause: pauseLocalCodingRun,
1738
- resume: (run) => withUpdateWorkSync(updateManager, () => resumeLocalCodingRun(run)),
1784
+ resume: (run) => withUpdateWork(updateManager, async () => {
1785
+ await requireRunProvider(run);
1786
+ return resumeLocalCodingRun(run);
1787
+ }),
1739
1788
  stop: stopLocalCodingRun,
1740
1789
  recheck: (run) =>
1741
1790
  withUpdateWork(updateManager, () =>
@@ -1772,16 +1821,6 @@ async function withUpdateWork(updateManager, action) {
1772
1821
  }
1773
1822
  }
1774
1823
 
1775
- function withUpdateWorkSync(updateManager, action) {
1776
- const release = acquireUpdateWork(updateManager);
1777
- if (!release) ensureAcceptingNewWork(updateManager);
1778
- try {
1779
- return action();
1780
- } finally {
1781
- release?.();
1782
- }
1783
- }
1784
-
1785
1824
  function updateInProgressResponse(updateManager) {
1786
1825
  return {
1787
1826
  error:
@@ -1983,15 +2022,17 @@ async function capabilities() {
1983
2022
  local_task_artifacts: true,
1984
2023
  local_task_streaming: true,
1985
2024
  local_task_idempotency: true,
2025
+ local_task_model: true,
1986
2026
  local_task_reconciliation: true,
1987
2027
  local_task_control_plane: true,
1988
2028
  pipeline_local_task_artifacts: true,
1989
2029
  durable_local_task_attachments: true,
1990
2030
  durable_local_task_scenario_packs: true,
1991
2031
  durable_local_task_steps: true,
2032
+ durable_local_task_step_model: true,
1992
2033
  bounded_local_task_subtasks: true,
1993
2034
  sqm_agent_skill_management: true,
1994
- sqm_local_review: codex.compatible,
2035
+ sqm_local_review: codex.compatible || claude.compatible,
1995
2036
  },
1996
2037
  session_ttl_hours: 48,
1997
2038
  };
@@ -2007,6 +2048,7 @@ async function inspectCodingLoopEnvironment({
2007
2048
  executionTarget,
2008
2049
  localPath,
2009
2050
  dependencies,
2051
+ providerStatus,
2010
2052
  }) {
2011
2053
  const checks = [
2012
2054
  {
@@ -2063,8 +2105,17 @@ async function inspectCodingLoopEnvironment({
2063
2105
  }),
2064
2106
  });
2065
2107
 
2108
+ const authFailure = inspection.compatible ? providerAuthenticationFailure(backend, providerStatus) : null;
2109
+ if (inspection.compatible) {
2110
+ checks.push({
2111
+ key: "provider_auth",
2112
+ status: authFailure ? "failed" : "passed",
2113
+ summary: authFailure?.summary || `${backend === "claude" ? "Claude Code" : "Codex"} is signed in.`,
2114
+ ...(authFailure ? { recovery: authFailure.recovery } : {}),
2115
+ });
2116
+ }
2066
2117
  return {
2067
- ready: Boolean(repositoryPath && inspection.compatible),
2118
+ ready: Boolean(repositoryPath && inspection.compatible && !authFailure),
2068
2119
  repository,
2069
2120
  backend,
2070
2121
  execution_target: executionTarget,
@@ -2332,3 +2383,61 @@ function multipartTooLargeError() {
2332
2383
  },
2333
2384
  );
2334
2385
  }
2386
+
2387
+ function daemonCapabilities(inspectedCapabilities, daemon) {
2388
+ const durableControlPlane =
2389
+ inspectedCapabilities?.features?.local_task_control_plane === true;
2390
+ const hasSqmLocalReview = Object.hasOwn(
2391
+ inspectedCapabilities?.features || {},
2392
+ "sqm_local_review",
2393
+ );
2394
+ return durableControlPlane || hasSqmLocalReview
2395
+ ? {
2396
+ ...inspectedCapabilities,
2397
+ features: {
2398
+ ...inspectedCapabilities?.features,
2399
+ ...(durableControlPlane
2400
+ ? {
2401
+ local_task_control_plane: daemon,
2402
+ pipeline_local_task_artifacts:
2403
+ daemon &&
2404
+ inspectedCapabilities.features.pipeline_local_task_artifacts === true,
2405
+ durable_local_task_attachments: daemon,
2406
+ durable_local_task_steps:
2407
+ daemon &&
2408
+ inspectedCapabilities.features.durable_local_task_steps === true,
2409
+ durable_local_task_step_model:
2410
+ daemon &&
2411
+ inspectedCapabilities.features.durable_local_task_step_model === true,
2412
+ bounded_local_task_subtasks:
2413
+ daemon &&
2414
+ inspectedCapabilities.features.bounded_local_task_subtasks === true,
2415
+ }
2416
+ : {}),
2417
+ ...(hasSqmLocalReview
2418
+ ? {
2419
+ sqm_local_review:
2420
+ daemon && inspectedCapabilities?.features?.sqm_local_review === true,
2421
+ }
2422
+ : {}),
2423
+ },
2424
+ }
2425
+ : inspectedCapabilities;
2426
+ }
2427
+
2428
+ function providerAuthenticationFailure(provider, status) {
2429
+ if (status?.state === "ready") return null;
2430
+ const label = provider === "claude" ? "Claude Code" : "Codex";
2431
+ const loginRequired = status?.state === "signed_out";
2432
+ const code = loginRequired ? "provider_login_required" : "provider_status_unknown";
2433
+ const summary = loginRequired
2434
+ ? `Sign in to ${label} before starting this task.`
2435
+ : `SIMY could not confirm ${label} authentication. Refresh the connection status before starting this task.`;
2436
+ return {
2437
+ code, summary,
2438
+ recovery: recoveryContractForPreflight({
2439
+ key: "provider_auth", summary,
2440
+ details: { provider, status: status?.state || "unknown" },
2441
+ }),
2442
+ };
2443
+ }
@@ -1,4 +1,6 @@
1
1
  import spawn from "cross-spawn";
2
+ import os from "node:os";
3
+ import path from "node:path";
2
4
 
3
5
  const MACOS_CODEX_BINARY = "/Applications/ChatGPT.app/Contents/Resources/codex";
4
6
 
@@ -22,7 +24,7 @@ export async function resolveBackendExecutable(backend, options = {}) {
22
24
 
23
25
  export async function inspectBackendExecutable(
24
26
  backend,
25
- { spawnImpl = spawn, candidates = backendCandidates(backend), timeoutMs = 1_500 } = {},
27
+ { spawnImpl = spawn, candidates = backendCandidates(backend), timeoutMs = 5_000 } = {},
26
28
  ) {
27
29
  const policy = versionPolicy(backend);
28
30
  for (const candidate of [...new Set(candidates.filter(Boolean))]) {
@@ -65,14 +67,26 @@ export async function inspectBackendExecutable(
65
67
  };
66
68
  }
67
69
 
68
- export function backendCandidates(backend) {
70
+ export function backendCandidates(backend, {
71
+ platform = process.platform,
72
+ environment = process.env,
73
+ home = os.homedir(),
74
+ } = {}) {
69
75
  if (backend === "claude") {
70
- return [process.env.SIMY_CLAUDE_BIN, "claude"];
76
+ const paths = platform === "win32" ? path.win32 : path.posix;
77
+ const userHome = platform === "win32" ? environment.USERPROFILE || home : home;
78
+ return [
79
+ environment.SIMY_CLAUDE_BIN,
80
+ "claude",
81
+ // Desktop launch environments may predate the installer's PATH update.
82
+ paths.join(userHome, ".local", "bin", platform === "win32" ? "claude.exe" : "claude"),
83
+ ...(platform === "darwin" ? ["/opt/homebrew/bin/claude", "/usr/local/bin/claude"] : []),
84
+ ];
71
85
  }
72
86
  return [
73
- process.env.SIMY_CODEX_BIN,
87
+ environment.SIMY_CODEX_BIN,
74
88
  "codex",
75
- ...(process.platform === "darwin" ? [MACOS_CODEX_BINARY] : []),
89
+ ...(platform === "darwin" ? [MACOS_CODEX_BINARY] : []),
76
90
  ];
77
91
  }
78
92
 
@@ -13,7 +13,7 @@ import {
13
13
  } from "./durable-local-task-steps-contract.js";
14
14
  import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
15
15
 
16
- export const CLI_VERSION = "0.5.21";
16
+ export const CLI_VERSION = "0.5.22";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function withCliContract(
@@ -43,6 +43,7 @@ export function withCliContract(
43
43
  installMode,
44
44
  backends: capabilities.backends,
45
45
  backendVersions: capabilities.backend_versions,
46
+ providerStatuses: capabilities.provider_statuses,
46
47
  authenticated,
47
48
  }),
48
49
  ...(installMode ? { install_mode: installMode } : {}),
@@ -19,7 +19,7 @@ export async function runConnectionReminder({
19
19
  } = {}) {
20
20
  const explicitWebOrigin = resolveExplicitWebOrigin(env);
21
21
  const sessionRoot = env.SIMY_HOME?.trim() || join(homedir(), ".simy");
22
- const pluginData = env.PLUGIN_DATA?.trim() || join(sessionRoot, "plugin-data", "simy");
22
+ const pluginData = env.PLUGIN_DATA?.trim() || env.CLAUDE_PLUGIN_DATA?.trim() || join(sessionRoot, "plugin-data", "simy");
23
23
  const sessionId = String(input.session_id || input.sessionId || env.CODEX_THREAD_ID || "unknown");
24
24
  const statePath = reminderStatePath(pluginData, explicitWebOrigin || "any-origin", sessionId);
25
25
  const connected = explicitWebOrigin
@@ -215,7 +215,7 @@ export function AgenticLoopConsole({ agent, onQuit = () => {} }) {
215
215
  setNotice("Executor paused.");
216
216
  break;
217
217
  case "resume":
218
- agent.controls.resume(selectedRun);
218
+ await agent.controls.resume(selectedRun);
219
219
  setNotice("Executor resumed.");
220
220
  break;
221
221
  case "stop":