@awak-app/simy-cli 0.3.3 → 0.5.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/src/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { spawn } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { createInterface } from "node:readline/promises";
5
6
 
6
7
  import { startAgent } from "./agent.js";
7
8
  import {
@@ -9,6 +10,8 @@ import {
9
10
  createCliAutoUpdater,
10
11
  readCliPackageInfo,
11
12
  restartDaemonProcess,
13
+ runCliUpdateGate,
14
+ shouldRetryIncompatibleCli,
12
15
  writeUpdateHandoffFromEnvironment,
13
16
  } from "./auto-update.js";
14
17
  import { openAuthorizationUrl } from "./browser.js";
@@ -20,12 +23,75 @@ import { DEFAULT_WEB_ORIGIN, resolveWebOrigin } from "./web-origin.js";
20
23
 
21
24
  const argv = process.argv.slice(2);
22
25
  const args = new Set(argv);
26
+ const dispatchArgv = argv.filter((value) => value !== "--no-auto-update");
27
+ const entryPath = fileURLToPath(import.meta.url);
28
+ const packageInfo = await readCliPackageInfo();
29
+ const updatesDisabled = args.has("--no-auto-update") || process.env.SIMY_AUTO_UPDATE === "0";
30
+
31
+ if (dispatchArgv[0] === "update") {
32
+ const checkOnly = args.has("--check");
33
+ const result = await runCliUpdateGate({
34
+ argv,
35
+ packageInfo,
36
+ mode: checkOnly ? "check" : "update",
37
+ disabled: updatesDisabled,
38
+ allowMajor: args.has("--yes"),
39
+ confirmMajor: confirmMajorUpdate,
40
+ });
41
+ process.exitCode = ["check_failed", "install_failed"].includes(result.outcome) ? 1 : 0;
42
+ } else {
43
+ const startupUpdate = await runCliUpdateGate({
44
+ argv,
45
+ packageInfo,
46
+ entryPath,
47
+ disabled: updatesDisabled,
48
+ });
49
+ if (startupUpdate.reexecuted) {
50
+ process.exitCode = startupUpdate.exitCode;
51
+ } else {
52
+ await runCommand();
53
+ }
54
+ }
23
55
 
24
- if (args.has("--help") || args.has("-h")) {
25
- console.log(`Usage:
56
+ async function runCommand() {
57
+ if (dispatchArgv[0] === "sqm") {
58
+ try {
59
+ const { runSqmCommand } = await import("./sqm/command.js");
60
+ process.exitCode = await runSqmCommand(dispatchArgv.slice(1), { cliVersion: packageInfo.version });
61
+ } catch (error) {
62
+ if (shouldRetryIncompatibleCli(error)) {
63
+ const retried = await runCliUpdateGate({
64
+ argv,
65
+ packageInfo,
66
+ entryPath,
67
+ mode: "incompatible",
68
+ disabled: updatesDisabled,
69
+ allowMajor: args.has("--yes"),
70
+ confirmMajor: confirmMajorUpdate,
71
+ incompatibleRetry: true,
72
+ });
73
+ if (retried.reexecuted) {
74
+ process.exitCode = retried.exitCode;
75
+ return;
76
+ }
77
+ const required = error.minimumVersion ? ` (requires ${error.minimumVersion}+)` : "";
78
+ console.error(`SQM requires a newer SIMY CLI${required}, but automatic recovery did not install one.`);
79
+ }
80
+ console.error(error instanceof Error ? error.message : String(error));
81
+ process.exitCode = 2;
82
+ }
83
+ return;
84
+ }
85
+
86
+ if (args.has("--help") || args.has("-h")) {
87
+ console.log(`Usage:
26
88
  simy Start the local SIMY agent
27
89
  simy --daemon Start it in the background
28
90
  simy --no-tui Start the foreground HTTP agent without the interactive console
91
+ simy sqm min-check Run the fast deterministic SQM diff preflight
92
+ simy sqm check Run full scenarios, write proof.json, and sign the run
93
+ simy update --check Check the installed and latest stable CLI versions
94
+ simy update Install an eligible stable CLI update
29
95
 
30
96
  Options:
31
97
  --daemon, --deamon Run detached in the background
@@ -33,73 +99,92 @@ Options:
33
99
  --no-open Do not open the browser when authorization is required
34
100
  --port <port> Bind a specific localhost port
35
101
  --host <url> Connect to a SIMY Web origin (default: ${DEFAULT_WEB_ORIGIN})
36
- --no-auto-update Disable the hourly CLI update check
102
+ --no-auto-update Disable update checks and automatic installation
37
103
 
38
104
  ${autoUpdateHelpText()}
39
105
  `);
40
- process.exit(0);
41
- }
106
+ return;
107
+ }
42
108
 
43
- const daemon = args.has("--daemon") || args.has("--deamon");
44
- const interactive = !daemon && !args.has("--no-tui") && process.stdin.isTTY && process.stdout.isTTY;
45
- const port = readPort(argv);
46
- const packageInfo = await readCliPackageInfo();
47
- let webOrigin;
48
- try {
49
- webOrigin = resolveWebOrigin(readOption(argv, "--host"));
50
- } catch (error) {
51
- console.error(error instanceof Error ? error.message : String(error));
52
- process.exit(2);
109
+ await runLocalAgent();
53
110
  }
54
111
 
55
- if (daemon && process.env.SIMY_DAEMON_CHILD !== "1") {
56
- const child = spawn(process.execPath, [fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
57
- detached: true,
58
- stdio: "ignore",
59
- env: { ...process.env, SIMY_DAEMON_CHILD: "1" },
60
- });
61
- child.unref();
62
- console.log("SIMY local agent started in the background.");
63
- process.exit(0);
64
- }
112
+ async function runLocalAgent() {
113
+ const daemon = args.has("--daemon") || args.has("--deamon");
114
+ const interactive = !daemon && !args.has("--no-tui") && process.stdin.isTTY && process.stdout.isTTY;
115
+ const port = readPort(argv);
116
+ let webOrigin;
117
+ try {
118
+ webOrigin = resolveWebOrigin(readOption(argv, "--host"));
119
+ } catch (error) {
120
+ console.error(error instanceof Error ? error.message : String(error));
121
+ process.exitCode = 2;
122
+ return;
123
+ }
65
124
 
66
- const updateManager = await createCliAutoUpdater({
67
- daemon,
68
- interactive,
69
- requestedPort: port,
70
- disabled: args.has("--no-auto-update") || process.env.SIMY_AUTO_UPDATE === "0",
71
- dependencies: { packageInfo },
72
- });
73
- const agent = await startAgent({
74
- requestedPort: port,
75
- daemon,
76
- webOrigin,
77
- quiet: interactive,
78
- updateManager,
79
- });
80
- const shutdownAgent = createAgentShutdown(agent);
81
- installAgentShutdownSignalHandlers({ shutdown: shutdownAgent });
82
- await writeUpdateHandoffFromEnvironment({ version: packageInfo.version, port: agent.port });
83
- updateManager.start({
84
- registry: agent.registry,
85
- restartDaemon: async (targetVersion) => {
86
- await restartDaemonProcess({
87
- entryPath: fileURLToPath(import.meta.url),
88
- argv: process.argv.slice(2),
89
- targetVersion,
125
+ if (daemon && process.env.SIMY_DAEMON_CHILD !== "1") {
126
+ const child = spawn(process.execPath, [fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
127
+ detached: true,
128
+ stdio: "ignore",
129
+ env: { ...process.env, SIMY_DAEMON_CHILD: "1" },
90
130
  });
91
- await shutdownAgent();
92
- },
93
- });
94
- if (agent.loginUrl && !args.has("--no-open")) {
95
- await openAuthorizationUrl(agent.loginUrl);
131
+ child.unref();
132
+ console.log("SIMY local agent started in the background.");
133
+ return;
134
+ }
135
+
136
+ const updateManager = await createCliAutoUpdater({
137
+ daemon,
138
+ interactive,
139
+ requestedPort: port,
140
+ disabled: args.has("--no-auto-update") || process.env.SIMY_AUTO_UPDATE === "0",
141
+ dependencies: { packageInfo },
142
+ });
143
+ const agent = await startAgent({
144
+ requestedPort: port,
145
+ daemon,
146
+ webOrigin,
147
+ quiet: interactive,
148
+ updateManager,
149
+ });
150
+ const shutdownAgent = createAgentShutdown(agent);
151
+ installAgentShutdownSignalHandlers({ shutdown: shutdownAgent });
152
+ await writeUpdateHandoffFromEnvironment({ version: packageInfo.version, port: agent.port });
153
+ updateManager.start({
154
+ registry: agent.registry,
155
+ restartDaemon: async (targetVersion) => {
156
+ await restartDaemonProcess({
157
+ entryPath: fileURLToPath(import.meta.url),
158
+ argv: process.argv.slice(2),
159
+ targetVersion,
160
+ });
161
+ await shutdownAgent();
162
+ },
163
+ });
164
+ if (agent.loginUrl && !args.has("--no-open")) {
165
+ await openAuthorizationUrl(agent.loginUrl);
166
+ }
167
+ if (interactive) {
168
+ const { runAgenticLoopConsole } = await import("./console/index.js");
169
+ try {
170
+ await runAgenticLoopConsole(agent);
171
+ } finally {
172
+ await shutdownAgent();
173
+ }
174
+ }
96
175
  }
97
- if (interactive) {
98
- const { runAgenticLoopConsole } = await import("./console/index.js");
176
+
177
+ async function confirmMajorUpdate({ currentVersion, latestVersion }) {
178
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
179
+ console.log(`Major update ${currentVersion} → ${latestVersion} requires confirmation. Re-run with \`simy update --yes\`.`);
180
+ return false;
181
+ }
182
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
99
183
  try {
100
- await runAgenticLoopConsole(agent);
184
+ const answer = await prompt.question(`Install major SIMY CLI update ${currentVersion} → ${latestVersion}? [y/N] `);
185
+ return /^(y|yes)$/i.test(answer.trim());
101
186
  } finally {
102
- await shutdownAgent();
187
+ prompt.close();
103
188
  }
104
189
  }
