@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
|
@@ -110,7 +110,9 @@ export async function writeRepositoryInventory(
|
|
|
110
110
|
repositories: mergeRepositoryInventory(repositories).map((item) => ({
|
|
111
111
|
repository: item.repository,
|
|
112
112
|
branch: item.branch,
|
|
113
|
+
default_branch: item.default_branch,
|
|
113
114
|
local_path: item.local_path,
|
|
115
|
+
...(item.capabilities?.length ? { capabilities: item.capabilities } : {}),
|
|
114
116
|
})),
|
|
115
117
|
},
|
|
116
118
|
null,
|
|
@@ -126,10 +128,14 @@ export function mergeRepositoryInventory(...inventories) {
|
|
|
126
128
|
const repository = normalizeGitHubRemote(item?.repository);
|
|
127
129
|
const localPath = String(item?.local_path || item?.localPath || "").trim();
|
|
128
130
|
if (!repository || !localPath) continue;
|
|
131
|
+
const capabilities = uniqueCapabilities(item?.capabilities);
|
|
129
132
|
byPath.set(resolve(localPath), {
|
|
130
133
|
repository,
|
|
131
134
|
branch: String(item?.branch || "").trim() || "dev",
|
|
135
|
+
default_branch:
|
|
136
|
+
String(item?.default_branch || item?.defaultBranch || "").trim() || "dev",
|
|
132
137
|
local_path: resolve(localPath),
|
|
138
|
+
...(capabilities.length ? { capabilities } : {}),
|
|
133
139
|
});
|
|
134
140
|
}
|
|
135
141
|
return [...byPath.values()].sort((left, right) =>
|
|
@@ -155,16 +161,114 @@ async function inspectGitRepository(directory) {
|
|
|
155
161
|
]);
|
|
156
162
|
const repository = normalizeGitHubRemote(remote);
|
|
157
163
|
if (!repository) return null;
|
|
164
|
+
const capabilities = await inspectRepositoryCapabilities(directory);
|
|
158
165
|
return {
|
|
159
166
|
repository,
|
|
160
167
|
branch: String(branch || "").trim() || "dev",
|
|
168
|
+
default_branch: await detectDefaultBranch(directory),
|
|
161
169
|
local_path: String(root || "").trim(),
|
|
170
|
+
...(capabilities.length ? { capabilities } : {}),
|
|
162
171
|
};
|
|
163
172
|
} catch {
|
|
164
173
|
return null;
|
|
165
174
|
}
|
|
166
175
|
}
|
|
167
176
|
|
|
177
|
+
async function inspectRepositoryCapabilities(directory) {
|
|
178
|
+
const capabilities = [];
|
|
179
|
+
const rootEntries = await safeDirectoryNames(directory);
|
|
180
|
+
const packageJson = await readJsonFile(join(directory, "package.json"));
|
|
181
|
+
const packageNames = Object.keys({
|
|
182
|
+
...(packageJson?.dependencies || {}),
|
|
183
|
+
...(packageJson?.devDependencies || {}),
|
|
184
|
+
});
|
|
185
|
+
const pythonConfig = await readTextFile(join(directory, "pyproject.toml"));
|
|
186
|
+
const extensionManifest =
|
|
187
|
+
(await readJsonFile(join(directory, "extension", "manifest.json"))) ||
|
|
188
|
+
(await readJsonFile(join(directory, "manifest.json")));
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
packageNames.some((name) => ["next", "react-dom", "vue", "svelte"].includes(name)) ||
|
|
192
|
+
[...rootEntries].some((name) => /^next\.config\./.test(name))
|
|
193
|
+
) {
|
|
194
|
+
capabilities.push("web_ui");
|
|
195
|
+
}
|
|
196
|
+
if ([2, 3].includes(extensionManifest?.manifest_version)) {
|
|
197
|
+
capabilities.push("browser_extension");
|
|
198
|
+
}
|
|
199
|
+
if (rootEntries.has("pubspec.yaml")) capabilities.push("mobile_ui");
|
|
200
|
+
if (rootEntries.has("supabase") || rootEntries.has("migrations")) {
|
|
201
|
+
capabilities.push("database_backend");
|
|
202
|
+
}
|
|
203
|
+
if (
|
|
204
|
+
packageJson?.bin ||
|
|
205
|
+
packageNames.some((name) => ["commander", "ink", "yargs", "oclif"].includes(name))
|
|
206
|
+
) {
|
|
207
|
+
capabilities.push("cli");
|
|
208
|
+
}
|
|
209
|
+
if (/\b(langgraph|langchain|crewai|autogen|openai-agents)\b/i.test(pythonConfig)) {
|
|
210
|
+
capabilities.push("agent_runtime");
|
|
211
|
+
}
|
|
212
|
+
if (
|
|
213
|
+
/\b(fastapi|django|flask)\b/i.test(pythonConfig) ||
|
|
214
|
+
packageNames.some((name) => ["express", "fastify", "hono", "koa"].includes(name))
|
|
215
|
+
) {
|
|
216
|
+
capabilities.push("backend_api");
|
|
217
|
+
}
|
|
218
|
+
return uniqueCapabilities(capabilities);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function safeDirectoryNames(directory) {
|
|
222
|
+
try {
|
|
223
|
+
return new Set((await readdir(directory, { withFileTypes: true })).map((entry) => entry.name));
|
|
224
|
+
} catch {
|
|
225
|
+
return new Set();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function readJsonFile(filePath) {
|
|
230
|
+
try {
|
|
231
|
+
const parsed = JSON.parse(await readFile(filePath, "utf8"));
|
|
232
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function readTextFile(filePath) {
|
|
239
|
+
try {
|
|
240
|
+
return await readFile(filePath, "utf8");
|
|
241
|
+
} catch {
|
|
242
|
+
return "";
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function detectDefaultBranch(directory) {
|
|
247
|
+
try {
|
|
248
|
+
const { stdout } = await execFileAsync(
|
|
249
|
+
"git",
|
|
250
|
+
["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
251
|
+
{ cwd: directory },
|
|
252
|
+
);
|
|
253
|
+
const branch = String(stdout || "").trim().replace(/^origin\//, "");
|
|
254
|
+
if (branch) return branch;
|
|
255
|
+
} catch {
|
|
256
|
+
// Fall through to local remote refs for repositories without origin/HEAD.
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const branch of ["dev", "main", "master"]) {
|
|
260
|
+
try {
|
|
261
|
+
await execFileAsync("git", ["show-ref", "--verify", `refs/remotes/origin/${branch}`], {
|
|
262
|
+
cwd: directory,
|
|
263
|
+
});
|
|
264
|
+
return branch;
|
|
265
|
+
} catch {
|
|
266
|
+
// Try the next conventional default branch.
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return "dev";
|
|
270
|
+
}
|
|
271
|
+
|
|
168
272
|
function shouldSkipDirectory(name) {
|
|
169
273
|
return name.startsWith(".") || SKIPPED_DIRECTORIES.has(name);
|
|
170
274
|
}
|
|
@@ -177,6 +281,19 @@ function uniqueStrings(values) {
|
|
|
177
281
|
return [...new Set((Array.isArray(values) ? values : []).map(String).filter(Boolean))];
|
|
178
282
|
}
|
|
179
283
|
|
|
284
|
+
function uniqueCapabilities(values) {
|
|
285
|
+
const allowed = new Set([
|
|
286
|
+
"web_ui",
|
|
287
|
+
"browser_extension",
|
|
288
|
+
"mobile_ui",
|
|
289
|
+
"database_backend",
|
|
290
|
+
"backend_api",
|
|
291
|
+
"agent_runtime",
|
|
292
|
+
"cli",
|
|
293
|
+
]);
|
|
294
|
+
return [...new Set((Array.isArray(values) ? values : []).filter((value) => allowed.has(value)))].sort();
|
|
295
|
+
}
|
|
296
|
+
|
|
180
297
|
function emptyInventory() {
|
|
181
298
|
return { version: INVENTORY_VERSION, authorizedRoots: [], repositories: [], scannedAt: null };
|
|
182
299
|
}
|
package/src/runner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFile
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
2
|
import { EventEmitter, once } from "node:events";
|
|
3
3
|
import { access } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
@@ -19,8 +19,17 @@ import {
|
|
|
19
19
|
redactExecutionText,
|
|
20
20
|
} from "./orchestrator/execution-io.js";
|
|
21
21
|
import { summarizeCodingLoopEvent } from "./orchestrator/presentation.js";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
22
|
+
import { refreshBudgetState } from "./orchestrator/budget.js";
|
|
23
|
+
import { appendEvent } from "./orchestrator/shared.js";
|
|
24
|
+
import {
|
|
25
|
+
createProviderStreamDecoder,
|
|
26
|
+
isNonFatalProviderDiagnostic,
|
|
27
|
+
} from "./provider-stream.js";
|
|
28
|
+
import {
|
|
29
|
+
claudeBackendArgs,
|
|
30
|
+
normalizeDesktopExecutionTarget,
|
|
31
|
+
resolveDesktopExecutorCommand,
|
|
32
|
+
} from "./desktop-executor.js";
|
|
24
33
|
import {
|
|
25
34
|
attachmentDescriptorForLedger,
|
|
26
35
|
cleanupRunAttachments,
|
|
@@ -67,9 +76,15 @@ export { LocalRunRegistry };
|
|
|
67
76
|
|
|
68
77
|
export function createRun({ runId, request, session, apiOrigin }) {
|
|
69
78
|
const emitter = new EventEmitter();
|
|
79
|
+
const normalizedRequest = {
|
|
80
|
+
...request,
|
|
81
|
+
execution_target: normalizeDesktopExecutionTarget(request?.execution_target),
|
|
82
|
+
execution_device_id:
|
|
83
|
+
String(request?.execution_device_id || session?.device_id || "").trim() || null,
|
|
84
|
+
};
|
|
70
85
|
return {
|
|
71
86
|
id: runId,
|
|
72
|
-
request,
|
|
87
|
+
request: normalizedRequest,
|
|
73
88
|
session,
|
|
74
89
|
apiOrigin,
|
|
75
90
|
status: "queued",
|
|
@@ -88,7 +103,7 @@ export function createRun({ runId, request, session, apiOrigin }) {
|
|
|
88
103
|
ledgerUpdateRunning: false,
|
|
89
104
|
snapshot: createCodingLoopSnapshot({
|
|
90
105
|
runId,
|
|
91
|
-
request,
|
|
106
|
+
request: normalizedRequest,
|
|
92
107
|
metadata: {
|
|
93
108
|
local_orchestrator_version: 1,
|
|
94
109
|
local_output_persisted: false,
|
|
@@ -99,7 +114,7 @@ export function createRun({ runId, request, session, apiOrigin }) {
|
|
|
99
114
|
|
|
100
115
|
export function restoreRun({ snapshot, session, apiOrigin, localPath = null }) {
|
|
101
116
|
if (!snapshot || typeof snapshot !== "object" || !snapshot.charter) {
|
|
102
|
-
throw new Error("Cannot restore an invalid
|
|
117
|
+
throw new Error("Cannot restore an invalid agentic loop snapshot.");
|
|
103
118
|
}
|
|
104
119
|
const charter = snapshot.charter;
|
|
105
120
|
const run = createRun({
|
|
@@ -108,6 +123,9 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null }) {
|
|
|
108
123
|
apiOrigin,
|
|
109
124
|
request: {
|
|
110
125
|
backend: charter.backend === "claude" ? "claude" : "codex",
|
|
126
|
+
execution_target: normalizeDesktopExecutionTarget(charter.execution_target),
|
|
127
|
+
execution_device_id:
|
|
128
|
+
String(snapshot.metadata?.execution_device_id || session?.device_id || "").trim() || null,
|
|
111
129
|
audit_backend:
|
|
112
130
|
charter.audit_backend === "claude" || charter.audit_backend === "codex"
|
|
113
131
|
? charter.audit_backend
|
|
@@ -117,6 +135,8 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null }) {
|
|
|
117
135
|
local_path: localPath,
|
|
118
136
|
base_branch: String(charter.base_branch || "dev"),
|
|
119
137
|
max_attempts: charter.max_attempts,
|
|
138
|
+
retry_budget: charter.retry_budget,
|
|
139
|
+
token_budget: charter.token_budget,
|
|
120
140
|
ui_evidence_root: String(charter.ui_evidence_root || ""),
|
|
121
141
|
acceptance_criteria: Array.isArray(charter.acceptance_criteria)
|
|
122
142
|
? charter.acceptance_criteria
|
|
@@ -129,17 +149,38 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null }) {
|
|
|
129
149
|
require_human_approval: charter.require_human_approval !== false,
|
|
130
150
|
must_not: Array.isArray(charter.must_not) ? charter.must_not : [],
|
|
131
151
|
proposal_id: typeof charter.proposal_id === "string" ? charter.proposal_id : null,
|
|
152
|
+
charter_context: {
|
|
153
|
+
source_charter_id: charter.id,
|
|
154
|
+
audit_backend: charter.audit_backend,
|
|
155
|
+
acceptance_criteria_source: charter.acceptance_criteria_source,
|
|
156
|
+
required_checks: charter.required_checks,
|
|
157
|
+
require_human_approval: charter.require_human_approval,
|
|
158
|
+
risk: charter.risk,
|
|
159
|
+
design_review: charter.design_review,
|
|
160
|
+
evidence_policy: charter.evidence_policy,
|
|
161
|
+
prompt_policy_report: charter.prompt_policy_report,
|
|
162
|
+
original_request: charter.thread_state?.original_request,
|
|
163
|
+
user_goal: charter.thread_state?.user_goal,
|
|
164
|
+
non_goals: charter.thread_state?.non_goals,
|
|
165
|
+
expected_finish_line: charter.thread_state?.expected_finish_line,
|
|
166
|
+
artifacts_required: charter.thread_state?.artifacts_required,
|
|
167
|
+
assumptions: charter.thread_state?.assumptions,
|
|
168
|
+
},
|
|
132
169
|
attachments: [],
|
|
133
170
|
},
|
|
134
171
|
});
|
|
135
172
|
const restoredSnapshot = structuredClone(snapshot);
|
|
173
|
+
restoredSnapshot.charter.retry_budget = run.snapshot.charter.retry_budget;
|
|
174
|
+
restoredSnapshot.charter.token_budget = run.snapshot.charter.token_budget;
|
|
175
|
+
restoredSnapshot.charter.max_attempts = run.snapshot.charter.max_attempts;
|
|
176
|
+
refreshBudgetState(restoredSnapshot);
|
|
136
177
|
const lifecycleState = restoredSnapshot.metadata?.pr_lifecycle_state;
|
|
137
178
|
const persistedState =
|
|
138
179
|
typeof lifecycleState === "string" && RESTORABLE_STATES.has(lifecycleState)
|
|
139
180
|
? lifecycleState
|
|
140
181
|
: restoredSnapshot.state;
|
|
141
182
|
if (!RESTORABLE_STATES.has(persistedState)) {
|
|
142
|
-
throw new Error(`
|
|
183
|
+
throw new Error(`Agentic loop ${run.id} is not in a restorable state.`);
|
|
143
184
|
}
|
|
144
185
|
const restoredFromState = INTERRUPTED_STATES.has(persistedState) ? persistedState : null;
|
|
145
186
|
const restoredState = restoredFromState ? "waiting_human" : persistedState;
|
|
@@ -147,15 +188,15 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null }) {
|
|
|
147
188
|
restoredSnapshot.events = Array.isArray(restoredSnapshot.events)
|
|
148
189
|
? restoredSnapshot.events
|
|
149
190
|
: [];
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
191
|
+
appendEvent(
|
|
192
|
+
restoredSnapshot,
|
|
193
|
+
"waiting_human",
|
|
194
|
+
"Local execution was interrupted and is ready to continue.",
|
|
195
|
+
{
|
|
154
196
|
code: "local_execution_interrupted",
|
|
155
197
|
interrupted_state: restoredFromState,
|
|
156
198
|
},
|
|
157
|
-
|
|
158
|
-
});
|
|
199
|
+
);
|
|
159
200
|
}
|
|
160
201
|
|
|
161
202
|
run.snapshot = restoredSnapshot;
|
|
@@ -190,14 +231,19 @@ export async function startLocalCodingRun(
|
|
|
190
231
|
export async function continueLocalCodingRun(run, guidance, dependencies = {}) {
|
|
191
232
|
const message = String(guidance || "").trim();
|
|
192
233
|
if (!message) throw new Error("Human guidance is required.");
|
|
193
|
-
if (run.operation || run.child) throw new Error("The selected
|
|
234
|
+
if (run.operation || run.child) throw new Error("The selected Agentic Loop run is still active.");
|
|
194
235
|
if (!RESUMABLE_STATES.has(run.status)) {
|
|
195
236
|
throw new Error("Human guidance is available only when the selected run is waiting.");
|
|
196
237
|
}
|
|
197
238
|
|
|
198
239
|
const nextAttempt = run.snapshot.attempts.length + 1;
|
|
199
|
-
|
|
200
|
-
|
|
240
|
+
const budget = refreshBudgetState(run.snapshot);
|
|
241
|
+
if (budget.token_exhausted) {
|
|
242
|
+
throw new Error("The Agentic Loop run used its token budget. Start a new run to continue.");
|
|
243
|
+
}
|
|
244
|
+
if (nextAttempt > run.snapshot.charter.max_attempts) {
|
|
245
|
+
throw new Error("The Agentic Loop run used its retry budget. Start a new run to continue.");
|
|
246
|
+
}
|
|
201
247
|
run.completedAt = null;
|
|
202
248
|
run.stopRequested = false;
|
|
203
249
|
run.controlState = "running";
|
|
@@ -216,7 +262,7 @@ export async function continueLocalCodingRunAfterRepositoryApproval(
|
|
|
216
262
|
if (!approvedRepository || !approvedPath) {
|
|
217
263
|
throw new Error("An approved local repository and path are required.");
|
|
218
264
|
}
|
|
219
|
-
if (run.operation || run.child) throw new Error("The selected
|
|
265
|
+
if (run.operation || run.child) throw new Error("The selected Agentic Loop run is still active.");
|
|
220
266
|
if (run.status !== "waiting_human") {
|
|
221
267
|
throw new Error("Repository approval is available only while the run is waiting.");
|
|
222
268
|
}
|
|
@@ -226,14 +272,9 @@ export async function continueLocalCodingRunAfterRepositoryApproval(
|
|
|
226
272
|
|
|
227
273
|
run.request.local_path = approvedPath;
|
|
228
274
|
run.snapshot.final_audit = null;
|
|
229
|
-
run.snapshot.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
detail: {
|
|
233
|
-
code: "local_repository_authorized",
|
|
234
|
-
repository: approvedRepository,
|
|
235
|
-
},
|
|
236
|
-
occurred_at: new Date().toISOString(),
|
|
275
|
+
appendEvent(run.snapshot, "queued", "Local repository authorization accepted.", {
|
|
276
|
+
code: "local_repository_authorized",
|
|
277
|
+
repository: approvedRepository,
|
|
237
278
|
});
|
|
238
279
|
run.snapshot.state = "queued";
|
|
239
280
|
run.status = "queued";
|
|
@@ -256,7 +297,9 @@ export function isLocalRepositoryApprovalPending(run) {
|
|
|
256
297
|
if (run?.status !== "waiting_human" || run.snapshot?.attempts?.length > 0) return false;
|
|
257
298
|
const events = Array.isArray(run.snapshot?.events) ? run.snapshot.events : [];
|
|
258
299
|
const markerIndex = events.findLastIndex(
|
|
259
|
-
(event) =>
|
|
300
|
+
(event) =>
|
|
301
|
+
event?.detail?.code === "local_repository_not_authorized" ||
|
|
302
|
+
event?.detail?.code === "local_repository_not_found",
|
|
260
303
|
);
|
|
261
304
|
if (markerIndex < 0) return false;
|
|
262
305
|
return events
|
|
@@ -305,7 +348,9 @@ export function pauseLocalCodingRun(run) {
|
|
|
305
348
|
if (process.platform === "win32") {
|
|
306
349
|
throw new Error("Process pause is not supported on Windows; stop the run instead.");
|
|
307
350
|
}
|
|
308
|
-
if (!run.child
|
|
351
|
+
if (!signalExecutorProcess(run.child, "SIGSTOP")) {
|
|
352
|
+
throw new Error("The executor process could not be paused.");
|
|
353
|
+
}
|
|
309
354
|
run.controlState = "paused";
|
|
310
355
|
emitControl(run, "paused", "Executor paused by the local human operator.");
|
|
311
356
|
}
|
|
@@ -316,7 +361,9 @@ export function resumeLocalCodingRun(run) {
|
|
|
316
361
|
if (process.platform === "win32") {
|
|
317
362
|
throw new Error("Process resume is not supported on Windows.");
|
|
318
363
|
}
|
|
319
|
-
if (!run.child
|
|
364
|
+
if (!signalExecutorProcess(run.child, "SIGCONT")) {
|
|
365
|
+
throw new Error("The executor process could not be resumed.");
|
|
366
|
+
}
|
|
320
367
|
run.controlState = "running";
|
|
321
368
|
emitControl(run, "resumed", "Executor resumed by the local human operator.");
|
|
322
369
|
}
|
|
@@ -324,7 +371,9 @@ export function resumeLocalCodingRun(run) {
|
|
|
324
371
|
export async function stopLocalCodingRun(run) {
|
|
325
372
|
if (run.status === "stopped") return run.snapshot;
|
|
326
373
|
if (run.stopRequested) {
|
|
327
|
-
await waitForLocalStop(run);
|
|
374
|
+
const stopped = await waitForLocalStop(run);
|
|
375
|
+
if (!stopped) throw new Error("The local executor did not stop within the safety timeout.");
|
|
376
|
+
await finalizeLocalStop(run);
|
|
328
377
|
return run.snapshot;
|
|
329
378
|
}
|
|
330
379
|
run.stopRequested = true;
|
|
@@ -333,6 +382,8 @@ export async function stopLocalCodingRun(run) {
|
|
|
333
382
|
|
|
334
383
|
const child = run.child;
|
|
335
384
|
if (!child) {
|
|
385
|
+
const stopped = await waitForLocalStop(run);
|
|
386
|
+
if (!stopped) throw new Error("The local executor did not stop within the safety timeout.");
|
|
336
387
|
await finalizeLocalStop(run);
|
|
337
388
|
return run.snapshot;
|
|
338
389
|
}
|
|
@@ -344,14 +395,15 @@ export async function stopLocalCodingRun(run) {
|
|
|
344
395
|
if (outcome === "timeout" && run.child === child) {
|
|
345
396
|
await signalAndWaitForClose(child, "SIGKILL", 750);
|
|
346
397
|
}
|
|
347
|
-
await waitForLocalStop(run);
|
|
398
|
+
const stopped = await waitForLocalStop(run);
|
|
399
|
+
if (!stopped) throw new Error("The local executor did not stop within the safety timeout.");
|
|
348
400
|
await finalizeLocalStop(run);
|
|
349
401
|
return run.snapshot;
|
|
350
402
|
}
|
|
351
403
|
|
|
352
404
|
async function signalAndWaitForClose(child, signal, timeoutMs) {
|
|
353
405
|
const close = once(child, "close").then(() => "closed");
|
|
354
|
-
child
|
|
406
|
+
signalExecutorProcess(child, signal);
|
|
355
407
|
return Promise.race([
|
|
356
408
|
close,
|
|
357
409
|
new Promise((resolve) => setTimeout(() => resolve("timeout"), timeoutMs)),
|
|
@@ -359,21 +411,37 @@ async function signalAndWaitForClose(child, signal, timeoutMs) {
|
|
|
359
411
|
}
|
|
360
412
|
|
|
361
413
|
async function waitForLocalStop(run) {
|
|
362
|
-
if (!run.operation) return;
|
|
363
|
-
|
|
364
|
-
run.operation.
|
|
365
|
-
|
|
414
|
+
if (!run.operation) return true;
|
|
415
|
+
return Promise.race([
|
|
416
|
+
run.operation.then(
|
|
417
|
+
() => true,
|
|
418
|
+
() => true,
|
|
419
|
+
),
|
|
420
|
+
new Promise((resolve) => setTimeout(() => resolve(false), 10_000)),
|
|
366
421
|
]);
|
|
367
422
|
}
|
|
368
423
|
|
|
424
|
+
export function signalExecutorProcess(
|
|
425
|
+
child,
|
|
426
|
+
signal,
|
|
427
|
+
{ killProcess = process.kill, platform = process.platform } = {},
|
|
428
|
+
) {
|
|
429
|
+
if (platform !== "win32" && Number.isInteger(child?.pid)) {
|
|
430
|
+
try {
|
|
431
|
+
killProcess(-child.pid, signal);
|
|
432
|
+
return true;
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if (error?.code !== "ESRCH") return child.kill?.(signal) === true;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return child?.kill?.(signal) === true;
|
|
438
|
+
}
|
|
439
|
+
|
|
369
440
|
async function finalizeLocalStop(run) {
|
|
370
441
|
if (run.status === "stopped") return;
|
|
371
442
|
if (run.snapshot.events.at(-1)?.state !== "stopped") {
|
|
372
|
-
run.snapshot.
|
|
373
|
-
|
|
374
|
-
message: "Coding loop stopped by the local human operator.",
|
|
375
|
-
detail: { source: "local_cli" },
|
|
376
|
-
occurred_at: new Date().toISOString(),
|
|
443
|
+
appendEvent(run.snapshot, "stopped", "Agentic loop stopped by the local human operator.", {
|
|
444
|
+
source: "local_cli",
|
|
377
445
|
});
|
|
378
446
|
}
|
|
379
447
|
await updateRun(run, "stopped");
|
|
@@ -410,11 +478,8 @@ async function runLocalCodingRun(
|
|
|
410
478
|
const message = error instanceof Error ? error.message : "Local repository could not be resolved.";
|
|
411
479
|
run.lastOutput = message;
|
|
412
480
|
run.snapshot.final_audit = preflightAudit(message);
|
|
413
|
-
run.snapshot.
|
|
414
|
-
|
|
415
|
-
message: "Local repository requires configuration.",
|
|
416
|
-
detail: { code: "local_repository_not_found" },
|
|
417
|
-
occurred_at: new Date().toISOString(),
|
|
481
|
+
appendEvent(run.snapshot, "waiting_human", "Local repository requires configuration.", {
|
|
482
|
+
code: "local_repository_not_authorized",
|
|
418
483
|
});
|
|
419
484
|
await cleanupLocalAttachments(run);
|
|
420
485
|
await updateRun(run, "waiting_human");
|
|
@@ -467,23 +532,15 @@ async function runLocalCodingRun(
|
|
|
467
532
|
} catch (error) {
|
|
468
533
|
const message = error instanceof Error ? error.message : "Local orchestration failed.";
|
|
469
534
|
emitOutput(run, message);
|
|
470
|
-
run.snapshot.
|
|
471
|
-
state: "failed",
|
|
472
|
-
message: "Local orchestration failed.",
|
|
473
|
-
detail: { error: message },
|
|
474
|
-
occurred_at: new Date().toISOString(),
|
|
475
|
-
});
|
|
535
|
+
appendEvent(run.snapshot, "failed", "Local orchestration failed.", { error: message });
|
|
476
536
|
await updateRun(run, "failed");
|
|
477
537
|
} finally {
|
|
478
538
|
const cleanupError = await cleanupLocalAttachments(run);
|
|
479
539
|
if (cleanupError) {
|
|
480
540
|
const message = `Local attachment cleanup failed: ${cleanupError}`;
|
|
481
541
|
emitOutput(run, message);
|
|
482
|
-
run.snapshot.
|
|
483
|
-
|
|
484
|
-
message: "Local attachment cleanup failed.",
|
|
485
|
-
detail: { error: cleanupError },
|
|
486
|
-
occurred_at: new Date().toISOString(),
|
|
542
|
+
appendEvent(run.snapshot, "failed", "Local attachment cleanup failed.", {
|
|
543
|
+
error: cleanupError,
|
|
487
544
|
});
|
|
488
545
|
run.snapshot.state = "failed";
|
|
489
546
|
}
|
|
@@ -533,7 +590,11 @@ async function executeProcessAttempt({
|
|
|
533
590
|
marker = "SIMY_RESULT_JSON:",
|
|
534
591
|
attemptNumber = null,
|
|
535
592
|
}) {
|
|
536
|
-
const command = await
|
|
593
|
+
const command = await resolveDesktopExecutorCommand({
|
|
594
|
+
backend,
|
|
595
|
+
instruction,
|
|
596
|
+
repositoryPath,
|
|
597
|
+
});
|
|
537
598
|
const startedAt = new Date().toISOString();
|
|
538
599
|
const stdout = [];
|
|
539
600
|
const stderr = [];
|
|
@@ -546,6 +607,7 @@ async function executeProcessAttempt({
|
|
|
546
607
|
cwd: repositoryPath,
|
|
547
608
|
env: { ...process.env, ...command.env },
|
|
548
609
|
stdio: ["ignore", "pipe", "pipe"],
|
|
610
|
+
detached: process.platform !== "win32",
|
|
549
611
|
});
|
|
550
612
|
run.child = child;
|
|
551
613
|
|
|
@@ -564,7 +626,11 @@ async function executeProcessAttempt({
|
|
|
564
626
|
const stderrDecoder = createProviderStreamDecoder({
|
|
565
627
|
backend,
|
|
566
628
|
stream: "stderr",
|
|
567
|
-
onLine: (line) =>
|
|
629
|
+
onLine: (line) =>
|
|
630
|
+
emitOutput(run, line, {
|
|
631
|
+
backend,
|
|
632
|
+
promote: !isNonFatalProviderDiagnostic(line),
|
|
633
|
+
}),
|
|
568
634
|
});
|
|
569
635
|
const collect = (target, decoder, chunk) => {
|
|
570
636
|
const text = chunk.toString("utf8");
|
|
@@ -608,58 +674,12 @@ async function executeProcessAttempt({
|
|
|
608
674
|
});
|
|
609
675
|
}
|
|
610
676
|
|
|
611
|
-
|
|
612
|
-
if (backend === "claude") {
|
|
613
|
-
const override = process.env.SIMY_CLAUDE_COMMAND;
|
|
614
|
-
if (override) return shellCommand(override, repositoryPath, instruction);
|
|
615
|
-
const binary = (await resolveBackendExecutable("claude")) || "claude";
|
|
616
|
-
return {
|
|
617
|
-
bin: binary,
|
|
618
|
-
args: claudeBackendArgs(instruction),
|
|
619
|
-
env: {},
|
|
620
|
-
spawn,
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
const override = process.env.SIMY_CODEX_COMMAND;
|
|
625
|
-
if (override) return shellCommand(override, repositoryPath, instruction);
|
|
626
|
-
const binary = (await resolveBackendExecutable("codex")) || "codex";
|
|
627
|
-
return {
|
|
628
|
-
bin: binary,
|
|
629
|
-
args: ["exec", "--json", instruction],
|
|
630
|
-
env: {},
|
|
631
|
-
spawn,
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
export function claudeBackendArgs(instruction) {
|
|
636
|
-
return [
|
|
637
|
-
"-p",
|
|
638
|
-
instruction,
|
|
639
|
-
"--output-format",
|
|
640
|
-
"stream-json",
|
|
641
|
-
"--verbose",
|
|
642
|
-
// The Coding Loop is already scoped to a verified checkout and explicitly
|
|
643
|
-
// approved by the user. Print mode cannot display permission prompts, so
|
|
644
|
-
// edits would otherwise be silently unavailable to the executor.
|
|
645
|
-
"--dangerously-skip-permissions",
|
|
646
|
-
];
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
function shellCommand(command, cwd, instruction) {
|
|
650
|
-
return {
|
|
651
|
-
bin: process.platform === "win32" ? "cmd.exe" : "sh",
|
|
652
|
-
args: process.platform === "win32" ? ["/d", "/s", "/c", command] : ["-lc", command],
|
|
653
|
-
cwd,
|
|
654
|
-
env: { SIMY_CODING_LOOP_REQUIREMENT: instruction },
|
|
655
|
-
spawn,
|
|
656
|
-
};
|
|
657
|
-
}
|
|
677
|
+
export { claudeBackendArgs } from "./desktop-executor.js";
|
|
658
678
|
|
|
659
679
|
export async function resolveRepositoryPath(request) {
|
|
660
680
|
const repository = String(request.repository || "").replace(/^https?:\/\/github\.com\//, "").replace(/\.git$/, "");
|
|
661
681
|
const repoName = repository.split("/").filter(Boolean).at(-1);
|
|
662
|
-
if (!repoName) throw new Error("The
|
|
682
|
+
if (!repoName) throw new Error("The agentic loop did not specify a valid GitHub repository.");
|
|
663
683
|
|
|
664
684
|
// An operator-provided root scopes this CLI process to the intended workspace.
|
|
665
685
|
// Persisted inventory paths can outlive a checkout, so only prefer them when no
|
|
@@ -722,11 +742,11 @@ function extractAssistantText(raw) {
|
|
|
722
742
|
return messages.join("\n");
|
|
723
743
|
}
|
|
724
744
|
|
|
725
|
-
function emitOutput(run, text, { backend = run.request.backend } = {}) {
|
|
745
|
+
function emitOutput(run, text, { backend = run.request.backend, promote = true } = {}) {
|
|
726
746
|
for (const line of String(text).split(/\r?\n/)) {
|
|
727
747
|
if (!line.trim()) continue;
|
|
728
748
|
const occurredAt = new Date().toISOString();
|
|
729
|
-
run.lastOutput = line;
|
|
749
|
+
if (promote) run.lastOutput = line;
|
|
730
750
|
run.logs.push({
|
|
731
751
|
text: line,
|
|
732
752
|
backend,
|