@awak-app/simy-cli 0.7.31 → 0.7.33

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.
Files changed (32) hide show
  1. package/README.md +22 -3
  2. package/package.json +3 -2
  3. package/src/agent.js +7 -2
  4. package/src/auto-update.js +10 -1
  5. package/src/cli-contract.js +1 -1
  6. package/src/cli-observability.js +1 -1
  7. package/src/codex-plugins/simy/hooks/delivery-loop.mjs +27 -3
  8. package/src/codex-plugins/simy/hooks/purpose-workspace.mjs +13 -4
  9. package/src/delivery-hooks/desktop.js +1 -1
  10. package/src/desktop-access.js +1 -1
  11. package/src/desktop-frame-writer.js +29 -0
  12. package/src/desktop-host.js +176 -74
  13. package/src/orchestrator/loop.js +2 -0
  14. package/src/session-supervision/analysis-manager.js +83 -18
  15. package/src/session-supervision/claude-desktop-lineage.js +1 -1
  16. package/src/session-supervision/claude-sessions.js +16 -4
  17. package/src/session-supervision/codex-app-server.js +39 -30
  18. package/src/session-supervision/continuation-readiness.js +58 -0
  19. package/src/session-supervision/decision-presentation.js +11 -3
  20. package/src/session-supervision/index.js +101 -24
  21. package/src/session-supervision/owner-transport.js +23 -8
  22. package/src/session-supervision/progressive-discovery.js +66 -0
  23. package/src/session-supervision/remote-entry.js +7 -4
  24. package/src/session-supervision/remote-host-recovery.js +21 -0
  25. package/src/session-supervision/remote-hosts.js +28 -8
  26. package/src/session-supervision/remote-owner-entry.js +20 -0
  27. package/src/session-supervision/remote-service.js +36 -0
  28. package/src/session-supervision/statistics.js +19 -3
  29. package/src/session-supervision/work-state.js +7 -1
  30. package/src/team-threads.js +3 -6
  31. package/src/update-connection.js +16 -0
  32. package/src/web-api.js +9 -0
package/README.md CHANGED
@@ -969,9 +969,20 @@ diagrams. This is a design proposal, not a shipped completion guarantee.
969
969
  Publishing is handled by GitHub Actions in `.github/workflows/publish.yml`.
970
970
 
971
971
  - Every push tag matching `v*.*.*` runs checks and publishes to npm.
972
- - Manual workflow runs perform the same checks; set `publish=true` to publish.
973
- - The repository must define an `NPM_TOKEN` secret with publish access to
974
- `@awak-app/simy-cli`.
972
+ - Manual workflow runs perform the same checks and verify the npm OIDC exchange.
973
+ Leave `publish=false` for a dry-run; set `publish=true` to publish.
974
+ - npm trusted publishing must allow direct `npm publish` for GitHub organization
975
+ `SpecialistDoctors-Inc`, repository `simy-cli`, workflow filename `publish.yml`,
976
+ with no environment restriction (this job does not use a GitHub environment).
977
+ Publishing uses GitHub-hosted runners, Node 22 and npm 11.21.0.
978
+ - The publisher verifies GitHub OIDC issuance and npm token exchange separately,
979
+ without logging or persisting credentials. It never falls back to `NPM_TOKEN`;
980
+ local npm login is not required for CI publishing.
981
+ - A registry outage can also cause an OIDC exchange failure. Check npm service
982
+ status and the failing preflight stage before changing account settings.
983
+ Rerunning a tag uses that tag's workflow; it does not pick up later fixes.
984
+ Retry only the npm job for an already released native version, and check the
985
+ current npm `latest` first so an older release cannot replace a newer one.
975
986
  - The package check inspects the final npm tarball contents and fails if any
976
987
  source map file would be published.
977
988
 
@@ -1518,3 +1529,11 @@ that operation continues to block writes. Observing the result settles local
1518
1529
  occupancy without accepting the task. Long-running tasks transmit current
1519
1530
  recovery context and recent work; the full history remains in durable state
1520
1531
  and can be inspected through `delivery status`.
