@awak-app/simy-cli 0.5.61 → 0.5.62

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
@@ -631,3 +631,20 @@ cross-view consistency. The workflow requires independent design, implementation
631
631
  To-Be and PR reviews, plus explicitly opted-in SQM before and after PR creation.
632
632
  The [workflow reference](src/orchestrator/delivery/references/shared-system-specifications.md)
633
633
  explains baseline updates, historical regression coverage and evidence limits.
634
+
635
+
636
+ ### Desktop SQM history
637
+
638
+ SQM Tasks has separate **Analysis** and **Checks** lists on macOS and Windows.
639
+ Analysis retains local review summaries; Checks retains both `simy sqm min-check`
640
+ and `simy sqm check` outcomes, including checks run by the Delivery Loop.
641
+ Each list keeps the latest 100 runs under `SIMY_HOME/sqm-history` (default
642
+ `~/.simy/sqm-history`), isolated by web origin, account and organization.
643
+ History remains visible with local analysis off and after restarting. A saved
644
+ in-flight analysis is shown as Interrupted until this client claims it again;
645
+ it does not contribute to the running count. The web workspace owns retry status.
646
+
647
+ Only summary metadata is retained, without analysis prompts, source inputs,
648
+ model responses or credentials. Historical summaries are not a replacement for
649
+ signed proof files or delivery acceptance. Records from versions that kept
650
+ analysis only in memory cannot be recovered after that process exits.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.5.61",
3
+ "version": "0.5.62",
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:session-supervision-syntax": "node --check ./src/session-supervision/index.js && node --check ./src/session-supervision/codex-app-server.js && node --check ./src/session-supervision/claude-sessions.js && node --check ./src/session-supervision/original-session-target.js && node --check ./src/session-supervision/action-association.js && node --check ./src/session-supervision/action-association-host.js && node --check ./src/session-supervision/action-association-api.js && node --check ./src/session-supervision/association-store.js && node --check ./src/session-supervision/action-deadline.js && node --check ./src/session-supervision/action-deadline-store.js && node --check ./src/codex-plugins/simy/hooks/action-association.mjs",
41
41
  "check:auto-update": "node --check ./src/auto-update.js && node --check ./scripts/auto-update-e2e.js && npm run check:sqm",
42
42
  "check:workflow-observation-sender": "node --check ./src/workflow-observation-sender.js",
43
- "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",
43
+ "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/history-store.js && node --check ./src/sqm/review-worker.js && node --check ./scripts/sqm-complete-loop-e2e.js && node --check ./scripts/sqm-local-review-e2e.js",
44
44
  "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",
45
45
  "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/codex-plugins/simy/hooks/dev-work.mjs && node --check ./src/dev-work-events.js && 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 && node --check ./src/codex-plugins/simy/hooks/delivery-loop.mjs && node --check ./src/delivery-hooks/command.js && node --check ./src/delivery-hooks/desktop.js && node --check ./src/delivery-hooks/supervisor.js && node --check ./src/session-supervision/automatic-continuation.js",
46
46
  "check:package": "node ./scripts/check-package-contents.js",
package/src/agent.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createSqmHistoryStore, projectSqmHistory, sqmHistoryScope } from "./sqm/history-store.js";
1
2
  import { createDesktopAccessPolicy, desktopAccessReady, requireDesktopAccess } from './desktop-access.js';
2
3
  import { readSqmEnabled, writeSqmEnabled } from "./desktop-sqm-settings.js";
3
4
  import { createServer } from "node:http";
