gnhf 0.1.26 → 0.1.28

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.
Files changed (3) hide show
  1. package/README.md +55 -26
  2. package/dist/cli.mjs +789 -69
  3. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -15,7 +15,9 @@ const AGENT_NAMES = [
15
15
  "claude",
16
16
  "codex",
17
17
  "rovodev",
18
- "opencode"
18
+ "opencode",
19
+ "copilot",
20
+ "pi"
19
21
  ];
20
22
  const DEFAULT_CONFIG = {
21
23
  agent: "claude",
@@ -25,6 +27,10 @@ const DEFAULT_CONFIG = {
25
27
  preventSleep: true
26
28
  };
27
29
  var InvalidConfigError = class extends Error {};
30
+ function formatAgentNameList() {
31
+ const quoted = AGENT_NAMES.map((name) => `"${name}"`);
32
+ return `${quoted.slice(0, -1).join(", ")}, or ${quoted[quoted.length - 1]}`;
33
+ }
28
34
  function normalizePreventSleep(value) {
29
35
  if (typeof value === "boolean") return value;
30
36
  if (typeof value !== "string") return void 0;
@@ -39,6 +45,8 @@ function isReservedAgentArg(agent, arg) {
39
45
  case "codex": return arg === "exec" || arg === "--json" || arg === "--output-schema" || arg.startsWith("--output-schema=") || arg === "--color" || arg.startsWith("--color=");
40
46
  case "opencode": return arg === "serve" || arg === "--hostname" || arg.startsWith("--hostname=") || arg === "--port" || arg.startsWith("--port=") || arg === "--print-logs";
41
47
  case "rovodev": return arg === "rovodev" || arg === "serve" || arg === "--disable-session-token";
48
+ case "copilot": return arg === "-p" || arg === "--prompt" || arg.startsWith("--prompt=") || arg === "-i" || arg === "--interactive" || arg.startsWith("--interactive=") || arg === "-s" || arg === "--silent" || arg === "--output-format" || arg.startsWith("--output-format=") || arg === "--stream" || arg.startsWith("--stream=") || arg === "--no-color" || arg === "--share" || arg.startsWith("--share=") || arg === "--share-gist";
49
+ case "pi": return arg === "--mode" || arg.startsWith("--mode=") || arg === "--print" || arg === "-p" || arg === "--continue" || arg === "-c" || arg === "--resume" || arg === "-r" || arg === "--session" || arg.startsWith("--session=") || arg === "--fork" || arg.startsWith("--fork=") || arg === "--session-dir" || arg.startsWith("--session-dir=") || arg === "--no-session" || arg === "--export" || arg.startsWith("--export=") || arg === "--list-models" || arg.startsWith("--list-models=") || arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v" || arg === "--api-key" || arg.startsWith("--api-key=");
42
50
  }
43
51
  }
44
52
  /**
@@ -62,7 +70,7 @@ function normalizeAgentPathOverride(value, configDir) {
62
70
  const validNames = new Set(AGENT_NAMES);
63
71
  const result = {};
64
72
  for (const [key, val] of Object.entries(value)) {
65
- if (!validNames.has(key)) throw new InvalidConfigError(`Invalid agent name in agentPathOverride: "${key}". Use "claude", "codex", "rovodev", or "opencode".`);
73
+ if (!validNames.has(key)) throw new InvalidConfigError(`Invalid agent name in agentPathOverride: "${key}". Use ${formatAgentNameList()}.`);
66
74
  if (typeof val !== "string") throw new InvalidConfigError(`Invalid path for agentPathOverride.${key}: expected a string`);
67
75
  if (val.trim() === "") throw new InvalidConfigError(`Invalid path for agentPathOverride.${key}: expected a non-empty string`);
68
76
  result[key] = resolveConfigPath(val, configDir);
@@ -86,7 +94,7 @@ function normalizeAgentArgsOverride(value) {
86
94
  const validNames = new Set(AGENT_NAMES);
87
95
  const result = {};
88
96
  for (const [key, rawConfig] of Object.entries(value)) {
89
- if (!validNames.has(key)) throw new InvalidConfigError(`Invalid agent name in agentArgsOverride: "${key}". Use "claude", "codex", "rovodev", or "opencode".`);
97
+ if (!validNames.has(key)) throw new InvalidConfigError(`Invalid agent name in agentArgsOverride: "${key}". Use ${formatAgentNameList()}.`);
90
98
  const args = normalizeAgentExtraArgs(rawConfig, `agentArgsOverride.${key}`, key);
91
99
  if (args !== void 0) result[key] = args;
92
100
  }
@@ -151,6 +159,8 @@ function serializeConfig(config) {
151
159
  "# agentPathOverride:",
152
160
  "# claude: /path/to/custom-claude",
153
161
  "# codex: /path/to/custom-codex",
162
+ "# copilot: /path/to/custom-copilot",
163
+ "# pi: /path/to/custom-pi",
154
164
  "",
155
165
  "# Per-agent CLI arg overrides (optional)",
156
166
  "# agentArgsOverride:",
@@ -159,7 +169,17 @@ function serializeConfig(config) {
159
169
  "# - gpt-5.4",
160
170
  "# - -c",
161
171
  "# - model_reasoning_effort=\"high\"",
162
- "# - --full-auto"
172
+ "# - --full-auto",
173
+ "# copilot:",
174
+ "# - --model",
175
+ "# - gpt-5.4",
176
+ "# pi:",
177
+ "# - --provider",
178
+ "# - openai-codex",
179
+ "# - --model",
180
+ "# - gpt-5.5",
181
+ "# - --thinking",
182
+ "# - high"
163
183
  ];
164
184
  if (agentPathOverrideSection) lines.push(...agentPathOverrideSection.split("\n"));
165
185
  if (agentArgsOverrideSection) lines.push(...agentArgsOverrideSection.split("\n"));
@@ -425,6 +445,21 @@ function removeWorktree(baseCwd, worktreePath) {
425
445
  worktreePath
426
446
  ], baseCwd);
427
447
  }
448
+ function worktreeExists(baseCwd, worktreePath) {
449
+ let output;
450
+ try {
451
+ output = git([
452
+ "worktree",
453
+ "list",
454
+ "--porcelain"
455
+ ], baseCwd);
456
+ } catch {
457
+ return false;
458
+ }
459
+ const target = resolve(worktreePath);
460
+ for (const line of output.split("\n")) if (line.startsWith("worktree ") && resolve(line.slice(9)) === target) return true;
461
+ return false;
462
+ }
428
463
  //#endregion
429
464
  //#region src/core/agents/types.ts
430
465
  function buildAgentOutputSchema(opts) {
@@ -457,12 +492,26 @@ function buildAgentOutputSchema(opts) {
457
492
  required
458
493
  };
459
494
  }
495
+ var PermanentAgentError = class extends Error {
496
+ detail;
497
+ constructor(message, detail) {
498
+ super(message, { cause: detail });
499
+ this.name = "PermanentAgentError";
500
+ this.detail = detail;
501
+ }
502
+ };
460
503
  //#endregion
461
504
  //#region src/core/run.ts
462
505
  const LOG_FILENAME = "gnhf.log";
506
+ const STOP_WHEN_FILENAME = "stop-when";
463
507
  function writeSchemaFile(schemaPath, includeStopField) {
464
508
  writeFileSync(schemaPath, JSON.stringify(buildAgentOutputSchema({ includeStopField }), null, 2), "utf-8");
465
509
  }
510
+ function readStopWhen(stopWhenPath) {
511
+ if (!existsSync(stopWhenPath)) return void 0;
512
+ const stopWhen = readFileSync(stopWhenPath, "utf-8").trim();
513
+ return stopWhen.length > 0 ? stopWhen : void 0;
514
+ }
466
515
  function ensureRunMetadataIgnored(cwd) {
467
516
  const excludePath = execFileSync("git", [
468
517
  "rev-parse",
@@ -496,6 +545,9 @@ function setupRun(runId, prompt, baseCommit, cwd, schemaOptions) {
496
545
  const hasStoredBaseCommit = existsSync(baseCommitPath);
497
546
  const resolvedBaseCommit = hasStoredBaseCommit ? readFileSync(baseCommitPath, "utf-8").trim() : baseCommit;
498
547
  if (!hasStoredBaseCommit) writeFileSync(baseCommitPath, `${baseCommit}\n`, "utf-8");
548
+ const stopWhenPath = join(runDir, STOP_WHEN_FILENAME);
549
+ const stopWhen = schemaOptions.stopWhen;
550
+ if (stopWhen !== void 0) writeFileSync(stopWhenPath, `${stopWhen}\n`, "utf-8");
499
551
  return {
500
552
  runId,
501
553
  runDir,
@@ -504,7 +556,9 @@ function setupRun(runId, prompt, baseCommit, cwd, schemaOptions) {
504
556
  schemaPath,
505
557
  logPath,
506
558
  baseCommit: resolvedBaseCommit,
507
- baseCommitPath
559
+ baseCommitPath,
560
+ stopWhenPath,
561
+ stopWhen
508
562
  };
509
563
  }
510
564
  function resumeRun(runId, cwd, schemaOptions) {
@@ -513,9 +567,19 @@ function resumeRun(runId, cwd, schemaOptions) {
513
567
  const promptPath = join(runDir, "prompt.md");
514
568
  const notesPath = join(runDir, "notes.md");
515
569
  const schemaPath = join(runDir, "output-schema.json");
516
- writeSchemaFile(schemaPath, schemaOptions.includeStopField);
517
570
  const logPath = join(runDir, LOG_FILENAME);
518
571
  const baseCommitPath = join(runDir, "base-commit");
572
+ const baseCommit = existsSync(baseCommitPath) ? readFileSync(baseCommitPath, "utf-8").trim() : backfillLegacyBaseCommit(runId, baseCommitPath, cwd);
573
+ const stopWhenPath = join(runDir, STOP_WHEN_FILENAME);
574
+ let stopWhen = readStopWhen(stopWhenPath);
575
+ if (schemaOptions.clearStopWhen) {
576
+ rmSync(stopWhenPath, { force: true });
577
+ stopWhen = void 0;
578
+ } else if (schemaOptions.stopWhen !== void 0) {
579
+ stopWhen = schemaOptions.stopWhen;
580
+ writeFileSync(stopWhenPath, `${stopWhen}\n`, "utf-8");
581
+ }
582
+ writeSchemaFile(schemaPath, schemaOptions.includeStopField || stopWhen !== void 0);
519
583
  return {
520
584
  runId,
521
585
  runDir,
@@ -523,8 +587,10 @@ function resumeRun(runId, cwd, schemaOptions) {
523
587
  notesPath,
524
588
  schemaPath,
525
589
  logPath,
526
- baseCommit: existsSync(baseCommitPath) ? readFileSync(baseCommitPath, "utf-8").trim() : backfillLegacyBaseCommit(runId, baseCommitPath, cwd),
527
- baseCommitPath
590
+ baseCommit,
591
+ baseCommitPath,
592
+ stopWhenPath,
593
+ stopWhen
528
594
  };
529
595
  }
530
596
  function backfillLegacyBaseCommit(runId, baseCommitPath, cwd) {
@@ -1044,7 +1110,7 @@ function setupAbortHandler(signal, child, reject, abortChild = () => {
1044
1110
  //#endregion
1045
1111
  //#region src/core/agents/claude.ts
1046
1112
  const DEFAULT_FINAL_RESULT_EXIT_GRACE_MS = 15e3;
1047
- function shouldUseWindowsShell$2(bin, platform) {
1113
+ function shouldUseWindowsShell$4(bin, platform) {
1048
1114
  if (platform !== "win32") return false;
1049
1115
  if (/\.(cmd|bat)$/i.test(bin)) return true;
1050
1116
  if (/[\\/]/.test(bin)) return false;
@@ -1105,7 +1171,7 @@ function buildClaudeArgs(prompt, schema, extraArgs) {
1105
1171
  ...userSpecifiedPermissionMode ? [] : ["--dangerously-skip-permissions"]
1106
1172
  ];
1107
1173
  }
1108
- function toTokenUsage(usage) {
1174
+ function toTokenUsage$1(usage) {
1109
1175
  return {
1110
1176
  inputTokens: (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0),
1111
1177
  outputTokens: usage.output_tokens ?? 0,
@@ -1113,11 +1179,14 @@ function toTokenUsage(usage) {
1113
1179
  cacheCreationTokens: usage.cache_creation_input_tokens ?? 0
1114
1180
  };
1115
1181
  }
1116
- function isSameUsage(a, b) {
1182
+ function isSameUsage$1(a, b) {
1117
1183
  return a.inputTokens === b.inputTokens && a.outputTokens === b.outputTokens && a.cacheReadTokens === b.cacheReadTokens && a.cacheCreationTokens === b.cacheCreationTokens;
1118
1184
  }
1119
1185
  function extendsUsage(next, previous) {
1120
- return next.inputTokens >= previous.inputTokens && next.outputTokens >= previous.outputTokens && next.cacheReadTokens >= previous.cacheReadTokens && next.cacheCreationTokens >= previous.cacheCreationTokens && !isSameUsage(next, previous);
1186
+ return next.inputTokens >= previous.inputTokens && next.outputTokens >= previous.outputTokens && next.cacheReadTokens >= previous.cacheReadTokens && next.cacheCreationTokens >= previous.cacheCreationTokens && !isSameUsage$1(next, previous);
1187
+ }
1188
+ function isPermanentClaudeError(stderr) {
1189
+ return /credit balance\s+is\s+too\s+low/i.test(stderr);
1121
1190
  }
1122
1191
  var ClaudeAgent = class {
1123
1192
  name = "claude";
@@ -1141,7 +1210,7 @@ var ClaudeAgent = class {
1141
1210
  const child = spawn(this.bin, buildClaudeArgs(prompt, this.schema, this.extraArgs), {
1142
1211
  cwd,
1143
1212
  detached: this.platform !== "win32",
1144
- shell: shouldUseWindowsShell$2(this.bin, this.platform),
1213
+ shell: shouldUseWindowsShell$4(this.bin, this.platform),
1145
1214
  stdio: [
1146
1215
  "ignore",
1147
1216
  "pipe",
@@ -1176,7 +1245,7 @@ var ClaudeAgent = class {
1176
1245
  parseJSONLStream(child.stdout, logStream, (event) => {
1177
1246
  if (event.type === "assistant") {
1178
1247
  const msg = event.message;
1179
- const nextUsage = toTokenUsage(msg.usage);
1248
+ const nextUsage = toTokenUsage$1(msg.usage);
1180
1249
  let messageId = msg.id;
1181
1250
  let previousUsage;
1182
1251
  if (messageId) {
@@ -1200,7 +1269,7 @@ var ClaudeAgent = class {
1200
1269
  previousUsage = usageByMessageId.get(messageId);
1201
1270
  pendingAnonymousAssistantUsage = null;
1202
1271
  lastAnonymousAssistantUsage = nextUsage;
1203
- } else if (lastAnonymousAssistantId && lastAnonymousAssistantUsage && isSameUsage(nextUsage, lastAnonymousAssistantUsage)) {
1272
+ } else if (lastAnonymousAssistantId && lastAnonymousAssistantUsage && isSameUsage$1(nextUsage, lastAnonymousAssistantUsage)) {
1204
1273
  messageId = lastAnonymousAssistantId;
1205
1274
  previousUsage = usageByMessageId.get(messageId);
1206
1275
  pendingAnonymousAssistantUsage ??= nextUsage;
@@ -1247,7 +1316,8 @@ var ClaudeAgent = class {
1247
1316
  if (finalResultCleanupTimer) clearTimeout(finalResultCleanupTimer);
1248
1317
  logStream?.end();
1249
1318
  if (code !== 0 && !closedAfterFinalCleanup) {
1250
- reject(/* @__PURE__ */ new Error(`claude exited with code ${code}: ${stderr}`));
1319
+ const detail = `claude exited with code ${code}: ${stderr}`;
1320
+ reject(isPermanentClaudeError(stderr) ? new PermanentAgentError("claude credit balance too low - see gnhf.log", detail) : new Error(detail));
1251
1321
  return;
