@awak-app/simy-cli 0.6.31 → 0.6.32

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.6.31",
3
+ "version": "0.6.32",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,7 +50,8 @@
50
50
  "test:macos-installer": "node --test packaging/test-macos-packaging.mjs",
51
51
  "build:macos-installer:production": "bash packaging/build-macos-arm64.sh --production",
52
52
  "test:e2e:claude-parity": "node ./scripts/claude-parity-runtime-e2e.js",
53
- "test:e2e:claude-plugin": "node ./scripts/claude-plugin-runtime-e2e.js"
53
+ "test:e2e:claude-plugin": "node ./scripts/claude-plugin-runtime-e2e.js",
54
+ "test:e2e:sqm-faults": "node ./scripts/sqm-complete-loop-e2e.js"
54
55
  },
55
56
  "engines": {
56
57
  "node": ">=20"
@@ -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.6.31";
16
+ export const CLI_VERSION = "0.6.32";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function narrativeImageCapabilityMetadata() {
@@ -6,7 +6,7 @@ import { pinnedWorkflowSource } from './local-runtime.js';
6
6
  const digest = bytes => createHash('sha256').update(bytes).digest('hex');
7
7
  // Exact shipped 0.6.23 and 0.6.24 entrypoints only. Customized hooks are retained.
8
8
  const shipped = new Set(['8a454293d119eac7cdb46c2df70c6b54a431fcf575bbf4f48f3373ca14414f3b', '339dc64d7ac980f9bfdafd1b2feea47ef8e86b2895b9d6e040086b005a806a79']);
9
- export const legacyRoutingShim = `// SIMY managed immutable-runtime route v1. Original exports retained for compatibility.
9
+ const originalLegacyRoutingShim = `// SIMY managed immutable-runtime route v1. Original exports retained for compatibility.
10
10
  export * from './delivery-loop.pre-runtime-route.mjs';
11
11
  import { readFile, readdir, lstat } from 'node:fs/promises';
12
12
  import { join, resolve } from 'node:path';
@@ -34,6 +34,10 @@ export async function main(provider) {
34
34
  } catch { process.stdout.write('{}\\n'); process.stderr.write('SIMY runtime route unavailable; original task state was preserved.\\n'); }
35
35
  }
36
36
  `;
37
+ export const legacyRoutingShim = originalLegacyRoutingShim.replace(
38
+ "for (const input of ['package.json', 'package-lock.json', 'README.md', 'src']) await visit(input);",
39
+ "for (const input of ['package.json', 'package-lock.json', 'README.md', 'src']) { try { await visit(input); } catch (error) { if (input === 'package-lock.json' && error.code === 'ENOENT') hash.update('packaged-without-lock\\0'); else throw error; } } if (pin.fingerprintVersion === 2) await visit('node_modules');",
40
+ );
37
41
  export async function routeLegacyHooks({ root, codexHome = process.env.CODEX_HOME || join(homedir(), '.codex') } = {}) {
38
42
  await pinnedWorkflowSource({ root });
39
43
  const folder = join(codexHome, 'plugins/cache/personal/simy');
@@ -47,9 +51,10 @@ export async function routeLegacyHooks({ root, codexHome = process.env.CODEX_HOM
47
51
  if (manifest.name !== 'simy' || manifest.version !== version || (await lstat(file)).isSymbolicLink()) continue;
48
52
  const bytes = await readFile(file), before = digest(bytes), next = digest(legacyRoutingShim);
49
53
  if (before === next) { receipts.push({ version, outcome: 'current', routeDigest: next }); continue; }
50
- if (!shipped.has(before)) { receipts.push({ version, outcome: 'customized_retained', originalDigest: before }); continue; }
54
+ const managedV1 = before === digest(originalLegacyRoutingShim);
55
+ if (!shipped.has(before) && !managedV1) { receipts.push({ version, outcome: 'customized_retained', originalDigest: before }); continue; }
51
56
  const backup = join(cache, 'hooks/delivery-loop.pre-runtime-route.mjs');
52
- try { await writeFile(backup, bytes, { mode: 0o600, flag: 'wx' }); }
57
+ try { if (!managedV1) await writeFile(backup, bytes, { mode: 0o600, flag: 'wx' }); }
53
58
  catch (error) { if (error.code !== 'EEXIST' || digest(await readFile(backup)) !== before) throw error; }
54
59
  const temporary = `${file}.${randomUUID()}.tmp`;
55
60
  await writeFile(temporary, legacyRoutingShim, { mode: 0o600 }); await rename(temporary, file);
@@ -1,16 +1,18 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { execFile } from 'node:child_process';
3
3
  import { promisify } from 'node:util';
4
- import { cp, lstat, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
4
+ import { cp, lstat, mkdir, readFile, readdir, realpath, rename, rm, writeFile } from 'node:fs/promises';
5
5
  import { dirname, join, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
+ import { acquireDesktopProcessLock } from '../desktop-process-lock.js';
8
+ import { compareVersions } from '../backend-executable.js';
7
9
  import { stableHookRuntime } from '../hook-runtime.js';
8
10
 
9
11
  const exec = promisify(execFile);
10
12
  const packageRoot = fileURLToPath(new URL('../../', import.meta.url));
11
13
  const inputs = ['package.json', 'package-lock.json', 'README.md', 'src'];
12
14
 
13
- async function fingerprint(source, { allowMissingLock = false } = {}) {
15
+ async function fingerprint(source, { allowMissingLock = false, includeDependencies = false } = {}) {
14
16
  const hash = createHash('sha256');
15
17
  async function visit(relative) {
16
18
  const target = join(source, relative);
@@ -29,6 +31,7 @@ async function fingerprint(source, { allowMissingLock = false } = {}) {
29
31
  else throw error;
30
32
  }
31
33
  }
34
+ if (includeDependencies) await visit('node_modules');
32
35
  return hash.digest('hex');
33
36
  }
34
37
 
@@ -40,12 +43,17 @@ async function atomicJson(file, value) {
40
43
 
41
44
  function quote(value) { return `'${value.replaceAll("'", "'\\''")}'`; }
42
45
 
43
- export async function localRuntimeStatus({ root, source = packageRoot, allowMissingLock = false } = {}) {
46
+ export async function localRuntimeStatus({ root, source = packageRoot, allowMissingLock = false, includeDependencies } = {}) {
44
47
  const pkg = JSON.parse(await readFile(join(source, 'package.json'), 'utf8'));
48
+ if (includeDependencies === undefined) {
49
+ try { includeDependencies = JSON.parse(await readFile(join(source, 'runtime-build.json'), 'utf8')).fingerprintVersion === 2; }
50
+ catch (error) { if (error.code !== 'ENOENT') throw error; includeDependencies = false; }
51
+ }
52
+ if (includeDependencies) allowMissingLock = true;
45
53
  let installation = null;
46
54
  try { installation = JSON.parse(await readFile(join(root, 'delivery-runtime', 'current.json'), 'utf8')); }
47
55
  catch (error) { if (error.code !== 'ENOENT') throw error; }
48
- return { version: pkg.version, source: resolve(source), sourceDigest: await fingerprint(source, { allowMissingLock }),
56
+ return { version: pkg.version, source: resolve(source), sourceDigest: await fingerprint(source, { allowMissingLock, includeDependencies }),
49
57
  purposeVersion: 2, decisionPolicy: 'llm-first-hc102', stateContractVersion: 2, consistencyMonitorVersion: 1, installation };
50
58
  }
51
59
 
@@ -58,14 +66,51 @@ export async function pinnedWorkflowSource({ root, source = packageRoot } = {})
58
66
  if (resolve(current.release || '') !== expected) throw new Error('Local workflow pin points outside its immutable releases.');
59
67
  const info = await lstat(expected);
60
68
  if (!info.isDirectory() || info.isSymbolicLink()) throw new Error('Pinned workflow source must be a regular directory.');
61
- const status = await localRuntimeStatus({ root, source: expected });
69
+ const status = await localRuntimeStatus({ root, source: expected, allowMissingLock: true, includeDependencies: current.fingerprintVersion === 2 });
62
70
  if (status.sourceDigest !== current.digest || status.version !== current.version) throw new Error('Pinned workflow runtime failed integrity verification.');
63
71
  return resolve(source) === expected ? null : expected;
64
72
  }
65
73
 
74
+ // Only the installer-selected bundle can advance an older local pin. Never
75
+ // adopt a checkout, downgrade a candidate, or touch task/provider state.
76
+ export async function adoptInstalledRuntime({ root, source = packageRoot, codexHome } = {}) {
77
+ let selected;
78
+ try { selected = await realpath(join(root, 'installer/current/Contents/Resources/cli')); }
79
+ catch (error) { if (error.code === 'ENOENT') return { adopted: false }; throw error; }
80
+ if (await realpath(source) !== selected) return { adopted: false };
81
+ const status = await localRuntimeStatus({ root, source, allowMissingLock: true, includeDependencies: false });
82
+ if (!status.installation || compareVersions(status.version, status.installation.version) <= 0) return { adopted: false };
83
+ // Validate the existing pin before replacing it; corruption is not an upgrade.
84
+ await pinnedWorkflowSource({ root, source });
85
+ const owner = await acquireDesktopProcessLock(root);
86
+ try {
87
+ const { routeLegacyHooks } = await import('./legacy-hook-routing.js');
88
+ await routeLegacyHooks({ root, ...(codexHome ? { codexHome } : {}) });
89
+ const installation = await installLocalRuntime({ root, source, allowMissingLock: true, includeDependencies: true,
90
+ expectedInstallationDigest: status.installation.digest,
91
+ verifyBeforeActivate: async () => {
92
+ if (await realpath(join(root, 'installer/current/Contents/Resources/cli')) !== selected) throw new Error('Installer selection changed during runtime adoption.');
93
+ },
94
+ installDependencies: async target => {
95
+ // Use the packaged dependencies without network or lifecycle scripts.
96
+ await cp(join(source, 'node_modules'), join(target, 'node_modules'), { recursive: true, dereference: true });
97
+ },
98
+ });
99
+ // A locally activated candidate is a separate explicit activation contract.
100
+ // Retain its receipt, but prevent watchdog replay of the superseded source.
101
+ const activeFile = join(root, 'desktop-runtime/active.json');
102
+ try {
103
+ const active = JSON.parse(await readFile(activeFile, 'utf8'));
104
+ if (active.digest !== installation.digest && !active.suspendedBy) await atomicJson(activeFile, { ...active, suspendedBy: 'runtime_upgrade_requires_activation', supersededBy: installation.digest });
105
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
106
+ return { adopted: true, installation };
107
+ } finally { await owner.release(); }
108
+ }
109
+
66
110
  // Install a frozen candidate alongside the released app, without replacing a
67
111
  // signed application, changing provider trust, or starting any registered task.
68
- export async function installLocalRuntime({ root, source = packageRoot,
112
+ export async function installLocalRuntime({ root, source = packageRoot, allowMissingLock = false, includeDependencies = false,
113
+ expectedInstallationDigest, verifyBeforeActivate = async () => {},
69
114
  installDependencies = async target => exec(process.platform === 'win32' ? 'npm.cmd' : 'npm',
70
115
  ['ci', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund'], { cwd: target, timeout: 120_000, maxBuffer: 1024 * 1024 }),
71
116
  } = {}) {
@@ -76,7 +121,8 @@ export async function installLocalRuntime({ root, source = packageRoot,
76
121
  await mkdir(lock); // An existing install is not silently stolen.
77
122
  const staged = join(storage, `.staged-${randomUUID()}`);
78
123
  try {
79
- const before = await localRuntimeStatus({ root, source });
124
+ const before = await localRuntimeStatus({ root, source, allowMissingLock, includeDependencies });
125
+ if (expectedInstallationDigest && before.installation?.digest !== expectedInstallationDigest) throw new Error('Runtime selection changed before adoption.');
80
126
  const digest = before.sourceDigest;
81
127
  const release = join(storage, 'releases', digest);
82
128
  await mkdir(dirname(release), { recursive: true, mode: 0o700 });
@@ -88,16 +134,21 @@ export async function installLocalRuntime({ root, source = packageRoot,
88
134
  } catch (error) { if (error.code !== 'ENOENT') throw error; }
89
135
  if (!exists) {
90
136
  await mkdir(staged, { mode: 0o700 });
91
- for (const input of inputs) await cp(join(source, input), join(staged, input), { recursive: true, errorOnExist: true });
92
- if (await fingerprint(staged) !== digest || await fingerprint(source) !== digest) throw new Error('Runtime source changed while copying; no candidate activated.');
93
- await installDependencies(staged);
94
- if (await fingerprint(staged) !== digest) throw new Error('Dependency installation changed the runtime source.');
95
- await atomicJson(join(staged, 'runtime-build.json'), { schema: 1, version: before.version, digest });
137
+ for (const input of inputs) {
138
+ try { await cp(join(source, input), join(staged, input), { recursive: true, errorOnExist: true }); }
139
+ catch (error) { if (!(allowMissingLock && input === 'package-lock.json' && error.code === 'ENOENT')) throw error; }
140
+ }
141
+ if (includeDependencies) await installDependencies(staged);
142
+ if (await fingerprint(staged, { allowMissingLock, includeDependencies }) !== digest || await fingerprint(source, { allowMissingLock, includeDependencies }) !== digest) throw new Error('Runtime source changed while copying; no candidate activated.');
143
+ if (!includeDependencies) await installDependencies(staged);
144
+ if (await fingerprint(staged, { allowMissingLock, includeDependencies }) !== digest) throw new Error('Dependency installation changed the runtime source.');
145
+ await atomicJson(join(staged, 'runtime-build.json'), { schema: 1, version: before.version, digest, fingerprintVersion: includeDependencies ? 2 : 1 });
96
146
  await rename(staged, release);
97
147
  } else {
98
148
  const saved = JSON.parse(await readFile(join(release, 'runtime-build.json'), 'utf8'));
99
- if (saved.schema !== 1 || saved.digest !== digest || await fingerprint(release) !== digest) throw new Error('Existing candidate failed integrity verification.');
149
+ if (saved.schema !== 1 || saved.digest !== digest || await fingerprint(release, { allowMissingLock, includeDependencies }) !== digest) throw new Error('Existing candidate failed integrity verification.');
100
150
  }
151
+ await verifyBeforeActivate();
101
152
  const bin = join(root, 'bin');
102
153
  await mkdir(bin, { recursive: true, mode: 0o700 });
103
154
  const launcher = join(bin, 'simy-delivery');
@@ -114,7 +165,7 @@ export async function installLocalRuntime({ root, source = packageRoot,
114
165
  await writeFile(cliTemporary, `#!/bin/sh\nexec ${quote(stableHookRuntime())} ${quote(join(release, 'src/index.js'))} --no-auto-update "$@"\n`, { mode: 0o700 });
115
166
  await rename(cliTemporary, cli);
116
167
  } finally { await rm(cliTemporary, { force: true }); }
