@awak-app/simy-cli 0.1.3 → 0.1.5

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.
@@ -0,0 +1,112 @@
1
+ import { clampAttempts } from "./shared.js";
2
+
3
+ export const DEFAULT_TOKEN_BUDGET = 250_000;
4
+ export const DEFAULT_RETRY_BUDGET = 2;
5
+ const MAX_TOKEN_BUDGET = 10_000_000;
6
+
7
+ export function resolveRunBudgets(request = {}) {
8
+ const legacyMaxAttempts = clampAttempts(request.max_attempts);
9
+ const explicitRetryBudget = integer(request.retry_budget);
10
+ const retryBudget =
11
+ explicitRetryBudget === null
12
+ ? legacyMaxAttempts - 1
13
+ : Math.min(4, Math.max(0, explicitRetryBudget));
14
+ return {
15
+ token_budget: normalizeTokenBudget(request.token_budget),
16
+ retry_budget: retryBudget,
17
+ max_attempts: retryBudget + 1,
18
+ };
19
+ }
20
+
21
+ export function budgetState(snapshot, pendingAttempt = null) {
22
+ const attempts = [
23
+ ...(Array.isArray(snapshot?.attempts) ? snapshot.attempts : []),
24
+ ...(pendingAttempt ? [pendingAttempt] : []),
25
+ ];
26
+ const tokenBudget = normalizeTokenBudget(snapshot?.charter?.token_budget);
27
+ const retryBudget = normalizeRetryBudget(
28
+ snapshot?.charter?.retry_budget,
29
+ snapshot?.charter?.max_attempts,
30
+ );
31
+ const tokensUsed = attempts.reduce(
32
+ (sum, attempt) => sum + tokenUsageTotal(attempt?.token_usage),
33
+ 0,
34
+ );
35
+ const attemptsUsed = attempts.length;
36
+ const retriesUsed = Math.max(attemptsUsed - 1, 0);
37
+ const tokenExhausted = tokensUsed >= tokenBudget;
38
+ const retryExhausted = attemptsUsed > 0 && retriesUsed >= retryBudget;
39
+ return {
40
+ schema_version: 1,
41
+ token_budget: tokenBudget,
42
+ tokens_used: tokensUsed,
43
+ tokens_remaining: Math.max(tokenBudget - tokensUsed, 0),
44
+ token_exhausted: tokenExhausted,
45
+ retry_budget: retryBudget,
46
+ retries_used: retriesUsed,
47
+ retries_remaining: Math.max(retryBudget - retriesUsed, 0),
48
+ retry_exhausted: retryExhausted,
49
+ exhausted: tokenExhausted || retryExhausted,
50
+ exhausted_reason: tokenExhausted
51
+ ? "token_budget_exhausted"
52
+ : retryExhausted
53
+ ? "retry_budget_exhausted"
54
+ : null,
55
+ };
56
+ }
57
+
58
+ export function refreshBudgetState(snapshot, pendingAttempt = null) {
59
+ const state = budgetState(snapshot, pendingAttempt);
60
+ snapshot.budget = state;
61
+ return state;
62
+ }
63
+
64
+ export function canStartProvider(snapshot, { attemptNumber, phase }) {
65
+ const state = refreshBudgetState(snapshot);
66
+ if (state.token_exhausted) {
67
+ return { allowed: false, reason: "token_budget_exhausted", budget: state };
68
+ }
69
+ if (phase === "executor" && attemptNumber > state.retry_budget + 1) {
70
+ return { allowed: false, reason: "retry_budget_exhausted", budget: state };
71
+ }
72
+ return { allowed: true, reason: null, budget: state };
73
+ }
74
+
75
+ export function tokenUsageTotal(value) {
76
+ if (!value || typeof value !== "object" || Array.isArray(value)) return 0;
77
+ if (Array.isArray(value.records)) {
78
+ return value.records.reduce((sum, record) => sum + recordTokenTotal(record), 0);
79
+ }
80
+ return recordTokenTotal(value);
81
+ }
82
+
83
+ function recordTokenTotal(value) {
84
+ if (!value || typeof value !== "object" || Array.isArray(value)) return 0;
85
+ const explicit = nonNegativeNumber(value.total_tokens);
86
+ if (explicit !== null) return explicit;
87
+ return (
88
+ (nonNegativeNumber(value.tokens_in ?? value.input_tokens) ?? 0) +
89
+ (nonNegativeNumber(value.tokens_out ?? value.output_tokens) ?? 0)
90
+ );
91
+ }
92
+
93
+ function normalizeTokenBudget(value) {
94
+ const parsed = integer(value);
95
+ if (parsed === null || parsed <= 0) return DEFAULT_TOKEN_BUDGET;
96
+ return Math.min(MAX_TOKEN_BUDGET, parsed);
97
+ }
98
+
99
+ function normalizeRetryBudget(value, maxAttempts) {
100
+ const parsed = integer(value);
101
+ if (parsed !== null) return Math.min(4, Math.max(0, parsed));
102
+ return clampAttempts(maxAttempts) - 1;
103
+ }
104
+
105
+ function integer(value) {
106
+ const parsed = Number.parseInt(String(value ?? ""), 10);
107
+ return Number.isFinite(parsed) ? parsed : null;
108
+ }
109
+
110
+ function nonNegativeNumber(value) {
111
+ return Number.isFinite(value) && value >= 0 ? Math.round(value) : null;
112
+ }
@@ -1,45 +1,105 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
 
