@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
|
@@ -2,14 +2,67 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
|
|
3
3
|
const MACOS_CODEX_BINARY = "/Applications/ChatGPT.app/Contents/Resources/codex";
|
|
4
4
|
|
|
5
|
-
export
|
|
5
|
+
export const BACKEND_VERSION_POLICIES = Object.freeze({
|
|
6
|
+
codex: Object.freeze({
|
|
7
|
+
label: "Codex",
|
|
8
|
+
minimumVersion: "0.144.0",
|
|
9
|
+
updateCommand: "npm install -g @openai/codex@latest",
|
|
10
|
+
}),
|
|
11
|
+
claude: Object.freeze({
|
|
12
|
+
label: "Claude Code",
|
|
13
|
+
minimumVersion: "2.1.200",
|
|
14
|
+
updateCommand: "claude update",
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export async function resolveBackendExecutable(backend, options = {}) {
|
|
19
|
+
const inspection = await inspectBackendExecutable(backend, options);
|
|
20
|
+
return inspection.available ? inspection.executable : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function inspectBackendExecutable(
|
|
6
24
|
backend,
|
|
7
25
|
{ spawnImpl = spawn, candidates = backendCandidates(backend), timeoutMs = 1_500 } = {},
|
|
8
26
|
) {
|
|
27
|
+
const policy = versionPolicy(backend);
|
|
9
28
|
for (const candidate of [...new Set(candidates.filter(Boolean))]) {
|
|
10
|
-
|
|
29
|
+
const probe = await probeExecutable(candidate, { spawnImpl, timeoutMs });
|
|
30
|
+
if (!probe.available) continue;
|
|
31
|
+
const installedVersion = parseBackendVersion(probe.output);
|
|
32
|
+
if (!installedVersion) {
|
|
33
|
+
return {
|
|
34
|
+
backend,
|
|
35
|
+
executable: candidate,
|
|
36
|
+
available: true,
|
|
37
|
+
compatible: false,
|
|
38
|
+
status: "unknown_version",
|
|
39
|
+
installed_version: null,
|
|
40
|
+
minimum_version: policy.minimumVersion,
|
|
41
|
+
update_command: policy.updateCommand,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const compatible = compareVersions(installedVersion, policy.minimumVersion) >= 0;
|
|
45
|
+
return {
|
|
46
|
+
backend,
|
|
47
|
+
executable: candidate,
|
|
48
|
+
available: true,
|
|
49
|
+
compatible,
|
|
50
|
+
status: compatible ? "compatible" : "outdated",
|
|
51
|
+
installed_version: installedVersion,
|
|
52
|
+
minimum_version: policy.minimumVersion,
|
|
53
|
+
update_command: policy.updateCommand,
|
|
54
|
+
};
|
|
11
55
|
}
|
|
12
|
-
return
|
|
56
|
+
return {
|
|
57
|
+
backend,
|
|
58
|
+
executable: null,
|
|
59
|
+
available: false,
|
|
60
|
+
compatible: false,
|
|
61
|
+
status: "missing",
|
|
62
|
+
installed_version: null,
|
|
63
|
+
minimum_version: policy.minimumVersion,
|
|
64
|
+
update_command: policy.updateCommand,
|
|
65
|
+
};
|
|
13
66
|
}
|
|
14
67
|
|
|
15
68
|
export function backendCandidates(backend) {
|
|
@@ -23,16 +76,49 @@ export function backendCandidates(backend) {
|
|
|
23
76
|
];
|
|
24
77
|
}
|
|
25
78
|
|
|
26
|
-
function
|
|
79
|
+
export function parseBackendVersion(output) {
|
|
80
|
+
const match = String(output || "").match(/(?:^|\s|v)(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?=\s|$|\))/);
|
|
81
|
+
return match?.[1] ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function compareVersions(left, right) {
|
|
85
|
+
const leftParts = numericVersionParts(left);
|
|
86
|
+
const rightParts = numericVersionParts(right);
|
|
87
|
+
for (let index = 0; index < 3; index += 1) {
|
|
88
|
+
if (leftParts[index] !== rightParts[index]) return leftParts[index] - rightParts[index];
|
|
89
|
+
}
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function versionPolicy(backend) {
|
|
94
|
+
return backend === "claude" ? BACKEND_VERSION_POLICIES.claude : BACKEND_VERSION_POLICIES.codex;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function numericVersionParts(value) {
|
|
98
|
+
const match = String(value || "").match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
99
|
+
if (!match) return [0, 0, 0];
|
|
100
|
+
return match.slice(1).map(Number);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function probeExecutable(command, { spawnImpl, timeoutMs }) {
|
|
27
104
|
return new Promise((resolve) => {
|
|
28
105
|
let settled = false;
|
|
29
106
|
let timeout;
|
|
30
|
-
|
|
107
|
+
let output = "";
|
|
108
|
+
const child = spawnImpl(command, ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
109
|
+
child.stdout?.setEncoding?.("utf8");
|
|
110
|
+
child.stderr?.setEncoding?.("utf8");
|
|
111
|
+
child.stdout?.on?.("data", (chunk) => {
|
|
112
|
+
output += chunk;
|
|
113
|
+
});
|
|
114
|
+
child.stderr?.on?.("data", (chunk) => {
|
|
115
|
+
output += chunk;
|
|
116
|
+
});
|
|
31
117
|
const finish = (available) => {
|
|
32
118
|
if (settled) return;
|
|
33
119
|
settled = true;
|
|
34
120
|
if (timeout) clearTimeout(timeout);
|
|
35
|
-
resolve(available);
|
|
121
|
+
resolve({ available, output: output.trim() });
|
|
36
122
|
};
|
|
37
123
|
timeout = setTimeout(() => {
|
|
38
124
|
child.kill?.("SIGTERM");
|
package/src/console/app.js
CHANGED
|
@@ -19,11 +19,12 @@ const TERMINAL_STATES = new Set([
|
|
|
19
19
|
]);
|
|
20
20
|
const NEW_TASK = "__new_task__";
|
|
21
21
|
|
|
22
|
-
export function
|
|
22
|
+
export function AgenticLoopConsole({ agent, onQuit = () => {} }) {
|
|
23
23
|
const { exit } = useApp();
|
|
24
24
|
const { stdout } = useStdout();
|
|
25
25
|
const [terminal, setTerminal] = useState(() => terminalSize(stdout));
|
|
26
26
|
const [runs, setRuns] = useState(() => agent.registry.list());
|
|
27
|
+
const [cliUpdate, setCliUpdate] = useState(() => agent.updates?.snapshot?.() ?? null);
|
|
27
28
|
const [selectedId, setSelectedId] = useState(() => runs[0]?.id ?? NEW_TASK);
|
|
28
29
|
const [inputMode, setInputMode] = useState(() => runs.length === 0);
|
|
29
30
|
const [input, setInput] = useState("");
|
|
@@ -51,6 +52,13 @@ export function CodingLoopConsole({ agent, onQuit = () => {} }) {
|
|
|
51
52
|
return () => agent.registry.off("change", update);
|
|
52
53
|
}, [agent.registry]);
|
|
53
54
|
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!agent.updates?.on) return undefined;
|
|
57
|
+
const update = (nextState) => setCliUpdate(nextState);
|
|
58
|
+
agent.updates.on("change", update);
|
|
59
|
+
return () => agent.updates.off("change", update);
|
|
60
|
+
}, [agent.updates]);
|
|
61
|
+
|
|
54
62
|
useEffect(() => {
|
|
55
63
|
const resize = () => setTerminal(terminalSize(stdout));
|
|
56
64
|
stdout.on("resize", resize);
|
|
@@ -68,7 +76,8 @@ export function CodingLoopConsole({ agent, onQuit = () => {} }) {
|
|
|
68
76
|
1 +
|
|
69
77
|
(waitingForHuman ? 1 : 0) +
|
|
70
78
|
(!selectedRun ? 1 : 0) +
|
|
71
|
-
(notice || busy ? 1 : 0)
|
|
79
|
+
(notice || busy ? 1 : 0) +
|
|
80
|
+
(visibleUpdateNotice(cliUpdate) ? 1 : 0);
|
|
72
81
|
const mainHeight = Math.max(8, terminal.rows - 5 - actionRows);
|
|
73
82
|
const detailInnerWidth = Math.max(20, terminal.columns - 4);
|
|
74
83
|
const detailRows = useMemo(
|
|
@@ -138,7 +147,7 @@ export function CodingLoopConsole({ agent, onQuit = () => {} }) {
|
|
|
138
147
|
}
|
|
139
148
|
if (!selectedRun) {
|
|
140
149
|
if (command.type !== "guidance") {
|
|
141
|
-
setNotice("That command requires a selected
|
|
150
|
+
setNotice("That command requires a selected Agentic Loop run.");
|
|
142
151
|
return;
|
|
143
152
|
}
|
|
144
153
|
setBusy(true);
|
|
@@ -175,12 +184,12 @@ export function CodingLoopConsole({ agent, onQuit = () => {} }) {
|
|
|
175
184
|
setNotice("Guidance queued for the next executor handoff.");
|
|
176
185
|
} else {
|
|
177
186
|
await agent.controls.continue(selectedRun, command.message);
|
|
178
|
-
setNotice("Human guidance accepted. The
|
|
187
|
+
setNotice("Human guidance accepted. The agentic loop is continuing.");
|
|
179
188
|
}
|
|
180
189
|
break;
|
|
181
190
|
case "decision":
|
|
182
191
|
await agent.controls.applyDecision(selectedRun, command);
|
|
183
|
-
setNotice("Human decision recorded. The
|
|
192
|
+
setNotice("Human decision recorded. The agentic loop is continuing.");
|
|
184
193
|
break;
|
|
185
194
|
case "recheck":
|
|
186
195
|
await agent.controls.recheck(selectedRun);
|
|
@@ -259,7 +268,7 @@ export function CodingLoopConsole({ agent, onQuit = () => {} }) {
|
|
|
259
268
|
await agent.controls.selectRepository(selectedRun, match);
|
|
260
269
|
setDetailOpen(true);
|
|
261
270
|
setDetailScrollFromEnd(0);
|
|
262
|
-
setNotice(`Authorized ${match.repository}. The same
|
|
271
|
+
setNotice(`Authorized ${match.repository}. The same Agentic Loop run is continuing.`);
|
|
263
272
|
} else if (selectedRun) {
|
|
264
273
|
setNotice(
|
|
265
274
|
`${selectedRun.request.repository} was not found under ${result.root}. Run /scan <path> for another authorized root.`,
|
|
@@ -290,7 +299,7 @@ export function CodingLoopConsole({ agent, onQuit = () => {} }) {
|
|
|
290
299
|
setRepositoryPicker(null);
|
|
291
300
|
setDetailOpen(true);
|
|
292
301
|
setDetailScrollFromEnd(0);
|
|
293
|
-
setNotice(`Authorized ${selected.repository}. The same
|
|
302
|
+
setNotice(`Authorized ${selected.repository}. The same Agentic Loop run is continuing.`);
|
|
294
303
|
} else {
|
|
295
304
|
const verified = await agent.controls.selectRepository(null, selected);
|
|
296
305
|
setDraft((current) => ({
|
|
@@ -461,7 +470,7 @@ export function CodingLoopConsole({ agent, onQuit = () => {} }) {
|
|
|
461
470
|
if (confirmStop) void execute("/stop");
|
|
462
471
|
else {
|
|
463
472
|
setConfirmStop(true);
|
|
464
|
-
setNotice("Press x again to stop the selected
|
|
473
|
+
setNotice("Press x again to stop the selected Agentic Loop run.");
|
|
465
474
|
}
|
|
466
475
|
return;
|
|
467
476
|
}
|
|
@@ -529,11 +538,14 @@ export function CodingLoopConsole({ agent, onQuit = () => {} }) {
|
|
|
529
538
|
draft,
|
|
530
539
|
repositoryMissing: isRepositoryMissing(selectedRun),
|
|
531
540
|
modalOpen: Boolean(scanPrompt || repositoryPicker),
|
|
541
|
+
cliUpdate,
|
|
532
542
|
}),
|
|
533
543
|
],
|
|
534
544
|
});
|
|
535
545
|
}
|
|
536
546
|
|
|
547
|
+
export const CodingLoopConsole = AgenticLoopConsole;
|
|
548
|
+
|
|
537
549
|
function Header({ agent, runCount }) {
|
|
538
550
|
return jsxs(Box, {
|
|
539
551
|
borderStyle: "single",
|
|
@@ -542,6 +554,12 @@ function Header({ agent, runCount }) {
|
|
|
542
554
|
children: [
|
|
543
555
|
jsx(Text, { bold: true, color: "cyan", children: "SIMY" }),
|
|
544
556
|
jsx(Text, { bold: true, children: " Coding Chat" }),
|
|
557
|
+
agent.updates?.snapshot?.().current_version
|
|
558
|
+
? jsx(Text, {
|
|
559
|
+
dimColor: true,
|
|
560
|
+
children: ` v${agent.updates.snapshot().current_version}`,
|
|
561
|
+
})
|
|
562
|
+
: null,
|
|
545
563
|
jsx(Text, { dimColor: true, children: ` localhost:${agent.port}` }),
|
|
546
564
|
jsx(Spacer, {}),
|
|
547
565
|
jsx(Text, { color: runCount > 0 ? "green" : "yellow", children: `${runCount} runs` }),
|
|
@@ -778,6 +796,7 @@ function ActionBar({
|
|
|
778
796
|
draft,
|
|
779
797
|
repositoryMissing,
|
|
780
798
|
modalOpen,
|
|
799
|
+
cliUpdate,
|
|
781
800
|
}) {
|
|
782
801
|
const waiting = run && (run.controlState === "waiting_human" || run.status === "waiting_human");
|
|
783
802
|
return jsxs(Box, {
|
|
@@ -823,10 +842,29 @@ function ActionBar({
|
|
|
823
842
|
: busy
|
|
824
843
|
? jsx(Text, { color: "cyan", children: "Working..." })
|
|
825
844
|
: null,
|
|
845
|
+
visibleUpdateNotice(cliUpdate)
|
|
846
|
+
? jsx(Text, {
|
|
847
|
+
color: cliUpdate.state === "failed" ? "red" : "yellow",
|
|
848
|
+
children: visibleUpdateNotice(cliUpdate),
|
|
849
|
+
})
|
|
850
|
+
: null,
|
|
826
851
|
],
|
|
827
852
|
});
|
|
828
853
|
}
|
|
829
854
|
|
|
855
|
+
function visibleUpdateNotice(update) {
|
|
856
|
+
if (
|
|
857
|
+
!update ||
|
|
858
|
+
!["update_available", "waiting_for_idle", "updating", "restarting", "failed"].includes(
|
|
859
|
+
update.state,
|
|
860
|
+
)
|
|
861
|
+
) {
|
|
862
|
+
return "";
|
|
863
|
+
}
|
|
864
|
+
const command = update.action?.command ? ` Run: ${update.action.command}` : "";
|
|
865
|
+
return `${update.message || "SIMY CLI update status changed."}${command}`;
|
|
866
|
+
}
|
|
867
|
+
|
|
830
868
|
function initialDraft(agent) {
|
|
831
869
|
const backends = agent.capabilities?.backends ?? {};
|
|
832
870
|
return {
|
package/src/console/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import { render } from "ink";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { AgenticLoopConsole } from "./app.js";
|
|
5
5
|
|
|
6
|
-
export async function
|
|
6
|
+
export async function runAgenticLoopConsole(agent, options = {}) {
|
|
7
7
|
const instance = render(
|
|
8
|
-
jsx(
|
|
8
|
+
jsx(AgenticLoopConsole, {
|
|
9
9
|
agent,
|
|
10
10
|
onQuit: options.onQuit,
|
|
11
11
|
}),
|
|
@@ -21,5 +21,6 @@ export async function runCodingLoopConsole(agent, options = {}) {
|
|
|
21
21
|
await instance.waitUntilExit();
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export
|
|
24
|
+
export const runCodingLoopConsole = runAgenticLoopConsole;
|
|
25
|
+
export { AgenticLoopConsole, CodingLoopConsole } from "./app.js";
|
|
25
26
|
export { consoleHelpLines, parseConsoleCommand } from "./commands.js";
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BACKEND_VERSION_POLICIES,
|
|
5
|
+
inspectBackendExecutable,
|
|
6
|
+
} from "./backend-executable.js";
|
|
7
|
+
|
|
8
|
+
export const DESKTOP_EXECUTION_TARGET = "desktop";
|
|
9
|
+
export const DESKTOP_EXECUTION_LABEL = "Your Desktop";
|
|
10
|
+
|
|
11
|
+
export function normalizeDesktopExecutionTarget(value) {
|
|
12
|
+
const target = String(value ?? "").trim().toLowerCase();
|
|
13
|
+
if (!target || target === DESKTOP_EXECUTION_TARGET) return DESKTOP_EXECUTION_TARGET;
|
|
14
|
+
const error = new Error(
|
|
15
|
+
`SIMY CLI runs only on ${DESKTOP_EXECUTION_LABEL}; execution_target must be desktop.`,
|
|
16
|
+
);
|
|
17
|
+
error.code = "unsupported_execution_target";
|
|
18
|
+
error.executionTarget = target;
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function inspectDesktopExecutor(backend, options = {}) {
|
|
23
|
+
const provider = normalizeBackend(backend);
|
|
24
|
+
const inspection = await inspectBackendExecutable(provider, options);
|
|
25
|
+
return {
|
|
26
|
+
execution_target: DESKTOP_EXECUTION_TARGET,
|
|
27
|
+
execution_label: DESKTOP_EXECUTION_LABEL,
|
|
28
|
+
provider_label: BACKEND_VERSION_POLICIES[provider].label,
|
|
29
|
+
...inspection,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function desktopExecutorCapability(inspection) {
|
|
34
|
+
return {
|
|
35
|
+
execution_target: DESKTOP_EXECUTION_TARGET,
|
|
36
|
+
execution_label: DESKTOP_EXECUTION_LABEL,
|
|
37
|
+
backend: inspection.backend,
|
|
38
|
+
provider_label: inspection.provider_label,
|
|
39
|
+
status: inspection.status,
|
|
40
|
+
available: inspection.available,
|
|
41
|
+
compatible: inspection.compatible,
|
|
42
|
+
installed_version: inspection.installed_version,
|
|
43
|
+
minimum_version: inspection.minimum_version,
|
|
44
|
+
update_command: inspection.update_command,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function resolveDesktopExecutorCommand({
|
|
49
|
+
backend,
|
|
50
|
+
instruction,
|
|
51
|
+
repositoryPath,
|
|
52
|
+
environment = process.env,
|
|
53
|
+
spawnImpl = spawn,
|
|
54
|
+
inspectionOptions = {},
|
|
55
|
+
} = {}) {
|
|
56
|
+
const provider = normalizeBackend(backend);
|
|
57
|
+
const target = normalizeDesktopExecutionTarget(DESKTOP_EXECUTION_TARGET);
|
|
58
|
+
const override =
|
|
59
|
+
provider === "claude" ? environment.SIMY_CLAUDE_COMMAND : environment.SIMY_CODEX_COMMAND;
|
|
60
|
+
if (override) {
|
|
61
|
+
return {
|
|
62
|
+
...shellCommand(override, repositoryPath, instruction, spawnImpl),
|
|
63
|
+
backend: provider,
|
|
64
|
+
execution_target: target,
|
|
65
|
+
verification: "explicit_command_override",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const inspection = await inspectDesktopExecutor(provider, {
|
|
70
|
+
...inspectionOptions,
|
|
71
|
+
spawnImpl: inspectionOptions.spawnImpl ?? spawnImpl,
|
|
72
|
+
});
|
|
73
|
+
if (!inspection.compatible || !inspection.executable) {
|
|
74
|
+
throw desktopExecutorUnavailableError(inspection);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
bin: inspection.executable,
|
|
78
|
+
args:
|
|
79
|
+
provider === "claude"
|
|
80
|
+
? claudeBackendArgs(instruction)
|
|
81
|
+
: ["exec", "--json", instruction],
|
|
82
|
+
env: {},
|
|
83
|
+
spawn: spawnImpl,
|
|
84
|
+
backend: provider,
|
|
85
|
+
execution_target: target,
|
|
86
|
+
verification: "compatible_version",
|
|
87
|
+
installed_version: inspection.installed_version,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function claudeBackendArgs(instruction) {
|
|
92
|
+
return [
|
|
93
|
+
"-p",
|
|
94
|
+
instruction,
|
|
95
|
+
"--output-format",
|
|
96
|
+
"stream-json",
|
|
97
|
+
"--verbose",
|
|
98
|
+
// The Agentic Loop is scoped to a verified checkout and explicitly approved
|
|
99
|
+
// by the user. Print mode cannot display permission prompts, so edits would
|
|
100
|
+
// otherwise be silently unavailable to the desktop executor.
|
|
101
|
+
"--dangerously-skip-permissions",
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeBackend(backend) {
|
|
106
|
+
if (backend === "codex" || backend === "claude") return backend;
|
|
107
|
+
throw new Error("Desktop executor backend must be codex or claude.");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function desktopExecutorUnavailableError(inspection) {
|
|
111
|
+
const provider = inspection.provider_label;
|
|
112
|
+
let message;
|
|
113
|
+
if (inspection.status === "outdated") {
|
|
114
|
+
message = `${provider} ${inspection.installed_version} is too old for ${DESKTOP_EXECUTION_LABEL}. Update to ${inspection.minimum_version} or newer: ${inspection.update_command}`;
|
|
115
|
+
} else if (inspection.status === "unknown_version") {
|
|
116
|
+
message = `${provider} is installed on ${DESKTOP_EXECUTION_LABEL}, but its version could not be verified. Update it and try again: ${inspection.update_command}`;
|
|
117
|
+
} else {
|
|
118
|
+
message = `${provider} is not installed on ${DESKTOP_EXECUTION_LABEL}. Install or update it before starting this run: ${inspection.update_command}`;
|
|
119
|
+
}
|
|
120
|
+
const error = new Error(message);
|
|
121
|
+
error.code = `desktop_executor_${inspection.status}`;
|
|
122
|
+
error.inspection = desktopExecutorCapability(inspection);
|
|
123
|
+
return error;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function shellCommand(command, cwd, instruction, spawnImpl) {
|
|
127
|
+
return {
|
|
128
|
+
bin: process.platform === "win32" ? "cmd.exe" : "sh",
|
|
129
|
+
args: process.platform === "win32" ? ["/d", "/s", "/c", command] : ["-lc", command],
|
|
130
|
+
cwd,
|
|
131
|
+
env: {
|
|
132
|
+
SIMY_AGENTIC_LOOP_REQUIREMENT: instruction,
|
|
133
|
+
SIMY_CODING_LOOP_REQUIREMENT: instruction,
|
|
134
|
+
},
|
|
135
|
+
spawn: spawnImpl,
|
|
136
|
+
};
|
|
137
|
+
}
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,13 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
6
|
import { startAgent } from "./agent.js";
|
|
7
|
+
import {
|
|
8
|
+
autoUpdateHelpText,
|
|
9
|
+
createCliAutoUpdater,
|
|
10
|
+
readCliPackageInfo,
|
|
11
|
+
restartDaemonProcess,
|
|
12
|
+
writeUpdateHandoffFromEnvironment,
|
|
13
|
+
} from "./auto-update.js";
|
|
7
14
|
import { openAuthorizationUrl } from "./browser.js";
|
|
8
15
|
import { DEFAULT_WEB_ORIGIN, resolveWebOrigin } from "./web-origin.js";
|
|
9
16
|
|
|
@@ -18,10 +25,13 @@ if (args.has("--help") || args.has("-h")) {
|
|
|
18
25
|
|
|
19
26
|
Options:
|
|
20
27
|
--daemon, --deamon Run detached in the background
|
|
21
|
-
--no-tui Disable the interactive
|
|
28
|
+
--no-tui Disable the interactive Agentic Loop console
|
|
22
29
|
--no-open Do not open the browser when authorization is required
|
|
23
30
|
--port <port> Bind a specific localhost port
|
|
24
31
|
--host <url> Connect to a SIMY Web origin (default: ${DEFAULT_WEB_ORIGIN})
|
|
32
|
+
--no-auto-update Disable the hourly CLI update check
|
|
33
|
+
|
|
34
|
+
${autoUpdateHelpText()}
|
|
25
35
|
`);
|
|
26
36
|
process.exit(0);
|
|
27
37
|
}
|
|
@@ -29,6 +39,7 @@ Options:
|
|
|
29
39
|
const daemon = args.has("--daemon") || args.has("--deamon");
|
|
30
40
|
const interactive = !daemon && !args.has("--no-tui") && process.stdin.isTTY && process.stdout.isTTY;
|
|
31
41
|
const port = readPort(argv);
|
|
42
|
+
const packageInfo = await readCliPackageInfo();
|
|
32
43
|
let webOrigin;
|
|
33
44
|
try {
|
|
34
45
|
webOrigin = resolveWebOrigin(readOption(argv, "--host"));
|
|
@@ -48,14 +59,39 @@ if (daemon && process.env.SIMY_DAEMON_CHILD !== "1") {
|
|
|
48
59
|
process.exit(0);
|
|
49
60
|
}
|
|
50
61
|
|
|
51
|
-
const
|
|
62
|
+
const updateManager = await createCliAutoUpdater({
|
|
63
|
+
daemon,
|
|
64
|
+
interactive,
|
|
65
|
+
requestedPort: port,
|
|
66
|
+
disabled: args.has("--no-auto-update") || process.env.SIMY_AUTO_UPDATE === "0",
|
|
67
|
+
dependencies: { packageInfo },
|
|
68
|
+
});
|
|
69
|
+
const agent = await startAgent({
|
|
70
|
+
requestedPort: port,
|
|
71
|
+
daemon,
|
|
72
|
+
webOrigin,
|
|
73
|
+
quiet: interactive,
|
|
74
|
+
updateManager,
|
|
75
|
+
});
|
|
76
|
+
await writeUpdateHandoffFromEnvironment({ version: packageInfo.version, port: agent.port });
|
|
77
|
+
updateManager.start({
|
|
78
|
+
registry: agent.registry,
|
|
79
|
+
restartDaemon: async (targetVersion) => {
|
|
80
|
+
await restartDaemonProcess({
|
|
81
|
+
entryPath: fileURLToPath(import.meta.url),
|
|
82
|
+
argv: process.argv.slice(2),
|
|
83
|
+
targetVersion,
|
|
84
|
+
});
|
|
85
|
+
await closeServer(agent.server);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
52
88
|
if (agent.loginUrl && !args.has("--no-open")) {
|
|
53
89
|
await openAuthorizationUrl(agent.loginUrl);
|
|
54
90
|
}
|
|
55
91
|
if (interactive) {
|
|
56
|
-
const {
|
|
92
|
+
const { runAgenticLoopConsole } = await import("./console/index.js");
|
|
57
93
|
try {
|
|
58
|
-
await
|
|
94
|
+
await runAgenticLoopConsole(agent);
|
|
59
95
|
} finally {
|
|
60
96
|
await closeServer(agent.server);
|
|
61
97
|
}
|
package/src/local-attachments.js
CHANGED
|
@@ -7,7 +7,7 @@ export const MAX_ATTACHMENT_COUNT = 5;
|
|
|
7
7
|
export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
8
8
|
export const MAX_ATTACHMENTS_TOTAL_BYTES = 50 * 1024 * 1024;
|
|
9
9
|
const RUN_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
10
|
-
const SAFE_RUN_ID = /^
|
|
10
|
+
const SAFE_RUN_ID = /^(?:agentic_loop|coding_loop)_[A-Za-z0-9_-]+$/;
|
|
11
11
|
const ALLOWED_EXACT_MIME_TYPES = new Set([
|
|
12
12
|
"application/json",
|
|
13
13
|
"application/octet-stream",
|
|
@@ -32,7 +32,7 @@ export function runsRoot(override) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export async function stageRunAttachments({ runId, attachments, manifest, root }) {
|
|
35
|
-
if (!SAFE_RUN_ID.test(runId)) throw new Error("invalid
|
|
35
|
+
if (!SAFE_RUN_ID.test(runId)) throw new Error("invalid agentic loop run_id");
|
|
36
36
|
if (!Array.isArray(attachments) || attachments.length === 0) return [];
|
|
37
37
|
if (attachments.length > MAX_ATTACHMENT_COUNT) {
|
|
38
38
|
throw new Error(`at most ${MAX_ATTACHMENT_COUNT} attachments are allowed`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { stat } from "node:fs/promises";
|
|
1
|
+
import { open, readdir, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
import {
|
|
@@ -7,8 +7,9 @@ import {
|
|
|
7
7
|
hasAllowedPrefix,
|
|
8
8
|
stringArray,
|
|
9
9
|
} from "./shared.js";
|
|
10
|
+
import { evaluateRetryProblemSolving } from "./problem-solving.js";
|
|
10
11
|
|
|
11
|
-
export async function auditAttempt(charter, attempt) {
|
|
12
|
+
export async function auditAttempt(charter, attempt, { previousAttempt = null } = {}) {
|
|
12
13
|
const checks = [];
|
|
13
14
|
const addCheck = (id, label, passed, detail, severity = "blocker", repairability = "auto") => {
|
|
14
15
|
checks.push({ id, label, passed, severity, detail, repairability, evidence_refs: [] });
|
|
@@ -112,6 +113,12 @@ export async function auditAttempt(charter, attempt) {
|
|
|
112
113
|
: "No committed local evidence artifacts reported.",
|
|
113
114
|
);
|
|
114
115
|
|
|
116
|
+
const retryProblemSolving = evaluateRetryProblemSolving(attempt, previousAttempt);
|
|
117
|
+
for (const check of retryProblemSolving.checks) {
|
|
118
|
+
addCheck(check.id, check.label, check.passed, check.detail);
|
|
119
|
+
}
|
|
120
|
+
attempt.retry_problem_solving = retryProblemSolving;
|
|
121
|
+
|
|
115
122
|
const evidenceRequired = charter.expected_evidence.includes("ui_evidence_path");
|
|
116
123
|
const evidenceArtifact = await inspectEvidence(charter, attempt.ui_evidence_path);
|
|
117
124
|
if (evidenceRequired || attempt.ui_evidence_path) {
|
|
@@ -126,6 +133,16 @@ export async function auditAttempt(charter, attempt) {
|
|
|
126
133
|
evidenceArtifact?.summary || "Local evidence path missing.",
|
|
127
134
|
);
|
|
128
135
|
}
|
|
136
|
+
if (charter.evidence_policy?.browser_required) {
|
|
137
|
+
addCheck(
|
|
138
|
+
"browser_visual_evidence_present",
|
|
139
|
+
"Browser screenshot or video evidence is present",
|
|
140
|
+
Boolean(evidenceArtifact?.visual_file_count > 0),
|
|
141
|
+
evidenceArtifact?.visual_file_count > 0
|
|
142
|
+
? `${evidenceArtifact.visual_file_count} screenshot or video artifact(s) found.`
|
|
143
|
+
: "UI and browser-extension work requires a real screenshot or video; a path, text file, or empty directory is not sufficient.",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
129
146
|
|
|
130
147
|
const findings = checks
|
|
131
148
|
.filter((check) => !check.passed)
|
|
@@ -301,17 +318,94 @@ async function inspectEvidence(charter, evidencePath) {
|
|
|
301
318
|
const underRoot = Boolean(
|
|
302
319
|
root && (resolved === root || resolved.startsWith(`${root}${path.sep}`)),
|
|
303
320
|
);
|
|
321
|
+
const directoryEvidence = fileStat?.isDirectory()
|
|
322
|
+
? await scanEvidenceDirectory(resolved)
|
|
323
|
+
: null;
|
|
324
|
+
const directKind = fileStat?.isFile() ? evidenceKind(resolved) : "unknown";
|
|
325
|
+
const visualFileCount = fileStat?.isFile()
|
|
326
|
+
? (await isVisualEvidenceFile(resolved))
|
|
327
|
+
? 1
|
|
328
|
+
: 0
|
|
329
|
+
: directoryEvidence?.visualFileCount ?? 0;
|
|
330
|
+
const sizeBytes = fileStat?.isDirectory()
|
|
331
|
+
? directoryEvidence?.sizeBytes ?? 0
|
|
332
|
+
: fileStat?.size ?? 0;
|
|
304
333
|
return {
|
|
305
334
|
path: resolved,
|
|
306
|
-
kind: fileStat?.isDirectory() ? "directory" :
|
|
335
|
+
kind: fileStat?.isDirectory() ? "directory" : directKind,
|
|
307
336
|
exists: Boolean(fileStat),
|
|
308
337
|
under_required_root: underRoot,
|
|
309
|
-
size_bytes:
|
|
310
|
-
file_count: fileStat?.isFile() ? 1 : 0,
|
|
338
|
+
size_bytes: sizeBytes,
|
|
339
|
+
file_count: fileStat?.isFile() ? 1 : directoryEvidence?.fileCount ?? 0,
|
|
340
|
+
visual_file_count: visualFileCount,
|
|
311
341
|
git_tracked: false,
|
|
312
342
|
summary:
|
|
313
|
-
fileStat && underRoot &&
|
|
314
|
-
?
|
|
343
|
+
fileStat && underRoot && sizeBytes > 0
|
|
344
|
+
? `Evidence exists under the required local root; ${visualFileCount} visual artifact(s) found.`
|
|
315
345
|
: "Evidence is missing, empty, or outside the required local root.",
|
|
316
346
|
};
|
|
317
347
|
}
|
|
348
|
+
|
|
349
|
+
async function scanEvidenceDirectory(directory) {
|
|
350
|
+
const pending = [directory];
|
|
351
|
+
let fileCount = 0;
|
|
352
|
+
let visualFileCount = 0;
|
|
353
|
+
let sizeBytes = 0;
|
|
354
|
+
while (pending.length > 0 && fileCount < 500) {
|
|
355
|
+
const current = pending.shift();
|
|
356
|
+
if (!current) break;
|
|
357
|
+
let entries;
|
|
358
|
+
try {
|
|
359
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
360
|
+
} catch {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
for (const entry of entries) {
|
|
364
|
+
if (entry.isSymbolicLink()) continue;
|
|
365
|
+
const candidate = path.join(current, entry.name);
|
|
366
|
+
if (entry.isDirectory()) {
|
|
367
|
+
pending.push(candidate);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (!entry.isFile()) continue;
|
|
371
|
+
fileCount += 1;
|
|
372
|
+
if (await isVisualEvidenceFile(candidate)) visualFileCount += 1;
|
|
373
|
+
try {
|
|
374
|
+
sizeBytes += (await stat(candidate)).size;
|
|
375
|
+
} catch {
|
|
376
|
+
// A file disappearing during inspection is simply not counted toward its size.
|
|
377
|
+
}
|
|
378
|
+
if (fileCount >= 500) break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return { fileCount, visualFileCount, sizeBytes };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function isVisualEvidenceFile(filePath) {
|
|
385
|
+
if (!["image", "video"].includes(evidenceKind(filePath))) return false;
|
|
386
|
+
const header = Buffer.alloc(16);
|
|
387
|
+
let handle;
|
|
388
|
+
try {
|
|
389
|
+
handle = await open(filePath, "r");
|
|
390
|
+
const { bytesRead } = await handle.read(header, 0, header.length, 0);
|
|
391
|
+
if (bytesRead === 0) return false;
|
|
392
|
+
} catch {
|
|
393
|
+
return false;
|
|
394
|
+
} finally {
|
|
395
|
+
try {
|
|
396
|
+
await handle?.close();
|
|
397
|
+
} catch {
|
|
398
|
+
// The evidence file may disappear while it is being inspected.
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const ascii = header.toString("ascii");
|
|
402
|
+
return (
|
|
403
|
+
header.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) ||
|
|
404
|
+
header.subarray(0, 3).equals(Buffer.from([255, 216, 255])) ||
|
|
405
|
+
ascii.startsWith("GIF87a") ||
|
|
406
|
+
ascii.startsWith("GIF89a") ||
|
|
407
|
+
(ascii.startsWith("RIFF") && ascii.slice(8, 12) === "WEBP") ||
|
|
408
|
+
ascii.slice(4, 8) === "ftyp" ||
|
|
409
|
+
header.subarray(0, 4).equals(Buffer.from([26, 69, 223, 163]))
|
|
410
|
+
);
|
|
411
|
+
}
|