105
190
 
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
 
@@ -79,8 +79,14 @@ export function completionEvidenceChecks(charter, attempt) {
79
79
  if (contract.version !== COMPLETION_CONTRACT_VERSION) return [];
80
80
 
81
81
  const uncertainty = normalizeUncertaintyVerification(attempt?.uncertainty_verification);
82
- const criterionEvidence = normalizeCriterionEvidence(attempt?.criterion_evidence);
83
82
  const rows = acceptanceCriterionRows(charter);
83
+ const criterionEvidence = normalizeCriterionEvidence(attempt?.criterion_evidence);
84
+ const expectedCriterionIds = new Set(rows.map((row) => row.id));
85
+ const unexpectedCriterionIds = uniqueStrings(
86
+ criterionEvidence
87
+ .map((item) => item.criterion_id)
88
+ .filter((criterionId) => !expectedCriterionIds.has(criterionId)),
89
+ );
84
90
  const checks = [
85
91
  check(
86
92
  "top_uncertainty_recorded",
@@ -110,6 +116,14 @@ export function completionEvidenceChecks(charter, attempt) {
110
116
  ? "The executor marked the highest uncertainty resolved with observed evidence."
111
117
  : uncertainty.fallback || "The highest uncertainty remains unresolved.",
112
118
  ),
119
+ check(
120
+ "criterion_evidence_scope",
121
+ "Criterion evidence matches the charter acceptance criteria",
122
+ unexpectedCriterionIds.length === 0,
123
+ unexpectedCriterionIds.length === 0
124
+ ? "Every criterion evidence row references a chartered acceptance criterion."
125
+ : `Unexpected criterion evidence: ${unexpectedCriterionIds.join(", ")}.`,
126
+ ),
113
127
  ];
114
128
 
115
129
  for (const row of rows) {
@@ -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,