@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Local SIMY coding-loop agent for Codex and Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {
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: "starting",
552
+ state: "resuming",
549
553
  });
550
554
  void continuation.catch((error) => reportRepositoryResumeError(error, quiet));
551
555
  return;
@@ -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 || [];
@@ -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 { createProviderStreamDecoder } from "./provider-stream.js";
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) => event?.detail?.code === "local_repository_not_found",
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.kill("SIGSTOP")) throw new Error("The executor process could not be paused.");
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.kill("SIGCONT")) throw new Error("The executor process could not be resumed.");
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.kill(signal);
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
- await Promise.race([
364
- run.operation.catch(() => null),
365
- new Promise((resolve) => setTimeout(resolve, 1_500)),
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: "local_repository_not_found" },
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) => emitOutput(run, line, { backend }),
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,