@awak-app/simy-cli 0.3.3 → 0.4.0

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
@@ -203,6 +203,61 @@ limit. SIMY Web can edit that suggestion and call
203
203
  `provider_token_budget`. The CLI persists the higher limit and resumes the same
204
204
  run. SIMY token consumption remains a separate platform billing record.
205
205
 
206
+ ## SQM local checks
207
+
208
+ SQM deliberately has two identity-bound stages:
209
+
210
+ 1. `simy sqm min-check` quickly inspects committed changes, the working tree,
211
+ untracked files, and deletions. It runs only deterministic low-cost rules,
212
+ reports preliminary findings, and writes the applicable state, transition,
213
+ stress, and scenario plan. A passing min-check is not a complete validation.
214
+ 2. `simy sqm check` verifies that repository, base, HEAD, working-tree digest,
215
+ and signed bundle still match the min-check. It then evaluates applicable
216
+ state transitions, invariants, stress/strength expectations, and scenarios,
217
+ writes `proof.json`, and signs that proof with the local device identity.
218
+
219
+ Normal runs do not require a separate sync command: the CLI
220
+ uses a verified local cache and revalidates it with TTL and ETag semantics.
221
+ The Agentic Loop runs min-check after each code modification and the full check
222
+ before PR readiness. Findings and the redacted signed-proof summary are recorded
223
+ in its audit; source text is never uploaded.
224
+
225
+ Configure the bundle service with `SIMY_SQM_BUNDLE_URL`. Authentication uses
226
+ the current SIMY session in the coding loop, or `SIMY_SQM_TOKEN`/
227
+ `SIMY_ACCESS_TOKEN` for the diagnostic command. On the first authenticated
228
+ HTTPS request, the CLI receives the bundle verification key from the SIMY
229
+ gateway, verifies the signed bundle, and pins that key to the endpoint,
230
+ account, organization, and repository cache identity for offline use. Explicit
231
+ trust anchors can instead be configured as a JSON key-ID map in
232
+ `SIMY_SQM_PUBLIC_KEYS_JSON`, or as one `SIMY_SQM_KEY_ID` plus
233
+ `SIMY_SQM_PUBLIC_KEY`. Public keys may be PEM, base64 SPKI, or a base64 raw
234
+ 32-byte Ed25519 key. Optional settings are:
235
+
236
+ - `SIMY_SQM_CACHE_DIR`: verified bundle cache location.
237
+ - `SIMY_SQM_CACHE_TTL_MS`: revalidation TTL, defaulting to five minutes.
238
+ - `SIMY_SQM_ACCOUNT_ID` and `SIMY_SQM_ORGANIZATION_ID`: tenant identity for
239
+ diagnostic checks. Coding-loop checks receive tenant identity from the
240
+ authenticated session. Cache entries are isolated by endpoint, account,
241
+ organization, and repository, and the organization is verified against the
242
+ signed bundle. `SIMY_SQM_ORGANIZATION_ID` is required for standalone
243
+ diagnostic checks; without a tenant identity SQM fails closed in shadow mode.
244
+
245
+ Use either phase with `--offline` to prohibit network access and `--refresh` to force
246
+ ETag revalidation, `--base <branch>` to select a base branch, and `--json` for
247
+ machine-readable results. Full check additionally accepts `--min-result`,
248
+ `--proof`, `--signed-evidence`, and `--no-upload`. The min result and full proof
249
+ share one pinned signed bundle and one exact repository identity, so a code
250
+ change cannot silently reuse an old preflight. If Cloud is unavailable, the CLI clearly warns and
251
+ uses the last unexpired signature-verified bundle only after a network failure
252
+ or Cloud 5xx response. Authentication, authorization, compatibility,
253
+ signature, schema, tenant, and expiry failures never execute stale knowledge.
254
+ With no verified cache it skips SQM
255
+ in shadow mode. Bundles contain only the fixed declarative checker vocabulary;
256
+ the CLI never executes shell, JavaScript, Python, WASM, or another remotely
257
+ supplied implementation. A module's matcher array has explicit **AND semantics**:
258
+ every matcher must match before any check in that module runs; it is not an OR
259
+ list.
260
+
206
261
  ## Interactive console
207
262
 
208
263
  Running `simy` in a terminal opens the Agentic Loop chat. It discovers the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  "start": "node ./src/index.js",