1532
+
1533
+ ### Reusable Auto Continue evaluation inputs
1534
+
1535
+ For the 20 versioned conversation regression cases, immutable stopping-protocol
1536
+ plans, and pinned SWE-bench Pro public-task preparation, see
1537
+ [the evaluation guide](scripts/auto-continue-eval/README.md). Run
1538
+ `npm run eval:auto-continue -- help` from a source checkout. This tooling prepares
1539
+ inputs; it does not launch models or establish continuation effectiveness.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.7.31",
3
+ "version": "0.7.33",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -54,7 +54,8 @@
54
54
  "test:e2e:claude-plugin": "node ./scripts/claude-plugin-runtime-e2e.js",
55
55
  "test:e2e:codex-plugin": "node ./scripts/codex-plugin-runtime-e2e.js",
56
56
  "test:e2e:flexible-delivery": "node --test --test-name-pattern=\"flexible prototype\" test/purpose-search.test.js",
57
- "test:e2e:sqm-faults": "node ./scripts/sqm-complete-loop-e2e.js"
57
+ "test:e2e:sqm-faults": "node ./scripts/sqm-complete-loop-e2e.js",
58
+ "eval:auto-continue": "node ./scripts/auto-continue-eval.js"
58
59
  },
59
60
  "engines": {
60
61
  "node": ">=20"
package/src/agent.js CHANGED
@@ -106,6 +106,7 @@ import {
106
106
  webApiErrorMessage,
107
107
  webApiHeaders,
108
108
  webApiUrl,
109
+ publicImageUrl,
109
110
  } from "./web-api.js";
110
111
  import {
111
112
  defaultRepositoryScanRoot,
@@ -672,9 +673,13 @@ export async function startAgent({
672
673
  if (!Array.isArray(payload.organizations)) throw new Error("Invalid organization list.");
673
674
  if (session !== requestedSession) return;
674
675
  organizationItems = payload.organizations.filter(org => typeof org.id === "string" && typeof org.name === "string")
675
- .slice(0, 100).map(org => ({ id: org.id, name: org.name.slice(0, 120),
676
+ .slice(0, 100).map(org => {
677
+ const logoUrl = publicImageUrl(org.logoUrl ?? org.logo_url);
678
+ return { id: org.id, name: org.name.slice(0, 120),
679
+ ...(logoUrl ? { logoUrl } : {}),
676
680
  ...(["ready", "sqm_required", "developer_required", "unavailable"].includes(org.desktopAccess) ? { desktopAccess: org.desktopAccess } : {}),
677
- ...(typeof org.slug === 'string' && org.slug && org.slug.length <= 160 && !/[\/\\?#\x00-\x1f]/.test(org.slug) ? { slug: org.slug } : {}) }));
681
+ ...(typeof org.slug === 'string' && org.slug && org.slug.length <= 160 && !/[\/\\?#\x00-\x1f]/.test(org.slug) ? { slug: org.slug } : {}) };
682
+ });
678
683
  organizationState = "ready";
679
684
  } catch {
680
685
  if (session === requestedSession) organizationState = "unavailable";
@@ -16,6 +16,9 @@ import { fileURLToPath } from "node:url";
16
16
  import { promisify } from "node:util";
17
17
 
18
18
  import { daemonRestartCommand } from "./web-origin.js";
19
+ import { updateConnectionEnvironment } from './update-connection.js';
20
+ import { connectionScope, assertConnectionScope } from './connection-diagnostics.js';
21
+ import { readSession } from './session-store.js';
19
22
  import { openExternalUrl } from "./browser.js";
20
23
  import {
21
24
  installMacosInstallerRelease,
@@ -1006,6 +1009,7 @@ export async function restartDaemonProcess({
1006
1009
  env = process.env,
1007
1010
  } = {}) {
1008
1011
  const handoffDirectory = path.join(stateRoot, "updates", "handoff");
1012
+ const expectedScope = expectedWebOrigin ? connectionScope(await readSession(expectedWebOrigin, stateRoot), expectedWebOrigin) : null;
1009
1013
  await mkdir(handoffDirectory, { recursive: true, mode: 0o700 });
1010
1014
  const nonce = randomBytes(24).toString("hex");
1011
1015
  const handoffPath = path.join(handoffDirectory, `${process.pid}-${nonce}.json`);
@@ -1013,7 +1017,7 @@ export async function restartDaemonProcess({
1013
1017
  detached: true,
1014
1018
  stdio: "ignore",
1015
1019
  env: {
1016
- ...env,
1020
+ ...(expectedWebOrigin ? updateConnectionEnvironment({ env, root: stateRoot, webOrigin: expectedWebOrigin }) : { ...env, SIMY_HOME: path.resolve(stateRoot) }),
1017
1021
  SIMY_DAEMON_CHILD: "1",
1018
1022
  SIMY_UPDATE_HANDOFF_FILE: handoffPath,
1019
1023
  SIMY_UPDATE_HANDOFF_NONCE: nonce,
@@ -1040,6 +1044,10 @@ export async function restartDaemonProcess({
1040
1044
  if (expectedInstallMode && handoff.install_mode !== expectedInstallMode) {
1041
1045
  throw new Error("Restarted SIMY reported a different installation channel.");
1042
1046
  }
1047
+ if (expectedScope?.account_id || expectedScope?.organization_id || expectedScope?.device_id) {
1048
+ assertConnectionScope(expectedScope, handoff.scope);
1049
+ assertConnectionScope(expectedScope, connectionScope(await readSession(expectedWebOrigin, stateRoot), expectedWebOrigin));
1050
+ }
1043
1051
  return handoff;
1044
1052
  } catch (error) {
1045
1053
  await terminateCandidate(child);
@@ -1073,6 +1081,7 @@ export async function writeUpdateHandoffFromEnvironment({
1073
1081
  port,
1074
1082
  pid: process.pid,
1075
1083
  web_origin: webOrigin,
1084
+ scope: webOrigin ? connectionScope(await readSession(webOrigin, stateRoot), webOrigin) : null,
1076
1085
  install_mode: installMode,
1077
1086
  ready_at: new Date().toISOString(),
1078
1087
  })}\n`,
@@ -14,7 +14,7 @@ import {
14
14
  } from "./durable-local-task-steps-contract.js";
15
15
  import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
16
16
 
17
- export const CLI_VERSION = "0.7.31";
17
+ export const CLI_VERSION = "0.7.33";
18
18
  export const CLI_API_CONTRACT_VERSION = 10;
19
19
 
20
20
  export function narrativeImageCapabilityMetadata() {
@@ -7,7 +7,7 @@ import { execFile } from 'node:child_process';
7
7
  import { promisify } from 'node:util';
8
8
  import { webApiHeaders, webApiUrl } from './web-api.js';
9
9
  // Keep this leaf module package-safe: hooks load it before the full CLI contract.
10
- const CLI_VERSION = '0.7.31';
10
+ const CLI_VERSION = '0.7.33';
11
11
 
12
12
  const STORE = 'cli-observability-state.json';
13
13
  const LEGACY_CONFIG = 'cli-observability.json';
@@ -1115,7 +1115,7 @@ export async function enablePurposeDelivery({ root = rootDirectory(), provider,
1115
1115
  if (managedBudgetReason(run, now)) throw new Error('Original budget exhausted; automatic continuation cannot reset it.');
1116
1116
  if (!run.managed) {
1117
1117
  const plan = purposeDiagnosisPlan({ approvalScopes: ['task'] }, 'Read the original conversation, reconstruct its purpose and completion conditions, then choose necessary work.');
1118
- run.managed = managedSpec({ acceptanceCriteria: [], contextBootstrap: true, autonomyPolicy: 'autonomous', approvalScopes: ['task'], approvalPolicy: 'wait',
1118
+ run.managed = managedSpec({ acceptanceCriteria: [], contextBootstrap: true, autonomyPolicy: 'balanced', approvalScopes: ['task'], approvalPolicy: 'wait',
1119
1119
  initialPlan: plan, initialAction: plan.alternatives[0].action, autoRecovery: true, resumeChannel: 'codex_app_server', resumeModel: admission.model, purpose },
1120
1120
  session, provider, record.process, admission, now);
1121
1121
  run.managed.purpose.previousCheckpoint = run.checkpoint;
@@ -2089,7 +2089,9 @@ export function deliveryStopBlocksResume(run, now = Date.now()) {
2089
2089
  }
2090
2090
  // Stopped remains terminal for hooks, polling and automatic recovery. Only the
2091
2091
  // explicit control action may reopen an enrolled app-server task for diagnosis.
2092
- export function canExplicitlyResumeStopped(run) {
2092
+ export function canExplicitlyResumeStopped(run, { standard = false } = {}) {
2093
+ if (standard && run?.state === 'stopped' && !run.managed)
2094
+ return canUpgradeDelivery({ ...run, state: 'active' }) && (run.failures || 0) < run.maxFailures;
2093
2095
  return run?.state === 'stopped' && run.managed?.resumeChannel === 'codex_app_server' &&
2094
2096
  Boolean(run.managed.autonomyPolicy && run.managed.approvalScopes?.length) &&
2095
2097
  !run.managed.resumeLease && !run.managed.liveWorkspaceLease;
@@ -2977,8 +2979,30 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
2977
2979
  run.providerRequest = null;
2978
2980
  const marker = readState(stopMarkerPath(root, provider, session));
2979
2981
  if (marker?.runId === run.id) run.handledStopMarkerId = marker.id;
2980
- const wasStopped = canExplicitlyResumeStopped(run);
2982
+ const wasStopped = canExplicitlyResumeStopped(run, { standard: provider === 'codex' });
2981
2983
  const wasVerifying = canResumePendingVerification(run);
2984
+ if (wasStopped && !run.managed) {
2985
+ const link = await providerLinkState(root, provider);
2986
+ if (expectedRunId !== run.id || expectedRevision !== deliveryDecisionRevision(run) ||
2987
+ !link.connected || resumeObservation?.providerEpoch !== link.epoch ||
2988
+ resumeObservation?.session !== session || resumeObservation.cwd !== record.cwd ||
2989
+ resumeObservation.source !== 'codex_app_server' || !text(resumeObservation.model, 200) ||
2990
+ !resumeObservation.purpose?.ruleDigest || deliveryStopBlocksResume(run, now))
2991
+ throw new Error('Explicit resume requires the exact stopped run and current original provider observation.');
2992
+ record.admission = { source: 'codex_app_server', provider, session, cwd: record.cwd,
2993
+ model: resumeObservation.model, validatedAt: now,
2994
+ ...(resumeObservation.nativeSandbox && resumeObservation.nativeSandboxSource?.type === 'sdk_rollout_turn_context' &&
2995
+ resumeObservation.nativeSandboxSource.session === session && resumeObservation.nativeSandboxSource.cwd === record.cwd
2996
+ ? { nativeSandbox: structuredClone(resumeObservation.nativeSandbox), nativeSandboxSource: structuredClone(resumeObservation.nativeSandboxSource) } : {}) };
2997
+ if (link.consentEpoch) record.providerConsentEpoch = link.consentEpoch;
2998
+ const plan = purposeDiagnosisPlan({ approvalScopes: ['task'] },
2999
+ 'Read the original conversation and latest corrections, reconcile prior effects, and reconstruct remaining authorized work. Do not replay prior actions.');
3000
+ run.managed = managedSpec({ acceptanceCriteria: [], contextBootstrap: true, autonomyPolicy: 'balanced',
3001
+ approvalScopes: ['task'], approvalPolicy: 'wait', initialPlan: plan, initialAction: plan.alternatives[0].action,
3002
+ autoRecovery: true, resumeChannel: 'codex_app_server', resumeModel: resumeObservation.model,
3003
+ purpose: resumeObservation.purpose }, session, provider, record.process, record.admission, now);
3004
+ run.managed.purpose.previousCheckpoint = run.checkpoint;
3005
+ }
2982
3006
  if (run.managed?.dependencyWait) throw new Error('Confirm the exact external dependency condition before resuming.');
2983
3007
  if (run.managed?.phase === 'needs_clarification') throw new Error('Answer the current session question before resuming.');
2984
3008
  if (!['blocked', 'recovery_required', 'paused'].includes(run.state) && !wasStopped && !wasVerifying) throw new Error('Only a blocked, paused, recoverable, unowned pending-verification task, or stopped managed app-server loop can resume.');
@@ -5,8 +5,15 @@ import { resolve, relative, isAbsolute, join } from 'node:path';
5
5
 
6
6
  // Completion reviews bind to the actual local state. No resets, stashes, writes,
7
7
  // shell commands from a worker, or special verifier registration are needed.
8
- export async function workspaceIdentity(cwd, stateRoot, inventory, { signal, excludedPaths = [], inventoryLimit = 256 } = {}) {
8
+ export async function workspaceIdentity(cwd, stateRoot, inventory, { signal, excludedPaths = [], inventoryLimit = 256,
9
+ maxEntries = Infinity, maxBytes = Infinity, deadline = Infinity } = {}) {
9
10
  signal?.throwIfAborted();
11
+ let entries = 0, bytes = 0;
12
+ const checkBudget = () => {
13
+ signal?.throwIfAborted();
14
+ if (entries > maxEntries || bytes > maxBytes || Date.now() >= deadline)
15
+ throw new Error('Optional workspace inventory exceeded its preparation budget; relevant files remain available for direct inspection.');
16
+ };
10
17
  const hash = createHash('sha256');
11
18
  const paths = [];
12
19
  const capture = entry => {
@@ -20,12 +27,13 @@ export async function workspaceIdentity(cwd, stateRoot, inventory, { signal, exc
20
27
  const info = await lstat(path); versions.set(path, info); return info;
21
28
  };
22
29
  const walk = async (directory, depth = 0) => {
23
- signal?.throwIfAborted();
30
+ checkBudget();
24
31
  if (depth > 30) throw new Error('Workspace review inventory exceeds its bound.');
25
32
  await remember(directory);
26
33
  for await (const entry of await opendir(directory)) {
27
34
  signal?.throwIfAborted();
28
35
  if (['.git', 'node_modules', '.npm-cache'].includes(entry.name)) continue;
36
+ entries++; checkBudget();
29
37
  const path = join(directory, entry.name);
30
38
  if (stateRoot && resolve(path) === resolve(stateRoot)) continue;
31
39
  if (entry.isDirectory()) await walk(path, depth + 1);
@@ -36,7 +44,7 @@ export async function workspaceIdentity(cwd, stateRoot, inventory, { signal, exc
36
44
 
37
45
  const buffer = Buffer.allocUnsafe(1024 * 1024);
38
46
  for (const path of paths) {
39
- signal?.throwIfAborted();
47
+ checkBudget();
40
48
  const full = resolve(cwd, path), rel = relative(cwd, full);
41
49
  if (rel.startsWith('..') || isAbsolute(rel)) throw new Error('Invalid workspace review path.');
42
50
  if (stateRoot && (full === resolve(stateRoot) || full.startsWith(resolve(stateRoot) + '/'))) continue;
@@ -57,6 +65,7 @@ export async function workspaceIdentity(cwd, stateRoot, inventory, { signal, exc
57
65
  continue;
58
66
  }
59
67
  if (!info.isFile()) { const identity=`nonregular:${info.mode}:${info.mtimeMs}`;hash.update(identity);capture({path,mode:info.mode,type:'nonregular',digest:createHash('sha256').update(identity).digest('hex')});continue; }
68
+ bytes += info.size; checkBudget();
60
69
  const actual = await realpath(full), actualRel = relative(cwd, actual);
61
70
  if (actualRel.startsWith('..') || isAbsolute(actualRel)) throw new Error('Workspace changed to an external link during review.');
62
71
  // Stream large artifacts with fixed memory. Verify the opened descriptor
@@ -69,7 +78,7 @@ export async function workspaceIdentity(cwd, stateRoot, inventory, { signal, exc
69
78
  hash.update(String(info.mode));
70
79
  let position = 0;
71
80
  while (position < before.size) {
72
- signal?.throwIfAborted();
81
+ checkBudget();
73
82
  const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, before.size - position), position);
74
83
  if (!bytesRead) throw new Error('Workspace artifact was truncated during hashing.');
75
84
  const chunk = buffer.subarray(0, bytesRead);
@@ -106,7 +106,7 @@ export async function deliveryHookStatus({ root = rootDirectory(), now = Date.no
106
106
  canPause: Boolean(r.run.managed) && (['active', 'auto_resuming'].includes(r.run.state) ||
107
107
  r.run.state === 'recovery_required' && ['session_supervision', 'approved_action'].includes(r.run.recoveryCause) &&
108
108
  r.run.managed.resumeChannel === 'codex_app_server' && r.run.managed.autoRecovery),
109
- canResume: (['blocked', 'recovery_required', 'paused'].includes(r.run.state) || canExplicitlyResumeStopped(r.run) || canResumePendingVerification(r.run)) && r.run.managed?.phase !== 'needs_clarification' &&
109
+ canResume: (['blocked', 'recovery_required', 'paused'].includes(r.run.state) || canExplicitlyResumeStopped(r.run, { standard: r.provider === 'codex' }) || canResumePendingVerification(r.run)) && r.run.managed?.phase !== 'needs_clarification' &&
110
110
  !r.run.managed?.pendingApproval && !r.run.managed?.controllerDecisionWait && !r.run.managed?.dependencyWait && !['pending', 'pending_received', 'transferred'].includes(r.run.managed?.handoff?.status) &&
111
111
  !deliveryStopBlocksResume(r.run, now),
112
112
  canApprove: r.run.state === 'blocked' && Boolean(r.run.managed?.pendingApproval) && !r.run.managed.pendingApproval.denied,
@@ -9,7 +9,7 @@ export function validDesktopAccessPath(path, slug) {
9
9
  export const desktopAccessReady = value => value?.state === 'ready';
10
10
  const recoveryActions = new Set(['refresh', 'reconnect', 'login', 'authorization_open', 'authorization_copy', 'organization_select', 'organization_cancel', 'organizations_refresh',
11
11
  'check_update', 'auto_update_set_enabled', 'update', 'prepare_update_restart', 'quit', 'task_stop', 'delivery_stop', 'delivery_pause', 'delivery_reject',
12
- 'provider_disconnect', 'desktop_access_refresh', 'desktop_access_settings', 'desktop_access_members']);
12
+ 'provider_disconnect', 'desktop_access_refresh', 'desktop_access_settings', 'desktop_access_members', 'desktop_sqm_settings']);
13
13
  export function desktopActionAllowed(access, command) {
14
14
  return desktopAccessReady(access) || recoveryActions.has(command?.action) ||
15
15
  command?.action === 'session_set_auto_continue' && command.enabled === false ||
@@ -0,0 +1,29 @@
1
+ // Status snapshots are replaceable; command receipts and stop notifications are
2
+ // not. Never enqueue periodic snapshots behind a native reader that has paused.
3
+ // Control frames retain the stream's FIFO order and never wait for GUI drain.
4
+ export function createDesktopFrameWriter(output) {
5
+ let blocked = false, pendingStatus = null, statusClosed = false;
6
+ const drain = () => {
7
+ blocked = false;
8
+ const frame = pendingStatus;
9
+ pendingStatus = null;
10
+ if (!statusClosed && frame !== null) write(frame, true);
11
+ };
12
+ const write = (frame, isStatus = false) => {
13
+ if (isStatus && statusClosed) return;
14
+ if (isStatus && blocked) { pendingStatus = frame; return; }
15
+ // A control frame must not be followed by a snapshot captured before it.
16
+ // The next authorized projection will publish fresh state.
17
+ if (!isStatus) pendingStatus = null;
18
+ const accepted = output.write(frame);
19
+ if (accepted === false && !blocked && !statusClosed) {
20
+ blocked = true;
21
+ output.once('drain', drain);
22
+ }
23
+ };
24
+ return { write, close() {
25
+ statusClosed = true;
26
+ pendingStatus = null;
27
+ output.removeListener('drain', drain);
28
+ } };
29
+ }