@awak-app/simy-cli 0.6.23 → 0.6.26

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 (64) hide show
  1. package/README.md +53 -0
  2. package/package.json +3 -3
  3. package/src/cli-contract.js +9 -1
  4. package/src/codex-plugins/simy/hooks/acceptance-policy.mjs +5 -0
  5. package/src/codex-plugins/simy/hooks/delivery-loop.mjs +533 -74
  6. package/src/codex-plugins/simy/hooks/purpose-cycle.mjs +63 -0
  7. package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +154 -0
  8. package/src/codex-plugins/simy/hooks/purpose-rule-policy.mjs +34 -0
  9. package/src/codex-plugins/simy/hooks/purpose-rule-review.mjs +65 -0
  10. package/src/codex-plugins/simy/hooks/purpose-search.mjs +123 -0
  11. package/src/codex-plugins/simy/hooks/purpose-workspace.mjs +97 -0
  12. package/src/codex-plugins/simy/skills/delivery-loop/SKILL.md +19 -5
  13. package/src/codex-plugins/simy/skills/sqm-loop/SKILL.md +19 -4
  14. package/src/delivery-hooks/command.js +48 -0
  15. package/src/delivery-hooks/consistency-monitor.js +126 -0
  16. package/src/delivery-hooks/desktop-runtime.js +70 -0
  17. package/src/delivery-hooks/desktop.js +13 -3
  18. package/src/delivery-hooks/legacy-hook-routing.js +61 -0
  19. package/src/delivery-hooks/local-runtime.js +123 -0
  20. package/src/delivery-hooks/local-supervisor.js +25 -0
  21. package/src/delivery-hooks/runtime-watchdog.js +101 -0
  22. package/src/delivery-hooks/supervisor.js +90 -22
  23. package/src/desktop-host.js +24 -1
  24. package/src/durable-local-task-worker.js +57 -12
  25. package/src/fp-narrative-image.js +180 -0
  26. package/src/index.js +1 -1
  27. package/src/local-task.js +1 -0
  28. package/src/orchestrator/audit.js +1 -1
  29. package/src/orchestrator/charter-repair.js +93 -0
  30. package/src/orchestrator/charter-validation.js +14 -6
  31. package/src/orchestrator/checklist-continuation.js +1 -1
  32. package/src/orchestrator/contract.js +1 -3
  33. package/src/orchestrator/delivery/WORKFLOW.md +33 -60
  34. package/src/orchestrator/delivery/controls/harness-controls.json +1326 -0
  35. package/src/orchestrator/delivery/references/delivery-levels.md +17 -44
  36. package/src/orchestrator/delivery/references/pr-closure.md +2 -3
  37. package/src/orchestrator/delivery/references/state-and-receipts.md +14 -5
  38. package/src/orchestrator/delivery/references/ux-specification.md +1 -2
  39. package/src/orchestrator/delivery-level.js +4 -5
  40. package/src/orchestrator/independent-audit.js +9 -5
  41. package/src/orchestrator/instruction.js +1 -0
  42. package/src/orchestrator/loop.js +55 -18
  43. package/src/orchestrator/risk.js +1 -1
  44. package/src/orchestrator/ux-specification.js +2 -2
  45. package/src/runner.js +4 -0
  46. package/src/session-supervision/analysis-manager.js +2 -2
  47. package/src/session-supervision/automatic-continuation.js +6 -5
  48. package/src/session-supervision/autonomous-upgrade.js +55 -0
  49. package/src/session-supervision/codex-app-server.js +200 -24
  50. package/src/session-supervision/consistency-auditor.js +113 -0
  51. package/src/session-supervision/index.js +38 -11
  52. package/src/session-supervision/purpose-candidate.js +44 -0
  53. package/src/session-supervision/purpose-instruction.js +47 -0
  54. package/src/session-supervision/purpose-observer.js +172 -0
  55. package/src/session-supervision/purpose-recovery.js +264 -0
  56. package/src/session-supervision/purpose-verification.js +154 -0
  57. package/src/sqm/agent-skill.js +6 -3
  58. package/src/sqm/artifact-paths.js +39 -0
  59. package/src/sqm/claude-plugin.js +7 -0
  60. package/src/sqm/command.js +95 -27
  61. package/src/sqm/history-store.js +3 -3
  62. package/src/sqm/repository.js +19 -5
  63. package/src/sqm/workflow-update.js +14 -0
  64. package/src/workflow-reporting.js +2 -0
