@awak-app/simy-cli 0.6.12 → 0.6.13

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 CHANGED
@@ -817,6 +817,32 @@ model responses or credentials. Historical summaries are not a replacement for
817
817
  signed proof files or delivery acceptance. Records from versions that kept
818
818
  analysis only in memory cannot be recovered after that process exits.
819
819
 
820
+ The CLI Agentic Loop uses [proportionate retry and review decisions](docs/stop-prevention/proportionate-continuation.md): reasoning notes are advisory, and an independent reviewer may explicitly defer or dismiss minor/warning/info findings with a nonempty explanation while retaining them in the receipt. Required acceptance evidence and major safety findings remain protected.
821
+
822
+ ### Local coding attempt result collection
823
+
824
+ For `executeProcessAttempt`, native provider JSONL is read only from a successfully finished turn's final
825
+ assistant/result channel. Tool output and intermediate messages are never a
826
+ fallback result. Legacy line-anchored markers still work and may contain formatted
827
+ JSON; missing, ambiguous, truncated, invalid, oversized and failed-turn results
828
+ have distinct `result_parse.code` values. Invalid collection cannot become a
829
+ successful empty result. Plain-text markers are supported only for explicit
830
+ shell command overrides. This does not independently verify executor claims.
831
+ The parser can re-read a saved raw result without rerunning the provider.
832
+ Local Task and direct-task result parsers are separate boundaries and are not
833
+ covered by this coding-attempt guarantee.
834
+
835
+ ### Explicit desktop provider selection
836
+
837
+ Local coding requests may specify `model` / `reasoning_effort` and independently
838
+ `audit_model` / `audit_reasoning_effort`. Both selections survive recovery.
839
+ Codex receives the exact model and reasoning config; the CLI does not use a
840
+ catalog to reject unknown model names or silently replace a rejected model.
841
+ Claude receives the exact model; explicit reasoning is unsupported by this
842
+ adapter and fails before dispatch. A custom shell override cannot enforce an
843
+ explicit selection and is rejected instead of dropping it. Provider acceptance,
844
+ authentication and actual availability still require the provider's response.
845
+ This changes command construction only; it does not start any workflow.
820
846
 
821
847
  ### Reusing a running service and launch recovery
822
848
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.6.12",
3
+ "version": "0.6.13",
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
@@ -1,3 +1,4 @@
1
+ import { normalizeProviderModelSelection } from "./provider-model-selection.js";
1
2
  import { assertConnectionScope, connectionScope, connectionDiagnostic } from "./connection-diagnostics.js";
2
3
  import { authorizeLaunch } from "./launch-authorization.js";
3
4
  import { normalizePurposeVerification, assertPurposeActor, preparePurposeVerification, verifyPurposeLocalTask, purposeSigningKeyPath, purposeVerificationCapability, purposeWorkspaceBusy } from "./purpose-local-proof.js";