15
15
  "test": "node --test",
16
16
  "test:e2e:console": "node ./scripts/console-e2e.js",
17
+ "test:e2e:sqm-complete-loop": "node ./scripts/sqm-complete-loop-e2e.js",
17
18
  "test:e2e:console:narrow": "CLI_E2E_COLUMNS=78 CLI_E2E_ROWS=40 node ./scripts/console-e2e.js",
18
19
  "test:e2e:console:no-color": "CLI_E2E_NO_COLOR=1 node ./scripts/console-e2e.js",
19
20
  "test:e2e:console:recovery": "node ./scripts/console-recovery-e2e.js",
@@ -35,7 +36,8 @@
35
36
  "test:e2e:console:real-provider": "node ./scripts/real-provider-console-e2e.js",
36
37
  "test:e2e:console:fullstack": "node ./scripts/fullstack-cli-smoke.js",
37
38
  "check": "npm run check:auto-update && node --check ./src/desktop-executor.js && node --check ./src/local-task.js && node --check ./scripts/desktop-executor-e2e.js && node --check ./scripts/pre-guardrail-e2e.js && node --check ./scripts/fixtures/fake-desktop-provider.js && npm run check:syntax && npm run check:real-provider-syntax && npm test && npm run check:package",
38
- "check:auto-update": "node --check ./src/auto-update.js && node --check ./scripts/auto-update-e2e.js",
39
+ "check:auto-update": "node --check ./src/auto-update.js && node --check ./scripts/auto-update-e2e.js && npm run check:sqm",
40
+ "check:sqm": "node --check ./src/sqm/canonical.js && node --check ./src/sqm/validation.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 ./scripts/sqm-complete-loop-e2e.js",
39
41
  "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",
40
42
  "check:syntax": "node --check ./src/index.js && node --check ./src/agent.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/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",
41
43
  "check:package": "node ./scripts/check-package-contents.js"
