@awak-app/simy-cli 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,11 +19,17 @@ import {
19
19
  redactExecutionText,
20
20
  } from "./orchestrator/execution-io.js";
21
21
  import { summarizeCodingLoopEvent } from "./orchestrator/presentation.js";
22
+ import { refreshBudgetState } from "./orchestrator/budget.js";
23
+ import { appendEvent } from "./orchestrator/shared.js";
22
24
  import {
23
25
  createProviderStreamDecoder,
24
26
  isNonFatalProviderDiagnostic,
25
27
  } from "./provider-stream.js";
26
- import { resolveBackendExecutable } from "./backend-executable.js";
28
+ import {
29
+ claudeBackendArgs,
30
+ normalizeDesktopExecutionTarget,
31
+ resolveDesktopExecutorCommand,
32
+ } from "./desktop-executor.js";
27
33
  import {
28
34
  attachmentDescriptorForLedger,
29
35
  cleanupRunAttachments,
@@ -70,9 +76,15 @@ export { LocalRunRegistry };
70
76
 
71
77
  export function createRun({ runId, request, session, apiOrigin }) {
72
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
+ };
73
85
  return {
74
86
  id: runId,
75
- request,
87
+ request: normalizedRequest,
76
88
  session,
77
89
  apiOrigin,
78
90
  status: "queued",
@@ -91,7 +103,7 @@ export function createRun({ runId, request, session, apiOrigin }) {
91
103
  ledgerUpdateRunning: false,
92
104
  snapshot: createCodingLoopSnapshot({
93
105
  runId,
94
- request,
106
+ request: normalizedRequest,
95
107
  metadata: {
96
108
  local_orchestrator_version: 1,
97
109
  local_output_persisted: false,
@@ -102,7 +114,7 @@ export function createRun({ runId, request, session, apiOrigin }) {
102
114
 
103
115
  export function restoreRun({ snapshot, session, apiOrigin, localPath = null }) {
104
116
  if (!snapshot || typeof snapshot !== "object" || !snapshot.charter) {
105
- throw new Error("Cannot restore an invalid coding loop snapshot.");
117
+ throw new Error("Cannot restore an invalid agentic loop snapshot.");
106
118
  }
107
119
  const charter = snapshot.charter;
108
120
  const run = createRun({
@@ -111,6 +123,9 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null }) {
111
123
  apiOrigin,
112
124
  request: {
113
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,
114
129
  audit_backend:
115
130
  charter.audit_backend === "claude" || charter.audit_backend === "codex"
116
131
  ? charter.audit_backend
@@ -120,6 +135,8 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null }) {
120
135
  local_path: localPath,
121
136
  base_branch: String(charter.base_branch || "dev"),
122
137
  max_attempts: charter.max_attempts,
138
+ retry_budget: charter.retry_budget,
139
+ token_budget: charter.token_budget,
123
140
  ui_evidence_root: String(charter.ui_evidence_root || ""),
124
141
  acceptance_criteria: Array.isArray(charter.acceptance_criteria)
125
142
  ? charter.acceptance_criteria
@@ -132,17 +149,38 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null }) {
132
149
  require_human_approval: charter.require_human_approval !== false,
133
150
  must_not: Array.isArray(charter.must_not) ? charter.must_not : [],
134
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
+ },
135
169
  attachments: [],
136
170
  },
137
171
  });
138
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);
139
177
  const lifecycleState = restoredSnapshot.metadata?.pr_lifecycle_state;
140
178
  const persistedState =
141
179
  typeof lifecycleState === "string" && RESTORABLE_STATES.has(lifecycleState)
142
180
  ? lifecycleState
143
181
  : restoredSnapshot.state;
144
182
  if (!RESTORABLE_STATES.has(persistedState)) {
145
- 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.`);
146
184
  }
147
185
  const restoredFromState = INTERRUPTED_STATES.has(persistedState) ? persistedState : null;
148
186
  const restoredState = restoredFromState ? "waiting_human" : persistedState;
@@ -150,15 +188,15 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null }) {
150
188
  restoredSnapshot.events = Array.isArray(restoredSnapshot.events)
151
189
  ? restoredSnapshot.events
152
190
  : [];
153
- restoredSnapshot.events.push({
154
- state: "waiting_human",
155
- message: "Local execution was interrupted and is ready to continue.",
156
- detail: {
191
+ appendEvent(
192
+ restoredSnapshot,
193
+ "waiting_human",
194
+ "Local execution was interrupted and is ready to continue.",
195
+ {
157
196
  code: "local_execution_interrupted",
158
197
  interrupted_state: restoredFromState,
159
198
  },
160
- occurred_at: new Date().toISOString(),
161
- });
199
+ );
162
200
  }
163
201
 
164
202
  run.snapshot = restoredSnapshot;
@@ -193,14 +231,19 @@ export async function startLocalCodingRun(
193
231
  export async function continueLocalCodingRun(run, guidance, dependencies = {}) {
194
232
  const message = String(guidance || "").trim();
195
233
  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.");
234
+ if (run.operation || run.child) throw new Error("The selected Agentic Loop run is still active.");
197
235
  if (!RESUMABLE_STATES.has(run.status)) {
198
236
  throw new Error("Human guidance is available only when the selected run is waiting.");
199
237
  }
200
238
 
201
239
  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);
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
+ }
204
247
  run.completedAt = null;
205
248
  run.stopRequested = false;
206
249
  run.controlState = "running";
@@ -219,7 +262,7 @@ export async function continueLocalCodingRunAfterRepositoryApproval(
219
262
  if (!approvedRepository || !approvedPath) {
220
263
  throw new Error("An approved local repository and path are required.");
221
264
  }
222
- 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.");
223
266
  if (run.status !== "waiting_human") {
224
267
  throw new Error("Repository approval is available only while the run is waiting.");
225
268
  }
@@ -229,14 +272,9 @@ export async function continueLocalCodingRunAfterRepositoryApproval(
229
272
 
230
273
  run.request.local_path = approvedPath;
231
274
  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(),
275
+ appendEvent(run.snapshot, "queued", "Local repository authorization accepted.", {
276
+ code: "local_repository_authorized",
277
+ repository: approvedRepository,
240
278
  });
241
279
  run.snapshot.state = "queued";
242
280
  run.status = "queued";
@@ -402,11 +440,8 @@ export function signalExecutorProcess(
402
440
  async function finalizeLocalStop(run) {
403
441
  if (run.status === "stopped") return;
404
442
  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(),
443
+ appendEvent(run.snapshot, "stopped", "Agentic loop stopped by the local human operator.", {
444
+ source: "local_cli",
410
445
  });
411
446
  }
412
447
  await updateRun(run, "stopped");
@@ -443,11 +478,8 @@ async function runLocalCodingRun(
443
478
  const message = error instanceof Error ? error.message : "Local repository could not be resolved.";
444
479
  run.lastOutput = message;
445
480
  run.snapshot.final_audit = preflightAudit(message);
446
- run.snapshot.events.push({
447
- state: "waiting_human",
448
- message: "Local repository requires configuration.",
449
- detail: { code: "local_repository_not_authorized" },
450
- occurred_at: new Date().toISOString(),
481
+ appendEvent(run.snapshot, "waiting_human", "Local repository requires configuration.", {
482
+ code: "local_repository_not_authorized",
451
483
  });
452
484
  await cleanupLocalAttachments(run);
453
485
  await updateRun(run, "waiting_human");
@@ -500,23 +532,15 @@ async function runLocalCodingRun(
500
532
  } catch (error) {
501
533
  const message = error instanceof Error ? error.message : "Local orchestration failed.";
502
534
  emitOutput(run, message);
503
- run.snapshot.events.push({
504
- state: "failed",
505
- message: "Local orchestration failed.",
506
- detail: { error: message },
507
- occurred_at: new Date().toISOString(),
508
- });
535
+ appendEvent(run.snapshot, "failed", "Local orchestration failed.", { error: message });
509
536
  await updateRun(run, "failed");
510
537
  } finally {
511
538
  const cleanupError = await cleanupLocalAttachments(run);
512
539
  if (cleanupError) {
513
540
  const message = `Local attachment cleanup failed: ${cleanupError}`;
514
541
  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(),
542
+ appendEvent(run.snapshot, "failed", "Local attachment cleanup failed.", {
543
+ error: cleanupError,
520
544
  });
521
545
  run.snapshot.state = "failed";
522
546
  }
@@ -566,7 +590,11 @@ async function executeProcessAttempt({
566
590
  marker = "SIMY_RESULT_JSON:",
567
591
  attemptNumber = null,
568
592
  }) {
569
- const command = await resolveBackendCommand({ backend, instruction, repositoryPath });
593
+ const command = await resolveDesktopExecutorCommand({
594
+ backend,
595
+ instruction,
596
+ repositoryPath,
597
+ });
570
598
  const startedAt = new Date().toISOString();
571
599
  const stdout = [];
572
600
  const stderr = [];
@@ -646,58 +674,12 @@ async function executeProcessAttempt({
646
674
  });
647
675
  }
648
676
 
649
- async function resolveBackendCommand({ backend, instruction, repositoryPath }) {
650
- if (backend === "claude") {
651
- const override = process.env.SIMY_CLAUDE_COMMAND;
652
- if (override) return shellCommand(override, repositoryPath, instruction);
653
- const binary = (await resolveBackendExecutable("claude")) || "claude";
654
- return {
655
- bin: binary,
656
- args: claudeBackendArgs(instruction),
657
- env: {},
658
- spawn,
659
- };
660
- }
661
-
662
- const override = process.env.SIMY_CODEX_COMMAND;
663
- if (override) return shellCommand(override, repositoryPath, instruction);
664
- const binary = (await resolveBackendExecutable("codex")) || "codex";
665
- return {
666
- bin: binary,
667
- args: ["exec", "--json", instruction],
668
- env: {},
669
- spawn,
670
- };
671
- }
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
-
687
- function shellCommand(command, cwd, instruction) {
688
- return {
689
- bin: process.platform === "win32" ? "cmd.exe" : "sh",
690
- args: process.platform === "win32" ? ["/d", "/s", "/c", command] : ["-lc", command],
691
- cwd,
692
- env: { SIMY_CODING_LOOP_REQUIREMENT: instruction },
693
- spawn,
694
- };
695
- }
677
+ export { claudeBackendArgs } from "./desktop-executor.js";
696
678
 
697
679
  export async function resolveRepositoryPath(request) {
698
680
  const repository = String(request.repository || "").replace(/^https?:\/\/github\.com\//, "").replace(/\.git$/, "");
699
681
  const repoName = repository.split("/").filter(Boolean).at(-1);
700
- 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.");
701
683
 
702
684
  // An operator-provided root scopes this CLI process to the intended workspace.
703
685
  // Persisted inventory paths can outlive a checkout, so only prefer them when no