@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.
@@ -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, spawn } from "node:child_process";
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 { createProviderStreamDecoder } from "./provider-stream.js";
23
- import { resolveBackendExecutable } from "./backend-executable.js";
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 coding loop snapshot.");
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(`Coding loop ${run.id} is not in a restorable state.`);
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
- restoredSnapshot.events.push({
151
- state: "waiting_human",
152
- message: "Local execution was interrupted and is ready to continue.",
153
- detail: {
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
- occurred_at: new Date().toISOString(),
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 coding run is still active.");
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
- if (nextAttempt > 5) throw new Error("The coding run reached the five-attempt safety limit.");
200
- run.snapshot.charter.max_attempts = Math.max(run.snapshot.charter.max_attempts, nextAttempt);
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 coding run is still active.");
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.events.push({
230
- state: "queued",
231
- message: "Local repository authorization accepted.",
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) => event?.detail?.code === "local_repository_not_found",
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.kill("SIGSTOP")) throw new Error("The executor process could not be paused.");
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.kill("SIGCONT")) throw new Error("The executor process could not be resumed.");
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.kill(signal);
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
- await Promise.race([
364
- run.operation.catch(() => null),
365
- new Promise((resolve) => setTimeout(resolve, 1_500)),
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.events.push({
373
- state: "stopped",
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.events.push({
414
- state: "waiting_human",
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.events.push({
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.events.push({
483
- state: "failed",
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 resolveBackendCommand({ backend, instruction, repositoryPath });
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) => emitOutput(run, line, { backend }),
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
- async function resolveBackendCommand({ backend, instruction, repositoryPath }) {
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 coding loop did not specify a valid GitHub repository.");
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,