@awak-app/simy-cli 0.5.28 → 0.5.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -223,6 +223,11 @@ keeps downloaded releases in the user cache, then replaces the desktop bundle
223
223
  after the app and its executor have stopped. The previous bundle is retained
224
224
  for recovery. If permissions or verification prevent replacement, SIMY shows
225
225
  an installer recovery action instead of reporting the desktop app as current.
226
+ Concurrent handoffs share one exclusive installation lock. Packages before
227
+ 0.5.29 may require one system-authorized installer update because their bundle
228
+ root is not writable by administrators. New packages grant the admin group
229
+ write access only to that root directory so later signed updates can exchange
230
+ the app; standard users still use the authorized installer when required.
226
231
  Handoff diagnostics are written to `~/.simy/installer/desktop-update.log`.
227
232
  Closing the window with **⌘W** keeps SIMY running in the menu bar; reopening it
228
233
  restores the existing window. **⌘Q** quits the app and its service.
@@ -337,6 +342,14 @@ SQM deliberately has two identity-bound stages:
337
342
  state transitions, invariants, stress/strength expectations, and scenarios,
338
343
  writes `proof.json`, and signs that proof with the local device identity.
339
344
 
345
+ The full-check summary reports applicable modules, evaluated rules, scenario
346
+ statuses and blocking/advisory scope. `passed` with `no_applicable_rules` or zero
347
+ scenario evaluations does not establish feature or historical incident coverage.
348
+ Scenario statuses come from deterministic rule evaluation, not browser/provider
349
+ execution. Keep concrete regression and delivery acceptance evidence separately.
350
+ If knowledge is unavailable, the command returns exit 2 with the service cause
351
+ and writes no new proof; any older artifact is not evidence for that attempt.
352
+
340
353
  Normal runs do not require a separate sync command: the CLI
341
354
  uses a verified local cache and revalidates it with TTL and ETag semantics.
342
355
  The Agentic Loop runs min-check after each code modification and the full check
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.5.28",
3
+ "version": "0.5.30",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,7 +40,7 @@
40
40
  "check:auto-update": "node --check ./src/auto-update.js && node --check ./scripts/auto-update-e2e.js && npm run check:sqm",
41
41
  "check:sqm": "node --check ./src/sqm/canonical.js && node --check ./src/sqm/validation.js && node --check ./src/sqm/reliability.js && node --check ./src/sqm/repository.js && node --check ./src/sqm/checkers.js && node --check ./src/sqm/bundle-store.js && node --check ./src/sqm/proof.js && node --check ./src/sqm/evidence-client.js && node --check ./src/sqm/session.js && node --check ./src/sqm/index.js && node --check ./src/sqm/command.js && node --check ./src/sqm/agent-skill.js && node --check ./src/sqm/workflow-onboarding.js && node --check ./src/sqm/workflow-update.js && node --check ./src/sqm/review-worker.js && node --check ./scripts/sqm-complete-loop-e2e.js && node --check ./scripts/sqm-local-review-e2e.js",
42
42
  "check:real-provider-syntax": "node --check ./scripts/real-codex-probe.js && node --check ./scripts/real-claude-probe.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/claude-parity-runtime-e2e.js && node --check ./scripts/claude-plugin-runtime-e2e.js",
43
- "check:syntax": "node --check ./src/index.js && node --check ./src/install-update.js && node --check ./src/agent.js && node --check ./src/connection-discovery.js && node --check ./src/codex-plugins/simy/hooks/connection-reminder.mjs && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/durable-local-task-worker.js && node --check ./src/durable-local-task-steps-contract.js && node --check ./src/bounded-local-task-subtasks-contract.js && node --check ./src/bounded-local-task-subtask-pool.js && node --check ./src/local-task-artifact-contract.js && node --check ./src/execution-capability-contract.js && node --check ./src/shutdown.js && node --check ./src/run-registry.js && node --check ./src/direct-executor.js && node --check ./src/execution-guardrail.js && node --check ./src/backend-executable.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/completion-contract.js && node --check ./src/orchestrator/pr-preparation.js && node --check ./src/orchestrator/minimum-completion.js && node --check ./src/orchestrator/delivery/scripts/check-minimum-completion.mjs && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./scripts/fixtures/completion-contract-fixture.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/completion-contract-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/local-task-default-workspace-e2e.js && node --check ./scripts/local-task-idempotency-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/console-recovery-e2e-fixture.js && node --check ./scripts/console-recovery-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js",
43
+ "check:syntax": "node --check ./src/index.js && node --check ./src/install-update.js && node --check ./src/agent.js && node --check ./src/connection-discovery.js && node --check ./src/codex-plugins/simy/hooks/connection-reminder.mjs && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/durable-local-task-worker.js && node --check ./src/durable-local-task-steps-contract.js && node --check ./src/bounded-local-task-subtasks-contract.js && node --check ./src/bounded-local-task-subtask-pool.js && node --check ./src/local-task-artifact-contract.js && node --check ./src/execution-capability-contract.js && node --check ./src/shutdown.js && node --check ./src/run-registry.js && node --check ./src/direct-executor.js && node --check ./src/execution-guardrail.js && node --check ./src/backend-executable.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/completion-contract.js && node --check ./src/orchestrator/pr-preparation.js && node --check ./src/orchestrator/minimum-completion.js && node --check ./src/orchestrator/delivery/scripts/check-minimum-completion.mjs && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./src/coding-run-store.js && node --check ./scripts/fixtures/completion-contract-fixture.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/completion-contract-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/local-task-default-workspace-e2e.js && node --check ./scripts/local-task-idempotency-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/console-recovery-e2e-fixture.js && node --check ./scripts/console-recovery-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js",
44
44
  "check:package": "node ./scripts/check-package-contents.js",
