@awak-app/simy-cli 0.5.21 → 0.5.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -3
- package/package.json +5 -3
- package/src/agent.js +184 -75
- package/src/backend-executable.js +19 -5
- package/src/cli-contract.js +2 -1
- package/src/codex-plugins/simy/hooks/connection-reminder.mjs +1 -1
- package/src/console/app.js +1 -1
- package/src/desktop-host.js +25 -13
- package/src/desktop-status.js +47 -21
- package/src/direct-executor.js +3 -0
- package/src/durable-local-task-steps-contract.js +6 -1
- package/src/durable-local-task-worker.js +5 -1
- package/src/execution-capability-contract.js +19 -22
- package/src/local-task.js +20 -1
- package/src/orchestrator/recovery.js +9 -0
- package/src/provider-stream.js +20 -2
- package/src/runner.js +4 -2
- package/src/sqm/agent-skill.js +8 -2
- package/src/sqm/claude-plugin-update.js +41 -0
- package/src/sqm/claude-plugin.js +122 -0
- package/src/sqm/provider-command.js +38 -0
- package/src/sqm/review-worker.js +87 -31
- package/src/sqm/workflow-onboarding.js +6 -2
- package/src/sqm/workflow-update.js +27 -8
package/src/desktop-host.js
CHANGED
|
@@ -152,7 +152,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
154
|
if (action === "refresh" || action === "reconnect") {
|
|
155
|
-
await agent.
|
|
155
|
+
if (agent.refreshProviderStatus) await agent.refreshProviderStatus();
|
|
156
|
+
else await agent.refreshCodexStatus?.();
|
|
156
157
|
await agent.refreshDesktopStatus();
|
|
157
158
|
}
|
|
158
159
|
else if (action === "login") {
|
|
@@ -166,7 +167,12 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
166
167
|
else if (action === "update") {
|
|
167
168
|
if (updater.snapshot().state === "waiting_for_idle") await updater.maybeApply();
|
|
168
169
|
else await updater.performAction();
|
|
169
|
-
} else if (action === "codex_login"
|
|
170
|
+
} else if (action === "codex_login" || action === "claude_login") {
|
|
171
|
+
const provider = action === "claude_login" ? "claude" : "codex";
|
|
172
|
+
const login = dependencies[`${provider}Login`];
|
|
173
|
+
if (login) await login(root);
|
|
174
|
+
else await openProviderLogin(provider, root);
|
|
175
|
+
}
|
|
170
176
|
else if (action === "quit") {
|
|
171
177
|
const status = await agent.getDesktopStatus();
|
|
172
178
|
if (status.tasks.activeCount > 0 && command.force !== true) throw new Error("Tasks are still running. Confirm before quitting.");
|
|
@@ -192,22 +198,28 @@ function validCandidateStatus(status, version, webOrigin) {
|
|
|
192
198
|
&& Array.isArray(status.tasks.items) && typeof status.codex?.state === "string";
|
|
193
199
|
}
|
|
194
200
|
|
|
195
|
-
async function
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
201
|
+
export async function openProviderLogin(provider, root, {
|
|
202
|
+
inspectExecutable = inspectBackendExecutable, spawnImpl = spawn,
|
|
203
|
+
execFileImpl = promisify(execFile), platform = process.platform, env = process.env,
|
|
204
|
+
} = {}) {
|
|
205
|
+
if (provider !== "codex" && provider !== "claude") throw new Error("Unsupported provider.");
|
|
206
|
+
const executable = await inspectExecutable(provider);
|
|
207
|
+
if (!executable.available || !executable.executable) throw new Error("Provider is not installed.");
|
|
208
|
+
const label = provider === "claude" ? "Claude Code" : "Codex";
|
|
209
|
+
const args = provider === "claude" ? ["auth", "login"] : ["login"];
|
|
210
|
+
if (platform === "win32") {
|
|
211
|
+
const child = spawnImpl(join(env.SystemRoot || "C:\\Windows", "System32/WindowsPowerShell/v1.0/powershell.exe"),
|
|
212
|
+
["-NoProfile", "-NoExit", "-Command", `& $env:SIMY_PROVIDER_LOGIN ${args.join(" ")}`],
|
|
213
|
+
{ detached: true, stdio: "ignore", windowsHide: false, env: { ...env, SIMY_PROVIDER_LOGIN: executable.executable } });
|
|
202
214
|
await new Promise((resolve, reject) => { child.once("spawn", resolve); child.once("error", reject); });
|
|
203
215
|
child.unref(); return;
|
|
204
216
|
}
|
|
205
|
-
const directory = await mkdtemp(join(root,
|
|
206
|
-
const script = join(directory,
|
|
217
|
+
const directory = await mkdtemp(join(root, `${provider}-login-`));
|
|
218
|
+
const script = join(directory, `Sign in to ${label}.command`);
|
|
207
219
|
const quote = (s) => `'${s.replaceAll("'", "'\\''")}'`;
|
|
208
|
-
await writeFile(script, `#!/bin/bash\nexport PATH="/opt/homebrew/bin:/usr/local/bin:$HOME/.local/bin:$PATH"\n${quote(
|
|
220
|
+
await writeFile(script, `#!/bin/bash\nexport PATH="/opt/homebrew/bin:/usr/local/bin:$HOME/.local/bin:$PATH"\n${quote(executable.executable)} ${args.join(" ")}\nresult=$?\nrm -f -- ${quote(script)}\nrmdir -- ${quote(directory)}\nexit "$result"\n`, { mode: 0o700 });
|
|
209
221
|
await chmod(script, 0o700);
|
|
210
|
-
try { await
|
|
222
|
+
try { await execFileImpl("/usr/bin/open", ["-a", "Terminal", script], { timeout: 10_000 }); }
|
|
211
223
|
catch (error) { await rm(directory, { recursive: true, force: true }); throw error; }
|
|
212
224
|
}
|
|
213
225
|
|
package/src/desktop-status.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
2
|
|
|
3
|
-
import { inspectBackendExecutable } from "./backend-executable.js";
|
|
3
|
+
import { BACKEND_VERSION_POLICIES, inspectBackendExecutable } from "./backend-executable.js";
|
|
4
4
|
import { isSessionValid } from "./session-store.js";
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const PROVIDER_STATUS_CACHE_MS = 30_000;
|
|
7
7
|
const MAX_DETAIL_LENGTH = 240;
|
|
8
8
|
const MAX_LABEL_LENGTH = 120;
|
|
9
9
|
const MAX_STATUS_ITEMS = 50;
|
|
@@ -92,7 +92,7 @@ export function desktopTaskDetail(registries, key) {
|
|
|
92
92
|
const task = registries[registryIndex]?.list?.().find((item) => item.id === match[2]);
|
|
93
93
|
if (!task) return null;
|
|
94
94
|
const attempt = task.snapshot?.attempts?.at(-1);
|
|
95
|
-
const request = task.request?.requirement || task.instruction || task.snapshot?.charter?.requirement;
|
|
95
|
+
const request = task.request?.requirement || task.display_instruction || task.instruction || task.snapshot?.charter?.requirement;
|
|
96
96
|
const result = typeof task.result === "string" ? task.result : task.result?.summary || attempt?.summary;
|
|
97
97
|
const links = [];
|
|
98
98
|
const pr = safeDesktopLink(attempt?.pr_url);
|
|
@@ -119,9 +119,13 @@ function safeDesktopLink(value) {
|
|
|
119
119
|
} catch { return null; }
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
export function createCodexStatusInspector({
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
export function createCodexStatusInspector(options = {}) {
|
|
123
|
+
return createProviderStatusInspector("codex", options);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function createProviderStatusInspector(provider, {
|
|
127
|
+
cacheMs = PROVIDER_STATUS_CACHE_MS,
|
|
128
|
+
inspect = (options) => inspectProviderStatus(provider, options),
|
|
125
129
|
now = () => Date.now(),
|
|
126
130
|
inspectOptions = {},
|
|
127
131
|
} = {}) {
|
|
@@ -145,42 +149,49 @@ export function createCodexStatusInspector({
|
|
|
145
149
|
};
|
|
146
150
|
}
|
|
147
151
|
|
|
148
|
-
export
|
|
152
|
+
export function inspectCodexStatus(options = {}) {
|
|
153
|
+
return inspectProviderStatus("codex", options);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function inspectProviderStatus(provider, {
|
|
149
157
|
inspectExecutable = inspectBackendExecutable,
|
|
150
158
|
spawnImpl = spawn,
|
|
151
159
|
timeoutMs = 2_000,
|
|
152
160
|
now = () => new Date(),
|
|
153
161
|
} = {}) {
|
|
162
|
+
const policy = BACKEND_VERSION_POLICIES[provider];
|
|
163
|
+
if (!policy) throw new Error("Unsupported provider.");
|
|
164
|
+
const label = policy.label;
|
|
154
165
|
const checkedAt = isoNow(now);
|
|
155
166
|
let executable;
|
|
156
167
|
try {
|
|
157
|
-
executable = await inspectExecutable(
|
|
168
|
+
executable = await inspectExecutable(provider, { spawnImpl, timeoutMs });
|
|
158
169
|
} catch {
|
|
159
|
-
return
|
|
170
|
+
return providerStatus("unknown", null, checkedAt, `${label} availability could not be checked.`);
|
|
160
171
|
}
|
|
161
172
|
const version = executable?.installed_version ?? null;
|
|
162
173
|
if (!executable?.available) {
|
|
163
|
-
return
|
|
174
|
+
return providerStatus("missing", version, checkedAt, `${label} is not installed or cannot be started.`);
|
|
164
175
|
}
|
|
165
176
|
if (!executable.compatible) {
|
|
166
|
-
return
|
|
177
|
+
return providerStatus(
|
|
167
178
|
executable.status === "outdated" ? "outdated" : "unknown",
|
|
168
179
|
version,
|
|
169
180
|
checkedAt,
|
|
170
181
|
executable.status === "outdated"
|
|
171
|
-
?
|
|
172
|
-
:
|
|
182
|
+
? `${label} ${version || "installed version"} is older than the supported minimum.`
|
|
183
|
+
: `The installed ${label} version could not be verified.`,
|
|
173
184
|
);
|
|
174
185
|
}
|
|
175
186
|
|
|
176
|
-
const probe = await runLoginStatus(executable.executable, { spawnImpl, timeoutMs });
|
|
187
|
+
const probe = await runLoginStatus(provider, executable.executable, { spawnImpl, timeoutMs });
|
|
177
188
|
if (probe.kind === "signed_in") {
|
|
178
|
-
return
|
|
189
|
+
return providerStatus("ready", version, checkedAt, `${label} is installed and signed in.`);
|
|
179
190
|
}
|
|
180
191
|
if (probe.kind === "signed_out") {
|
|
181
|
-
return
|
|
192
|
+
return providerStatus("signed_out", version, checkedAt, `${label} is installed but signed out.`);
|
|
182
193
|
}
|
|
183
|
-
return
|
|
194
|
+
return providerStatus("unknown", version, checkedAt, `${label} login status could not be verified.`);
|
|
184
195
|
}
|
|
185
196
|
|
|
186
197
|
export function safeStatusDetail(cause) {
|
|
@@ -219,6 +230,7 @@ function taskStatusItem(task, registryIndex) {
|
|
|
219
230
|
label: bounded(
|
|
220
231
|
String(
|
|
221
232
|
task?.request?.requirement ||
|
|
233
|
+
task?.display_instruction ||
|
|
222
234
|
task?.instruction ||
|
|
223
235
|
task?.snapshot?.charter?.requirement ||
|
|
224
236
|
task?.id ||
|
|
@@ -242,10 +254,11 @@ function sessionUserLabel(session) {
|
|
|
242
254
|
return null;
|
|
243
255
|
}
|
|
244
256
|
|
|
245
|
-
function runLoginStatus(command, { spawnImpl, timeoutMs }) {
|
|
257
|
+
function runLoginStatus(provider, command, { spawnImpl, timeoutMs }) {
|
|
246
258
|
return new Promise((resolve) => {
|
|
247
259
|
let settled = false;
|
|
248
260
|
let output = "";
|
|
261
|
+
let stdout = "";
|
|
249
262
|
let timeout;
|
|
250
263
|
let child;
|
|
251
264
|
const finish = (kind) => {
|
|
@@ -255,7 +268,8 @@ function runLoginStatus(command, { spawnImpl, timeoutMs }) {
|
|
|
255
268
|
resolve({ kind });
|
|
256
269
|
};
|
|
257
270
|
try {
|
|
258
|
-
child = spawnImpl(command, ["login", "status"], {
|
|
271
|
+
child = spawnImpl(command, provider === "claude" ? ["auth", "status", "--json"] : ["login", "status"], {
|
|
272
|
+
windowsHide: true,
|
|
259
273
|
stdio: ["ignore", "pipe", "pipe"],
|
|
260
274
|
});
|
|
261
275
|
} catch {
|
|
@@ -266,7 +280,10 @@ function runLoginStatus(command, { spawnImpl, timeoutMs }) {
|
|
|
266
280
|
if (output.length >= MAX_PROBE_OUTPUT) return;
|
|
267
281
|
output += String(chunk).slice(0, MAX_PROBE_OUTPUT - output.length);
|
|
268
282
|
};
|
|
269
|
-
child.stdout?.on?.("data",
|
|
283
|
+
child.stdout?.on?.("data", (chunk) => {
|
|
284
|
+
stdout += String(chunk).slice(0, Math.max(0, MAX_PROBE_OUTPUT - stdout.length));
|
|
285
|
+
append(chunk);
|
|
286
|
+
});
|
|
270
287
|
child.stderr?.on?.("data", append);
|
|
271
288
|
timeout = setTimeout(() => {
|
|
272
289
|
child.kill?.("SIGTERM");
|
|
@@ -274,6 +291,15 @@ function runLoginStatus(command, { spawnImpl, timeoutMs }) {
|
|
|
274
291
|
}, timeoutMs);
|
|
275
292
|
child.on?.("error", () => finish("unknown"));
|
|
276
293
|
child.on?.("close", (code) => {
|
|
294
|
+
if (provider === "claude") {
|
|
295
|
+
let status;
|
|
296
|
+
try { status = JSON.parse(stdout); } catch { finish("unknown"); return; }
|
|
297
|
+
// Only the documented boolean and successful exit establish readiness.
|
|
298
|
+
// Never publish email, organization, auth-method details or raw output.
|
|
299
|
+
finish(code === 0 && status?.loggedIn === true ? "signed_in"
|
|
300
|
+
: status?.loggedIn === false && (code === 0 || code === 1) ? "signed_out" : "unknown");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
277
303
|
if (/not logged in|signed out|login required|please log in/i.test(output)) {
|
|
278
304
|
finish("signed_out");
|
|
279
305
|
} else if (code === 0 && /logged in|signed in/i.test(output)) {
|
|
@@ -285,7 +311,7 @@ function runLoginStatus(command, { spawnImpl, timeoutMs }) {
|
|
|
285
311
|
});
|
|
286
312
|
}
|
|
287
313
|
|
|
288
|
-
function
|
|
314
|
+
function providerStatus(state, version, checkedAt, detail) {
|
|
289
315
|
return { state, version, checkedAt, detail };
|
|
290
316
|
}
|
|
291
317
|
|
package/src/direct-executor.js
CHANGED
|
@@ -74,6 +74,7 @@ export async function startDirectTask(
|
|
|
74
74
|
|
|
75
75
|
return new Promise((resolve) => {
|
|
76
76
|
let settled = false;
|
|
77
|
+
let terminalError = null;
|
|
77
78
|
const rawStdout = [];
|
|
78
79
|
const tokenRecords = [];
|
|
79
80
|
const child = command.spawn(command.bin, command.args, {
|
|
@@ -96,6 +97,7 @@ export async function startDirectTask(
|
|
|
96
97
|
backend: task.backend,
|
|
97
98
|
stream: "stdout",
|
|
98
99
|
onLine,
|
|
100
|
+
onTerminalError: (message) => { terminalError ||= message; },
|
|
99
101
|
onUsage: (usage) => tokenRecords.push({ ...usage, phase: "direct", backend: task.backend }),
|
|
100
102
|
});
|
|
101
103
|
const stderrDecoder = createProviderStreamDecoder({
|
|
@@ -115,6 +117,7 @@ export async function startDirectTask(
|
|
|
115
117
|
settled = true;
|
|
116
118
|
stdoutDecoder.flush();
|
|
117
119
|
stderrDecoder.flush();
|
|
120
|
+
error ||= terminalError;
|
|
118
121
|
task.child = null;
|
|
119
122
|
task.token_usage = { records: tokenRecords };
|
|
120
123
|
task.status = error || exitCode !== 0 ? "failed" : "succeeded";
|
|
@@ -116,7 +116,8 @@ export function buildControlledLocalTaskPlan(orchestration) {
|
|
|
116
116
|
|
|
117
117
|
const payload = orchestration.request_payload;
|
|
118
118
|
assertRecord(payload, "orchestration.request_payload");
|
|
119
|
-
|
|
119
|
+
// Current server payloads omit the retired interaction mode; reject conflicting legacy values.
|
|
120
|
+
if (payload.mode !== undefined && payload.mode !== "local_task") {
|
|
120
121
|
fail("orchestration.request_payload.mode is unsupported.");
|
|
121
122
|
}
|
|
122
123
|
const instruction = requiredString(
|
|
@@ -252,6 +253,7 @@ export function normalizeDurableLocalTaskStepClaim(
|
|
|
252
253
|
"lease_epoch",
|
|
253
254
|
"lease_expires_at",
|
|
254
255
|
"claim_replayed",
|
|
256
|
+
...(Object.hasOwn(claim, "model") ? ["model"] : []),
|
|
255
257
|
]),
|
|
256
258
|
"claim",
|
|
257
259
|
);
|
|
@@ -261,6 +263,7 @@ export function normalizeDurableLocalTaskStepClaim(
|
|
|
261
263
|
if (!SUPPORTED_DISPOSITIONS.has(claim.disposition)) {
|
|
262
264
|
fail("Unsupported durable Local Task step disposition.");
|
|
263
265
|
}
|
|
266
|
+
const model = claim.model == null ? null : requiredString(claim.model, "model", 160);
|
|
264
267
|
const turnId = requiredUuid(claim.turn_id, "turn_id");
|
|
265
268
|
const plan = normalizePlan(claim.plan);
|
|
266
269
|
const currentStepId = requiredStepId(claim.current_step_id, "current_step_id");
|
|
@@ -299,6 +302,7 @@ export function normalizeDurableLocalTaskStepClaim(
|
|
|
299
302
|
turnId,
|
|
300
303
|
plan,
|
|
301
304
|
step,
|
|
305
|
+
model,
|
|
302
306
|
progress,
|
|
303
307
|
previousStepContext,
|
|
304
308
|
authorization: null,
|
|
@@ -355,6 +359,7 @@ export function normalizeDurableLocalTaskStepClaim(
|
|
|
355
359
|
turnId,
|
|
356
360
|
plan,
|
|
357
361
|
step,
|
|
362
|
+
model,
|
|
358
363
|
progress,
|
|
359
364
|
previousStepContext,
|
|
360
365
|
authorization,
|
|
@@ -753,7 +753,9 @@ export function createDurableLocalTaskWorker({
|
|
|
753
753
|
task = createLocalTask({
|
|
754
754
|
id: workspace.localTaskId,
|
|
755
755
|
instruction: durableLocalTaskStepInstruction(normalized),
|
|
756
|
+
displayInstruction: normalized.step.instruction,
|
|
756
757
|
backend: normalized.step.backend,
|
|
758
|
+
model: normalized.model,
|
|
757
759
|
operation: normalized.step.operation,
|
|
758
760
|
permissionMode: normalized.step.permission_mode,
|
|
759
761
|
workspaceMode: normalized.step.workspace_mode,
|
|
@@ -1108,6 +1110,7 @@ export function createDurableLocalTaskWorker({
|
|
|
1108
1110
|
id: workspace.localTaskId,
|
|
1109
1111
|
instruction: normalized.instruction,
|
|
1110
1112
|
backend: normalized.spec.backend,
|
|
1113
|
+
model: normalized.spec.model ?? null,
|
|
1111
1114
|
operation: normalized.spec.operation,
|
|
1112
1115
|
permissionMode: normalized.spec.permission_mode,
|
|
1113
1116
|
workspaceMode: normalized.spec.workspace_mode,
|
|
@@ -1661,7 +1664,8 @@ function normalizeClaim(claim, { leaseSeconds, nowMs = Date.now() } = {}) {
|
|
|
1661
1664
|
}
|
|
1662
1665
|
const payload = claim.request_payload;
|
|
1663
1666
|
const spec = claim.execution_spec;
|
|
1664
|
-
if (!payload || typeof payload !== "object" ||
|
|
1667
|
+
if (!payload || typeof payload !== "object" ||
|
|
1668
|
+
(payload.mode !== undefined && payload.mode !== "local_task")) {
|
|
1665
1669
|
throw workerError("SIMY Web returned an invalid Local Task request.", "invalid_local_task_request");
|
|
1666
1670
|
}
|
|
1667
1671
|
const instruction = String(payload.input || "").trim();
|
|
@@ -6,6 +6,7 @@ export const EXECUTION_CAPABILITY_CONTRACT_VERSION = 1;
|
|
|
6
6
|
export function buildExecutionCapabilityContract({
|
|
7
7
|
backends = {},
|
|
8
8
|
backendVersions = {},
|
|
9
|
+
providerStatuses,
|
|
9
10
|
authenticated = false,
|
|
10
11
|
} = {}) {
|
|
11
12
|
const artifactTypes = supportedArtifactTypes();
|
|
@@ -17,6 +18,7 @@ export function buildExecutionCapabilityContract({
|
|
|
17
18
|
inspection: backendVersions?.[provider],
|
|
18
19
|
legacyAvailable: backends?.[provider] === true,
|
|
19
20
|
authenticated,
|
|
21
|
+
authStatus: providerStatuses?.[provider],
|
|
20
22
|
artifactTypes,
|
|
21
23
|
}),
|
|
22
24
|
),
|
|
@@ -29,10 +31,19 @@ function localExecutionCapability({
|
|
|
29
31
|
inspection,
|
|
30
32
|
legacyAvailable,
|
|
31
33
|
authenticated,
|
|
34
|
+
authStatus,
|
|
32
35
|
artifactTypes,
|
|
33
36
|
}) {
|
|
34
37
|
const binary = normalizeInspection(provider, inspection, legacyAvailable);
|
|
35
|
-
const
|
|
38
|
+
const providerAuth = authStatus?.state === "ready" ? "ready"
|
|
39
|
+
: authStatus?.state === "signed_out" ? "required"
|
|
40
|
+
: authStatus?.state === "missing" || authStatus?.state === "outdated" ? "unavailable" : "unknown";
|
|
41
|
+
// Old clients have no provider auth observation. Preserve their v1 contract;
|
|
42
|
+
// once the new status is supplied, unknown or signed-out cannot mean ready.
|
|
43
|
+
const ready = authenticated && binary.compatible && (!authStatus || providerAuth === "ready");
|
|
44
|
+
const reason = ready ? "ready" : !authenticated ? "device_session_required"
|
|
45
|
+
: !binary.compatible ? (binary.status === "outdated" || binary.status === "unknown_version" ? "provider_update_required" : "provider_missing")
|
|
46
|
+
: providerAuth === "required" ? "provider_login_required" : "provider_status_unknown";
|
|
36
47
|
return {
|
|
37
48
|
schema_version: EXECUTION_CAPABILITY_SCHEMA_VERSION,
|
|
38
49
|
capability_contract_version: EXECUTION_CAPABILITY_CONTRACT_VERSION,
|
|
@@ -54,28 +65,14 @@ function localExecutionCapability({
|
|
|
54
65
|
},
|
|
55
66
|
authentication_readiness: {
|
|
56
67
|
device_session: authenticated ? "ready" : "required",
|
|
57
|
-
|
|
58
|
-
// prove the Provider account is authenticated.
|
|
59
|
-
provider: "unknown",
|
|
68
|
+
provider: providerAuth,
|
|
60
69
|
},
|
|
61
|
-
readiness: {
|
|
62
|
-
|
|
63
|
-
reason:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
: binary.status === "outdated" || binary.status === "unknown_version"
|
|
68
|
-
? "provider_update_required"
|
|
69
|
-
: "provider_missing",
|
|
70
|
-
},
|
|
71
|
-
missing_capability_recovery_actions: ready
|
|
72
|
-
? []
|
|
73
|
-
: [
|
|
74
|
-
{
|
|
75
|
-
action: binary.status === "missing" ? "install_provider" : "update_provider",
|
|
76
|
-
command: binary.update_command,
|
|
77
|
-
},
|
|
78
|
-
],
|
|
70
|
+
readiness: { ready, reason },
|
|
71
|
+
missing_capability_recovery_actions: ready ? []
|
|
72
|
+
: reason === "provider_login_required" ? [{ action: "login_provider", command: provider === "claude" ? "claude auth login" : "codex login" }]
|
|
73
|
+
: reason === "provider_status_unknown" ? [{ action: "refresh_provider" }]
|
|
74
|
+
: reason === "device_session_required" ? [{ action: "connect_device" }]
|
|
75
|
+
: [{ action: binary.status === "missing" ? "install_provider" : "update_provider", command: binary.update_command }],
|
|
79
76
|
};
|
|
80
77
|
}
|
|
81
78
|
|
package/src/local-task.js
CHANGED
|
@@ -220,9 +220,18 @@ async function initializeLocalTaskGitRepository(workspacePath) {
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
function normalizeLocalTaskModel(model) {
|
|
224
|
+
if (model == null) return null;
|
|
225
|
+
if (typeof model !== "string" || !model.trim() || model.length > 200 || /[\r\n]/.test(model)) {
|
|
226
|
+
throw new Error("Local Task model must be a non-empty model identifier.");
|
|
227
|
+
}
|
|
228
|
+
return model.trim();
|
|
229
|
+
}
|
|
230
|
+
|
|
223
231
|
export function createLocalTask({
|
|
224
232
|
id = createLocalTaskId(),
|
|
225
233
|
instruction,
|
|
234
|
+
displayInstruction = null,
|
|
226
235
|
backend,
|
|
227
236
|
model = null,
|
|
228
237
|
operation = "read",
|
|
@@ -257,8 +266,9 @@ export function createLocalTask({
|
|
|
257
266
|
status: "queued",
|
|
258
267
|
control_state: "queued",
|
|
259
268
|
instruction: String(instruction || "").trim(),
|
|
269
|
+
...(displayInstruction ? { display_instruction: String(displayInstruction).trim() } : {}),
|
|
260
270
|
backend,
|
|
261
|
-
model:
|
|
271
|
+
model: normalizeLocalTaskModel(model),
|
|
262
272
|
operation,
|
|
263
273
|
permission_mode: permissionMode,
|
|
264
274
|
workspace_mode: workspaceMode,
|
|
@@ -306,6 +316,7 @@ export function hashLocalTaskIdempotencyKey(value) {
|
|
|
306
316
|
export function createLocalTaskRequestFingerprint({
|
|
307
317
|
instruction,
|
|
308
318
|
backend,
|
|
319
|
+
model = null,
|
|
309
320
|
operation,
|
|
310
321
|
permissionMode,
|
|
311
322
|
workspaceMode,
|
|
@@ -316,6 +327,7 @@ export function createLocalTaskRequestFingerprint({
|
|
|
316
327
|
attachmentManifest,
|
|
317
328
|
scenarioPack,
|
|
318
329
|
}) {
|
|
330
|
+
const normalizedModel = normalizeLocalTaskModel(model);
|
|
319
331
|
const manifest = Array.isArray(attachmentManifest)
|
|
320
332
|
? attachmentManifest
|
|
321
333
|
.filter((item) => item && typeof item === "object")
|
|
@@ -329,6 +341,7 @@ export function createLocalTaskRequestFingerprint({
|
|
|
329
341
|
const payload = {
|
|
330
342
|
instruction: String(instruction || "").trim(),
|
|
331
343
|
backend: String(backend || ""),
|
|
344
|
+
...(normalizedModel ? { model: normalizedModel } : {}),
|
|
332
345
|
operation: String(operation || ""),
|
|
333
346
|
permission_mode: String(permissionMode || ""),
|
|
334
347
|
workspace_mode: String(workspaceMode || ""),
|
|
@@ -499,6 +512,10 @@ export async function startLocalTask(
|
|
|
499
512
|
stream: "stdout",
|
|
500
513
|
onLine,
|
|
501
514
|
onUsage,
|
|
515
|
+
onTerminalError: (message) => {
|
|
516
|
+
task.error ||= message;
|
|
517
|
+
task.reason_code ||= "provider_execution_failed";
|
|
518
|
+
},
|
|
502
519
|
});
|
|
503
520
|
const stderrDecoder = createProviderStreamDecoder({
|
|
504
521
|
backend: task.backend,
|
|
@@ -663,7 +680,9 @@ export function localTaskSnapshot(task, { includeEvents = true, includeLocalPath
|
|
|
663
680
|
status: task.status,
|
|
664
681
|
control_state: task.control_state,
|
|
665
682
|
instruction: task.instruction,
|
|
683
|
+
...(task.display_instruction ? { display_instruction: task.display_instruction } : {}),
|
|
666
684
|
backend: task.backend,
|
|
685
|
+
...(task.model ? { model: task.model } : {}),
|
|
667
686
|
operation: task.operation,
|
|
668
687
|
permission_mode: task.permission_mode,
|
|
669
688
|
workspace_mode: task.workspace_mode,
|
|
@@ -32,6 +32,15 @@ export function recoveryContractForEvent({ snapshot, state, message, detail = {}
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export function recoveryContractForPreflight({ key, summary, details = {} }) {
|
|
35
|
+
if (key === "provider_auth") {
|
|
36
|
+
const loginRequired = details.status === "signed_out";
|
|
37
|
+
const actions = loginRequired ? [{
|
|
38
|
+
id: "login_provider", type: "run_command", label: "Sign in to the provider",
|
|
39
|
+
description: "Finish signing in, then check the connection again.",
|
|
40
|
+
command: details.provider === "claude" ? "claude auth login" : "codex login", payload: {},
|
|
41
|
+
}, checkAgainAction()] : [checkAgainAction()];
|
|
42
|
+
return preflightContract(loginRequired ? "provider_login_required" : "provider_status_unknown", summary, actions);
|
|
43
|
+
}
|
|
35
44
|
const updateCommand = clean(details.update_command);
|
|
36
45
|
if (key === "backend") {
|
|
37
46
|
const reasonCode =
|
package/src/provider-stream.js
CHANGED
|
@@ -5,7 +5,7 @@ const STRUCTURED_MARKERS = ["SIMY_RESULT_JSON:", "SIMY_AUDIT_JSON:"];
|
|
|
5
5
|
const CODEX_MCP_AUTH_WARNING =
|
|
6
6
|
"[Codex] MCP warning: optional connector authorization expired; coding continues.";
|
|
7
7
|
|
|
8
|
-
export function createProviderStreamDecoder({ backend, stream = "stdout", onLine, onUsage }) {
|
|
8
|
+
export function createProviderStreamDecoder({ backend, stream = "stdout", onLine, onUsage, onTerminalError }) {
|
|
9
9
|
let pending = "";
|
|
10
10
|
let previous = "";
|
|
11
11
|
let model = "";
|
|
@@ -22,6 +22,9 @@ export function createProviderStreamDecoder({ backend, stream = "stdout", onLine
|
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
const terminalError = providerTerminalError(backend, event);
|
|
26
|
+
if (terminalError && onTerminalError) onTerminalError(terminalError);
|
|
27
|
+
|
|
25
28
|
model = providerModel(backend, event) || model;
|
|
26
29
|
const usage = extractProviderTokenUsage(backend, event, { model });
|
|
27
30
|
if (usage && onUsage) onUsage(usage);
|
|
@@ -50,6 +53,20 @@ export function createProviderStreamDecoder({ backend, stream = "stdout", onLine
|
|
|
50
53
|
};
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
// Tool failures can be recovered by the provider. Only terminal turn/result
|
|
57
|
+
// envelopes override a successful operating-system exit code.
|
|
58
|
+
export function providerTerminalError(backend, event) {
|
|
59
|
+
if (!event || typeof event !== "object") return null;
|
|
60
|
+
if (backend === "claude" && event.type === "result" &&
|
|
61
|
+
(event.is_error === true || String(event.subtype || "").startsWith("error"))) {
|
|
62
|
+
return cleanLine(event.result || (Array.isArray(event.errors) ? event.errors.join("; ") : "") || event.subtype || "Claude Code execution failed");
|
|
63
|
+
}
|
|
64
|
+
if (backend === "codex" && event.type === "turn.failed") {
|
|
65
|
+
return cleanLine(errorMessage(event));
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
export function extractProviderTokenUsage(backend, event, { model = "" } = {}) {
|
|
54
71
|
if (!event || typeof event !== "object") return null;
|
|
55
72
|
if (backend === "claude") {
|
|
@@ -124,7 +141,8 @@ function formatClaudeEvent(event) {
|
|
|
124
141
|
return formatClaudeToolResults(prefix, event.message?.content);
|
|
125
142
|
}
|
|
126
143
|
if (event.type === "result") {
|
|
127
|
-
|
|
144
|
+
const terminalError = providerTerminalError("claude", event);
|
|
145
|
+
if (terminalError) return [`${prefix} error: ${terminalError}`];
|
|
128
146
|
const turns = Number.isInteger(event.num_turns) ? ` | ${event.num_turns} turn(s)` : "";
|
|
129
147
|
const duration = Number.isFinite(event.duration_ms)
|
|
130
148
|
? ` | ${(event.duration_ms / 1_000).toFixed(1)}s`
|
package/src/runner.js
CHANGED
|
@@ -701,6 +701,7 @@ async function executeProcessAttempt({
|
|
|
701
701
|
|
|
702
702
|
return new Promise((resolve) => {
|
|
703
703
|
let settled = false;
|
|
704
|
+
let terminalError = null;
|
|
704
705
|
const child = command.spawn(command.bin, command.args, {
|
|
705
706
|
cwd: repositoryPath,
|
|
706
707
|
env: { ...process.env, ...command.env },
|
|
@@ -713,6 +714,7 @@ async function executeProcessAttempt({
|
|
|
713
714
|
backend,
|
|
714
715
|
stream: "stdout",
|
|
715
716
|
onLine: (line) => emitOutput(run, line, { backend }),
|
|
717
|
+
onTerminalError: (message) => { terminalError ||= message; },
|
|
716
718
|
onUsage: (usage) =>
|
|
717
719
|
tokenUsageRecords.push({
|
|
718
720
|
...usage,
|
|
@@ -752,8 +754,8 @@ async function executeProcessAttempt({
|
|
|
752
754
|
: (value) => parseStructuredMarker(value, marker);
|
|
753
755
|
const result = parse(assistantText);
|
|
754
756
|
resolve({
|
|
755
|
-
exitCode,
|
|
756
|
-
error,
|
|
757
|
+
exitCode: terminalError && exitCode === 0 ? 1 : exitCode,
|
|
758
|
+
error: error || terminalError,
|
|
757
759
|
result: Object.keys(result).length > 0 ? result : parse(rawStdout),
|
|
758
760
|
tokenUsage:
|
|
759
761
|
tokenUsageRecords.length > 0 ? { records: tokenUsageRecords } : {},
|
package/src/sqm/agent-skill.js
CHANGED
|
@@ -4,6 +4,8 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
|
+
import { inspectClaudePlugin, installClaudePlugin } from "./claude-plugin.js";
|
|
8
|
+
|
|
7
9
|
export const SQM_AGENT_SKILL_ID = "simy-sqm-pr-check";
|
|
8
10
|
export const SQM_AGENT_SKILL_VERSION = "3.0.0";
|
|
9
11
|
export const SIMY_CODEX_PLUGIN_ID = "simy";
|
|
@@ -57,6 +59,7 @@ export function sqmAgentSkillDigest() {
|
|
|
57
59
|
export async function inspectSqmAgentSkill({ home = homedir() } = {}) {
|
|
58
60
|
const digest = sqmAgentSkillDigest();
|
|
59
61
|
const codexPlugin = await inspectCodexPlugin({ home });
|
|
62
|
+
const claudePlugin = await inspectClaudePlugin({ home });
|
|
60
63
|
const hookPath = managedHookPath(home);
|
|
61
64
|
const platforms = {};
|
|
62
65
|
for (const [platform, segments] of Object.entries(TARGETS)) {
|
|
@@ -64,7 +67,8 @@ export async function inspectSqmAgentSkill({ home = homedir() } = {}) {
|
|
|
64
67
|
const installedDigest = await fileDigest(target);
|
|
65
68
|
const skillMatches = installedDigest === digest;
|
|
66
69
|
const hookConfigured = await hasManagedHook(platform, home, hookPath);
|
|
67
|
-
const
|
|
70
|
+
const plugin = platform === "codex" ? codexPlugin : claudePlugin;
|
|
71
|
+
const platformExtraMatches = plugin.installed;
|
|
68
72
|
platforms[platform] = {
|
|
69
73
|
path: target,
|
|
70
74
|
hook_path: platformHookConfigPath(platform, home),
|
|
@@ -76,7 +80,7 @@ export async function inspectSqmAgentSkill({ home = homedir() } = {}) {
|
|
|
76
80
|
: skillMatches && !hookConfigured && platformExtraMatches
|
|
77
81
|
? "installed"
|
|
78
82
|
: "outdated",
|
|
79
|
-
|
|
83
|
+
plugin,
|
|
80
84
|
};
|
|
81
85
|
}
|
|
82
86
|
return {
|
|
@@ -85,6 +89,7 @@ export async function inspectSqmAgentSkill({ home = homedir() } = {}) {
|
|
|
85
89
|
digest: `sha256:${digest}`,
|
|
86
90
|
enforcement: "explicit_opt_in",
|
|
87
91
|
codex_plugin: codexPlugin,
|
|
92
|
+
claude_plugin: claudePlugin,
|
|
88
93
|
installed: Object.values(platforms).every((entry) => entry.installed),
|
|
89
94
|
platforms,
|
|
90
95
|
};
|
|
@@ -96,6 +101,7 @@ export async function installSqmAgentSkill({ home = homedir(), platforms = Objec
|
|
|
96
101
|
throw new Error("platforms must contain codex and/or claude");
|
|
97
102
|
}
|
|
98
103
|
if (requested.includes("codex")) await installCodexPlugin({ home });
|
|
104
|
+
if (requested.includes("claude")) await installClaudePlugin({ home });
|
|
99
105
|
await retireLegacySqmGate({ home });
|
|
100
106
|
for (const platform of requested) {
|
|
101
107
|
await atomicText(path.join(home, ...TARGETS[platform]), SKILL_CONTENT, 0o600);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CLAUDE_MARKETPLACE, CLAUDE_PLUGIN_SELECTOR } from "./claude-plugin.js";
|
|
4
|
+
|
|
5
|
+
export async function refreshClaudePlugin({ status, home, claudeHome, resolveExecutable, run }) {
|
|
6
|
+
if (!status.claude_plugin?.installed) return { outcome: "not_enabled" };
|
|
7
|
+
const executable = await resolveExecutable("claude");
|
|
8
|
+
if (!executable) return { outcome: "not_enabled" };
|
|
9
|
+
const options = { timeout: 10_000, maxBuffer: 1024 * 1024,
|
|
10
|
+
env: { ...process.env, HOME: home, CLAUDE_CONFIG_DIR: claudeHome } };
|
|
11
|
+
const list = async () => JSON.parse((await run(executable, ["plugin", "list", "--json"], options)).stdout);
|
|
12
|
+
const installed = (await list()).find((entry) => entry.id === CLAUDE_PLUGIN_SELECTOR && entry.scope === "user");
|
|
13
|
+
// Never install or re-enable a plugin on a background update, or modify a
|
|
14
|
+
// project/local/managed scope that SIMY did not install for this user.
|
|
15
|
+
if (!installed?.enabled) return { outcome: "not_enabled" };
|
|
16
|
+
const marketplaces = JSON.parse((await run(executable, ["plugin", "marketplace", "list", "--json"], options)).stdout);
|
|
17
|
+
const marketplace = marketplaces.find((entry) => entry.name === CLAUDE_MARKETPLACE);
|
|
18
|
+
if (marketplace?.source !== "directory" || !marketplace.path ||
|
|
19
|
+
await realpath(marketplace.path) !== await realpath(status.claude_plugin.marketplace_root)) {
|
|
20
|
+
throw new Error("Enabled SIMY Claude plugin points to a different marketplace; refresh skipped.");
|
|
21
|
+
}
|
|
22
|
+
const cacheCurrent = async (entry) => {
|
|
23
|
+
if (!entry?.installPath || entry.version !== status.claude_plugin.version) return false;
|
|
24
|
+
const cacheRoot = await realpath(path.join(claudeHome, "plugins/cache")).catch(() => null);
|
|
25
|
+
if (!cacheRoot) return false;
|
|
26
|
+
const installedPath = await realpath(entry.installPath).catch(() => null);
|
|
27
|
+
const relative = installedPath && path.relative(cacheRoot, installedPath);
|
|
28
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return false;
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(await readFile(path.join(installedPath, ".claude-plugin/plugin.json"), "utf8")).version === status.claude_plugin.version;
|
|
31
|
+
} catch (error) { if (error.code === "ENOENT" || error instanceof SyntaxError) return false; throw error; }
|
|
32
|
+
};
|
|
33
|
+
if (await cacheCurrent(installed)) return { outcome: "current" };
|
|
34
|
+
await run(executable, ["plugin", "marketplace", "update", CLAUDE_MARKETPLACE], options);
|
|
35
|
+
await run(executable, ["plugin", "update", CLAUDE_PLUGIN_SELECTOR, "--scope", "user"], options);
|
|
36
|
+
const refreshed = (await list()).find((entry) => entry.id === CLAUDE_PLUGIN_SELECTOR && entry.scope === "user");
|
|
37
|
+
if (!refreshed?.enabled || !await cacheCurrent(refreshed)) {
|
|
38
|
+
throw new Error("Claude Code did not confirm the updated SIMY plugin cache; will retry.");
|
|
39
|
+
}
|
|
40
|
+
return { outcome: "updated", version: status.claude_plugin.version };
|
|
41
|
+
}
|