@awak-app/simy-cli 0.1.2 → 0.1.4
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 +39 -5
- package/package.json +14 -4
- package/src/agent.js +209 -24
- package/src/backend-executable.js +92 -6
- package/src/console/app.js +9 -7
- package/src/console/index.js +5 -4
- package/src/desktop-executor.js +137 -0
- package/src/index.js +3 -3
- package/src/local-attachments.js +2 -2
- package/src/orchestrator/audit.js +101 -7
- package/src/orchestrator/budget.js +112 -0
- package/src/orchestrator/contract.js +175 -27
- package/src/orchestrator/independent-audit.js +18 -4
- package/src/orchestrator/index.js +1 -0
- package/src/orchestrator/instruction.js +41 -5
- package/src/orchestrator/loop.js +185 -7
- package/src/orchestrator/presentation.js +2 -2
- package/src/orchestrator/problem-solving.js +129 -0
- package/src/orchestrator/recovery.js +271 -0
- package/src/orchestrator/result.js +2 -0
- package/src/orchestrator/retry.js +140 -0
- package/src/orchestrator/shared.js +87 -5
- package/src/provider-stream.js +17 -0
- package/src/repository-inventory.js +117 -0
- package/src/runner.js +128 -108
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
const RECOVERABLE_STATES = new Set(["waiting_human", "blocked", "failed"]);
|
|
2
|
+
|
|
3
|
+
export function recoveryContractForEvent({ snapshot, state, message, detail = {} }) {
|
|
4
|
+
if (!RECOVERABLE_STATES.has(state)) return null;
|
|
5
|
+
if (isRecoveryContract(detail.recovery)) return detail.recovery;
|
|
6
|
+
|
|
7
|
+
const reasonCode = recoveryReasonCode(state, message, detail);
|
|
8
|
+
const attemptNumber = positiveInteger(detail.attempt_number);
|
|
9
|
+
const actions = recoveryActions(reasonCode);
|
|
10
|
+
return {
|
|
11
|
+
schema_version: 1,
|
|
12
|
+
state,
|
|
13
|
+
reason_code: reasonCode,
|
|
14
|
+
user_message: recoveryMessage(reasonCode),
|
|
15
|
+
primary_action_id: actions[0].id,
|
|
16
|
+
actions,
|
|
17
|
+
technical_details: {
|
|
18
|
+
event_code: clean(detail.code) || null,
|
|
19
|
+
attempt_number: attemptNumber,
|
|
20
|
+
max_attempts: positiveInteger(snapshot?.charter?.max_attempts),
|
|
21
|
+
token_budget: positiveInteger(snapshot?.charter?.token_budget),
|
|
22
|
+
retry_budget: nonNegativeInteger(snapshot?.charter?.retry_budget),
|
|
23
|
+
tokens_used: nonNegativeInteger(detail?.budget?.tokens_used),
|
|
24
|
+
retries_used: nonNegativeInteger(detail?.budget?.retries_used),
|
|
25
|
+
finding_codes: strings(detail.finding_codes),
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function recoveryContractForPreflight({ key, summary, details = {} }) {
|
|
31
|
+
const updateCommand = clean(details.update_command);
|
|
32
|
+
if (key === "backend") {
|
|
33
|
+
const reasonCode =
|
|
34
|
+
details.status === "outdated"
|
|
35
|
+
? "executor_outdated"
|
|
36
|
+
: details.status === "unknown_version"
|
|
37
|
+
? "executor_version_unknown"
|
|
38
|
+
: "executor_missing";
|
|
39
|
+
const actions = [
|
|
40
|
+
{
|
|
41
|
+
id: "update_executor",
|
|
42
|
+
type: "run_command",
|
|
43
|
+
label: "Update the executor",
|
|
44
|
+
description: "Run this command in Terminal, then check again.",
|
|
45
|
+
command: updateCommand || null,
|
|
46
|
+
payload: {},
|
|
47
|
+
},
|
|
48
|
+
checkAgainAction(),
|
|
49
|
+
];
|
|
50
|
+
return preflightContract(reasonCode, summary, actions, {
|
|
51
|
+
installed_version: clean(details.installed_version) || null,
|
|
52
|
+
minimum_version: clean(details.minimum_version) || null,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (key === "repository") {
|
|
56
|
+
const actions = [
|
|
57
|
+
{
|
|
58
|
+
id: "choose_repository",
|
|
59
|
+
type: "select_repository",
|
|
60
|
+
label: "Choose the local repository",
|
|
61
|
+
description: "Select the checkout that should receive this task, then check again.",
|
|
62
|
+
command: null,
|
|
63
|
+
payload: {},
|
|
64
|
+
},
|
|
65
|
+
checkAgainAction(),
|
|
66
|
+
];
|
|
67
|
+
return preflightContract("repository_unavailable", summary, actions);
|
|
68
|
+
}
|
|
69
|
+
const actions = [
|
|
70
|
+
{
|
|
71
|
+
id: "reconnect_cli",
|
|
72
|
+
type: "reconnect_cli",
|
|
73
|
+
label: "Reconnect SIMY CLI",
|
|
74
|
+
description: "Open SIMY CLI and sign in again, then check the environment.",
|
|
75
|
+
command: "simy",
|
|
76
|
+
payload: {},
|
|
77
|
+
},
|
|
78
|
+
checkAgainAction(),
|
|
79
|
+
];
|
|
80
|
+
return preflightContract("cli_session_unavailable", summary, actions);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function isRecoveryContract(value) {
|
|
84
|
+
return Boolean(
|
|
85
|
+
value &&
|
|
86
|
+
typeof value === "object" &&
|
|
87
|
+
!Array.isArray(value) &&
|
|
88
|
+
value.schema_version === 1 &&
|
|
89
|
+
typeof value.reason_code === "string" &&
|
|
90
|
+
typeof value.user_message === "string" &&
|
|
91
|
+
typeof value.primary_action_id === "string" &&
|
|
92
|
+
Array.isArray(value.actions) &&
|
|
93
|
+
value.actions.length > 0,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function recoveryReasonCode(state, message, detail) {
|
|
98
|
+
const code = clean(detail.code);
|
|
99
|
+
if (code === "local_repository_not_authorized") return "repository_authorization_required";
|
|
100
|
+
if (code === "local_execution_interrupted") return "local_execution_interrupted";
|
|
101
|
+
if (code === "retry_circuit_open" || detail.reason === "identical_commit_and_findings") {
|
|
102
|
+
return "retry_strategy_unchanged";
|
|
103
|
+
}
|
|
104
|
+
if (code === "token_budget_exhausted") return "token_budget_exhausted";
|
|
105
|
+
if (code === "retry_budget_exhausted") return "retry_budget_exhausted";
|
|
106
|
+
const findings = strings(detail.finding_codes);
|
|
107
|
+
if (
|
|
108
|
+
findings.some((finding) =>
|
|
109
|
+
[
|
|
110
|
+
"EXPLICIT_ACCEPTANCE_CRITERIA_REQUIRED",
|
|
111
|
+
"DESIGN_REVIEW_REQUIRED",
|
|
112
|
+
"REQUIRED_CI_CHECKS_MISSING",
|
|
113
|
+
].includes(finding),
|
|
114
|
+
)
|
|
115
|
+
) {
|
|
116
|
+
return "requirements_incomplete";
|
|
117
|
+
}
|
|
118
|
+
if (state === "waiting_human") return "human_input_required";
|
|
119
|
+
if (state === "blocked" && /(?:attempt|retry) budget/i.test(clean(message))) {
|
|
120
|
+
return "retry_budget_exhausted";
|
|
121
|
+
}
|
|
122
|
+
if (state === "blocked") return "automatic_progress_blocked";
|
|
123
|
+
if (/attachment cleanup/i.test(clean(message))) return "attachment_cleanup_failed";
|
|
124
|
+
return "local_orchestration_failed";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function recoveryMessage(reasonCode) {
|
|
128
|
+
const messages = {
|
|
129
|
+
repository_authorization_required:
|
|
130
|
+
"SIMY needs permission to find the selected repository before it can continue.",
|
|
131
|
+
local_execution_interrupted:
|
|
132
|
+
"The local executor stopped during a previous CLI session and is ready for your direction.",
|
|
133
|
+
retry_strategy_unchanged:
|
|
134
|
+
"SIMY stopped repeating the same unsuccessful result. Tell it what to try differently.",
|
|
135
|
+
requirements_incomplete:
|
|
136
|
+
"SIMY needs the missing requirement or approval before implementation can start.",
|
|
137
|
+
human_input_required: "SIMY needs your decision or clarification before it can continue.",
|
|
138
|
+
token_budget_exhausted:
|
|
139
|
+
"This run used its token budget. Start a new run if you want SIMY to continue with a fresh limit.",
|
|
140
|
+
retry_budget_exhausted:
|
|
141
|
+
"This run used its automatic retry budget. Start a new run to continue with a fresh limit.",
|
|
142
|
+
automatic_progress_blocked:
|
|
143
|
+
"SIMY cannot continue automatically. Review the issue and tell it how to proceed.",
|
|
144
|
+
attachment_cleanup_failed:
|
|
145
|
+
"SIMY could not finish cleaning up a local attachment. Review the details before retrying.",
|
|
146
|
+
local_orchestration_failed:
|
|
147
|
+
"The local executor stopped unexpectedly. Give SIMY guidance to retry or start a new run.",
|
|
148
|
+
};
|
|
149
|
+
return messages[reasonCode] || messages.automatic_progress_blocked;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function recoveryActions(reasonCode) {
|
|
153
|
+
if (reasonCode === "repository_authorization_required") {
|
|
154
|
+
return [
|
|
155
|
+
{
|
|
156
|
+
id: "approve_repository_scan",
|
|
157
|
+
type: "resolve_hil",
|
|
158
|
+
label: "Allow repository scan",
|
|
159
|
+
description: "Review the folders SIMY will scan, then allow it to find the checkout.",
|
|
160
|
+
command: null,
|
|
161
|
+
payload: { request_kind: "local_repository_scan" },
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
if (reasonCode === "requirements_incomplete") {
|
|
166
|
+
return [
|
|
167
|
+
guidanceAction(
|
|
168
|
+
"provide_missing_requirements",
|
|
169
|
+
"Add the missing information",
|
|
170
|
+
"Send the required acceptance criteria, review approval, or check names to SIMY.",
|
|
171
|
+
),
|
|
172
|
+
newRunAction(),
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
if (reasonCode === "retry_strategy_unchanged") {
|
|
176
|
+
return [
|
|
177
|
+
guidanceAction(
|
|
178
|
+
"change_strategy",
|
|
179
|
+
"Tell SIMY what to try differently",
|
|
180
|
+
"Describe a new hypothesis, implementation approach, or evidence source before continuing.",
|
|
181
|
+
),
|
|
182
|
+
newRunAction(),
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
if (reasonCode === "token_budget_exhausted" || reasonCode === "retry_budget_exhausted") {
|
|
186
|
+
return [newRunAction()];
|
|
187
|
+
}
|
|
188
|
+
if (reasonCode === "local_execution_interrupted") {
|
|
189
|
+
return [
|
|
190
|
+
guidanceAction(
|
|
191
|
+
"continue_interrupted_run",
|
|
192
|
+
"Continue this run",
|
|
193
|
+
"Confirm what SIMY should do next in the restored run.",
|
|
194
|
+
),
|
|
195
|
+
newRunAction(),
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
if (reasonCode === "human_input_required") {
|
|
199
|
+
return [
|
|
200
|
+
guidanceAction(
|
|
201
|
+
"provide_guidance",
|
|
202
|
+
"Give SIMY guidance",
|
|
203
|
+
"Answer the open question or explain what must change before continuing.",
|
|
204
|
+
),
|
|
205
|
+
newRunAction(),
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
return [
|
|
209
|
+
guidanceAction(
|
|
210
|
+
"retry_with_guidance",
|
|
211
|
+
"Retry with guidance",
|
|
212
|
+
"Explain what SIMY should change before retrying this run.",
|
|
213
|
+
),
|
|
214
|
+
newRunAction(),
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function guidanceAction(id, label, description) {
|
|
219
|
+
return { id, type: "send_guidance", label, description, command: null, payload: {} };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function newRunAction() {
|
|
223
|
+
return {
|
|
224
|
+
id: "start_new_run",
|
|
225
|
+
type: "start_new_run",
|
|
226
|
+
label: "Start a new run",
|
|
227
|
+
description: "Keep this record and begin again with a revised task.",
|
|
228
|
+
command: null,
|
|
229
|
+
payload: {},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function checkAgainAction() {
|
|
234
|
+
return {
|
|
235
|
+
id: "check_again",
|
|
236
|
+
type: "retry_preflight",
|
|
237
|
+
label: "Check again",
|
|
238
|
+
description: "Run the local environment check again after completing the recovery step.",
|
|
239
|
+
command: null,
|
|
240
|
+
payload: {},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function preflightContract(reasonCode, summary, actions, technicalDetails = {}) {
|
|
245
|
+
return {
|
|
246
|
+
schema_version: 1,
|
|
247
|
+
state: "blocked",
|
|
248
|
+
reason_code: reasonCode,
|
|
249
|
+
user_message: clean(summary) || "The local environment is not ready.",
|
|
250
|
+
primary_action_id: actions[0].id,
|
|
251
|
+
actions,
|
|
252
|
+
technical_details: technicalDetails,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function clean(value) {
|
|
257
|
+
return typeof value === "string" ? value.trim() : "";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function strings(value) {
|
|
261
|
+
return Array.isArray(value) ? value.map(clean).filter(Boolean) : [];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function positiveInteger(value) {
|
|
265
|
+
const number = Number.parseInt(String(value ?? ""), 10);
|
|
266
|
+
return Number.isInteger(number) && number > 0 ? number : null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function nonNegativeInteger(value) {
|
|
270
|
+
return Number.isInteger(value) && value >= 0 ? value : null;
|
|
271
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cleanString, stringArray } from "./shared.js";
|
|
2
|
+
import { normalizeProblemSolving } from "./problem-solving.js";
|
|
2
3
|
|
|
3
4
|
export function buildAttempt({ attemptNumber, charter, instruction, promptInterventions, execution }) {
|
|
4
5
|
const result = execution.result && typeof execution.result === "object" ? execution.result : {};
|
|
@@ -26,6 +27,7 @@ export function buildAttempt({ attemptNumber, charter, instruction, promptInterv
|
|
|
26
27
|
tests_passed: typeof result.tests_passed === "boolean" ? result.tests_passed : null,
|
|
27
28
|
ui_evidence_path: cleanString(result.ui_evidence_path) || null,
|
|
28
29
|
summary,
|
|
30
|
+
problem_solving: normalizeProblemSolving(result.problem_solving),
|
|
29
31
|
prompt_interventions: promptInterventions,
|
|
30
32
|
work_log_signals: [],
|
|
31
33
|
work_log_interventions: [],
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { cleanString, stringArray } from "./shared.js";
|
|
4
|
+
|
|
5
|
+
const RETRY_FINGERPRINT_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
export function evaluateRetryCircuit({ attempt, previousAttempt, findings }) {
|
|
8
|
+
const fingerprint = buildRetryFingerprint(attempt, findings);
|
|
9
|
+
attempt.retry_fingerprint = fingerprint;
|
|
10
|
+
const previousFingerprint = previousAttempt
|
|
11
|
+
? previousAttempt.retry_fingerprint ||
|
|
12
|
+
buildRetryFingerprint(previousAttempt, findingsForAttempt(previousAttempt))
|
|
13
|
+
: null;
|
|
14
|
+
return {
|
|
15
|
+
open: Boolean(previousFingerprint?.digest && previousFingerprint.digest === fingerprint.digest),
|
|
16
|
+
fingerprint,
|
|
17
|
+
previous_fingerprint: previousFingerprint,
|
|
18
|
+
previous_attempt_number: previousAttempt?.attempt_number ?? null,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildRetryFingerprint(attempt, findings) {
|
|
23
|
+
const commitSha =
|
|
24
|
+
cleanString(attempt?.observed_evidence?.local?.head_sha) ||
|
|
25
|
+
cleanString(attempt?.commit_sha) ||
|
|
26
|
+
null;
|
|
27
|
+
const normalizedFindings = normalizeFindings(findings);
|
|
28
|
+
const normalizedEvidence = normalizeEvidence(attempt);
|
|
29
|
+
const findingPayload = stableJson(normalizedFindings);
|
|
30
|
+
const evidencePayload = stableJson(normalizedEvidence);
|
|
31
|
+
const payload = stableJson({
|
|
32
|
+
version: RETRY_FINGERPRINT_VERSION,
|
|
33
|
+
commit_sha: commitSha,
|
|
34
|
+
findings: normalizedFindings,
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
version: RETRY_FINGERPRINT_VERSION,
|
|
38
|
+
digest: sha256(payload),
|
|
39
|
+
commit_sha: commitSha,
|
|
40
|
+
finding_codes: normalizedFindings.map((finding) => finding.code),
|
|
41
|
+
findings_sha256: sha256(findingPayload),
|
|
42
|
+
evidence_sha256: sha256(evidencePayload),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function findingsForAttempt(attempt) {
|
|
47
|
+
return [
|
|
48
|
+
...(Array.isArray(attempt?.audit?.findings) ? attempt.audit.findings : []),
|
|
49
|
+
...(Array.isArray(attempt?.independent_audit?.findings)
|
|
50
|
+
? attempt.independent_audit.findings
|
|
51
|
+
: []),
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeFindings(findings) {
|
|
56
|
+
return (Array.isArray(findings) ? findings : [])
|
|
57
|
+
.filter((finding) => finding && typeof finding === "object")
|
|
58
|
+
.map((finding) => ({
|
|
59
|
+
passed:
|
|
60
|
+
finding.passed === true ? true : finding.passed === false ? false : null,
|
|
61
|
+
code: cleanString(finding.code) || null,
|
|
62
|
+
severity: cleanString(finding.severity).toLowerCase() || null,
|
|
63
|
+
target: cleanString(finding.target) || null,
|
|
64
|
+
repairability: cleanString(finding.repairability).toLowerCase() || null,
|
|
65
|
+
}))
|
|
66
|
+
.sort(compareStableValues);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeEvidence(attempt) {
|
|
70
|
+
const local = attempt?.observed_evidence?.local || {};
|
|
71
|
+
const github = attempt?.observed_evidence?.github || {};
|
|
72
|
+
return {
|
|
73
|
+
reported: {
|
|
74
|
+
branch_name: cleanString(attempt?.branch_name) || null,
|
|
75
|
+
commit_sha: cleanString(attempt?.commit_sha) || null,
|
|
76
|
+
pr_url: cleanString(attempt?.pr_url) || null,
|
|
77
|
+
pr_number: Number.isInteger(attempt?.pr_number) ? attempt.pr_number : null,
|
|
78
|
+
tests_run: sortedStrings(attempt?.tests_run),
|
|
79
|
+
tests_passed:
|
|
80
|
+
typeof attempt?.tests_passed === "boolean" ? attempt.tests_passed : null,
|
|
81
|
+
ui_evidence_path: cleanString(attempt?.ui_evidence_path) || null,
|
|
82
|
+
changed_files: sortedStrings(attempt?.raw_output?.changed_files),
|
|
83
|
+
},
|
|
84
|
+
local: {
|
|
85
|
+
available: local.available === true,
|
|
86
|
+
head_sha: cleanString(local.head_sha) || null,
|
|
87
|
+
branch_name: cleanString(local.branch_name) || null,
|
|
88
|
+
commit_headline: cleanString(local.commit_headline) || null,
|
|
89
|
+
working_tree_clean: local.working_tree_clean === true,
|
|
90
|
+
changed_files: sortedStrings(local.changed_files),
|
|
91
|
+
status_lines: sortedStrings(local.status_lines),
|
|
92
|
+
errors: sortedStrings(local.errors),
|
|
93
|
+
},
|
|
94
|
+
github: {
|
|
95
|
+
available: github.available === true,
|
|
96
|
+
url: cleanString(github.url) || null,
|
|
97
|
+
number: Number.isInteger(github.number) ? github.number : null,
|
|
98
|
+
title: cleanString(github.title) || null,
|
|
99
|
+
is_draft: github.is_draft === true,
|
|
100
|
+
mergeable: cleanString(github.mergeable) || null,
|
|
101
|
+
merge_state_status: cleanString(github.merge_state_status) || null,
|
|
102
|
+
review_decision: cleanString(github.review_decision) || null,
|
|
103
|
+
approvals: sortedStrings(github.approvals),
|
|
104
|
+
head_branch: cleanString(github.head_branch) || null,
|
|
105
|
+
head_sha: cleanString(github.head_sha) || null,
|
|
106
|
+
repository: cleanString(github.repository) || null,
|
|
107
|
+
base_branch: cleanString(github.base_branch) || null,
|
|
108
|
+
checks: normalizeChecks(github.checks),
|
|
109
|
+
error: cleanString(github.error) || null,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function normalizeChecks(checks) {
|
|
115
|
+
return (Array.isArray(checks) ? checks : [])
|
|
116
|
+
.filter((check) => check && typeof check === "object")
|
|
117
|
+
.map((check) => ({
|
|
118
|
+
name: cleanString(check.name) || null,
|
|
119
|
+
state: cleanString(check.state) || null,
|
|
120
|
+
status: cleanString(check.status) || null,
|
|
121
|
+
conclusion: cleanString(check.conclusion) || null,
|
|
122
|
+
}))
|
|
123
|
+
.sort(compareStableValues);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function sortedStrings(values) {
|
|
127
|
+
return [...new Set(stringArray(values))].sort();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function compareStableValues(left, right) {
|
|
131
|
+
return stableJson(left).localeCompare(stableJson(right));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function stableJson(value) {
|
|
135
|
+
return JSON.stringify(value);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function sha256(value) {
|
|
139
|
+
return createHash("sha256").update(value).digest("hex");
|
|
140
|
+
}
|
|
@@ -1,10 +1,47 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
+
import { recoveryContractForEvent } from "./recovery.js";
|
|
4
|
+
|
|
3
5
|
const ALLOWED_PREFIXES = ["feat:", "chore:", "fix:", "refactor:", "infra:"];
|
|
4
6
|
const MAX_ATTEMPTS = 5;
|
|
7
|
+
const PROCESS_MESSAGE_KEY_BY_STATE = Object.freeze({
|
|
8
|
+
queued: "agenticLoop.process.queued",
|
|
9
|
+
risk_classifying: "agenticLoop.process.riskClassifying",
|
|
10
|
+
chartering: "agenticLoop.process.chartering",
|
|
11
|
+
dispatching: "agenticLoop.process.dispatching",
|
|
12
|
+
coding: "agenticLoop.process.coding",
|
|
13
|
+
collecting_evidence: "agenticLoop.process.collectingEvidence",
|
|
14
|
+
auditing: "agenticLoop.process.auditing",
|
|
15
|
+
independent_auditing: "agenticLoop.process.independentAuditing",
|
|
16
|
+
checking_pr: "agenticLoop.process.checkingPr",
|
|
17
|
+
re_instructing: "agenticLoop.process.reInstructing",
|
|
18
|
+
pr_ready_for_review: "agenticLoop.process.readyForReview",
|
|
19
|
+
merge_ready: "agenticLoop.process.mergeReady",
|
|
20
|
+
waiting_human: "agenticLoop.process.waitingHuman",
|
|
21
|
+
blocked: "agenticLoop.process.blocked",
|
|
22
|
+
failed: "agenticLoop.process.failed",
|
|
23
|
+
stopped: "agenticLoop.process.stopped",
|
|
24
|
+
done: "agenticLoop.process.done",
|
|
25
|
+
});
|
|
26
|
+
const PROCESS_MESSAGE_PARAM_KEYS = Object.freeze([
|
|
27
|
+
"attempt_number",
|
|
28
|
+
"backend",
|
|
29
|
+
"previous_backend",
|
|
30
|
+
"next_backend",
|
|
31
|
+
"interrupted_state",
|
|
32
|
+
]);
|
|
33
|
+
const PROCESS_MESSAGE_KEY_BY_CODE = Object.freeze({
|
|
34
|
+
local_execution_interrupted: "agenticLoop.process.executionInterrupted",
|
|
35
|
+
local_repository_not_authorized: "agenticLoop.process.repositoryRequired",
|
|
36
|
+
local_repository_not_found: "agenticLoop.process.repositoryRequired",
|
|
37
|
+
retry_circuit_open: "agenticLoop.process.retryStrategyNeeded",
|
|
38
|
+
});
|
|
5
39
|
|
|
6
40
|
export function appendEvent(snapshot, state, message, detail = {}) {
|
|
7
|
-
snapshot
|
|
41
|
+
const recovery = recoveryContractForEvent({ snapshot, state, message, detail });
|
|
42
|
+
snapshot.events.push(
|
|
43
|
+
event(state, message, recovery ? { ...detail, recovery } : detail),
|
|
44
|
+
);
|
|
8
45
|
}
|
|
9
46
|
|
|
10
47
|
export function bullets(items) {
|
|
@@ -22,7 +59,35 @@ export function cleanString(value) {
|
|
|
22
59
|
}
|
|
23
60
|
|
|
24
61
|
export function event(state, message, detail = {}) {
|
|
25
|
-
return {
|
|
62
|
+
return {
|
|
63
|
+
state,
|
|
64
|
+
message,
|
|
65
|
+
detail: processMessageDetail(state, detail),
|
|
66
|
+
occurred_at: new Date().toISOString(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function processMessageDetail(state, detail = {}) {
|
|
71
|
+
const normalized = detail && typeof detail === "object" && !Array.isArray(detail) ? detail : {};
|
|
72
|
+
if (typeof normalized.message_key === "string" && normalized.message_key.trim()) {
|
|
73
|
+
return { ...normalized };
|
|
74
|
+
}
|
|
75
|
+
const messageKey =
|
|
76
|
+
PROCESS_MESSAGE_KEY_BY_CODE[normalized.code] ?? PROCESS_MESSAGE_KEY_BY_STATE[state];
|
|
77
|
+
if (!messageKey) return { ...normalized };
|
|
78
|
+
const messageParams = Object.fromEntries(
|
|
79
|
+
PROCESS_MESSAGE_PARAM_KEYS.flatMap((key) => {
|
|
80
|
+
const value = normalized[key];
|
|
81
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean"
|
|
82
|
+
? [[key, value]]
|
|
83
|
+
: [];
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
return {
|
|
87
|
+
...normalized,
|
|
88
|
+
message_key: messageKey,
|
|
89
|
+
message_params: messageParams,
|
|
90
|
+
};
|
|
26
91
|
}
|
|
27
92
|
|
|
28
93
|
export function evidenceKind(value) {
|
|
@@ -40,9 +105,26 @@ export function hasAllowedPrefix(value) {
|
|
|
40
105
|
}
|
|
41
106
|
|
|
42
107
|
export function looksLikeUiTask(value) {
|
|
43
|
-
return
|
|
44
|
-
|
|
45
|
-
|
|
108
|
+
return visualEvidenceSurface(value) !== null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function visualEvidenceSurface(value) {
|
|
112
|
+
const text = cleanString(value);
|
|
113
|
+
if (
|
|
114
|
+
/\b(browser|chrome|firefox|edge)\s+extension\b|\bextension\s+(popup|manifest|service worker)\b|拡張機能|浏览器扩展|瀏覽器擴充/i.test(
|
|
115
|
+
text,
|
|
116
|
+
)
|
|
117
|
+
) {
|
|
118
|
+
return "browser_extension";
|
|
119
|
+
}
|
|
120
|
+
if (
|
|
121
|
+
/\b(ui|ux|page|screen|component|frontend|browser|css|tailwind|react|next)\b|画面|ページ|页面|界面|按钮|表示/i.test(
|
|
122
|
+
text,
|
|
123
|
+
)
|
|
124
|
+
) {
|
|
125
|
+
return "web_ui";
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
46
128
|
}
|
|
47
129
|
|
|
48
130
|
export async function publish(snapshot, state, onUpdate) {
|
package/src/provider-stream.js
CHANGED
|
@@ -2,6 +2,8 @@ import { stripVTControlCharacters } from "node:util";
|
|
|
2
2
|
|
|
3
3
|
const MAX_LINE_CHARS = 2_000;
|
|
4
4
|
const STRUCTURED_MARKERS = ["SIMY_RESULT_JSON:", "SIMY_AUDIT_JSON:"];
|
|
5
|
+
const CODEX_MCP_AUTH_WARNING =
|
|
6
|
+
"[Codex] MCP warning: optional connector authorization expired; coding continues.";
|
|
5
7
|
|
|
6
8
|
export function createProviderStreamDecoder({ backend, stream = "stdout", onLine, onUsage }) {
|
|
7
9
|
let pending = "";
|
|
@@ -63,6 +65,10 @@ export function formatProviderEvent(backend, event) {
|
|
|
63
65
|
return backend === "claude" ? formatClaudeEvent(event) : formatCodexEvent(event);
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
export function isNonFatalProviderDiagnostic(line) {
|
|
69
|
+
return line === CODEX_MCP_AUTH_WARNING;
|
|
70
|
+
}
|
|
71
|
+
|
|
66
72
|
function formatCodexEvent(event) {
|
|
67
73
|
const prefix = "[Codex]";
|
|
68
74
|
if (event.type === "thread.started") {
|
|
@@ -189,6 +195,9 @@ function contentLines(prefix, label, value) {
|
|
|
189
195
|
|
|
190
196
|
function formatPlainLine(backend, stream, value) {
|
|
191
197
|
const prefix = backend === "claude" ? "[Claude Code]" : "[Codex]";
|
|
198
|
+
if (backend === "codex" && isCodexMcpAuthorizationDiagnostic(value)) {
|
|
199
|
+
return CODEX_MCP_AUTH_WARNING;
|
|
200
|
+
}
|
|
192
201
|
const diagnostic = String(value).match(
|
|
193
202
|
/^\S+\s+(WARN|ERROR|INFO)\s+([\w.-]+(?:::[\w.-]+)*):\s*(.*)$/,
|
|
194
203
|
);
|
|
@@ -199,6 +208,14 @@ function formatPlainLine(backend, stream, value) {
|
|
|
199
208
|
return stream === "stderr" ? `${prefix} stderr: ${value}` : `${prefix} ${value}`;
|
|
200
209
|
}
|
|
201
210
|
|
|
211
|
+
function isCodexMcpAuthorizationDiagnostic(value) {
|
|
212
|
+
const text = String(value || "");
|
|
213
|
+
return (
|
|
214
|
+
/(?:rmcp::|codex_mcp::)/.test(text) &&
|
|
215
|
+
/(?:invalid_grant|AuthorizationRequired|OAuth authorization required)/i.test(text)
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
202
219
|
function toolResultText(value) {
|
|
203
220
|
if (typeof value === "string") return value;
|
|
204
221
|
if (!Array.isArray(value)) return "";
|