gnhf 0.1.25 → 0.1.27

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 +44 -16
  2. package/dist/cli.mjs +799 -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) {
@@ -460,9 +495,15 @@ function buildAgentOutputSchema(opts) {
460
495
  //#endregion
461
496
  //#region src/core/run.ts
462
497
  const LOG_FILENAME = "gnhf.log";
498
+ const STOP_WHEN_FILENAME = "stop-when";
463
499
  function writeSchemaFile(schemaPath, includeStopField) {
464
500
  writeFileSync(schemaPath, JSON.stringify(buildAgentOutputSchema({ includeStopField }), null, 2), "utf-8");
465
501
  }
502
+ function readStopWhen(stopWhenPath) {
503
+ if (!existsSync(stopWhenPath)) return void 0;
504
+ const stopWhen = readFileSync(stopWhenPath, "utf-8").trim();
505
+ return stopWhen.length > 0 ? stopWhen : void 0;
506
+ }
466
507
  function ensureRunMetadataIgnored(cwd) {
467
508
  const excludePath = execFileSync("git", [
468
509
  "rev-parse",
@@ -496,6 +537,9 @@ function setupRun(runId, prompt, baseCommit, cwd, schemaOptions) {
496
537
  const hasStoredBaseCommit = existsSync(baseCommitPath);
497
538
  const resolvedBaseCommit = hasStoredBaseCommit ? readFileSync(baseCommitPath, "utf-8").trim() : baseCommit;
498
539
  if (!hasStoredBaseCommit) writeFileSync(baseCommitPath, `${baseCommit}\n`, "utf-8");
540
+ const stopWhenPath = join(runDir, STOP_WHEN_FILENAME);
541
+ const stopWhen = schemaOptions.stopWhen;
542
+ if (stopWhen !== void 0) writeFileSync(stopWhenPath, `${stopWhen}\n`, "utf-8");
499
543
  return {
500
544
  runId,
501
545
  runDir,
@@ -504,7 +548,9 @@ function setupRun(runId, prompt, baseCommit, cwd, schemaOptions) {
504
548
  schemaPath,
505
549
  logPath,
506
550
  baseCommit: resolvedBaseCommit,
507
- baseCommitPath
551
+ baseCommitPath,
552
+ stopWhenPath,
553
+ stopWhen
508
554
  };
509
555
  }
510
556
  function resumeRun(runId, cwd, schemaOptions) {
@@ -513,9 +559,19 @@ function resumeRun(runId, cwd, schemaOptions) {
513
559
  const promptPath = join(runDir, "prompt.md");
514
560
  const notesPath = join(runDir, "notes.md");
515
561
  const schemaPath = join(runDir, "output-schema.json");
516
- writeSchemaFile(schemaPath, schemaOptions.includeStopField);
517
562
  const logPath = join(runDir, LOG_FILENAME);
518
563
  const baseCommitPath = join(runDir, "base-commit");
564
+ const baseCommit = existsSync(baseCommitPath) ? readFileSync(baseCommitPath, "utf-8").trim() : backfillLegacyBaseCommit(runId, baseCommitPath, cwd);
565
+ const stopWhenPath = join(runDir, STOP_WHEN_FILENAME);
566
+ let stopWhen = readStopWhen(stopWhenPath);
567
+ if (schemaOptions.clearStopWhen) {
568
+ rmSync(stopWhenPath, { force: true });
569
+ stopWhen = void 0;
570
+ } else if (schemaOptions.stopWhen !== void 0) {
571
+ stopWhen = schemaOptions.stopWhen;
572
+ writeFileSync(stopWhenPath, `${stopWhen}\n`, "utf-8");
573
+ }
574
+ writeSchemaFile(schemaPath, schemaOptions.includeStopField || stopWhen !== void 0);
519
575
  return {
520
576
  runId,
521
577
  runDir,
@@ -523,8 +579,10 @@ function resumeRun(runId, cwd, schemaOptions) {
523
579
  notesPath,
524
580
  schemaPath,
525
581
  logPath,
526
- baseCommit: existsSync(baseCommitPath) ? readFileSync(baseCommitPath, "utf-8").trim() : backfillLegacyBaseCommit(runId, baseCommitPath, cwd),
527
- baseCommitPath
582
+ baseCommit,
583
+ baseCommitPath,
584
+ stopWhenPath,
585
+ stopWhen
528
586
  };
529
587
  }
530
588
  function backfillLegacyBaseCommit(runId, baseCommitPath, cwd) {
@@ -1043,7 +1101,8 @@ function setupAbortHandler(signal, child, reject, abortChild = () => {
1043
1101
  }
1044
1102
  //#endregion
1045
1103
  //#region src/core/agents/claude.ts
1046
- function shouldUseWindowsShell$2(bin, platform) {
1104
+ const DEFAULT_FINAL_RESULT_EXIT_GRACE_MS = 15e3;
1105
+ function shouldUseWindowsShell$4(bin, platform) {
1047
1106
  if (platform !== "win32") return false;
1048
1107
  if (/\.(cmd|bat)$/i.test(bin)) return true;
1049
1108
  if (/[\\/]/.test(bin)) return false;
@@ -1073,8 +1132,22 @@ function terminateClaudeProcess(child, platform) {
1073
1132
  } catch {}
1074
1133
  return;
1075
1134
  }
1135
+ if (child.pid) try {
1136
+ process.kill(-child.pid, "SIGTERM");
1137
+ return;
1138
+ } catch {}
1076
1139
  child.kill("SIGTERM");
1077
1140
  }
1141
+ async function shutdownClaudeProcess(child, platform) {
1142
+ if (platform === "win32") {
1143
+ terminateClaudeProcess(child, platform);
1144
+ return;
1145
+ }
1146
+ await shutdownChildProcess(child, { detached: true });
1147
+ }
1148
+ function isFinalStructuredResult(event) {
1149
+ return !event.is_error && event.subtype === "success" && !!event.structured_output;
1150
+ }
1078
1151
  function buildClaudeArgs(prompt, schema, extraArgs) {
1079
1152
  const userArgs = extraArgs ?? [];
1080
1153
  const userSpecifiedPermissionMode = userArgs.some((arg) => arg === "--dangerously-skip-permissions" || arg === "--permission-mode" || arg.startsWith("--permission-mode=") || arg === "--permission-prompt-tool" || arg.startsWith("--permission-prompt-tool="));
@@ -1090,7 +1163,7 @@ function buildClaudeArgs(prompt, schema, extraArgs) {
1090
1163
  ...userSpecifiedPermissionMode ? [] : ["--dangerously-skip-permissions"]
1091
1164
  ];
1092
1165
  }
1093
- function toTokenUsage(usage) {
1166
+ function toTokenUsage$1(usage) {
1094
1167
  return {
1095
1168
  inputTokens: (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0),
1096
1169
  outputTokens: usage.output_tokens ?? 0,
@@ -1098,22 +1171,24 @@ function toTokenUsage(usage) {
1098
1171
  cacheCreationTokens: usage.cache_creation_input_tokens ?? 0
1099
1172
  };
1100
1173
  }
1101
- function isSameUsage(a, b) {
1174
+ function isSameUsage$1(a, b) {
1102
1175
  return a.inputTokens === b.inputTokens && a.outputTokens === b.outputTokens && a.cacheReadTokens === b.cacheReadTokens && a.cacheCreationTokens === b.cacheCreationTokens;
1103
1176
  }
1104
1177
  function extendsUsage(next, previous) {
1105
- return next.inputTokens >= previous.inputTokens && next.outputTokens >= previous.outputTokens && next.cacheReadTokens >= previous.cacheReadTokens && next.cacheCreationTokens >= previous.cacheCreationTokens && !isSameUsage(next, previous);
1178
+ return next.inputTokens >= previous.inputTokens && next.outputTokens >= previous.outputTokens && next.cacheReadTokens >= previous.cacheReadTokens && next.cacheCreationTokens >= previous.cacheCreationTokens && !isSameUsage$1(next, previous);
1106
1179
  }
1107
1180
  var ClaudeAgent = class {
1108
1181
  name = "claude";
1109
1182
  bin;
1110
1183
  extraArgs;
1184
+ finalResultGraceMs;
1111
1185
  platform;
1112
1186
  schema;
1113
1187
  constructor(binOrDeps = {}) {
1114
1188
  const deps = typeof binOrDeps === "string" ? { bin: binOrDeps } : binOrDeps;
1115
1189
  this.bin = deps.bin ?? "claude";
1116
1190
  this.extraArgs = deps.extraArgs;
1191
+ this.finalResultGraceMs = deps.finalResultGraceMs ?? DEFAULT_FINAL_RESULT_EXIT_GRACE_MS;
1117
1192
  this.platform = deps.platform ?? process.platform;
1118
1193
  this.schema = deps.schema ?? buildAgentOutputSchema({ includeStopField: false });
1119
1194
  }
@@ -1123,7 +1198,8 @@ var ClaudeAgent = class {
1123
1198
  const logStream = logPath ? createWriteStream(logPath) : null;
1124
1199
  const child = spawn(this.bin, buildClaudeArgs(prompt, this.schema, this.extraArgs), {
1125
1200
  cwd,
1126
- shell: shouldUseWindowsShell$2(this.bin, this.platform),
1201
+ detached: this.platform !== "win32",
1202
+ shell: shouldUseWindowsShell$4(this.bin, this.platform),
1127
1203
  stdio: [
1128
1204
  "ignore",
1129
1205
  "pipe",
@@ -1133,7 +1209,11 @@ var ClaudeAgent = class {
1133
1209
  });
1134
1210
  if (setupAbortHandler(signal, child, reject, () => terminateClaudeProcess(child, this.platform))) return;
1135
1211
  let resultEvent = null;
1212
+ let finalStructuredResultEvent = null;
1136
1213
  let latestResultUsage = null;
1214
+ let finalResultCleanupTimer = null;
1215
+ let closedAfterFinalCleanup = false;
1216
+ let stderr = "";
1137
1217
  const cumulative = {
1138
1218
  inputTokens: 0,
1139
1219
  outputTokens: 0,
@@ -1145,10 +1225,16 @@ var ClaudeAgent = class {
1145
1225
  let lastAnonymousAssistantId = null;
1146
1226
  let lastAnonymousAssistantUsage = null;
1147
1227
  let pendingAnonymousAssistantUsage = null;
1228
+ child.stderr.on("data", (data) => {
1229
+ stderr += data.toString();
1230
+ });
1231
+ child.on("error", (err) => {
1232
+ reject(/* @__PURE__ */ new Error(`Failed to spawn claude: ${err.message}`));
1233
+ });
1148
1234
  parseJSONLStream(child.stdout, logStream, (event) => {
1149
1235
  if (event.type === "assistant") {
1150
1236
  const msg = event.message;
1151
- const nextUsage = toTokenUsage(msg.usage);
1237
+ const nextUsage = toTokenUsage$1(msg.usage);
1152
1238
  let messageId = msg.id;
1153
1239
  let previousUsage;
1154
1240
  if (messageId) {
@@ -1172,7 +1258,7 @@ var ClaudeAgent = class {
1172
1258
  previousUsage = usageByMessageId.get(messageId);
1173
1259
  pendingAnonymousAssistantUsage = null;
1174
1260
  lastAnonymousAssistantUsage = nextUsage;
1175
- } else if (lastAnonymousAssistantId && lastAnonymousAssistantUsage && isSameUsage(nextUsage, lastAnonymousAssistantUsage)) {
1261
+ } else if (lastAnonymousAssistantId && lastAnonymousAssistantUsage && isSameUsage$1(nextUsage, lastAnonymousAssistantUsage)) {
1176
1262
  messageId = lastAnonymousAssistantId;
1177
1263
  previousUsage = usageByMessageId.get(messageId);
1178
1264
  pendingAnonymousAssistantUsage ??= nextUsage;
@@ -1205,24 +1291,38 @@ var ClaudeAgent = class {
1205
1291
  if (event.type === "result") {
1206
1292
  const next = event;
1207
1293
  latestResultUsage = next.usage;
1208
- if (next.is_error || next.subtype !== "success" || next.structured_output || !resultEvent) resultEvent = next;
1294
+ if (isFinalStructuredResult(next)) {
1295
+ finalStructuredResultEvent = next;
1296
+ if (finalResultCleanupTimer) clearTimeout(finalResultCleanupTimer);
1297
+ finalResultCleanupTimer = setTimeout(() => {
1298
+ closedAfterFinalCleanup = true;
1299
+ shutdownClaudeProcess(child, this.platform);
1300
+ }, this.finalResultGraceMs);
1301
+ } else if (!finalStructuredResultEvent && (next.is_error || next.subtype !== "success" || next.structured_output || !resultEvent)) resultEvent = next;
1209
1302
  }
1210
1303
  });
1211
- setupChildProcessHandlers(child, "claude", logStream, reject, () => {
1212
- if (!resultEvent) {
1304
+ child.on("close", (code) => {
1305
+ if (finalResultCleanupTimer) clearTimeout(finalResultCleanupTimer);
1306
+ logStream?.end();
1307
+ if (code !== 0 && !closedAfterFinalCleanup) {
1308
+ reject(/* @__PURE__ */ new Error(`claude exited with code ${code}: ${stderr}`));
1309
+ return;
1310
+ }
1311
+ const terminalResultEvent = finalStructuredResultEvent ?? resultEvent;
1312
+ if (!terminalResultEvent) {
1213
1313
  reject(/* @__PURE__ */ new Error("claude returned no result event"));
1214
1314
  return;
1215
1315
  }
1216
- if (resultEvent.is_error || resultEvent.subtype !== "success") {
1217
- reject(/* @__PURE__ */ new Error(`claude reported error: ${JSON.stringify(resultEvent)}`));
1316
+ if (terminalResultEvent.is_error || terminalResultEvent.subtype !== "success") {
1317
+ reject(/* @__PURE__ */ new Error(`claude reported error: ${JSON.stringify(terminalResultEvent)}`));
1218
1318
  return;
1219
1319
  }
1220
- if (!resultEvent.structured_output) {
1320
+ if (!terminalResultEvent.structured_output) {
1221
1321
  reject(/* @__PURE__ */ new Error("claude returned no structured_output"));
1222
1322
  return;
1223
1323
  }
1224
- const output = resultEvent.structured_output;
1225
- const usage = toTokenUsage(latestResultUsage ?? resultEvent.usage);
1324
+ const output = terminalResultEvent.structured_output;
1325
+ const usage = toTokenUsage$1(latestResultUsage ?? terminalResultEvent.usage);
1226
1326
  onUsage?.(usage);
1227
1327
  resolve({
1228
1328
  output,
@@ -1233,8 +1333,180 @@ var ClaudeAgent = class {
1233
1333
  }
1234
1334
  };
1235
1335
  //#endregion
1336
+ //#region src/core/agents/copilot.ts
1337
+ function shouldUseWindowsShell$3(bin, platform) {
1338
+ if (platform !== "win32") return false;
1339
+ if (/\.(cmd|bat)$/i.test(bin)) return true;
1340
+ if (/[\\/]/.test(bin)) return false;
1341
+ try {
1342
+ const firstMatch = execFileSync("where", [bin], {
1343
+ encoding: "utf8",
1344
+ stdio: [
1345
+ "ignore",
1346
+ "pipe",
1347
+ "ignore"
1348
+ ]
1349
+ }).split(/\r?\n/).map((line) => line.trim()).find(Boolean);
1350
+ return firstMatch ? /\.(cmd|bat)$/i.test(firstMatch) : false;
1351
+ } catch {
1352
+ return false;
1353
+ }
1354
+ }
1355
+ function terminateCopilotProcess(child, platform) {
1356
+ if (platform === "win32" && child.pid) {
1357
+ try {
1358
+ execFileSync("taskkill", [
1359
+ "/T",
1360
+ "/F",
1361
+ "/PID",
1362
+ String(child.pid)
1363
+ ], { stdio: "ignore" });
1364
+ } catch {}
1365
+ return;
1366
+ }
1367
+ child.kill("SIGTERM");
1368
+ }
1369
+ function userSpecifiedPermissionMode(userArgs) {
1370
+ 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="));
1371
+ }
1372
+ function buildCopilotPrompt(prompt, schema) {
1373
+ return `${prompt}
1374
+
1375
+ ## gnhf final output contract
1376
+
1377
+ When the iteration is complete, your final answer must be a single JSON object that matches this JSON Schema:
1378
+
1379
+ \`\`\`json
1380
+ ${JSON.stringify(schema, null, 2)}
1381
+ \`\`\`
1382
+
1383
+ Return only the JSON object in the final answer. Do not wrap it in Markdown. Do not include explanatory prose outside the JSON object.`;
1384
+ }
1385
+ function buildCopilotArgs(prompt, schema, extraArgs) {
1386
+ const userArgs = extraArgs ?? [];
1387
+ return [
1388
+ ...userArgs,
1389
+ "-p",
1390
+ buildCopilotPrompt(prompt, schema),
1391
+ "--output-format",
1392
+ "json",
1393
+ "--stream",
1394
+ "off",
1395
+ "--no-color",
1396
+ ...userSpecifiedPermissionMode(userArgs) ? [] : ["--allow-all"]
1397
+ ];
1398
+ }
1399
+ function stripJsonFence(text) {
1400
+ const trimmed = text.trim();
1401
+ return trimmed.match(/^```(?:json)?\s*\n([\s\S]*?)\n```$/i)?.[1]?.trim() ?? trimmed;
1402
+ }
1403
+ function numberField$1(usage, names) {
1404
+ for (const name of names) {
1405
+ const value = usage[name];
1406
+ if (typeof value === "number") return value;
1407
+ }
1408
+ }
1409
+ function usageFromRecord(usage) {
1410
+ const inputTokens = numberField$1(usage, ["inputTokens", "input_tokens"]);
1411
+ const outputTokens = numberField$1(usage, ["outputTokens", "output_tokens"]);
1412
+ const cacheReadTokens = numberField$1(usage, [
1413
+ "cacheReadTokens",
1414
+ "cache_read_tokens",
1415
+ "cache_read_input_tokens"
1416
+ ]);
1417
+ const cacheCreationTokens = numberField$1(usage, [
1418
+ "cacheCreationTokens",
1419
+ "cacheWriteTokens",
1420
+ "cache_creation_tokens",
1421
+ "cache_creation_input_tokens",
1422
+ "cache_write_tokens"
1423
+ ]);
1424
+ if (inputTokens === void 0 && outputTokens === void 0 && cacheReadTokens === void 0 && cacheCreationTokens === void 0) return null;
1425
+ return {
1426
+ inputTokens: inputTokens ?? 0,
1427
+ outputTokens: outputTokens ?? 0,
1428
+ cacheReadTokens: cacheReadTokens ?? 0,
1429
+ cacheCreationTokens: cacheCreationTokens ?? 0
1430
+ };
1431
+ }
1432
+ var CopilotAgent = class {
1433
+ name = "copilot";
1434
+ bin;
1435
+ extraArgs;
1436
+ platform;
1437
+ schema;
1438
+ constructor(binOrDeps = {}) {
1439
+ const deps = typeof binOrDeps === "string" ? { bin: binOrDeps } : binOrDeps;
1440
+ this.bin = deps.bin ?? "copilot";
1441
+ this.extraArgs = deps.extraArgs;
1442
+ this.platform = deps.platform ?? process.platform;
1443
+ this.schema = deps.schema ?? buildAgentOutputSchema({ includeStopField: false });
1444
+ }
1445
+ run(prompt, cwd, options) {
1446
+ const { onUsage, onMessage, signal, logPath } = options ?? {};
1447
+ return new Promise((resolve, reject) => {
1448
+ const logStream = logPath ? createWriteStream(logPath) : null;
1449
+ const child = spawn(this.bin, buildCopilotArgs(prompt, this.schema, this.extraArgs), {
1450
+ cwd,
1451
+ shell: shouldUseWindowsShell$3(this.bin, this.platform),
1452
+ stdio: [
1453
+ "ignore",
1454
+ "pipe",
1455
+ "pipe"
1456
+ ],
1457
+ env: process.env
1458
+ });
1459
+ if (setupAbortHandler(signal, child, reject, () => terminateCopilotProcess(child, this.platform))) return;
1460
+ let lastAgentMessage = null;
1461
+ const cumulative = {
1462
+ inputTokens: 0,
1463
+ outputTokens: 0,
1464
+ cacheReadTokens: 0,
1465
+ cacheCreationTokens: 0
1466
+ };
1467
+ parseJSONLStream(child.stdout, logStream, (event) => {
1468
+ if (event.type === "assistant.message") {
1469
+ const data = event.data;
1470
+ if (typeof data.content === "string") {
1471
+ lastAgentMessage = data.content;
1472
+ onMessage?.(data.content);
1473
+ }
1474
+ if (typeof data.outputTokens === "number") {
1475
+ cumulative.outputTokens += data.outputTokens;
1476
+ onUsage?.({ ...cumulative });
1477
+ }
1478
+ }
1479
+ if ("usage" in event && event.usage) {
1480
+ const usage = usageFromRecord(event.usage);
1481
+ if (usage) {
1482
+ cumulative.inputTokens = usage.inputTokens;
1483
+ cumulative.outputTokens = Math.max(cumulative.outputTokens, usage.outputTokens);
1484
+ cumulative.cacheReadTokens = usage.cacheReadTokens;
1485
+ cumulative.cacheCreationTokens = usage.cacheCreationTokens;
1486
+ onUsage?.({ ...cumulative });
1487
+ }
1488
+ }
1489
+ });
1490
+ setupChildProcessHandlers(child, "copilot", logStream, reject, () => {
1491
+ if (!lastAgentMessage) {
1492
+ reject(/* @__PURE__ */ new Error("copilot returned no agent message"));
1493
+ return;
1494
+ }
1495
+ try {
1496
+ resolve({
1497
+ output: JSON.parse(stripJsonFence(lastAgentMessage)),
1498
+ usage: cumulative
1499
+ });
1500
+ } catch (err) {
1501
+ reject(/* @__PURE__ */ new Error(`Failed to parse copilot output: ${err instanceof Error ? err.message : err}`));
1502
+ }
1503
+ });
1504
+ });
1505
+ }
1506
+ };
1507
+ //#endregion
1236
1508
  //#region src/core/agents/codex.ts
1237
- function shouldUseWindowsShell$1(bin, platform) {
1509
+ function shouldUseWindowsShell$2(bin, platform) {
1238
1510
  if (platform !== "win32") return false;
1239
1511
  if (/\.(cmd|bat)$/i.test(bin)) return true;
1240
1512
  if (/[\\/]/.test(bin)) return false;
@@ -1300,7 +1572,7 @@ var CodexAgent = class {
1300
1572
  const logStream = logPath ? createWriteStream(logPath) : null;
1301
1573
  const child = spawn(this.bin, buildCodexArgs(prompt, this.schemaPath, this.extraArgs), {
1302
1574
  cwd,
1303
- shell: shouldUseWindowsShell$1(this.bin, this.platform),
1575
+ shell: shouldUseWindowsShell$2(this.bin, this.platform),
1304
1576
  stdio: [
1305
1577
  "ignore",
1306
1578
  "pipe",
@@ -2162,6 +2434,287 @@ var OpenCodeAgent = class {
2162
2434
  }
2163
2435
  };
2164
2436
  //#endregion
2437
+ //#region src/core/agents/pi.ts
2438
+ function shouldUseWindowsShell$1(bin, platform) {
2439
+ if (platform !== "win32") return false;
2440
+ if (/\.(cmd|bat)$/i.test(bin)) return true;
2441
+ if (/[\\/]/.test(bin)) return false;
2442
+ try {
2443
+ const firstMatch = execFileSync("where", [bin], {
2444
+ encoding: "utf8",
2445
+ stdio: [
2446
+ "ignore",
2447
+ "pipe",
2448
+ "ignore"
2449
+ ]
2450
+ }).split(/\r?\n/).map((line) => line.trim()).find(Boolean);
2451
+ return firstMatch ? /\.(cmd|bat)$/i.test(firstMatch) : false;
2452
+ } catch {
2453
+ return false;
2454
+ }
2455
+ }
2456
+ function terminatePiProcess(child, platform) {
2457
+ if (platform === "win32" && child.pid) {
2458
+ try {
2459
+ execFileSync("taskkill", [
2460
+ "/T",
2461
+ "/F",
2462
+ "/PID",
2463
+ String(child.pid)
2464
+ ], { stdio: "ignore" });
2465
+ } catch {}
2466
+ return;
2467
+ }
2468
+ if (child.pid) try {
2469
+ process.kill(-child.pid, "SIGTERM");
2470
+ return;
2471
+ } catch {}
2472
+ child.kill("SIGTERM");
2473
+ }
2474
+ function buildPiPrompt(prompt, schema) {
2475
+ return `${prompt}
2476
+
2477
+ ## gnhf final output contract
2478
+
2479
+ 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.
2480
+
2481
+ ${JSON.stringify(schema, null, 2)}`;
2482
+ }
2483
+ function buildPiArgs(extraArgs) {
2484
+ return [
2485
+ ...extraArgs ?? [],
2486
+ "--mode",
2487
+ "json",
2488
+ "--no-session"
2489
+ ];
2490
+ }
2491
+ function isRecord(value) {
2492
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2493
+ }
2494
+ function stringField(record, names) {
2495
+ for (const name of names) {
2496
+ const value = record[name];
2497
+ if (typeof value === "string") return value;
2498
+ }
2499
+ }
2500
+ function numberField(record, names) {
2501
+ for (const name of names) {
2502
+ const value = record[name];
2503
+ if (typeof value === "number") return value;
2504
+ }
2505
+ }
2506
+ function toTokenUsage(usage) {
2507
+ if (!usage) return null;
2508
+ return {
2509
+ inputTokens: numberField(usage, ["input"]) ?? 0,
2510
+ outputTokens: numberField(usage, ["output"]) ?? 0,
2511
+ cacheReadTokens: numberField(usage, ["cacheRead"]) ?? 0,
2512
+ cacheCreationTokens: numberField(usage, ["cacheWrite"]) ?? 0
2513
+ };
2514
+ }
2515
+ function isSameUsage(a, b) {
2516
+ return a.inputTokens === b.inputTokens && a.outputTokens === b.outputTokens && a.cacheReadTokens === b.cacheReadTokens && a.cacheCreationTokens === b.cacheCreationTokens;
2517
+ }
2518
+ function messageKey(message) {
2519
+ const responseId = stringField(message, ["responseId", "id"]);
2520
+ if (responseId) return responseId;
2521
+ const timestamp = message.timestamp;
2522
+ if (typeof timestamp === "string" || typeof timestamp === "number") return `timestamp:${timestamp}`;
2523
+ return null;
2524
+ }
2525
+ function roleOf(message) {
2526
+ return isRecord(message) && typeof message.role === "string" ? message.role : void 0;
2527
+ }
2528
+ function textFromContentBlock(block) {
2529
+ if (typeof block === "string") return block;
2530
+ if (!isRecord(block)) return null;
2531
+ if (typeof block.text === "string") return block.text;
2532
+ if (typeof block.content === "string") return block.content;
2533
+ return null;
2534
+ }
2535
+ function textFromAssistantMessage(message) {
2536
+ if (!message) return "";
2537
+ if (typeof message.text === "string") return message.text;
2538
+ if (typeof message.content === "string") return message.content;
2539
+ if (Array.isArray(message.content)) return message.content.map(textFromContentBlock).filter((text) => text !== null).join("");
2540
+ return "";
2541
+ }
2542
+ function compactJson(value) {
2543
+ try {
2544
+ return JSON.stringify(value);
2545
+ } catch {
2546
+ return String(value);
2547
+ }
2548
+ }
2549
+ function validatePiOutput(value, schema) {
2550
+ if (!isRecord(value)) throw new Error("expected an object");
2551
+ if (typeof value.success !== "boolean") throw new Error("success must be a boolean");
2552
+ if (typeof value.summary !== "string") throw new Error("summary must be a string");
2553
+ 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");
2554
+ 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");
2555
+ if (schema.required.includes("should_fully_stop") && typeof value.should_fully_stop !== "boolean") throw new Error("should_fully_stop must be a boolean");
2556
+ if (schema.additionalProperties === false) {
2557
+ const allowed = new Set(Object.keys(schema.properties));
2558
+ const extraKey = Object.keys(value).find((key) => !allowed.has(key));
2559
+ if (extraKey) throw new Error(`unexpected property ${extraKey}`);
2560
+ }
2561
+ return value;
2562
+ }
2563
+ function textByIndexToString(textByIndex) {
2564
+ return [...textByIndex.entries()].sort(([a], [b]) => a - b).map(([, text]) => text).join("");
2565
+ }
2566
+ var PiAgent = class {
2567
+ name = "pi";
2568
+ bin;
2569
+ extraArgs;
2570
+ platform;
2571
+ schema;
2572
+ constructor(deps = {}) {
2573
+ this.bin = deps.bin ?? "pi";
2574
+ this.extraArgs = deps.extraArgs;
2575
+ this.platform = deps.platform ?? process.platform;
2576
+ this.schema = deps.schema ?? buildAgentOutputSchema({ includeStopField: false });
2577
+ }
2578
+ run(prompt, cwd, options) {
2579
+ const { onUsage, onMessage, signal, logPath } = options ?? {};
2580
+ return new Promise((resolve, reject) => {
2581
+ const logStream = logPath ? createWriteStream(logPath) : null;
2582
+ const child = spawn(this.bin, buildPiArgs(this.extraArgs), {
2583
+ cwd,
2584
+ detached: this.platform !== "win32",
2585
+ shell: shouldUseWindowsShell$1(this.bin, this.platform),
2586
+ stdio: [
2587
+ "pipe",
2588
+ "pipe",
2589
+ "pipe"
2590
+ ],
2591
+ env: process.env
2592
+ });
2593
+ child.stdin?.write(buildPiPrompt(prompt, this.schema));
2594
+ child.stdin?.end();
2595
+ if (setupAbortHandler(signal, child, reject, () => terminatePiProcess(child, this.platform))) return;
2596
+ let latestAssistantMessage = null;
2597
+ const streamTextByIndex = /* @__PURE__ */ new Map();
2598
+ const completeTextByIndex = /* @__PURE__ */ new Map();
2599
+ const usageByMessageKey = /* @__PURE__ */ new Map();
2600
+ let lastEmittedUsage = {
2601
+ inputTokens: 0,
2602
+ outputTokens: 0,
2603
+ cacheReadTokens: 0,
2604
+ cacheCreationTokens: 0
2605
+ };
2606
+ let anonymousKeySeq = 0;
2607
+ let currentStreamingMessageKey = null;
2608
+ const updateUsage = (message, streaming = false) => {
2609
+ const usage = isRecord(message.usage) ? toTokenUsage(message.usage) : null;
2610
+ if (!usage) return;
2611
+ let key = messageKey(message);
2612
+ if (key === null) if (streaming && currentStreamingMessageKey !== null) key = currentStreamingMessageKey;
2613
+ else {
2614
+ key = `assistant-anonymous-${anonymousKeySeq++}`;
2615
+ if (streaming) currentStreamingMessageKey = key;
2616
+ }
2617
+ usageByMessageKey.set(key, usage);
2618
+ const cumulative = {
2619
+ inputTokens: 0,
2620
+ outputTokens: 0,
2621
+ cacheReadTokens: 0,
2622
+ cacheCreationTokens: 0
2623
+ };
2624
+ for (const entry of usageByMessageKey.values()) {
2625
+ cumulative.inputTokens += entry.inputTokens;
2626
+ cumulative.outputTokens += entry.outputTokens;
2627
+ cumulative.cacheReadTokens += entry.cacheReadTokens;
2628
+ cumulative.cacheCreationTokens += entry.cacheCreationTokens;
2629
+ }
2630
+ if (!isSameUsage(cumulative, lastEmittedUsage)) {
2631
+ lastEmittedUsage = cumulative;
2632
+ onUsage?.({ ...cumulative });
2633
+ }
2634
+ };
2635
+ const rememberAssistantMessage = (message, streaming = false) => {
2636
+ if (!isRecord(message) || roleOf(message) !== "assistant") return;
2637
+ latestAssistantMessage = message;
2638
+ updateUsage(message, streaming);
2639
+ };
2640
+ parseJSONLStream(child.stdout, logStream, (event) => {
2641
+ if (!isRecord(event)) return;
2642
+ if (event.type === "message_update") {
2643
+ rememberAssistantMessage(event.message, true);
2644
+ if (isRecord(event.assistantMessageEvent)) {
2645
+ const assistantEvent = event.assistantMessageEvent;
2646
+ const contentIndex = numberField(assistantEvent, ["contentIndex", "content_index"]) ?? 0;
2647
+ if (assistantEvent.type === "text_delta") {
2648
+ const delta = stringField(assistantEvent, [
2649
+ "delta",
2650
+ "text",
2651
+ "content"
2652
+ ]);
2653
+ if (delta) {
2654
+ const next = (streamTextByIndex.get(contentIndex) ?? "") + delta;
2655
+ streamTextByIndex.set(contentIndex, next);
2656
+ const visible = next.trim();
2657
+ if (visible) onMessage?.(visible);
2658
+ }
2659
+ }
2660
+ if (assistantEvent.type === "text_end") {
2661
+ const text = stringField(assistantEvent, ["text", "content"]) ?? streamTextByIndex.get(contentIndex) ?? "";
2662
+ completeTextByIndex.set(contentIndex, text);
2663
+ const visible = text.trim();
2664
+ if (visible) onMessage?.(visible);
2665
+ }
2666
+ }
2667
+ }
2668
+ if (event.type === "message_end" || event.type === "turn_end") {
2669
+ rememberAssistantMessage(event.message, true);
2670
+ currentStreamingMessageKey = null;
2671
+ }
2672
+ if (event.type === "agent_end" && Array.isArray(event.messages) && !latestAssistantMessage) for (let i = event.messages.length - 1; i >= 0; i -= 1) {
2673
+ const message = event.messages[i];
2674
+ if (roleOf(message) === "assistant") {
2675
+ rememberAssistantMessage(message);
2676
+ break;
2677
+ }
2678
+ }
2679
+ });
2680
+ setupChildProcessHandlers(child, "pi", logStream, reject, () => {
2681
+ if (latestAssistantMessage) {
2682
+ const stopReason = latestAssistantMessage.stopReason;
2683
+ if (stopReason === "error" || stopReason === "aborted") {
2684
+ const errorMessage = stringField(latestAssistantMessage, [
2685
+ "errorMessage",
2686
+ "error",
2687
+ "message"
2688
+ ]) ?? compactJson(latestAssistantMessage);
2689
+ reject(/* @__PURE__ */ new Error(`pi reported error: ${errorMessage}`));
2690
+ return;
2691
+ }
2692
+ }
2693
+ const finalText = textFromAssistantMessage(latestAssistantMessage).trim() || textByIndexToString(completeTextByIndex).trim() || textByIndexToString(streamTextByIndex).trim();
2694
+ if (!finalText) {
2695
+ reject(/* @__PURE__ */ new Error("pi returned no text output"));
2696
+ return;
2697
+ }
2698
+ let parsed;
2699
+ try {
2700
+ parsed = JSON.parse(finalText);
2701
+ } catch (err) {
2702
+ reject(/* @__PURE__ */ new Error(`Failed to parse pi output: ${err instanceof Error ? err.message : err}`));
2703
+ return;
2704
+ }
2705
+ try {
2706
+ resolve({
2707
+ output: validatePiOutput(parsed, this.schema),
2708
+ usage: lastEmittedUsage
2709
+ });
2710
+ } catch (err) {
2711
+ reject(/* @__PURE__ */ new Error(`Invalid pi output: ${err instanceof Error ? err.message : err}`));
2712
+ }
2713
+ });
2714
+ });
2715
+ }
2716
+ };
2717
+ //#endregion
2165
2718
  //#region src/core/agents/rovodev.ts
2166
2719
  function buildSystemPrompt(schema) {
2167
2720
  return [
@@ -2787,11 +3340,21 @@ function createAgent(name, runInfo, pathOverride, agentArgsOverride, options) {
2787
3340
  bin: pathOverride,
2788
3341
  extraArgs: agentArgsOverride
2789
3342
  });
3343
+ case "copilot": return new CopilotAgent({
3344
+ bin: pathOverride,
3345
+ extraArgs: agentArgsOverride,
3346
+ schema
3347
+ });
2790
3348
  case "opencode": return new OpenCodeAgent({
2791
3349
  bin: pathOverride,
2792
3350
  extraArgs: agentArgsOverride,
2793
3351
  schema
2794
3352
  });
3353
+ case "pi": return new PiAgent({
3354
+ bin: pathOverride,
3355
+ extraArgs: agentArgsOverride,
3356
+ schema
3357
+ });
2795
3358
  case "rovodev": return new RovoDevAgent(runInfo.schemaPath, {
2796
3359
  bin: pathOverride,
2797
3360
  extraArgs: agentArgsOverride
@@ -2799,6 +3362,19 @@ function createAgent(name, runInfo, pathOverride, agentArgsOverride, options) {
2799
3362
  }
2800
3363
  }
2801
3364
  //#endregion
3365
+ //#region src/core/interrupt-state.ts
3366
+ function getInterruptDisposition(state) {
3367
+ if (state.status === "aborted") return "exit";
3368
+ if (state.gracefulStopRequested || state.status === "stopped") return "force-stop";
3369
+ return "request-graceful-stop";
3370
+ }
3371
+ function getInterruptHint(state) {
3372
+ const disposition = getInterruptDisposition(state);
3373
+ if (disposition === "exit") return "exit";
3374
+ if (disposition === "force-stop") return "force-stop";
3375
+ return "resume";
3376
+ }
3377
+ //#endregion
2802
3378
  //#region src/templates/iteration-prompt.ts
2803
3379
  function buildIterationPrompt(params) {
2804
3380
  const outputFields = [
@@ -2818,7 +3394,8 @@ This is iteration ${params.n}. Each iteration aims to make an incremental step f
2818
3394
  2. Identify the next smallest logical unit of work that's individually verifiable and would make incremental progress towards the objective, and treat that as the scope of this iteration
2819
3395
  3. If you attempted a solution and it didn't end up moving the needle on the objective, document learnings and record success=false, then conclude the iteration rather than continuously pivoting
2820
3396
  4. If you made code changes, run build/tests/linters/formatters if available to validate your work. Do NOT make any git commits - that will be handled automatically by the gnhf orchestrator
2821
- 6. Finally, respond with a JSON object according to the provided schema
3397
+ 5. If you started any long-running background processes (dev servers, browsers, watchers, Electron, etc.), stop them before finishing the iteration
3398
+ 6. Only submit the final JSON object after the result is final: your work is complete, validation is done, and you have stopped any background processes you started
2822
3399
 
2823
3400
  ## Output
2824
3401
 
@@ -2844,8 +3421,10 @@ var Orchestrator = class extends EventEmitter {
2844
3421
  activeAbortController = null;
2845
3422
  pendingAbortReason = null;
2846
3423
  loopDone = false;
3424
+ stoppedEventEmitted = false;
2847
3425
  state = {
2848
3426
  status: "running",
3427
+ gracefulStopRequested: false,
2849
3428
  currentIteration: 0,
2850
3429
  totalInputTokens: 0,
2851
3430
  totalOutputTokens: 0,
@@ -2871,7 +3450,27 @@ var Orchestrator = class extends EventEmitter {
2871
3450
  this.state.commitCount = getBranchCommitCount(this.runInfo.baseCommit, this.cwd);
2872
3451
  }
2873
3452
  getState() {
2874
- return { ...this.state };
3453
+ return {
3454
+ ...this.state,
3455
+ interruptHint: getInterruptHint(this.state)
3456
+ };
3457
+ }
3458
+ requestGracefulStop() {
3459
+ if (this.stopRequested || this.state.gracefulStopRequested || this.loopDone) return;
3460
+ this.state.gracefulStopRequested = true;
3461
+ appendDebugLog("orchestrator:graceful-stop-requested", {
3462
+ iteration: this.state.currentIteration,
3463
+ hasActiveIteration: this.activeIterationPromise !== null,
3464
+ status: this.state.status
3465
+ });
3466
+ this.emit("state", this.getState());
3467
+ if (this.state.status === "waiting") this.activeAbortController?.abort();
3468
+ }
3469
+ handleInterrupt() {
3470
+ const disposition = getInterruptDisposition(this.state);
3471
+ if (disposition === "request-graceful-stop") this.requestGracefulStop();
3472
+ else if (disposition === "force-stop") this.stop();
3473
+ return disposition;
2875
3474
  }
2876
3475
  stop() {
2877
3476
  this.stopRequested = true;
@@ -2881,8 +3480,9 @@ var Orchestrator = class extends EventEmitter {
2881
3480
  loopDone: this.loopDone
2882
3481
  });
2883
3482
  this.activeAbortController?.abort();
3483
+ this.state.gracefulStopRequested = false;
2884
3484
  if (this.loopDone) {
2885
- this.emit("stopped");
3485
+ this.emitStopped();
2886
3486
  return;
2887
3487
  }
2888
3488
  if (this.stopPromise) return;
@@ -2907,7 +3507,7 @@ var Orchestrator = class extends EventEmitter {
2907
3507
  resetHard(this.cwd);
2908
3508
  this.state.status = "stopped";
2909
3509
  this.emit("state", this.getState());
2910
- this.emit("stopped");
3510
+ this.emitStopped();
2911
3511
  })();
2912
3512
  }
2913
3513
  async start() {
@@ -2931,6 +3531,7 @@ var Orchestrator = class extends EventEmitter {
2931
3531
  this.abort(preIterationAbortReason);
2932
3532
  break;
2933
3533
  }
3534
+ if (this.stopForGracefulShutdown()) break;
2934
3535
  this.state.currentIteration++;
2935
3536
  this.state.status = "running";
2936
3537
  this.emit("iteration:start", this.state.currentIteration);
@@ -2986,6 +3587,7 @@ var Orchestrator = class extends EventEmitter {
2986
3587
  totalOutputTokens: this.state.totalOutputTokens,
2987
3588
  commitCount: this.state.commitCount
2988
3589
  });
3590
+ if (this.stopForGracefulShutdown()) break;
2989
3591
  if (this.limits.stopWhen !== void 0 && result.shouldFullyStop) {
2990
3592
  this.abort("stop condition met");
2991
3593
  break;
@@ -3016,6 +3618,7 @@ var Orchestrator = class extends EventEmitter {
3016
3618
  });
3017
3619
  this.state.waitingUntil = null;
3018
3620
  if (!this.stopRequested) {
3621
+ if (this.stopForGracefulShutdown()) break;
3019
3622
  this.state.status = "running";
3020
3623
  this.emit("state", this.getState());
3021
3624
  }
@@ -3032,6 +3635,7 @@ var Orchestrator = class extends EventEmitter {
3032
3635
  if (this.stopPromise) await this.stopPromise;
3033
3636
  else await this.closeAgent();
3034
3637
  this.loopDone = true;
3638
+ if (this.didStopWithoutForce()) this.emitStopped();
3035
3639
  appendDebugLog("orchestrator:end", {
3036
3640
  status: this.state.status,
3037
3641
  iterations: this.state.currentIteration,
@@ -3194,8 +3798,27 @@ var Orchestrator = class extends EventEmitter {
3194
3798
  if (totalTokens < this.limits.maxTokens) return null;
3195
3799
  return `max tokens reached (${totalTokens}/${this.limits.maxTokens})`;
3196
3800
  }
3801
+ finishGracefulStop() {
3802
+ this.state.status = "stopped";
3803
+ this.state.gracefulStopRequested = false;
3804
+ this.state.waitingUntil = null;
3805
+ appendDebugLog("orchestrator:graceful-stop-complete", {
3806
+ iteration: this.state.currentIteration,
3807
+ consecutiveFailures: this.state.consecutiveFailures
3808
+ });
3809
+ this.emit("state", this.getState());
3810
+ }
3811
+ stopForGracefulShutdown() {
3812
+ if (!this.state.gracefulStopRequested) return false;
3813
+ this.finishGracefulStop();
3814
+ return true;
3815
+ }
3816
+ didStopWithoutForce() {
3817
+ return this.stopPromise === null && this.state.status === "stopped";
3818
+ }
3197
3819
  abort(reason) {
3198
3820
  this.state.status = "aborted";
3821
+ this.state.gracefulStopRequested = false;
3199
3822
  this.state.lastMessage = reason;
3200
3823
  this.state.waitingUntil = null;
3201
3824
  appendDebugLog("orchestrator:abort", {
@@ -3213,6 +3836,11 @@ var Orchestrator = class extends EventEmitter {
3213
3836
  appendDebugLog("agent:close:error", { error: serializeError(err) });
3214
3837
  }
3215
3838
  }
3839
+ emitStopped() {
3840
+ if (this.stoppedEventEmitted) return;
3841
+ this.stoppedEventEmitted = true;
3842
+ this.emit("stopped");
3843
+ }
3216
3844
  snapshotGitState() {
3217
3845
  try {
3218
3846
  return {
@@ -3281,6 +3909,7 @@ const INITIAL_ELAPSED_MS = 29237e3;
3281
3909
  var MockOrchestrator = class extends EventEmitter {
3282
3910
  state = {
3283
3911
  status: "running",
3912
+ gracefulStopRequested: false,
3284
3913
  currentIteration: 14,
3285
3914
  totalInputTokens: 873e5,
3286
3915
  totalOutputTokens: 86e4,
@@ -3297,20 +3926,35 @@ var MockOrchestrator = class extends EventEmitter {
3297
3926
  tokenTimer = null;
3298
3927
  messageTimer = null;
3299
3928
  messageIndex = 0;
3929
+ stoppedEventEmitted = false;
3300
3930
  getState() {
3301
3931
  return {
3302
3932
  ...this.state,
3933
+ interruptHint: getInterruptHint(this.state),
3303
3934
  iterations: [...this.state.iterations]
3304
3935
  };
3305
3936
  }
3306
3937
  stop() {
3938
+ if (this.state.status === "stopped") return;
3307
3939
  if (this.tokenTimer) clearTimeout(this.tokenTimer);
3308
3940
  if (this.messageTimer) clearTimeout(this.messageTimer);
3309
3941
  this.tokenTimer = null;
3310
3942
  this.messageTimer = null;
3311
3943
  this.state.status = "stopped";
3944
+ this.state.gracefulStopRequested = false;
3312
3945
  this.emit("state", this.getState());
3313
- this.emit("stopped");
3946
+ this.emitStopped();
3947
+ }
3948
+ requestGracefulStop() {
3949
+ if (this.state.gracefulStopRequested || this.state.status !== "running") return;
3950
+ this.state.gracefulStopRequested = true;
3951
+ this.emit("state", this.getState());
3952
+ }
3953
+ handleInterrupt() {
3954
+ const disposition = getInterruptDisposition(this.state);
3955
+ if (disposition === "request-graceful-stop") this.requestGracefulStop();
3956
+ else if (disposition === "force-stop") this.stop();
3957
+ return disposition;
3314
3958
  }
3315
3959
  start() {
3316
3960
  this.emit("state", this.getState());
@@ -3319,6 +3963,10 @@ var MockOrchestrator = class extends EventEmitter {
3319
3963
  }
3320
3964
  scheduleTokenBump() {
3321
3965
  this.tokenTimer = setTimeout(() => {
3966
+ if (this.state.gracefulStopRequested) {
3967
+ this.stop();
3968
+ return;
3969
+ }
3322
3970
  this.state.totalInputTokens += randInt(4e4, 18e4);
3323
3971
  this.state.totalOutputTokens += randInt(200, 2e3);
3324
3972
  this.emit("state", this.getState());
@@ -3328,12 +3976,21 @@ var MockOrchestrator = class extends EventEmitter {
3328
3976
  scheduleNextMessage() {
3329
3977
  const delay = randInt(3e3, 7e3);
3330
3978
  this.messageTimer = setTimeout(() => {
3979
+ if (this.state.gracefulStopRequested) {
3980
+ this.stop();
3981
+ return;
3982
+ }
3331
3983
  this.messageIndex = (this.messageIndex + 1) % AGENT_MESSAGES.length;
3332
3984
  this.state.lastMessage = AGENT_MESSAGES[this.messageIndex];
3333
3985
  this.emit("state", this.getState());
3334
3986
  this.scheduleNextMessage();
3335
3987
  }, delay);
3336
3988
  }
3989
+ emitStopped() {
3990
+ if (this.stoppedEventEmitted) return;
3991
+ this.stoppedEventEmitted = true;
3992
+ this.emit("stopped");
3993
+ }
3337
3994
  };
3338
3995
  //#endregion
3339
3996
  //#region src/utils/stars.ts
@@ -3623,6 +4280,7 @@ const MOON_PHASE_PERIOD = 1600;
3623
4280
  const MAX_MSG_LINES = 3;
3624
4281
  const MAX_MSG_LINE_LEN = CONTENT_WIDTH;
3625
4282
  const RESUME_HINT = "[ctrl+c to stop, gnhf again to resume]";
4283
+ const GRACEFUL_STOP_HINT = "[graceful stop requested, ctrl+c again to force stop, gnhf again to resume]";
3626
4284
  const DONE_HINT = "[ctrl+c to exit]";
3627
4285
  function spacedLabel(text) {
3628
4286
  return text.split("").join(" ");
@@ -3761,8 +4419,8 @@ function centerLineCells(content, width) {
3761
4419
  ...emptyCells(rightPad)
3762
4420
  ];
3763
4421
  }
3764
- function renderResumeHintCells(width, done) {
3765
- return centerLineCells(textToCells(done ? DONE_HINT : RESUME_HINT, "dim"), width);
4422
+ function renderResumeHintCells(width, interruptHint) {
4423
+ return centerLineCells(textToCells(interruptHint === "exit" ? DONE_HINT : interruptHint === "force-stop" ? GRACEFUL_STOP_HINT : RESUME_HINT, "dim"), width);
3766
4424
  }
3767
4425
  /**
3768
4426
  * Builds the centered content viewport for the renderer.
@@ -3870,8 +4528,7 @@ function buildFrameCells(prompt, agentName, state, topStars, bottomStars, sideSt
3870
4528
  ]);
3871
4529
  }
3872
4530
  for (let y = 0; y < bottomHeight; y++) frame.push(renderStarLineCells(bottomStars, terminalWidth, y, now));
3873
- const isDone = state.status === "aborted";
3874
- frame.push(renderResumeHintCells(terminalWidth, isDone));
4531
+ frame.push(renderResumeHintCells(terminalWidth, state.interruptHint));
3875
4532
  frame.push(emptyCells(terminalWidth));
3876
4533
  return frame;
3877
4534
  }
@@ -3895,6 +4552,7 @@ var Renderer = class {
3895
4552
  seedTop;
3896
4553
  seedBottom;
3897
4554
  seedSide;
4555
+ onInterrupt;
3898
4556
  handleState = (newState) => {
3899
4557
  this.state = {
3900
4558
  ...newState,
@@ -3905,10 +4563,11 @@ var Renderer = class {
3905
4563
  handleStopped = () => {
3906
4564
  this.stop("stopped");
3907
4565
  };
3908
- constructor(orchestrator, prompt, agentName) {
4566
+ constructor(orchestrator, prompt, agentName, onInterrupt) {
3909
4567
  this.orchestrator = orchestrator;
3910
4568
  this.prompt = prompt;
3911
4569
  this.agentName = agentName;
4570
+ this.onInterrupt = onInterrupt;
3912
4571
  this.state = orchestrator.getState();
3913
4572
  this.seedTop = Math.floor(Math.random() * 2147483646) + 1;
3914
4573
  this.seedBottom = Math.floor(Math.random() * 2147483646) + 1;
@@ -3924,10 +4583,7 @@ var Renderer = class {
3924
4583
  process$1.stdin.setRawMode(true);
3925
4584
  process$1.stdin.resume();
3926
4585
  process$1.stdin.on("data", (data) => {
3927
- if (data[0] === 3) {
3928
- this.stop("interrupted");
3929
- this.orchestrator.stop();
3930
- }
4586
+ if (data[0] === 3) this.onInterrupt();
3931
4587
  });
3932
4588
  }
3933
4589
  this.interval = setInterval(() => this.render(), TICK_MS);
@@ -4024,6 +4680,8 @@ const GNHF_REEXEC_STDIN_PROMPT = "GNHF_REEXEC_STDIN_PROMPT";
4024
4680
  const GNHF_REEXEC_STDIN_PROMPT_FILE = "GNHF_REEXEC_STDIN_PROMPT_FILE";
4025
4681
  const GNHF_REEXEC_STDIN_PROMPT_DIR_PREFIX = "gnhf-stdin-";
4026
4682
  const GNHF_REEXEC_STDIN_PROMPT_FILENAME = "prompt.txt";
4683
+ const AGENT_NAME_SET = new Set(AGENT_NAMES);
4684
+ const AGENT_NAME_LIST = `"${AGENT_NAMES.slice(0, -1).join("\", \"")}", or "${AGENT_NAMES[AGENT_NAMES.length - 1]}"`;
4027
4685
  var PromptSignalError = class extends Error {
4028
4686
  constructor(signal) {
4029
4687
  super(signal);
@@ -4045,6 +4703,22 @@ function humanizeErrorMessage(message) {
4045
4703
  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.";
4046
4704
  return message;
4047
4705
  }
4706
+ function isAgentName(name) {
4707
+ return AGENT_NAME_SET.has(name);
4708
+ }
4709
+ function buildSchemaOptions(stopWhen) {
4710
+ return stopWhen === void 0 ? { includeStopField: false } : {
4711
+ includeStopField: true,
4712
+ stopWhen
4713
+ };
4714
+ }
4715
+ function buildResumeSchemaOptions(stopWhen) {
4716
+ if (stopWhen === "") return {
4717
+ includeStopField: false,
4718
+ clearStopWhen: true
4719
+ };
4720
+ return buildSchemaOptions(stopWhen);
4721
+ }
4048
4722
  function initializeNewBranch(prompt, cwd, schemaOptions) {
4049
4723
  ensureCleanWorkingTree(cwd);
4050
4724
  const baseCommit = getHeadCommit(cwd);
@@ -4059,11 +4733,27 @@ function initializeWorktreeRun(prompt, cwd, schemaOptions) {
4059
4733
  const branchName = slugifyPrompt(prompt);
4060
4734
  const runId = branchName.split("/")[1];
4061
4735
  const worktreePath = join(dirname(repoRoot), `${basename(repoRoot)}-gnhf-worktrees`, runId);
4736
+ if (worktreeExists(repoRoot, worktreePath) && existsSync(join(worktreePath, ".gnhf", "runs", runId))) {
4737
+ let worktreeBranch;
4738
+ try {
4739
+ worktreeBranch = getCurrentBranch(worktreePath);
4740
+ } catch (error) {
4741
+ 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.`);
4742
+ }
4743
+ 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.`);
4744
+ return {
4745
+ runInfo: resumeRun(runId, worktreePath, schemaOptions),
4746
+ worktreePath,
4747
+ effectiveCwd: worktreePath,
4748
+ resumed: true
4749
+ };
4750
+ }
4062
4751
  createWorktree(repoRoot, worktreePath, branchName);
4063
4752
  return {
4064
4753
  runInfo: setupRun(runId, prompt, baseCommit, worktreePath, schemaOptions),
4065
4754
  worktreePath,
4066
- effectiveCwd: worktreePath
4755
+ effectiveCwd: worktreePath,
4756
+ resumed: false
4067
4757
  };
4068
4758
  }
4069
4759
  function openPromptTerminal() {
@@ -4189,11 +4879,13 @@ function readReexecStdinPrompt(env) {
4189
4879
  }
4190
4880
  }
4191
4881
  const program = new Command();
4192
- 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) => {
4882
+ 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) => {
4193
4883
  if (options.mock) {
4194
4884
  const mock = new MockOrchestrator();
4195
4885
  enterAltScreen();
4196
- const renderer = new Renderer(mock, "let's minimize app startup latency without sacrificing any functionality", "claude");
4886
+ const renderer = new Renderer(mock, "let's minimize app startup latency without sacrificing any functionality", "claude", () => {
4887
+ mock.handleInterrupt();
4888
+ });
4197
4889
  renderer.start();
4198
4890
  mock.start();
4199
4891
  await renderer.waitUntilExit();
@@ -4205,16 +4897,16 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
4205
4897
  let prompt = promptArg;
4206
4898
  let promptFromStdin = false;
4207
4899
  const agentName = options.agent;
4208
- if (agentName !== void 0 && agentName !== "claude" && agentName !== "codex" && agentName !== "rovodev" && agentName !== "opencode") {
4209
- console.error(`Unknown agent: ${options.agent}. Use "claude", "codex", "rovodev", or "opencode".`);
4900
+ if (agentName !== void 0 && !isAgentName(agentName)) {
4901
+ console.error(`Unknown agent: ${options.agent}. Use ${AGENT_NAME_LIST}.`);
4210
4902
  process$1.exit(1);
4211
4903
  }
4212
4904
  const config = {
4213
4905
  ...loadConfig(agentName ? { agent: agentName } : {}),
4214
4906
  ...options.preventSleep === void 0 ? {} : { preventSleep: options.preventSleep }
4215
4907
  };
4216
- if (config.agent !== "claude" && config.agent !== "codex" && config.agent !== "rovodev" && config.agent !== "opencode") {
4217
- console.error(`Unknown agent: ${config.agent}. Use "claude", "codex", "rovodev", or "opencode".`);
4908
+ if (!isAgentName(config.agent)) {
4909
+ console.error(`Unknown agent: ${config.agent}. Use ${AGENT_NAME_LIST}.`);
4218
4910
  process$1.exit(1);
4219
4911
  }
4220
4912
  if (!prompt && process$1.env.GNHF_SLEEP_INHIBITED === "1") prompt = readReexecStdinPrompt(process$1.env);
@@ -4228,7 +4920,9 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
4228
4920
  let worktreeCleanup = null;
4229
4921
  const currentBranch = getCurrentBranch(cwd);
4230
4922
  const onGnhfBranch = currentBranch.startsWith("gnhf/");
4231
- const schemaOptions = { includeStopField: options.stopWhen !== void 0 };
4923
+ const cliStopWhen = options.stopWhen === "" ? void 0 : options.stopWhen;
4924
+ let effectiveStopWhen = cliStopWhen;
4925
+ let schemaOptions = buildSchemaOptions(effectiveStopWhen);
4232
4926
  let runInfo;
4233
4927
  let startIteration = 0;
4234
4928
  if (options.worktree) {
@@ -4244,31 +4938,49 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
4244
4938
  runInfo = wt.runInfo;
4245
4939
  effectiveCwd = wt.effectiveCwd;
4246
4940
  worktreePath = wt.worktreePath;
4247
- worktreeCleanup = () => {
4248
- try {
4249
- removeWorktree(cwd, wt.worktreePath);
4250
- } catch {}
4251
- };
4252
- const exitCleanup = worktreeCleanup;
4253
- process$1.on("exit", () => {
4254
- if (worktreeCleanup === exitCleanup) exitCleanup();
4255
- });
4941
+ if (wt.resumed) {
4942
+ startIteration = getLastIterationNumber(runInfo);
4943
+ console.error(`\n gnhf: resuming preserved worktree at ${worktreePath}\n gnhf: continuing run ${runInfo.runId} from iteration ${startIteration}\n`);
4944
+ } else {
4945
+ worktreeCleanup = () => {
4946
+ try {
4947
+ removeWorktree(cwd, wt.worktreePath);
4948
+ } catch {}
4949
+ };
4950
+ const exitCleanup = worktreeCleanup;
4951
+ process$1.on("exit", () => {
4952
+ if (worktreeCleanup === exitCleanup) exitCleanup();
4953
+ });
4954
+ }
4256
4955
  } else if (onGnhfBranch) {
4257
4956
  const existingRunId = currentBranch.slice(5);
4258
- const existing = resumeRun(existingRunId, cwd, schemaOptions);
4957
+ let existing = resumeRun(existingRunId, cwd, { includeStopField: false });
4259
4958
  const existingPrompt = readFileSync(existing.promptPath, "utf-8");
4260
4959
  if (!prompt || prompt === existingPrompt) {
4960
+ existing = resumeRun(existingRunId, cwd, buildResumeSchemaOptions(options.stopWhen));
4961
+ const resumeStopWhen = existing.stopWhen;
4962
+ const resumeSchemaOptions = buildSchemaOptions(resumeStopWhen);
4261
4963
  prompt = existingPrompt;
4262
4964
  runInfo = existing;
4965
+ effectiveStopWhen = resumeStopWhen;
4966
+ schemaOptions = resumeSchemaOptions;
4263
4967
  startIteration = getLastIterationNumber(existing);
4264
4968
  } else {
4265
4969
  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.");
4266
4970
  if (answer === "o") {
4267
4971
  ensureCleanWorkingTree(cwd);
4268
- runInfo = setupRun(existingRunId, prompt, existing.baseCommit, cwd, schemaOptions);
4972
+ existing = resumeRun(existingRunId, cwd, buildResumeSchemaOptions(options.stopWhen));
4973
+ const resumeStopWhen = existing.stopWhen;
4974
+ const resumeSchemaOptions = buildSchemaOptions(resumeStopWhen);
4975
+ runInfo = setupRun(existingRunId, prompt, existing.baseCommit, cwd, resumeSchemaOptions);
4976
+ effectiveStopWhen = resumeStopWhen;
4977
+ schemaOptions = resumeSchemaOptions;
4269
4978
  startIteration = getLastIterationNumber(existing);
4270
- } else if (answer === "n") runInfo = initializeNewBranch(prompt, cwd, schemaOptions);
4271
- else process$1.exit(0);
4979
+ } else if (answer === "n") {
4980
+ effectiveStopWhen = cliStopWhen;
4981
+ schemaOptions = buildSchemaOptions(effectiveStopWhen);
4982
+ runInfo = initializeNewBranch(prompt, cwd, schemaOptions);
4983
+ } else process$1.exit(0);
4272
4984
  }
4273
4985
  } else {
4274
4986
  if (!prompt) {
@@ -4303,7 +5015,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
4303
5015
  startIteration,
4304
5016
  maxIterations: options.maxIterations,
4305
5017
  maxTokens: options.maxTokens,
4306
- stopWhen: options.stopWhen,
5018
+ stopWhen: effectiveStopWhen,
4307
5019
  preventSleep: config.preventSleep,
4308
5020
  agentArgsOverride: config.agentArgsOverride?.[config.agent],
4309
5021
  worktree: options.worktree,
@@ -4315,21 +5027,39 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
4315
5027
  const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent], config.agentArgsOverride?.[config.agent], schemaOptions), runInfo, prompt, effectiveCwd, startIteration, {
4316
5028
  maxIterations: options.maxIterations,
4317
5029
  maxTokens: options.maxTokens,
4318
- stopWhen: options.stopWhen
5030
+ stopWhen: effectiveStopWhen
4319
5031
  });
4320
5032
  let shutdownSignal = null;
4321
- enterAltScreen();
4322
- const renderer = new Renderer(orchestrator, prompt, config.agent);
4323
- renderer.start();
4324
- const requestShutdown = (signal) => {
4325
- if (shutdownSignal) return;
5033
+ let forceShutdownRequested = false;
5034
+ const requestForceShutdown = (signal) => {
5035
+ if (forceShutdownRequested) return;
5036
+ forceShutdownRequested = true;
4326
5037
  shutdownSignal = signal;
4327
5038
  appendDebugLog(`signal:${signal}`);
4328
5039
  renderer.stop();
5040
+ };
5041
+ const handleSigInt = () => {
5042
+ const disposition = orchestrator.handleInterrupt();
5043
+ if (disposition === "force-stop") {
5044
+ requestForceShutdown("SIGINT");
5045
+ return;
5046
+ }
5047
+ if (disposition === "exit") {
5048
+ shutdownSignal = "SIGINT";
5049
+ appendDebugLog("signal:SIGINT");
5050
+ renderer.stop("interrupted");
5051
+ return;
5052
+ }
5053
+ shutdownSignal = "SIGINT";
5054
+ appendDebugLog("signal:SIGINT");
5055
+ };
5056
+ const handleSigTerm = () => {
4329
5057
  orchestrator.stop();
5058
+ requestForceShutdown("SIGTERM");
4330
5059
  };
4331
- const handleSigInt = () => requestShutdown("SIGINT");
4332
- const handleSigTerm = () => requestShutdown("SIGTERM");
5060
+ enterAltScreen();
5061
+ const renderer = new Renderer(orchestrator, prompt, config.agent, handleSigInt);
5062
+ renderer.start();
4333
5063
  process$1.on("SIGINT", handleSigInt);
4334
5064
  process$1.on("SIGTERM", handleSigTerm);
4335
5065
  const orchestratorPromise = orchestrator.start().finally(() => {