@awak-app/simy-cli 0.5.72 → 0.5.73

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.
Files changed (48) hide show
  1. package/README.md +84 -0
  2. package/package.json +2 -2
  3. package/src/agent.js +191 -11
  4. package/src/backend-executable.js +21 -1
  5. package/src/cli-contract.js +1 -1
  6. package/src/codex-plugins/simy/hooks/README.md +22 -0
  7. package/src/codex-plugins/simy/hooks/continuation-policy.mjs +27 -0
  8. package/src/codex-plugins/simy/hooks/delivery-loop.mjs +179 -34
  9. package/src/codex-plugins/simy/hooks/outcome-progress.mjs +129 -0
  10. package/src/coding-run-recovery.js +88 -0
  11. package/src/coding-run-store.js +2 -2
  12. package/src/continuation-policy.js +2 -0
  13. package/src/delivery-hooks/command.js +11 -5
  14. package/src/delivery-hooks/desktop.js +12 -3
  15. package/src/delivery-hooks/supervisor.js +14 -10
  16. package/src/desktop-host.js +8 -6
  17. package/src/desktop-sqm-settings.js +24 -7
  18. package/src/device-signing-identity.js +65 -0
  19. package/src/diagnostic-log.js +68 -0
  20. package/src/doctor.js +55 -0
  21. package/src/index.js +7 -0
  22. package/src/local-task.js +68 -3
  23. package/src/orchestrator/budget.js +55 -4
  24. package/src/orchestrator/charter-validation.js +33 -0
  25. package/src/orchestrator/completion-contract.js +1 -1
  26. package/src/orchestrator/contract.js +2 -0
  27. package/src/orchestrator/delivery/references/state-and-receipts.md +21 -3
  28. package/src/orchestrator/independent-audit.js +9 -0
  29. package/src/orchestrator/instruction.js +6 -1
  30. package/src/orchestrator/loop.js +38 -41
  31. package/src/orchestrator/outcome-monitor.js +58 -38
  32. package/src/orchestrator/outcome-progress.js +2 -0
  33. package/src/purpose-local-proof.js +380 -0
  34. package/src/runner.js +224 -36
  35. package/src/session-supervision/codex-app-server.js +3 -2
  36. package/src/session-supervision/index.js +16 -4
  37. package/src/sqm/bundle-store.js +11 -7
  38. package/src/sqm/claude-plugin.js +2 -0
  39. package/src/sqm/command.js +36 -4
  40. package/src/sqm/diagnostics.js +1 -1
  41. package/src/sqm/evidence-client.js +23 -7
  42. package/src/sqm/history-store.js +4 -0
  43. package/src/sqm/index.js +104 -14
  44. package/src/sqm/proof.js +3 -23
  45. package/src/sqm/request-recovery.js +82 -0
  46. package/src/sqm/review-worker.js +59 -12
  47. package/src/sqm/session.js +33 -3
  48. package/src/startup-preflight.js +44 -0
@@ -3,6 +3,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
 
6
+ import { inspectProviderStatus } from "../desktop-status.js";
6
7
  import { inspectDesktopExecutor } from "../desktop-executor.js";
