@awak-app/simy-cli 0.1.1 → 0.1.3
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 +45 -4
- package/package.json +16 -3
- package/src/agent.js +717 -41
- package/src/backend-executable.js +44 -0
- package/src/browser.js +59 -0
- package/src/console/app.js +1042 -0
- package/src/console/commands.js +100 -0
- package/src/console/index.js +25 -0
- package/src/index.js +22 -1
- package/src/local-attachments.js +270 -0
- package/src/orchestrator/contract.js +1 -0
- package/src/orchestrator/independent-audit.js +25 -0
- package/src/orchestrator/index.js +1 -1
- package/src/orchestrator/instruction.js +27 -1
- package/src/orchestrator/loop.js +65 -25
- package/src/orchestrator/presentation.js +189 -0
- package/src/orchestrator/result.js +11 -0
- package/src/provider-stream.js +327 -0
- package/src/repository-inventory.js +216 -0
- package/src/run-registry.js +44 -0
- package/src/runner.js +565 -66
- package/src/web-api.js +66 -0
- package/src/workspace-context.js +37 -0
package/src/runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
|
-
import { EventEmitter } from "node:events";
|
|
2
|
+
import { EventEmitter, once } from "node:events";
|
|
3
3
|
import { access } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
@@ -18,32 +18,55 @@ import {
|
|
|
18
18
|
redactExecutionLogs,
|
|
19
19
|
redactExecutionText,
|
|
20
20
|
} from "./orchestrator/execution-io.js";
|
|
21
|
+
import { summarizeCodingLoopEvent } from "./orchestrator/presentation.js";
|
|
22
|
+
import {
|
|
23
|
+
createProviderStreamDecoder,
|
|
24
|
+
isNonFatalProviderDiagnostic,
|
|
25
|
+
} from "./provider-stream.js";
|
|
26
|
+
import { resolveBackendExecutable } from "./backend-executable.js";
|
|
27
|
+
import {
|
|
28
|
+
attachmentDescriptorForLedger,
|
|
29
|
+
cleanupRunAttachments,
|
|
30
|
+
markAttachmentsCleaned,
|
|
31
|
+
markAttachmentsCleanupFailed,
|
|
32
|
+
markAttachmentsDelivered,
|
|
33
|
+
} from "./local-attachments.js";
|
|
34
|
+
import { LocalRunRegistry } from "./run-registry.js";
|
|
35
|
+
import { webApiHeaders, webApiUrl } from "./web-api.js";
|
|
36
|
+
import { normalizeGitHubRemote } from "./workspace-context.js";
|
|
21
37
|
|
|
22
38
|
const execFileAsync = promisify(execFile);
|
|
39
|
+
const MAX_LOCAL_LOG_LINES = 1_000;
|
|
23
40
|
const TERMINAL_STATES = new Set([
|
|
24
41
|
"merge_ready",
|
|
25
42
|
"pr_ready_for_review",
|
|
26
43
|
"failed",
|
|
27
44
|
"blocked",
|
|
28
45
|
"waiting_human",
|
|
46
|
+
"stopped",
|
|
29
47
|
]);
|
|
48
|
+
const RESUMABLE_STATES = new Set([
|
|
49
|
+
"waiting_human",
|
|
50
|
+
"pr_ready_for_review",
|
|
51
|
+
"blocked",
|
|
52
|
+
"failed",
|
|
53
|
+
"stopped",
|
|
54
|
+
]);
|
|
55
|
+
const INTERRUPTED_STATES = new Set([
|
|
56
|
+
"queued",
|
|
57
|
+
"risk_classifying",
|
|
58
|
+
"chartering",
|
|
59
|
+
"dispatching",
|
|
60
|
+
"coding",
|
|
61
|
+
"collecting_evidence",
|
|
62
|
+
"auditing",
|
|
63
|
+
"independent_auditing",
|
|
64
|
+
"checking_pr",
|
|
65
|
+
"re_instructing",
|
|
66
|
+
]);
|
|
67
|
+
const RESTORABLE_STATES = new Set([...RESUMABLE_STATES, ...INTERRUPTED_STATES]);
|
|
30
68
|
|
|
31
|
-
export
|
|
32
|
-
#runs = new Map();
|
|
33
|
-
|
|
34
|
-
create(run) {
|
|
35
|
-
if (this.#runs.has(run.id)) throw new Error(`Run ${run.id} already exists.`);
|
|
36
|
-
this.#runs.set(run.id, run);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
get(runId) {
|
|
40
|
-
return this.#runs.get(runId) ?? null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
has(runId) {
|
|
44
|
-
return this.#runs.has(runId);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
69
|
+
export { LocalRunRegistry };
|
|
47
70
|
|
|
48
71
|
export function createRun({ runId, request, session, apiOrigin }) {
|
|
49
72
|
const emitter = new EventEmitter();
|
|
@@ -57,6 +80,12 @@ export function createRun({ runId, request, session, apiOrigin }) {
|
|
|
57
80
|
startedAt: null,
|
|
58
81
|
completedAt: null,
|
|
59
82
|
child: null,
|
|
83
|
+
operation: null,
|
|
84
|
+
controlState: "queued",
|
|
85
|
+
stopRequested: false,
|
|
86
|
+
logs: [],
|
|
87
|
+
pendingGuidance: [],
|
|
88
|
+
resumeCount: 0,
|
|
60
89
|
emitter,
|
|
61
90
|
pendingLedgerUpdate: null,
|
|
62
91
|
ledgerUpdateRunning: false,
|
|
@@ -71,13 +100,342 @@ export function createRun({ runId, request, session, apiOrigin }) {
|
|
|
71
100
|
};
|
|
72
101
|
}
|
|
73
102
|
|
|
103
|
+
export function restoreRun({ snapshot, session, apiOrigin, localPath = null }) {
|
|
104
|
+
if (!snapshot || typeof snapshot !== "object" || !snapshot.charter) {
|
|
105
|
+
throw new Error("Cannot restore an invalid coding loop snapshot.");
|
|
106
|
+
}
|
|
107
|
+
const charter = snapshot.charter;
|
|
108
|
+
const run = createRun({
|
|
109
|
+
runId: String(snapshot.id || ""),
|
|
110
|
+
session,
|
|
111
|
+
apiOrigin,
|
|
112
|
+
request: {
|
|
113
|
+
backend: charter.backend === "claude" ? "claude" : "codex",
|
|
114
|
+
audit_backend:
|
|
115
|
+
charter.audit_backend === "claude" || charter.audit_backend === "codex"
|
|
116
|
+
? charter.audit_backend
|
|
117
|
+
: null,
|
|
118
|
+
requirement: String(charter.requirement || ""),
|
|
119
|
+
repository: String(charter.repository || ""),
|
|
120
|
+
local_path: localPath,
|
|
121
|
+
base_branch: String(charter.base_branch || "dev"),
|
|
122
|
+
max_attempts: charter.max_attempts,
|
|
123
|
+
ui_evidence_root: String(charter.ui_evidence_root || ""),
|
|
124
|
+
acceptance_criteria: Array.isArray(charter.acceptance_criteria)
|
|
125
|
+
? charter.acceptance_criteria
|
|
126
|
+
: [],
|
|
127
|
+
expected_tests: Array.isArray(charter.expected_tests) ? charter.expected_tests : [],
|
|
128
|
+
expected_evidence: Array.isArray(charter.expected_evidence)
|
|
129
|
+
? charter.expected_evidence
|
|
130
|
+
: [],
|
|
131
|
+
required_checks: Array.isArray(charter.required_checks) ? charter.required_checks : [],
|
|
132
|
+
require_human_approval: charter.require_human_approval !== false,
|
|
133
|
+
must_not: Array.isArray(charter.must_not) ? charter.must_not : [],
|
|
134
|
+
proposal_id: typeof charter.proposal_id === "string" ? charter.proposal_id : null,
|
|
135
|
+
attachments: [],
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
const restoredSnapshot = structuredClone(snapshot);
|
|
139
|
+
const lifecycleState = restoredSnapshot.metadata?.pr_lifecycle_state;
|
|
140
|
+
const persistedState =
|
|
141
|
+
typeof lifecycleState === "string" && RESTORABLE_STATES.has(lifecycleState)
|
|
142
|
+
? lifecycleState
|
|
143
|
+
: restoredSnapshot.state;
|
|
144
|
+
if (!RESTORABLE_STATES.has(persistedState)) {
|
|
145
|
+
throw new Error(`Coding loop ${run.id} is not in a restorable state.`);
|
|
146
|
+
}
|
|
147
|
+
const restoredFromState = INTERRUPTED_STATES.has(persistedState) ? persistedState : null;
|
|
148
|
+
const restoredState = restoredFromState ? "waiting_human" : persistedState;
|
|
149
|
+
if (restoredFromState) {
|
|
150
|
+
restoredSnapshot.events = Array.isArray(restoredSnapshot.events)
|
|
151
|
+
? restoredSnapshot.events
|
|
152
|
+
: [];
|
|
153
|
+
restoredSnapshot.events.push({
|
|
154
|
+
state: "waiting_human",
|
|
155
|
+
message: "Local execution was interrupted and is ready to continue.",
|
|
156
|
+
detail: {
|
|
157
|
+
code: "local_execution_interrupted",
|
|
158
|
+
interrupted_state: restoredFromState,
|
|
159
|
+
},
|
|
160
|
+
occurred_at: new Date().toISOString(),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
run.snapshot = restoredSnapshot;
|
|
165
|
+
run.snapshot.state = restoredState;
|
|
166
|
+
run.status = restoredState;
|
|
167
|
+
run.lastOutput = restoredFromState
|
|
168
|
+
? `Local CLI restarted during ${restoredFromState}; provide guidance to continue this run.`
|
|
169
|
+
: String(restoredSnapshot.metadata?.last_agent_output || "");
|
|
170
|
+
run.startedAt = restoredSnapshot.created_at || null;
|
|
171
|
+
run.completedAt = restoredSnapshot.updated_at || null;
|
|
172
|
+
run.controlState = restoredState === "stopped" ? "stopped" : "waiting_human";
|
|
173
|
+
run.restoredFromState = restoredFromState;
|
|
174
|
+
return run;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function publishRestoredLocalCodingRun(run) {
|
|
178
|
+
if (!run?.restoredFromState) return run?.snapshot || null;
|
|
179
|
+
await updateRun(run, "waiting_human");
|
|
180
|
+
return run.snapshot;
|
|
181
|
+
}
|
|
182
|
+
|
|
74
183
|
export async function startLocalCodingRun(
|
|
75
184
|
run,
|
|
76
|
-
|
|
185
|
+
dependencies = {},
|
|
77
186
|
) {
|
|
78
|
-
if (run.child || TERMINAL_STATES.has(run.status) || run.startedAt) return;
|
|
187
|
+
if (run.operation || run.child || TERMINAL_STATES.has(run.status) || run.startedAt) return;
|
|
79
188
|
run.startedAt = new Date().toISOString();
|
|
189
|
+
run.controlState = "running";
|
|
190
|
+
return executeLocalCodingRun(run, dependencies);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function continueLocalCodingRun(run, guidance, dependencies = {}) {
|
|
194
|
+
const message = String(guidance || "").trim();
|
|
195
|
+
if (!message) throw new Error("Human guidance is required.");
|
|
196
|
+
if (run.operation || run.child) throw new Error("The selected coding run is still active.");
|
|
197
|
+
if (!RESUMABLE_STATES.has(run.status)) {
|
|
198
|
+
throw new Error("Human guidance is available only when the selected run is waiting.");
|
|
199
|
+
}
|
|
80
200
|
|
|
201
|
+
const nextAttempt = run.snapshot.attempts.length + 1;
|
|
202
|
+
if (nextAttempt > 5) throw new Error("The coding run reached the five-attempt safety limit.");
|
|
203
|
+
run.snapshot.charter.max_attempts = Math.max(run.snapshot.charter.max_attempts, nextAttempt);
|
|
204
|
+
run.completedAt = null;
|
|
205
|
+
run.stopRequested = false;
|
|
206
|
+
run.controlState = "running";
|
|
207
|
+
run.resumeCount += 1;
|
|
208
|
+
emitControl(run, "human_input", `Human guidance accepted: ${message}`);
|
|
209
|
+
return executeLocalCodingRun(run, dependencies, { humanGuidance: message, resume: true });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function continueLocalCodingRunAfterRepositoryApproval(
|
|
213
|
+
run,
|
|
214
|
+
{ repository, localPath } = {},
|
|
215
|
+
dependencies = {},
|
|
216
|
+
) {
|
|
217
|
+
const approvedRepository = String(repository || run.request.repository || "").trim();
|
|
218
|
+
const approvedPath = String(localPath || "").trim();
|
|
219
|
+
if (!approvedRepository || !approvedPath) {
|
|
220
|
+
throw new Error("An approved local repository and path are required.");
|
|
221
|
+
}
|
|
222
|
+
if (run.operation || run.child) throw new Error("The selected coding run is still active.");
|
|
223
|
+
if (run.status !== "waiting_human") {
|
|
224
|
+
throw new Error("Repository approval is available only while the run is waiting.");
|
|
225
|
+
}
|
|
226
|
+
if (run.snapshot.attempts.length > 0) {
|
|
227
|
+
throw new Error("Repository approval cannot restart a run after a coding attempt.");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
run.request.local_path = approvedPath;
|
|
231
|
+
run.snapshot.final_audit = null;
|
|
232
|
+
run.snapshot.events.push({
|
|
233
|
+
state: "queued",
|
|
234
|
+
message: "Local repository authorization accepted.",
|
|
235
|
+
detail: {
|
|
236
|
+
code: "local_repository_authorized",
|
|
237
|
+
repository: approvedRepository,
|
|
238
|
+
},
|
|
239
|
+
occurred_at: new Date().toISOString(),
|
|
240
|
+
});
|
|
241
|
+
run.snapshot.state = "queued";
|
|
242
|
+
run.status = "queued";
|
|
243
|
+
run.lastOutput = `Authorized local repository: ${approvedRepository}.`;
|
|
244
|
+
run.completedAt = null;
|
|
245
|
+
run.stopRequested = false;
|
|
246
|
+
run.controlState = "running";
|
|
247
|
+
emitControl(
|
|
248
|
+
run,
|
|
249
|
+
"repository_authorized",
|
|
250
|
+
`Authorized local repository selected: ${approvedRepository}.`,
|
|
251
|
+
);
|
|
252
|
+
return executeLocalCodingRun(run, dependencies, {
|
|
253
|
+
humanGuidance: `Repository scan approved: ${approvedRepository}.`,
|
|
254
|
+
resume: false,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function isLocalRepositoryApprovalPending(run) {
|
|
259
|
+
if (run?.status !== "waiting_human" || run.snapshot?.attempts?.length > 0) return false;
|
|
260
|
+
const events = Array.isArray(run.snapshot?.events) ? run.snapshot.events : [];
|
|
261
|
+
const markerIndex = events.findLastIndex(
|
|
262
|
+
(event) =>
|
|
263
|
+
event?.detail?.code === "local_repository_not_authorized" ||
|
|
264
|
+
event?.detail?.code === "local_repository_not_found",
|
|
265
|
+
);
|
|
266
|
+
if (markerIndex < 0) return false;
|
|
267
|
+
return events
|
|
268
|
+
.slice(markerIndex + 1)
|
|
269
|
+
.every((event) => event?.state === "waiting_human" || event?.state === "pr_ready_for_review");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function queueLocalCodingGuidance(run, guidance) {
|
|
273
|
+
const message = String(guidance || "").trim();
|
|
274
|
+
if (!message) throw new Error("Human guidance is required.");
|
|
275
|
+
run.pendingGuidance.push(message);
|
|
276
|
+
emitControl(
|
|
277
|
+
run,
|
|
278
|
+
"guidance_queued",
|
|
279
|
+
"Human guidance queued for the next executor handoff.",
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function applyLocalHumanDecision(
|
|
284
|
+
run,
|
|
285
|
+
{ message, designApproval = false, requiredChecks = [], acceptanceCriteria = [] } = {},
|
|
286
|
+
dependencies = {},
|
|
287
|
+
) {
|
|
288
|
+
const guidance = String(message || "").trim();
|
|
289
|
+
if (!guidance) throw new Error("A human decision note is required.");
|
|
290
|
+
const charter = run.snapshot.charter;
|
|
291
|
+
if (acceptanceCriteria.length > 0) {
|
|
292
|
+
charter.acceptance_criteria = acceptanceCriteria.map(String).map((item) => item.trim()).filter(Boolean);
|
|
293
|
+
charter.acceptance_criteria_source = "explicit";
|
|
294
|
+
}
|
|
295
|
+
if (requiredChecks.length > 0) {
|
|
296
|
+
charter.required_checks = requiredChecks.map(String).map((item) => item.trim()).filter(Boolean);
|
|
297
|
+
}
|
|
298
|
+
if (designApproval) {
|
|
299
|
+
charter.design_review.summary = guidance;
|
|
300
|
+
charter.design_review.approved_by = run.session?.device_id || "local-cli-human";
|
|
301
|
+
charter.design_review.evidence_url = `local-cli://approval/${encodeURIComponent(run.id)}/${Date.now()}`;
|
|
302
|
+
charter.design_review.status = "approved";
|
|
303
|
+
}
|
|
304
|
+
return continueLocalCodingRun(run, guidance, dependencies);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function pauseLocalCodingRun(run) {
|
|
308
|
+
if (!run.child) throw new Error("The selected run has no active executor process.");
|
|
309
|
+
if (run.controlState === "paused") throw new Error("The executor process is already paused.");
|
|
310
|
+
if (process.platform === "win32") {
|
|
311
|
+
throw new Error("Process pause is not supported on Windows; stop the run instead.");
|
|
312
|
+
}
|
|
313
|
+
if (!signalExecutorProcess(run.child, "SIGSTOP")) {
|
|
314
|
+
throw new Error("The executor process could not be paused.");
|
|
315
|
+
}
|
|
316
|
+
run.controlState = "paused";
|
|
317
|
+
emitControl(run, "paused", "Executor paused by the local human operator.");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function resumeLocalCodingRun(run) {
|
|
321
|
+
if (!run.child) throw new Error("The selected run has no paused executor process.");
|
|
322
|
+
if (run.controlState !== "paused") throw new Error("The executor process is not paused.");
|
|
323
|
+
if (process.platform === "win32") {
|
|
324
|
+
throw new Error("Process resume is not supported on Windows.");
|
|
325
|
+
}
|
|
326
|
+
if (!signalExecutorProcess(run.child, "SIGCONT")) {
|
|
327
|
+
throw new Error("The executor process could not be resumed.");
|
|
328
|
+
}
|
|
329
|
+
run.controlState = "running";
|
|
330
|
+
emitControl(run, "resumed", "Executor resumed by the local human operator.");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export async function stopLocalCodingRun(run) {
|
|
334
|
+
if (run.status === "stopped") return run.snapshot;
|
|
335
|
+
if (run.stopRequested) {
|
|
336
|
+
const stopped = await waitForLocalStop(run);
|
|
337
|
+
if (!stopped) throw new Error("The local executor did not stop within the safety timeout.");
|
|
338
|
+
await finalizeLocalStop(run);
|
|
339
|
+
return run.snapshot;
|
|
340
|
+
}
|
|
341
|
+
run.stopRequested = true;
|
|
342
|
+
run.controlState = "stopping";
|
|
343
|
+
emitControl(run, "stopping", "Stop requested by the local human operator.");
|
|
344
|
+
|
|
345
|
+
const child = run.child;
|
|
346
|
+
if (!child) {
|
|
347
|
+
const stopped = await waitForLocalStop(run);
|
|
348
|
+
if (!stopped) throw new Error("The local executor did not stop within the safety timeout.");
|
|
349
|
+
await finalizeLocalStop(run);
|
|
350
|
+
return run.snapshot;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let outcome = await signalAndWaitForClose(child, "SIGINT", 1_500);
|
|
354
|
+
if (outcome === "timeout" && run.child === child) {
|
|
355
|
+
outcome = await signalAndWaitForClose(child, "SIGTERM", 1_500);
|
|
356
|
+
}
|
|
357
|
+
if (outcome === "timeout" && run.child === child) {
|
|
358
|
+
await signalAndWaitForClose(child, "SIGKILL", 750);
|
|
359
|
+
}
|
|
360
|
+
const stopped = await waitForLocalStop(run);
|
|
361
|
+
if (!stopped) throw new Error("The local executor did not stop within the safety timeout.");
|
|
362
|
+
await finalizeLocalStop(run);
|
|
363
|
+
return run.snapshot;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function signalAndWaitForClose(child, signal, timeoutMs) {
|
|
367
|
+
const close = once(child, "close").then(() => "closed");
|
|
368
|
+
signalExecutorProcess(child, signal);
|
|
369
|
+
return Promise.race([
|
|
370
|
+
close,
|
|
371
|
+
new Promise((resolve) => setTimeout(() => resolve("timeout"), timeoutMs)),
|
|
372
|
+
]);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function waitForLocalStop(run) {
|
|
376
|
+
if (!run.operation) return true;
|
|
377
|
+
return Promise.race([
|
|
378
|
+
run.operation.then(
|
|
379
|
+
() => true,
|
|
380
|
+
() => true,
|
|
381
|
+
),
|
|
382
|
+
new Promise((resolve) => setTimeout(() => resolve(false), 10_000)),
|
|
383
|
+
]);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function signalExecutorProcess(
|
|
387
|
+
child,
|
|
388
|
+
signal,
|
|
389
|
+
{ killProcess = process.kill, platform = process.platform } = {},
|
|
390
|
+
) {
|
|
391
|
+
if (platform !== "win32" && Number.isInteger(child?.pid)) {
|
|
392
|
+
try {
|
|
393
|
+
killProcess(-child.pid, signal);
|
|
394
|
+
return true;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (error?.code !== "ESRCH") return child.kill?.(signal) === true;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return child?.kill?.(signal) === true;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function finalizeLocalStop(run) {
|
|
403
|
+
if (run.status === "stopped") return;
|
|
404
|
+
if (run.snapshot.events.at(-1)?.state !== "stopped") {
|
|
405
|
+
run.snapshot.events.push({
|
|
406
|
+
state: "stopped",
|
|
407
|
+
message: "Coding loop stopped by the local human operator.",
|
|
408
|
+
detail: { source: "local_cli" },
|
|
409
|
+
occurred_at: new Date().toISOString(),
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
await updateRun(run, "stopped");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function executeLocalCodingRun(
|
|
416
|
+
run,
|
|
417
|
+
{ executeAttempt, executeIndependentAudit, collectEvidence } = {},
|
|
418
|
+
{ humanGuidance = "", resume = false } = {},
|
|
419
|
+
) {
|
|
420
|
+
const operation = runLocalCodingRun(run, {
|
|
421
|
+
executeAttempt,
|
|
422
|
+
executeIndependentAudit,
|
|
423
|
+
collectEvidence,
|
|
424
|
+
humanGuidance,
|
|
425
|
+
resume,
|
|
426
|
+
});
|
|
427
|
+
run.operation = operation;
|
|
428
|
+
try {
|
|
429
|
+
return await operation;
|
|
430
|
+
} finally {
|
|
431
|
+
if (run.operation === operation) run.operation = null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function runLocalCodingRun(
|
|
436
|
+
run,
|
|
437
|
+
{ executeAttempt, executeIndependentAudit, collectEvidence, humanGuidance, resume },
|
|
438
|
+
) {
|
|
81
439
|
let repositoryPath;
|
|
82
440
|
try {
|
|
83
441
|
repositoryPath = await resolveRepositoryPath(run.request);
|
|
@@ -88,13 +446,16 @@ export async function startLocalCodingRun(
|
|
|
88
446
|
run.snapshot.events.push({
|
|
89
447
|
state: "waiting_human",
|
|
90
448
|
message: "Local repository requires configuration.",
|
|
91
|
-
detail: { code: "
|
|
449
|
+
detail: { code: "local_repository_not_authorized" },
|
|
92
450
|
occurred_at: new Date().toISOString(),
|
|
93
451
|
});
|
|
452
|
+
await cleanupLocalAttachments(run);
|
|
94
453
|
await updateRun(run, "waiting_human");
|
|
95
454
|
return;
|
|
96
455
|
}
|
|
97
456
|
|
|
457
|
+
markAttachmentsDelivered(run.request.attachments);
|
|
458
|
+
|
|
98
459
|
const executor =
|
|
99
460
|
executeAttempt ||
|
|
100
461
|
((context) =>
|
|
@@ -127,6 +488,10 @@ export async function startLocalCodingRun(
|
|
|
127
488
|
executeAttempt: executor,
|
|
128
489
|
executeIndependentAudit: independentAuditor,
|
|
129
490
|
collectEvidence: evidenceCollector,
|
|
491
|
+
humanGuidance,
|
|
492
|
+
resume,
|
|
493
|
+
shouldStop: () => run.stopRequested,
|
|
494
|
+
consumeHumanGuidance: () => run.pendingGuidance.splice(0).join("\n\n"),
|
|
130
495
|
onUpdate: async (snapshot) => {
|
|
131
496
|
run.snapshot = snapshot;
|
|
132
497
|
await updateRun(run, snapshot.state);
|
|
@@ -142,6 +507,31 @@ export async function startLocalCodingRun(
|
|
|
142
507
|
occurred_at: new Date().toISOString(),
|
|
143
508
|
});
|
|
144
509
|
await updateRun(run, "failed");
|
|
510
|
+
} finally {
|
|
511
|
+
const cleanupError = await cleanupLocalAttachments(run);
|
|
512
|
+
if (cleanupError) {
|
|
513
|
+
const message = `Local attachment cleanup failed: ${cleanupError}`;
|
|
514
|
+
emitOutput(run, message);
|
|
515
|
+
run.snapshot.events.push({
|
|
516
|
+
state: "failed",
|
|
517
|
+
message: "Local attachment cleanup failed.",
|
|
518
|
+
detail: { error: cleanupError },
|
|
519
|
+
occurred_at: new Date().toISOString(),
|
|
520
|
+
});
|
|
521
|
+
run.snapshot.state = "failed";
|
|
522
|
+
}
|
|
523
|
+
await updateRun(run, run.snapshot.state);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async function cleanupLocalAttachments(run) {
|
|
528
|
+
try {
|
|
529
|
+
await cleanupRunAttachments(run.request.attachments);
|
|
530
|
+
markAttachmentsCleaned(run.request.attachments);
|
|
531
|
+
return null;
|
|
532
|
+
} catch (error) {
|
|
533
|
+
markAttachmentsCleanupFailed(run.request.attachments, error);
|
|
534
|
+
return error instanceof Error ? error.message : "attachment cleanup failed";
|
|
145
535
|
}
|
|
146
536
|
}
|
|
147
537
|
|
|
@@ -174,11 +564,14 @@ async function executeProcessAttempt({
|
|
|
174
564
|
repositoryPath,
|
|
175
565
|
run,
|
|
176
566
|
marker = "SIMY_RESULT_JSON:",
|
|
567
|
+
attemptNumber = null,
|
|
177
568
|
}) {
|
|
178
|
-
const command = resolveBackendCommand({ backend, instruction, repositoryPath });
|
|
569
|
+
const command = await resolveBackendCommand({ backend, instruction, repositoryPath });
|
|
179
570
|
const startedAt = new Date().toISOString();
|
|
180
571
|
const stdout = [];
|
|
181
572
|
const stderr = [];
|
|
573
|
+
const tokenUsageRecords = [];
|
|
574
|
+
const phase = marker === "SIMY_AUDIT_JSON:" ? "reviewer" : "executor";
|
|
182
575
|
|
|
183
576
|
return new Promise((resolve) => {
|
|
184
577
|
let settled = false;
|
|
@@ -186,20 +579,44 @@ async function executeProcessAttempt({
|
|
|
186
579
|
cwd: repositoryPath,
|
|
187
580
|
env: { ...process.env, ...command.env },
|
|
188
581
|
stdio: ["ignore", "pipe", "pipe"],
|
|
582
|
+
detached: process.platform !== "win32",
|
|
189
583
|
});
|
|
190
584
|
run.child = child;
|
|
191
585
|
|
|
192
|
-
const
|
|
586
|
+
const stdoutDecoder = createProviderStreamDecoder({
|
|
587
|
+
backend,
|
|
588
|
+
stream: "stdout",
|
|
589
|
+
onLine: (line) => emitOutput(run, line, { backend }),
|
|
590
|
+
onUsage: (usage) =>
|
|
591
|
+
tokenUsageRecords.push({
|
|
592
|
+
...usage,
|
|
593
|
+
phase,
|
|
594
|
+
...(Number.isInteger(attemptNumber) ? { attempt_number: attemptNumber } : {}),
|
|
595
|
+
backend,
|
|
596
|
+
}),
|
|
597
|
+
});
|
|
598
|
+
const stderrDecoder = createProviderStreamDecoder({
|
|
599
|
+
backend,
|
|
600
|
+
stream: "stderr",
|
|
601
|
+
onLine: (line) =>
|
|
602
|
+
emitOutput(run, line, {
|
|
603
|
+
backend,
|
|
604
|
+
promote: !isNonFatalProviderDiagnostic(line),
|
|
605
|
+
}),
|
|
606
|
+
});
|
|
607
|
+
const collect = (target, decoder, chunk) => {
|
|
193
608
|
const text = chunk.toString("utf8");
|
|
194
609
|
target.push(text);
|
|
195
|
-
|
|
610
|
+
decoder.push(text);
|
|
196
611
|
};
|
|
197
|
-
child.stdout?.on("data", (chunk) => collect(stdout, chunk));
|
|
198
|
-
child.stderr?.on("data", (chunk) => collect(stderr, chunk));
|
|
612
|
+
child.stdout?.on("data", (chunk) => collect(stdout, stdoutDecoder, chunk));
|
|
613
|
+
child.stderr?.on("data", (chunk) => collect(stderr, stderrDecoder, chunk));
|
|
199
614
|
|
|
200
615
|
const finish = ({ exitCode = null, error = null } = {}) => {
|
|
201
616
|
if (settled) return;
|
|
202
617
|
settled = true;
|
|
618
|
+
stdoutDecoder.flush();
|
|
619
|
+
stderrDecoder.flush();
|
|
203
620
|
run.child = null;
|
|
204
621
|
const rawStdout = stdout.join("");
|
|
205
622
|
const assistantText = extractAssistantText(rawStdout);
|
|
@@ -212,6 +629,8 @@ async function executeProcessAttempt({
|
|
|
212
629
|
exitCode,
|
|
213
630
|
error,
|
|
214
631
|
result: Object.keys(result).length > 0 ? result : parse(rawStdout),
|
|
632
|
+
tokenUsage:
|
|
633
|
+
tokenUsageRecords.length > 0 ? { records: tokenUsageRecords } : {},
|
|
215
634
|
logs: [...stdout, ...stderr]
|
|
216
635
|
.join("")
|
|
217
636
|
.split(/\r?\n/)
|
|
@@ -227,13 +646,14 @@ async function executeProcessAttempt({
|
|
|
227
646
|
});
|
|
228
647
|
}
|
|
229
648
|
|
|
230
|
-
function resolveBackendCommand({ backend, instruction, repositoryPath }) {
|
|
649
|
+
async function resolveBackendCommand({ backend, instruction, repositoryPath }) {
|
|
231
650
|
if (backend === "claude") {
|
|
232
651
|
const override = process.env.SIMY_CLAUDE_COMMAND;
|
|
233
652
|
if (override) return shellCommand(override, repositoryPath, instruction);
|
|
653
|
+
const binary = (await resolveBackendExecutable("claude")) || "claude";
|
|
234
654
|
return {
|
|
235
|
-
bin:
|
|
236
|
-
args:
|
|
655
|
+
bin: binary,
|
|
656
|
+
args: claudeBackendArgs(instruction),
|
|
237
657
|
env: {},
|
|
238
658
|
spawn,
|
|
239
659
|
};
|
|
@@ -241,14 +661,29 @@ function resolveBackendCommand({ backend, instruction, repositoryPath }) {
|
|
|
241
661
|
|
|
242
662
|
const override = process.env.SIMY_CODEX_COMMAND;
|
|
243
663
|
if (override) return shellCommand(override, repositoryPath, instruction);
|
|
664
|
+
const binary = (await resolveBackendExecutable("codex")) || "codex";
|
|
244
665
|
return {
|
|
245
|
-
bin:
|
|
666
|
+
bin: binary,
|
|
246
667
|
args: ["exec", "--json", instruction],
|
|
247
668
|
env: {},
|
|
248
669
|
spawn,
|
|
249
670
|
};
|
|
250
671
|
}
|
|
251
672
|
|
|
673
|
+
export function claudeBackendArgs(instruction) {
|
|
674
|
+
return [
|
|
675
|
+
"-p",
|
|
676
|
+
instruction,
|
|
677
|
+
"--output-format",
|
|
678
|
+
"stream-json",
|
|
679
|
+
"--verbose",
|
|
680
|
+
// The Coding Loop is already scoped to a verified checkout and explicitly
|
|
681
|
+
// approved by the user. Print mode cannot display permission prompts, so
|
|
682
|
+
// edits would otherwise be silently unavailable to the executor.
|
|
683
|
+
"--dangerously-skip-permissions",
|
|
684
|
+
];
|
|
685
|
+
}
|
|
686
|
+
|
|
252
687
|
function shellCommand(command, cwd, instruction) {
|
|
253
688
|
return {
|
|
254
689
|
bin: process.platform === "win32" ? "cmd.exe" : "sh",
|
|
@@ -259,12 +694,15 @@ function shellCommand(command, cwd, instruction) {
|
|
|
259
694
|
};
|
|
260
695
|
}
|
|
261
696
|
|
|
262
|
-
async function resolveRepositoryPath(request) {
|
|
697
|
+
export async function resolveRepositoryPath(request) {
|
|
263
698
|
const repository = String(request.repository || "").replace(/^https?:\/\/github\.com\//, "").replace(/\.git$/, "");
|
|
264
699
|
const repoName = repository.split("/").filter(Boolean).at(-1);
|
|
265
700
|
if (!repoName) throw new Error("The coding loop did not specify a valid GitHub repository.");
|
|
266
701
|
|
|
267
|
-
|
|
702
|
+
// An operator-provided root scopes this CLI process to the intended workspace.
|
|
703
|
+
// Persisted inventory paths can outlive a checkout, so only prefer them when no
|
|
704
|
+
// explicit process scope is configured.
|
|
705
|
+
const roots = [process.env.SIMY_REPO_ROOT, request.local_path, process.cwd()].filter(Boolean);
|
|
268
706
|
const candidates = new Set();
|
|
269
707
|
for (const root of roots) {
|
|
270
708
|
const resolved = path.resolve(String(root));
|
|
@@ -280,29 +718,19 @@ async function resolveRepositoryPath(request) {
|
|
|
280
718
|
|
|
281
719
|
throw new Error(
|
|
282
720
|
`Repository ${repository} was not found below ${process.env.SIMY_REPO_ROOT || process.cwd()}. ` +
|
|
283
|
-
"
|
|
721
|
+
"Open SIMY CLI and press s to authorize a local repository scan, or set SIMY_REPO_ROOT.",
|
|
284
722
|
);
|
|
285
723
|
}
|
|
286
724
|
|
|
287
725
|
async function gitRemote(cwd) {
|
|
288
726
|
try {
|
|
289
727
|
const { stdout } = await execFileAsync("git", ["remote", "get-url", "origin"], { cwd });
|
|
290
|
-
return normalizeGitHubRemote(stdout);
|
|
728
|
+
return normalizeGitHubRemote(stdout)?.toLowerCase() ?? null;
|
|
291
729
|
} catch {
|
|
292
730
|
return null;
|
|
293
731
|
}
|
|
294
732
|
}
|
|
295
733
|
|
|
296
|
-
function normalizeGitHubRemote(value) {
|
|
297
|
-
return String(value || "")
|
|
298
|
-
.trim()
|
|
299
|
-
.replace(/^git@github\.com:/, "")
|
|
300
|
-
.replace(/^ssh:\/\/git@github\.com\//, "")
|
|
301
|
-
.replace(/^https?:\/\/github\.com\//, "")
|
|
302
|
-
.replace(/\.git$/, "")
|
|
303
|
-
.toLowerCase();
|
|
304
|
-
}
|
|
305
|
-
|
|
306
734
|
async function pathExists(value) {
|
|
307
735
|
try {
|
|
308
736
|
await access(value);
|
|
@@ -332,25 +760,55 @@ function extractAssistantText(raw) {
|
|
|
332
760
|
return messages.join("\n");
|
|
333
761
|
}
|
|
334
762
|
|
|
335
|
-
function emitOutput(run, text) {
|
|
763
|
+
function emitOutput(run, text, { backend = run.request.backend, promote = true } = {}) {
|
|
336
764
|
for (const line of String(text).split(/\r?\n/)) {
|
|
337
765
|
if (!line.trim()) continue;
|
|
338
|
-
|
|
766
|
+
const occurredAt = new Date().toISOString();
|
|
767
|
+
if (promote) run.lastOutput = line;
|
|
768
|
+
run.logs.push({
|
|
769
|
+
text: line,
|
|
770
|
+
backend,
|
|
771
|
+
occurred_at: occurredAt,
|
|
772
|
+
});
|
|
773
|
+
if (run.logs.length > MAX_LOCAL_LOG_LINES) {
|
|
774
|
+
run.logs.splice(0, run.logs.length - MAX_LOCAL_LOG_LINES);
|
|
775
|
+
}
|
|
339
776
|
run.emitter.emit("event", {
|
|
340
777
|
type: "output",
|
|
341
778
|
run_id: run.id,
|
|
342
|
-
backend
|
|
779
|
+
backend,
|
|
343
780
|
text: line,
|
|
344
|
-
occurred_at:
|
|
781
|
+
occurred_at: occurredAt,
|
|
345
782
|
});
|
|
346
783
|
}
|
|
347
784
|
}
|
|
348
785
|
|
|
786
|
+
function emitControl(run, state, message) {
|
|
787
|
+
run.emitter.emit("event", {
|
|
788
|
+
type: "control",
|
|
789
|
+
run_id: run.id,
|
|
790
|
+
state,
|
|
791
|
+
message,
|
|
792
|
+
occurred_at: new Date().toISOString(),
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
349
796
|
async function updateRun(run, state) {
|
|
350
797
|
run.status = state;
|
|
351
798
|
run.snapshot.state = state;
|
|
352
799
|
run.snapshot.updated_at = new Date().toISOString();
|
|
353
800
|
run.completedAt = TERMINAL_STATES.has(state) ? run.snapshot.updated_at : null;
|
|
801
|
+
if (state === "waiting_human" || state === "pr_ready_for_review") {
|
|
802
|
+
run.controlState = "waiting_human";
|
|
803
|
+
} else if (state === "stopped") {
|
|
804
|
+
run.controlState = "stopped";
|
|
805
|
+
} else if (state === "merge_ready") {
|
|
806
|
+
run.controlState = "complete";
|
|
807
|
+
} else if (TERMINAL_STATES.has(state)) {
|
|
808
|
+
run.controlState = "complete";
|
|
809
|
+
} else if (run.controlState !== "paused" && run.controlState !== "stopping") {
|
|
810
|
+
run.controlState = "running";
|
|
811
|
+
}
|
|
354
812
|
|
|
355
813
|
run.emitter.emit("event", { type: "run", run: toLedgerSnapshot(run.snapshot) });
|
|
356
814
|
const latestEvent = run.snapshot.events.at(-1);
|
|
@@ -395,15 +853,18 @@ async function drainLedgerUpdates(run) {
|
|
|
395
853
|
}
|
|
396
854
|
|
|
397
855
|
async function persistRunStatus(run, update) {
|
|
398
|
-
const url =
|
|
856
|
+
const url = webApiUrl(`runs/${encodeURIComponent(run.id)}/status`, {
|
|
857
|
+
webOrigin: run.apiOrigin,
|
|
858
|
+
apiBaseUrl: run.session.api_base_url,
|
|
859
|
+
});
|
|
399
860
|
try {
|
|
400
861
|
const response = await fetch(url, {
|
|
401
862
|
method: "POST",
|
|
402
863
|
signal: AbortSignal.timeout(5000),
|
|
403
|
-
headers:
|
|
404
|
-
|
|
405
|
-
"Content-Type": "application/json",
|
|
406
|
-
|
|
864
|
+
headers: webApiHeaders(
|
|
865
|
+
{ token: run.session.token },
|
|
866
|
+
{ "Content-Type": "application/json" },
|
|
867
|
+
),
|
|
407
868
|
body: JSON.stringify({
|
|
408
869
|
state: update.state,
|
|
409
870
|
last_agent_output: redactExecutionText(run.lastOutput, { maxChars: 2_000 }).text || null,
|
|
@@ -420,15 +881,32 @@ async function persistRunStatus(run, update) {
|
|
|
420
881
|
}
|
|
421
882
|
|
|
422
883
|
export function toLedgerSnapshot(snapshot) {
|
|
884
|
+
const attachments = snapshot.charter.attachments || [];
|
|
423
885
|
return {
|
|
424
886
|
...snapshot,
|
|
887
|
+
charter: {
|
|
888
|
+
...snapshot.charter,
|
|
889
|
+
attachments: (snapshot.charter.attachments || []).map(attachmentDescriptorForLedger),
|
|
890
|
+
},
|
|
425
891
|
state: toCompatibleCodingLoopState(snapshot.state),
|
|
426
|
-
attempts: snapshot.attempts.map(redactedAttemptForLedger),
|
|
427
|
-
events: snapshot.events.map((item) =>
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
892
|
+
attempts: snapshot.attempts.map((attempt) => redactedAttemptForLedger(attempt, attachments)),
|
|
893
|
+
events: snapshot.events.map((item) => {
|
|
894
|
+
const presentation = summarizeCodingLoopEvent(snapshot, item);
|
|
895
|
+
return {
|
|
896
|
+
...item,
|
|
897
|
+
state: toCompatibleCodingLoopState(item.state),
|
|
898
|
+
detail: redactedEventDetail(
|
|
899
|
+
{
|
|
900
|
+
...item.detail,
|
|
901
|
+
step_summary: presentation.summary,
|
|
902
|
+
step_result: presentation.result,
|
|
903
|
+
next_action: presentation.next,
|
|
904
|
+
},
|
|
905
|
+
item.state,
|
|
906
|
+
attachments,
|
|
907
|
+
),
|
|
908
|
+
};
|
|
909
|
+
}),
|
|
432
910
|
metadata: {
|
|
433
911
|
...(snapshot.metadata || {}),
|
|
434
912
|
pr_lifecycle_state: snapshot.state,
|
|
@@ -440,15 +918,19 @@ export function toLedgerSnapshot(snapshot) {
|
|
|
440
918
|
};
|
|
441
919
|
}
|
|
442
920
|
|
|
443
|
-
function redactedAttemptForLedger(attempt) {
|
|
921
|
+
function redactedAttemptForLedger(attempt, attachments) {
|
|
444
922
|
const instruction = redactExecutionText(attempt.instruction);
|
|
445
923
|
const executorLogs = redactExecutionLogs(attempt.executor_logs);
|
|
446
|
-
const
|
|
924
|
+
const redactedInstruction = redactAttachmentPaths(instruction.text, attachments);
|
|
925
|
+
const redactedExecutorLogs = executorLogs.lines.map((line) =>
|
|
926
|
+
redactAttachmentPaths(line, attachments),
|
|
927
|
+
);
|
|
928
|
+
const logText = redactedExecutorLogs.join("\n");
|
|
447
929
|
return {
|
|
448
930
|
...attempt,
|
|
449
|
-
instruction:
|
|
450
|
-
instruction_sha256: executionTextSha256(
|
|
451
|
-
executor_logs:
|
|
931
|
+
instruction: redactedInstruction,
|
|
932
|
+
instruction_sha256: executionTextSha256(redactedInstruction),
|
|
933
|
+
executor_logs: redactedExecutorLogs,
|
|
452
934
|
executor_logs_sha256: executionTextSha256(logText),
|
|
453
935
|
io_redaction: {
|
|
454
936
|
policy_version: EXECUTION_IO_POLICY_VERSION,
|
|
@@ -462,15 +944,20 @@ function redactedAttemptForLedger(attempt) {
|
|
|
462
944
|
};
|
|
463
945
|
}
|
|
464
946
|
|
|
465
|
-
function redactedEventDetail(value, lifecycleState) {
|
|
947
|
+
function redactedEventDetail(value, lifecycleState, attachments) {
|
|
466
948
|
const detail = value && typeof value === "object" ? value : {};
|
|
467
949
|
const result = {};
|
|
468
950
|
for (const [key, item] of Object.entries(detail)) {
|
|
469
951
|
if (["instruction", "reinstruction", "prompt", "last_agent_output"].includes(key)) {
|
|
470
|
-
result[key] =
|
|
952
|
+
result[key] = redactAttachmentPaths(
|
|
953
|
+
redactExecutionText(item, { maxChars: 24_000 }).text,
|
|
954
|
+
attachments,
|
|
955
|
+
);
|
|
471
956
|
} else if (["executor_logs", "stdout", "stderr"].includes(key)) {
|
|
472
957
|
const logs = Array.isArray(item) ? item : [item];
|
|
473
|
-
result[key] = redactExecutionLogs(logs).lines
|
|
958
|
+
result[key] = redactExecutionLogs(logs).lines.map((line) =>
|
|
959
|
+
redactAttachmentPaths(line, attachments),
|
|
960
|
+
);
|
|
474
961
|
} else {
|
|
475
962
|
result[key] = item;
|
|
476
963
|
}
|
|
@@ -479,6 +966,16 @@ function redactedEventDetail(value, lifecycleState) {
|
|
|
479
966
|
return result;
|
|
480
967
|
}
|
|
481
968
|
|
|
969
|
+
function redactAttachmentPaths(value, attachments) {
|
|
970
|
+
let text = String(value || "");
|
|
971
|
+
for (const attachment of attachments) {
|
|
972
|
+
if (typeof attachment?.local_path === "string" && attachment.local_path) {
|
|
973
|
+
text = text.replaceAll(attachment.local_path, `[LOCAL_ATTACHMENT:${attachment.name}]`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return text;
|
|
977
|
+
}
|
|
978
|
+
|
|
482
979
|
function observedEvidenceForLedger(value) {
|
|
483
980
|
if (!value || typeof value !== "object") return null;
|
|
484
981
|
const local = value.local && typeof value.local === "object" ? value.local : {};
|
|
@@ -530,6 +1027,8 @@ export function toCompatibleCodingLoopState(state) {
|
|
|
530
1027
|
return "waiting_human";
|
|
531
1028
|
case "merge_ready":
|
|
532
1029
|
return "done";
|
|
1030
|
+
case "stopped":
|
|
1031
|
+
return "failed";
|
|
533
1032
|
default:
|
|
534
1033
|
return state;
|
|
535
1034
|
}
|