45
45
  "build:macos-installer:dev": "bash packaging/build-macos-arm64.sh --unsigned-dev",
46
46
  "build:macos-installer": "bash packaging/build-macos-arm64.sh --production",
package/src/agent.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  createRun,
11
11
  increaseLocalCodingRunProviderTokenBudget,
12
12
  LocalRunRegistry,
13
+ markCodingRunMonitorFailure,
13
14
  isLocalRepositoryApprovalPending,
14
15
  pauseLocalCodingRun,
15
16
  queueLocalCodingGuidance,
@@ -19,10 +20,12 @@ import {
19
20
  restoreRun,
20
21
  resumeLocalCodingRun,
21
22
  startLocalCodingRun,
23
+ syncLocalCodingRun,
22
24
  stopLocalCodingRun,
23
25
  toCompatibleCodingLoopState,
24
26
  toLedgerSnapshot,
25
27
  } from "./runner.js";
28
+ import { codingRunsRoot, loadCodingRunSnapshots, saveCodingRunSnapshot } from "./coding-run-store.js";
26
29
  import {
27
30
  expiresAtFromNow,
28
31
  isSessionValid,
@@ -134,6 +137,7 @@ import {
134
137
  } from "./connection-discovery.js";
135
138
 
136
139
  const DEVICE_HEARTBEAT_INTERVAL_MS = 20_000;
140
+ const CODING_RUN_MONITOR_INTERVAL_MS = 5 * 60_000;
137
141
 
138
142
  export async function startAgent({
139
143
  requestedPort = 0,
@@ -148,6 +152,7 @@ export async function startAgent({
148
152
  } = {}) {
149
153
  const apiOrigin = resolveWebOrigin(webOrigin);
150
154
  const registry = new LocalRunRegistry();
155
+ const codingRunStateRoot = codingRunsRoot(sessionRoot);
151
156
  const directRegistry = new DirectTaskRegistry();
152
157
  const localTaskRegistry = new LocalTaskRegistry();
153
158
  const taskStorageRoot = localTasksRoot(
@@ -217,6 +222,8 @@ export async function startAgent({
217
222
  let port = requestedPort;
218
223
  let heartbeatTimer = null;
219
224
  let heartbeatInFlight = null;
225
+ let codingRunMonitorTimer = null;
226
+ let codingRunMonitorInFlight = null;
220
227
  let shuttingDown = false;
221
228
  const notifyDesktopStatusChange = () => {
222
229
  if (shuttingDown) return;
@@ -296,6 +303,14 @@ export async function startAgent({
296
303
  return loginUrl.toString();
297
304
  };
298
305
 
306
+ const publishOrSyncRestoredRun = async (run) => {
307
+ if (run.restoredFromState || run.restoredObjectiveUnverified) {
308
+ await publishRestoredLocalCodingRun(run);
309
+ } else {
310
+ syncLocalCodingRun(run);
311
+ }
312
+ };
313
+
299
314
  const restoreRemoteRuns = async () => {
300
315
  if (shuttingDown || !isSessionValid(session, Date.now(), apiOrigin) || !session?.device_id) return;
301
316
  const snapshots = dependencies.listRemoteRuns
@@ -309,24 +324,113 @@ export async function startAgent({
309
324
  apiBaseUrl: session.api_base_url,
310
325
  token: session.token,
311
326
  });
327
+ const remoteIds = new Set();
312
328
  let restored = false;
313
329
  for (const snapshot of snapshots) {
314
330
  if (shuttingDown) return;
315
- if (!snapshot?.id || registry.has(snapshot.id)) continue;
331
+ if (!snapshot?.id) continue;
332
+ remoteIds.add(snapshot.id);
333
+ if (registry.has(snapshot.id)) {
334
+ const localRun = registry.get(snapshot.id);
335
+ localRun.session = session;
336
+ localRun.apiOrigin = apiOrigin;
337
+ const remoteTime = Date.parse(snapshot.updated_at || "") || 0;
338
+ const localTime = Date.parse(localRun.snapshot.updated_at || "") || 0;
339
+ if (remoteTime > localTime) {
340
+ const repository = findRepository(repositoryInventory, snapshot.charter?.repository);
341
+ const remoteRun = restoreRun({
342
+ snapshot, session, apiOrigin,
343
+ localPath: repository?.local_path || null,
344
+ localStateRoot: codingRunStateRoot,
345
+ });
346
+ Object.assign(localRun, {
347
+ request: remoteRun.request,
348
+ status: remoteRun.status,
349
+ snapshot: remoteRun.snapshot,
350
+ controlState: remoteRun.controlState,
351
+ completedAt: remoteRun.completedAt,
352
+ restoredFromState: remoteRun.restoredFromState,
353
+ restoredObjectiveUnverified: remoteRun.restoredObjectiveUnverified,
354
+ });
355
+ await saveCodingRunSnapshot(codingRunStateRoot, localRun.snapshot);
356
+ await publishRestoredLocalCodingRun(localRun);
357
+ } else {
358
+ await publishOrSyncRestoredRun(localRun);
359
+ }
360
+ continue;
361
+ }
316
362
  const repository = findRepository(repositoryInventory, snapshot.charter?.repository);
317
363
  const restoredRun = restoreRun({
318
364
  snapshot,
319
365
  session,
320
366
  apiOrigin,
321
367
  localPath: repository?.local_path || null,
368
+ localStateRoot: codingRunStateRoot,
322
369
  });
323
370
  registry.create(restoredRun);
324
371
  restored = true;
325
372
  await publishRestoredLocalCodingRun(restoredRun);
326
373
  }
374
+ for (const localRun of registry.list()) {
375
+ if (!remoteIds.has(localRun.id)) await publishOrSyncRestoredRun(localRun);
376
+ }
327
377
  if (restored) notifyDesktopStatusChange();
328
378
  };
329
379
 
380
+ const restoreLocalCodingRuns = async () => {
381
+ const { snapshots, errors } = await loadCodingRunSnapshots(codingRunStateRoot);
382
+ for (const error of errors) {
383
+ reportStartupConnectionError(new Error(`Coding run ${error.id}: ${error.message}`), quiet);
384
+ }
385
+ for (const snapshot of snapshots) {
386
+ const boundDevice = snapshot?.metadata?.execution_device_id;
387
+ if (boundDevice && boundDevice !== session?.device_id) continue;
388
+ if (registry.has(snapshot.id)) {
389
+ const existing = registry.get(snapshot.id);
390
+ existing.session = session;
391
+ existing.apiOrigin = apiOrigin;
392
+ continue;
393
+ }
394
+ try {
395
+ const repository = findRepository(repositoryInventory, snapshot.charter?.repository);
396
+ const run = restoreRun({
397
+ snapshot, session, apiOrigin,
398
+ localPath: repository?.local_path || null,
399
+ localStateRoot: codingRunStateRoot,
400
+ });
401
+ // Keep the persisted timestamp until the Web ledger has been compared.
402
+ // Publishing recovery here would make an older local record look newest.
403
+ registry.create(run);
404
+ } catch (error) {
405
+ reportStartupConnectionError(error, quiet);
406
+ }
407
+ }
408
+ };
409
+
410
+ const monitorCodingRuns = () => {
411
+ if (!daemon || !isSessionValid(session, Date.now(), apiOrigin)) return Promise.resolve();
412
+ if (codingRunMonitorInFlight) return codingRunMonitorInFlight;
413
+ codingRunMonitorInFlight = (async () => {
414
+ for (const run of registry.list()) {
415
+ if (!["pr_ready_for_review", "merge_ready"].includes(run.status) || run.operation || run.child) continue;
416
+ try {
417
+ await (dependencies.recheckCodingRun || recheckLocalCodingRun)(run);
418
+ } catch (error) {
419
+ await markCodingRunMonitorFailure(run, error);
420
+ }
421
+ }
422
+ })().finally(() => { codingRunMonitorInFlight = null; });
423
+ return codingRunMonitorInFlight;
424
+ };
425
+
426
+ const startCodingRunMonitor = () => {
427
+ if (!daemon || codingRunMonitorTimer) return;
428
+ codingRunMonitorTimer = setInterval(() => {
429
+ void monitorCodingRuns().catch((error) => reportStartupConnectionError(error, quiet));
430
+ }, dependencies.codingRunMonitorIntervalMs || CODING_RUN_MONITOR_INTERVAL_MS);
431
+ codingRunMonitorTimer.unref();
432
+ };
433
+
330
434
  const heartbeat = () => {
331
435
  if (shuttingDown || !isSessionValid(session, Date.now(), apiOrigin)) return Promise.resolve(false);
332
436
  if (heartbeatInFlight) return heartbeatInFlight;
@@ -406,7 +510,9 @@ export async function startAgent({
406
510
  reportHeartbeatError(error, quiet);
407
511
  }
408
512
  try {
513
+ await restoreLocalCodingRuns();
409
514
  await restoreRemoteRuns();
515
+ startCodingRunMonitor();
410
516
  } catch (error) {
411
517
  reportStartupConnectionError(error, quiet);
412
518
  }
@@ -1389,8 +1495,10 @@ export async function startAgent({
1389
1495
  },
1390
1496
  session,
1391
1497
  apiOrigin,
1498
+ localStateRoot: codingRunStateRoot,
1392
1499
  });
1393
1500
  try {
1501
+ await saveCodingRunSnapshot(codingRunStateRoot, run.snapshot);
1394
1502
  registry.create(run);
1395
1503
  } catch (error) {
1396
1504
  await cleanupRunAttachments(stagedAttachments);
@@ -1678,8 +1786,10 @@ export async function startAgent({
1678
1786
  await refreshAuthorizedRepositoryInventory();
1679
1787
  if (shuttingDown) return;
1680
1788
  await heartbeat();
1789
+ await restoreLocalCodingRuns();
1681
1790
  if (shuttingDown) return;
1682
1791
  await restoreRemoteRuns();
1792
+ startCodingRunMonitor();
1683
1793
  } catch (error) {
1684
1794
  reportStartupConnectionError(error, quiet);
1685
1795
  }
@@ -1703,6 +1813,8 @@ export async function startAgent({
1703
1813
  shuttingDown = true;
1704
1814
  shutdownPromise = (async () => {
1705
1815
  if (heartbeatTimer) clearInterval(heartbeatTimer);
1816
+ if (codingRunMonitorTimer) clearInterval(codingRunMonitorTimer);
1817
+ if (codingRunMonitorInFlight) await codingRunMonitorInFlight;
1706
1818
  updateManager?.stop?.();
1707
1819
  await sqmLocalReviewWorker?.close?.();
1708
1820
  await durableLocalTaskWorker?.close?.();
@@ -1824,7 +1936,8 @@ export async function startAgent({
1824
1936
  });
1825
1937
  const runId = String(remoteRun?.id || "");
1826
1938
  if (!runId) throw new Error("SIMY Web did not return an Agentic Loop run id.");
1827
- const run = createRun({ runId, request, session, apiOrigin });
1939
+ const run = createRun({ runId, request, session, apiOrigin, localStateRoot: codingRunStateRoot });
1940
+ await saveCodingRunSnapshot(codingRunStateRoot, run.snapshot);
1828
1941
  registry.create(run);
1829
1942
  void startLocalCodingRun(run, runOptions);
1830
1943
  return run;
@@ -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.28";
16
+ export const CLI_VERSION = "0.5.30";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function withCliContract(
@@ -14,7 +14,9 @@ Treat the current Codex thread, its attachments, and the active repository as th
14
14
  3. Run `simy sqm min-check` from the repository root.
15
15
  4. Run `simy sqm check --proof proof.json --signed-evidence proof.json.signed.json`.
16
16
  5. Read `proof.json` and require `outcome.status` to be `passed` before reporting success or proposing a pull request.
17
- 6. Report the checked repository/revision, proof paths, outcome, blockers, and any evidence gap.
17
+ 6. Verify the proof's repository, base, HEAD and working-tree/diff identity against the candidate, and verify its bound signature with SQM tooling. Use separate artifact paths outside the diff; do not edit between min-check and check or reuse stale proof after a candidate change.
18
+ 7. Report outcome reasons, matched modules, evaluated rules and scenario statuses from `executions`, separating blocking and advisory results. `passed` with `no_applicable_rules`, zero scenario evaluations, or advisory-only checks is not feature or incident coverage. Scenario statuses are deterministic evaluations, not evidence that a browser, provider or deployed journey ran. Record relevant uncovered review findings and their concrete regression evidence or remaining gap.
19
+ 8. Report the checked repository/revision, proof paths, outcome, blockers, and any evidence gap. Keep delivery acceptance pending for unverified applicable paths even when SQM passes.
18
20
 
19
21
  If implementation is required before the check can pass, obtain or confirm the user's change authorization and make the smallest relevant repair. A request to run SQM alone does not authorize unrelated edits, a PR, merge, deployment, or external messages.
20
22
 
@@ -0,0 +1,69 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { homedir } from "node:os";
3
+ import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+
6
+ import { redactExecutionValue } from "./orchestrator/execution-io.js";
7
+
8
+ const RUN_ID = /^[A-Za-z0-9_-]{1,160}$/;
9
+ const writes = new Map();
10
+
11
+ export function codingRunsRoot(sessionRoot) {
12
+ return path.resolve(sessionRoot || path.join(homedir(), ".simy"), "coding-runs");
13
+ }
14
+
15
+ export async function saveCodingRunSnapshot(root, snapshot) {
16
+ const id = String(snapshot?.id || "");
17
+ if (!RUN_ID.test(id)) throw new Error("Invalid coding run ID for durable storage.");
18
+ // Capture at enqueue time: later mutations must not rewrite a queued version.
19
+ snapshot = redactExecutionValue(snapshot);
20
+ const directory = path.resolve(root);
21
+ const key = path.join(directory, id);
22
+ const previous = writes.get(key) || Promise.resolve();
23
+ const current = previous.catch(() => undefined).then(async () => {
24
+ await mkdir(directory, { recursive: true, mode: 0o700 });
25
+ const body = JSON.stringify(snapshot);
26
+ const document = JSON.stringify({
27
+ version: 1,
28
+ sha256: createHash("sha256").update(body).digest("hex"),
29
+ snapshot,
30
+ });
31
+ const target = `${key}.json`;
32
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
33
+ await writeFile(temporary, `${document}\n`, { mode: 0o600 });
34
+ await rename(temporary, target);
35
+ });
36
+ writes.set(key, current);
37
+ try { await current; } finally {
38
+ if (writes.get(key) === current) writes.delete(key);
39
+ }
40
+ }
41
+
42
+ export async function loadCodingRunSnapshots(root) {
43
+ let files;
44
+ try {
45
+ files = await readdir(root, { withFileTypes: true });
46
+ } catch (error) {
47
+ if (error?.code === "ENOENT") return { snapshots: [], errors: [] };
48
+ throw error;
49
+ }
50
+ const snapshots = [];
51
+ const errors = [];
52
+ for (const file of files) {
53
+ if (!file.isFile() || !file.name.endsWith(".json")) continue;
54
+ const id = file.name.slice(0, -5);
55
+ if (!RUN_ID.test(id)) continue;
56
+ try {
57
+ const document = JSON.parse(await readFile(path.join(root, file.name), "utf8"));
58
+ const body = JSON.stringify(document.snapshot);
59
+ const hash = createHash("sha256").update(body).digest("hex");
60
+ if (document.version !== 1 || document.sha256 !== hash || document.snapshot?.id !== id) {
61
+ throw new Error("Coding run record failed integrity check.");
62
+ }
63
+ snapshots.push(document.snapshot);
64
+ } catch (error) {
65
+ errors.push({ id, message: error instanceof Error ? error.message : String(error) });
66
+ }
67
+ }
68
+ return { snapshots, errors };
69
+ }
package/src/local-task.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { buildWorkflowReportingInstruction } from "./workflow-reporting.js";
1
2
  import { execFile } from "node:child_process";
2
3
  import { createHash, randomUUID } from "node:crypto";
3
4
  import { EventEmitter, once } from "node:events";
@@ -957,6 +958,7 @@ function localTaskProviderInstruction(task) {
957
958
  : null;
958
959
  return [
959
960
  "You are the local executor for a SIMY Local Task, not an Agentic Loop.",
961
+ buildWorkflowReportingInstruction(),
960
962
  "Complete this bounded task once. Do not create Git branches, commits, pull requests, audits, or automatic retries.",
961
963
  `Permission mode: ${task.permission_mode}.`,
962
964
  `Task workspace: ${task.workspace_path}`,
@@ -16,6 +16,7 @@ import {
16
16
  } from "./pr-preparation.js";
17
17
  import {
18
18
  completionEvidenceChecks,
19
+ hasUserOutcomeProof,
19
20
  normalizeVisualReview,
20
21
  } from "./completion-contract.js";
21
22
 
@@ -340,6 +341,8 @@ export function evaluatePrReadiness(charter, attempt) {
340
341
  readinessCheck("minimum_independent_binding",
341
342
  Boolean(independentAudit?.evidence_identity_sha256 && independentAudit.evidence_identity_sha256 === independentAuditEvidenceIdentity(attempt)),
342
343
  "Independent review must cover the current head, base, CI, skips, preparation rows, companion revisions, and minimum evidence."),
344
+ readinessCheck("user_outcome_proven", hasUserOutcomeProof(independentAudit?.coverage_review),
345
+ "Independent review must observe the original user outcome and cite its evidence."),
343
346
  readinessCheck("implementation_gate", attempt.audit?.passed === true, "Implementation gate must pass."),
344
347
  readinessCheck("pr_preparation_gate", preparation.passed, preparation.detail),
345
348
  readinessCheck(
@@ -8,12 +8,12 @@ export const ACCEPTANCE_EVIDENCE_PATH = fileURLToPath(
8
8
  export function buildChecklistContinuationInstruction({ auditor = false } = {}) {
9
9
  return section("checklist_continuation_contract", [
10
10
  `The table is a progress checkpoint, not the deliverable or a stopping condition. Read: ${ACCEPTANCE_EVIDENCE_PATH}`,
11
- "Show the complete 依頼チェックリスト with columns #, ご依頼, 現状, 判定 after chartering, each evaluated attempt, and in the final receipt. Preserve stable criterion IDs, all rows and both applicable evidence layers. ✅ requires current valid passing evidence for every applicable layer; ❌ means an observed failure; 🟡 means pending, missing, stale, blocked or unverifiable evidence. Show counts and highest proven state.",
12
- "After presenting any non-green row, classify its cause and immediately take the next safe authorized action: implement missing behavior, repair a failure, exercise an untested path, collect missing evidence, or rerun stale verification. Re-observe the same criterion against the current candidate, run the validator, update the table, and repeat until every applicable requested outcome and gate for the authorized endpoint is verified. A next-action list without performing feasible actions is not completion. Never require another continue request merely because a table was shown.",
11
+ "Maintain the complete 依頼チェックリスト with columns #, ご依頼, 現状, 判定 in the evidence record after chartering, each evaluated attempt, and in the final receipt. User-facing reporting follows workflow_reporting_contract; show the full table only when requested or when it is the most useful requested answer, never as mandatory commentary. Preserve stable criterion IDs, all rows and both applicable evidence layers. ✅ requires current valid passing evidence for every applicable layer; ❌ means an observed failure; 🟡 means pending, missing, stale, blocked or unverifiable evidence. Retain counts and highest proven state in that record.",
12
+ "After observing any non-green row, classify its cause and immediately take the next safe authorized action: implement missing behavior, repair a failure, exercise an untested path, collect missing evidence, or rerun stale verification. Re-observe the same criterion against the current candidate, run the validator, update the table, and repeat until every applicable requested outcome and gate for the authorized endpoint is verified. A next-action list without performing feasible actions is not completion. Never require another continue request merely because a table was shown.",
13
13
  "For CI or other in-progress dependencies, use the available bounded wait/recheck or approved monitoring mechanism and maintain progress communication. Unchanged external state is not itself a blocker. Do not start duplicate jobs or busy-poll. Recheck current revision/results, repair authorized failures, and keep waiting only within the existing time, attempt and cost limits. If waiting cannot continue, report the concrete limit and explicit resumption condition; do not silently abandon it or promise an unscheduled follow-up.",
14
14
  "If a row genuinely requires new authority, credentials, a user/product decision, an unavailable dependency or an exhausted safety budget, retain it as non-green and record affected IDs, evidence of the blocker, safe alternatives attempted, exact missing input, owner and resumption trigger. Continue independent in-scope work. Stop with blocked, best_available, failed or cancelled as appropriate, explicitly unfinished, not verified_complete. Never raise budgets, bypass a stop, or expand authority to force green.",
15
15
  "Do not end successfully while a safe authorized repair, verification or bounded wait remains. All-green applies to the frozen authorized endpoint: PR-only work does not authorize merge/deploy, but a PR URL, an improvement PR, an issue, a narrower test or a future plan does not satisfy an unmet outcome. Never drop/merge away rows, weaken criteria, relabel applicable checks as not_applicable, recolor unknowns, or copy new digests onto old evidence to force green. Do not change the requested endpoint to match the work already completed.",
16
- "Before the final response, independently reconcile the original request with all criterion IDs and current evidence, check remaining endpoint gates, and display the full table and explicit disposition. Distinguish a partial artifact from verified completion and state both yellow/red counts and what remains. A pr_ready_for_review or blocked controller state is not proof of overall completion. These instructions strengthen continuation and reporting; they do not add a background scheduler or remove controller limits.",
16
+ "Before the final response, independently reconcile the original request with all criterion IDs and current evidence, check remaining endpoint gates, and preserve the full table and explicit disposition in the evidence record. Distinguish a partial artifact from verified completion and disclose material remaining outcomes in the requested response format; keep yellow/red counts in the record. A pr_ready_for_review or blocked controller state is not proof of overall completion. These instructions strengthen continuation and reporting; they do not add a background scheduler or remove controller limits.",
17
17
  auditor
18
18
  ? "Remain read-only. Check that the executor performed feasible follow-up actions after non-green tables. Report premature stopping, omitted rows, unsupported green claims, or a blocker without evidence as findings, alongside exact criterion IDs and the necessary next action. Do not approve from a table's colors or the executor's completion wording."
19
19
  : "Execute the continuation within this attempt instead of merely returning a yellow table or delegating feasible remaining work to the requester. Return control honestly at a genuine boundary; never fabricate completion evidence.",
@@ -4,6 +4,13 @@ import { cleanString, stringArray } from "./shared.js";
4
4
  export const COMPLETION_CONTRACT_VERSION = 2;
5
5
  const criterionIdPattern = /^[A-Za-z][A-Za-z0-9]*(?:[-_.][A-Za-z0-9]+)*$/;
6
6
 
7
+ export function hasUserOutcomeProof(coverage) {
8
+ const proof = coverage?.user_outcome;
9
+ return proof?.passed === true && Boolean(cleanString(proof.verification_method)) &&
10
+ Boolean(cleanString(proof.observed_result)) && Array.isArray(proof.evidence_refs) &&
11
+ proof.evidence_refs.some((ref) => Boolean(cleanString(ref)));
12
+ }
13
+
7
14
  export function buildCompletionContract({
8
15
  inherited,
9
16
  requirement,
@@ -10,6 +10,7 @@ import {
10
10
  import { budgetState, resolveRunBudgets } from "./budget.js";
11
11
  import { classifyRisk } from "./risk.js";
12
12
  import { buildCompletionContract } from "./completion-contract.js";
13
+ import { freezeObjective } from "./outcome-monitor.js";
13
14
  import { defaultPrPreparationContract } from "./pr-preparation.js";
14
15
  import {
15
16
  DESKTOP_EXECUTION_TARGET,
@@ -56,11 +57,11 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
56
57
  cleanString(charterContext.acceptance_criteria_source) === "explicit"
57
58
  ? "explicit"
58
59
  : acceptanceCriteria.length > 0 &&
59
- cleanString(charterContext.acceptance_criteria_source) === "default"
60
- ? "default"
60
+ cleanString(charterContext.acceptance_criteria_source) === "request"
61
+ ? "request"
61
62
  : acceptanceCriteria.length > 0
62
63
  ? "explicit"
63
- : "default";
64
+ : "request";
64
65
  const inheritedRequiredChecks = stringArray(charterContext.required_checks);
65
66
  const inheritedNonGoals = stringArray(charterContext.non_goals);
66
67
  const inheritedArtifacts = stringArray(charterContext.artifacts_required);
@@ -71,7 +72,7 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
71
72
  acceptanceCriteria.length > 0
72
73
  ? acceptanceCriteria
73
74
  : [
74
- "The requested change is implemented without unrelated scope.",
75
+ cleanString(request.requirement),
75
76
  "Relevant tests pass and are recorded.",
76
77
  `A pull request targets ${baseBranch}.`,
77
78
  ];
@@ -216,6 +217,7 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
216
217
  updated_at: now,
217
218
  };
218
219
  snapshot.budget = budgetState(snapshot);
220
+ freezeObjective(snapshot.charter);
219
221
  return snapshot;
220
222
  }
221
223
 
@@ -30,6 +30,28 @@ deployment, exact-revision verification, and cleanup gates in this workflow. Do
30
30
  push these development-only gates into every personal workflow merely because
31
31
  they are required here.
32
32
 
33
+ ## Report outcomes in the requested form
34
+
35
+ Apply the common [outcome reporting contract](../../workflow-reporting.md) to
36
+ executor progress, user-facing summaries and independent review. The runtime
37
+ injects the same instruction into Coding execution/audit and non-coding Local
38
+ Tasks; this is a model instruction, not a semantic output filter or a new gate.
39
+ Keep the outcome contract, execution evidence and response contract distinct.
40
+ Reporting-only corrections do not waive acceptance, change the goal or grant
41
+ new authority. Preserve requested questions/order/omissions on retry and resume.
42
+ Respond to the user's decisions and outcomes; keep routine diagnostics and
43
+ unchanged waits in the existing evidence record. Follow requested cadence and
44
+ host-required liveness updates without inventing progress.
45
+
46
+ ## Confirm asynchronous handoffs and recover safely
47
+
48
+ Apply the common asynchronous handoff contract in [outcome reporting](../../workflow-reporting.md).
49
+ Submission accepted is not execution started. Keep the original operation identity,
50
+ resolve the canonical resource and verify the requested start/outcome boundary.
51
+ Missing list entries are inconclusive. Reconcile before resubmitting; use supported
52
+ idempotent recovery and bounded waits, and report an actual blocker when recovery
53
+ cannot proceed. A promise to investigate does not satisfy the recovery obligation.
54
+
33
55
  ## Confirm the delivery level in chat
34
56
 
35
57
  Before material implementation, read [delivery levels](references/delivery-levels.md),
@@ -147,11 +169,12 @@ checklists held only in chat or the model's memory are insufficient.
147
169
  Update the matrix and table after every evaluated test, repair, review, or scope,
148
170
  revision, or environment change. Preserve prior snapshots/receipts and reset
149
171
  invalidated rows to pending; never delete a failed criterion to make the table
150
- green. Report the new highest proven state and the remaining rows.
172
+ green. Record the new highest proven state and the remaining rows internally;
173
+ user-facing updates follow the response contract.
151
174
 
152
- Use the compact request checklist as the loop's default visible progress surface.
175
+ Use the compact request checklist as the evidence record's status surface.
153
176
  After freezing the charter, after each evaluated attempt, and in the final
154
- receipt, show a Markdown table in chat headed `依頼チェックリスト` with exactly
177
+ evidence receipt, maintain a Markdown table headed `依頼チェックリスト` with
155
178
  these columns: `#`, `ご依頼`, `現状`, `判定`. Render one numbered row per frozen
156
179
  criterion, combining its user and system verification layers into one honest
157
180
  status:
@@ -176,7 +199,10 @@ make the presentation green.
176
199
 
177
200
  ### Continue after the table; do not stop at yellow
178
201
 
179
- The table is an intermediate checkpoint. Immediately classify each non-green row
202
+ The evidence table is an intermediate checkpoint, not mandatory chat output.
203
+ Show it when requested or when it best answers the request; a specified response
204
+ format takes precedence. Otherwise report the outcome and material gaps concisely.
205
+ Immediately classify each non-green row
180
206
  and perform the next safe authorized action: implementation or repair for missing
181
207
  or failed behavior, execution/evidence collection for untested paths, and fresh
182
208
  verification for stale results. Re-observe the same criterion, run the validator,
@@ -201,8 +227,10 @@ non-green. All-green targets only the authorized endpoint, not unrequested merge
201
227
  or deployment. Never weaken criteria or raise budgets to force green.
202
228
 
203
229
  Before the final response, reconcile original requirements, all IDs, current
204
- evidence and endpoint gates; display the full table, counts, highest proven state
205
- and disposition. Independent audit checks premature stopping as well as evidence.
230
+ evidence and endpoint gates; retain the full table, counts, highest proven state
231
+ and disposition in the evidence receipt. The user-facing answer follows the response
232
+ contract and discloses material unmet outcomes. Independent audit checks premature
233
+ stopping, answer relevance and hidden limitations as well as evidence.
206
234
 
207
235
  For user-facing outcomes, require both goal-driven user interaction and technical
208
236
  invariant checks against the same run and context. For saving, test save/reload/
@@ -395,6 +423,65 @@ from an earlier one. In particular:
395
423
 
396
424
  ## Run one controlled loop
397
425
 
426
+ The controller freezes the original user goal, outcome, minimum scope, proof
427
+ requirements, non-goals, finish line, and stable acceptance IDs in the persisted
428
+ charter. If explicit acceptance criteria are absent, the user's goal itself is
429
+ the first acceptance criterion; tests and PR creation cannot replace that
430
+ outcome. Direct human changes create a successor version with the old contract
431
+ preserved. An unrecorded edit blocks execution and PR recheck. An approved
432
+ revision invalidates the previous independent outcome assessment; do not reuse
433
+ old evidence to claim the revised goal was met.
434
+ Even when a caller supplies its own acceptance list, independent review must
435
+ separately prove the original user goal with an observed operation and
436
+ postcondition. A process-only acceptance list cannot waive that goal.
437
+
438
+ After each attempt the controller records which acceptance IDs were positively
439
+ supported by the independent auditor. If no independent review occurred, report
440
+ insufficient evidence. When a further reviewed attempt leaves the same user
441
+ outcomes unproven without adding any independently supported outcome, stop
442
+ automatic retries and require a revised approach against the approved objective.
443
+ Changed code, passing tests, and additional review comments do not themselves
444
+ count as outcome progress. Each state transition is written to a private local
445
+ run record before it is published; failed Web ledger updates are retried. On
446
+ restart, an unverifiable objective cannot retain completion status. An
447
+ authenticated SIMY CLI daemon rechecks PR-ready runs every five minutes,
448
+ including after the chat ends. It revokes a completion claim when current
449
+ verification is unavailable. Monitoring requires the daemon and host to remain
450
+ running; it does not monitor unrelated Codex Desktop tasks or execute repairs
451
+ without an authorized run.
452
+
453
+ For a Codex Desktop task whose requested outcome must be checked after the
454
+ conversation ends, register a Codex app heartbeat against that task before
455
+ claiming it is monitored. Preserve the original outcome, acceptance evidence,
456
+ the exact task or operation identity, the requested completion boundary, and
457
+ the response preferences, cadence, time/attempt/cost limits, and stop or
458
+ ask-user conditions in the heartbeat prompt. Verify that the automation
459
+ is active and bound to the intended task. On each run, use the Codex app's
460
+ supported task status and wait/read operations to distinguish a queued creation
461
+ ID, a resolved task ID, execution start, and the actual result. An absent list
462
+ entry or an idle label alone is not a terminal result. Reconcile the exact
463
+ operation before any retry; do not create a replacement while its fate is
464
+ unknown. Report an actionable blocker if identity, access, or evidence cannot
465
+ be resolved. Stay quiet on unchanged non-actionable state, and stop the
466
+ heartbeat when the accepted outcome is proved or monitoring is explicitly
467
+ cancelled. On authoritative failure, exhausted limits, or an unresolved
468
+ terminal identity/access blocker, report the evidence and pause or stop the
469
+ heartbeat pending a human decision. This is a separately registered,
470
+ task-scoped Codex app monitor;
471
+ the CLI daemon does not automatically register it or cover every Desktop task.
472
+ For a delegated implementation task, compare monitoring cadence with actual
473
+ execution. If its canonical task is idle and authorized acceptance work remains,
474
+ record a stall rather than a quiet unchanged status. With explicit recovery
475
+ authority and supported task tools, wake the same task once and verify its
476
+ execution starts; keep the outcome open
477
+ until independent acceptance evidence changes. A read-only monitor reports the
478
+ stall and owner instead. If a task is active but acceptance evidence has not
479
+ advanced over the agreed window, inspect its work and external waits before
480
+ calling it stalled. Never describe monitoring or an attempted wake as delivery.
481
+ Use the [Desktop task monitoring triggers](references/desktop-monitor-triggers.md)
482
+ when choosing an observation cadence, stall threshold, resume action, notification,
483
+ and duplicate-prevention rule for a Codex app heartbeat.
484
+
398
485
  For each attempt:
399
486
 
400
487
  1. **Observe:** load the current snapshot and revision-bound evidence.