@@ -0,0 +1,25 @@
1
+ import { dirname } from 'node:path';
2
+ import { setTimeout as delay } from 'node:timers/promises';
3
+ import { assertNoDesktopHostForOrigin } from '../desktop-process-lock.js';
4
+ import { createManagedSupervisor } from './supervisor.js';
5
+
6
+ export async function superviseLocalDelivery({ root, once = false, signal,
7
+ createSupervisor = createManagedSupervisor, checkOwner = assertNoDesktopHostForOrigin, wait = delay } = {}) {
8
+ // Delivery records share this local root across origins. A staging desktop
9
+ // can own the same provider session, so fence every live owner in this root.
10
+ const check = () => checkOwner(dirname(root), null);
11
+ await check();
12
+ const supervisor = createSupervisor({ root });
13
+ const stop = () => { void supervisor.close(); };
14
+ signal?.addEventListener('abort', stop, { once: true });
15
+ try {
16
+ do {
17
+ if (signal?.aborted) break;
18
+ await check();
19
+ await supervisor.tick();
20
+ if (once) break;
21
+ try { await wait(3_000, undefined, { signal }); }
22
+ catch (error) { if (!signal?.aborted) throw error; }
23
+ } while (!signal?.aborted);
24
+ } finally { signal?.removeEventListener('abort', stop); await supervisor.close(); }
25
+ }
@@ -0,0 +1,101 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { randomUUID } from 'node:crypto';
7
+ import { setTimeout as delay } from 'node:timers/promises';
8
+ import { assertNoDesktopHostForOrigin } from '../desktop-process-lock.js';
9
+ import { pinnedWorkflowSource, localRuntimeStatus } from './local-runtime.js';
10
+ import { desktopRuntimeStatus } from './desktop-runtime.js';
11
+ import { consistencyMonitorStatus } from './consistency-monitor.js';
12
+ const exec = promisify(execFile);
13
+ async function read(file) { try { return JSON.parse(await readFile(file, 'utf8')); } catch (error) { if (error.code === 'ENOENT') return null; throw error; } }
14
+ async function save(file, value) {
15
+ const temp = `${file}.${randomUUID()}.tmp`;
16
+ try { await writeFile(temp, JSON.stringify(value), { mode: 0o600 }); await rename(temp, file); }
17
+ finally { const { rm } = await import('node:fs/promises'); await rm(temp, { force: true }); }
18
+ }
19
+ async function candidate(root) {
20
+ await pinnedWorkflowSource({ root });
21
+ const pin = await read(join(root, 'delivery-runtime/current.json'));
22
+ const value = await read(join(root, 'desktop-runtime/candidate.json'));
23
+ if (!pin || value?.digest !== pin.digest || value?.version !== pin.version || value.app !== join(root, 'desktop-runtime', `SIMY Local-${pin.digest}.app`)) throw Error('Local desktop candidate identity mismatch.');
24
+ const actual = await localRuntimeStatus({ root, source: join(value.app, 'Contents/Resources/cli') });
25
+ if (actual.sourceDigest !== pin.digest || actual.version !== pin.version) throw Error('Local desktop candidate integrity mismatch.');
26
+ return value;
27
+ }
28
+
29
+ export async function suspendDesktopWatchdog(root) {
30
+ const file = join(root, 'desktop-runtime/active.json'), active = await read(file);
31
+ if (active) await save(file, { ...active, suspendedBy: 'user_quit', suspendedAt: Date.now() });
32
+ }
33
+
34
+ export async function activateLocalDesktop({ root, checkOwner = assertNoDesktopHostForOrigin, run = exec } = {}) {
35
+ const value = await candidate(root);
36
+ await checkOwner(root, null); // A live owner is never killed by activation.
37
+ await run('/usr/bin/codesign', ['--verify', '--deep', '--strict', value.app]);
38
+ await run('/usr/bin/open', ['-n', '-a', value.app]);
39
+ await save(join(root, 'desktop-runtime/active.json'), { ...value, activatedAt: Date.now(), scope: 'previously_opted_in_tasks' });
40
+ return { ...value, activation: 'launch_requested' };
41
+ }
42
+
43
+ // External deterministic observer survives host exits and sleep/wake. Only an
44
+ // explicitly activated candidate may be relaunched, with the normal owner lock.
45
+ // A live mismatched/unhealthy host is reported, never forcibly replaced.
46
+ export async function tickRuntimeWatchdog({ root, now = Date.now(), run = exec,
47
+ checkOwner = assertNoDesktopHostForOrigin, observeHost = desktopRuntimeStatus,
48
+ observeMonitor = consistencyMonitorStatus } = {}) {
49
+ await mkdir(join(root, 'desktop-runtime'), { recursive: true, mode: 0o700 });
50
+ const file = join(root, 'desktop-runtime/watchdog.json');
51
+ const previous = await read(file) || {};
52
+ const state = { lastAttemptAt: now, lastSuccessAt: previous.lastSuccessAt || null, ownerPid: process.pid, launches: previous.launches || 0, lastLaunchAt: previous.lastLaunchAt || null,
53
+ host: null, monitor: null, error: null, action: 'observe' };
54
+ try {
55
+ state.host = await observeHost({ root });
56
+ state.monitor = await observeMonitor(join(root, 'delivery-hooks'), now);
57
+ const active = await read(join(root, 'desktop-runtime/active.json'));
58
+ if (active?.suspendedBy === 'user_quit') state.action = 'user_quit_preserved';
59
+ if (active && !active.suspendedBy && state.host.state !== 'matched' && now - (previous.lastLaunchAt || 0) >= 60_000) {
60
+ try { await checkOwner(root, null); }
61
+ catch (error) { if (error.code !== 'process_conflict') throw error; state.action = 'live_owner_preserved'; }
62
+ if (state.action !== 'live_owner_preserved') {
63
+ const value = await candidate(root);
64
+ // Installing another candidate does not authorize its automatic activation.
65
+ if (value.digest !== active.digest) throw Error('Active desktop pin changed; activate the prepared candidate explicitly.');
66
+ await run('/usr/bin/codesign', ['--verify', '--deep', '--strict', value.app]);
67
+ await run('/usr/bin/open', ['-n', '-a', value.app]);
68
+ state.action = 'relaunch_requested'; state.lastLaunchAt = now; state.launches++;
69
+ }
70
+ }
71
+ state.lastSuccessAt = now;
72
+ } catch { state.error = { code: 'runtime_watchdog_failed', at: now }; }
73
+ await save(file, state);
74
+ return state;
75
+ }
76
+ export async function watchLocalRuntime({ root, signal, once = false } = {}) {
77
+ do {
78
+ if (signal?.aborted) break;
79
+ await tickRuntimeWatchdog({ root });
80
+ if (once) break;
81
+ try { await delay(30_000, undefined, { signal }); } catch (error) { if (!signal?.aborted) throw error; }
82
+ } while (!signal?.aborted);
83
+ }
84
+
85
+ export async function installRuntimeWatchdog({ root, home = homedir(), run = exec, platform = process.platform } = {}) {
86
+ if (platform !== 'darwin') throw Error('Desktop watchdog installation currently requires macOS.');
87
+ const active = await read(join(root, 'desktop-runtime/active.json'));
88
+ const value = await candidate(root);
89
+ if (!active || active.digest !== value.digest) throw Error('Activate the verified desktop before installing its watchdog.');
90
+ const folder = join(home, 'Library/LaunchAgents'); await mkdir(folder, { recursive: true });
91
+ const label = 'com.awak.simy.local-runtime-watchdog';
92
+ const plist = join(folder, `${label}.plist`);
93
+ const xml = value => String(value).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
94
+ const content = `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>${label}</string><key>ProgramArguments</key><array><string>${xml(join(root, 'bin/simy-delivery'))}</string><string>runtime</string><string>watch</string></array><key>EnvironmentVariables</key><dict><key>SIMY_HOME</key><string>${xml(root)}</string></dict><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>ThrottleInterval</key><integer>30</integer></dict></plist>`;
95
+ await writeFile(plist, content, { mode: 0o600 });
96
+ await run('/usr/bin/plutil', ['-lint', plist]);
97
+ const domain = `gui/${process.getuid()}`;
98
+ try { await run('/bin/launchctl', ['bootout', `${domain}/${label}`]); } catch { /* First install has no job. */ }
99
+ await run('/bin/launchctl', ['bootstrap', domain, plist]);
100
+ return { label, plist, scope: 'activated_local_desktop_only' };
101
+ }
@@ -1,16 +1,23 @@
1
+ import { retainControllerEvidence } from '../codex-plugins/simy/hooks/purpose-evidence.mjs';
2
+ import { workspaceIdentity, reviewWorkspaceIdentity } from '../session-supervision/purpose-verification.js';
3
+ import { searchEnabled, failureLimitReached } from '../codex-plugins/simy/hooks/purpose-search.mjs';
4
+ import { reconcilePurposeTasks } from '../session-supervision/purpose-recovery.js';
5
+ import { runConsistencyMonitor } from './consistency-monitor.js';
6
+ import { desktopRuntimeStatus } from './desktop-runtime.js';
7
+ import { auditConsistencyTask } from '../session-supervision/consistency-auditor.js';
1
8
  import { minimalSufficientChangeInstruction } from '../codex-plugins/simy/hooks/minimal-sufficient.mjs';