7
8
  import {
8
9
  webApiErrorMessage,
@@ -88,7 +89,8 @@ export function createSqmLocalReviewWorker({
88
89
  leaseSeconds = DEFAULT_LEASE_SECONDS,
89
90
  heartbeatIntervalMs = Math.max(10_000, Math.floor(leaseSeconds * 500)),
90
91
  getProvider = () => "codex",
91
- runReview = (request, provider) => runStructuredProviderReview(provider, request),
92
+ runReview = (request, provider, options) => runStructuredProviderReview(provider, request, { ...options, beforeDispatch: options?.beforeLeaseDispatch }),
93
+ getEnvironmentKey = () => null,
92
94
  beginWork = () => () => {},
93
95
  onError = () => {},
94
96
  onChange = () => {},
@@ -104,7 +106,7 @@ export function createSqmLocalReviewWorker({
104
106
  let activeJobId = null;
105
107
  let pollPromise = null;
106
108
  let closed = false;
107
- let issue = null;
109
+ let issue = null, pausedEnvironment = null, pauseReason = null;
108
110
  let issueContext = null;
109
111
  const failureDetails = new WeakMap();
110
112
  const pauses = new Map();
@@ -132,7 +134,7 @@ export function createSqmLocalReviewWorker({
132
134
  item.state = state; item.finishedAt = new Date().toISOString();
133
135
  item.result = state === "completed" ? "SQM review submitted." : "";
134
136
  item.error = state === "failed" ? formatFailure(failure, failureReported) : "";
135
- if (failure) { item.phase = failure.stage; item.errorCode = failure.code; }
137
+ if (failure) { item.phase = failure.stage; item.errorStage = failure.stage; item.errorCode = failure.code; item.httpStatus = failure.status; }
136
138
  await persist(scope, item);
137
139
  }
138
140
  onChange();
@@ -148,11 +150,11 @@ export function createSqmLocalReviewWorker({
148
150
 
149
151
  // Repair only a rejected model result, under the same live lease. Network,
150
152
  // authorization, and persistence failures must not repeat provider execution.
151
- const validatedReview = async (request, provider, code, submit, setStage) => {
153
+ const validatedReview = async (request, provider, code, submit, setStage, reviewOptions) => {
152
154
  let nextRequest = request;
153
155
  for (let attempt = 0; ; attempt += 1) {
154
156
  setStage(false);
155
- const review = await runReview(nextRequest, provider);
157
+ const review = await runReview(nextRequest, provider, reviewOptions);
156
158
  setStage(true);
157
159
  try {
158
160
  return { review, response: await submit(review) };
@@ -180,22 +182,33 @@ export function createSqmLocalReviewWorker({
180
182
  heartbeatTimer = setInterval(heartbeat, heartbeatIntervalMs);
181
183
  heartbeatTimer.unref?.();
182
184
  let stage = "analysis";
185
+ const reviewOptions = { providerReportingSupported: claim.capabilities?.reported_executor_provider === true,
186
+ beforeLeaseDispatch: () => api.request({ action: "heartbeat", ...leaseBody(claim, provider) }) };
187
+ const providerReport = (review, selectedProvider = provider) => claim.capabilities?.reported_executor_provider === true
188
+ ? { reported_executor_provider: review.provider || selectedProvider } : {};
183
189
  try {
184
190
  const { review: analysis, response: advanced } = await validatedReview(claim.request, provider, "invalid_sqm_analysis_output", analysis => api.request({
185
191
  action: "advance",
186
192
  ...leaseBody(claim, provider),
187
193
  analysis_output: analysis.output,
188
194
  executor_model: analysis.model,
189
- }), submitting => { stage = submitting ? "advance" : "analysis"; if (submitting) pauses.delete(contextKey(provider, historyScope)); });
195
+ ...providerReport(analysis),
196
+ }), submitting => { stage = submitting ? "advance" : "analysis"; if (submitting) pauses.delete(contextKey(provider, historyScope)); }, reviewOptions);
197
+ const analysisProvider = analysis.provider || provider;
198
+ const item = history.find(item => item.id === claim.job_id);
199
+ if (item) item.provider = analysisProvider;
190
200
  if (advanced.completed) { await updateTask(claim.job_id, "completed", historyScope); return advanced; }
191
201
  stage = "adjudication";
192
- const { response: completed } = await validatedReview(advanced.request, provider, "invalid_sqm_adjudication_output", adjudication => api.request({
202
+ const { review: adjudication, response: completed } = await validatedReview(advanced.request, analysisProvider, "invalid_sqm_adjudication_output", adjudication => api.request({
193
203
  action: "complete",
194
204
  ...leaseBody(claim, provider),
195
205
  analysis_output: analysis.output,
196
206
  adjudication_output: adjudication.output,
197
207
  executor_model: adjudication.model || analysis.model,
198
- }), submitting => { stage = submitting ? "complete" : "adjudication"; if (submitting) pauses.delete(contextKey(provider, historyScope)); });
208
+ ...providerReport(adjudication, analysisProvider),
209
+ ...(claim.capabilities?.reported_executor_provider === true ? { analysis_executor_provider: analysisProvider, analysis_executor_model: analysis.model } : {}),
210
+ }), submitting => { stage = submitting ? "complete" : "adjudication"; if (submitting) pauses.delete(contextKey(provider, historyScope)); }, reviewOptions);
211
+ if (item) item.provider = adjudication.provider || analysisProvider;
199
212
  await updateTask(claim.job_id, "completed", historyScope);
200
213
  return completed;
201
214
  } catch (error) {
@@ -221,7 +234,7 @@ export function createSqmLocalReviewWorker({
221
234
  return {
222
235
  snapshot() {
223
236
  const pause = currentPause();
224
- return { active_job_id: activeJobId, polling: Boolean(pollPromise), closed, issue: issueContext === contextKey(getProvider(), getHistoryScope()) ? issue : null,
237
+ return { active_job_id: activeJobId, polling: Boolean(pollPromise), closed, paused: pausedEnvironment !== null, pause_reason: pauseReason, issue: issueContext === contextKey(getProvider(), getHistoryScope()) ? issue : null,
225
238
  ...(pause ? { provider_pause: { code: pause.code, status: pause.status, retry_at: new Date(pause.retryAt).toISOString(), message: pause.message } } : {}) };
226
239
  },
227
240
  desktopTasks() { return { activeCount: activeJobId ? 1 : 0, items: visibleHistory().map(item => ({ ...item })) }; },
@@ -229,9 +242,13 @@ export function createSqmLocalReviewWorker({
229
242
  const item = visibleHistory().find(item => item.key === key);
230
243
  return item ? { key, instruction: `SQM local review · ${item.provider}`, result: item.state === "completed" ? "SQM review submitted." : "", error: item.error || "", links: item.webUrl ? [{ label: "Analysis details", url: item.webUrl }] : [] } : null;
231
244
  },
232
- resetHistory() { if (!pollPromise) { history.splice(0); issue = null; } },
245
+ retry() { if (closed || pollPromise) return false; pausedEnvironment = null; pauseReason = null; pauses.delete(contextKey(getProvider(), getHistoryScope())); issue = null; onChange(); return true; },
246
+ resetHistory() { if (!pollPromise) { history.splice(0); issue = null; pausedEnvironment = null; pauseReason = null; } },
233
247
  poll() {
234
248
  if (closed || pollPromise) return pollPromise || Promise.resolve(null);
249
+ const environmentKey = getEnvironmentKey();
250
+ if (pausedEnvironment !== null && pausedEnvironment === environmentKey) return Promise.resolve(null);
251
+ if (pausedEnvironment !== null) { pausedEnvironment = null; pauseReason = null; pauses.delete(contextKey(getProvider(), getHistoryScope())); issue = null; }
235
252
  const provider = getProvider();
236
253
  if (provider == null) return Promise.resolve(null);
237
254
  assertProvider(provider);
@@ -273,6 +290,9 @@ export function createSqmLocalReviewWorker({
273
290
  history.unshift(item); historyScopes.set(item, JSON.stringify(scope)); history.splice(50);
274
291
  await persist(scope, item);
275
292
  }
293
+ if (["provider_model_unsupported", "codex_local_unavailable", "claude_local_unavailable", "provider_sign_in_required", "provider_status_unknown"].includes(error.code)) {
294
+ pausedEnvironment = environmentKey; pauseReason = error.code;
295
+ }
276
296
  issueContext = key;
277
297
  issue = error.status === 403 ? "forbidden" : error.status === 401 ? "signed_out" : "unavailable";
278
298
  onError(error);
@@ -296,7 +316,33 @@ export function runStructuredCodexReview(request, options) {
296
316
  return runStructuredProviderReview("codex", request, options);
297
317
  }
298
318
 
299
- export async function runStructuredProviderReview(
319
+ export async function runStructuredProviderReview(provider, request, options = {}) {
320
+ const fallbackProviders = options.enableEngineFallback === true && options.providerReportingSupported === true && options.readOnlyAnalysis !== false && Array.isArray(options.allowedFallbackProviders)
321
+ ? [...new Set(options.allowedFallbackProviders.filter(candidate => ["codex", "claude"].includes(candidate) && candidate !== provider))] : [];
322
+ if (!fallbackProviders.length) return runSingleProviderReview(provider, request, options);
323
+ const deadlineAt = Date.now() + (options.executionTimeoutMs ?? DEFAULT_EXECUTION_TIMEOUT_MS);
324
+ const attempts = [];
325
+ for (const candidate of [provider, ...fallbackProviders]) {
326
+ if (options.signal?.aborted) throw new SqmLocalReviewError("Local analysis was cancelled.", { code: "analysis_cancelled" });
327
+ if (Date.now() >= deadlineAt) throw new SqmLocalReviewError("Local analysis timed out.", { code: "analysis_timeout" });
328
+ if (candidate !== provider) {
329
+ await options.beforeDispatch?.(candidate);
330
+ const readiness = await (options.inspectProviderStatus || inspectProviderStatus)(candidate);
331
+ if (readiness.state !== "ready") throw new SqmLocalReviewError("Fallback provider is not signed in and ready.", {
332
+ code: readiness.state === "signed_out" ? "provider_sign_in_required" : "provider_status_unknown" });
333
+ }
334
+ try {
335
+ const result = await runSingleProviderReview(candidate, request, { ...options, deadlineAt,
336
+ beforeDispatch: () => options.beforeDispatch?.(candidate) });
337
+ return { ...result, provider: candidate, provider_attempts: [...attempts, { provider: candidate, code: "completed" }] };
338
+ } catch (error) {
339
+ attempts.push({ provider: candidate, code: error.code });
340
+ if (!["provider_model_unsupported", "codex_local_unavailable", "claude_local_unavailable"].includes(error.code) || candidate === fallbackProviders.at(-1)) throw error;
341
+ }
342
+ }
343
+ }
344
+
345
+ async function runSingleProviderReview(
300
346
  provider,
301
347
  request,
302
348
  {
@@ -308,6 +354,7 @@ export async function runStructuredProviderReview(
308
354
  readOnlyAnalysis = true,
309
355
  terminationGraceMs = 1_000,
310
356
  beforeDispatch = async () => {},
357
+ deadlineAt,
311
358
  } = {},
312
359
  ) {
313
360
  assertProvider(provider);
@@ -330,7 +377,7 @@ export async function runStructuredProviderReview(
330
377
  "Return only JSON that strictly matches the provided output schema.",
331
378
  request.prompt,
332
379
  ].join("\n\n");
333
- const deadline = Date.now() + executionTimeoutMs;
380
+ const deadline = deadlineAt ?? Date.now() + executionTimeoutMs;
334
381
  const models = provider === "codex" ? SQM_CODEX_MODELS : [null];
335
382
  let stdout;
336
383
  let selectedModel;
@@ -18,10 +18,18 @@ export async function ensureSqmSession(
18
18
  startAgentImpl = startAgent,
19
19
  openAuthorizationUrlImpl = openAuthorizationUrl,
20
20
  createAgentShutdownImpl = createAgentShutdown,
21
+ expectedScope,
22
+ signal,
23
+ rejectedToken,
21
24
  } = {},
22
25
  ) {
23
26
  let session = await readSessionImpl(webOrigin);
24
- if (sqmSessionReady(session, webOrigin, now(), isSessionValidImpl)) return session;
27
+ const pinnedScope = expectedScope || sqmSessionScope(session, webOrigin);
28
+ signal?.throwIfAborted();
29
+ if (session?.token !== rejectedToken && sqmSessionReady(session, webOrigin, now(), isSessionValidImpl)) {
30
+ assertSqmSessionScope(session, webOrigin, pinnedScope);
31
+ return session;
32
+ }
25
33
 
26
34
  const agent = await startAgentImpl({ requestedPort: 0, webOrigin, quiet: true });
27
35
  const shutdown = createAgentShutdownImpl(agent);
@@ -35,19 +43,41 @@ export async function ensureSqmSession(
35
43
 
36
44
  const deadline = now() + timeoutMs;
37
45
  while (now() < deadline) {
46
+ signal?.throwIfAborted();
38
47
  await sleep(Math.min(500, Math.max(1, deadline - now())));
39
48
  session = await readSessionImpl(webOrigin);
40
- if (sqmSessionReady(session, webOrigin, now(), isSessionValidImpl)) {
49
+ if (session?.token !== rejectedToken && sqmSessionReady(session, webOrigin, now(), isSessionValidImpl)) {
50
+ assertSqmSessionScope(session, webOrigin, pinnedScope);
41
51
  logger.log("SIMY CLI authorization completed. Continuing SQM check.");
42
52
  return session;
43
53
  }
44
54
  }
45
- throw new Error("SIMY authorization timed out. Complete the browser sign-in and rerun the SQM command.");
55
+ const error = new Error("SIMY authorization timed out. Complete the browser sign-in and rerun the SQM command.");
56
+ error.code = "authorization_timeout";
57
+ throw error;
46
58
  } finally {
47
59
  await shutdown();
48
60
  }
49
61
  }
50
62
 
63
+ export function sqmSessionScope(session, webOrigin) {
64
+ return { account_id: session?.account_id || session?.auth_user_id || null,
65
+ organization_id: session?.organization_id || session?.org_id || null,
66
+ device_id: session?.device_id || null, api_base_url: session?.api_base_url || null,
67
+ web_origin: new URL(webOrigin).origin };
68
+ }
69
+
70
+ export function assertSqmSessionScope(session, webOrigin, expected) {
71
+ const current = sqmSessionScope(session, webOrigin);
72
+ for (const field of ["account_id", "organization_id", "device_id", "api_base_url", "web_origin"]) {
73
+ if (expected?.[field] && expected[field] !== current[field]) {
74
+ const error = new Error(`SQM authorization changed its ${field}; resume the original authorized scope.`);
75
+ error.code = "authorization_scope_changed";
76
+ throw error;
77
+ }
78
+ }
79
+ }
80
+
51
81
  function sqmSessionReady(session, webOrigin, now, isSessionValidImpl) {
52
82
  return Boolean(
53
83
  isSessionValidImpl(session, now, webOrigin) &&
@@ -0,0 +1,44 @@
1
+ import { createCodingLoopSnapshot } from "./orchestrator/contract.js";
2
+ import { validateCharter } from "./orchestrator/charter-validation.js";
3
+
4
+ // Reuse the execution charter rather than inventing a second approval policy.
5
+ // These checks inspect local metadata only; model access requires real dispatch.
6
+ export function startupRequirementChecks(request = {}) {
7
+ const checks = [{ key: "model_access", status: "unknown", blocking: false,
8
+ summary: "Signing in does not prove access to the requested model. Access is checked on dispatch without a paid preflight request.",
9
+ action: { id: "review_model_access", type: "inspect_provider", label: "Review provider and model settings" } }];
10
+ if (!String(request.requirement || "").trim()) {
11
+ checks.push({ key: "task_requirements", status: "unknown", blocking: false,
12
+ summary: "Environment check only. Supply the task requirement to check acceptance criteria and design approval.",
13
+ action: { id: "provide_requirement", type: "edit_requirements", label: "Provide task requirements" } });
14
+ return checks;
15
+ }
16
+ const { charter } = createCodingLoopSnapshot({ runId: "preflight", request });
17
+ const findings = validateCharter(charter);
18
+ for (const [key, code, passed, actionId, label] of [
19
+ ["acceptance_criteria", "EXPLICIT_ACCEPTANCE_CRITERIA_REQUIRED", "Acceptance criteria satisfy the task risk policy.", "provide_acceptance_criteria", "Provide explicit acceptance criteria"],
20
+ ["design_review", "DESIGN_REVIEW_REQUIRED", "Design approval satisfies the task risk policy.", "approve_design", "Provide design review and approval evidence"],
21
+ ["required_checks", "REQUIRED_CI_CHECKS_MISSING", "Required checks satisfy the task risk policy.", "provide_required_checks", "Provide CI and security check names"],
22
+ ]) {
23
+ const finding = findings.find(item => item.code === code);
24
+ checks.push({ key, status: finding ? "failed" : "passed", blocking: Boolean(finding),
25
+ summary: finding?.explanation || passed,
26
+ ...(finding ? { code, action: { id: actionId, type: "edit_requirements", label } } : {}) });
27
+ }
28
+ checks.push({ key: "assurance_selection", status: "unknown", blocking: false,
29
+ summary: "The executor must verify the five-level comparison and user selection in existing decision evidence before material implementation.",
30
+ action: { id: "review_assurance_selection", type: "review_decision_evidence", label: "Review assurance level and selection evidence" } });
31
+ checks.push({ key: "external_dependencies", status: "unknown", blocking: false,
32
+ summary: "Confirm test accounts, external resources and permissions required by the acceptance criteria. Local readiness does not certify Cloud entitlement.",
33
+ action: { id: "confirm_external_dependencies", type: "edit_requirements", label: "Confirm external inputs and permissions" } });
34
+ return checks;
35
+ }
36
+
37
+ export function summarizeStartupChecks(checks) {
38
+ const blockers = checks.filter(check => check.status === "failed" && check.blocking !== false);
39
+ const unknown = checks.filter(check => check.status === "unknown");
40
+ return { status: blockers.length ? "blocked" : unknown.length ? "needs_confirmation" : "ready",
41
+ blocker_keys: blockers.map(check => check.key), unknown_keys: unknown.map(check => check.key),
42
+ actions: checks.flatMap(check => check.action ? [{ check_key: check.key, ...check.action }]
43
+ : (check.recovery?.actions || []).map(action => ({ check_key: check.key, ...action }))) };
44
+ }