@awak-app/simy-cli 0.5.22 → 0.5.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.5.22",
3
+ "version": "0.5.25",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/agent.js CHANGED
@@ -141,6 +141,8 @@ export async function startAgent({
141
141
  webOrigin = null,
142
142
  sessionRoot,
143
143
  updateManager = null,
144
+ deferStartupSynchronization = false,
145
+ onDesktopStatusChange = null,
144
146
  dependencies = {},
145
147
  quiet = false,
146
148
  } = {}) {
@@ -188,7 +190,10 @@ export async function startAgent({
188
190
  : [],
189
191
  );
190
192
  const installMode = updateManager?.snapshot?.()?.install_mode ?? null;
191
- let contractSourceCapabilities = daemonCapabilities(await readCapabilities(dependencies), daemon);
193
+ let contractSourceCapabilities = daemonCapabilities(
194
+ deferStartupSynchronization ? unverifiedCapabilities() : await readCapabilities(dependencies),
195
+ daemon,
196
+ );
192
197
  let availableCapabilities;
193
198
  let providerStatuses = { codex: { state: "unknown" }, claude: { state: "unknown" } };
194
199
  const refreshAvailableCapabilities = () => {
@@ -212,6 +217,11 @@ export async function startAgent({
212
217
  let port = requestedPort;
213
218
  let heartbeatTimer = null;
214
219
  let heartbeatInFlight = null;
220
+ let shuttingDown = false;
221
+ const notifyDesktopStatusChange = () => {
222
+ if (shuttingDown) return;
223
+ try { onDesktopStatusChange?.(); } catch { /* Desktop status polling remains available. */ }
224
+ };
215
225
  let durableLocalTaskWorker = null;
216
226
  let sqmLocalReviewWorker = null;
217
227
  let loginUrl = null;
@@ -226,19 +236,31 @@ export async function startAgent({
226
236
  now: dependencies.statusNowMs,
227
237
  });
228
238
 
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;
239
+ let providerReadInFlight = null;
240
+ const readProviderStatuses = ({ force = false } = {}) => {
241
+ if (providerReadInFlight) return providerReadInFlight;
242
+ providerReadInFlight = (async () => {
243
+ const previous = providerStatuses;
244
+ await Promise.all(["codex", "claude"].map(async (provider) => {
245
+ const next = await (provider === "codex" ? codexStatus : claudeStatus)({ force });
246
+ // Publish each verified result independently. A slow Claude probe must
247
+ // not keep a completed Codex result hidden (or vice versa).
248
+ providerStatuses = { ...providerStatuses, [provider]: next };
249
+ refreshAvailableCapabilities();
250
+ if (previous[provider]?.version !== next.version || previous[provider]?.state !== next.state) {
251
+ notifyDesktopStatusChange();
252
+ }
253
+ }));
254
+ const binaryStateChanged = ["codex", "claude"].some((provider) =>
255
+ previous[provider]?.version !== providerStatuses[provider]?.version ||
256
+ previous[provider]?.state !== providerStatuses[provider]?.state);
257
+ if (!shuttingDown && (force || binaryStateChanged)) {
258
+ contractSourceCapabilities = daemonCapabilities(await readCapabilities(dependencies), daemon);
259
+ refreshAvailableCapabilities();
260
+ }
261
+ return providerStatuses;
262
+ })().finally(() => { providerReadInFlight = null; });
263
+ return providerReadInFlight;
242
264
  };
243
265
 
244
266
  const inspectReadyBackend = async (backend, options) => {
@@ -275,7 +297,7 @@ export async function startAgent({
275
297
  };
276
298
 
277
299
  const restoreRemoteRuns = async () => {
278
- if (!isSessionValid(session, Date.now(), apiOrigin) || !session?.device_id) return;
300
+ if (shuttingDown || !isSessionValid(session, Date.now(), apiOrigin) || !session?.device_id) return;
279
301
  const snapshots = dependencies.listRemoteRuns
280
302
  ? await dependencies.listRemoteRuns({
281
303
  apiOrigin,
@@ -287,7 +309,9 @@ export async function startAgent({
287
309
  apiBaseUrl: session.api_base_url,
288
310
  token: session.token,
289
311
  });
312
+ let restored = false;
290
313
  for (const snapshot of snapshots) {
314
+ if (shuttingDown) return;
291
315
  if (!snapshot?.id || registry.has(snapshot.id)) continue;
292
316
  const repository = findRepository(repositoryInventory, snapshot.charter?.repository);
293
317
  const restoredRun = restoreRun({
@@ -297,14 +321,17 @@ export async function startAgent({
297
321
  localPath: repository?.local_path || null,
298
322
  });
299
323
  registry.create(restoredRun);
324
+ restored = true;
300
325
  await publishRestoredLocalCodingRun(restoredRun);
301
326
  }
327
+ if (restored) notifyDesktopStatusChange();
302
328
  };
303
329
 
304
330
  const heartbeat = () => {
305
- if (!isSessionValid(session, Date.now(), apiOrigin)) return Promise.resolve(false);
331
+ if (shuttingDown || !isSessionValid(session, Date.now(), apiOrigin)) return Promise.resolve(false);
306
332
  if (heartbeatInFlight) return heartbeatInFlight;
307
333
  connectionStatus.attempt();
334
+ notifyDesktopStatusChange();
308
335
  let deviceHeartbeatSucceeded = false;
309
336
  const advertisedProviders = JSON.stringify(providerStatuses);
310
337
  heartbeatInFlight = Promise.resolve()
@@ -319,24 +346,36 @@ export async function startAgent({
319
346
  }),
320
347
  )
321
348
  .then(async () => {
349
+ if (shuttingDown) return false;
322
350
  deviceHeartbeatSucceeded = true;
323
351
  connectionStatus.success();
352
+ notifyDesktopStatusChange();
324
353
  // Register the device immediately; provider subprocesses must not delay
325
354
  // 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 });
355
+ const publishVerifiedProviders = async () => {
356
+ await readProviderStatuses();
357
+ if (!shuttingDown && JSON.stringify(providerStatuses) !== advertisedProviders) {
358
+ await heartbeatDevice({ apiOrigin, apiBaseUrl: session.api_base_url, token: session.token,
359
+ port, capabilities: refreshAvailableCapabilities(), repoInventory: repositoryInventory });
360
+ }
361
+ };
362
+ if (deferStartupSynchronization) {
363
+ void publishVerifiedProviders().catch((error) => reportHeartbeatError(error, quiet));
364
+ } else {
365
+ await publishVerifiedProviders();
330
366
  }
367
+ if (shuttingDown) return false;
331
368
  const sqmResult = sqmLocalReviewWorker
332
369
  ? await sqmLocalReviewWorker.poll()
333
370
  : null;
371
+ if (shuttingDown) return false;
334
372
  if (!sqmResult && durableLocalTaskWorker) await durableLocalTaskWorker.poll();
335
373
  return true;
336
374
  })
337
375
  .catch((error) => {
338
376
  if (!deviceHeartbeatSucceeded) {
339
377
  const state = connectionStatus.failure(error);
378
+ notifyDesktopStatusChange();
340
379
  if (state === "unauthorized") {
341
380
  session = null;
342
381
  loginUrl = null;
@@ -1634,20 +1673,34 @@ export async function startAgent({
1634
1673
  }
1635
1674
  }
1636
1675
 
1637
- if (isSessionValid(session, Date.now(), apiOrigin)) {
1676
+ const synchronizeStartup = async () => {
1638
1677
  try {
1639
1678
  await refreshAuthorizedRepositoryInventory();
1679
+ if (shuttingDown) return;
1640
1680
  await heartbeat();
1681
+ if (shuttingDown) return;
1641
1682
  await restoreRemoteRuns();
1642
1683
  } catch (error) {
1643
1684
  reportStartupConnectionError(error, quiet);
1644
1685
  }
1686
+ };
1687
+ if (deferStartupSynchronization) {
1688
+ // The desktop can show restored tasks and account state while network and
1689
+ // provider checks run. Unknown remains unknown until each probe succeeds.
1690
+ void readProviderStatuses().catch((error) => reportStartupConnectionError(error, quiet));
1691
+ if (isSessionValid(session, Date.now(), apiOrigin)) {
1692
+ startHeartbeatTimer();
1693
+ void synchronizeStartup();
1694
+ }
1695
+ } else if (isSessionValid(session, Date.now(), apiOrigin)) {
1696
+ await synchronizeStartup();
1645
1697
  startHeartbeatTimer();
1646
1698
  }
1647
1699
 
1648
1700
  let shutdownPromise = null;
1649
1701
  const shutdown = () => {
1650
1702
  if (shutdownPromise) return shutdownPromise;
1703
+ shuttingDown = true;
1651
1704
  shutdownPromise = (async () => {
1652
1705
  if (heartbeatTimer) clearInterval(heartbeatTimer);
1653
1706
  updateManager?.stop?.();
@@ -1670,7 +1723,14 @@ export async function startAgent({
1670
1723
  });
1671
1724
  const getDesktopStatus = async () => {
1672
1725
  const auth = desktopAuthStatus(session, { webOrigin: apiOrigin });
1673
- const { codex, claude } = await readProviderStatuses();
1726
+ if (deferStartupSynchronization && !shuttingDown) {
1727
+ // Cached checks return immediately; expired checks refresh in the
1728
+ // background without making the desktop frame wait for subprocesses.
1729
+ void readProviderStatuses().catch((error) => reportStartupConnectionError(error, quiet));
1730
+ }
1731
+ const { codex, claude } = deferStartupSynchronization
1732
+ ? providerStatuses
1733
+ : await readProviderStatuses();
1674
1734
  return {
1675
1735
  schemaVersion: 1,
1676
1736
  sampledAt: new Date().toISOString(),
@@ -1995,6 +2055,26 @@ async function capabilities() {
1995
2055
  inspectDesktopExecutor("codex"),
1996
2056
  inspectDesktopExecutor("claude"),
1997
2057
  ]);
2058
+ return capabilitiesFromInspections(codex, claude);
2059
+ }
2060
+
2061
+ function unverifiedCapabilities() {
2062
+ const unknown = (backend) => ({
2063
+ backend,
2064
+ execution_target: DESKTOP_EXECUTION_TARGET,
2065
+ execution_label: DESKTOP_EXECUTION_LABEL,
2066
+ provider_label: BACKEND_VERSION_POLICIES[backend].label,
2067
+ status: "checking",
2068
+ available: false,
2069
+ compatible: false,
2070
+ installed_version: null,
2071
+ minimum_version: BACKEND_VERSION_POLICIES[backend].minimumVersion,
2072
+ update_command: BACKEND_VERSION_POLICIES[backend].updateCommand,
2073
+ });
2074
+ return capabilitiesFromInspections(unknown("codex"), unknown("claude"));
2075
+ }
2076
+
2077
+ function capabilitiesFromInspections(codex, claude) {
1998
2078
  return {
1999
2079
  backends: { codex: codex.compatible, claude: claude.compatible },
2000
2080
  backend_versions: {
@@ -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.22";
16
+ export const CLI_VERSION = "0.5.25";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function withCliContract(
@@ -78,6 +78,11 @@ export async function startDesktopHost({ input = process.stdin, output = process
78
78
  });
79
79
  agent = await (dependencies.startAgent || startAgent)({
80
80
  daemon: true, webOrigin, sessionRoot: root, quiet: true, updateManager: updater,
81
+ deferStartupSynchronization: true,
82
+ onDesktopStatusChange: () => {
83
+ // Let an in-progress frame finish before publishing its new snapshot.
84
+ setImmediate(() => { void publish().catch(() => {}); });
85
+ },
81
86
  });
82
87
  stopWorkflows = (dependencies.startWorkflowUpdates || startWorkflowUpdates)({ enabled: env.SIMY_AUTO_UPDATE !== "0" });
83
88
  updater.on?.("change", () => { void publish().catch(() => {}); });
@@ -93,14 +93,18 @@ export function desktopTaskDetail(registries, key) {
93
93
  if (!task) return null;
94
94
  const attempt = task.snapshot?.attempts?.at(-1);
95
95
  const request = task.request?.requirement || task.display_instruction || task.instruction || task.snapshot?.charter?.requirement;
96
+ const purpose = purposePresentation(request);
96
97
  const result = typeof task.result === "string" ? task.result : task.result?.summary || attempt?.summary;
98
+ const report = reportPresentation(task.result) || reportPresentation(attempt?.raw_output?.agent_report, true) || reportPresentation(result);
97
99
  const links = [];
98
100
  const pr = safeDesktopLink(attempt?.pr_url);
99
101
  if (pr) links.push({ label: "Pull request", url: pr });
100
102
  return {
101
103
  key,
102
- instruction: desktopContent(request),
103
- result: desktopContent(result),
104
+ instruction: desktopContent(purpose?.purpose || request),
105
+ ...(purpose ? { purpose, technicalInstruction: desktopContent(request) } : {}),
106
+ result: desktopContent(report?.summary || result),
107
+ ...(report ? { report, ...(typeof result === "string" && result.trimStart().startsWith("{") ? { technicalResult: desktopContent(result) } : {}) } : {}),
104
108
  error: desktopContent(task.error, 1000),
105
109
  truncated: [request, result].some((value) => typeof value === "string" && value.length > 16000),
106
110
  links,
@@ -224,26 +228,82 @@ function taskStatusItem(task, registryIndex) {
224
228
  ),
225
229
  48,
226
230
  );
231
+ const request = task?.request?.requirement || task?.display_instruction || task?.instruction ||
232
+ task?.snapshot?.charter?.requirement;
233
+ const label = purposePresentation(request)?.purpose || request || task?.id || "Task";
227
234
  return {
228
235
  key: `${registryIndex}:${bounded(task?.id || "", 240)}`,
229
236
  id: bounded(String(task?.id || `unknown-${registryIndex}`), MAX_LABEL_LENGTH),
230
- label: bounded(
231
- String(
232
- task?.request?.requirement ||
233
- task?.display_instruction ||
234
- task?.instruction ||
235
- task?.snapshot?.charter?.requirement ||
236
- task?.id ||
237
- "Task",
238
- ),
239
- MAX_LABEL_LENGTH,
240
- ),
237
+ label: bounded(String(label), MAX_LABEL_LENGTH),
241
238
  state,
242
239
  updatedAt:
243
240
  Date.parse(task?.updated_at || task?.snapshot?.updated_at || task?.startedAt || 0) || 0,
244
241
  };
245
242
  }
246
243
 
244
+ // The Purpose-driven executor embeds a user-facing charter inside a generated
245
+ // instruction. Project only known charter fields; keep the envelope available
246
+ // on selection so no original context is lost or reinterpreted as progress.
247
+ function purposePresentation(request) {
248
+ if (typeof request !== "string" || request.length > 262_144 ||
249
+ !(request.includes("Purpose Charter and constraints (authoritative context):") ||
250
+ request.includes("Purpose Charter, hypotheses, verification order, and completion criteria (authoritative context):")) ||
251
+ !request.includes("You are the executor, not the conductor.")) return null;
252
+ const marker = "\nPurpose Charter:\n";
253
+ const start = request.indexOf(marker);
254
+ if (start < 0) return null;
255
+ const jsonStart = request.indexOf("{", start + marker.length);
256
+ if (jsonStart < 0) return null;
257
+ let depth = 0, quoted = false, escaped = false, end = -1;
258
+ for (let i = jsonStart; i < request.length; i += 1) {
259
+ const char = request[i];
260
+ if (escaped) { escaped = false; continue; }
261
+ if (quoted && char === "\\") { escaped = true; continue; }
262
+ if (char === '"') { quoted = !quoted; continue; }
263
+ if (quoted) continue;
264
+ if (char === "{") depth += 1;
265
+ if (char === "}") {
266
+ depth -= 1;
267
+ if (depth === 0) { end = i + 1; break; }
268
+ }
269
+ }
270
+ if (end < 0) return null;
271
+ try {
272
+ const charter = JSON.parse(request.slice(jsonStart, end));
273
+ const clean = (value, max = 2000) => typeof value === "string" ? desktopContent(value, max).trim() : "";
274
+ const purpose = clean(charter.original_request || charter.requirement || charter.purpose);
275
+ if (!purpose) return null;
276
+ return {
277
+ purpose,
278
+ persona: clean(charter.persona),
279
+ expectedValue: clean(charter.expected_value),
280
+ successCriteria: Array.isArray(charter.success_criteria)
281
+ ? charter.success_criteria.filter((value) => typeof value === "string").slice(0, 20).map((value) => clean(value, 1000)).filter(Boolean)
282
+ : [],
283
+ };
284
+ } catch { return null; }
285
+ }
286
+
287
+ function reportPresentation(result, allowSchemaLess = false) {
288
+ if (typeof result !== "string" && (typeof result !== "object" || result === null)) return null;
289
+ try {
290
+ const value = typeof result === "string"
291
+ ? result.length <= 65_536 && result.trimStart().startsWith("{") ? JSON.parse(result) : null
292
+ : result;
293
+ if ((value?.schema_version !== "agent_report.v1" && !(allowSchemaLess && !value?.schema_version)) || typeof value.summary !== "string") return null;
294
+ const list = (field) => Array.isArray(value[field])
295
+ ? value[field].filter((item) => typeof item === "string").slice(0, 20).map((item) => desktopContent(item, 1000))
296
+ : [];
297
+ return {
298
+ summary: desktopContent(value.summary),
299
+ testsRun: list("tests_run"),
300
+ evidenceRefs: list("evidence_refs"),
301
+ unresolvedItems: list("unresolved_items"),
302
+ risks: list("risks"),
303
+ };
304
+ } catch { return null; }
305
+ }
306
+
247
307
  function sessionUserLabel(session) {
248
308
  if (!session) return null;
249
309
  for (const key of ["user_label", "display_name", "email", "account_id", "auth_user_id"]) {