@@ -1998,6 +1999,13 @@ export async function startAgent({
1998
1999
  return;
1999
2000
  }
2000
2001
  const { body, attachments } = await readCodingLoopStart(req);
2002
+ let providerSelection;
2003
+ try {
2004
+ providerSelection = normalizeProviderModelSelection(body);
2005
+ } catch (error) {
2006
+ json(res, 400, { error: error.message, code: "invalid_provider_selection" });
2007
+ return;
2008
+ }
2001
2009
  const guardrailSource = String(body.source_message || "").trim();
2002
2010
  if (guardrailSource) {
2003
2011
  const guardrail = classifyExecutionRequirement(guardrailSource);
@@ -2101,6 +2109,7 @@ export async function startAgent({
2101
2109
  const run = createRun({
2102
2110
  runId,
2103
2111
  request: {
2112
+ ...providerSelection,
2104
2113
  backend: body.backend === "claude" ? "claude" : "codex",
2105
2114
  execution_target: executionTarget,
2106
2115
  execution_device_id: session?.device_id || null,
@@ -2752,6 +2761,7 @@ export async function startAgent({
2752
2761
  scanRepositories,
2753
2762
  selectRepository,
2754
2763
  create: (input) => withUpdateWork(updateManager, async () => {
2764
+ const providerSelection = normalizeProviderModelSelection(input);
2755
2765
  const scope = await authorizeDesktopWork();
2756
2766
  if (!isSessionValid(session, Date.now(), apiOrigin)) {
2757
2767
  throw new Error("Sign in to SIMY before starting a coding task.");
@@ -2767,6 +2777,8 @@ export async function startAgent({
2767
2777
  }
2768
2778
  const indexedRepository = await ensureRepositoryIndexed(repository);
2769
2779
  const request = {
2780
+ ...providerSelection,
2781
+ audit_backend: input.audit_backend === "claude" || input.audit_backend === "codex" ? input.audit_backend : null,
2770
2782
  requirement,
2771
2783
  repository,
2772
2784
  local_path: indexedRepository?.local_path || input.localPath || workspace.localPath,
@@ -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.6.12";
16
+ export const CLI_VERSION = "0.6.13";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function withCliContract(
@@ -1,4 +1,5 @@
1
1
  import spawn from "cross-spawn";
2
+ import { normalizeProviderModelSelection } from "./provider-model-selection.js";
2
3
 
3
4
  import {
4
5
  BACKEND_VERSION_POLICIES,
@@ -48,6 +49,7 @@ export function desktopExecutorCapability(inspection) {
48
49
  export async function resolveDesktopExecutorCommand({
49
50
  backend,
50
51
  model = null,
52
+ reasoningEffort = null,
51
53
  instruction,
52
54
  repositoryPath,
53
55
  executionKind = "agentic_loop",
@@ -62,17 +64,26 @@ export async function resolveDesktopExecutorCommand({
62
64
  throw new Error("Desktop execution kind must be agentic_loop or direct.");
63
65
  }
64
66
  const normalizedPermissionMode = normalizePermissionMode(permissionMode);
65
- const normalizedModel =
66
- model === null || model === undefined
67
- ? null
68
- : String(model).trim();
69
- if (normalizedModel !== null && !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$/.test(normalizedModel)) {
70
- throw new Error("Desktop executor model is invalid.");
67
+ const { model: normalizedModel, reasoning_effort: effort } =
68
+ normalizeProviderModelSelection({ model, reasoning_effort: reasoningEffort });
69
+ if (provider !== "codex" && effort !== null) {
70
+ const error = new Error("Explicit reasoning effort is not supported by this Claude command adapter.");
71
+ error.code = "desktop_executor_selection_unsupported";
72
+ throw error;
71
73
  }
74
+ const selectionArgs = [
75
+ ...(normalizedModel ? ["--model", normalizedModel] : []),
76
+ ...(effort ? ["--config", `model_reasoning_effort=${JSON.stringify(effort)}`] : []),
77
+ ];
72
78
  const target = normalizeDesktopExecutionTarget(DESKTOP_EXECUTION_TARGET);
73
79
  const override =
74
80
  provider === "claude" ? environment.SIMY_CLAUDE_COMMAND : environment.SIMY_CODEX_COMMAND;
75
81
  if (override) {
82
+ if (normalizedModel || effort) {
83
+ const error = new Error("Explicit provider selection cannot be enforced by a shell command override.");
84
+ error.code = "desktop_executor_selection_unsupported";
85
+ throw error;
86
+ }
76
87
  return {
77
88
  ...shellCommand(override, repositoryPath, instruction, spawnImpl),
78
89
  backend: provider,
@@ -100,7 +111,7 @@ export async function resolveDesktopExecutorCommand({
100
111
  normalizedPermissionMode,
101
112
  normalizedModel,
102
113
  )
103
- : claudeBackendArgs(instruction)
114
+ : claudeBackendArgs(instruction, normalizedModel)
104
115
  : directExecution
105
116
  ? [
106
117
  "exec",
@@ -108,22 +119,24 @@ export async function resolveDesktopExecutorCommand({
108
119
  "--skip-git-repo-check",
109
120
  "--sandbox",
110
121
  normalizedPermissionMode === "read_only" ? "read-only" : "workspace-write",
111
- ...(normalizedModel ? ["--model", normalizedModel] : []),
122
+ ...selectionArgs,
112
123
  instruction,
113
124
  ]
114
- : ["exec", "--json", instruction],
125
+ : ["exec", "--json", ...selectionArgs, instruction],
115
126
  env: {},
116
127
  spawn: spawnImpl,
117
128
  backend: provider,
118
129
  execution_target: target,
119
130
  execution_kind: executionKind,
120
131
  permission_mode: normalizedPermissionMode,
132
+ requested_model: normalizedModel,
133
+ requested_reasoning_effort: effort,
121
134
  verification: "compatible_version",
122
135
  installed_version: inspection.installed_version,
123
136
  };
124
137
  }
125
138
 
126
- export function claudeBackendArgs(instruction) {
139
+ export function claudeBackendArgs(instruction, model = null) {
127
140
  return [
128
141
  "-p",
129
142
  instruction,
@@ -134,6 +147,7 @@ export function claudeBackendArgs(instruction) {
134
147
  // by the user. Print mode cannot display permission prompts, so edits would
135
148
  // otherwise be silently unavailable to the desktop executor.
136
149
  "--dangerously-skip-permissions",
150
+ ...(model ? ["--model", model] : []),
137
151
  ];
138
152
  }
139
153
 
@@ -134,7 +134,7 @@ export async function auditAttempt(charter, attempt, { previousAttempt = null, s
134
134
 
135
135
  const retryProblemSolving = evaluateRetryProblemSolving(attempt, previousAttempt);
136
136
  for (const check of retryProblemSolving.checks) {
137
- addCheck(check.id, check.label, check.passed, check.detail);
137
+ addCheck(check.id, check.label, check.passed, check.detail, check.severity);
138
138
  }
139
139
  attempt.retry_problem_solving = retryProblemSolving;
140
140
 
@@ -220,7 +220,7 @@ export async function auditAttempt(charter, attempt, { previousAttempt = null, s
220
220
  }
221
221
 
222
222
  const findings = checks
223
- .filter((check) => !check.passed)
223
+ .filter((check) => !check.passed && check.severity !== "advisory")
224
224
  .map((check) => ({
225
225
  code: check.id.toUpperCase(),
226
226
  title: check.label,
@@ -1,3 +1,4 @@
1
+ import { normalizeProviderModelSelection } from "../provider-model-selection.js";
1
2
  import { randomUUID } from "node:crypto";
2
3
 
3
4
  import {
@@ -103,6 +104,7 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
103
104
  charter: {
104
105
  id: `${runId}_charter`,
105
106
  requirement: cleanString(request.requirement),
107
+ ...normalizeProviderModelSelection(request),
106
108
  repository: cleanString(request.repository),
107
109
  backend: request.backend === "claude" ? "claude" : "codex",
108
110
  execution_target: executionTarget,
@@ -112,7 +112,7 @@ export function buildIndependentAuditInstruction(charter, attempt) {
112
112
  "criterion_results must contain one entry per stable criterion ID with criterion_id, passed, verification_method, evidence_refs, and observed_result.",
113
113
  "visual_review must contain inspected, artifacts, and acceptance_observations.",
114
114
  "Only passed=false entries are findings. Omit passing checks, and never report passed=true evidence as a finding.",
115
- "Use passed=false when a blocker or major finding remains, or when evidence is unavailable.",
115
+ "Use passed=false when a blocker or major finding remains, or when required evidence is unavailable. For minor/warning/info findings, choose disposition=fix, defer, or dismiss based on impact and reversibility. Include the reason in explanation. Deferring or dismissing a non-major finding does not require human input and must not hide it. Major/blocker findings cannot be deferred by this field.",
116
116
  ].join("\n"),
117
117
  ),
118
118
  ].join("\n\n");
@@ -165,6 +165,8 @@ export function buildIndependentAudit(execution, charter = null, attempt = null)
165
165
  target: cleanString(item.target) || "pull_request",
166
166
  explanation: cleanString(item.explanation) || "Independent audit finding.",
167
167
  repairability: item.repairability === "manual" ? "manual" : "auto",
168
+ disposition: ["defer", "dismiss"].includes(item.disposition) && cleanString(item.explanation)
169
+ ? item.disposition : "fix",
168
170
  blocker_kind: ["machine", "human", "external"].includes(item.blocker_kind)
169
171
  ? item.blocker_kind : item.repairability === "manual" ? "human" : "machine",
170
172
  auto_fix_hint: cleanString(item.auto_fix_hint) || null,
@@ -249,10 +251,16 @@ export function buildIndependentAudit(execution, charter = null, attempt = null)
249
251
  findings.push({ passed: false, code: "USER_OUTCOME_UNPROVEN", severity: "blocker",
250
252
  target: "user_outcome", explanation: "The original user goal lacks independently observed operation and postcondition evidence.", repairability: "auto" });
251
253
  }
252
- const requiresHuman = findings.some((item) => item.repairability === "manual");
254
+ // Only the independent reviewer can defer/dismiss a non-major finding.
255
+ // Unknown severities, generated evidence failures and major findings remain blocking.
256
+ const followUps = findings.filter(item => ["minor", "warning", "info"].includes(item.severity) &&
257
+ ["defer", "dismiss"].includes(item.disposition));
258
+ const blockingFindings = findings.filter(item => !followUps.includes(item));
259
+ const requiresHuman = blockingFindings.some((item) => item.repairability === "manual");
253
260
  return {
254
261
  ...objectiveBinding,
255
- passed: result.passed === true && findings.length === 0,
262
+ passed: blockingFindings.length === 0 && (result.passed === true || followUps.length > 0),
263
+ follow_ups: followUps,
256
264
  summary: cleanString(result.summary) || "Independent audit completed.",
257
265
  findings,
258
266
  criterion_results: criterionResults,
@@ -171,8 +171,8 @@ export function buildCodingInstruction(
171
171
  [
172
172
  "Treat implementation as hypothesis testing: state assumptions, list plausible causes or approaches, inspect discriminating evidence, then choose the smallest strategy supported by that evidence.",
173
173
  attemptNumber > 1
174
- ? "This is a retry. Before editing, generate at least eight distinct MECE hypotheses, select a different primary hypothesis or approach, and do not repeat the previous strategy."
175
- : "For the first attempt, identify the single highest uncertainty and run the smallest discriminating verification before editing.",
174
+ ? "This is a retry. Repair clear failures directly; compare plausible causes when needed. Reasoning notes are advisory, not a prerequisite to editing."
175
+ : "For the first attempt, investigate material uncertainty before a risky operation; use a reasonable assumption for low-impact reversible choices and verify as you proceed.",
176
176
  attemptNumber > 1
177
177
  ? "Explain exactly what changed from the previous attempt and why the new evidence supports this strategy."
178
178
  : "Record the uncertainty, first verification, concrete evidence checked, observed result, selected hypothesis, and strategy in the structured result.",
@@ -244,7 +244,7 @@ export function buildPromptInterventions(
244
244
  trigger: "repair_focus",
245
245
  title: "Repair focus",
246
246
  prompt:
247
- "Diagnose the reported gate failures with at least eight distinct hypotheses, select a new strategy supported by evidence, and do not repeat the previous approach.",
247
+ "Repair clear failures directly and verify the change. Investigate competing causes only when uncertainty warrants it; do not invent eight hypotheses or change strategy merely to satisfy a form.",
248
248
  evidence_required: [
249
249
  "hypotheses",
250
250
  "selected_hypothesis_id",
@@ -10,7 +10,7 @@ import {
10
10
  } from "./independent-audit.js";
11
11
  import { buildCodingInstruction, buildPromptInterventions } from "./instruction.js";
12
12
  import { canStartProvider, codingAttemptCeiling, refreshBudgetState } from "./budget.js";
13
- import { buildAttempt, mergeTokenUsage } from "./result.js";
13
+ import { buildAttempt, mergeTokenUsage, normalizeResultParseDiagnostic } from "./result.js";
14
14
  import { evaluateRetryCircuit } from "./retry.js";
15
15
  import { assessOutcomeProgress, verifyObjective } from "./outcome-monitor.js";
16
16
  import { appendEvent, publish } from "./shared.js";
@@ -291,6 +291,7 @@ async function runCodingLoopOwned({
291
291
  return snapshot;
292
292
  }
293
293
 
294
+ attempt.independent_audit.result_parse = normalizeResultParseDiagnostic(auditExecution.result_parse);
294
295
  attempt.independent_audit.minimum_matrix_sha256 = reviewedMatrix;
295
296
  attempt.independent_audit.evidence_identity_sha256 = reviewedEvidenceIdentity;
296
297
  attempt.independent_audit.objective_digest = reviewedObjectiveDigest;
@@ -1,6 +1,6 @@
1
1
  import { cleanString, stringArray } from "./shared.js";
2
2
 
3
- const MIN_RETRY_HYPOTHESES = 8;
3
+ const MIN_RETRY_HYPOTHESES = 1;
4
4
 
5
5
  export function normalizeProblemSolving(value) {
6
6
  const record = value && typeof value === "object" && !Array.isArray(value) ? value : {};
@@ -49,7 +49,7 @@ export function evaluateRetryProblemSolving(attempt, previousAttempt) {
49
49
  const checks = [
50
50
  {
51
51
  id: "retry_hypotheses_expanded",
52
- label: "Retry considers at least eight distinct hypotheses",
52
+ label: "Retry considers a plausible explanation",
53
53
  passed: distinctHypotheses >= MIN_RETRY_HYPOTHESES,
54
54
  detail: `Expected at least ${MIN_RETRY_HYPOTHESES} distinct hypotheses; recorded ${distinctHypotheses}.`,
55
55
  },
@@ -78,9 +78,10 @@ export function evaluateRetryProblemSolving(attempt, previousAttempt) {
78
78
  },
79
79
  ];
80
80
  return {
81
- required: true,
82
- passed: checks.every((check) => check.passed),
83
- checks,
81
+ required: false,
82
+ passed: true,
83
+ assessment_complete: checks.every((check) => check.passed),
84
+ checks: checks.map(check => ({ ...check, severity: "advisory" })),
84
85
  current,
85
86
  previous,
86
87
  };
@@ -91,7 +92,7 @@ export function problemSolvingResultContract() {
91
92
  "problem_solving must be an object with hypotheses, selected_hypothesis_id, evidence_checked, strategy, and changed_from_previous.",
92
93
  "Each hypothesis must use {id, statement}; evidence_checked must list concrete observations or commands.",
93
94
  "On attempt 1, record the assumptions, hypotheses considered, selected hypothesis, and implementation strategy; changed_from_previous may be null.",
94
- `On every retry, record at least ${MIN_RETRY_HYPOTHESES} distinct MECE hypotheses, select one of them, check discriminating evidence, use a strategy different from the previous attempt, and explain the change in changed_from_previous.`,
95
+ "On retry, use reasoning proportional to the uncertainty. If the repair is clear, make it and verify it; do not invent a fixed number of hypotheses or change a working strategy merely to satisfy a form. For persistent unexplained failures, compare plausible causes and inspect discriminating evidence. These reasoning notes are advisory; unchanged failures are still subject to the retry circuit.",
95
96
  ].join("\n");
96
97
  }
97
98
 
@@ -56,6 +56,7 @@ export function buildAttempt({ attemptNumber, charter, instruction, promptInterv
56
56
  visual_qa_result: null,
57
57
  release_gate: null,
58
58
  executor_logs: stringArray(execution.logs).slice(-100),
59
+ result_parse: normalizeResultParseDiagnostic(execution.result_parse),
59
60
  token_usage:
60
61
  execution.tokenUsage && typeof execution.tokenUsage === "object"
61
62
  ? execution.tokenUsage
@@ -79,16 +80,104 @@ export function parseStructuredResult(text) {
79
80
  }
80
81
 
81
82
  export function parseStructuredMarker(text, marker) {
82
- const index = String(text || "").lastIndexOf(marker);
83
- if (index < 0) return {};
84
- const afterMarker = String(text).slice(index + marker.length).trim();
85
- const firstLine = afterMarker.split(/\r?\n/, 1)[0].trim();
83
+ return diagnoseStructuredMarker(text, marker).value;
84
+ }
85
+
86
+ // Parse only a line-anchored result channel. Braces in JSON strings are not
87
+ // boundaries; JSON.parse remains responsible for syntax and object validation.
88
+ export function diagnoseStructuredMarker(text, marker, { maxBytes = 1024 * 1024 } = {}) {
89
+ const failure = code => ({ code, value: {} });
90
+ if (typeof text !== "string") return failure("missing_marker");
91
+ if (Buffer.byteLength(text, "utf8") > maxBytes) return failure("result_too_large");
92
+ const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
93
+ const matches = [...text.matchAll(new RegExp(`(?:^|\\n)[ \t]*${escaped}`, "g"))];
94
+ if (!matches.length) return failure("missing_marker");
95
+ if (matches.length !== 1) return failure("ambiguous_marker");
96
+ const payload = text.slice(matches[0].index + matches[0][0].length).trim();
97
+ if (!payload) return failure("truncated_json");
98
+ if (!payload.startsWith("{")) return failure("result_root_not_object");
99
+ let depth = 0, quoted = false, escapedChar = false, end = -1;
100
+ for (let index = 0; index < payload.length; index += 1) {
101
+ const char = payload[index];
102
+ if (quoted) {
103
+ if (escapedChar) escapedChar = false;
104
+ else if (char === "\\") escapedChar = true;
105
+ else if (char === '"') quoted = false;
106
+ } else if (char === '"') quoted = true;
107
+ else if (char === "{" || char === "[") depth += 1;
108
+ else if (char === "}" || char === "]") {
109
+ depth -= 1;
110
+ if (depth === 0) { end = index + 1; break; }
111
+ }
112
+ }
113
+ if (end < 0) return failure("truncated_json");
114
+ if (payload.slice(end).trim()) return failure("trailing_result_content");
86
115
  try {
87
- const value = JSON.parse(firstLine);
88
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
89
- } catch {
90
- return {};
116
+ const value = JSON.parse(payload.slice(0, end));
117
+ if (!value || Array.isArray(value) || typeof value !== "object") return failure("result_root_not_object");
118
+ if (!Object.keys(value).length) return failure("empty_result");
119
+ return { code: "parsed", value };
120
+ } catch { return failure("invalid_json"); }
121
+ }
122
+
123
+ // Native JSONL admits results only from a successfully finished provider turn.
124
+ // Command output, echoed prompts and intermediate assistant items cannot be a
125
+ // fallback channel. Explicit shell overrides retain the documented legacy text.
126
+ export function parseProviderStructuredResult(raw, { backend, marker = "SIMY_RESULT_JSON:", allowPlainText = false,
127
+ maxBytes = 1024 * 1024, maxStreamBytes = 64 * 1024 * 1024 } = {}) {
128
+ const failure = code => ({ code, value: {} });
129
+ if (typeof raw !== "string") return failure("missing_provider_final");
130
+ if (Buffer.byteLength(raw, "utf8") > maxStreamBytes) return failure("provider_stream_too_large");
131
+ const events = [];
132
+ let malformedStream = false;
133
+ for (const line of raw.split(/\r?\n/)) {
134
+ if (!line.trim()) continue;
135
+ try {
136
+ const event = JSON.parse(line);
137
+ if (event && typeof event === "object" && !Array.isArray(event) && typeof event.type === "string") {
138
+ // Tool payload size is independent of the bounded final result. Retain
139
+ // event identity/order, not arbitrary command output, during selection.
140
+ events.push(event.type === "result" || event.item?.type === "agent_message"
141
+ ? event : { type: event.type, item: { type: event.item?.type } });
142
+ } else malformedStream = true;
143
+ }
144
+ catch { malformedStream = true; }
91
145
  }
146
+ const nativeTypes = new Set(backend === "claude"
147
+ ? ["system", "assistant", "user", "result", "stream_event", "tool_progress", "tool_use_summary", "rate_limit_event", "auth_status"]
148
+ : ["thread.started", "turn.started", "turn.completed", "turn.failed", "item.started", "item.updated", "item.completed", "error"]);
149
+ const native = events.some(e => nativeTypes.has(e.type));
150
+ // A discarded line could be a later error or unfinished turn. Native streams
151
+ // must stay intact; only an explicit non-native override admits plain text.
152
+ if (malformedStream && (native || !allowPlainText)) return failure("malformed_provider_stream");
153
+ if (!native) return allowPlainText ? diagnoseStructuredMarker(raw, marker, {maxBytes}) : failure("missing_provider_final");
154
+ if (backend === "claude") {
155
+ const results = events.filter(e => e.type === "result");
156
+ if (results.length > 1) return failure("contradictory_provider_terminal");
157
+ const final = results[0];
158
+ if (!final) return failure("missing_provider_final");
159
+ if (events.slice(events.indexOf(final) + 1).some(e => typeof e.type === "string")) return failure("unsettled_provider_turn");
160
+ if (final.is_error === true || String(final.subtype || "").startsWith("error")) return failure("provider_failed");
161
+ return diagnoseStructuredMarker(final.result, marker, {maxBytes});
162
+ }
163
+ const end = events.findLastIndex(e => e.type === "turn.completed" || e.type === "turn.failed");
164
+ if (end < 0) return failure("missing_provider_final");
165
+ if (events[end].type === "turn.failed") return failure("provider_failed");
166
+ const start = events.slice(0, end).findLastIndex(e => e.type === "turn.started");
167
+ const turn = events.slice(start + 1, end);
168
+ if (turn.some(e => e.type === "turn.failed" || e.type === "turn.completed")) {
169
+ return failure("contradictory_provider_terminal");
170
+ }
171
+ const finalIndex = turn.findLastIndex(e => e.type === "item.completed" &&
172
+ e.item?.type === "agent_message" && (!e.item.phase || e.item.phase === "final_answer"));
173
+ if (finalIndex < 0) return failure("missing_provider_final");
174
+ if (turn.slice(finalIndex + 1).some(e => typeof e.type === "string")) {
175
+ return failure("nonterminal_agent_message");
176
+ }
177
+ const final = turn[finalIndex].item;
178
+ // A later unsettled turn must not replay an earlier completed result.
179
+ if (events.slice(end + 1).some(e => typeof e.type === "string")) return failure("unsettled_provider_turn");
180
+ return diagnoseStructuredMarker(final.text, marker, {maxBytes});
92
181
  }
93
182
 
94
183
  function buildArtifactPreviews(result) {
@@ -125,3 +214,10 @@ function isUsageRecord(value) {
125
214
  }
126
215
 
127
216
  function safeDigest(compute) { try { return compute(); } catch { return null; } }
217
+
218
+ export function normalizeResultParseDiagnostic(value) {
219
+ if (!value) return null;
220
+ const codes = ["parsed", "missing_marker", "result_too_large", "ambiguous_marker", "truncated_json", "result_root_not_object", "trailing_result_content", "empty_result", "invalid_json", "missing_provider_final", "provider_failed", "unsettled_provider_turn", "nonterminal_agent_message", "contradictory_provider_terminal", "provider_stream_too_large", "malformed_provider_stream"];
221
+ return { code: codes.includes(value.code) ? value.code : "unknown_parse_diagnostic",
222
+ channel: ["override_or_native", "provider_final"].includes(value.channel) ? value.channel : "unknown" };
223
+ }
@@ -0,0 +1,24 @@
1
+ const SELECTION_FIELDS = ["model", "reasoning_effort", "audit_model", "audit_reasoning_effort"];
2
+ const REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
3
+
4
+ // Preserve explicit selections at request boundaries; do not infer a model catalog.
5
+ export function normalizeProviderModelSelection(input = {}) {
6
+ const result = {};
7
+ for (const field of SELECTION_FIELDS) {
8
+ const value = input[field];
9
+ if (value == null) {
10
+ result[field] = null;
11
+ continue;
12
+ }
13
+ if (typeof value !== "string" || !value.trim()) {
14
+ throw new Error(`${field} must be a non-empty string.`);
15
+ }
16
+ const normalized = value.trim();
17
+ const valid = field.endsWith("reasoning_effort")
18
+ ? REASONING_EFFORTS.has(normalized)
19
+ : /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,159}(?:\[1m\])?$/.test(normalized);
20
+ if (!valid) throw new Error(`${field} is invalid.`);
21
+ result[field] = normalized;
22
+ }
23
+ return result;
24
+ }
package/src/runner.js CHANGED
@@ -8,11 +8,10 @@ import { promisify } from "node:util";
8
8
  import {
9
9
  collectPrEvidence,
10
10
  createCodingLoopSnapshot,
11
- parseStructuredMarker,
12
- parseStructuredResult,
13
11
  recheckPrReadiness,
14
12
  runCodingLoop,
15
13
  } from "./orchestrator/index.js";
14
+ import { parseProviderStructuredResult } from "./orchestrator/result.js";
16
15
  import {
17
16
  EXECUTION_IO_POLICY_VERSION,
18
17
  executionTextSha256,
@@ -180,6 +179,10 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null, loc
180
179
  sqmHistoryRoot,
181
180
  request: {
182
181
  backend: charter.backend === "claude" ? "claude" : "codex",
182
+ model: charter.model || null,
183
+ reasoning_effort: charter.reasoning_effort || null,
184
+ audit_model: charter.audit_model || null,
185
+ audit_reasoning_effort: charter.audit_reasoning_effort || null,
183
186
  execution_target: normalizeDesktopExecutionTarget(charter.execution_target),
184
187
  execution_device_id:
185
188
  String(snapshot.metadata?.execution_device_id || session?.device_id || "").trim() || null,
@@ -1012,18 +1015,21 @@ export async function executeProcessAttempt({
1012
1015
  marker = "SIMY_RESULT_JSON:",
1013
1016
  attemptNumber = null,
1014
1017
  resolveCommandImpl = resolveDesktopExecutorCommand,
1018
+ charter = run?.snapshot?.charter,
1015
1019
  onExecutionComplete = null,
1016
1020
  }) {
1021
+ const phase = marker === "SIMY_AUDIT_JSON:" ? "reviewer" : "executor";
1017
1022
  const command = await resolveCommandImpl({
1018
1023
  backend,
1019
1024
  instruction,
1020
1025
  repositoryPath,
1026
+ model: phase === "reviewer" ? charter?.audit_model : charter?.model,
1027
+ reasoningEffort: phase === "reviewer" ? charter?.audit_reasoning_effort : charter?.reasoning_effort,
1021
1028
  });
1022
1029
  const startedAt = new Date().toISOString();
1023
1030
  const stdout = [];
1024
1031
  const stderr = [];
1025
1032
  const tokenUsageRecords = [];
1026
- const phase = marker === "SIMY_AUDIT_JSON:" ? "reviewer" : "executor";
1027
1033
  const initialBudget = budgetState(run.snapshot);
1028
1034
  const invocationId = randomUUID();
1029
1035
  const observationSender =
@@ -1106,18 +1112,17 @@ export async function executeProcessAttempt({
1106
1112
  run.child = null;
1107
1113
  await Promise.all(checkpointWrites);
1108
1114
  const rawStdout = stdout.join("");
1109
- const assistantText = extractAssistantText(rawStdout);
1110
- const parse =
1111
- marker === "SIMY_RESULT_JSON:"
1112
- ? parseStructuredResult
1113
- : (value) => parseStructuredMarker(value, marker);
1114
- const result = parse(assistantText);
1115
+ const parsed = parseProviderStructuredResult(rawStdout, {
1116
+ backend, marker, allowPlainText: command.verification === "explicit_command_override",
1117
+ });
1118
+ const parseError = parsed.code === "parsed" ? null : `Provider result unavailable: ${parsed.code}.`;
1115
1119
  const execution = {
1116
1120
  invocationId,
1117
1121
  exitCode: terminalError && exitCode === 0 ? 1 : exitCode,
1118
- error: error || terminalError,
1122
+ error: error || terminalError || parseError,
1119
1123
  ...(terminalErrorCode ? { errorCode: terminalErrorCode } : {}),
1120
- result: Object.keys(result).length > 0 ? result : parse(rawStdout),
1124
+ result: parsed.value,
1125
+ result_parse: { code: parsed.code, channel: command.verification === "explicit_command_override" ? "override_or_native" : "provider_final" },
1121
1126
  tokenUsage:
1122
1127
  tokenUsageRecords.length > 0 ? { records: tokenUsageRecords } : {},
1123
1128
  logs: [...stdout, ...stderr]
@@ -1212,25 +1217,6 @@ async function pathExists(value) {
1212
1217
  }
1213
1218
  }
1214
1219
 
1215
- function extractAssistantText(raw) {
1216
- const messages = [];
1217
- for (const line of String(raw || "").split(/\r?\n/)) {
1218
- if (!line.trim().startsWith("{")) continue;
1219
- try {
1220
- const item = JSON.parse(line);
1221
- if (item.type === "result" && typeof item.result === "string") messages.push(item.result);
1222
- if (item.type === "item.completed" && item.item?.type === "agent_message") {
1223
- if (typeof item.item.text === "string") messages.push(item.item.text);
1224
- }
1225
- if (item.type === "assistant" && typeof item.message?.content === "string") {
1226
- messages.push(item.message.content);
1227
- }
1228
- } catch {
1229
- // Non-JSON executor output is still available to the fallback parser.
1230
- }
1231
- }
1232
- return messages.join("\n");
1233
- }
1234
1220
 
1235
1221
  function emitOutput(run, text, { backend = run.request.backend, promote = true } = {}) {
1236
1222
  for (const line of String(text).split(/\r?\n/)) {