@awak-app/simy-cli 0.1.2 → 0.1.3
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/package.json +1 -1
- package/src/agent.js +5 -1
- package/src/orchestrator/loop.js +4 -0
- package/src/provider-stream.js +17 -0
- package/src/repository-inventory.js +30 -0
- package/src/runner.js +53 -15
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -421,6 +421,10 @@ export async function startAgent({
|
|
|
421
421
|
const body = await readJson(req);
|
|
422
422
|
if (body.action === "stop") {
|
|
423
423
|
await stopLocalCodingRun(run);
|
|
424
|
+
if (run.status !== "stopped" || run.controlState !== "stopped") {
|
|
425
|
+
json(res, 500, { error: "local executor stop was not confirmed" });
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
424
428
|
} else if (body.action === "pause") {
|
|
425
429
|
pauseLocalCodingRun(run);
|
|
426
430
|
} else if (body.action === "resume") {
|
|
@@ -545,7 +549,7 @@ export async function startAgent({
|
|
|
545
549
|
repository: selected.repository,
|
|
546
550
|
local_path: selected.local_path,
|
|
547
551
|
root: scan.root,
|
|
548
|
-
state: "
|
|
552
|
+
state: "resuming",
|
|
549
553
|
});
|
|
550
554
|
void continuation.catch((error) => reportRepositoryResumeError(error, quiet));
|
|
551
555
|
return;
|
package/src/orchestrator/loop.js
CHANGED
|
@@ -109,6 +109,7 @@ export async function runCodingLoop({
|
|
|
109
109
|
});
|
|
110
110
|
await publish(snapshot, "collecting_evidence", onUpdate);
|
|
111
111
|
attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
|
|
112
|
+
if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
|
|
112
113
|
|
|
113
114
|
appendEvent(snapshot, "auditing", `Verifying local evidence for attempt ${attemptNumber}.`, {
|
|
114
115
|
attempt_number: attemptNumber,
|
|
@@ -122,6 +123,7 @@ export async function runCodingLoop({
|
|
|
122
123
|
});
|
|
123
124
|
await publish(snapshot, "auditing", onUpdate);
|
|
124
125
|
attempt.audit = await auditAttempt(snapshot.charter, attempt);
|
|
126
|
+
if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
|
|
125
127
|
attempt.implementation_gate = attempt.audit.implementation_gate;
|
|
126
128
|
snapshot.attempts.push(attempt);
|
|
127
129
|
|
|
@@ -162,7 +164,9 @@ export async function runCodingLoop({
|
|
|
162
164
|
|
|
163
165
|
// Re-collect after the read-only auditor to catch any mutated HEAD or working tree.
|
|
164
166
|
attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
|
|
167
|
+
if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
|
|
165
168
|
attempt.audit = await auditAttempt(snapshot.charter, attempt);
|
|
169
|
+
if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
|
|
166
170
|
attempt.implementation_gate = attempt.audit.implementation_gate;
|
|
167
171
|
|
|
168
172
|
const independentFindings = attempt.independent_audit.findings || [];
|
package/src/provider-stream.js
CHANGED
|
@@ -2,6 +2,8 @@ import { stripVTControlCharacters } from "node:util";
|
|
|
2
2
|
|
|
3
3
|
const MAX_LINE_CHARS = 2_000;
|
|
4
4
|
const STRUCTURED_MARKERS = ["SIMY_RESULT_JSON:", "SIMY_AUDIT_JSON:"];
|
|
5
|
+
const CODEX_MCP_AUTH_WARNING =
|
|
6
|
+
"[Codex] MCP warning: optional connector authorization expired; coding continues.";
|
|
5
7
|
|
|
6
8
|
export function createProviderStreamDecoder({ backend, stream = "stdout", onLine, onUsage }) {
|
|
7
9
|
let pending = "";
|
|
@@ -63,6 +65,10 @@ export function formatProviderEvent(backend, event) {
|
|
|
63
65
|
return backend === "claude" ? formatClaudeEvent(event) : formatCodexEvent(event);
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
export function isNonFatalProviderDiagnostic(line) {
|
|
69
|
+
return line === CODEX_MCP_AUTH_WARNING;
|
|
70
|
+
}
|
|
71
|
+
|
|
66
72
|
function formatCodexEvent(event) {
|
|
67
73
|
const prefix = "[Codex]";
|
|
68
74
|
if (event.type === "thread.started") {
|
|
@@ -189,6 +195,9 @@ function contentLines(prefix, label, value) {
|
|
|
189
195
|
|
|
190
196
|
function formatPlainLine(backend, stream, value) {
|
|
191
197
|
const prefix = backend === "claude" ? "[Claude Code]" : "[Codex]";
|
|
198
|
+
if (backend === "codex" && isCodexMcpAuthorizationDiagnostic(value)) {
|
|
199
|
+
return CODEX_MCP_AUTH_WARNING;
|
|
200
|
+
}
|
|
192
201
|
const diagnostic = String(value).match(
|
|
193
202
|
/^\S+\s+(WARN|ERROR|INFO)\s+([\w.-]+(?:::[\w.-]+)*):\s*(.*)$/,
|
|
194
203
|
);
|
|
@@ -199,6 +208,14 @@ function formatPlainLine(backend, stream, value) {
|
|
|
199
208
|
return stream === "stderr" ? `${prefix} stderr: ${value}` : `${prefix} ${value}`;
|
|
200
209
|
}
|
|
201
210
|
|
|
211
|
+
function isCodexMcpAuthorizationDiagnostic(value) {
|
|
212
|
+
const text = String(value || "");
|
|
213
|
+
return (
|
|
214
|
+
/(?:rmcp::|codex_mcp::)/.test(text) &&
|
|
215
|
+
/(?:invalid_grant|AuthorizationRequired|OAuth authorization required)/i.test(text)
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
202
219
|
function toolResultText(value) {
|
|
203
220
|
if (typeof value === "string") return value;
|
|
204
221
|
if (!Array.isArray(value)) return "";
|
|
@@ -110,6 +110,7 @@ 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,
|
|
114
115
|
})),
|
|
115
116
|
},
|
|
@@ -129,6 +130,8 @@ export function mergeRepositoryInventory(...inventories) {
|
|
|
129
130
|
byPath.set(resolve(localPath), {
|
|
130
131
|
repository,
|
|
131
132
|
branch: String(item?.branch || "").trim() || "dev",
|
|
133
|
+
default_branch:
|
|
134
|
+
String(item?.default_branch || item?.defaultBranch || "").trim() || "dev",
|
|
132
135
|
local_path: resolve(localPath),
|
|
133
136
|
});
|
|
134
137
|
}
|
|
@@ -158,6 +161,7 @@ async function inspectGitRepository(directory) {
|
|
|
158
161
|
return {
|
|
159
162
|
repository,
|
|
160
163
|
branch: String(branch || "").trim() || "dev",
|
|
164
|
+
default_branch: await detectDefaultBranch(directory),
|
|
161
165
|
local_path: String(root || "").trim(),
|
|
162
166
|
};
|
|
163
167
|
} catch {
|
|
@@ -165,6 +169,32 @@ async function inspectGitRepository(directory) {
|
|
|
165
169
|
}
|
|
166
170
|
}
|
|
167
171
|
|
|
172
|
+
async function detectDefaultBranch(directory) {
|
|
173
|
+
try {
|
|
174
|
+
const { stdout } = await execFileAsync(
|
|
175
|
+
"git",
|
|
176
|
+
["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
177
|
+
{ cwd: directory },
|
|
178
|
+
);
|
|
179
|
+
const branch = String(stdout || "").trim().replace(/^origin\//, "");
|
|
180
|
+
if (branch) return branch;
|
|
181
|
+
} catch {
|
|
182
|
+
// Fall through to local remote refs for repositories without origin/HEAD.
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const branch of ["dev", "main", "master"]) {
|
|
186
|
+
try {
|
|
187
|
+
await execFileAsync("git", ["show-ref", "--verify", `refs/remotes/origin/${branch}`], {
|
|
188
|
+
cwd: directory,
|
|
189
|
+
});
|
|
190
|
+
return branch;
|
|
191
|
+
} catch {
|
|
192
|
+
// Try the next conventional default branch.
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return "dev";
|
|
196
|
+
}
|
|
197
|
+
|
|
168
198
|
function shouldSkipDirectory(name) {
|
|
169
199
|
return name.startsWith(".") || SKIPPED_DIRECTORIES.has(name);
|
|
170
200
|
}
|
package/src/runner.js
CHANGED
|
@@ -19,7 +19,10 @@ import {
|
|
|
19
19
|
redactExecutionText,
|
|
20
20
|
} from "./orchestrator/execution-io.js";
|
|
21
21
|
import { summarizeCodingLoopEvent } from "./orchestrator/presentation.js";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
createProviderStreamDecoder,
|
|
24
|
+
isNonFatalProviderDiagnostic,
|
|
25
|
+
} from "./provider-stream.js";
|
|
23
26
|
import { resolveBackendExecutable } from "./backend-executable.js";
|
|
24
27
|
import {
|
|
25
28
|
attachmentDescriptorForLedger,
|
|
@@ -256,7 +259,9 @@ export function isLocalRepositoryApprovalPending(run) {
|
|
|
256
259
|
if (run?.status !== "waiting_human" || run.snapshot?.attempts?.length > 0) return false;
|
|
257
260
|
const events = Array.isArray(run.snapshot?.events) ? run.snapshot.events : [];
|
|
258
261
|
const markerIndex = events.findLastIndex(
|
|
259
|
-
(event) =>
|
|
262
|
+
(event) =>
|
|
263
|
+
event?.detail?.code === "local_repository_not_authorized" ||
|
|
264
|
+
event?.detail?.code === "local_repository_not_found",
|
|
260
265
|
);
|
|
261
266
|
if (markerIndex < 0) return false;
|
|
262
267
|
return events
|
|
@@ -305,7 +310,9 @@ export function pauseLocalCodingRun(run) {
|
|
|
305
310
|
if (process.platform === "win32") {
|
|
306
311
|
throw new Error("Process pause is not supported on Windows; stop the run instead.");
|
|
307
312
|
}
|
|
308
|
-
if (!run.child
|
|
313
|
+
if (!signalExecutorProcess(run.child, "SIGSTOP")) {
|
|
314
|
+
throw new Error("The executor process could not be paused.");
|
|
315
|
+
}
|
|
309
316
|
run.controlState = "paused";
|
|
310
317
|
emitControl(run, "paused", "Executor paused by the local human operator.");
|
|
311
318
|
}
|
|
@@ -316,7 +323,9 @@ export function resumeLocalCodingRun(run) {
|
|
|
316
323
|
if (process.platform === "win32") {
|
|
317
324
|
throw new Error("Process resume is not supported on Windows.");
|
|
318
325
|
}
|
|
319
|
-
if (!run.child
|
|
326
|
+
if (!signalExecutorProcess(run.child, "SIGCONT")) {
|
|
327
|
+
throw new Error("The executor process could not be resumed.");
|
|
328
|
+
}
|
|
320
329
|
run.controlState = "running";
|
|
321
330
|
emitControl(run, "resumed", "Executor resumed by the local human operator.");
|
|
322
331
|
}
|
|
@@ -324,7 +333,9 @@ export function resumeLocalCodingRun(run) {
|
|
|
324
333
|
export async function stopLocalCodingRun(run) {
|
|
325
334
|
if (run.status === "stopped") return run.snapshot;
|
|
326
335
|
if (run.stopRequested) {
|
|
327
|
-
await waitForLocalStop(run);
|
|
336
|
+
const stopped = await waitForLocalStop(run);
|
|
337
|
+
if (!stopped) throw new Error("The local executor did not stop within the safety timeout.");
|
|
338
|
+
await finalizeLocalStop(run);
|
|
328
339
|
return run.snapshot;
|
|
329
340
|
}
|
|
330
341
|
run.stopRequested = true;
|
|
@@ -333,6 +344,8 @@ export async function stopLocalCodingRun(run) {
|
|
|
333
344
|
|
|
334
345
|
const child = run.child;
|
|
335
346
|
if (!child) {
|
|
347
|
+
const stopped = await waitForLocalStop(run);
|
|
348
|
+
if (!stopped) throw new Error("The local executor did not stop within the safety timeout.");
|
|
336
349
|
await finalizeLocalStop(run);
|
|
337
350
|
return run.snapshot;
|
|
338
351
|
}
|
|
@@ -344,14 +357,15 @@ export async function stopLocalCodingRun(run) {
|
|
|
344
357
|
if (outcome === "timeout" && run.child === child) {
|
|
345
358
|
await signalAndWaitForClose(child, "SIGKILL", 750);
|
|
346
359
|
}
|
|
347
|
-
await waitForLocalStop(run);
|
|
360
|
+
const stopped = await waitForLocalStop(run);
|
|
361
|
+
if (!stopped) throw new Error("The local executor did not stop within the safety timeout.");
|
|
348
362
|
await finalizeLocalStop(run);
|
|
349
363
|
return run.snapshot;
|
|
350
364
|
}
|
|
351
365
|
|
|
352
366
|
async function signalAndWaitForClose(child, signal, timeoutMs) {
|
|
353
367
|
const close = once(child, "close").then(() => "closed");
|
|
354
|
-
child
|
|
368
|
+
signalExecutorProcess(child, signal);
|
|
355
369
|
return Promise.race([
|
|
356
370
|
close,
|
|
357
371
|
new Promise((resolve) => setTimeout(() => resolve("timeout"), timeoutMs)),
|
|
@@ -359,13 +373,32 @@ async function signalAndWaitForClose(child, signal, timeoutMs) {
|
|
|
359
373
|
}
|
|
360
374
|
|
|
361
375
|
async function waitForLocalStop(run) {
|
|
362
|
-
if (!run.operation) return;
|
|
363
|
-
|
|
364
|
-
run.operation.
|
|
365
|
-
|
|
376
|
+
if (!run.operation) return true;
|
|
377
|
+
return Promise.race([
|
|
378
|
+
run.operation.then(
|
|
379
|
+
() => true,
|
|
380
|
+
() => true,
|
|
381
|
+
),
|
|
382
|
+
new Promise((resolve) => setTimeout(() => resolve(false), 10_000)),
|
|
366
383
|
]);
|
|
367
384
|
}
|
|
368
385
|
|
|
386
|
+
export function signalExecutorProcess(
|
|
387
|
+
child,
|
|
388
|
+
signal,
|
|
389
|
+
{ killProcess = process.kill, platform = process.platform } = {},
|
|
390
|
+
) {
|
|
391
|
+
if (platform !== "win32" && Number.isInteger(child?.pid)) {
|
|
392
|
+
try {
|
|
393
|
+
killProcess(-child.pid, signal);
|
|
394
|
+
return true;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (error?.code !== "ESRCH") return child.kill?.(signal) === true;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return child?.kill?.(signal) === true;
|
|
400
|
+
}
|
|
401
|
+
|
|
369
402
|
async function finalizeLocalStop(run) {
|
|
370
403
|
if (run.status === "stopped") return;
|
|
371
404
|
if (run.snapshot.events.at(-1)?.state !== "stopped") {
|
|
@@ -413,7 +446,7 @@ async function runLocalCodingRun(
|
|
|
413
446
|
run.snapshot.events.push({
|
|
414
447
|
state: "waiting_human",
|
|
415
448
|
message: "Local repository requires configuration.",
|
|
416
|
-
detail: { code: "
|
|
449
|
+
detail: { code: "local_repository_not_authorized" },
|
|
417
450
|
occurred_at: new Date().toISOString(),
|
|
418
451
|
});
|
|
419
452
|
await cleanupLocalAttachments(run);
|
|
@@ -546,6 +579,7 @@ async function executeProcessAttempt({
|
|
|
546
579
|
cwd: repositoryPath,
|
|
547
580
|
env: { ...process.env, ...command.env },
|
|
548
581
|
stdio: ["ignore", "pipe", "pipe"],
|
|
582
|
+
detached: process.platform !== "win32",
|
|
549
583
|
});
|
|
550
584
|
run.child = child;
|
|
551
585
|
|
|
@@ -564,7 +598,11 @@ async function executeProcessAttempt({
|
|
|
564
598
|
const stderrDecoder = createProviderStreamDecoder({
|
|
565
599
|
backend,
|
|
566
600
|
stream: "stderr",
|
|
567
|
-
onLine: (line) =>
|
|
601
|
+
onLine: (line) =>
|
|
602
|
+
emitOutput(run, line, {
|
|
603
|
+
backend,
|
|
604
|
+
promote: !isNonFatalProviderDiagnostic(line),
|
|
605
|
+
}),
|
|
568
606
|
});
|
|
569
607
|
const collect = (target, decoder, chunk) => {
|
|
570
608
|
const text = chunk.toString("utf8");
|
|
@@ -722,11 +760,11 @@ function extractAssistantText(raw) {
|
|
|
722
760
|
return messages.join("\n");
|
|
723
761
|
}
|
|
724
762
|
|
|
725
|
-
function emitOutput(run, text, { backend = run.request.backend } = {}) {
|
|
763
|
+
function emitOutput(run, text, { backend = run.request.backend, promote = true } = {}) {
|
|
726
764
|
for (const line of String(text).split(/\r?\n/)) {
|
|
727
765
|
if (!line.trim()) continue;
|
|
728
766
|
const occurredAt = new Date().toISOString();
|
|
729
|
-
run.lastOutput = line;
|
|
767
|
+
if (promote) run.lastOutput = line;
|
|
730
768
|
run.logs.push({
|
|
731
769
|
text: line,
|
|
732
770
|
backend,
|