@awak-app/simy-cli 0.5.69 → 0.5.71

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.5.69",
3
+ "version": "0.5.71",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/agent.js CHANGED
@@ -875,7 +875,7 @@ export async function startAgent({
875
875
  runReview: async (request, provider) => {
876
876
  const scope = await authorizeDesktopWork();
877
877
  const beforeDispatch = () => authorizeDesktopWork(scope);
878
- return (dependencies.runSqmLocalReview || ((value, selected, options) => runStructuredProviderReview(selected, value, options)))(request, provider, { beforeDispatch });
878
+ return (dependencies.runSqmLocalReview || ((value, selected, options) => runStructuredProviderReview(selected, value, options)))(request, provider, { beforeDispatch, readOnlyAnalysis: true });
879
879
  },
880
880
  onChange: notifyDesktopStatusChange,
881
881
  getWorkspaceUrl: () => {
@@ -13,7 +13,7 @@ import {
13
13
  } from "./durable-local-task-steps-contract.js";
14
14
  import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
15
15
 
16
- export const CLI_VERSION = "0.5.69";
16
+ export const CLI_VERSION = "0.5.71";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function withCliContract(
@@ -14,16 +14,19 @@ const DEFAULT_LEASE_SECONDS = 300;
14
14
  // Claim can retrieve and freeze organization-scoped Slack evidence (45s budget).
15
15
  const DEFAULT_REQUEST_TIMEOUT_MS = 75_000;
16
16
  const DEFAULT_EXECUTION_TIMEOUT_MS = 240_000;
17
- // Pin SQM independently of the user's interactive model and CLI defaults.
18
- export const SQM_CODEX_MODEL = "gpt-6.1-sol";
17
+ // Keep analysis independent of interactive config. Availability varies by
18
+ // account and CLI; only an explicit model rejection may try the next model.
19
+ export const SQM_CODEX_MODELS = Object.freeze(["gpt-6-astra", "gpt-6.1-sol"]);
20
+ export const SQM_CODEX_MODEL = SQM_CODEX_MODELS[0];
19
21
 
20
22
  export class SqmLocalReviewError extends Error {
21
- constructor(message, { status = null, code = "sqm_local_review_error", stage = null, jobId = null, failureRecorded = false } = {}) {
23
+ constructor(message, { status = null, code = "sqm_local_review_error", stage = null, jobId = null, failureRecorded = false, validationFeedback = "" } = {}) {
22
24
  super(message);
23
25
  this.name = "SqmLocalReviewError";
24
26
  this.status = status;
25
27
  this.code = code;
26
28
  this.stage = stage; this.jobId = jobId; this.failureRecorded = failureRecorded;
29
+ this.validationFeedback = typeof validationFeedback === "string" ? validationFeedback.slice(0, 2000) : "";
27
30
  }
28
31
  }
29
32
 
@@ -71,6 +74,7 @@ export function createSqmLocalReviewApi({
71
74
  status: response.status,
72
75
  code: machineCode(payload?.code || payload?.detail?.code),
73
76
  stage: payload?.detail?.stage, jobId: payload?.detail?.job_id, failureRecorded: payload?.detail?.failure_recorded === true,
77
+ validationFeedback: payload?.detail?.validation_feedback,
74
78
  });
75
79
  }
76
80
  return payload;
@@ -124,6 +128,23 @@ export function createSqmLocalReviewWorker({
124
128
  lease_token: claim.lease_token,
125
129
  });
126
130
 
131
+ // Repair only a rejected model result, under the same live lease. Network,
132
+ // authorization, and persistence failures must not repeat provider execution.
133
+ const validatedReview = async (request, provider, code, submit, setStage) => {
134
+ let nextRequest = request;
135
+ for (let attempt = 0; ; attempt += 1) {
136
+ setStage(false);
137
+ const review = await runReview(nextRequest, provider);
138
+ setStage(true);
139
+ try {
140
+ return { review, response: await submit(review) };
141
+ } catch (error) {
142
+ if (attempt >= 1 || error.status !== 422 || error.code !== code || !error.validationFeedback) throw error;
143
+ nextRequest = { ...request, prompt: `${request.prompt}\n\nThe previous result failed validation. Return a corrected complete JSON object using only the supplied evidence. Fix these constraints:\n${error.validationFeedback}` };
144
+ }
145
+ }
146
+ };
147
+
127
148
  const processClaim = async (claim, provider, historyScope, workspaceUrl) => {
128
149
  activeJobId = claim.job_id;
129
150
  const priorIndex = history.findIndex(item => item.id === claim.job_id);
@@ -142,25 +163,21 @@ export function createSqmLocalReviewWorker({
142
163
  heartbeatTimer.unref?.();
143
164
  let stage = "analysis";
144
165
  try {
145
- const analysis = await runReview(claim.request, provider);
146
- stage = "advance";
147
- const advanced = await api.request({
166
+ const { review: analysis, response: advanced } = await validatedReview(claim.request, provider, "invalid_sqm_analysis_output", analysis => api.request({
148
167
  action: "advance",
149
168
  ...leaseBody(claim, provider),
150
169
  analysis_output: analysis.output,
151
170
  executor_model: analysis.model,
152
- });
171
+ }), submitting => { stage = submitting ? "advance" : "analysis"; });
153
172
  if (advanced.completed) { await updateTask(claim.job_id, "completed", historyScope); return advanced; }
154
173
  stage = "adjudication";
155
- const adjudication = await runReview(advanced.request, provider);
156
- stage = "complete";
157
- const completed = await api.request({
174
+ const { response: completed } = await validatedReview(advanced.request, provider, "invalid_sqm_adjudication_output", adjudication => api.request({
158
175
  action: "complete",
159
176
  ...leaseBody(claim, provider),
160
177
  analysis_output: analysis.output,
161
178
  adjudication_output: adjudication.output,
162
179
  executor_model: adjudication.model || analysis.model,
163
- });
180
+ }), submitting => { stage = submitting ? "complete" : "adjudication"; });
164
181
  await updateTask(claim.job_id, "completed", historyScope);
165
182
  return completed;
166
183
  } catch (error) {
@@ -253,7 +270,7 @@ export async function runStructuredProviderReview(
253
270
  executionTimeoutMs = DEFAULT_EXECUTION_TIMEOUT_MS,
254
271
  environment = process.env,
255
272
  signal,
256
- readOnlyAnalysis = false,
273
+ readOnlyAnalysis = true,
257
274
  terminationGraceMs = 1_000,
258
275
  beforeDispatch = async () => {},
259
276
  } = {},
@@ -278,38 +295,54 @@ export async function runStructuredProviderReview(
278
295
  "Return only JSON that strictly matches the provided output schema.",
279
296
  request.prompt,
280
297
  ].join("\n\n");
281
- await beforeDispatch();
282
- if (signal?.aborted) throw new SqmLocalReviewError('Local analysis was cancelled.', { code: 'analysis_cancelled' });
283
- const stdout = await spawnReview(
284
- inspection.executable,
285
- provider === "claude" ? [
286
- "--print", "--output-format", "json",
287
- "--json-schema", JSON.stringify(request.output_schema),
288
- "--no-session-persistence", "--tools", "",
289
- "--setting-sources", "", "--settings", '{"disableAllHooks":true}',
290
- "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}',
291
- ...(readOnlyAnalysis ? ["--system-prompt", request.system, "--max-turns", "1"] : []),
292
- ] : [
293
- "exec",
294
- "--model",
295
- SQM_CODEX_MODEL,
296
- "--skip-git-repo-check",
297
- "--sandbox",
298
- "read-only",
299
- "--ephemeral",
300
- "--ignore-user-config",
301
- "--ignore-rules",
302
- ...(readOnlyAnalysis ? codexAnalysisArguments(request.system) : []),
303
- "--output-schema",
304
- schemaPath,
305
- "--output-last-message",
306
- outputPath,
307
- "-",
308
- ],
309
- prompt,
310
- { spawnImpl, executionTimeoutMs, label, cwd: workDir, environment, provider, signal, readOnlyAnalysis,
311
- terminationGraceMs: Number.isInteger(terminationGraceMs) ? Math.max(1, Math.min(terminationGraceMs, 5_000)) : 1_000 },
312
- );
298
+ const deadline = Date.now() + executionTimeoutMs;
299
+ const models = provider === "codex" ? SQM_CODEX_MODELS : [null];
300
+ let stdout;
301
+ let selectedModel;
302
+ for (const model of models) {
303
+ await beforeDispatch();
304
+ if (signal?.aborted) throw new SqmLocalReviewError('Local analysis was cancelled.', { code: 'analysis_cancelled' });
305
+ const remaining = deadline - Date.now();
306
+ if (remaining <= 0) throw new SqmLocalReviewError('Local analysis timed out.', { code: 'analysis_timeout' });
307
+ // Never consume a partial/stale output file left by a rejected model.
308
+ await rm(outputPath, { force: true });
309
+ try {
310
+ stdout = await spawnReview(
311
+ inspection.executable,
312
+ provider === "claude" ? [
313
+ "--print", "--output-format", "json",
314
+ "--json-schema", JSON.stringify(request.output_schema),
315
+ "--no-session-persistence", "--tools", "",
316
+ "--setting-sources", "", "--settings", '{"disableAllHooks":true}',
317
+ "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}',
318
+ ...(readOnlyAnalysis ? ["--system-prompt", request.system, "--max-turns", "1"] : []),
319
+ ] : [
320
+ "exec",
321
+ "--model",
322
+ model,
323
+ "--skip-git-repo-check",
324
+ "--sandbox",
325
+ "read-only",
326
+ "--ephemeral",
327
+ "--ignore-user-config",
328
+ "--ignore-rules",
329
+ ...(readOnlyAnalysis ? codexAnalysisArguments(request.system) : []),
330
+ "--output-schema",
331
+ schemaPath,
332
+ "--output-last-message",
333
+ outputPath,
334
+ "-",
335
+ ],
336
+ prompt,
337
+ { spawnImpl, executionTimeoutMs: remaining, label, cwd: workDir, environment, provider, signal, readOnlyAnalysis, model,
338
+ terminationGraceMs: Number.isInteger(terminationGraceMs) ? Math.max(1, Math.min(terminationGraceMs, 5_000)) : 1_000 },
339
+ );
340
+ selectedModel = model;
341
+ break;
342
+ } catch (error) {
343
+ if (error.code !== "provider_model_unsupported" || model === models.at(-1)) throw error;
344
+ }
345
+ }
313
346
  const envelope = provider === "claude" ? JSON.parse(stdout) : null;
314
347
  if (envelope?.is_error || (envelope && envelope.subtype !== "success")) {
315
348
  if (readOnlyAnalysis && envelope?.is_error === true && providerSignInRequired(envelope.result)) {
@@ -325,7 +358,7 @@ export async function runStructuredProviderReview(
325
358
  }
326
359
  return {
327
360
  output,
328
- model: provider === "codex" ? SQM_CODEX_MODEL : `${provider}-local/${inspection.installed_version || "unknown"}`,
361
+ model: provider === "codex" ? selectedModel : `${provider}-local/${inspection.installed_version || "unknown"}`,
329
362
  };
330
363
  } catch (error) {
331
364
  if (error instanceof SqmLocalReviewError || error?.code === 'desktop_access_required') throw error;
@@ -350,7 +383,7 @@ function codexAnalysisArguments(system) {
350
383
  '-c', `developer_instructions=${JSON.stringify(system)}`];
351
384
  }
352
385
 
353
- function spawnReview(command, args, prompt, { spawnImpl, executionTimeoutMs, label, cwd, environment, provider, signal, readOnlyAnalysis, terminationGraceMs }) {
386
+ function spawnReview(command, args, prompt, { spawnImpl, executionTimeoutMs, label, cwd, environment, provider, signal, readOnlyAnalysis, terminationGraceMs, model }) {
354
387
  return new Promise((resolve, reject) => {
355
388
  if (signal?.aborted) return reject(new Error('Local analysis was cancelled'));
356
389
  const child = spawnImpl(command, args, {
@@ -361,6 +394,7 @@ function spawnReview(command, args, prompt, { spawnImpl, executionTimeoutMs, lab
361
394
  });
362
395
  let stdout = "";
363
396
  let stderr = "";
397
+ let stderrPending = "";
364
398
  let settled = false;
365
399
  let terminalError = null;
366
400
  let killTimer = null;
@@ -396,9 +430,16 @@ function spawnReview(command, args, prompt, { spawnImpl, executionTimeoutMs, lab
396
430
  for (const line of stdout.split('\n').filter(Boolean)) {
397
431
  let event;
398
432
  try { event = JSON.parse(line); } catch { continue; }
399
- if (['error', 'turn.failed'].includes(event.type) && providerSignInRequired(event.message ?? event.error?.message)) {
400
- halt(new SqmLocalReviewError('Sign in to the local AI app again.', { code: 'provider_sign_in_required' }));
401
- return;
433
+ if (['error', 'turn.failed'].includes(event.type)) {
434
+ const message = event.message ?? event.error?.message;
435
+ if (providerSignInRequired(message)) {
436
+ halt(new SqmLocalReviewError('Sign in to the local AI app again.', { code: 'provider_sign_in_required' }));
437
+ return;
438
+ }
439
+ if (providerModelUnsupported(message, model)) {
440
+ halt(modelUnsupportedError(model));
441
+ return;
442
+ }
402
443
  }
403
444
  if (/command_execution|mcp_tool_call|web_search|file_change|tool_call/.test(event.item?.type || '')) {
404
445
  halt(new Error('Read-only analysis attempted to use a tool'));
@@ -414,9 +455,32 @@ function spawnReview(command, args, prompt, { spawnImpl, executionTimeoutMs, lab
414
455
  child.stderr?.setEncoding?.("utf8");
415
456
  child.stderr?.on?.("data", (chunk) => {
416
457
  if (!readOnlyAnalysis) stderr = `${stderr}${chunk}`.slice(-4000);
458
+ if (provider === "codex") {
459
+ stderrPending = `${stderrPending}${chunk}`.slice(-8000);
460
+ const lines = stderrPending.split(/\r?\n/);
461
+ stderrPending = lines.pop() || "";
462
+ for (const line of lines) checkStderr(line);
463
+ }
417
464
  });
465
+ const checkStderr = line => {
466
+ // Native CLI diagnostics only; never match warnings or assistant text.
467
+ const match = line.match(/^\s*ERROR:\s*(.*)$/);
468
+ if (!match) return;
469
+ let message = match[1];
470
+ try {
471
+ const event = JSON.parse(message);
472
+ if (event.type !== "error") return;
473
+ message = event.error?.message ?? event.message;
474
+ } catch { /* Older CLIs print a plain native ERROR line. */ }
475
+ if (providerSignInRequired(message)) {
476
+ halt(new SqmLocalReviewError('Sign in to the local AI app again.', { code: 'provider_sign_in_required' }));
477
+ } else if (providerModelUnsupported(message, model)) {
478
+ halt(modelUnsupportedError(model));
479
+ }
480
+ };
418
481
  child.on("error", finish);
419
482
  child.on("close", (code) => {
483
+ if (provider === "codex") checkStderr(stderrPending);
420
484
  if (readOnlyAnalysis && provider === 'claude' && !terminalError) {
421
485
  let envelope;
422
486
  try { envelope = JSON.parse(stdout); } catch { /* Non-JSON failure remains unavailable. */ }
@@ -433,10 +497,22 @@ function spawnReview(command, args, prompt, { spawnImpl, executionTimeoutMs, lab
433
497
  });
434
498
  }
435
499
 
500
+ function providerModelUnsupported(message, model) {
501
+ if (typeof message !== "string" || typeof model !== "string") return false;
502
+ const rejection = message.trim().match(/^The ['"]([^'"]+)['"] model is not supported(?: when using Codex with a ChatGPT account)?\.?$/i);
503
+ return rejection?.[1] === model;
504
+ }
505
+
506
+ function modelUnsupportedError(model) {
507
+ return new SqmLocalReviewError(`The local Codex account does not support ${model}. Update Codex or check the models available to this account.`, {
508
+ code: "provider_model_unsupported", status: 400,
509
+ });
510
+ }
511
+
436
512
  function providerSignInRequired(message) {
437
513
  // Match explicit provider authentication failures only, never general 403s
438
514
  // (which can be permissions/quotas) or model-generated prose/tool output.
439
- return typeof message === 'string' && /(?:not logged in|please (?:log|sign) in|invalid api key|authentication token (?:is )?(?:invalid|expired)|refresh token (?:has )?expired)/i.test(message.slice(0, 4000));
515
+ return typeof message === 'string' && /(?:\b401\s+Unauthorized\b|not logged in|please (?:log|sign) in|invalid api key|authentication token (?:is )?(?:invalid|expired)|refresh token (?:has )?expired)/i.test(message.slice(0, 4000));
440
516
  }
441
517
 
442
518
  function safeReviewEnvironment(environment, provider) {
@@ -496,6 +572,7 @@ function reviewFailure(error, stage, provider) {
496
572
  function formatFailure(failure, failureReported = null) {
497
573
  if (!failure) return "SQM analysis failed. No failure diagnostics were recorded.";
498
574
  const hint = failure.code === "provider_sign_in_required" ? "Sign in to the local AI app again."
575
+ : failure.code === "provider_model_unsupported" ? "The local Codex account rejected the configured SQM models. Update Codex or check this account's available models."
499
576
  : failure.code === "analysis_timeout" ? "The local AI analysis exceeded its time limit."
500
577
  : failure.code === "sqm_source_loading_failed" ? "SIMY could not load evidence for this analysis."
501
578
  : failure.status === 403 || failure.code === "desktop_access_required" ? "Check SQM and Developer access for the selected organization."