3
3
  import {
4
- clampAttempts,
5
4
  cleanString,
6
5
  event,
7
6
  looksLikeUiTask,
8
7
  stringArray,
8
+ visualEvidenceSurface,
9
9
  } from "./shared.js";
10
+ import { budgetState, resolveRunBudgets } from "./budget.js";
10
11
  import { classifyRisk } from "./risk.js";
12
+ import {
13
+ DESKTOP_EXECUTION_TARGET,
14
+ normalizeDesktopExecutionTarget,
15
+ } from "../desktop-executor.js";
11
16
 
12
17
  export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
13
18
  const now = new Date().toISOString();
14
19
  const baseBranch = cleanString(request.base_branch) || "dev";
15
20
  const expectedEvidence = stringArray(request.expected_evidence);
16
- if (looksLikeUiTask(request.requirement) && !expectedEvidence.includes("ui_evidence_path")) {
17
- expectedEvidence.push("ui_evidence_path");
18
- }
19
21
  const acceptanceCriteria = stringArray(request.acceptance_criteria);
20
22
  const mustNot = stringArray(request.must_not);
21
- const risk = classifyRisk(request);
22
- const designSummary = cleanString(request.design_summary);
23
- const designApprovedBy = cleanString(request.design_review_approved_by);
24
- const designEvidenceUrl = cleanString(request.design_review_url);
23
+ const charterContext = record(request.charter_context);
24
+ const risk = inheritRisk(classifyRisk(request), record(charterContext.risk));
25
+ const inheritedDesignReview = record(charterContext.design_review);
26
+ const inheritedPromptPolicyReport = record(charterContext.prompt_policy_report);
27
+ const inheritedEvidencePolicy = record(charterContext.evidence_policy);
28
+ const classifiedVisualSurface = visualEvidenceSurface(request.requirement);
29
+ const inheritedVisualSurface = ["web_ui", "browser_extension"].includes(
30
+ inheritedEvidencePolicy.surface,
31
+ )
32
+ ? inheritedEvidencePolicy.surface
33
+ : null;
34
+ const visualSurface = classifiedVisualSurface || inheritedVisualSurface;
35
+ const browserEvidenceRequired =
36
+ inheritedEvidencePolicy.browser_required === true || visualSurface !== null;
37
+ if (browserEvidenceRequired && !expectedEvidence.includes("ui_evidence_path")) {
38
+ expectedEvidence.push("ui_evidence_path");
39
+ }
40
+ if (browserEvidenceRequired && !expectedEvidence.includes("browser_screenshot_or_video")) {
41
+ expectedEvidence.push("browser_screenshot_or_video");
42
+ }
43
+ const designSummary = cleanString(
44
+ inheritedDesignReview.summary || request.design_summary,
45
+ );
46
+ const designApprovedBy = cleanString(
47
+ inheritedDesignReview.approved_by || request.design_review_approved_by,
48
+ );
49
+ const designEvidenceUrl = cleanString(
50
+ inheritedDesignReview.evidence_url || request.design_review_url,
51
+ );
52
+ const acceptanceCriteriaSource =
53
+ acceptanceCriteria.length > 0 &&
54
+ cleanString(charterContext.acceptance_criteria_source) === "explicit"
55
+ ? "explicit"
56
+ : acceptanceCriteria.length > 0 &&
57
+ cleanString(charterContext.acceptance_criteria_source) === "default"
58
+ ? "default"
59
+ : acceptanceCriteria.length > 0
60
+ ? "explicit"
61
+ : "default";
62
+ const inheritedRequiredChecks = stringArray(charterContext.required_checks);
63
+ const inheritedNonGoals = stringArray(charterContext.non_goals);
64
+ const inheritedArtifacts = stringArray(charterContext.artifacts_required);
65
+ const inheritedAssumptions = stringArray(charterContext.assumptions);
66
+ const designReviewRequired =
67
+ risk.requires_design_review || inheritedDesignReview.required === true;
68
+ const designReviewApproved =
69
+ inheritedDesignReview.status === "approved" &&
70
+ Boolean(designSummary && designApprovedBy && designEvidenceUrl);
25
71
 
