@awak-app/simy-cli 0.5.61 → 0.5.63
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 +17 -0
- package/package.json +2 -2
- package/src/agent.js +46 -14
- package/src/cli-contract.js +1 -1
- package/src/desktop-access.js +1 -1
- package/src/desktop-host.js +13 -5
- package/src/runner.js +8 -2
- package/src/sqm/command.js +55 -45
- package/src/sqm/history-store.js +108 -0
- package/src/sqm/index.js +11 -1
- package/src/sqm/review-worker.js +26 -8
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.
|
|
3
|
+
"version": "0.5.63",
|
|
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";
|
|
@@ -198,6 +199,7 @@ export async function startAgent({
|
|
|
198
199
|
let organizationState = "unknown";
|
|
199
200
|
let organizationRefresh = null;
|
|
200
201
|
let pendingOrganization = null;
|
|
202
|
+
let desktopLoginPending = false;
|
|
201
203
|
await cleanupExpiredRuns();
|
|
202
204
|
for (const task of await restoreLocalTasks({ root: taskStorageRoot })) {
|
|
203
205
|
localTaskRegistry.create(task);
|
|
@@ -287,6 +289,7 @@ export async function startAgent({
|
|
|
287
289
|
};
|
|
288
290
|
refreshAvailableCapabilities();
|
|
289
291
|
let durableLocalTaskWorker = null;
|
|
292
|
+
const sqmHistory = createSqmHistoryStore({ root: sessionRoot });
|
|
290
293
|
let sqmLocalReviewWorker = null;
|
|
291
294
|
let codexWorkPatternSync = null;
|
|
292
295
|
let codexWorkPatternSyncInFlight = null;
|
|
@@ -467,7 +470,9 @@ export async function startAgent({
|
|
|
467
470
|
})();
|
|
468
471
|
return organizationRefresh;
|
|
469
472
|
};
|
|
470
|
-
const requestDesktopLogin = (organizationId = null) => {
|
|
473
|
+
const requestDesktopLogin = (organizationId = null, { passive = false } = {}) => {
|
|
474
|
+
// Reopening the verification page must keep the original nonce and scope.
|
|
475
|
+
if (!organizationId && pendingOrganization) return loginUrl?.toString() ?? null;
|
|
471
476
|
let organizationSlug = null;
|
|
472
477
|
if (organizationId) {
|
|
473
478
|
if (!isSessionValid(session, Date.now(), apiOrigin)) throw new Error("Sign in before switching organizations.");
|
|
@@ -484,8 +489,10 @@ export async function startAgent({
|
|
|
484
489
|
notifyDesktopStatusChange();
|
|
485
490
|
} else if (isSessionValid(session, Date.now(), apiOrigin)) {
|
|
486
491
|
if (!enforceDesktopAccess || desktopAccess.snapshot().state !== 'signed_out') return null;
|
|
487
|
-
|
|
488
|
-
|
|
492
|
+
if (!passive || !loginUrl) {
|
|
493
|
+
authNonce = randomBytes(16).toString("base64url");
|
|
494
|
+
loginUrl = null;
|
|
495
|
+
}
|
|
489
496
|
}
|
|
490
497
|
if (!loginUrl) {
|
|
491
498
|
loginUrl = new URL("/local-cli/connect", apiOrigin);
|
|
@@ -499,6 +506,7 @@ export async function startAgent({
|
|
|
499
506
|
loginUrl.searchParams.set('org_slug', organizationSlug);
|
|
500
507
|
}
|
|
501
508
|
}
|
|
509
|
+
if (!passive) { desktopLoginPending = true; notifyDesktopStatusChange(); }
|
|
502
510
|
return loginUrl.toString();
|
|
503
511
|
};
|
|
504
512
|
|
|
@@ -541,7 +549,7 @@ export async function startAgent({
|
|
|
541
549
|
const remoteRun = restoreRun({
|
|
542
550
|
snapshot, session, apiOrigin,
|
|
543
551
|
localPath: repository?.local_path || null,
|
|
544
|
-
localStateRoot: codingRunStateRoot,
|
|
552
|
+
localStateRoot: codingRunStateRoot, sqmHistoryRoot: sessionRoot,
|
|
545
553
|
});
|
|
546
554
|
Object.assign(localRun, {
|
|
547
555
|
request: remoteRun.request,
|
|
@@ -565,7 +573,7 @@ export async function startAgent({
|
|
|
565
573
|
session,
|
|
566
574
|
apiOrigin,
|
|
567
575
|
localPath: repository?.local_path || null,
|
|
568
|
-
localStateRoot: codingRunStateRoot,
|
|
576
|
+
localStateRoot: codingRunStateRoot, sqmHistoryRoot: sessionRoot,
|
|
569
577
|
});
|
|
570
578
|
registry.create(restoredRun);
|
|
571
579
|
restored = true;
|
|
@@ -597,7 +605,7 @@ export async function startAgent({
|
|
|
597
605
|
const run = restoreRun({
|
|
598
606
|
snapshot, session, apiOrigin,
|
|
599
607
|
localPath: repository?.local_path || null,
|
|
600
|
-
localStateRoot: codingRunStateRoot,
|
|
608
|
+
localStateRoot: codingRunStateRoot, sqmHistoryRoot: sessionRoot,
|
|
601
609
|
});
|
|
602
610
|
// Keep the persisted timestamp until the Web ledger has been compared.
|
|
603
611
|
// Publishing recovery here would make an older local record look newest.
|
|
@@ -689,6 +697,8 @@ export async function startAgent({
|
|
|
689
697
|
if (session?.device_id && session.token) reclaimableSession = session;
|
|
690
698
|
session = null;
|
|
691
699
|
loginUrl = null;
|
|
700
|
+
pendingOrganization = null;
|
|
701
|
+
desktopLoginPending = false;
|
|
692
702
|
refreshAvailableCapabilities();
|
|
693
703
|
requestDesktopLogin();
|
|
694
704
|
}
|
|
@@ -836,6 +846,9 @@ export async function startAgent({
|
|
|
836
846
|
return (dependencies.runSqmLocalReview || ((value, selected, options) => runStructuredProviderReview(selected, value, options)))(request, provider, { beforeDispatch });
|
|
837
847
|
},
|
|
838
848
|
onChange: notifyDesktopStatusChange,
|
|
849
|
+
getHistoryScope: () => sqmHistoryScope(apiOrigin, session),
|
|
850
|
+
recordHistory: (scope, item) => sqmHistory.record(scope, item),
|
|
851
|
+
onHistoryError: () => { if (!quiet) console.error("Could not save the local SQM analysis history."); },
|
|
839
852
|
getProvider: () => !desktopWorkAllowed() || availableCapabilities?.features?.sqm_local_review !== true ? null : ["codex", "claude"].find(
|
|
840
853
|
(provider) => availableCapabilities?.backends?.[provider] === true &&
|
|
841
854
|
providerStatuses[provider]?.state === "ready",
|
|
@@ -1189,6 +1202,7 @@ export async function startAgent({
|
|
|
1189
1202
|
await writeSession(apiOrigin, session, sessionRoot);
|
|
1190
1203
|
authNonce = randomBytes(16).toString("base64url");
|
|
1191
1204
|
pendingOrganization = null;
|
|
1205
|
+
desktopLoginPending = false;
|
|
1192
1206
|
sqmEnabled = await readSqmEnabled(sessionRoot, apiOrigin, session);
|
|
1193
1207
|
if (changingScope) sqmLocalReviewWorker?.resetHistory?.();
|
|
1194
1208
|
organizationState = "unknown";
|
|
@@ -1869,7 +1883,7 @@ export async function startAgent({
|
|
|
1869
1883
|
},
|
|
1870
1884
|
session,
|
|
1871
1885
|
apiOrigin,
|
|
1872
|
-
localStateRoot: codingRunStateRoot,
|
|
1886
|
+
localStateRoot: codingRunStateRoot, sqmHistoryRoot: sessionRoot,
|
|
1873
1887
|
});
|
|
1874
1888
|
try {
|
|
1875
1889
|
await saveCodingRunSnapshot(codingRunStateRoot, run.snapshot);
|
|
@@ -2151,7 +2165,7 @@ export async function startAgent({
|
|
|
2151
2165
|
}
|
|
2152
2166
|
|
|
2153
2167
|
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
2154
|
-
requestDesktopLogin();
|
|
2168
|
+
requestDesktopLogin(null, { passive: true });
|
|
2155
2169
|
if (!quiet) {
|
|
2156
2170
|
console.log(`Sign in to SIMY: ${loginUrl.toString()}`);
|
|
2157
2171
|
console.log(`Session file: ${sessionPath(apiOrigin, sessionRoot)}`);
|
|
@@ -2213,6 +2227,12 @@ export async function startAgent({
|
|
|
2213
2227
|
server.on("close", () => {
|
|
2214
2228
|
void shutdown().catch((error) => reportDurableLocalTaskError(error, quiet));
|
|
2215
2229
|
});
|
|
2230
|
+
const currentHistoryScope = () => isSessionValid(session, Date.now(), apiOrigin) ? sqmHistoryScope(apiOrigin, session) : null;
|
|
2231
|
+
const historyForCurrentScope = async () => {
|
|
2232
|
+
const scope = currentHistoryScope();
|
|
2233
|
+
const items = await sqmHistory.items(scope).catch(() => []);
|
|
2234
|
+
return JSON.stringify(scope) === JSON.stringify(currentHistoryScope()) ? items : [];
|
|
2235
|
+
};
|
|
2216
2236
|
const getDesktopStatus = async () => {
|
|
2217
2237
|
const auth = desktopAuthStatus(session, { webOrigin: apiOrigin });
|
|
2218
2238
|
if (auth.state === "signed_in" && organizationState === "unknown") void refreshOrganizations();
|
|
@@ -2231,6 +2251,7 @@ export async function startAgent({
|
|
|
2231
2251
|
version: CLI_VERSION,
|
|
2232
2252
|
webOrigin: apiOrigin,
|
|
2233
2253
|
auth,
|
|
2254
|
+
authorization: { pending: Boolean(pendingOrganization || desktopLoginPending) },
|
|
2234
2255
|
...(enforceDesktopAccess ? { access: { ...desktopAccess.snapshot(), revision: desktopScopeRevision() } } : {}),
|
|
2235
2256
|
connection: connectionStatus.snapshot({ authenticated: auth.state === "signed_in" }),
|
|
2236
2257
|
tasks: desktopTaskStatus([registry, directRegistry, localTaskRegistry]),
|
|
@@ -2240,12 +2261,13 @@ export async function startAgent({
|
|
|
2240
2261
|
activeJobId: sqmLocalReviewWorker?.snapshot?.().active_job_id ?? null,
|
|
2241
2262
|
issue: sqmLocalReviewWorker?.snapshot?.().issue ?? null,
|
|
2242
2263
|
providerReady: [codex, claude].some(provider => provider?.state === "ready") },
|
|
2243
|
-
|
|
2264
|
+
...projectSqmHistory(await historyForCurrentScope(), sqmLocalReviewWorker?.desktopTasks?.()),
|
|
2244
2265
|
update: updateManager?.snapshot?.() ?? null,
|
|
2245
2266
|
codex,
|
|
2246
2267
|
claude,
|
|
2247
2268
|
providers: { codex, claude },
|
|
2248
|
-
|
|
2269
|
+
// URLs stay inside the host; native UI reopens the current request by action.
|
|
2270
|
+
loginUrl: auth.state === "signed_in" ? null : requestDesktopLogin(null, { passive: true }),
|
|
2249
2271
|
};
|
|
2250
2272
|
};
|
|
2251
2273
|
return {
|
|
@@ -2259,9 +2281,10 @@ export async function startAgent({
|
|
|
2259
2281
|
shutdown,
|
|
2260
2282
|
webOrigin: apiOrigin,
|
|
2261
2283
|
get loginUrl() {
|
|
2262
|
-
return requestDesktopLogin();
|
|
2284
|
+
return requestDesktopLogin(null, { passive: true });
|
|
2263
2285
|
},
|
|
2264
2286
|
requestDesktopLogin,
|
|
2287
|
+
getPendingAuthorizationUrl: () => pendingOrganization || desktopLoginPending ? loginUrl?.toString() ?? null : null,
|
|
2265
2288
|
async refreshDesktopAccess() {
|
|
2266
2289
|
const previous = desktopAccess.snapshot().state;
|
|
2267
2290
|
const result = await desktopAccess.refresh(true);
|
|
@@ -2273,7 +2296,7 @@ export async function startAgent({
|
|
|
2273
2296
|
getDesktopAccess: () => ({ ...desktopAccess.snapshot(), revision: desktopScopeRevision() }),
|
|
2274
2297
|
requireDesktopAccess: authorizeDesktopWork,
|
|
2275
2298
|
refreshOrganizations,
|
|
2276
|
-
cancelOrganizationSelection() { authNonce = randomBytes(16).toString("base64url"); pendingOrganization = null; loginUrl = null; refreshAvailableCapabilities(); notifyDesktopStatusChange(); },
|
|
2299
|
+
cancelOrganizationSelection() { authNonce = randomBytes(16).toString("base64url"); pendingOrganization = null; desktopLoginPending = false; loginUrl = null; refreshAvailableCapabilities(); notifyDesktopStatusChange(); },
|
|
2277
2300
|
async setSqmEnabled(enabled) {
|
|
2278
2301
|
if (enabled) await authorizeDesktopWork();
|
|
2279
2302
|
if (!isSessionValid(session, Date.now(), apiOrigin) || pendingOrganization) throw new Error("Sign in to an organization first.");
|
|
@@ -2286,7 +2309,16 @@ export async function startAgent({
|
|
|
2286
2309
|
void heartbeat().catch(error => reportHeartbeatError(error, quiet));
|
|
2287
2310
|
},
|
|
2288
2311
|
getDesktopStatus,
|
|
2289
|
-
getDesktopTask: (key) =>
|
|
2312
|
+
getDesktopTask: async (key) => {
|
|
2313
|
+
if (key?.startsWith("sqm:") || key?.startsWith("sqm-check:")) {
|
|
2314
|
+
const scope = currentHistoryScope();
|
|
2315
|
+
if (!scope) return null;
|
|
2316
|
+
const detail = await sqmHistory.detail(scope, key).catch(() => null);
|
|
2317
|
+
if (JSON.stringify(scope) !== JSON.stringify(currentHistoryScope())) return null;
|
|
2318
|
+
return sqmLocalReviewWorker?.desktopTaskDetail?.(key) ?? detail;
|
|
2319
|
+
}
|
|
2320
|
+
return desktopTaskDetail([registry, directRegistry, localTaskRegistry], key);
|
|
2321
|
+
},
|
|
2290
2322
|
async refreshDesktopStatus() {
|
|
2291
2323
|
if (isSessionValid(session, Date.now(), apiOrigin)) {
|
|
2292
2324
|
await heartbeat().catch((error) => reportHeartbeatError(error, quiet));
|
|
@@ -2350,7 +2382,7 @@ export async function startAgent({
|
|
|
2350
2382
|
});
|
|
2351
2383
|
const runId = String(remoteRun?.id || "");
|
|
2352
2384
|
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 });
|
|
2385
|
+
const run = createRun({ runId, request, session, apiOrigin, localStateRoot: codingRunStateRoot, sqmHistoryRoot: sessionRoot });
|
|
2354
2386
|
await saveCodingRunSnapshot(codingRunStateRoot, run.snapshot);
|
|
2355
2387
|
registry.create(run);
|
|
2356
2388
|
await authorizeDesktopWork(scope);
|
package/src/cli-contract.js
CHANGED
|
@@ -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.
|
|
16
|
+
export const CLI_VERSION = "0.5.63";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
package/src/desktop-access.js
CHANGED
|
@@ -7,7 +7,7 @@ export function validDesktopAccessPath(path, slug) {
|
|
|
7
7
|
return path === expected || path === `${expected}/`;
|
|
8
8
|
}
|
|
9
9
|
export const desktopAccessReady = value => value?.state === 'ready';
|
|
10
|
-
const recoveryActions = new Set(['refresh', 'reconnect', 'login', 'organization_select', 'organization_cancel', 'organizations_refresh',
|
|
10
|
+
const recoveryActions = new Set(['refresh', 'reconnect', 'login', 'authorization_open', 'organization_select', 'organization_cancel', 'organizations_refresh',
|
|
11
11
|
'check_update', 'update', 'quit', 'delivery_stop', 'delivery_pause', 'delivery_reject',
|
|
12
12
|
'desktop_access_refresh', 'desktop_access_settings', 'desktop_access_members']);
|
|
13
13
|
export function desktopActionAllowed(access, command) {
|
package/src/desktop-host.js
CHANGED
|
@@ -39,6 +39,7 @@ const silentLogger = { log() {}, error() {} };
|
|
|
39
39
|
const MAX_DESKTOP_FRAME_BYTES = 8 * 1024 * 1024;
|
|
40
40
|
|
|
41
41
|
function desktopActionError(action, error) {
|
|
42
|
+
if (action === 'authorization_open' || error?.code === 'desktop_browser_unavailable') return "Could not open your browser. Open the verification link again, or check your default browser.";
|
|
42
43
|
if (error?.code === "desktop_access_required") return "Sign in to an organization with SQM and Developer access before using SIMY.";
|
|
43
44
|
if (action === "session_deadline_decide") return error?.code === "conflict"
|
|
44
45
|
? "Deadline changed; refresh before deciding." : "The deadline is unavailable. Refresh this conversation before deciding.";
|
|
@@ -214,6 +215,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
214
215
|
for (const item of sessionStatus.sessions) automaticContinuation?.revoke(item.provider, item.session);
|
|
215
216
|
if (currentAccess.state !== 'checking') await analysisManager.update([]);
|
|
216
217
|
status.tasks = { activeCount: status.tasks?.activeCount || 0, items: [] };
|
|
218
|
+
status.sqmChecks = { activeCount: 0, items: [] };
|
|
217
219
|
status.sqmTasks = { activeCount: status.sqmTasks?.activeCount || 0, items: [] };
|
|
218
220
|
status.deliveryHooks = { runs: [], providers: {} };
|
|
219
221
|
status.sessionSupervision = { sessions: [], providers: {}, refreshedAt: null };
|
|
@@ -435,16 +437,22 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
435
437
|
else if (action === "organizations_refresh") await agent.refreshOrganizations();
|
|
436
438
|
else if (action === "sqm_set_enabled") await agent.setSqmEnabled(command.enabled);
|
|
437
439
|
else if (action === "organization_cancel") agent.cancelOrganizationSelection();
|
|
438
|
-
else if (action === "login" || action === "organization_select") {
|
|
440
|
+
else if (action === "login" || action === "organization_select" || action === "authorization_open") {
|
|
439
441
|
if (action === "organization_select" && (typeof command.organizationId !== "string" || !command.organizationId)) throw new Error("Select an organization first.");
|
|
440
442
|
if (action === "organization_select" && activeQueuedAction && !["refresh", "reconnect", "task_detail", "organizations_refresh", "check_update"].includes(activeQueuedAction)) {
|
|
441
443
|
throw Object.assign(new Error("Wait for the current action to finish before switching organizations."), { code: "desktop_organization_busy" });
|
|
442
444
|
}
|
|
443
|
-
const url =
|
|
445
|
+
const url = action === "authorization_open" ? agent.getPendingAuthorizationUrl?.()
|
|
446
|
+
: await agent.requestDesktopLogin(action === "organization_select" ? command.organizationId : null);
|
|
447
|
+
if (action === 'authorization_open' && !url) throw new Error('No pending authorization.');
|
|
444
448
|
if (url) {
|
|
445
449
|
if (new URL(url).origin !== webOrigin) throw new Error("Invalid login origin.");
|
|
446
|
-
|
|
447
|
-
|
|
450
|
+
// Keep the pending request after an OS/browser failure so the user can
|
|
451
|
+
// use the visible fallback without silently canceling authorization.
|
|
452
|
+
try {
|
|
453
|
+
const result = await (dependencies.openLogin || openAuthorizationUrl)(url, { logger: silentLogger });
|
|
454
|
+
if (!result.opened) throw new Error('Browser unavailable.');
|
|
455
|
+
} catch { throw Object.assign(new Error('Browser unavailable.'), { code: 'desktop_browser_unavailable' }); }
|
|
448
456
|
}
|
|
449
457
|
} else if (action === "check_update") await updater.checkNow();
|
|
450
458
|
else if (action === "update") {
|
|
@@ -559,7 +567,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
559
567
|
emit({ type: "result", id: command.id, ok: false, message: desktopActionError(command.action, error) });
|
|
560
568
|
}
|
|
561
569
|
};
|
|
562
|
-
if (["login", "organization_select", "organization_cancel"].includes(command.action)) {
|
|
570
|
+
if (["login", "organization_select", "organization_cancel", "authorization_open"].includes(command.action)) {
|
|
563
571
|
authorizationQueue = authorizationQueue.then(execute).catch(() => {});
|
|
564
572
|
} else {
|
|
565
573
|
queue = queue.then(async () => {
|
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,
|
package/src/sqm/command.js
CHANGED
|
@@ -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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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) {
|
package/src/sqm/review-worker.js
CHANGED
|
@@ -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
|
|
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)
|
|
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:
|
|
173
|
+
desktopTasks() { return { activeCount: activeJobId ? 1 : 0, items: visibleHistory().map(item => ({ ...item })) }; },
|
|
156
174
|
desktopTaskDetail(key) {
|
|
157
|
-
const item =
|
|
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; } },
|