@@ -287,6 +288,7 @@ export async function startAgent({
287
288
  };
288
289
  refreshAvailableCapabilities();
289
290
  let durableLocalTaskWorker = null;
291
+ const sqmHistory = createSqmHistoryStore({ root: sessionRoot });
290
292
  let sqmLocalReviewWorker = null;
291
293
  let codexWorkPatternSync = null;
292
294
  let codexWorkPatternSyncInFlight = null;
@@ -541,7 +543,7 @@ export async function startAgent({
541
543
  const remoteRun = restoreRun({
542
544
  snapshot, session, apiOrigin,
543
545
  localPath: repository?.local_path || null,
544
- localStateRoot: codingRunStateRoot,
546
+ localStateRoot: codingRunStateRoot, sqmHistoryRoot: sessionRoot,
545
547
  });
546
548
  Object.assign(localRun, {
547
549
  request: remoteRun.request,
@@ -565,7 +567,7 @@ export async function startAgent({
565
567
  session,
566
568
  apiOrigin,
567
569
  localPath: repository?.local_path || null,
568
- localStateRoot: codingRunStateRoot,
570
+ localStateRoot: codingRunStateRoot, sqmHistoryRoot: sessionRoot,
569
571
  });
570
572
  registry.create(restoredRun);
571
573
  restored = true;
@@ -597,7 +599,7 @@ export async function startAgent({
597
599
  const run = restoreRun({
598
600
  snapshot, session, apiOrigin,
599
601
  localPath: repository?.local_path || null,
600
- localStateRoot: codingRunStateRoot,
602
+ localStateRoot: codingRunStateRoot, sqmHistoryRoot: sessionRoot,
601
603
  });
602
604
  // Keep the persisted timestamp until the Web ledger has been compared.
603
605
  // Publishing recovery here would make an older local record look newest.
@@ -836,6 +838,9 @@ export async function startAgent({
836
838
  return (dependencies.runSqmLocalReview || ((value, selected, options) => runStructuredProviderReview(selected, value, options)))(request, provider, { beforeDispatch });
837
839
  },
838
840
  onChange: notifyDesktopStatusChange,
841
+ getHistoryScope: () => sqmHistoryScope(apiOrigin, session),
842
+ recordHistory: (scope, item) => sqmHistory.record(scope, item),
843
+ onHistoryError: () => { if (!quiet) console.error("Could not save the local SQM analysis history."); },
839
844
  getProvider: () => !desktopWorkAllowed() || availableCapabilities?.features?.sqm_local_review !== true ? null : ["codex", "claude"].find(
840
845
  (provider) => availableCapabilities?.backends?.[provider] === true &&
841
846
  providerStatuses[provider]?.state === "ready",
@@ -1869,7 +1874,7 @@ export async function startAgent({
1869
1874
  },
1870
1875
  session,
1871
1876
  apiOrigin,
1872
- localStateRoot: codingRunStateRoot,
1877
+ localStateRoot: codingRunStateRoot, sqmHistoryRoot: sessionRoot,
1873
1878
  });
1874
1879
  try {
1875
1880
  await saveCodingRunSnapshot(codingRunStateRoot, run.snapshot);
@@ -2213,6 +2218,12 @@ export async function startAgent({
2213
2218
  server.on("close", () => {
2214
2219
  void shutdown().catch((error) => reportDurableLocalTaskError(error, quiet));
2215
2220
  });
2221
+ const currentHistoryScope = () => isSessionValid(session, Date.now(), apiOrigin) ? sqmHistoryScope(apiOrigin, session) : null;
2222
+ const historyForCurrentScope = async () => {
2223
+ const scope = currentHistoryScope();
2224
+ const items = await sqmHistory.items(scope).catch(() => []);
2225
+ return JSON.stringify(scope) === JSON.stringify(currentHistoryScope()) ? items : [];
2226
+ };
2216
2227
  const getDesktopStatus = async () => {
2217
2228
  const auth = desktopAuthStatus(session, { webOrigin: apiOrigin });
2218
2229
  if (auth.state === "signed_in" && organizationState === "unknown") void refreshOrganizations();
@@ -2240,7 +2251,7 @@ export async function startAgent({
2240
2251
  activeJobId: sqmLocalReviewWorker?.snapshot?.().active_job_id ?? null,
2241
2252
  issue: sqmLocalReviewWorker?.snapshot?.().issue ?? null,
2242
2253
  providerReady: [codex, claude].some(provider => provider?.state === "ready") },
2243
- sqmTasks: sqmLocalReviewWorker?.desktopTasks?.() ?? { activeCount: 0, items: [] },
2254
+ ...projectSqmHistory(await historyForCurrentScope(), sqmLocalReviewWorker?.desktopTasks?.()),
2244
2255
  update: updateManager?.snapshot?.() ?? null,
2245
2256
  codex,
2246
2257
  claude,
@@ -2286,7 +2297,16 @@ export async function startAgent({
2286
2297
  void heartbeat().catch(error => reportHeartbeatError(error, quiet));
2287
2298
  },
2288
2299
  getDesktopStatus,
2289
- getDesktopTask: (key) => sqmLocalReviewWorker?.desktopTaskDetail?.(key) ?? desktopTaskDetail([registry, directRegistry, localTaskRegistry], key),
2300
+ getDesktopTask: async (key) => {
2301
+ if (key?.startsWith("sqm:") || key?.startsWith("sqm-check:")) {
2302
+ const scope = currentHistoryScope();
2303
+ if (!scope) return null;
2304
+ const detail = await sqmHistory.detail(scope, key).catch(() => null);
2305
+ if (JSON.stringify(scope) !== JSON.stringify(currentHistoryScope())) return null;
2306
+ return sqmLocalReviewWorker?.desktopTaskDetail?.(key) ?? detail;
2307
+ }
2308
+ return desktopTaskDetail([registry, directRegistry, localTaskRegistry], key);
2309
+ },
2290
2310
  async refreshDesktopStatus() {
2291
2311
  if (isSessionValid(session, Date.now(), apiOrigin)) {
2292
2312
  await heartbeat().catch((error) => reportHeartbeatError(error, quiet));
@@ -2350,7 +2370,7 @@ export async function startAgent({
2350
2370
  });
2351
2371
  const runId = String(remoteRun?.id || "");
2352
2372
  if (!runId) throw new Error("SIMY Web did not return an Agentic Loop run id.");
2353
- const run = createRun({ runId, request, session, apiOrigin, localStateRoot: codingRunStateRoot });
2373
+ const run = createRun({ runId, request, session, apiOrigin, localStateRoot: codingRunStateRoot, sqmHistoryRoot: sessionRoot });
2354
2374
  await saveCodingRunSnapshot(codingRunStateRoot, run.snapshot);
2355
2375
  registry.create(run);
2356
2376
  await authorizeDesktopWork(scope);
@@ -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.61";
16
+ export const CLI_VERSION = "0.5.62";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function withCliContract(
@@ -214,6 +214,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
214
214
  for (const item of sessionStatus.sessions) automaticContinuation?.revoke(item.provider, item.session);
215
215
  if (currentAccess.state !== 'checking') await analysisManager.update([]);
216
216
  status.tasks = { activeCount: status.tasks?.activeCount || 0, items: [] };
217
+ status.sqmChecks = { activeCount: 0, items: [] };
217
218
  status.sqmTasks = { activeCount: status.sqmTasks?.activeCount || 0, items: [] };
218
219
  status.deliveryHooks = { runs: [], providers: {} };
219
220
  status.sessionSupervision = { sessions: [], providers: {}, refreshedAt: null };
package/src/runner.js CHANGED
@@ -86,7 +86,7 @@ const PR_PREPARATION_IDS = new Set(PR_PREPARATION_CHECKS.map((row) => row.id));
86
86
 
87
87
  export { LocalRunRegistry };
88
88
 
89
- export function createRun({ runId, request, session, apiOrigin, localStateRoot = null }) {
89
+ export function createRun({ runId, request, session, apiOrigin, localStateRoot = null, sqmHistoryRoot = null }) {
90
90
  const emitter = new EventEmitter();
91
91
  const normalizedRequest = {
92
92
  ...request,
@@ -111,6 +111,7 @@ export function createRun({ runId, request, session, apiOrigin, localStateRoot =
111
111
  session,
112
112
  apiOrigin,
113
113
  localStateRoot,
114
+ sqmHistoryRoot,
114
115
  status: "queued",
115
116
  lastOutput: "",
116
117
  startedAt: null,
@@ -160,7 +161,7 @@ export function createRun({ runId, request, session, apiOrigin, localStateRoot =
160
161
  };
161
162
  }
162
163
 
163
- export function restoreRun({ snapshot, session, apiOrigin, localPath = null, localStateRoot = null }) {
164
+ export function restoreRun({ snapshot, session, apiOrigin, localPath = null, localStateRoot = null, sqmHistoryRoot = null }) {
164
165
  if (!snapshot || typeof snapshot !== "object" || !snapshot.charter) {
165
166
  throw new Error("Cannot restore an invalid agentic loop snapshot.");
166
167
  }
@@ -170,6 +171,7 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null, loc
170
171
  session,
171
172
  apiOrigin,
172
173
  localStateRoot,
174
+ sqmHistoryRoot,
173
175
  request: {
174
176
  backend: charter.backend === "claude" ? "claude" : "codex",
175
177
  execution_target: normalizeDesktopExecutionTarget(charter.execution_target),
@@ -678,6 +680,8 @@ async function runLocalCodingRun(
678
680
  token: run.session?.token,
679
681
  endpoint: sqmBundleEndpoint(run.session),
680
682
  evidenceEndpoint: sqmEvidenceEndpoint(run.session),
683
+ webOrigin: run.apiOrigin,
684
+ historyRoot: run.sqmHistoryRoot,
681
685
  accountId: run.session?.account_id || run.session?.auth_user_id || run.session?.device_id,
682
686
  organizationId: run.session?.organization_id || run.session?.org_id,
683
687
  deviceId: run.session?.device_id,
@@ -751,6 +755,8 @@ export async function recheckLocalCodingRun(run, { collectEvidence } = {}) {
751
755
  token: run.session?.token,
752
756
  endpoint: sqmBundleEndpoint(run.session),
753
757
  evidenceEndpoint: sqmEvidenceEndpoint(run.session),
758
+ webOrigin: run.apiOrigin,
759
+ historyRoot: run.sqmHistoryRoot,
754
760
  accountId: run.session?.account_id || run.session?.auth_user_id || run.session?.device_id,
755
761
  organizationId: run.session?.organization_id || run.session?.org_id,
756
762
  deviceId: run.session?.device_id,
@@ -1,3 +1,4 @@
1
+ import { createSqmCheckRecorder } from "./history-store.js";
1
2
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
3
  import { homedir } from "node:os";
3
4
  import path from "node:path";
@@ -46,54 +47,63 @@ export async function runSqmCommand(argv, { cliVersion }) {
46
47
  organizationId,
47
48
  };
48
49
 
49
- if (argv[0] === "min-check") {
50
- const result = await runSqmMinCheck(common);
51
- const output = option(argv, "--output") || defaultMinResultPath();
52
- await atomicJson(output, result);
53
- if (argv.includes("--json")) console.log(JSON.stringify(result, null, 2));
54
- else printMin(result, output);
55
- return result.status === "unavailable" ? 2 : result.status === "preliminary_failed" ? 1 : 0;
56
- }
57
-
58
- const minPath = option(argv, "--min-result") || defaultMinResultPath();
59
- let minCheck;
50
+ const save = createSqmCheckRecorder({ scope: { webOrigin, accountId, organizationId },
51
+ phase: argv[0] === "min-check" ? "min" : "full", cwd: common.cwd, baseBranch: common.baseBranch });
60
52
  try {
61
- minCheck = JSON.parse(await readFile(minPath, "utf8"));
53
+ if (argv[0] === "min-check") {
54
+ const result = await runSqmMinCheck(common);
55
+ const output = option(argv, "--output") || defaultMinResultPath();
56
+ await atomicJson(output, result);
57
+ await save(result);
58
+ if (argv.includes("--json")) console.log(JSON.stringify(result, null, 2));
59
+ else printMin(result, output);
60
+ return result.status === "unavailable" ? 2 : result.status === "preliminary_failed" ? 1 : 0;
61
+ }
62
+
63
+ const minPath = option(argv, "--min-result") || defaultMinResultPath();
64
+ let minCheck;
65
+ try {
66
+ minCheck = JSON.parse(await readFile(minPath, "utf8"));
67
+ } catch (error) {
68
+ if (error?.code !== "ENOENT") throw error;
69
+ console.warn("No min-check result was found; running the deterministic min-check first.");
70
+ minCheck = await runSqmMinCheck(common);
71
+ await atomicJson(minPath, minCheck);
72
+ }
73
+ if (minCheck.status === "unavailable") { await save(minCheck); return printUnavailable(minCheck, argv); }
74
+ const result = await runSqmCheck({ ...common, minCheck, deviceId, signingKeyPath: option(argv, "--signing-key") || undefined });
75
+ if (result.status === "unavailable") { await save(result); return printUnavailable(result, argv); }
76
+ const proofPath = path.resolve(option(argv, "--proof") || "proof.json");
77
+ const evidencePath = path.resolve(option(argv, "--signed-evidence") || `${proofPath}.signed.json`);
78
+ const diagnosticsPath = path.resolve(option(argv, "--diagnostics") || `${proofPath}.diagnostics.json`);
79
+ await Promise.all([
80
+ atomicJson(proofPath, result.proof),
81
+ atomicJson(evidencePath, result.signed_evidence),
82
+ atomicJson(diagnosticsPath, {
83
+ policy_version: "sqm-reliability.v1",
84
+ matcher_diagnostics: result.matcher_diagnostics,
85
+ }),
86
+ ]);
87
+ let upload = { uploaded: false, reason: "disabled" };
88
+ if (!argv.includes("--no-upload")) {
89
+ const evidenceEndpoint = process.env.SIMY_SQM_EVIDENCE_URL || (validSession?.api_base_url ? new URL("sqm/run-evidence", validSession.api_base_url).href : undefined);
90
+ upload = await uploadRunEvidence({ endpoint: evidenceEndpoint, token, proof: result.proof, signedEvidence: result.signed_evidence });
91
+ }
92
+ result.upload = upload;
93
+ result.artifacts = {
94
+ min_check: minPath,
95
+ proof: proofPath,
96
+ signed_evidence: evidencePath,
97
+ diagnostics: diagnosticsPath,
98
+ };
99
+ await save(result);
100
+ if (argv.includes("--json")) console.log(JSON.stringify(result, null, 2));
101
+ else printFull(result);
102
+ return result.proof.outcome.status === "failed" ? 1 : 0;
62
103
  } catch (error) {
63
- if (error?.code !== "ENOENT") throw error;
64
- console.warn("No min-check result was found; running the deterministic min-check first.");
65
- minCheck = await runSqmMinCheck(common);
66
- await atomicJson(minPath, minCheck);
67
- }
68
- if (minCheck.status === "unavailable") return printUnavailable(minCheck, argv);
69
- const result = await runSqmCheck({ ...common, minCheck, deviceId, signingKeyPath: option(argv, "--signing-key") || undefined });
70
- if (result.status === "unavailable") return printUnavailable(result, argv);
71
- const proofPath = path.resolve(option(argv, "--proof") || "proof.json");
72
- const evidencePath = path.resolve(option(argv, "--signed-evidence") || `${proofPath}.signed.json`);
73
- const diagnosticsPath = path.resolve(option(argv, "--diagnostics") || `${proofPath}.diagnostics.json`);
74
- await Promise.all([
75
- atomicJson(proofPath, result.proof),
76
- atomicJson(evidencePath, result.signed_evidence),
77
- atomicJson(diagnosticsPath, {
78
- policy_version: "sqm-reliability.v1",
79
- matcher_diagnostics: result.matcher_diagnostics,
80
- }),
81
- ]);
82
- let upload = { uploaded: false, reason: "disabled" };
83
- if (!argv.includes("--no-upload")) {
84
- const evidenceEndpoint = process.env.SIMY_SQM_EVIDENCE_URL || (validSession?.api_base_url ? new URL("sqm/run-evidence", validSession.api_base_url).href : undefined);
85
- upload = await uploadRunEvidence({ endpoint: evidenceEndpoint, token, proof: result.proof, signedEvidence: result.signed_evidence });
104
+ await save({ status: "failed" });
105
+ throw error;
86
106
  }
87
- result.upload = upload;
88
- result.artifacts = {
89
- min_check: minPath,
90
- proof: proofPath,
91
- signed_evidence: evidencePath,
92
- diagnostics: diagnosticsPath,
93
- };
94
- if (argv.includes("--json")) console.log(JSON.stringify(result, null, 2));
95
- else printFull(result);
96
- return result.proof.outcome.status === "failed" ? 1 : 0;
97
107
  }
98
108
 
99
109
  function printHelp() {
@@ -0,0 +1,108 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
3
+ import { basename, join } from "node:path";
4
+ import { formatSqmCoverage } from "./report.js";
5
+ import { defaultSessionRoot } from "../session-store.js";
6
+
7
+ const LIMIT = 100;
8
+ const hash = value => createHash("sha256").update(value).digest("hex");
9
+
10
+ export function sqmHistoryScope(webOrigin, session) {
11
+ if (!session?.account_id || !session?.organization_id) return null;
12
+ return { webOrigin: new URL(webOrigin).origin, accountId: session.account_id, organizationId: session.organization_id };
13
+ }
14
+
15
+ // One atomic file per run prevents the daemon and independent CLI commands from
16
+ // overwriting each other's history. Only display metadata is persisted.
17
+ export function createSqmHistoryStore({ root = defaultSessionRoot(), now = () => Date.now() } = {}) {
18
+ const cache = new Map();
19
+ const directory = scope => scope?.webOrigin && scope?.accountId && scope?.organizationId
20
+ ? join(root, "sqm-history", hash(JSON.stringify([new URL(scope.webOrigin).origin, scope.accountId, scope.organizationId]))) : null;
21
+ async function list(scope) {
22
+ const dir = directory(scope);
23
+ if (!dir) return [];
24
+ const cached = cache.get(dir);
25
+ if (cached && now() - cached.at < 3000) return cached.items;
26
+ let names;
27
+ try { names = await readdir(dir); } catch (error) { if (error.code === "ENOENT") return []; throw error; }
28
+ const items = (await Promise.all(names.filter(name => /^[a-f0-9]{64}\.json$/.test(name)).map(async name => {
29
+ try {
30
+ const text = await readFile(join(dir, name), "utf8");
31
+ if (text.length > 16_384) return null;
32
+ return metadata(JSON.parse(text));
33
+ } catch { return null; }
34
+ }))).filter(Boolean).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
35
+ cache.set(dir, { at: now(), items });
36
+ return items;
37
+ }
38
+ return {
39
+ async record(scope, entry) {
40
+ const dir = directory(scope), item = metadata(entry);
41
+ if (!dir || !item) return false;
42
+ await mkdir(dir, { recursive: true, mode: 0o700 });
43
+ const target = join(dir, `${hash(item.key)}.json`), temp = `${target}.${randomUUID()}.tmp`;
44
+ try {
45
+ await writeFile(temp, JSON.stringify(item), { mode: 0o600 });
46
+ await rename(temp, target);
47
+ } finally { await rm(temp, { force: true }); cache.delete(dir); }
48
+ const items = await list(scope);
49
+ for (const kind of ["analysis", "check"]) {
50
+ await Promise.all(items.filter(row => row.kind === kind).slice(LIMIT).map(row => rm(join(dir, `${hash(row.key)}.json`), { force: true })));
51
+ }
52
+ cache.delete(dir);
53
+ return true;
54
+ },
55
+ async items(scope) {
56
+ // A saved running row is not evidence that this process owns its lease.
57
+ return (await list(scope)).map(item => ({ ...item, state: item.state === "running" ? "interrupted" : item.state }));
58
+ },
59
+ async detail(scope, key) {
60
+ const item = (await this.items(scope)).find(item => item.key === key);
61
+ if (!item) return null;
62
+ return { key, instruction: item.instruction || `SQM local review · ${item.provider || "local"}`,
63
+ result: item.result || "", error: item.state === "interrupted" ? "This client stopped before the local run finished. Check the web workspace for its current retry status." : item.error || "", links: [] };
64
+ },
65
+ };
66
+ }
67
+
68
+ function metadata(entry) {
69
+ if (!entry || !["analysis", "check"].includes(entry.kind) || typeof entry.key !== "string" || !entry.key.startsWith(entry.kind === "analysis" ? "sqm:" : "sqm-check:")) return null;
70
+ if (!["running", "completed", "failed", "unavailable", "interrupted", "verification_pending"].includes(entry.state) || !Number.isFinite(Date.parse(entry.startedAt))) return null;
71
+ const item = {};
72
+ for (const name of ["key", "id", "kind", "label", "state", "provider", "phase", "repository", "startedAt", "finishedAt", "instruction", "result", "error"]) {
73
+ if (typeof entry[name] === "string") item[name] = entry[name].slice(0, ["instruction", "result", "error"].includes(name) ? 4096 : 512);
74
+ }
75
+ return item;
76
+ }
77
+
78
+ export function projectSqmHistory(items, live = { activeCount: 0, items: [] }) {
79
+ const analyses = new Map(items.filter(item => item.kind === "analysis").map(item => [item.key, item]));
80
+ for (const item of live.items || []) analyses.set(item.key, { ...analyses.get(item.key), ...item });
81
+ const rows = items => items.sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || "")).slice(0, LIMIT)
82
+ .map(({ instruction, result, error, ...item }) => item);
83
+ return { sqmTasks: { activeCount: live.activeCount || 0, items: rows([...analyses.values()]) },
84
+ sqmChecks: { activeCount: 0, items: rows(items.filter(item => item.kind === "check")) } };
85
+ }
86
+
87
+ export function createSqmCheckRecorder({ root, scope, phase, cwd, baseBranch, onError = () => console.warn("Could not save the local SQM check history.") }) {
88
+ if (!scope?.webOrigin || !scope?.accountId || !scope?.organizationId) return async () => {};
89
+ const history = createSqmHistoryStore({ root });
90
+ const key = `sqm-check:${randomUUID()}`;
91
+ const startedAt = new Date().toISOString();
92
+ return async result => {
93
+ const proof = result.proof, identity = proof?.repository_identity || result.repository_identity || result.min_check?.repository_identity;
94
+ const repository = identity?.repository || result.repository;
95
+ const outcome = proof?.outcome.status || result.min_check?.status || result.status;
96
+ const state = outcome === "unavailable" ? "unavailable" : ["failed", "preliminary_failed"].includes(outcome) ? "failed" : ["requires_human", "requires_post_deploy"].includes(outcome) ? "verification_pending" : "completed";
97
+ const summary = proof ? [`Full check: ${proof.outcome.status}`, formatSqmCoverage(proof), `Evidence upload: ${result.upload?.uploaded ? "uploaded" : "kept locally"}`].join("\n")
98
+ : state === "unavailable" ? "SQM could not obtain a verified result. Retry the command to see current diagnostics."
99
+ : state === "failed" && !identity ? "The command did not finish. Retry it to see current diagnostics."
100
+ : `Min-check: ${result.min_check?.status || result.status} · ${(result.preliminary_findings || result.min_check?.preliminary_findings)?.length || 0} preliminary finding(s) · ${(result.matched_modules || result.min_check?.matched_modules)?.length || 0} matched module(s).\nThis is a preflight; run the full check for signed verification.`;
101
+ try {
102
+ await history.record(scope, { key, id: key, kind: "check", phase, state, startedAt, finishedAt: new Date().toISOString(), repository,
103
+ label: `${phase === "min" ? "Min-check" : "Check"} · ${repository || basename(cwd)}`,
104
+ instruction: [repository || basename(cwd), `Base: ${identity?.base_ref || baseBranch}`, identity?.head_commit ? `Head: ${identity.head_commit}` : ""].filter(Boolean).join("\n"),
105
+ result: summary });
106
+ } catch { onError(); }
107
+ };
108
+ }
package/src/sqm/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createSqmCheckRecorder } from "./history-store.js";
1
2
  import { evaluateKnowledgeBundle } from "./checkers.js";
2
3
  import { loadKnowledgeBundle } from "./bundle-store.js";
3
4
  import { uploadRunEvidence } from "./evidence-client.js";
@@ -15,6 +16,8 @@ export function createSqmChecker({
15
16
  accountId,
16
17
  organizationId,
17
18
  deviceId,
19
+ webOrigin,
20
+ historyRoot,
18
21
  evidenceEndpoint,
19
22
  uploadRunEvidenceImpl = uploadRunEvidence,
20
23
  inspectRepositoryImpl = inspectRepository,
@@ -27,7 +30,7 @@ export function createSqmChecker({
27
30
  let pinnedRepository = null;
28
31
  let latestMinCheck = null;
29
32
 
30
- return async function checkCurrentRepository(_attempt = null, { phase = "min" } = {}) {
33
+ const checkCurrentRepository = async function(_attempt = null, { phase = "min" } = {}) {
31
34
  let repository;
32
35
  try {
33
36
  repository = await inspectRepositoryImpl(cwd, { baseBranch, runCommand: dependencies.runCommand });
@@ -64,6 +67,13 @@ export function createSqmChecker({
64
67
  return { ...unavailable(`SQM ${phase} execution failed: ${message(error)}`, repository.repository), bundle: bundleIdentity(loaded.bundle), warnings: [...loaded.warnings, `SQM ${phase} execution failed: ${message(error)}`] };
65
68
  }
66
69
  };
70
+ return async (attempt, options = {}) => {
71
+ const save = createSqmCheckRecorder({ root: historyRoot, scope: historyRoot ? { webOrigin, accountId, organizationId } : null, phase: options.phase || "min", cwd, baseBranch });
72
+ try {
73
+ const result = await checkCurrentRepository(attempt, options);
74
+ await save(result); return result;
75
+ } catch (error) { await save({ status: "failed" }); throw error; }
76
+ };
67
77
  }
68
78
 
69
79
  export async function runSqmMinCheck(options) {
@@ -86,6 +86,9 @@ export function createSqmLocalReviewWorker({
86
86
  beginWork = () => () => {},
87
87
  onError = () => {},
88
88
  onChange = () => {},
89
+ getHistoryScope = () => null,
90
+ recordHistory = async () => {},
91
+ onHistoryError = () => {},
89
92
  } = {}) {
90
93
  if (!api || typeof api.request !== "function") throw new TypeError("api.request is required");
91
94
  let activeJobId = null;
@@ -93,9 +96,19 @@ export function createSqmLocalReviewWorker({
93
96
  let closed = false;
94
97
  let issue = null;
95
98
  const history = [];
96
- const updateTask = (id, state) => {
99
+ const historyScopes = new WeakMap();
100
+ const visibleHistory = () => history.filter(item => historyScopes.get(item) === JSON.stringify(getHistoryScope()));
101
+ const persist = async (scope, item) => {
102
+ try { await recordHistory(scope, item); } catch { onHistoryError(); /* History failure must not fail a leased review. */ }
103
+ };
104
+ const updateTask = async (id, state, scope) => {
97
105
  const item = history.find(item => item.id === id);
98
- if (item) item.state = state;
106
+ if (item) {
107
+ item.state = state; item.finishedAt = new Date().toISOString();
108
+ item.result = state === "completed" ? "SQM review submitted." : "";
109
+ item.error = state === "failed" ? "Local review failed. Check the SQM workspace for retry status." : "";
110
+ await persist(scope, item);
111
+ }
99
112
  onChange();
100
113
  };
101
114
 
@@ -108,10 +121,15 @@ export function createSqmLocalReviewWorker({
108
121
  });
109
122
 
110
123
  const processClaim = async (claim, provider) => {
124
+ const historyScope = getHistoryScope();
111
125
  activeJobId = claim.job_id;
126
+ const priorIndex = history.findIndex(item => item.id === claim.job_id);
127
+ if (priorIndex >= 0) history.splice(priorIndex, 1);
112
128
  history.unshift({ id: claim.job_id, key: `sqm:${claim.job_id}`, label: `SQM · ${String(claim.job_id).slice(0, 8)}`,
113
- state: "running", provider, startedAt: new Date().toISOString() });
129
+ kind: "analysis", state: "running", provider, startedAt: new Date().toISOString() });
130
+ historyScopes.set(history[0], JSON.stringify(historyScope));
114
131
  history.splice(50);
132
+ await persist(historyScope, history[0]);
115
133
  onChange();
116
134
  let heartbeatTimer = null;
117
135
  const heartbeat = () => {
@@ -127,7 +145,7 @@ export function createSqmLocalReviewWorker({
127
145
  analysis_output: analysis.output,
128
146
  executor_model: analysis.model,
129
147
  });
130
- if (advanced.completed) { updateTask(claim.job_id, "completed"); return advanced; }
148
+ if (advanced.completed) { await updateTask(claim.job_id, "completed", historyScope); return advanced; }
131
149
  const adjudication = await runReview(advanced.request, provider);
132
150
  const completed = await api.request({
133
151
  action: "complete",
@@ -136,10 +154,10 @@ export function createSqmLocalReviewWorker({
136
154
  adjudication_output: adjudication.output,
137
155
  executor_model: adjudication.model || analysis.model,
138
156
  });
139
- updateTask(claim.job_id, "completed");
157
+ await updateTask(claim.job_id, "completed", historyScope);
140
158
  return completed;
141
159
  } catch (error) {
142
- updateTask(claim.job_id, "failed");
160
+ await updateTask(claim.job_id, "failed", historyScope);
143
161
  throw error;
144
162
  } finally {
145
163
  if (heartbeatTimer) clearInterval(heartbeatTimer);
@@ -152,9 +170,9 @@ export function createSqmLocalReviewWorker({
152
170
  snapshot() {
153
171
  return { active_job_id: activeJobId, polling: Boolean(pollPromise), closed, issue };
154
172
  },
155
- desktopTasks() { return { activeCount: activeJobId ? 1 : 0, items: history.map(item => ({ ...item })) }; },
173
+ desktopTasks() { return { activeCount: activeJobId ? 1 : 0, items: visibleHistory().map(item => ({ ...item })) }; },
156
174
  desktopTaskDetail(key) {
157
- const item = history.find(item => item.key === key);
175
+ const item = visibleHistory().find(item => item.key === key);
158
176
  return item ? { key, instruction: `SQM local review · ${item.provider}`, result: item.state === "completed" ? "SQM review submitted." : "", error: item.state === "failed" ? "Local review failed. Check the SQM workspace for retry status." : "", links: [] } : null;
159
177
  },
160
178
  resetHistory() { if (!pollPromise) { history.splice(0); issue = null; } },