26
- return {
72
+ const budgets = resolveRunBudgets(request);
73
+ const executionTarget = normalizeDesktopExecutionTarget(request.execution_target);
74
+ const executionDeviceId = cleanString(request.execution_device_id) || null;
75
+
76
+ const snapshot = {
27
77
  id: runId,
28
78
  state: "queued",
29
- metadata: { ...metadata, execution_location: "local_cli" },
79
+ metadata: {
80
+ ...metadata,
81
+ execution_location: "local_cli",
82
+ execution_target: executionTarget,
83
+ execution_device_id: executionDeviceId,
84
+ },
30
85
  charter: {
31
86
  id: `${runId}_charter`,
32
87
  requirement: cleanString(request.requirement),
33
88
  repository: cleanString(request.repository),
34
89
  backend: request.backend === "claude" ? "claude" : "codex",
90
+ execution_target: executionTarget,
35
91
  audit_backend:
36
- request.audit_backend === "claude" || request.audit_backend === "codex"
92
+ charterContext.audit_backend === "claude" || charterContext.audit_backend === "codex"
93
+ ? charterContext.audit_backend
94
+ : request.audit_backend === "claude" || request.audit_backend === "codex"
37
95
  ? request.audit_backend
38
96
  : request.backend === "claude"
39
97
  ? "claude"
40
98
  : "codex",
41
99
  base_branch: baseBranch,
42
- max_attempts: clampAttempts(request.max_attempts),
100
+ max_attempts: budgets.max_attempts,
101
+ retry_budget: budgets.retry_budget,
102
+ token_budget: budgets.token_budget,
43
103
  ui_evidence_root: cleanString(request.ui_evidence_root),
44
104
  acceptance_criteria:
45
105
  acceptanceCriteria.length > 0
@@ -49,12 +109,18 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
49
109
  "Relevant tests pass and are recorded.",
50
110
  `A pull request targets ${baseBranch}.`,
51
111
  ],
52
- acceptance_criteria_source: acceptanceCriteria.length > 0 ? "explicit" : "default",
112
+ acceptance_criteria_source: acceptanceCriteriaSource,
53
113
  expected_tests: stringArray(request.expected_tests),
54
114
  expected_evidence: expectedEvidence,
55
- required_checks: stringArray(request.required_checks),
115
+ required_checks:
116
+ inheritedRequiredChecks.length > 0
117
+ ? inheritedRequiredChecks
118
+ : stringArray(request.required_checks),
56
119
  require_human_approval:
57
- risk.requires_design_review || request.require_human_approval !== false,
120
+ risk.requires_design_review ||
121
+ charterContext.require_human_approval === true ||
122
+ (charterContext.require_human_approval !== false &&
123
+ request.require_human_approval !== false),
58
124
  must_not:
59
125
  mustNot.length > 0
60
126
  ? mustNot
@@ -67,32 +133,56 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
67
133
  attachments: Array.isArray(request.attachments) ? request.attachments : [],
68
134
  risk,
69
135
  design_review: {
70
- required: risk.requires_design_review,
136
+ required: designReviewRequired,
71
137
  summary: designSummary || null,
72
138
  approved_by: designApprovedBy || null,
73
139
  evidence_url: designEvidenceUrl || null,
74
- status: risk.requires_design_review
75
- ? designSummary && designApprovedBy && designEvidenceUrl
140
+ status: designReviewRequired
141
+ ? designReviewApproved || (designSummary && designApprovedBy && designEvidenceUrl)
76
142
  ? "approved"
77
143
  : "missing"
78
144
  : "not_required",
79
145
  },
146
+ evidence_policy: {
147
+ browser_required: browserEvidenceRequired,
148
+ surface: visualSurface,
149
+ accepted_artifact_kinds: browserEvidenceRequired ? ["image", "video"] : [],
150
+ visual_review_required: browserEvidenceRequired,
151
+ },
80
152
  thread_state: {
81
- original_request: cleanString(request.requirement),
82
- user_goal: cleanString(request.requirement),
83
- non_goals: ["Unrelated refactors", "Server-side execution of repository code"],
84
- expected_finish_line: `A verified pull request targeting ${baseBranch}.`,
85
- artifacts_required: ["pr_url", "commit_sha", "tests_run", ...expectedEvidence],
153
+ original_request:
154
+ cleanString(charterContext.original_request) || cleanString(request.requirement),
155
+ user_goal: cleanString(charterContext.user_goal) || cleanString(request.requirement),
156
+ non_goals:
157
+ inheritedNonGoals.length > 0
158
+ ? inheritedNonGoals
159
+ : ["Unrelated refactors", "Server-side execution of repository code"],
160
+ expected_finish_line:
161
+ cleanString(charterContext.expected_finish_line) ||
162
+ `A verified pull request targeting ${baseBranch}.`,
163
+ artifacts_required:
164
+ inheritedArtifacts.length > 0
165
+ ? inheritedArtifacts
166
+ : ["pr_url", "commit_sha", "tests_run", ...expectedEvidence],
86
167
  failure_count: 0,
87
- assumptions: [],
168
+ retry_circuit_breaker: {
169
+ state: "closed",
170
+ reason: null,
171
+ opened_at: null,
172
+ },
173
+ assumptions: inheritedAssumptions,
88
174
  },
89
- prompt_policy_report: buildPromptPolicyReport(request, risk),
175
+ prompt_policy_report: mergePromptPolicyReport(
176
+ buildPromptPolicyReport(request, risk),
177
+ inheritedPromptPolicyReport,
178
+ ),
90
179
  created_at: now,
91
180
  },
92
181
  attempts: [],
93
182
  events: [
94
- event("queued", "Coding loop queued for local orchestration.", {
183
+ event("queued", "Agentic loop queued for local orchestration.", {
95
184
  execution_location: "local_cli",
185
+ execution_target: DESKTOP_EXECUTION_TARGET,
96
186
  }),
97
187
  ],
98
188
  final_audit: null,
@@ -101,10 +191,68 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
101
191
  created_at: now,
102
192
  updated_at: now,
103
193
  };
194
+ snapshot.budget = budgetState(snapshot);
195
+ return snapshot;
196
+ }
197
+
198
+ function inheritRisk(classifiedRisk, inheritedRisk) {
199
+ const weight = { low: 0, medium: 1, high: 2, critical: 3 };
200
+ const inheritedLevel = Object.hasOwn(weight, inheritedRisk.level)
201
+ ? inheritedRisk.level
202
+ : "low";
203
+ const level =
204
+ weight[inheritedLevel] > weight[classifiedRisk.level]
205
+ ? inheritedLevel
206
+ : classifiedRisk.level;
207
+ const tags = [...new Set([...classifiedRisk.tags, ...stringArray(inheritedRisk.tags)])];
208
+ const requiredControls = [
209
+ ...new Set([
210
+ ...classifiedRisk.required_controls,
211
+ ...stringArray(inheritedRisk.required_controls),
212
+ ]),
213
+ ];
214
+ const requiresDesignReview =
215
+ classifiedRisk.requires_design_review ||
216
+ inheritedRisk.requires_design_review === true ||
217
+ level === "high" ||
218
+ level === "critical";
219
+ if (requiresDesignReview && !requiredControls.includes("design_review")) {
220
+ requiredControls.push("design_review");
221
+ }
222
+ return {
223
+ level,
224
+ tags,
225
+ requires_design_review: requiresDesignReview,
226
+ required_controls: requiredControls,
227
+ };
228
+ }
229
+
230
+ function record(value) {
231
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
232
+ }
233
+
234
+ function mergePromptPolicyReport(current, inherited) {
235
+ const registryId = cleanString(inherited.registry_id);
236
+ if (!registryId) return current;
237
+ return {
238
+ registry_id: registryId,
239
+ matched_policy_ids: uniqueStrings(
240
+ inherited.matched_policy_ids,
241
+ current.matched_policy_ids,
242
+ ),
243
+ matched_triggers: uniqueStrings(inherited.matched_triggers, current.matched_triggers),
244
+ risk_level: current.risk_level,
245
+ risk_tags: uniqueStrings(inherited.risk_tags, current.risk_tags),
246
+ reasons: uniqueStrings(inherited.reasons, current.reasons),
247
+ };
248
+ }
249
+
250
+ function uniqueStrings(...values) {
251
+ return [...new Set(values.flatMap((value) => stringArray(value)))];
104
252
  }
105
253
 
106
254
  export function newRunId() {
107
- return `coding_loop_${randomUUID()}`;
255
+ return `agentic_loop_${randomUUID()}`;
108
256
  }
109
257
 
110
258
  function buildPromptPolicyReport(request, risk) {
@@ -56,7 +56,8 @@ export function buildIndependentAuditInstruction(charter, attempt) {
56
56
  [
57
57
  "Finish with exactly one line beginning SIMY_AUDIT_JSON: followed by one JSON object.",
58
58
  "Required keys: passed, summary, findings.",
59
- "Each finding must contain code, severity, target, explanation, and repairability.",
59
+ "Each finding candidate must contain passed, code, severity, target, explanation, and repairability.",
60
+ "Only passed=false entries are findings. Omit passing checks, and never report passed=true evidence as a finding.",
60
61
  "Use passed=false when a blocker or major finding remains, or when evidence is unavailable.",
61
62
  ].join("\n"),
62
63
  ),
@@ -76,6 +77,7 @@ export function buildIndependentAudit(execution) {
76
77
  summary: cleanString(execution?.error || result.summary) || "Independent audit did not return valid evidence.",
77
78
  findings: [
78
79
  {
80
+ passed: false,
79
81
  code: "INDEPENDENT_AUDIT_UNAVAILABLE",
80
82
  severity: "blocker",
81
83
  target: "independent_audit",
@@ -87,9 +89,10 @@ export function buildIndependentAudit(execution) {
87
89
  };
88
90
  }
89
91
 
90
- const findings = result.findings
92
+ const evaluations = result.findings
91
93
  .filter((item) => item && typeof item === "object")
92
94
  .map((item, index) => ({
95
+ passed: item.passed === true ? true : item.passed === false ? false : null,
93
96
  code: cleanString(item.code) || `INDEPENDENT_AUDIT_${index + 1}`,
94
97
  severity: cleanString(item.severity).toLowerCase() || "major",
95
98
  target: cleanString(item.target) || "pull_request",
@@ -97,10 +100,21 @@ export function buildIndependentAudit(execution) {
97
100
  repairability: item.repairability === "manual" ? "manual" : "auto",
98
101
  auto_fix_hint: cleanString(item.auto_fix_hint) || null,
99
102
  }));
100
- const blocking = findings.some((item) => ["blocker", "major"].includes(item.severity));
103
+ const findings = evaluations.filter((item) => item.passed === false);
104
+ if (result.passed === false && findings.length === 0) {
105
+ findings.push({
106
+ passed: false,
107
+ code: "INDEPENDENT_AUDIT_FAILED_WITHOUT_FINDING",
108
+ severity: "blocker",
109
+ target: "independent_audit",
110
+ explanation: "The independent auditor returned passed=false without an explicit failed finding.",
111
+ repairability: "manual",
112
+ auto_fix_hint: null,
113
+ });
114
+ }
101
115
  const requiresHuman = findings.some((item) => item.repairability === "manual");
102
116
  return {
103
- passed: result.passed === true && !blocking && !requiresHuman,
117
+ passed: result.passed === true && findings.length === 0,
104
118
  summary: cleanString(result.summary) || "Independent audit completed.",
105
119
  findings,
106
120
  requires_human: requiresHuman,
@@ -3,3 +3,4 @@ export { collectPrEvidence } from "./evidence.js";
3
3
  export { buildIndependentAuditInstruction } from "./independent-audit.js";
4
4
  export { recheckPrReadiness, runCodingLoop } from "./loop.js";
5
5
  export { mergeTokenUsage, parseStructuredMarker, parseStructuredResult } from "./result.js";
6
+ export { buildRetryFingerprint, evaluateRetryCircuit } from "./retry.js";
@@ -1,21 +1,33 @@
1
1
  import { bullets, looksLikeUiTask, section } from "./shared.js";
2
+ import { problemSolvingResultContract } from "./problem-solving.js";
2
3
 
3
4
  export function buildCodingInstruction(
4
5
  charter,
5
- { attemptNumber, previousAttempt = null, previousFindings = [], promptInterventions = [] },
6
+ {
7
+ attemptNumber,
8
+ budget = null,
9
+ previousAttempt = null,
10
+ previousFindings = [],
11
+ promptInterventions = [],
12
+ },
6
13
  ) {
7
14
  const sections = [
8
- section("role", "You are the local coding executor for a SIMY coding loop."),
15
+ section("role", "You are the local coding executor for a SIMY agentic loop."),
9
16
  section(
10
17
  "requirement_charter",
11
18
  [
12
19
  `Requirement: ${charter.requirement}`,
20
+ `Original request: ${charter.thread_state.original_request}`,
21
+ `User goal: ${charter.thread_state.user_goal}`,
13
22
  `Repository: ${charter.repository}`,
23
+ `Execution target: Your Desktop (${charter.execution_target || "desktop"})`,
14
24
  `Required PR base branch: ${charter.base_branch}`,
15
25
  `Risk level: ${charter.risk.level}`,
16
26
  `Design summary: ${charter.design_review.summary || "not required"}`,
17
27
  `Design approved by: ${charter.design_review.approved_by || "not required"}`,
18
28
  `Attempt: ${attemptNumber}/${charter.max_attempts}`,
29
+ `Retry budget: ${Math.max(attemptNumber - 1, 0)}/${charter.retry_budget}`,
30
+ `Token budget remaining: ${budget?.tokens_remaining ?? charter.token_budget}/${charter.token_budget}`,
19
31
  `Local evidence root: ${charter.ui_evidence_root || "not required"}`,
20
32
  ].join("\n"),
21
33
  ),
@@ -46,6 +58,9 @@ export function buildCodingInstruction(
46
58
  [
47
59
  previousAttempt ? `Previous attempt summary: ${previousAttempt.summary || "missing"}` : "",
48
60
  previousAttempt ? `Previous PR: ${previousAttempt.pr_url || "missing"}` : "",
61
+ previousAttempt
62
+ ? `Previous strategy: ${previousAttempt.problem_solving?.strategy || "not recorded"}`
63
+ : "",
49
64
  "Findings to repair:",
50
65
  bullets(
51
66
  previousFindings.map(
@@ -70,6 +85,18 @@ export function buildCodingInstruction(
70
85
  }
71
86
 
72
87
  sections.push(
88
+ section(
89
+ "problem_solving_protocol",
90
+ [
91
+ "Treat implementation as hypothesis testing: state assumptions, list plausible causes or approaches, inspect discriminating evidence, then choose the smallest strategy supported by that evidence.",
92
+ attemptNumber > 1
93
+ ? "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."
94
+ : "For the first attempt, make uncertainty and the selected implementation hypothesis explicit before editing.",
95
+ attemptNumber > 1
96
+ ? "Explain exactly what changed from the previous attempt and why the new evidence supports this strategy."
97
+ : "Record the selected hypothesis, evidence checked, and strategy in the structured result.",
98
+ ].join("\n"),
99
+ ),
73
100
  section(
74
101
  "execution_contract",
75
102
  [
@@ -84,7 +111,8 @@ export function buildCodingInstruction(
84
111
  "result_contract",
85
112
  [
86
113
  "Finish with exactly one line beginning SIMY_RESULT_JSON: followed by one JSON object.",
87
- "Required keys: outcome_kind, summary, branch_name, commit_sha, commit_message_headline, pr_url, pr_number, pr_title, pr_base_branch, tests_run, tests_passed, ui_evidence_path, unrelated_changes_detected, secret_scan_passed, changed_files, residual_risks.",
114
+ "Required keys: outcome_kind, summary, branch_name, commit_sha, commit_message_headline, pr_url, pr_number, pr_title, pr_base_branch, tests_run, tests_passed, ui_evidence_path, unrelated_changes_detected, secret_scan_passed, changed_files, residual_risks, problem_solving.",
115
+ problemSolvingResultContract(),
88
116
  "Use null for unavailable scalar values and [] for unavailable arrays. Do not claim evidence that was not observed.",
89
117
  ].join("\n"),
90
118
  ),
@@ -129,8 +157,16 @@ export function buildPromptInterventions(
129
157
  rows.push({
130
158
  trigger: "repair_focus",
131
159
  title: "Repair focus",
132
- prompt: "Fix only the reported gate failures, then rerun the affected verification.",
133
- evidence_required: ["fixed_finding_codes", "rerun_report"],
160
+ prompt:
161
+ "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.",
162
+ evidence_required: [
163
+ "hypotheses",
164
+ "selected_hypothesis_id",
165
+ "evidence_checked",
166
+ "changed_from_previous",
167
+ "fixed_finding_codes",
168
+ "rerun_report",
169
+ ],
134
170
  });
135
171
  }
136
172
  if (humanGuidance.trim()) {