@awak-app/simy-cli 0.4.0 → 0.5.1
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 +44 -13
- package/package.json +3 -2
- package/src/agent.js +104 -15
- package/src/auto-update.js +256 -9
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/.codex-plugin/plugin.json +26 -0
- package/src/codex-plugins/simy/skills/agentic-delivery-loop/SKILL.md +154 -0
- package/src/codex-plugins/simy/skills/agentic-delivery-loop/agents/openai.yaml +5 -0
- package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/change-boundary-matrix.md +67 -0
- package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/chrome-user-verification.md +136 -0
- package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/state-and-receipts.md +142 -0
- package/src/codex-plugins/simy/skills/pr-review/SKILL.md +18 -0
- package/src/codex-plugins/simy/skills/pr-review/agents/openai.yaml +6 -0
- package/src/codex-plugins/simy/skills/sqm-loop/SKILL.md +21 -0
- package/src/codex-plugins/simy/skills/sqm-loop/agents/openai.yaml +6 -0
- package/src/index.js +138 -67
- package/src/orchestrator/completion-contract.js +15 -1
- package/src/sqm/agent-skill.js +373 -0
- package/src/sqm/bundle-store.js +42 -0
- package/src/sqm/review-worker.js +278 -0
package/src/sqm/bundle-store.js
CHANGED
|
@@ -9,6 +9,18 @@ import { validateKnowledgeBundle } from "./validation.js";
|
|
|
9
9
|
const DEFAULT_TTL_MS = 5 * 60 * 1000;
|
|
10
10
|
export const SQM_BUNDLE_FETCH_TIMEOUT_MS = 15_000;
|
|
11
11
|
|
|
12
|
+
export class SqmIncompatibleCliError extends Error {
|
|
13
|
+
constructor(message, { currentVersion, minimumVersion, modules = [] } = {}) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "SqmIncompatibleCliError";
|
|
16
|
+
this.code = "incompatible_cli";
|
|
17
|
+
this.currentVersion = currentVersion || null;
|
|
18
|
+
this.minimumVersion = minimumVersion || null;
|
|
19
|
+
this.modules = modules;
|
|
20
|
+
this.fallbackAllowed = false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
export async function loadKnowledgeBundle({
|
|
13
25
|
repository,
|
|
14
26
|
cliVersion,
|
|
@@ -68,6 +80,7 @@ export async function loadKnowledgeBundle({
|
|
|
68
80
|
});
|
|
69
81
|
return result(bundle, [], "cloud");
|
|
70
82
|
} catch (error) {
|
|
83
|
+
if (error?.code === "incompatible_cli") throw error;
|
|
71
84
|
if (error?.fallbackAllowed !== false && cached && Date.parse(cached.bundle.expires_at) > now) return result(cached.bundle, [`SQM Cloud unavailable; using last verified bundle: ${message(error)}`], "stale_cache");
|
|
72
85
|
return result(null, [`SQM knowledge could not be used; shadow checks were skipped: ${message(error)}`], "unavailable");
|
|
73
86
|
}
|
|
@@ -169,6 +182,18 @@ async function fetchBundle({ endpoint, repository, cliVersion, token, organizati
|
|
|
169
182
|
}
|
|
170
183
|
if (!response.ok) {
|
|
171
184
|
const body = await response.text().catch(() => "");
|
|
185
|
+
const parsed = parseErrorBody(body);
|
|
186
|
+
if (response.status === 409 && parsed?.error?.code === "incompatible_cli") {
|
|
187
|
+
const modules = Array.isArray(parsed.error.modules) ? parsed.error.modules : [];
|
|
188
|
+
const minimumVersion = modules
|
|
189
|
+
.map((module) => String(module?.min_cli_version || ""))
|
|
190
|
+
.filter(Boolean)
|
|
191
|
+
.sort(compareSemverDescending)[0] || null;
|
|
192
|
+
throw new SqmIncompatibleCliError(
|
|
193
|
+
parsed.error.message || "The selected knowledge bundle requires a newer SIMY CLI.",
|
|
194
|
+
{ currentVersion: parsed.error.cli_version || cliVersion, minimumVersion, modules },
|
|
195
|
+
);
|
|
196
|
+
}
|
|
172
197
|
const error = new Error(`SQM Cloud returned HTTP ${response.status}${body ? `: ${body.slice(0, 300)}` : ""}.`);
|
|
173
198
|
error.fallbackAllowed = response.status >= 500 && response.status <= 599;
|
|
174
199
|
throw error;
|
|
@@ -186,6 +211,23 @@ async function fetchBundle({ endpoint, repository, cliVersion, token, organizati
|
|
|
186
211
|
}
|
|
187
212
|
}
|
|
188
213
|
|
|
214
|
+
function parseErrorBody(body) {
|
|
215
|
+
try {
|
|
216
|
+
return JSON.parse(body);
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function compareSemverDescending(left, right) {
|
|
223
|
+
const a = left.split(".").map(Number);
|
|
224
|
+
const b = right.split(".").map(Number);
|
|
225
|
+
for (let index = 0; index < 3; index += 1) {
|
|
226
|
+
if ((a[index] || 0) !== (b[index] || 0)) return (b[index] || 0) - (a[index] || 0);
|
|
227
|
+
}
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
189
231
|
async function readVerifiedCache(location, options, identity) {
|
|
190
232
|
try {
|
|
191
233
|
const [bundleRaw, metadataRaw] = await Promise.all([readFile(location.bundle, "utf8"), readFile(location.metadata, "utf8")]);
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { inspectDesktopExecutor } from "../desktop-executor.js";
|
|
7
|
+
import {
|
|
8
|
+
webApiErrorMessage,
|
|
9
|
+
webApiHeaders,
|
|
10
|
+
webApiUrl,
|
|
11
|
+
} from "../web-api.js";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_LEASE_SECONDS = 300;
|
|
14
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 20_000;
|
|
15
|
+
const DEFAULT_EXECUTION_TIMEOUT_MS = 240_000;
|
|
16
|
+
|
|
17
|
+
export class SqmLocalReviewError extends Error {
|
|
18
|
+
constructor(message, { status = null, code = "sqm_local_review_error" } = {}) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "SqmLocalReviewError";
|
|
21
|
+
this.status = status;
|
|
22
|
+
this.code = code;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createSqmLocalReviewApi({
|
|
27
|
+
getConnection,
|
|
28
|
+
fetchImpl = globalThis.fetch,
|
|
29
|
+
requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
|
|
30
|
+
} = {}) {
|
|
31
|
+
if (typeof getConnection !== "function") throw new TypeError("getConnection is required");
|
|
32
|
+
if (typeof fetchImpl !== "function") throw new TypeError("fetchImpl is required");
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
async request(body) {
|
|
36
|
+
const connection = getConnection();
|
|
37
|
+
if (!connection?.apiOrigin || !connection?.token) {
|
|
38
|
+
throw new SqmLocalReviewError("The SIMY CLI session is not connected.", {
|
|
39
|
+
code: "local_cli_session_unavailable",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
let response;
|
|
43
|
+
try {
|
|
44
|
+
response = await fetchImpl(
|
|
45
|
+
webApiUrl("sqm/reviews", {
|
|
46
|
+
webOrigin: connection.apiOrigin,
|
|
47
|
+
apiBaseUrl: connection.apiBaseUrl,
|
|
48
|
+
}),
|
|
49
|
+
{
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: webApiHeaders(
|
|
52
|
+
{ token: connection.token },
|
|
53
|
+
{ "Content-Type": "application/json" },
|
|
54
|
+
),
|
|
55
|
+
body: JSON.stringify(body),
|
|
56
|
+
signal: AbortSignal.timeout(requestTimeoutMs),
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
throw new SqmLocalReviewError(`SIMY Web could not be reached: ${errorMessage(error)}`, {
|
|
61
|
+
code: "sqm_local_review_gateway_unavailable",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const payload = await response.json().catch(() => null);
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new SqmLocalReviewError(webApiErrorMessage(payload, response.status), {
|
|
67
|
+
status: response.status,
|
|
68
|
+
code: machineCode(payload?.code || payload?.detail?.code),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return payload;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createSqmLocalReviewWorker({
|
|
77
|
+
api,
|
|
78
|
+
workerId = "sqm-local-review",
|
|
79
|
+
leaseSeconds = DEFAULT_LEASE_SECONDS,
|
|
80
|
+
heartbeatIntervalMs = Math.max(10_000, Math.floor(leaseSeconds * 500)),
|
|
81
|
+
runReview = runStructuredCodexReview,
|
|
82
|
+
beginWork = () => () => {},
|
|
83
|
+
onError = () => {},
|
|
84
|
+
} = {}) {
|
|
85
|
+
if (!api || typeof api.request !== "function") throw new TypeError("api.request is required");
|
|
86
|
+
let activeJobId = null;
|
|
87
|
+
let pollPromise = null;
|
|
88
|
+
let closed = false;
|
|
89
|
+
|
|
90
|
+
const leaseBody = (claim) => ({
|
|
91
|
+
worker_id: workerId,
|
|
92
|
+
lease_seconds: leaseSeconds,
|
|
93
|
+
job_id: claim.job_id,
|
|
94
|
+
lease_token: claim.lease_token,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const processClaim = async (claim) => {
|
|
98
|
+
activeJobId = claim.job_id;
|
|
99
|
+
let heartbeatTimer = null;
|
|
100
|
+
const heartbeat = () => {
|
|
101
|
+
void api.request({ action: "heartbeat", ...leaseBody(claim) }).catch(onError);
|
|
102
|
+
};
|
|
103
|
+
heartbeatTimer = setInterval(heartbeat, heartbeatIntervalMs);
|
|
104
|
+
heartbeatTimer.unref?.();
|
|
105
|
+
try {
|
|
106
|
+
const analysis = await runReview(claim.request);
|
|
107
|
+
const advanced = await api.request({
|
|
108
|
+
action: "advance",
|
|
109
|
+
...leaseBody(claim),
|
|
110
|
+
analysis_output: analysis.output,
|
|
111
|
+
executor_model: analysis.model,
|
|
112
|
+
});
|
|
113
|
+
if (advanced.completed) return advanced;
|
|
114
|
+
const adjudication = await runReview(advanced.request);
|
|
115
|
+
return api.request({
|
|
116
|
+
action: "complete",
|
|
117
|
+
...leaseBody(claim),
|
|
118
|
+
analysis_output: analysis.output,
|
|
119
|
+
adjudication_output: adjudication.output,
|
|
120
|
+
executor_model: adjudication.model || analysis.model,
|
|
121
|
+
});
|
|
122
|
+
} finally {
|
|
123
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
124
|
+
activeJobId = null;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
snapshot() {
|
|
130
|
+
return { active_job_id: activeJobId, closed };
|
|
131
|
+
},
|
|
132
|
+
poll() {
|
|
133
|
+
if (closed || pollPromise) return pollPromise || Promise.resolve(null);
|
|
134
|
+
const releaseWork = beginWork?.();
|
|
135
|
+
if (!releaseWork) return Promise.resolve(null);
|
|
136
|
+
pollPromise = (async () => {
|
|
137
|
+
const claim = await api.request({
|
|
138
|
+
action: "claim",
|
|
139
|
+
worker_id: workerId,
|
|
140
|
+
lease_seconds: leaseSeconds,
|
|
141
|
+
});
|
|
142
|
+
if (!claim?.claimed) return null;
|
|
143
|
+
return processClaim(claim);
|
|
144
|
+
})()
|
|
145
|
+
.catch((error) => {
|
|
146
|
+
onError(error);
|
|
147
|
+
return null;
|
|
148
|
+
})
|
|
149
|
+
.finally(() => {
|
|
150
|
+
pollPromise = null;
|
|
151
|
+
releaseWork();
|
|
152
|
+
});
|
|
153
|
+
return pollPromise;
|
|
154
|
+
},
|
|
155
|
+
async close() {
|
|
156
|
+
closed = true;
|
|
157
|
+
await pollPromise;
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function runStructuredCodexReview(
|
|
163
|
+
request,
|
|
164
|
+
{
|
|
165
|
+
spawnImpl = spawn,
|
|
166
|
+
inspectExecutor = inspectDesktopExecutor,
|
|
167
|
+
executionTimeoutMs = DEFAULT_EXECUTION_TIMEOUT_MS,
|
|
168
|
+
} = {},
|
|
169
|
+
) {
|
|
170
|
+
assertStructuredRequest(request);
|
|
171
|
+
const inspection = await inspectExecutor("codex");
|
|
172
|
+
if (!inspection?.compatible || !inspection.executable) {
|
|
173
|
+
throw new SqmLocalReviewError("A compatible local Codex executable is required.", {
|
|
174
|
+
code: "codex_local_unavailable",
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const workDir = await mkdtemp(join(tmpdir(), "simy-sqm-review-"));
|
|
178
|
+
const schemaPath = join(workDir, "output-schema.json");
|
|
179
|
+
const outputPath = join(workDir, "output.json");
|
|
180
|
+
try {
|
|
181
|
+
await writeFile(schemaPath, JSON.stringify(request.output_schema), { mode: 0o600 });
|
|
182
|
+
const prompt = [
|
|
183
|
+
request.system,
|
|
184
|
+
"Return only JSON that strictly matches the provided output schema.",
|
|
185
|
+
request.prompt,
|
|
186
|
+
].join("\n\n");
|
|
187
|
+
await spawnCodex(
|
|
188
|
+
inspection.executable,
|
|
189
|
+
[
|
|
190
|
+
"exec",
|
|
191
|
+
"--skip-git-repo-check",
|
|
192
|
+
"--sandbox",
|
|
193
|
+
"read-only",
|
|
194
|
+
"--ephemeral",
|
|
195
|
+
"--ignore-user-config",
|
|
196
|
+
"--ignore-rules",
|
|
197
|
+
"--output-schema",
|
|
198
|
+
schemaPath,
|
|
199
|
+
"--output-last-message",
|
|
200
|
+
outputPath,
|
|
201
|
+
"-",
|
|
202
|
+
],
|
|
203
|
+
prompt,
|
|
204
|
+
{ spawnImpl, executionTimeoutMs },
|
|
205
|
+
);
|
|
206
|
+
const output = JSON.parse(await readFile(outputPath, "utf8"));
|
|
207
|
+
if (!output || Array.isArray(output) || typeof output !== "object") {
|
|
208
|
+
throw new Error("Codex returned a non-object JSON value");
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
output,
|
|
212
|
+
model: `codex-local/${inspection.installed_version || "unknown"}`,
|
|
213
|
+
};
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error instanceof SqmLocalReviewError) throw error;
|
|
216
|
+
throw new SqmLocalReviewError(`Local Codex review failed: ${errorMessage(error)}`, {
|
|
217
|
+
code: "codex_local_review_failed",
|
|
218
|
+
});
|
|
219
|
+
} finally {
|
|
220
|
+
await rm(workDir, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function spawnCodex(command, args, prompt, { spawnImpl, executionTimeoutMs }) {
|
|
225
|
+
return new Promise((resolve, reject) => {
|
|
226
|
+
const child = spawnImpl(command, args, {
|
|
227
|
+
env: safeCodexEnvironment(process.env),
|
|
228
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
229
|
+
});
|
|
230
|
+
let stderr = "";
|
|
231
|
+
let settled = false;
|
|
232
|
+
const timer = setTimeout(() => {
|
|
233
|
+
child.kill?.("SIGTERM");
|
|
234
|
+
finish(new Error(`Codex timed out after ${executionTimeoutMs}ms`));
|
|
235
|
+
}, executionTimeoutMs);
|
|
236
|
+
timer.unref?.();
|
|
237
|
+
const finish = (error = null) => {
|
|
238
|
+
if (settled) return;
|
|
239
|
+
settled = true;
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
error ? reject(error) : resolve();
|
|
242
|
+
};
|
|
243
|
+
child.stderr?.setEncoding?.("utf8");
|
|
244
|
+
child.stderr?.on?.("data", (chunk) => {
|
|
245
|
+
stderr = `${stderr}${chunk}`.slice(-4000);
|
|
246
|
+
});
|
|
247
|
+
child.on("error", finish);
|
|
248
|
+
child.on("close", (code) => {
|
|
249
|
+
finish(code === 0 ? null : new Error(stderr.trim() || `Codex exited with code ${code}`));
|
|
250
|
+
});
|
|
251
|
+
child.stdin?.end(prompt);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function safeCodexEnvironment(environment) {
|
|
256
|
+
const allowed = [
|
|
257
|
+
"HOME", "PATH", "CODEX_HOME", "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR",
|
|
258
|
+
"SSL_CERT_FILE", "SSL_CERT_DIR", "HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", "NO_PROXY",
|
|
259
|
+
];
|
|
260
|
+
return Object.fromEntries(allowed.filter((key) => environment[key]).map((key) => [key, environment[key]]));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function assertStructuredRequest(request) {
|
|
264
|
+
if (!request || typeof request !== "object") throw new TypeError("review request is required");
|
|
265
|
+
for (const key of ["system", "prompt", "output_schema"]) {
|
|
266
|
+
if (!request[key]) throw new TypeError(`review request ${key} is required`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function machineCode(value) {
|
|
271
|
+
return /^[a-z][a-z0-9_]{0,127}$/.test(String(value || ""))
|
|
272
|
+
? String(value)
|
|
273
|
+
: "sqm_local_review_error";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function errorMessage(error) {
|
|
277
|
+
return error instanceof Error ? error.message : String(error);
|
|
278
|
+
}
|