117
- const current = { schema: 1, version: before.version, digest, release, launcher, cli,
168
+ const current = { schema: 1, version: before.version, digest, release, launcher, cli, fingerprintVersion: includeDependencies ? 2 : 1,
118
169
  previous: before.installation?.digest === digest ? before.installation.previous
119
170
  : before.installation ? { version: before.installation.version, digest: before.installation.digest, release: before.installation.release } : null };
120
171
  await atomicJson(join(storage, 'current.json'), current);
@@ -33,7 +33,7 @@ import { startAgent } from "./agent.js";
33
33
  import { createRuntimeDiagnostics, captureRuntimeOutput } from "./runtime-diagnostics.js";
34
34
  import { suspendDesktopWatchdog } from './delivery-hooks/runtime-watchdog.js';
35
35
  import { attestDesktopRuntime } from './delivery-hooks/desktop-runtime.js';
36
- import { pinnedWorkflowSource } from './delivery-hooks/local-runtime.js';
36
+ import { adoptInstalledRuntime, pinnedWorkflowSource } from './delivery-hooks/local-runtime.js';
37
37
  import { applyLoginShellPath } from "./login-shell-path.js";
38
38
  import { createCliAutoUpdater, readCliPackageInfo } from "./auto-update.js";
39
39
  import { createAgentShutdown } from "./shutdown.js";
@@ -1465,6 +1465,15 @@ export function isDesktopHostEntry(entry = process.argv[1]) {
1465
1465
  catch { return false; }
1466
1466
  }
1467
1467
 
1468
+ function desktopStartupError(error, fallbackMessage = "SIMY could not start. Check whether another SIMY window is already running, then reconnect.") {
1469
+ const conflict = error?.code === PROCESS_CONFLICT_CODE;
1470
+ return {
1471
+ type: "error",
1472
+ ...(conflict ? { code: PROCESS_CONFLICT_CODE, conflicts: error.conflicts || [] } : {}),
1473
+ message: conflict ? PROCESS_CONFLICT_MESSAGE : fallbackMessage,
1474
+ };
1475
+ }
1476
+
1468
1477
  export function startDesktopEntry() {
1469
1478
  let host;
1470
1479
  const stop = () => { void host?.close().finally(() => process.exit(0)); };
@@ -1475,14 +1484,7 @@ export function startDesktopEntry() {
1475
1484
  captureRuntimeOutput(diagnostics, { stderr: process.stderr });
1476
1485
  applyLoginShellPath({ diagnostics }).catch(() => {}).then(() => startDesktopHost()).then((value) => { host = value; }).catch((error) => {
1477
1486
  diagnostics.write(`startup failed: ${error?.stack || error}`);
1478
- const conflict = error?.code === PROCESS_CONFLICT_CODE;
1479
- process.stdout.write(`${JSON.stringify({
1480
- type: "error",
1481
- ...(conflict ? { code: PROCESS_CONFLICT_CODE, conflicts: error.conflicts || [] } : {}),
1482
- message: conflict
1483
- ? PROCESS_CONFLICT_MESSAGE
1484
- : "SIMY could not start. Check whether another SIMY window is already running, then reconnect.",
1485
- })}\n`);
1487
+ process.stdout.write(`${JSON.stringify(desktopStartupError(error))}\n`);
1486
1488
  process.exitCode = 1;
1487
1489
  });
1488
1490
  }
@@ -1490,14 +1492,20 @@ export function startDesktopEntry() {
1490
1492
  if (isDesktopHostEntry()) {
1491
1493
  try {
1492
1494
  const root = process.env.SIMY_HOME?.trim() || join(homedir(), '.simy');
1495
+ try { await adoptInstalledRuntime({ root }); }
1496
+ catch (error) {
1497
+ if (error.code === PROCESS_CONFLICT_CODE) throw error;
1498
+ await pinnedWorkflowSource({ root }); // Only a verified previous runtime may recover.
1499
+ process.stderr.write('SIMY runtime upgrade deferred: ' + error.message + '\n');
1500
+ }
1493
1501
  const selected = await pinnedWorkflowSource({ root });
1494
1502
  if (selected) {
1495
1503
  process.env.SIMY_AUTO_UPDATE = '0';
1496
1504
  const selectedHost = await import(pathToFileURL(join(selected, 'src/desktop-host.js')).href);
1497
1505
  selectedHost.startDesktopEntry();
1498
1506
  } else startDesktopEntry();
1499
- } catch {
1500
- process.stdout.write(JSON.stringify({ type: 'error', message: 'Local desktop runtime integrity could not be verified. Inspect delivery runtime status; the previous task state was retained.' }) + '\n');
1507
+ } catch (error) {
1508
+ process.stdout.write(JSON.stringify(desktopStartupError(error, 'Local desktop runtime integrity could not be verified. Inspect delivery runtime status; the previous task state was retained.')) + '\n');
1501
1509
  process.exitCode = 1;
1502
1510
  }
1503
1511
  }