package/src/agent.js CHANGED
@@ -557,6 +557,16 @@ export async function startAgent({
557
557
  device_id: typeof body.device_id === "string" ? body.device_id : null,
558
558
  api_origin: apiOrigin,
559
559
  api_base_url: apiBaseUrl,
560
+ account_id: typeof body.account_id === "string"
561
+ ? body.account_id
562
+ : typeof body.auth_user_id === "string"
563
+ ? body.auth_user_id
564
+ : null,
565
+ organization_id: typeof body.organization_id === "string"
566
+ ? body.organization_id
567
+ : typeof body.org_id === "string"
568
+ ? body.org_id
569
+ : null,
560
570
  expires_at: typeof body.expires_at === "string" ? body.expires_at : expiresAtFromNow(),
561
571
  };
562
572
  await writeSession(apiOrigin, session, sessionRoot);
@@ -679,6 +689,13 @@ export async function startAgent({
679
689
  json(res, 400, { error: "instruction is required" });
680
690
  return;
681
691
  }
692
+ const authorizationInstruction = String(
693
+ body.authorization_instruction || instruction,
694
+ ).trim();
695
+ if (!authorizationInstruction) {
696
+ json(res, 400, { error: "authorization_instruction is required" });
697
+ return;
698
+ }
682
699
  if (body.backend !== "codex" && body.backend !== "claude") {
683
700
  json(res, 400, { error: "backend must be codex or claude" });
684
701
  return;
@@ -686,7 +703,7 @@ export async function startAgent({
686
703
  const idempotencyKey = String(
687
704
  req.headers["idempotency-key"] || body.client_request_id || "",
688
705
  ).trim();
689
- const guardrail = classifyExecutionRequirement(instruction);
706
+ const guardrail = classifyExecutionRequirement(authorizationInstruction);
690
707
  if (
691
708
  guardrail.decision !== "direct_executor" ||
692
709
  body.guardrail?.policy_version !== EXECUTION_GUARDRAIL_POLICY_VERSION ||
@@ -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.3.3";
16
+ export const CLI_VERSION = "0.4.0";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function withCliContract(
@@ -1,4 +1,4 @@
1
- export const EXECUTION_GUARDRAIL_POLICY_VERSION = "2026-07-26.1";
1
+ export const EXECUTION_GUARDRAIL_POLICY_VERSION = "2026-07-29.1";
2
2
 
3
3
  const OBSERVE_PATTERN =
4
4
  /\b(monitor|watch|observe|track|status|progress|keep an eye on)\b|監視|見守|進捗|状況を?(?:見|確認)|监控|監控|观察|觀察|进度|進度|状态|狀態/i;
@@ -98,7 +98,7 @@ export function classifyExecutionRequirement(sourceMessage) {
98
98
  reason_code: "observe_existing_provider_session",
99
99
  });
100
100
  }
101
- if (changeRequested && knowledgeWork && !codingTarget) {
101
+ if (changeRequested && (knowledgeWork || hasObject) && !codingTarget) {
102
102
  return guardrail("direct_executor", {
103
103
  change_requested: true,
104
104
  repository_required: false,
package/src/index.js CHANGED
@@ -21,11 +21,25 @@ import { DEFAULT_WEB_ORIGIN, resolveWebOrigin } from "./web-origin.js";
21
21
  const argv = process.argv.slice(2);
22
22
  const args = new Set(argv);
23
23
 
24
+ if (argv[0] === "sqm") {
25
+ const packageInfo = await readCliPackageInfo();
26
+ try {
27
+ const { runSqmCommand } = await import("./sqm/command.js");
28
+ process.exitCode = await runSqmCommand(argv.slice(1), { cliVersion: packageInfo.version });
29
+ } catch (error) {
30
+ console.error(error instanceof Error ? error.message : String(error));
31
+ process.exitCode = 2;
32
+ }
33
+ process.exit();
34
+ }
35
+
24
36
  if (args.has("--help") || args.has("-h")) {
25
37
  console.log(`Usage:
26
38
  simy Start the local SIMY agent
27
39
  simy --daemon Start it in the background
28
40
  simy --no-tui Start the foreground HTTP agent without the interactive console
41
+ simy sqm min-check Run the fast deterministic SQM diff preflight
42
+ simy sqm check Run full scenarios, write proof.json, and sign the run
29
43
 
30
44
  Options:
31
45
  --daemon, --deamon Run detached in the background
package/src/local-task.js CHANGED
@@ -3,15 +3,17 @@ import { createHash, randomUUID } from "node:crypto";
3
3
  import { EventEmitter, once } from "node:events";
4
4
  import {
5
5
  mkdir,
6
+ mkdtemp,
6
7
  lstat,
7
8
  readFile,
8
9
  readdir,
9
10
  realpath,
10
11
  rename,
12
+ rm,
11
13
  stat,
12
14
  writeFile,
13
15
  } from "node:fs/promises";
14
- import { homedir } from "node:os";
16
+ import { homedir, tmpdir } from "node:os";
15
17
  import path from "node:path";
16
18
  import { promisify } from "node:util";
17
19
 
@@ -426,17 +428,38 @@ export async function startLocalTask(
426
428
  // synchronous spawn boundary so a stopped task can never start Provider work.
427
429
  if (task.stop_requested || task.status !== "running") return task;
428
430
 
429
- return new Promise((resolve) => {
431
+ const runtimeTempBase = process.platform === "darwin" ? "/tmp" : tmpdir();
432
+ const runtimeTempRoot = await mkdtemp(
433
+ path.join(runtimeTempBase, `simy-local-task-${task.id}-`),
434
+ );
435
+ allowCodexRuntimeTemp(command, runtimeTempRoot);
436
+
437
+ return new Promise((resolve, reject) => {
430
438
  let settled = false;
431
439
  let timeout = null;
432
440
  const rawStdout = [];
433
441
  const tokenRecords = [];
434
- const child = command.spawn(command.bin, command.args, {
435
- cwd: task.workspace_path,
436
- env: { ...process.env, ...command.env },
437
- stdio: ["ignore", "pipe", "pipe"],
438
- detached: process.platform !== "win32",
439
- });
442
+ let child;
443
+ try {
444
+ child = command.spawn(command.bin, command.args, {
445
+ cwd: task.workspace_path,
446
+ env: {
447
+ ...process.env,
448
+ TMPDIR: runtimeTempRoot,
449
+ TMP: runtimeTempRoot,
450
+ TEMP: runtimeTempRoot,
451
+ ...command.env,
452
+ },
453
+ stdio: ["ignore", "pipe", "pipe"],
454
+ detached: process.platform !== "win32",
455
+ });
456
+ } catch (error) {
457
+ void rm(runtimeTempRoot, { recursive: true, force: true }).then(
458
+ () => reject(error),
459
+ () => reject(error),
460
+ );
461
+ return;
462
+ }
440
463
  task.child = child;
441
464
 
442
465
  const onLine = (line) => {
@@ -523,6 +546,7 @@ export async function startLocalTask(
523
546
  let artifacts = [];
524
547
  let scenarioAcceptance = null;
525
548
  try {
549
+ await rm(runtimeTempRoot, { recursive: true, force: true });
526
550
  artifacts = task.outputs_root
527
551
  ? await collectLocalTaskArtifacts(task.outputs_root, {
528
552
  localTaskId: task.id,
@@ -1085,6 +1109,25 @@ function providerTokenCount(record) {
1085
1109
  return Number(record?.total_tokens || 0);
1086
1110
  }
1087
1111
 
1112
+ function allowCodexRuntimeTemp(command, runtimeTempRoot) {
1113
+ if (
1114
+ command.backend !== "codex" ||
1115
+ command.execution_kind !== "direct" ||
1116
+ command.verification === "explicit_command_override" ||
1117
+ command.args[0] !== "exec" ||
1118
+ command.args.length < 2
1119
+ ) {
1120
+ return;
1121
+ }
1122
+ const instruction = command.args.at(-1);
1123
+ command.args = [
1124
+ ...command.args.slice(0, -1),
1125
+ "--add-dir",
1126
+ runtimeTempRoot,
1127
+ instruction,
1128
+ ];
1129
+ }
1130
+
1088
1131
  function providerLabel(backend) {
1089
1132
  return backend === "claude" ? "Claude Code" : "Codex";
1090
1133
  }
@@ -13,7 +13,7 @@ import {
13
13
  normalizeVisualReview,
14
14
  } from "./completion-contract.js";
15
15
 
16
- export async function auditAttempt(charter, attempt, { previousAttempt = null } = {}) {
16
+ export async function auditAttempt(charter, attempt, { previousAttempt = null, sqm = null } = {}) {
17
17
  const checks = [];
18
18
  const addCheck = (
19
19
  id,
@@ -219,6 +219,7 @@ export async function auditAttempt(charter, attempt, { previousAttempt = null }
219
219
  ? "Implementation evidence passed local verification."
220
220
  : "Implementation evidence failed local verification.",
221
221
  checks,
222
+ sqm: normalizeSqmAudit(sqm),
222
223
  };
223
224
 
224
225
  return {
@@ -257,6 +258,46 @@ export async function auditAttempt(charter, attempt, { previousAttempt = null }
257
258
  release_gate: null,
258
259
  work_log_signals: [],
259
260
  work_log_interventions: [],
261
+ sqm: normalizeSqmAudit(sqm),
262
+ };
263
+ }
264
+
265
+ function normalizeSqmAudit(sqm) {
266
+ if (!sqm) return null;
267
+ return {
268
+ status: sqm.status || "unavailable",
269
+ mode: "shadow",
270
+ bundle: sqm.bundle || null,
271
+ warnings: Array.isArray(sqm.warnings) ? sqm.warnings : [],
272
+ findings: (Array.isArray(sqm.findings) ? sqm.findings : []).map((finding) => ({
273
+ finding_id: finding.finding_id,
274
+ module_id: finding.module_id,
275
+ module_version: finding.module_version,
276
+ rule_id: finding.rule_id,
277
+ severity: finding.severity,
278
+ file: finding.file || null,
279
+ line: finding.line || null,
280
+ side: finding.side || null,
281
+ evidence_hash: finding.evidence_hash,
282
+ bundle_id: finding.bundle_id,
283
+ bundle_version: finding.bundle_version,
284
+ bundle_digest: finding.bundle_digest,
285
+ })),
286
+ min_check: sqm.min_check ? {
287
+ result_digest: sqm.min_check.result_digest || null,
288
+ status: sqm.min_check.status || null,
289
+ executed_at: sqm.min_check.executed_at || null,
290
+ } : null,
291
+ proof: sqm.proof ? {
292
+ proof_id: sqm.proof.proof_id || null,
293
+ digest: sqm.proof.integrity?.digest || null,
294
+ outcome: sqm.proof.outcome?.status || null,
295
+ } : null,
296
+ signed_evidence: sqm.signed_evidence ? {
297
+ evidence_id: sqm.signed_evidence.evidence_id || null,
298
+ key_id: sqm.signed_evidence.signer?.key_id || null,
299
+ algorithm: sqm.signed_evidence.integrity?.algorithm || null,
300
+ } : null,
260
301
  };
261
302
  }
262
303
 
@@ -14,6 +14,7 @@ export async function runCodingLoop({
14
14
  executeAttempt,
15
15
  executeIndependentAudit,
16
16
  collectEvidence,
17
+ checkSqm = null,
17
18
  onUpdate,
18
19
  humanGuidance = "",
19
20
  resume = false,
@@ -127,6 +128,7 @@ export async function runCodingLoop({
127
128
  });
128
129
  await publish(snapshot, "collecting_evidence", onUpdate);
129
130
  attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
131
+ attempt.sqm = await safeSqm(checkSqm, attempt);
130
132
  if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
131
133
 
132
134
  appendEvent(snapshot, "auditing", `Verifying local evidence for attempt ${attemptNumber}.`, {
@@ -140,7 +142,10 @@ export async function runCodingLoop({
140
142
  attempt.observed_evidence?.github?.available === true,
141
143
  });
142
144
  await publish(snapshot, "auditing", onUpdate);
143
- attempt.audit = await auditAttempt(snapshot.charter, attempt, { previousAttempt });
145
+ attempt.audit = await auditAttempt(snapshot.charter, attempt, {
146
+ previousAttempt,
147
+ sqm: attempt.sqm,
148
+ });
144
149
  if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
145
150
  attempt.implementation_gate = attempt.audit.implementation_gate;
146
151
  snapshot.attempts.push(attempt);
@@ -212,8 +217,12 @@ export async function runCodingLoop({
212
217
 
213
218
  // Re-collect after the read-only auditor to catch any mutated HEAD or working tree.
214
219
  attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
220
+ attempt.sqm = await safeSqm(checkSqm, attempt, attempt.sqm);
215
221
  if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
216
- attempt.audit = await auditAttempt(snapshot.charter, attempt, { previousAttempt });
222
+ attempt.audit = await auditAttempt(snapshot.charter, attempt, {
223
+ previousAttempt,
224
+ sqm: attempt.sqm,
225
+ });
217
226
  if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
218
227
  attempt.implementation_gate = attempt.audit.implementation_gate;
219
228
 
@@ -267,7 +276,7 @@ async function stopCodingLoop(snapshot, onUpdate) {
267
276
  return snapshot;
268
277
  }
269
278
 
270
- export async function recheckPrReadiness({ snapshot, collectEvidence, onUpdate }) {
279
+ export async function recheckPrReadiness({ snapshot, collectEvidence, checkSqm = null, onUpdate }) {
271
280
  const attempt = snapshot.attempts.at(-1);
272
281
  if (!attempt || !attempt.audit?.passed || !attempt.independent_audit?.passed) return snapshot;
273
282
  appendEvent(snapshot, "checking_pr", "Refreshing GitHub review and CI evidence.", {
@@ -275,8 +284,10 @@ export async function recheckPrReadiness({ snapshot, collectEvidence, onUpdate }
275
284
  });
276
285
  await publish(snapshot, "checking_pr", onUpdate);
277
286
  attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
287
+ attempt.sqm = await safeSqm(checkSqm, attempt, attempt.sqm, "full");
278
288
  attempt.audit = await auditAttempt(snapshot.charter, attempt, {
279
289
  previousAttempt: snapshot.attempts.at(-2) ?? null,
290
+ sqm: attempt.sqm,
280
291
  });
281
292
  snapshot.implementation_gate_passed = attempt.audit.passed;
282
293
  attempt.pr_readiness = evaluatePrReadiness(snapshot.charter, attempt);
@@ -352,6 +363,21 @@ async function safeEvidence(collector, attempt) {
352
363
  }
353
364
  }
354
365
 
366
+ async function safeSqm(checker, attempt, previous = null, phase = "min") {
367
+ if (!checker) return previous;
368
+ try {
369
+ return await checker(attempt, { phase });
370
+ } catch (error) {
371
+ return {
372
+ status: "unavailable",
373
+ mode: "shadow",
374
+ bundle: null,
375
+ findings: [],
376
+ warnings: [error instanceof Error ? error.message : "SQM shadow check failed."],
377
+ };
378
+ }
379
+ }
380
+
355
381
  function unavailableEvidence(error) {
356
382
  return {
357
383
  collected_at: new Date().toISOString(),
package/src/runner.js CHANGED
@@ -40,6 +40,8 @@ import {
40
40
  import { LocalRunRegistry } from "./run-registry.js";
41
41
  import { webApiHeaders, webApiUrl } from "./web-api.js";
42
42
  import { normalizeGitHubRemote } from "./workspace-context.js";
43
+ import { CLI_VERSION } from "./cli-contract.js";
44
+ import { createSqmChecker } from "./sqm/index.js";
43
45
 
44
46
  const execFileAsync = promisify(execFile);
45
47
  const MAX_LOCAL_LOG_LINES = 1_000;
@@ -543,6 +545,20 @@ async function runLocalCodingRun(
543
545
  attempt,
544
546
  repositoryPath,
545
547
  }));
548
+ const sqmChecker =
549
+ run.sqmChecker ||
550
+ createSqmChecker({
551
+ cwd: repositoryPath,
552
+ baseBranch: run.snapshot.charter.base_branch,
553
+ cliVersion: CLI_VERSION,
554
+ token: run.session?.token,
555
+ endpoint: sqmBundleEndpoint(run.session),
556
+ evidenceEndpoint: sqmEvidenceEndpoint(run.session),
557
+ accountId: run.session?.account_id || run.session?.auth_user_id || run.session?.device_id,
558
+ organizationId: run.session?.organization_id || run.session?.org_id,
559
+ deviceId: run.session?.device_id,
560
+ });
561
+ run.sqmChecker = sqmChecker;
546
562
 
547
563
  try {
548
564
  await runCodingLoop({
@@ -550,6 +566,7 @@ async function runLocalCodingRun(
550
566
  executeAttempt: executor,
551
567
  executeIndependentAudit: independentAuditor,
552
568
  collectEvidence: evidenceCollector,
569
+ checkSqm: sqmChecker,
553
570
  humanGuidance,
554
571
  resume,
555
572
  shouldStop: () => run.stopRequested,
@@ -601,9 +618,24 @@ export async function recheckLocalCodingRun(run, { collectEvidence } = {}) {
601
618
  attempt,
602
619
  repositoryPath,
603
620
  }));
621
+ const sqmChecker =
622
+ run.sqmChecker ||
623
+ createSqmChecker({
624
+ cwd: repositoryPath,
625
+ baseBranch: run.snapshot.charter.base_branch,
626
+ cliVersion: CLI_VERSION,
627
+ token: run.session?.token,
628
+ endpoint: sqmBundleEndpoint(run.session),
629
+ evidenceEndpoint: sqmEvidenceEndpoint(run.session),
630
+ accountId: run.session?.account_id || run.session?.auth_user_id || run.session?.device_id,
631
+ organizationId: run.session?.organization_id || run.session?.org_id,
632
+ deviceId: run.session?.device_id,
633
+ });
634
+ run.sqmChecker = sqmChecker;
604
635
  await recheckPrReadiness({
605
636
  snapshot: run.snapshot,
606
637
  collectEvidence: evidenceCollector,
638
+ checkSqm: sqmChecker,
607
639
  onUpdate: async (snapshot) => {
608
640
  run.snapshot = snapshot;
609
641
  await updateRun(run, snapshot.state);
@@ -612,6 +644,18 @@ export async function recheckLocalCodingRun(run, { collectEvidence } = {}) {
612
644
  return run.snapshot;
613
645
  }
614
646
 
647
+ function sqmBundleEndpoint(session) {
648
+ if (process.env.SIMY_SQM_BUNDLE_URL?.trim()) return process.env.SIMY_SQM_BUNDLE_URL.trim();
649
+ if (!session?.api_base_url) return undefined;
650
+ return new URL("sqm/knowledge-bundle", session.api_base_url).href;
651
+ }
652
+
653
+ function sqmEvidenceEndpoint(session) {
654
+ if (process.env.SIMY_SQM_EVIDENCE_URL?.trim()) return process.env.SIMY_SQM_EVIDENCE_URL.trim();
655
+ if (!session?.api_base_url) return undefined;
656
+ return new URL("sqm/run-evidence", session.api_base_url).href;
657
+ }
658
+
615
659
  async function executeProcessAttempt({
616
660
  backend,
617
661
  instruction,