1252
1322
  }
1253
1323
  const terminalResultEvent = finalStructuredResultEvent ?? resultEvent;
@@ -1264,7 +1334,7 @@ var ClaudeAgent = class {
1264
1334
  return;
1265
1335
  }
1266
1336
  const output = terminalResultEvent.structured_output;
1267
- const usage = toTokenUsage(latestResultUsage ?? terminalResultEvent.usage);
1337
+ const usage = toTokenUsage$1(latestResultUsage ?? terminalResultEvent.usage);
1268
1338
  onUsage?.(usage);
1269
1339
  resolve({
1270
1340
  output,
@@ -1275,8 +1345,180 @@ var ClaudeAgent = class {
1275
1345
  }
1276
1346
  };
1277
1347
  //#endregion
1348
+ //#region src/core/agents/copilot.ts
1349
+ function shouldUseWindowsShell$3(bin, platform) {
1350
+ if (platform !== "win32") return false;
1351
+ if (/\.(cmd|bat)$/i.test(bin)) return true;
1352
+ if (/[\\/]/.test(bin)) return false;
1353
+ try {
1354
+ const firstMatch = execFileSync("where", [bin], {
1355
+ encoding: "utf8",
1356
+ stdio: [
1357
+ "ignore",
1358
+ "pipe",
1359
+ "ignore"
1360
+ ]
1361
+ }).split(/\r?\n/).map((line) => line.trim()).find(Boolean);
1362
+ return firstMatch ? /\.(cmd|bat)$/i.test(firstMatch) : false;
1363
+ } catch {
1364
+ return false;
1365
+ }
1366
+ }
1367
+ function terminateCopilotProcess(child, platform) {
1368
+ if (platform === "win32" && child.pid) {
1369
+ try {
1370
+ execFileSync("taskkill", [
1371
+ "/T",
1372
+ "/F",
1373
+ "/PID",
1374
+ String(child.pid)
1375
+ ], { stdio: "ignore" });
1376
+ } catch {}
1377
+ return;
1378
+ }
1379
+ child.kill("SIGTERM");
1380
+ }
1381
+ function userSpecifiedPermissionMode(userArgs) {
1382
+ return userArgs.some((arg) => arg === "--allow-all" || arg === "--yolo" || arg === "--allow-all-tools" || arg === "--allow-all-paths" || arg === "--allow-all-urls" || arg === "--allow-tool" || arg.startsWith("--allow-tool=") || arg === "--allow-url" || arg.startsWith("--allow-url=") || arg === "--deny-tool" || arg.startsWith("--deny-tool=") || arg === "--deny-url" || arg.startsWith("--deny-url=") || arg === "--available-tools" || arg.startsWith("--available-tools=") || arg === "--excluded-tools" || arg.startsWith("--excluded-tools="));
1383
+ }
1384
+ function buildCopilotPrompt(prompt, schema) {
1385
+ return `${prompt}
1386
+
1387
+ ## gnhf final output contract
1388
+
1389
+ When the iteration is complete, your final answer must be a single JSON object that matches this JSON Schema:
1390
+
1391
+ \`\`\`json
1392
+ ${JSON.stringify(schema, null, 2)}
1393
+ \`\`\`
1394
+
1395
+ Return only the JSON object in the final answer. Do not wrap it in Markdown. Do not include explanatory prose outside the JSON object.`;
1396
+ }
1397
+ function buildCopilotArgs(prompt, schema, extraArgs) {
1398
+ const userArgs = extraArgs ?? [];
1399
+ return [
1400
+ ...userArgs,
1401
+ "-p",
1402
+ buildCopilotPrompt(prompt, schema),
1403
+ "--output-format",
1404
+ "json",
1405
+ "--stream",
1406
+ "off",
1407
+ "--no-color",
1408
+ ...userSpecifiedPermissionMode(userArgs) ? [] : ["--allow-all"]
1409
+ ];
1410
+ }
1411
+ function stripJsonFence(text) {
1412
+ const trimmed = text.trim();
1413
+ return trimmed.match(/^```(?:json)?\s*\n([\s\S]*?)\n```$/i)?.[1]?.trim() ?? trimmed;
1414
+ }
1415
+ function numberField$1(usage, names) {
1416
+ for (const name of names) {
1417
+ const value = usage[name];
1418
+ if (typeof value === "number") return value;
1419
+ }
1420
+ }
1421
+ function usageFromRecord(usage) {
1422
+ const inputTokens = numberField$1(usage, ["inputTokens", "input_tokens"]);
1423
+ const outputTokens = numberField$1(usage, ["outputTokens", "output_tokens"]);
1424
+ const cacheReadTokens = numberField$1(usage, [
1425
+ "cacheReadTokens",
1426
+ "cache_read_tokens",
1427
+ "cache_read_input_tokens"
1428
+ ]);
1429
+ const cacheCreationTokens = numberField$1(usage, [
1430
+ "cacheCreationTokens",
1431
+ "cacheWriteTokens",
1432
+ "cache_creation_tokens",
1433
+ "cache_creation_input_tokens",
1434
+ "cache_write_tokens"
1435
+ ]);
1436
+ if (inputTokens === void 0 && outputTokens === void 0 && cacheReadTokens === void 0 && cacheCreationTokens === void 0) return null;
1437
+ return {
1438
+ inputTokens: inputTokens ?? 0,
1439
+ outputTokens: outputTokens ?? 0,
1440
+ cacheReadTokens: cacheReadTokens ?? 0,
1441
+ cacheCreationTokens: cacheCreationTokens ?? 0
1442
+ };
1443
+ }
1444
+ var CopilotAgent = class {
1445
+ name = "copilot";
1446
+ bin;
1447
+ extraArgs;
1448
+ platform;
1449
+ schema;
1450
+ constructor(binOrDeps = {}) {
1451
+ const deps = typeof binOrDeps === "string" ? { bin: binOrDeps } : binOrDeps;
1452
+ this.bin = deps.bin ?? "copilot";
1453
+ this.extraArgs = deps.extraArgs;
1454
+ this.platform = deps.platform ?? process.platform;
1455
+ this.schema = deps.schema ?? buildAgentOutputSchema({ includeStopField: false });
1456
+ }
1457
+ run(prompt, cwd, options) {
1458
+ const { onUsage, onMessage, signal, logPath } = options ?? {};
1459
+ return new Promise((resolve, reject) => {
1460
+ const logStream = logPath ? createWriteStream(logPath) : null;
1461
+ const child = spawn(this.bin, buildCopilotArgs(prompt, this.schema, this.extraArgs), {
1462
+ cwd,
1463
+ shell: shouldUseWindowsShell$3(this.bin, this.platform),
1464
+ stdio: [
1465
+ "ignore",
1466
+ "pipe",
1467
+ "pipe"
1468
+ ],
1469
+ env: process.env
1470
+ });
1471
+ if (setupAbortHandler(signal, child, reject, () => terminateCopilotProcess(child, this.platform))) return;
1472
+ let lastAgentMessage = null;
1473
+ const cumulative = {
1474
+ inputTokens: 0,
1475
+ outputTokens: 0,
1476
+ cacheReadTokens: 0,
1477
+ cacheCreationTokens: 0
1478
+ };
1479
+ parseJSONLStream(child.stdout, logStream, (event) => {
1480
+ if (event.type === "assistant.message") {
1481
+ const data = event.data;
1482
+ if (typeof data.content === "string") {
1483
+ lastAgentMessage = data.content;
1484
+ onMessage?.(data.content);
1485
+ }
1486
+ if (typeof data.outputTokens === "number") {
1487
+ cumulative.outputTokens += data.outputTokens;
1488
+ onUsage?.({ ...cumulative });
1489
+ }
1490
+ }
1491
+ if ("usage" in event && event.usage) {
1492
+ const usage = usageFromRecord(event.usage);
1493
+ if (usage) {
1494
+ cumulative.inputTokens = usage.inputTokens;
1495
+ cumulative.outputTokens = Math.max(cumulative.outputTokens, usage.outputTokens);
1496
+ cumulative.cacheReadTokens = usage.cacheReadTokens;
1497
+ cumulative.cacheCreationTokens = usage.cacheCreationTokens;
1498
+ onUsage?.({ ...cumulative });
1499
+ }
1500
+ }
1501
+ });
1502
+ setupChildProcessHandlers(child, "copilot", logStream, reject, () => {
1503
+ if (!lastAgentMessage) {
1504
+ reject(/* @__PURE__ */ new Error("copilot returned no agent message"));
1505
+ return;
1506
+ }
1507
+ try {
1508
+ resolve({
1509
+ output: JSON.parse(stripJsonFence(lastAgentMessage)),
1510
+ usage: cumulative
1511
+ });
1512
+ } catch (err) {
1513
+ reject(/* @__PURE__ */ new Error(`Failed to parse copilot output: ${err instanceof Error ? err.message : err}`));
1514
+ }
1515
+ });
1516
+ });
1517
+ }
1518
+ };
1519
+ //#endregion
1278
1520
  //#region src/core/agents/codex.ts
1279
- function shouldUseWindowsShell$1(bin, platform) {
1521
+ function shouldUseWindowsShell$2(bin, platform) {
1280
1522
  if (platform !== "win32") return false;
1281
1523
  if (/\.(cmd|bat)$/i.test(bin)) return true;
1282
1524
  if (/[\\/]/.test(bin)) return false;
@@ -1342,7 +1584,7 @@ var CodexAgent = class {
1342
1584
  const logStream = logPath ? createWriteStream(logPath) : null;
1343
1585
  const child = spawn(this.bin, buildCodexArgs(prompt, this.schemaPath, this.extraArgs), {
1344
1586
  cwd,
1345
- shell: shouldUseWindowsShell$1(this.bin, this.platform),
1587
+ shell: shouldUseWindowsShell$2(this.bin, this.platform),
1346
1588
  stdio: [
1347
1589
  "ignore",
1348
1590
  "pipe",
@@ -2204,6 +2446,287 @@ var OpenCodeAgent = class {
2204
2446
  }
2205
2447
  };
2206
2448
  //#endregion
2449
+ //#region src/core/agents/pi.ts
2450
+ function shouldUseWindowsShell$1(bin, platform) {
2451
+ if (platform !== "win32") return false;
2452
+ if (/\.(cmd|bat)$/i.test(bin)) return true;
2453
+ if (/[\\/]/.test(bin)) return false;
2454
+ try {
2455
+ const firstMatch = execFileSync("where", [bin], {
2456
+ encoding: "utf8",
2457
+ stdio: [
2458
+ "ignore",
2459
+ "pipe",
2460
+ "ignore"
2461
+ ]
2462
+ }).split(/\r?\n/).map((line) => line.trim()).find(Boolean);
2463
+ return firstMatch ? /\.(cmd|bat)$/i.test(firstMatch) : false;
2464
+ } catch {
2465
+ return false;
2466
+ }
2467
+ }
2468
+ function terminatePiProcess(child, platform) {
2469
+ if (platform === "win32" && child.pid) {
2470
+ try {
2471
+ execFileSync("taskkill", [
2472
+ "/T",
2473
+ "/F",
2474
+ "/PID",
2475
+ String(child.pid)
2476
+ ], { stdio: "ignore" });
2477
+ } catch {}
2478
+ return;
2479
+ }
2480
+ if (child.pid) try {
2481
+ process.kill(-child.pid, "SIGTERM");
2482
+ return;
2483
+ } catch {}
2484
+ child.kill("SIGTERM");
2485
+ }
2486
+ function buildPiPrompt(prompt, schema) {
2487
+ return `${prompt}
2488
+
2489
+ ## gnhf final output contract
2490
+
2491
+ When the iteration is complete, your final assistant response must be only valid JSON matching this JSON Schema. Do not wrap it in Markdown fences. Do not include prose before or after the JSON object.
2492
+
2493
+ ${JSON.stringify(schema, null, 2)}`;
2494
+ }
2495
+ function buildPiArgs(extraArgs) {
2496
+ return [
2497
+ ...extraArgs ?? [],
2498
+ "--mode",
2499
+ "json",
2500
+ "--no-session"
2501
+ ];
2502
+ }
2503
+ function isRecord(value) {
2504
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2505
+ }
2506
+ function stringField(record, names) {
2507
+ for (const name of names) {
2508
+ const value = record[name];
2509
+ if (typeof value === "string") return value;
2510
+ }
2511
+ }
2512
+ function numberField(record, names) {
2513
+ for (const name of names) {
2514
+ const value = record[name];
2515
+ if (typeof value === "number") return value;
2516
+ }
2517
+ }
2518
+ function toTokenUsage(usage) {
2519
+ if (!usage) return null;
2520
+ return {
2521
+ inputTokens: numberField(usage, ["input"]) ?? 0,
2522
+ outputTokens: numberField(usage, ["output"]) ?? 0,
2523
+ cacheReadTokens: numberField(usage, ["cacheRead"]) ?? 0,
2524
+ cacheCreationTokens: numberField(usage, ["cacheWrite"]) ?? 0
2525
+ };
2526
+ }
2527
+ function isSameUsage(a, b) {
2528
+ return a.inputTokens === b.inputTokens && a.outputTokens === b.outputTokens && a.cacheReadTokens === b.cacheReadTokens && a.cacheCreationTokens === b.cacheCreationTokens;
2529
+ }
2530
+ function messageKey(message) {
2531
+ const responseId = stringField(message, ["responseId", "id"]);
2532
+ if (responseId) return responseId;
2533
+ const timestamp = message.timestamp;
2534
+ if (typeof timestamp === "string" || typeof timestamp === "number") return `timestamp:${timestamp}`;
2535
+ return null;
2536
+ }
2537
+ function roleOf(message) {
2538
+ return isRecord(message) && typeof message.role === "string" ? message.role : void 0;
2539
+ }
2540
+ function textFromContentBlock(block) {
2541
+ if (typeof block === "string") return block;
2542
+ if (!isRecord(block)) return null;
2543
+ if (typeof block.text === "string") return block.text;
2544
+ if (typeof block.content === "string") return block.content;
2545
+ return null;
2546
+ }
2547
+ function textFromAssistantMessage(message) {
2548
+ if (!message) return "";
2549
+ if (typeof message.text === "string") return message.text;
2550
+ if (typeof message.content === "string") return message.content;
2551
+ if (Array.isArray(message.content)) return message.content.map(textFromContentBlock).filter((text) => text !== null).join("");
2552
+ return "";
2553
+ }
2554
+ function compactJson(value) {
2555
+ try {
2556
+ return JSON.stringify(value);
2557
+ } catch {
2558
+ return String(value);
2559
+ }
2560
+ }
2561
+ function validatePiOutput(value, schema) {
2562
+ if (!isRecord(value)) throw new Error("expected an object");
2563
+ if (typeof value.success !== "boolean") throw new Error("success must be a boolean");
2564
+ if (typeof value.summary !== "string") throw new Error("summary must be a string");
2565
+ if (!Array.isArray(value.key_changes_made) || !value.key_changes_made.every((item) => typeof item === "string")) throw new Error("key_changes_made must be an array of strings");
2566
+ if (!Array.isArray(value.key_learnings) || !value.key_learnings.every((item) => typeof item === "string")) throw new Error("key_learnings must be an array of strings");
2567
+ if (schema.required.includes("should_fully_stop") && typeof value.should_fully_stop !== "boolean") throw new Error("should_fully_stop must be a boolean");
2568
+ if (schema.additionalProperties === false) {
2569
+ const allowed = new Set(Object.keys(schema.properties));
2570
+ const extraKey = Object.keys(value).find((key) => !allowed.has(key));
2571
+ if (extraKey) throw new Error(`unexpected property ${extraKey}`);
2572
+ }
2573
+ return value;
2574
+ }
2575
+ function textByIndexToString(textByIndex) {
2576
+ return [...textByIndex.entries()].sort(([a], [b]) => a - b).map(([, text]) => text).join("");
2577
+ }
2578
+ var PiAgent = class {
2579
+ name = "pi";
2580
+ bin;
2581
+ extraArgs;
2582
+ platform;
2583
+ schema;
2584
+ constructor(deps = {}) {
2585
+ this.bin = deps.bin ?? "pi";
2586
+ this.extraArgs = deps.extraArgs;
2587
+ this.platform = deps.platform ?? process.platform;
2588
+ this.schema = deps.schema ?? buildAgentOutputSchema({ includeStopField: false });
2589
+ }
2590
+ run(prompt, cwd, options) {
2591
+ const { onUsage, onMessage, signal, logPath } = options ?? {};
2592
+ return new Promise((resolve, reject) => {
2593
+ const logStream = logPath ? createWriteStream(logPath) : null;
2594
+ const child = spawn(this.bin, buildPiArgs(this.extraArgs), {
2595
+ cwd,
2596
+ detached: this.platform !== "win32",
2597
+ shell: shouldUseWindowsShell$1(this.bin, this.platform),
2598
+ stdio: [
2599
+ "pipe",
2600
+ "pipe",
2601
+ "pipe"
2602
+ ],
2603
+ env: process.env
2604
+ });
2605
+ child.stdin?.write(buildPiPrompt(prompt, this.schema));
2606
+ child.stdin?.end();
2607
+ if (setupAbortHandler(signal, child, reject, () => terminatePiProcess(child, this.platform))) return;
2608
+ let latestAssistantMessage = null;
2609
+ const streamTextByIndex = /* @__PURE__ */ new Map();
2610
+ const completeTextByIndex = /* @__PURE__ */ new Map();
2611
+ const usageByMessageKey = /* @__PURE__ */ new Map();
2612
+ let lastEmittedUsage = {
2613
+ inputTokens: 0,
2614
+ outputTokens: 0,
2615
+ cacheReadTokens: 0,
2616
+ cacheCreationTokens: 0
2617
+ };
2618
+ let anonymousKeySeq = 0;
2619
+ let currentStreamingMessageKey = null;
2620
+ const updateUsage = (message, streaming = false) => {
2621
+ const usage = isRecord(message.usage) ? toTokenUsage(message.usage) : null;
2622
+ if (!usage) return;
2623
+ let key = messageKey(message);
2624
+ if (key === null) if (streaming && currentStreamingMessageKey !== null) key = currentStreamingMessageKey;
2625
+ else {
2626
+ key = `assistant-anonymous-${anonymousKeySeq++}`;
2627
+ if (streaming) currentStreamingMessageKey = key;
2628
+ }
2629
+ usageByMessageKey.set(key, usage);
2630
+ const cumulative = {
2631
+ inputTokens: 0,
2632
+ outputTokens: 0,
2633
+ cacheReadTokens: 0,
2634
+ cacheCreationTokens: 0
2635
+ };
2636
+ for (const entry of usageByMessageKey.values()) {
2637
+ cumulative.inputTokens += entry.inputTokens;
2638
+ cumulative.outputTokens += entry.outputTokens;
2639
+ cumulative.cacheReadTokens += entry.cacheReadTokens;
2640
+ cumulative.cacheCreationTokens += entry.cacheCreationTokens;
2641
+ }
2642
+ if (!isSameUsage(cumulative, lastEmittedUsage)) {
2643
+ lastEmittedUsage = cumulative;
2644
+ onUsage?.({ ...cumulative });
2645
+ }
2646
+ };
2647
+ const rememberAssistantMessage = (message, streaming = false) => {
2648
+ if (!isRecord(message) || roleOf(message) !== "assistant") return;
2649
+ latestAssistantMessage = message;
2650
+ updateUsage(message, streaming);
2651
+ };
2652
+ parseJSONLStream(child.stdout, logStream, (event) => {
2653
+ if (!isRecord(event)) return;
2654
+ if (event.type === "message_update") {
2655
+ rememberAssistantMessage(event.message, true);
2656
+ if (isRecord(event.assistantMessageEvent)) {
2657
+ const assistantEvent = event.assistantMessageEvent;
2658
+ const contentIndex = numberField(assistantEvent, ["contentIndex", "content_index"]) ?? 0;
2659
+ if (assistantEvent.type === "text_delta") {
2660
+ const delta = stringField(assistantEvent, [
2661
+ "delta",
2662
+ "text",
2663
+ "content"
2664
+ ]);
2665
+ if (delta) {
2666
+ const next = (streamTextByIndex.get(contentIndex) ?? "") + delta;
2667
+ streamTextByIndex.set(contentIndex, next);
2668
+ const visible = next.trim();
2669
+ if (visible) onMessage?.(visible);
2670
+ }
2671
+ }
2672
+ if (assistantEvent.type === "text_end") {
2673
+ const text = stringField(assistantEvent, ["text", "content"]) ?? streamTextByIndex.get(contentIndex) ?? "";
2674
+ completeTextByIndex.set(contentIndex, text);
2675
+ const visible = text.trim();
2676
+ if (visible) onMessage?.(visible);
2677
+ }
2678
+ }
2679
+ }
2680
+ if (event.type === "message_end" || event.type === "turn_end") {
2681
+ rememberAssistantMessage(event.message, true);
2682
+ currentStreamingMessageKey = null;
2683
+ }
2684
+ if (event.type === "agent_end" && Array.isArray(event.messages) && !latestAssistantMessage) for (let i = event.messages.length - 1; i >= 0; i -= 1) {
2685
+ const message = event.messages[i];
2686
+ if (roleOf(message) === "assistant") {
2687
+ rememberAssistantMessage(message);
2688
+ break;
2689
+ }
2690
+ }
2691
+ });
2692
+ setupChildProcessHandlers(child, "pi", logStream, reject, () => {
2693
+ if (latestAssistantMessage) {
2694
+ const stopReason = latestAssistantMessage.stopReason;
2695
+ if (stopReason === "error" || stopReason === "aborted") {
2696
+ const errorMessage = stringField(latestAssistantMessage, [
2697
+ "errorMessage",
2698
+ "error",
2699
+ "message"
2700
+ ]) ?? compactJson(latestAssistantMessage);
2701
+ reject(/* @__PURE__ */ new Error(`pi reported error: ${errorMessage}`));
2702
+ return;
2703
+ }
2704
+ }
2705
+ const finalText = textFromAssistantMessage(latestAssistantMessage).trim() || textByIndexToString(completeTextByIndex).trim() || textByIndexToString(streamTextByIndex).trim();
2706
+ if (!finalText) {
2707
+ reject(/* @__PURE__ */ new Error("pi returned no text output"));
2708
+ return;
2709
+ }
2710
+ let parsed;
2711
+ try {
2712
+ parsed = JSON.parse(finalText);
2713
+ } catch (err) {
2714
+ reject(/* @__PURE__ */ new Error(`Failed to parse pi output: ${err instanceof Error ? err.message : err}`));
2715
+ return;
2716
+ }
2717
+ try {
2718
+ resolve({
2719
+ output: validatePiOutput(parsed, this.schema),
2720
+ usage: lastEmittedUsage
2721
+ });
2722
+ } catch (err) {
2723
+ reject(/* @__PURE__ */ new Error(`Invalid pi output: ${err instanceof Error ? err.message : err}`));
2724
+ }
2725
+ });
2726
+ });
2727
+ }
2728
+ };
2729
+ //#endregion
2207
2730
  //#region src/core/agents/rovodev.ts
2208
2731
  function buildSystemPrompt(schema) {
2209
2732
  return [
@@ -2829,11 +3352,21 @@ function createAgent(name, runInfo, pathOverride, agentArgsOverride, options) {
2829
3352
  bin: pathOverride,
2830
3353
  extraArgs: agentArgsOverride
2831
3354
  });
3355
+ case "copilot": return new CopilotAgent({
3356
+ bin: pathOverride,
3357
+ extraArgs: agentArgsOverride,
3358
+ schema
3359
+ });
2832
3360
  case "opencode": return new OpenCodeAgent({
2833
3361
  bin: pathOverride,
2834
3362
  extraArgs: agentArgsOverride,
2835
3363
  schema
2836
3364
  });
3365
+ case "pi": return new PiAgent({
3366
+ bin: pathOverride,
3367
+ extraArgs: agentArgsOverride,
3368
+ schema
3369
+ });
2837
3370
  case "rovodev": return new RovoDevAgent(runInfo.schemaPath, {
2838
3371
  bin: pathOverride,
2839
3372
  extraArgs: agentArgsOverride
@@ -2841,6 +3374,19 @@ function createAgent(name, runInfo, pathOverride, agentArgsOverride, options) {
2841
3374
  }
2842
3375
  }
2843
3376
  //#endregion
3377
+ //#region src/core/interrupt-state.ts
3378
+ function getInterruptDisposition(state) {
3379
+ if (state.status === "aborted") return "exit";
3380
+ if (state.gracefulStopRequested || state.status === "stopped") return "force-stop";
3381
+ return "request-graceful-stop";
3382
+ }
3383
+ function getInterruptHint(state) {
3384
+ const disposition = getInterruptDisposition(state);
3385
+ if (disposition === "exit") return "exit";
3386
+ if (disposition === "force-stop") return "force-stop";
3387
+ return "resume";
3388
+ }
3389
+ //#endregion
2844
3390
  //#region src/templates/iteration-prompt.ts
2845
3391
  function buildIterationPrompt(params) {
2846
3392
  const outputFields = [
@@ -2887,8 +3433,10 @@ var Orchestrator = class extends EventEmitter {
2887
3433
  activeAbortController = null;
2888
3434
  pendingAbortReason = null;
2889
3435
  loopDone = false;
3436
+ stoppedEventEmitted = false;
2890
3437
  state = {
2891
3438
  status: "running",
3439
+ gracefulStopRequested: false,
2892
3440
  currentIteration: 0,
2893
3441
  totalInputTokens: 0,
2894
3442
  totalOutputTokens: 0,
@@ -2900,7 +3448,8 @@ var Orchestrator = class extends EventEmitter {
2900
3448
  consecutiveErrors: 0,
2901
3449
  startTime: /* @__PURE__ */ new Date(),
2902
3450
  waitingUntil: null,
2903
- lastMessage: null
3451
+ lastMessage: null,
3452
+ lastAgentError: null
2904
3453
  };
2905
3454
  constructor(config, agent, runInfo, prompt, cwd, startIteration = 0, limits = {}) {
2906
3455
  super();
@@ -2914,7 +3463,27 @@ var Orchestrator = class extends EventEmitter {
2914
3463
  this.state.commitCount = getBranchCommitCount(this.runInfo.baseCommit, this.cwd);
2915
3464
  }
2916
3465
  getState() {
2917
- return { ...this.state };
3466
+ return {
3467
+ ...this.state,
3468
+ interruptHint: getInterruptHint(this.state)
3469
+ };
3470
+ }
3471
+ requestGracefulStop() {
3472
+ if (this.stopRequested || this.state.gracefulStopRequested || this.loopDone) return;
3473
+ this.state.gracefulStopRequested = true;
3474
+ appendDebugLog("orchestrator:graceful-stop-requested", {
3475
+ iteration: this.state.currentIteration,
3476
+ hasActiveIteration: this.activeIterationPromise !== null,
3477
+ status: this.state.status
3478
+ });
3479
+ this.emit("state", this.getState());
3480
+ if (this.state.status === "waiting") this.activeAbortController?.abort();
3481
+ }
3482
+ handleInterrupt() {
3483
+ const disposition = getInterruptDisposition(this.state);
3484
+ if (disposition === "request-graceful-stop") this.requestGracefulStop();
3485
+ else if (disposition === "force-stop") this.stop();
3486
+ return disposition;
2918
3487
  }
2919
3488
  stop() {
2920
3489
  this.stopRequested = true;
@@ -2924,8 +3493,9 @@ var Orchestrator = class extends EventEmitter {
2924
3493
  loopDone: this.loopDone
2925
3494
  });
2926
3495
  this.activeAbortController?.abort();
3496
+ this.state.gracefulStopRequested = false;
2927
3497
  if (this.loopDone) {
2928
- this.emit("stopped");
3498
+ this.emitStopped();
2929
3499
  return;
2930
3500
  }
2931
3501
  if (this.stopPromise) return;
@@ -2950,7 +3520,7 @@ var Orchestrator = class extends EventEmitter {
2950
3520
  resetHard(this.cwd);
2951
3521
  this.state.status = "stopped";
2952
3522
  this.emit("state", this.getState());
2953
- this.emit("stopped");
3523
+ this.emitStopped();
2954
3524
  })();
2955
3525
  }
2956
3526
  async start() {
@@ -2974,6 +3544,7 @@ var Orchestrator = class extends EventEmitter {
2974
3544
  this.abort(preIterationAbortReason);
2975
3545
  break;
2976
3546
  }
3547
+ if (this.stopForGracefulShutdown()) break;
2977
3548
  this.state.currentIteration++;
2978
3549
  this.state.status = "running";
2979
3550
  this.emit("iteration:start", this.state.currentIteration);
@@ -3029,6 +3600,7 @@ var Orchestrator = class extends EventEmitter {
3029
3600
  totalOutputTokens: this.state.totalOutputTokens,
3030
3601
  commitCount: this.state.commitCount
3031
3602
  });
3603
+ if (this.stopForGracefulShutdown()) break;
3032
3604
  if (this.limits.stopWhen !== void 0 && result.shouldFullyStop) {
3033
3605
  this.abort("stop condition met");
3034
3606
  break;
@@ -3059,6 +3631,7 @@ var Orchestrator = class extends EventEmitter {
3059
3631
  });
3060
3632
  this.state.waitingUntil = null;
3061
3633
  if (!this.stopRequested) {
3634
+ if (this.stopForGracefulShutdown()) break;
3062
3635
  this.state.status = "running";
3063
3636
  this.emit("state", this.getState());
3064
3637
  }
@@ -3075,6 +3648,7 @@ var Orchestrator = class extends EventEmitter {
3075
3648
  if (this.stopPromise) await this.stopPromise;
3076
3649
  else await this.closeAgent();
3077
3650
  this.loopDone = true;
3651
+ if (this.didStopWithoutForce()) this.emitStopped();
3078
3652
  appendDebugLog("orchestrator:end", {
3079
3653
  status: this.state.status,
3080
3654
  iterations: this.state.currentIteration,
@@ -3166,6 +3740,14 @@ var Orchestrator = class extends EventEmitter {
3166
3740
  elapsedMs,
3167
3741
  error: serializeError(err)
3168
3742
  });
3743
+ if (err instanceof PermanentAgentError) {
3744
+ resetHard(this.cwd);
3745
+ this.state.lastAgentError = err.detail;
3746
+ return {
3747
+ type: "aborted",
3748
+ reason: err.message
3749
+ };
3750
+ }
3169
3751
  const summary = err instanceof Error ? err.message : String(err);
3170
3752
  return {
3171
3753
  type: "completed",
@@ -3184,6 +3766,7 @@ var Orchestrator = class extends EventEmitter {
3184
3766
  this.state.successCount++;
3185
3767
  this.state.consecutiveFailures = 0;
3186
3768
  this.state.consecutiveErrors = 0;
3769
+ this.state.lastAgentError = null;
3187
3770
  return {
3188
3771
  number: this.state.currentIteration,
3189
3772
  success: true,
@@ -3198,8 +3781,13 @@ var Orchestrator = class extends EventEmitter {
3198
3781
  resetHard(this.cwd);
3199
3782
  this.state.failCount++;
3200
3783
  this.state.consecutiveFailures++;
3201
- if (kind === "error") this.state.consecutiveErrors++;
3202
- else this.state.consecutiveErrors = 0;
3784
+ if (kind === "error") {
3785
+ this.state.consecutiveErrors++;
3786
+ this.state.lastAgentError = recordSummary;
3787
+ } else {
3788
+ this.state.consecutiveErrors = 0;
3789
+ this.state.lastAgentError = null;
3790
+ }
3203
3791
  return {
3204
3792
  number: this.state.currentIteration,
3205
3793
  success: false,
@@ -3237,8 +3825,27 @@ var Orchestrator = class extends EventEmitter {
3237
3825
  if (totalTokens < this.limits.maxTokens) return null;
3238
3826
  return `max tokens reached (${totalTokens}/${this.limits.maxTokens})`;
3239
3827
  }
3828
+ finishGracefulStop() {
3829
+ this.state.status = "stopped";
3830
+ this.state.gracefulStopRequested = false;
3831
+ this.state.waitingUntil = null;
3832
+ appendDebugLog("orchestrator:graceful-stop-complete", {
3833
+ iteration: this.state.currentIteration,
3834
+ consecutiveFailures: this.state.consecutiveFailures
3835
+ });
3836
+ this.emit("state", this.getState());
3837
+ }
3838
+ stopForGracefulShutdown() {
3839
+ if (!this.state.gracefulStopRequested) return false;
3840
+ this.finishGracefulStop();
3841
+ return true;
3842
+ }
3843
+ didStopWithoutForce() {
3844
+ return this.stopPromise === null && this.state.status === "stopped";
3845
+ }
3240
3846
  abort(reason) {
3241
3847
  this.state.status = "aborted";
3848
+ this.state.gracefulStopRequested = false;
3242
3849
  this.state.lastMessage = reason;
3243
3850
  this.state.waitingUntil = null;
3244
3851
  appendDebugLog("orchestrator:abort", {
@@ -3256,6 +3863,11 @@ var Orchestrator = class extends EventEmitter {
3256
3863
  appendDebugLog("agent:close:error", { error: serializeError(err) });
3257
3864
  }
3258
3865
  }
3866
+ emitStopped() {
3867
+ if (this.stoppedEventEmitted) return;
3868
+ this.stoppedEventEmitted = true;
3869
+ this.emit("stopped");
3870
+ }
3259
3871
  snapshotGitState() {
3260
3872
  try {
3261
3873
  return {
@@ -3324,6 +3936,7 @@ const INITIAL_ELAPSED_MS = 29237e3;
3324
3936
  var MockOrchestrator = class extends EventEmitter {
3325
3937
  state = {
3326
3938
  status: "running",
3939
+ gracefulStopRequested: false,
3327
3940
  currentIteration: 14,
3328
3941
  totalInputTokens: 873e5,
3329
3942
  totalOutputTokens: 86e4,
@@ -3340,20 +3953,35 @@ var MockOrchestrator = class extends EventEmitter {
3340
3953
  tokenTimer = null;
3341
3954
  messageTimer = null;
3342
3955
  messageIndex = 0;
3956
+ stoppedEventEmitted = false;
3343
3957
  getState() {
3344
3958
  return {
3345
3959
  ...this.state,
3960
+ interruptHint: getInterruptHint(this.state),
3346
3961
  iterations: [...this.state.iterations]
3347
3962
  };
3348
3963
  }
3349
3964
  stop() {
3965
+ if (this.state.status === "stopped") return;
3350
3966
  if (this.tokenTimer) clearTimeout(this.tokenTimer);
3351
3967
  if (this.messageTimer) clearTimeout(this.messageTimer);
3352
3968
  this.tokenTimer = null;
3353
3969
  this.messageTimer = null;
3354
3970
  this.state.status = "stopped";
3971
+ this.state.gracefulStopRequested = false;
3355
3972
  this.emit("state", this.getState());
3356
- this.emit("stopped");
3973
+ this.emitStopped();
3974
+ }
3975
+ requestGracefulStop() {
3976
+ if (this.state.gracefulStopRequested || this.state.status !== "running") return;
3977
+ this.state.gracefulStopRequested = true;
3978
+ this.emit("state", this.getState());
3979
+ }
3980
+ handleInterrupt() {
3981
+ const disposition = getInterruptDisposition(this.state);
3982
+ if (disposition === "request-graceful-stop") this.requestGracefulStop();
3983
+ else if (disposition === "force-stop") this.stop();
3984
+ return disposition;
3357
3985
  }
3358
3986
  start() {
3359
3987
  this.emit("state", this.getState());
@@ -3362,6 +3990,10 @@ var MockOrchestrator = class extends EventEmitter {
3362
3990
  }
3363
3991
  scheduleTokenBump() {
3364
3992
  this.tokenTimer = setTimeout(() => {
3993
+ if (this.state.gracefulStopRequested) {
3994
+ this.stop();
3995
+ return;
3996
+ }
3365
3997
  this.state.totalInputTokens += randInt(4e4, 18e4);
3366
3998
  this.state.totalOutputTokens += randInt(200, 2e3);
3367
3999
  this.emit("state", this.getState());
@@ -3371,12 +4003,21 @@ var MockOrchestrator = class extends EventEmitter {
3371
4003
  scheduleNextMessage() {
3372
4004
  const delay = randInt(3e3, 7e3);
3373
4005
  this.messageTimer = setTimeout(() => {
4006
+ if (this.state.gracefulStopRequested) {
4007
+ this.stop();
4008
+ return;
4009
+ }
3374
4010
  this.messageIndex = (this.messageIndex + 1) % AGENT_MESSAGES.length;
3375
4011
  this.state.lastMessage = AGENT_MESSAGES[this.messageIndex];
3376
4012
  this.emit("state", this.getState());
3377
4013
  this.scheduleNextMessage();
3378
4014
  }, delay);
3379
4015
  }
4016
+ emitStopped() {
4017
+ if (this.stoppedEventEmitted) return;
4018
+ this.stoppedEventEmitted = true;
4019
+ this.emit("stopped");
4020
+ }
3380
4021
  };
3381
4022
  //#endregion
3382
4023
  //#region src/utils/stars.ts
@@ -3666,6 +4307,7 @@ const MOON_PHASE_PERIOD = 1600;
3666
4307
  const MAX_MSG_LINES = 3;
3667
4308
  const MAX_MSG_LINE_LEN = CONTENT_WIDTH;
3668
4309
  const RESUME_HINT = "[ctrl+c to stop, gnhf again to resume]";
4310
+ const GRACEFUL_STOP_HINT = "[graceful stop requested, ctrl+c again to force stop, gnhf again to resume]";
3669
4311
  const DONE_HINT = "[ctrl+c to exit]";
3670
4312
  function spacedLabel(text) {
3671
4313
  return text.split("").join(" ");
@@ -3719,10 +4361,15 @@ function renderStatsCells(elapsed, inputTokens, outputTokens, commitCount) {
3719
4361
  ...textToCells(formatCommitCount(commitCount), "normal")
3720
4362
  ];
3721
4363
  }
3722
- function renderAgentMessageCells(message, status) {
4364
+ function renderAgentMessageCells(message, status, lastAgentError) {
3723
4365
  const lines = [];
3724
- if (status === "waiting") lines.push("waiting (backoff)...");
3725
- else if (status === "aborted" && !message) lines.push("max consecutive failures reached");
4366
+ if (status === "waiting") {
4367
+ lines.push("waiting (backoff)...");
4368
+ if (lastAgentError) lines.push(...wordWrap(lastAgentError, MAX_MSG_LINE_LEN, 2));
4369
+ } else if (status === "aborted" && lastAgentError) {
4370
+ lines.push(...wordWrap(message ?? "max consecutive failures reached", MAX_MSG_LINE_LEN, 1));
4371
+ lines.push(...wordWrap(lastAgentError, MAX_MSG_LINE_LEN, 2));
4372
+ } else if (status === "aborted" && !message) lines.push("max consecutive failures reached");
3726
4373
  else if (!message) lines.push("working...");
3727
4374
  else {
3728
4375
  const wrapped = wordWrap(message, MAX_MSG_LINE_LEN, MAX_MSG_LINES);
@@ -3804,8 +4451,8 @@ function centerLineCells(content, width) {
3804
4451
  ...emptyCells(rightPad)
3805
4452
  ];
3806
4453
  }
3807
- function renderResumeHintCells(width, done) {
3808
- return centerLineCells(textToCells(done ? DONE_HINT : RESUME_HINT, "dim"), width);
4454
+ function renderResumeHintCells(width, interruptHint) {
4455
+ return centerLineCells(textToCells(interruptHint === "exit" ? DONE_HINT : interruptHint === "force-stop" ? GRACEFUL_STOP_HINT : RESUME_HINT, "dim"), width);
3809
4456
  }
3810
4457
  /**
3811
4458
  * Builds the centered content viewport for the renderer.
@@ -3845,7 +4492,7 @@ function buildContentCells(prompt, agentName, state, elapsed, now, availableHeig
3845
4492
  agent: [
3846
4493
  [],
3847
4494
  [],
3848
- ...renderAgentMessageCells(state.lastMessage, state.status)
4495
+ ...renderAgentMessageCells(state.lastMessage, state.status, state.lastAgentError)
3849
4496
  ],
3850
4497
  moon: [
3851
4498
  [],
@@ -3913,8 +4560,7 @@ function buildFrameCells(prompt, agentName, state, topStars, bottomStars, sideSt
3913
4560
  ]);
3914
4561
  }
3915
4562
  for (let y = 0; y < bottomHeight; y++) frame.push(renderStarLineCells(bottomStars, terminalWidth, y, now));
3916
- const isDone = state.status === "aborted";
3917
- frame.push(renderResumeHintCells(terminalWidth, isDone));
4563
+ frame.push(renderResumeHintCells(terminalWidth, state.interruptHint));
3918
4564
  frame.push(emptyCells(terminalWidth));
3919
4565
  return frame;
3920
4566
  }
@@ -3938,6 +4584,7 @@ var Renderer = class {
3938
4584
  seedTop;
3939
4585
  seedBottom;
3940
4586
  seedSide;
4587
+ onInterrupt;
3941
4588
  handleState = (newState) => {
3942
4589
  this.state = {
3943
4590
  ...newState,
@@ -3948,10 +4595,11 @@ var Renderer = class {
3948
4595
  handleStopped = () => {
3949
4596
  this.stop("stopped");
3950
4597
  };
3951
- constructor(orchestrator, prompt, agentName) {
4598
+ constructor(orchestrator, prompt, agentName, onInterrupt) {
3952
4599
  this.orchestrator = orchestrator;
3953
4600
  this.prompt = prompt;
3954
4601
  this.agentName = agentName;
4602
+ this.onInterrupt = onInterrupt;
3955
4603
  this.state = orchestrator.getState();
3956
4604
  this.seedTop = Math.floor(Math.random() * 2147483646) + 1;
3957
4605
  this.seedBottom = Math.floor(Math.random() * 2147483646) + 1;
@@ -3967,10 +4615,7 @@ var Renderer = class {
3967
4615
  process$1.stdin.setRawMode(true);
3968
4616
  process$1.stdin.resume();
3969
4617
  process$1.stdin.on("data", (data) => {
3970
- if (data[0] === 3) {
3971
- this.stop("interrupted");
3972
- this.orchestrator.stop();
3973
- }
4618
+ if (data[0] === 3) this.onInterrupt();
3974
4619
  });
3975
4620
  }
3976
4621
  this.interval = setInterval(() => this.render(), TICK_MS);
@@ -4067,6 +4712,8 @@ const GNHF_REEXEC_STDIN_PROMPT = "GNHF_REEXEC_STDIN_PROMPT";
4067
4712
  const GNHF_REEXEC_STDIN_PROMPT_FILE = "GNHF_REEXEC_STDIN_PROMPT_FILE";
4068
4713
  const GNHF_REEXEC_STDIN_PROMPT_DIR_PREFIX = "gnhf-stdin-";
4069
4714
  const GNHF_REEXEC_STDIN_PROMPT_FILENAME = "prompt.txt";
4715
+ const AGENT_NAME_SET = new Set(AGENT_NAMES);
4716
+ const AGENT_NAME_LIST = `"${AGENT_NAMES.slice(0, -1).join("\", \"")}", or "${AGENT_NAMES[AGENT_NAMES.length - 1]}"`;
4070
4717
  var PromptSignalError = class extends Error {
4071
4718
  constructor(signal) {
4072
4719
  super(signal);
@@ -4088,6 +4735,22 @@ function humanizeErrorMessage(message) {
4088
4735
  if (message.includes("not a git repository")) return "This command must be run inside a Git repository. Change into a repo or run \"git init\" first.";
4089
4736
  return message;
4090
4737
  }
4738
+ function isAgentName(name) {
4739
+ return AGENT_NAME_SET.has(name);
4740
+ }
4741
+ function buildSchemaOptions(stopWhen) {
4742
+ return stopWhen === void 0 ? { includeStopField: false } : {
4743
+ includeStopField: true,
4744
+ stopWhen
4745
+ };
4746
+ }
4747
+ function buildResumeSchemaOptions(stopWhen) {
4748
+ if (stopWhen === "") return {
4749
+ includeStopField: false,
4750
+ clearStopWhen: true
4751
+ };
4752
+ return buildSchemaOptions(stopWhen);
4753
+ }
4091
4754
  function initializeNewBranch(prompt, cwd, schemaOptions) {
4092
4755
  ensureCleanWorkingTree(cwd);
4093
4756
  const baseCommit = getHeadCommit(cwd);
@@ -4102,11 +4765,27 @@ function initializeWorktreeRun(prompt, cwd, schemaOptions) {
4102
4765
  const branchName = slugifyPrompt(prompt);
4103
4766
  const runId = branchName.split("/")[1];
4104
4767
  const worktreePath = join(dirname(repoRoot), `${basename(repoRoot)}-gnhf-worktrees`, runId);
4768
+ if (worktreeExists(repoRoot, worktreePath) && existsSync(join(worktreePath, ".gnhf", "runs", runId))) {
4769
+ let worktreeBranch;
4770
+ try {
4771
+ worktreeBranch = getCurrentBranch(worktreePath);
4772
+ } catch (error) {
4773
+ throw new Error(`Preserved worktree at ${worktreePath} is in an unexpected state (${error instanceof Error ? error.message : String(error)}). Fix the worktree manually or remove it with "git worktree remove ${worktreePath}" before re-running.`);
4774
+ }
4775
+ if (worktreeBranch !== branchName) throw new Error(`Preserved worktree at ${worktreePath} is on branch "${worktreeBranch}" rather than "${branchName}". Restore it to "${branchName}" with "git -C ${worktreePath} checkout ${branchName}", or remove the worktree with "git worktree remove ${worktreePath}" to start fresh.`);
4776
+ return {
4777
+ runInfo: resumeRun(runId, worktreePath, schemaOptions),
4778
+ worktreePath,
4779
+ effectiveCwd: worktreePath,
4780
+ resumed: true
4781
+ };
4782
+ }
4105
4783
  createWorktree(repoRoot, worktreePath, branchName);
4106
4784
  return {
4107
4785
  runInfo: setupRun(runId, prompt, baseCommit, worktreePath, schemaOptions),
4108
4786
  worktreePath,
4109
- effectiveCwd: worktreePath
4787
+ effectiveCwd: worktreePath,
4788
+ resumed: false
4110
4789
  };
4111
4790
  }
4112
4791
  function openPromptTerminal() {
@@ -4232,11 +4911,13 @@ function readReexecStdinPrompt(env) {
4232
4911
  }
4233
4912
  }
4234
4913
  const program = new Command();
4235
- program.name("gnhf").description("Before I go to bed, I tell my agents: good night, have fun").version(packageVersion).argument("[prompt]", "The objective for the coding agent").option("--agent <agent>", "Agent to use (claude, codex, rovodev, or opencode)").option("--max-iterations <n>", "Abort after N total iterations", parseNonNegativeInteger).option("--max-tokens <n>", "Abort after N total input+output tokens", parseNonNegativeInteger).option("--stop-when <condition>", "End the loop when the agent reports this natural-language condition is met").option("--prevent-sleep <mode>", "Prevent system sleep during the run (\"on\" or \"off\")", parseOnOffBoolean).option("--worktree", "Run in a separate git worktree (enables multiple agents on the same repo)", false).option("--mock", "", false).action(async (promptArg, options) => {
4914
+ program.name("gnhf").description("Before I go to bed, I tell my agents: good night, have fun").version(packageVersion).argument("[prompt]", "The objective for the coding agent").option("--agent <agent>", `Agent to use (${AGENT_NAMES.join(", ")})`).option("--max-iterations <n>", "Abort after N total iterations", parseNonNegativeInteger).option("--max-tokens <n>", "Abort after N total input+output tokens", parseNonNegativeInteger).option("--stop-when <condition>", "End when the agent reports this condition; resumes reuse it, pass a new value to overwrite or \"\" to clear").option("--prevent-sleep <mode>", "Prevent system sleep during the run (\"on\" or \"off\")", parseOnOffBoolean).option("--worktree", "Run in a separate git worktree (enables multiple agents on the same repo)", false).option("--mock", "", false).action(async (promptArg, options) => {
4236
4915
  if (options.mock) {
4237
4916
  const mock = new MockOrchestrator();
4238
4917
  enterAltScreen();
4239
- const renderer = new Renderer(mock, "let's minimize app startup latency without sacrificing any functionality", "claude");
4918
+ const renderer = new Renderer(mock, "let's minimize app startup latency without sacrificing any functionality", "claude", () => {
4919
+ mock.handleInterrupt();
4920
+ });
4240
4921
  renderer.start();
4241
4922
  mock.start();
4242
4923
  await renderer.waitUntilExit();
@@ -4248,16 +4929,16 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
4248
4929
  let prompt = promptArg;
4249
4930
  let promptFromStdin = false;
4250
4931
  const agentName = options.agent;
4251
- if (agentName !== void 0 && agentName !== "claude" && agentName !== "codex" && agentName !== "rovodev" && agentName !== "opencode") {
4252
- console.error(`Unknown agent: ${options.agent}. Use "claude", "codex", "rovodev", or "opencode".`);
4932
+ if (agentName !== void 0 && !isAgentName(agentName)) {
4933
+ console.error(`Unknown agent: ${options.agent}. Use ${AGENT_NAME_LIST}.`);
4253
4934
  process$1.exit(1);
4254
4935
  }
4255
4936
  const config = {
4256
4937
  ...loadConfig(agentName ? { agent: agentName } : {}),
4257
4938
  ...options.preventSleep === void 0 ? {} : { preventSleep: options.preventSleep }
4258
4939
  };
4259
- if (config.agent !== "claude" && config.agent !== "codex" && config.agent !== "rovodev" && config.agent !== "opencode") {
4260
- console.error(`Unknown agent: ${config.agent}. Use "claude", "codex", "rovodev", or "opencode".`);
4940
+ if (!isAgentName(config.agent)) {
4941
+ console.error(`Unknown agent: ${config.agent}. Use ${AGENT_NAME_LIST}.`);
4261
4942
  process$1.exit(1);
4262
4943
  }
4263
4944
  if (!prompt && process$1.env.GNHF_SLEEP_INHIBITED === "1") prompt = readReexecStdinPrompt(process$1.env);
@@ -4271,7 +4952,9 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
4271
4952
  let worktreeCleanup = null;
4272
4953
  const currentBranch = getCurrentBranch(cwd);
4273
4954
  const onGnhfBranch = currentBranch.startsWith("gnhf/");
4274
- const schemaOptions = { includeStopField: options.stopWhen !== void 0 };
4955
+ const cliStopWhen = options.stopWhen === "" ? void 0 : options.stopWhen;
4956
+ let effectiveStopWhen = cliStopWhen;
4957
+ let schemaOptions = buildSchemaOptions(effectiveStopWhen);
4275
4958
  let runInfo;
4276
4959
  let startIteration = 0;
4277
4960
  if (options.worktree) {
@@ -4287,31 +4970,49 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
4287
4970
  runInfo = wt.runInfo;
4288
4971
  effectiveCwd = wt.effectiveCwd;
4289
4972
  worktreePath = wt.worktreePath;
4290
- worktreeCleanup = () => {
4291
- try {
4292
- removeWorktree(cwd, wt.worktreePath);
4293
- } catch {}
4294
- };
4295
- const exitCleanup = worktreeCleanup;
4296
- process$1.on("exit", () => {
4297
- if (worktreeCleanup === exitCleanup) exitCleanup();
4298
- });
4973
+ if (wt.resumed) {
4974
+ startIteration = getLastIterationNumber(runInfo);
4975
+ console.error(`\n gnhf: resuming preserved worktree at ${worktreePath}\n gnhf: continuing run ${runInfo.runId} from iteration ${startIteration}\n`);
4976
+ } else {
4977
+ worktreeCleanup = () => {
4978
+ try {
4979
+ removeWorktree(cwd, wt.worktreePath);
4980
+ } catch {}
4981
+ };
4982
+ const exitCleanup = worktreeCleanup;
4983
+ process$1.on("exit", () => {
4984
+ if (worktreeCleanup === exitCleanup) exitCleanup();
4985
+ });
4986
+ }
4299
4987
  } else if (onGnhfBranch) {
4300
4988
  const existingRunId = currentBranch.slice(5);
4301
- const existing = resumeRun(existingRunId, cwd, schemaOptions);
4989
+ let existing = resumeRun(existingRunId, cwd, { includeStopField: false });
4302
4990
  const existingPrompt = readFileSync(existing.promptPath, "utf-8");
4303
4991
  if (!prompt || prompt === existingPrompt) {
4992
+ existing = resumeRun(existingRunId, cwd, buildResumeSchemaOptions(options.stopWhen));
4993
+ const resumeStopWhen = existing.stopWhen;
4994
+ const resumeSchemaOptions = buildSchemaOptions(resumeStopWhen);
4304
4995
  prompt = existingPrompt;
4305
4996
  runInfo = existing;
4997
+ effectiveStopWhen = resumeStopWhen;
4998
+ schemaOptions = resumeSchemaOptions;
4306
4999
  startIteration = getLastIterationNumber(existing);
4307
5000
  } else {
4308
5001
  const answer = await ask(`You are on gnhf branch "${currentBranch}".\n (o) Update prompt and continue current run\n (n) Start a new branch on top of this one\n (q) Quit\nChoose [o/n/q]: `, "The overwrite prompt closed before a choice was entered. Re-run gnhf from an interactive terminal and choose o, n, or q.", "Cannot show the overwrite prompt because stdin is not interactive. Re-run gnhf from an interactive terminal and choose o, n, or q.");
4309
5002
  if (answer === "o") {
4310
5003
  ensureCleanWorkingTree(cwd);
4311
- runInfo = setupRun(existingRunId, prompt, existing.baseCommit, cwd, schemaOptions);
5004
+ existing = resumeRun(existingRunId, cwd, buildResumeSchemaOptions(options.stopWhen));
5005
+ const resumeStopWhen = existing.stopWhen;
5006
+ const resumeSchemaOptions = buildSchemaOptions(resumeStopWhen);
5007
+ runInfo = setupRun(existingRunId, prompt, existing.baseCommit, cwd, resumeSchemaOptions);
5008
+ effectiveStopWhen = resumeStopWhen;
5009
+ schemaOptions = resumeSchemaOptions;
4312
5010
  startIteration = getLastIterationNumber(existing);
4313
- } else if (answer === "n") runInfo = initializeNewBranch(prompt, cwd, schemaOptions);
4314
- else process$1.exit(0);
5011
+ } else if (answer === "n") {
5012
+ effectiveStopWhen = cliStopWhen;
5013
+ schemaOptions = buildSchemaOptions(effectiveStopWhen);
5014
+ runInfo = initializeNewBranch(prompt, cwd, schemaOptions);
5015
+ } else process$1.exit(0);
4315
5016
  }
4316
5017
  } else {
4317
5018
  if (!prompt) {
@@ -4346,7 +5047,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
4346
5047
  startIteration,
4347
5048
  maxIterations: options.maxIterations,
4348
5049
  maxTokens: options.maxTokens,
4349
- stopWhen: options.stopWhen,
5050
+ stopWhen: effectiveStopWhen,
4350
5051
  preventSleep: config.preventSleep,
4351
5052
  agentArgsOverride: config.agentArgsOverride?.[config.agent],
4352
5053
  worktree: options.worktree,
@@ -4358,21 +5059,39 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
4358
5059
  const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent], config.agentArgsOverride?.[config.agent], schemaOptions), runInfo, prompt, effectiveCwd, startIteration, {
4359
5060
  maxIterations: options.maxIterations,
4360
5061
  maxTokens: options.maxTokens,
4361
- stopWhen: options.stopWhen
5062
+ stopWhen: effectiveStopWhen
4362
5063
  });
4363
5064
  let shutdownSignal = null;
4364
- enterAltScreen();
4365
- const renderer = new Renderer(orchestrator, prompt, config.agent);
4366
- renderer.start();
4367
- const requestShutdown = (signal) => {
4368
- if (shutdownSignal) return;
5065
+ let forceShutdownRequested = false;
5066
+ const requestForceShutdown = (signal) => {
5067
+ if (forceShutdownRequested) return;
5068
+ forceShutdownRequested = true;
4369
5069
  shutdownSignal = signal;
4370
5070
  appendDebugLog(`signal:${signal}`);
4371
5071
  renderer.stop();
5072
+ };
5073
+ const handleSigInt = () => {
5074
+ const disposition = orchestrator.handleInterrupt();
5075
+ if (disposition === "force-stop") {
5076
+ requestForceShutdown("SIGINT");
5077
+ return;
5078
+ }
5079
+ if (disposition === "exit") {
5080
+ shutdownSignal = "SIGINT";
5081
+ appendDebugLog("signal:SIGINT");
5082
+ renderer.stop("interrupted");
5083
+ return;
5084
+ }
5085
+ shutdownSignal = "SIGINT";
5086
+ appendDebugLog("signal:SIGINT");
5087
+ };
5088
+ const handleSigTerm = () => {
4372
5089
  orchestrator.stop();
5090
+ requestForceShutdown("SIGTERM");
4373
5091
  };
4374
- const handleSigInt = () => requestShutdown("SIGINT");
4375
- const handleSigTerm = () => requestShutdown("SIGTERM");
5092
+ enterAltScreen();
5093
+ const renderer = new Renderer(orchestrator, prompt, config.agent, handleSigInt);
5094
+ renderer.start();
4376
5095
  process$1.on("SIGINT", handleSigInt);
4377
5096
  process$1.on("SIGTERM", handleSigTerm);
4378
5097
  const orchestratorPromise = orchestrator.start().finally(() => {
@@ -4413,6 +5132,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
4413
5132
  commitCount: finalState.commitCount,
4414
5133
  worktreePath
4415
5134
  });
5135
+ if (finalState.status === "aborted") console.error(`\n gnhf: Run log: ${runInfo.logPath}\n`);
4416
5136
  if (worktreePath) if (finalState.commitCount > 0) {
4417
5137
  worktreeCleanup = null;
4418
5138
  console.error(`\n gnhf: worktree preserved at ${worktreePath}\n gnhf: merge the branch and remove with: git worktree remove "${worktreePath}"\n`);