2
9
  import { requireProviderLink } from '../provider-links.js';
3
10
  import { spawn } from 'node:child_process';
4
11
  import { randomUUID } from 'node:crypto';
5
12
  import { dirname } from 'node:path';
6
- import { automaticContinuationAllowed, assessedCheckpointContract, checkpointDelivery, decideStop, deliveryProgressContext, deliveryStop, listRecords, readRecord, managedBudgetReason, notifyState, processIdentity, sharesTaskWorkspace, taskDirectory, reconcileRecords, updateRecord, verifyAutomaticCriteria } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
13
+ import { canExecuteAutonomousExternal, automaticContinuationAllowed, settlePurposeVerification, assessedCheckpointContract, checkpointDelivery, decideStop, deliveryProgressContext, deliveryStop, listRecords, readRecord, managedBudgetReason, notifyState, processIdentity, sharesTaskWorkspace, taskDirectory, reconcileRecords, updateRecord, verifyAutomaticCriteria } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
7
14
  import { resolveBackendExecutable } from '../backend-executable.js';
8
15
  import { executeCodexAppServer } from '../session-supervision/codex-app-server.js';
9
16
  import { acquireWorkspaceLease, leaseOwnerDead } from '../session-supervision/workspace-lease.js';
10
17
 
11
18
  const MAX_OUTPUT = 64 * 1024;
