@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/README.md +75 -7
- package/package.json +16 -4
- package/src/agent.js +306 -33
- package/src/auto-update.js +631 -0
- package/src/backend-executable.js +92 -6
- package/src/console/app.js +46 -8
- package/src/console/index.js +5 -4
- package/src/desktop-executor.js +137 -0
- package/src/index.js +40 -4
- 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 +181 -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/repository-inventory.js +87 -0
- package/src/runner.js +76 -94
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,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 {
|
|
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
|
|
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(`
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
203
|
-
|
|
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
|
|
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.
|
|
233
|
-
|
|
234
|
-
|
|
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.
|
|
406
|
-
|
|
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.
|
|
447
|
-
|
|
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.
|
|
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.
|
|
516
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|