@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/README.md +99 -13
- package/package.json +4 -2
- package/src/agent.js +18 -1
- package/src/auto-update.js +256 -9
- package/src/cli-contract.js +1 -1
- package/src/execution-guardrail.js +2 -2
- package/src/index.js +143 -58
- package/src/local-task.js +51 -8
- package/src/orchestrator/audit.js +42 -1
- package/src/orchestrator/completion-contract.js +15 -1
- package/src/orchestrator/loop.js +29 -3
- package/src/runner.js +44 -0
- package/src/sqm/bundle-store.js +291 -0
- package/src/sqm/canonical.js +45 -0
- package/src/sqm/checkers.js +299 -0
- package/src/sqm/command.js +149 -0
- package/src/sqm/evidence-client.js +12 -0
- package/src/sqm/index.js +141 -0
- package/src/sqm/proof.js +154 -0
- package/src/sqm/repository.js +163 -0
- package/src/sqm/session.js +58 -0
- package/src/sqm/validation.js +305 -0
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
|
-
|
|
25
|
-
|
|
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
|
|
102
|
+
--no-auto-update Disable update checks and automatic installation
|
|
37
103
|
|
|
38
104
|
${autoUpdateHelpText()}
|
|
39
105
|
`);
|
|
40
|
-
|
|
41
|
-
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
42
108
|
|
|
43
|
-
|
|
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
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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) {
|
package/src/orchestrator/loop.js
CHANGED
|
@@ -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, {
|
|
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, {
|
|
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,
|