12
- const SAFE_PROVIDER_CODES = new Set(['identity', 'permission_narrowed', 'permission_restore', 'turn_failed', 'checkpoint',
13
- 'closed', 'timeout', 'unsupported', 'unavailable', 'manual_provider_action', 'checkpoint_rejected', 'provider_approval']);
19
+ const SAFE_PROVIDER_CODES = new Set(['authentication_required', 'identity', 'permission_narrowed', 'permission_restore', 'turn_failed', 'checkpoint',
20
+ 'closed', 'timeout', 'unsupported', 'unavailable', 'manual_provider_action', 'checkpoint_rejected', 'provider_approval', 'provider_question', 'usage_unavailable']);
14
21
  function safeProviderCode(code) { return SAFE_PROVIDER_CODES.has(code) ? code : 'provider_error'; }
15
22
  function providerFailureReason(code) {
16
23
  if (code === 'permission_restore') return 'Codex sandbox restoration could not be confirmed; inspect the original conversation before resuming.';
@@ -133,18 +140,21 @@ export async function executeCodexResume(record, { signal, root, leaseId, execut
133
140
  });
134
141
  }
135
142
 
136
- export async function tickManagedTasks({ root, now = Date.now(), execute, inspectProcess = processIdentity, signal, beforeClaim, authorizeDispatch, readRecords } = {}) {
143
+ export async function tickManagedTasks({ root, now = Date.now(), execute, inspectProcess = processIdentity, signal, beforeClaim, authorizeDispatch, readRecords, observeTurn, observeAuthentication } = {}) {
137
144
  const tickWallStart = Date.now();
138
145
  const currentTime = () => now + Math.max(0, Date.now() - tickWallStart);
139
146
  if (signal?.aborted) return;
140
- const records = await reconcileRecords({ root, now, ...(inspectProcess ? { inspectProcess } : {}),
141
- ...(readRecords ? { records: await readRecords() } : {}) });
147
+ const readCandidates = readRecords || (() => listRecords(root));
148
+ await reconcilePurposeTasks({ root, signal, now, observeTurn, observeAuthentication, readRecords: readCandidates });
149
+ await reconcileRecords({ root, now, ...(inspectProcess ? { inspectProcess } : {}), records: await readCandidates() });
142
150
  if (signal?.aborted) return;
151
+ const records = await readCandidates();
143
152
  for (let record of records) {
144
153
  if (signal?.aborted) break;
145
154
  const claimNow = currentTime();
146
155
  let run = record.run;
147
156
  if (!run?.managed) continue;
157
+ if (run.managed.maxTokens && run.managed.outcomeAudit?.lease) continue;
148
158
  if (run.state === 'auto_resuming') {
149
159
  const owner = run.managed.resumeLease?.owner;
150
160
  const ownerDead = await leaseOwnerDead(owner, inspectProcess);
@@ -152,9 +162,10 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
152
162
  if (!ownerDead && (owner || claimNow - run.managed.resumeLease?.claimedAt <= 5 * 60_000)) continue;
153
163
  await updateRecord(root, record.provider, record.session, current => {
154
164
  if (current?.run?.state !== 'auto_resuming' || current.run.managed.resumeLease?.id !== run.managed.resumeLease?.id) return current;
155
- current.run.state = 'recovery_required'; current.run.managed.autoRecovery = false;
165
+ current.run.state = 'recovery_required'; current.run.recoveryCause = 'interrupted_owner'; current.run.managed.autoRecovery = false;
156
166
  current.run.reason = 'Managed recovery was interrupted; inspect the original session before manual resume.';
157
167
  const attempt = current.run.managed.executionAttempt;
168
+ if (attempt?.budgetReservation && attempt.phase === 'dispatching') { attempt.budgetReservation.state = 'unknown'; current.run.managed.unknownConsumption = true; if (current.run.managed.maxTokens) current.run.managed.usageBlocked = true; }
158
169
  if (attempt?.phase === 'dispatching' && attempt.action?.kind !== 'read') current.run.managed.executionUncertain = {
159
170
  attemptId: attempt.id, decisionId: attempt.decisionId, action: attempt.action, at: claimNow };
160
171
  current.run.updatedAt = claimNow; notifyState(current.run, claimNow); return current;
@@ -165,7 +176,7 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
165
176
  try { await requireProviderLink({ root, provider: record.provider }); } catch { continue; }
166
177
  const appServer = run.managed.resumeChannel === 'codex_app_server';
167
178
  const causeAllowed = run.recoveryCause === 'process_exit' || appServer && ['session_supervision', 'approved_action'].includes(run.recoveryCause);
168
- const approvedWait = run.managed.decision?.outcome === 'wait' && ['read', 'local_edit', 'local_test'].includes(run.managed.decision.action?.kind) &&
179
+ const approvedWait = run.managed.decision?.outcome === 'wait' && ['read', 'local_edit', 'local_test', 'external'].includes(run.managed.decision.action?.kind) &&
169
180
  run.managed.approvedAction?.id === run.managed.decision.id && !run.managed.approvedAction.dispatchedAt;
170
181
  if (run.managed.executionUncertain && run.managed.recoveryDiagnosisDecisionId !== run.managed.decision?.id) continue;
171
182
  if (['pending', 'transferred', 'pending_received'].includes(run.managed.handoff?.status)) continue;
@@ -196,17 +207,22 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
196
207
  if (!automaticContinuationAllowed(current)) return current;
197
208
  const appChannel = live?.managed?.resumeChannel === 'codex_app_server';
198
209
  const allowedCause = live?.recoveryCause === 'process_exit' || appChannel && ['session_supervision', 'approved_action'].includes(live?.recoveryCause);
199
- const approved = live?.managed?.decision?.outcome === 'wait' && ['read', 'local_edit', 'local_test'].includes(live.managed.decision.action?.kind) &&
210
+ const approved = live?.managed?.decision?.outcome === 'wait' && ['read', 'local_edit', 'local_test', 'external'].includes(live.managed.decision.action?.kind) &&
200
211
  live.managed.approvedAction?.id === live.managed.decision.id && !live.managed.approvedAction.dispatchedAt;
201
212
  if (!live?.managed?.autoRecovery || live.state !== 'recovery_required' || !allowedCause || live.managed.pendingApproval || live.managed.autonomyPolicy && (!live.managed.decision || live.managed.decision.outcome === 'wait' && !approved) || appChannel && live.managed.nextAttemptAt > currentTime()) return current;
202
213
  const stop = deliveryStop(live, currentTime());
203
214
  if (stop) {
204
- live.state = stop.code === 'failure_limit' ? 'failed' : 'paused'; live.reason = stop.reason; live.updatedAt = currentTime(); notifyState(live, live.updatedAt); return current;
215
+ live.state = stop.code === 'purpose_budget' ? 'budget_exhausted' : stop.code === 'failure_limit' ? 'failed' : 'paused'; live.reason = stop.reason; live.updatedAt = currentTime(); notifyState(live, live.updatedAt); return current;
205
216
  }
206
217
  live.state = 'auto_resuming'; live.managed.resumeLease = { id: leaseId, claimedAt: currentTime(), checkpointRevision: live.revision, decisionId: live.managed.decision?.id || null,
207
218
  owner: { pid: process.pid, identity: ownerIdentity } };
208
219
  live.managed.executionAttempt = { id: leaseId, decisionId: live.managed.decision?.id || null,
209
- action: live.managed.decision?.action || live.managed.proposedAction, phase: 'claimed', claimedAt: currentTime(), lastActivityAt: currentTime() };
220
+ action: live.managed.decision?.action || live.managed.proposedAction,
221
+ budgetReservation: { rootTaskId: live.managed.taskId, tokens: live.managed.maxTokens ? Math.max(0, live.managed.maxTokens - (live.managed.tokensUsed || 0)) : null, state: 'reserved', epoch: leaseId }, phase: 'claimed', claimedAt: currentTime(), lastActivityAt: currentTime() };
222
+ for (const request of live.managed.consistencyOutbox || []) if (['pending', 'received', 'inspected', 'inspection_failed'].includes(request.stage) && request.targetTask === live.managed.taskId) {
223
+ request.stage = 'received'; request.receivedAt = currentTime(); request.decisionId = live.managed.decision?.id;
224
+ request.leaseId = leaseId;
225
+ }
210
226
  live.managed.autoAttempts = (live.managed.autoAttempts || 0) + 1;
211
227
  live.updatedAt = currentTime(); return current;
212
228
  }); } catch (error) {
@@ -220,6 +236,15 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
220
236
  let result;
221
237
  let resource;
222
238
  let lastActivityAt = currentTime();
239
+ let usage = null, usageQueue = Promise.resolve(), usageError = null;
240
+ // Tool and token notifications can arrive together. Share one in-process
241
+ // write queue so our own receipts do not contend for the record lock.
242
+ let receiptQueue = Promise.resolve();
243
+ const persistReceipt = mutate => {
244
+ const pending = receiptQueue.then(() => updateRecord(root, record.provider, record.session, mutate));
245
+ receiptQueue = pending.catch(() => {});
246
+ return pending;
247
+ };
223
248
  let dispatched = false, waitingForTool = false;
224
249
  const controller = new AbortController();
225
250
  const abort = () => controller.abort();
@@ -263,6 +288,14 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
263
288
  await authorizeClaim();
264
289
  const runner = execute || (appServer ? executeCodexAppServer : executeCodexResume);
265
290
  result = await runner(claimed, { signal: controller.signal, root, leaseId, currentTime, leaseMonitored: true,
291
+ onEvidence: async evidence => {
292
+ await persistReceipt(current => {
293
+ if (current?.run?.managed?.resumeLease?.id !== leaseId || current.run.managed.decision?.id !== evidence.actionId) throw new Error('Evidence capture lease changed.');
294
+ if (searchEnabled(current.run.managed)) retainControllerEvidence(current.run.managed, evidence, currentTime());
295
+ return current;
296
+ });
297
+ },
298
+ onUsage: observed => { usage = observed; usageQueue = persistReceipt(current => { if (current?.run?.managed?.resumeLease?.id === leaseId) current.run.managed.executionAttempt.observedUsage = observed; return current; }).catch(error=>{usageError=error;}); if (claimed.run.managed.maxTokens && (claimed.run.managed.tokensUsed || 0) + observed.tokens >= claimed.run.managed.maxTokens) controller.abort(); },
266
299
  onActivity: observation => { lastActivityAt = currentTime(); waitingForTool = observation?.waitingForTool === true; },
267
300
  beforeDispatch: async providerSandbox => {
268
301
  await assertActiveLease(root, record.provider, record.session, leaseId, controller.signal, currentTime);
@@ -272,9 +305,13 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
272
305
  if (!automaticContinuationAllowed(current)) throw new Error('Automatic continuation was disabled before provider dispatch.');
273
306
  if (appServer && !live.managed.providerSandbox) {
274
307
  if (!providerSandbox || !['readOnly', 'workspaceWrite', 'dangerFullAccess', 'externalSandbox'].includes(providerSandbox.type)) throw new Error('Original Codex sandbox policy is unavailable.');
275
- live.managed.providerSandbox = { type: providerSandbox.type,
308
+ live.managed.providerSandbox = { type: providerSandbox.type, networkAccess: providerSandbox.networkAccess === true,
276
309
  ...(providerSandbox.type === 'workspaceWrite' ? { writableRoots: Array.isArray(providerSandbox.writableRoots) ? providerSandbox.writableRoots.filter(value => typeof value === 'string').slice(0, 20) : [] } : {}) };
277
310
  }
311
+ const selected=live.managed.plan?.alternatives?.find(row=>row.id===live.managed.decision?.selectedOptionId);
312
+ const ledger=live.managed.purpose?.evidence;
313
+ if(selected?.dependsOn?.some(id=>!ledger?.units?.some(unit=>unit.id===id && unit.status==='verified' && unit.purposeRevision===ledger.purposeRevision))) throw new Error('Required dependency is not independently verified.');
314
+ if (live.managed.decision?.autonomousExternal && !canExecuteAutonomousExternal(live.managed)) throw new Error('Autonomous external authority changed before dispatch.');
278
315
  const approved = live.managed.approvedAction;
279
316
  live.managed.executionAttempt.phase = 'dispatching';
280
317
  live.managed.executionAttempt.dispatchedAt = currentTime();
@@ -295,13 +332,17 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
295
332
  : error?.code === 'desktop_access_required'
296
333
  ? { code: -3, busy: true, accessDenied: true }
297
334
  : { code: controller.signal.aborted ? -2 : -1, cancelled: controller.signal.aborted,
298
- errorCode: error?.code || 'provider_error', restorationUnverified: error?.restorationUnverified === true }; }
299
- finally { stopWatch(); signal?.removeEventListener('abort', abort); }
335
+ providerTurnId: error?.providerTurnId, question: error?.question, errorCode: error?.code || 'provider_error', restorationUnverified: error?.restorationUnverified === true }; }
336
+ try {
337
+ await usageQueue;
338
+ if (usageError) result = { ...result, code: -1, errorCode: 'usage_unavailable' };
339
+ usage ||= result?.usage || null;
300
340
  if (controller.signal.aborted) result = { ...result, code: -2, cancelled: true };
301
341
  if (result.code === 0) {
302
342
  try {
303
- await checkpointDelivery({ root, provider: record.provider, session: record.session, cwd: record.cwd,
304
- checkpoint: result.checkpoint || parseAgentCheckpoint(result.output, record.session), expectedLease: leaseId, now: Date.now() });
343
+ if (result.verification) await settlePurposeVerification({ root, provider: record.provider, session: record.session, expectedLease: leaseId, review: result.verification, verifySnapshot: current => reviewWorkspaceIdentity(current, root, result.verification, { signal: controller.signal }), now: Date.now() });
344
+ else await checkpointDelivery({ root, provider: record.provider, session: record.session, cwd: record.cwd,
345
+ checkpoint: result.checkpoint || parseAgentCheckpoint(result.output, record.session), expectedLease: leaseId, toolEvidence: result.toolEvidence || [], sourceContext: result.sourceContext, controllerEvidence: result.controllerEvidence, now: Date.now() });
305
346
  await updateRecord(root, record.provider, record.session, current => {
306
347
  if (current?.run?.managed?.executionAttempt?.id !== leaseId) return current;
307
348
  current.run.managed.executionAttempt.phase = 'checkpointed';
@@ -318,16 +359,27 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
318
359
  }
319
360
  return current;
320
361
  });
321
- } catch { result = { code: -1, errorCode: 'checkpoint_rejected' }; }
362
+ } catch (error) { result = { ...result, code: -1, errorCode: 'checkpoint_rejected' }; }
322
363
  }
323
364
  await updateRecord(root, record.provider, record.session, async current => {
324
365
  const live = current?.run;
325
366
  if (live?.managed?.resumeLease?.id !== leaseId) return current;
326
367
  delete live.managed.resumeLease;
368
+ if (searchEnabled(live.managed) && !result.busy && (dispatched || result.code === 0)) live.continuations += 1;
369
+ if (live.managed.executionAttempt?.budgetReservation) live.managed.executionAttempt.budgetReservation.state = usage && result.code===0 ? 'settled' : dispatched ? 'unknown' : 'released';
370
+ if (dispatched && (!usage || result.code!==0)) {live.managed.unknownConsumption = true;if(live.managed.maxTokens) live.managed.usageBlocked=true;}
371
+ if (usage) {
372
+ live.managed.tokensUsed = (live.managed.tokensUsed || 0) + usage.tokens;
373
+ live.managed.usageAvailable = true;
374
+ live.managed.lastUsage = { ...usage, attemptId: leaseId };
375
+ } else if (!result.busy && live.managed.maxTokens && dispatched) {
376
+ live.managed.usageAvailable = false; live.managed.usageBlocked = true;
377
+ }
327
378
  const attempt = live.managed.executionAttempt;
328
379
  if (!result.busy && attempt?.phase === 'dispatching' && attempt.action?.kind !== 'read') live.managed.executionUncertain = {
329
380
  attemptId: leaseId, decisionId: attempt.decisionId, action: attempt.action, at: Date.now() };
330
381
  live.managed.attempts ||= [];
382
+ if (live.managed.purpose?.enabled && result.providerTurnId) live.managed.purpose.lastOwnedTurnId = result.providerTurnId;
331
383
  live.managed.attempts.push({ id: leaseId, startedAt: claimed.run.managed.resumeLease.claimedAt, endedAt: Date.now(),
332
384
  checkpointRevision: live.revision, outcome: result.busy ? 'busy' : result.cancelled ? 'cancelled' : result.code === 0 ? 'checkpoint' : 'failed',
333
385
  errorCode: result.code === 0 || result.busy || result.cancelled ? null : safeProviderCode(result.errorCode),
@@ -354,15 +406,24 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
354
406
  live.state = 'recovery_required'; live.recoveryCause = 'session_supervision';
355
407
  live.reason = result.accessDenied ? 'Sign in to a SIMY organization with SQM and Developer access before continuing.' : 'Original Codex conversation is in use; SIMY will retry after the writer lock becomes available.';
356
408
  live.managed.nextAttemptAt = Date.now() + 15_000;
409
+ } else if (result.errorCode === 'authentication_required') {
410
+ live.state = 'blocked'; live.blocker = 'external_dependency'; live.blockerSource = 'connection'; live.managed.autoRecovery = false;
411
+ live.managed.authenticationWait = { since: Date.now(), nextCheckAt: Date.now() + 60_000 };
412
+ live.reason = 'The configured provider is not logged in. SIMY will recheck account availability and resume diagnosis when restored within the existing budget.';
413
+ } else if (result.errorCode === 'provider_question') {
414
+ live.state = 'blocked'; live.blocker = 'missing_information'; live.blockerSource = 'question'; live.managed.autoRecovery = false;
415
+ live.managed.userQuestion = result.question || 'Inspect the original conversation for the unanswered provider question.';
416
+ live.reason = 'Investigate the provider question; use existing evidence for routine decisions and preserve genuine user choices.';
357
417
  } else if (result.errorCode === 'provider_approval') {
358
- live.state = 'blocked'; live.blocker = 'authorization'; live.managed.autoRecovery = false;
418
+ live.state = 'blocked'; live.blocker = 'authorization'; live.blockerSource = 'provider'; live.managed.autoRecovery = false;
419
+ live.managed.nativeWait = { turnId: result.providerTurnId || null, observedAt: Date.now() };
359
420
  live.reason = 'Codex requires provider approval or user input in the original conversation.';
360
421
  } else if (result.cancelled) {
361
422
  live.managed.autoRecovery = false; live.state = 'recovery_required'; live.recoveryCause = 'shutdown_cancelled';
362
423
  live.reason = 'Managed recovery was cancelled; inspect the original session before manual resume.';
363
424
  } else if (result.code !== 0) {
364
425
  live.failures += 1; live.managed.autoRecovery = false;
365
- live.state = live.failures >= live.maxFailures ? 'failed' : 'recovery_required';
426
+ live.state = failureLimitReached(live) ? 'failed' : 'recovery_required';
366
427
  live.recoveryCause = 'provider_error';
367
428
  live.reason = appServer ? providerFailureReason(safeProviderCode(result.errorCode)) +
368
429
  (result.restorationUnverified ? ' Original provider permissions also require inspection.' : '') :
@@ -370,7 +431,7 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
370
431
  } else if (live.revision <= run.revision) {
371
432
  live.failures += 1;
372
433
  live.reason = result.code === 0 ? 'Codex recovery returned without a checkpoint.' : 'Codex recovery failed; inspect the original session.';
373
- live.state = live.failures >= live.maxFailures ? 'failed' : 'recovery_required';
434
+ live.state = failureLimitReached(live) ? 'failed' : 'recovery_required';
374
435
  live.recoveryCause = 'process_exit';
375
436
  } else {
376
437
  if (live.checkpoint?.status === 'completed') {
@@ -393,18 +454,25 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
393
454
  // Settling uncertainty must precede unlocking: otherwise another session
394
455
  // could start a write in the effect/checkpoint persistence gap.
395
456
  await resource?.release({ unconfirmed: result.restorationUnverified === true });
457
+ } finally { stopWatch(); signal?.removeEventListener('abort', abort); }
396
458
  }
397
459
  }
398
460
 
399
461
  export function createManagedSupervisor(options) {
400
- let running = null, closed = false;
462
+ let running = null, auditing = null, closed = false;
401
463
  const controller = new AbortController();
402
464
  return {
403
465
  tick() {
404
466
  if (closed) return Promise.resolve();
467
+ if (!auditing) auditing = (async () => {
468
+ const runtime = await desktopRuntimeStatus({ root: dirname(options.root) }).catch(() => ({ state: 'runtime_observation_failed' }));
469
+ await runConsistencyMonitor({ root: options.root, runtime, readRecords: options.readRecords || (() => listRecords(options.root)),
470
+ onAudit: (record, findings, request) => auditConsistencyTask(record, findings, request, {
471
+ root: options.root, signal: controller.signal, authorize: options.authorizeDispatch || (async () => {}) }) });
472
+ })().catch(() => {}).finally(() => { auditing = null; });
405
473
  if (!running) running = tickManagedTasks({ ...options, signal: controller.signal }).finally(() => { running = null; });
406
474
  return running;
407
475
  },
408
- async close() { closed = true; controller.abort(); await running; },
476
+ async close() { closed = true; controller.abort(); await Promise.allSettled([running, auditing]); },
409
477
  };
410
478
  }
@@ -23,12 +23,16 @@ import { spawn, execFile } from "node:child_process";
23
23
  import { promisify, isDeepStrictEqual } from "node:util";
24
24
  import { createInterface } from "node:readline";
25
25
  import { fileURLToPath } from "node:url";
26
+ import { pathToFileURL } from "node:url";
26
27
  import { realpathSync } from "node:fs";
27
28
  import { dirname, join } from "node:path";
28
29
  import { homedir } from "node:os";
29
30
  import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
30
31
  import { startAgent } from "./agent.js";
31
32
  import { createRuntimeDiagnostics, captureRuntimeOutput } from "./runtime-diagnostics.js";
33
+ import { suspendDesktopWatchdog } from './delivery-hooks/runtime-watchdog.js';
34
+ import { attestDesktopRuntime } from './delivery-hooks/desktop-runtime.js';
35
+ import { pinnedWorkflowSource } from './delivery-hooks/local-runtime.js';
32
36
  import { applyLoginShellPath } from "./login-shell-path.js";
33
37
  import { createCliAutoUpdater, readCliPackageInfo } from "./auto-update.js";
34
38
  import { createAgentShutdown } from "./shutdown.js";
@@ -139,6 +143,9 @@ export async function startDesktopHost({ input = process.stdin, output = process
139
143
  throw new Error("Desktop update supervisor is no longer running.");
140
144
  }
141
145
  const lock = candidate ? null : await acquireDesktopProcessLock(root, { webOrigin });
146
+ if (lock && !dependencies.startAgent) {
147
+ try { await attestDesktopRuntime({ root }); } catch (error) { await lock.release(); throw error; }
148
+ }
142
149
  const supervisorToken = lock?.nonce || env.SIMY_DESKTOP_SUPERVISOR_TOKEN;
143
150
  const deliveryRoot = join(root, "delivery-hooks");
144
151
  const recordReader = createRecordReader(deliveryRoot);
@@ -970,6 +977,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
970
977
  const status = await agent.getDesktopStatus();
971
978
  if ((status.tasks?.activeCount || 0) + (status.sqmTasks?.activeCount || 0) > 0) throw new Error("Tasks are still running. Confirm before quitting.");
972
979
  }
980
+ if (command.forUpdate !== true) await suspendDesktopWatchdog(root);
973
981
  // Cancel first, then acknowledge. The native app waits for the host
974
982
  // exit; this deadline also covers a stuck remote flush/discovery.
975
983
  const deadline = setTimeout(async () => {
@@ -1455,7 +1463,7 @@ export function isDesktopHostEntry(entry = process.argv[1]) {
1455
1463
  catch { return false; }
1456
1464
  }
1457
1465
 
1458
- if (isDesktopHostEntry()) {
1466
+ export function startDesktopEntry() {
1459
1467
  let host;
1460
1468
  const stop = () => { void host?.close().finally(() => process.exit(0)); };
1461
1469
  process.once("SIGTERM", stop); process.once("SIGINT", stop);
@@ -1476,3 +1484,18 @@ if (isDesktopHostEntry()) {
1476
1484
  process.exitCode = 1;
1477
1485
  });
1478
1486
  }
1487
+
1488
+ if (isDesktopHostEntry()) {
1489
+ try {
1490
+ const root = process.env.SIMY_HOME?.trim() || join(homedir(), '.simy');
1491
+ const selected = await pinnedWorkflowSource({ root });
1492
+ if (selected) {
1493
+ process.env.SIMY_AUTO_UPDATE = '0';
1494
+ const selectedHost = await import(pathToFileURL(join(selected, 'src/desktop-host.js')).href);
1495
+ selectedHost.startDesktopEntry();
1496
+ } else startDesktopEntry();
1497
+ } catch {
1498
+ 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');
1499
+ process.exitCode = 1;
1500
+ }
1501
+ }