gnhf 0.1.26 → 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.
- package/README.md +43 -16
- package/dist/cli.mjs +748 -61
- 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
|
|
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
|
|
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
|
|
527
|
-
baseCommitPath
|
|
582
|
+
baseCommit,
|
|
583
|
+
baseCommitPath,
|
|
584
|
+
stopWhenPath,
|
|
585
|
+
stopWhen
|
|
528
586
|
};
|
|
529
587
|
}
|
|
530
588
|
function backfillLegacyBaseCommit(runId, baseCommitPath, cwd) {
|
|
@@ -1044,7 +1102,7 @@ function setupAbortHandler(signal, child, reject, abortChild = () => {
|
|
|
1044
1102
|
//#endregion
|
|
1045
1103
|
//#region src/core/agents/claude.ts
|
|
1046
1104
|
const DEFAULT_FINAL_RESULT_EXIT_GRACE_MS = 15e3;
|
|
1047
|
-
function shouldUseWindowsShell$
|
|
1105
|
+
function shouldUseWindowsShell$4(bin, platform) {
|
|
1048
1106
|
if (platform !== "win32") return false;
|
|
1049
1107
|
if (/\.(cmd|bat)$/i.test(bin)) return true;
|
|
1050
1108
|
if (/[\\/]/.test(bin)) return false;
|
|
@@ -1105,7 +1163,7 @@ function buildClaudeArgs(prompt, schema, extraArgs) {
|
|
|
1105
1163
|
...userSpecifiedPermissionMode ? [] : ["--dangerously-skip-permissions"]
|
|
1106
1164
|
];
|
|
1107
1165
|
}
|
|
1108
|
-
function toTokenUsage(usage) {
|
|
1166
|
+
function toTokenUsage$1(usage) {
|
|
1109
1167
|
return {
|
|
1110
1168
|
inputTokens: (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0),
|
|
1111
1169
|
outputTokens: usage.output_tokens ?? 0,
|
|
@@ -1113,11 +1171,11 @@ function toTokenUsage(usage) {
|
|
|
1113
1171
|
cacheCreationTokens: usage.cache_creation_input_tokens ?? 0
|
|
1114
1172
|
};
|
|
1115
1173
|
}
|
|
1116
|
-
function isSameUsage(a, b) {
|
|
1174
|
+
function isSameUsage$1(a, b) {
|
|
1117
1175
|
return a.inputTokens === b.inputTokens && a.outputTokens === b.outputTokens && a.cacheReadTokens === b.cacheReadTokens && a.cacheCreationTokens === b.cacheCreationTokens;
|
|
1118
1176
|
}
|
|
1119
1177
|
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);
|
|
1178
|
+
return next.inputTokens >= previous.inputTokens && next.outputTokens >= previous.outputTokens && next.cacheReadTokens >= previous.cacheReadTokens && next.cacheCreationTokens >= previous.cacheCreationTokens && !isSameUsage$1(next, previous);
|
|
1121
1179
|
}
|
|
1122
1180
|
var ClaudeAgent = class {
|
|
1123
1181
|
name = "claude";
|
|
@@ -1141,7 +1199,7 @@ var ClaudeAgent = class {
|
|
|
1141
1199
|
const child = spawn(this.bin, buildClaudeArgs(prompt, this.schema, this.extraArgs), {
|
|
1142
1200
|
cwd,
|
|
1143
1201
|
detached: this.platform !== "win32",
|
|
1144
|
-
shell: shouldUseWindowsShell$
|
|
1202
|
+
shell: shouldUseWindowsShell$4(this.bin, this.platform),
|
|
1145
1203
|
stdio: [
|
|
1146
1204
|
"ignore",
|
|
1147
1205
|
"pipe",
|
|
@@ -1176,7 +1234,7 @@ var ClaudeAgent = class {
|
|
|
1176
1234
|
parseJSONLStream(child.stdout, logStream, (event) => {
|
|
1177
1235
|
if (event.type === "assistant") {
|
|
1178
1236
|
const msg = event.message;
|
|
1179
|
-
const nextUsage = toTokenUsage(msg.usage);
|
|
1237
|
+
const nextUsage = toTokenUsage$1(msg.usage);
|
|
1180
1238
|
let messageId = msg.id;
|
|
1181
1239
|
let previousUsage;
|
|
1182
1240
|
if (messageId) {
|
|
@@ -1200,7 +1258,7 @@ var ClaudeAgent = class {
|
|
|
1200
1258
|
previousUsage = usageByMessageId.get(messageId);
|
|
1201
1259
|
pendingAnonymousAssistantUsage = null;
|
|
1202
1260
|
lastAnonymousAssistantUsage = nextUsage;
|
|
1203
|
-
} else if (lastAnonymousAssistantId && lastAnonymousAssistantUsage && isSameUsage(nextUsage, lastAnonymousAssistantUsage)) {
|
|
1261
|
+
} else if (lastAnonymousAssistantId && lastAnonymousAssistantUsage && isSameUsage$1(nextUsage, lastAnonymousAssistantUsage)) {
|
|
1204
1262
|
messageId = lastAnonymousAssistantId;
|
|
1205
1263
|
previousUsage = usageByMessageId.get(messageId);
|
|
1206
1264
|
pendingAnonymousAssistantUsage ??= nextUsage;
|
|
@@ -1264,7 +1322,7 @@ var ClaudeAgent = class {
|
|
|
1264
1322
|
return;
|
|
1265
1323
|
}
|
|
1266
1324
|
const output = terminalResultEvent.structured_output;
|
|
1267
|
-
const usage = toTokenUsage(latestResultUsage ?? terminalResultEvent.usage);
|
|
1325
|
+
const usage = toTokenUsage$1(latestResultUsage ?? terminalResultEvent.usage);
|
|
1268
1326
|
onUsage?.(usage);
|
|
1269
1327
|
resolve({
|
|
1270
1328
|
output,
|
|
@@ -1275,8 +1333,180 @@ var ClaudeAgent = class {
|
|
|
1275
1333
|
}
|
|
1276
1334
|
};
|
|
1277
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
|
|
1278
1508
|
//#region src/core/agents/codex.ts
|
|
1279
|
-
function shouldUseWindowsShell$
|
|
1509
|
+
function shouldUseWindowsShell$2(bin, platform) {
|
|
1280
1510
|
if (platform !== "win32") return false;
|
|
1281
1511
|
if (/\.(cmd|bat)$/i.test(bin)) return true;
|
|
1282
1512
|
if (/[\\/]/.test(bin)) return false;
|
|
@@ -1342,7 +1572,7 @@ var CodexAgent = class {
|
|
|
1342
1572
|
const logStream = logPath ? createWriteStream(logPath) : null;
|
|
1343
1573
|
const child = spawn(this.bin, buildCodexArgs(prompt, this.schemaPath, this.extraArgs), {
|
|
1344
1574
|
cwd,
|
|
1345
|
-
shell: shouldUseWindowsShell$
|
|
1575
|
+
shell: shouldUseWindowsShell$2(this.bin, this.platform),
|
|
1346
1576
|
stdio: [
|
|
1347
1577
|
"ignore",
|
|
1348
1578
|
"pipe",
|
|
@@ -2204,6 +2434,287 @@ var OpenCodeAgent = class {
|
|
|
2204
2434
|
}
|
|
2205
2435
|
};
|
|
2206
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
|
|
2207
2718
|
//#region src/core/agents/rovodev.ts
|
|
2208
2719
|
function buildSystemPrompt(schema) {
|
|
2209
2720
|
return [
|
|
@@ -2829,11 +3340,21 @@ function createAgent(name, runInfo, pathOverride, agentArgsOverride, options) {
|
|
|
2829
3340
|
bin: pathOverride,
|
|
2830
3341
|
extraArgs: agentArgsOverride
|
|
2831
3342
|
});
|
|
3343
|
+
case "copilot": return new CopilotAgent({
|
|
3344
|
+
bin: pathOverride,
|
|
3345
|
+
extraArgs: agentArgsOverride,
|
|
3346
|
+
schema
|
|
3347
|
+
});
|
|
2832
3348
|
case "opencode": return new OpenCodeAgent({
|
|
2833
3349
|
bin: pathOverride,
|
|
2834
3350
|
extraArgs: agentArgsOverride,
|
|
2835
3351
|
schema
|
|
2836
3352
|
});
|
|
3353
|
+
case "pi": return new PiAgent({
|
|
3354
|
+
bin: pathOverride,
|
|
3355
|
+
extraArgs: agentArgsOverride,
|
|
3356
|
+
schema
|
|
3357
|
+
});
|
|
2837
3358
|
case "rovodev": return new RovoDevAgent(runInfo.schemaPath, {
|
|
2838
3359
|
bin: pathOverride,
|
|
2839
3360
|
extraArgs: agentArgsOverride
|
|
@@ -2841,6 +3362,19 @@ function createAgent(name, runInfo, pathOverride, agentArgsOverride, options) {
|
|
|
2841
3362
|
}
|
|
2842
3363
|
}
|
|
2843
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
|
|
2844
3378
|
//#region src/templates/iteration-prompt.ts
|
|
2845
3379
|
function buildIterationPrompt(params) {
|
|
2846
3380
|
const outputFields = [
|
|
@@ -2887,8 +3421,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2887
3421
|
activeAbortController = null;
|
|
2888
3422
|
pendingAbortReason = null;
|
|
2889
3423
|
loopDone = false;
|
|
3424
|
+
stoppedEventEmitted = false;
|
|
2890
3425
|
state = {
|
|
2891
3426
|
status: "running",
|
|
3427
|
+
gracefulStopRequested: false,
|
|
2892
3428
|
currentIteration: 0,
|
|
2893
3429
|
totalInputTokens: 0,
|
|
2894
3430
|
totalOutputTokens: 0,
|
|
@@ -2914,7 +3450,27 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2914
3450
|
this.state.commitCount = getBranchCommitCount(this.runInfo.baseCommit, this.cwd);
|
|
2915
3451
|
}
|
|
2916
3452
|
getState() {
|
|
2917
|
-
return {
|
|
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;
|
|
2918
3474
|
}
|
|
2919
3475
|
stop() {
|
|
2920
3476
|
this.stopRequested = true;
|
|
@@ -2924,8 +3480,9 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2924
3480
|
loopDone: this.loopDone
|
|
2925
3481
|
});
|
|
2926
3482
|
this.activeAbortController?.abort();
|
|
3483
|
+
this.state.gracefulStopRequested = false;
|
|
2927
3484
|
if (this.loopDone) {
|
|
2928
|
-
this.
|
|
3485
|
+
this.emitStopped();
|
|
2929
3486
|
return;
|
|
2930
3487
|
}
|
|
2931
3488
|
if (this.stopPromise) return;
|
|
@@ -2950,7 +3507,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2950
3507
|
resetHard(this.cwd);
|
|
2951
3508
|
this.state.status = "stopped";
|
|
2952
3509
|
this.emit("state", this.getState());
|
|
2953
|
-
this.
|
|
3510
|
+
this.emitStopped();
|
|
2954
3511
|
})();
|
|
2955
3512
|
}
|
|
2956
3513
|
async start() {
|
|
@@ -2974,6 +3531,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2974
3531
|
this.abort(preIterationAbortReason);
|
|
2975
3532
|
break;
|
|
2976
3533
|
}
|
|
3534
|
+
if (this.stopForGracefulShutdown()) break;
|
|
2977
3535
|
this.state.currentIteration++;
|
|
2978
3536
|
this.state.status = "running";
|
|
2979
3537
|
this.emit("iteration:start", this.state.currentIteration);
|
|
@@ -3029,6 +3587,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
3029
3587
|
totalOutputTokens: this.state.totalOutputTokens,
|
|
3030
3588
|
commitCount: this.state.commitCount
|
|
3031
3589
|
});
|
|
3590
|
+
if (this.stopForGracefulShutdown()) break;
|
|
3032
3591
|
if (this.limits.stopWhen !== void 0 && result.shouldFullyStop) {
|
|
3033
3592
|
this.abort("stop condition met");
|
|
3034
3593
|
break;
|
|
@@ -3059,6 +3618,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
3059
3618
|
});
|
|
3060
3619
|
this.state.waitingUntil = null;
|
|
3061
3620
|
if (!this.stopRequested) {
|
|
3621
|
+
if (this.stopForGracefulShutdown()) break;
|
|
3062
3622
|
this.state.status = "running";
|
|
3063
3623
|
this.emit("state", this.getState());
|
|
3064
3624
|
}
|
|
@@ -3075,6 +3635,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
3075
3635
|
if (this.stopPromise) await this.stopPromise;
|
|
3076
3636
|
else await this.closeAgent();
|
|
3077
3637
|
this.loopDone = true;
|
|
3638
|
+
if (this.didStopWithoutForce()) this.emitStopped();
|
|
3078
3639
|
appendDebugLog("orchestrator:end", {
|
|
3079
3640
|
status: this.state.status,
|
|
3080
3641
|
iterations: this.state.currentIteration,
|
|
@@ -3237,8 +3798,27 @@ var Orchestrator = class extends EventEmitter {
|
|
|
3237
3798
|
if (totalTokens < this.limits.maxTokens) return null;
|
|
3238
3799
|
return `max tokens reached (${totalTokens}/${this.limits.maxTokens})`;
|
|
3239
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
|
+
}
|
|
3240
3819
|
abort(reason) {
|
|
3241
3820
|
this.state.status = "aborted";
|
|
3821
|
+
this.state.gracefulStopRequested = false;
|
|
3242
3822
|
this.state.lastMessage = reason;
|
|
3243
3823
|
this.state.waitingUntil = null;
|
|
3244
3824
|
appendDebugLog("orchestrator:abort", {
|
|
@@ -3256,6 +3836,11 @@ var Orchestrator = class extends EventEmitter {
|
|
|
3256
3836
|
appendDebugLog("agent:close:error", { error: serializeError(err) });
|
|
3257
3837
|
}
|
|
3258
3838
|
}
|
|
3839
|
+
emitStopped() {
|
|
3840
|
+
if (this.stoppedEventEmitted) return;
|
|
3841
|
+
this.stoppedEventEmitted = true;
|
|
3842
|
+
this.emit("stopped");
|
|
3843
|
+
}
|
|
3259
3844
|
snapshotGitState() {
|
|
3260
3845
|
try {
|
|
3261
3846
|
return {
|
|
@@ -3324,6 +3909,7 @@ const INITIAL_ELAPSED_MS = 29237e3;
|
|
|
3324
3909
|
var MockOrchestrator = class extends EventEmitter {
|
|
3325
3910
|
state = {
|
|
3326
3911
|
status: "running",
|
|
3912
|
+
gracefulStopRequested: false,
|
|
3327
3913
|
currentIteration: 14,
|
|
3328
3914
|
totalInputTokens: 873e5,
|
|
3329
3915
|
totalOutputTokens: 86e4,
|
|
@@ -3340,20 +3926,35 @@ var MockOrchestrator = class extends EventEmitter {
|
|
|
3340
3926
|
tokenTimer = null;
|
|
3341
3927
|
messageTimer = null;
|
|
3342
3928
|
messageIndex = 0;
|
|
3929
|
+
stoppedEventEmitted = false;
|
|
3343
3930
|
getState() {
|
|
3344
3931
|
return {
|
|
3345
3932
|
...this.state,
|
|
3933
|
+
interruptHint: getInterruptHint(this.state),
|
|
3346
3934
|
iterations: [...this.state.iterations]
|
|
3347
3935
|
};
|
|
3348
3936
|
}
|
|
3349
3937
|
stop() {
|
|
3938
|
+
if (this.state.status === "stopped") return;
|
|
3350
3939
|
if (this.tokenTimer) clearTimeout(this.tokenTimer);
|
|
3351
3940
|
if (this.messageTimer) clearTimeout(this.messageTimer);
|
|
3352
3941
|
this.tokenTimer = null;
|
|
3353
3942
|
this.messageTimer = null;
|
|
3354
3943
|
this.state.status = "stopped";
|
|
3944
|
+
this.state.gracefulStopRequested = false;
|
|
3945
|
+
this.emit("state", this.getState());
|
|
3946
|
+
this.emitStopped();
|
|
3947
|
+
}
|
|
3948
|
+
requestGracefulStop() {
|
|
3949
|
+
if (this.state.gracefulStopRequested || this.state.status !== "running") return;
|
|
3950
|
+
this.state.gracefulStopRequested = true;
|
|
3355
3951
|
this.emit("state", this.getState());
|
|
3356
|
-
|
|
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;
|
|
3357
3958
|
}
|
|
3358
3959
|
start() {
|
|
3359
3960
|
this.emit("state", this.getState());
|
|
@@ -3362,6 +3963,10 @@ var MockOrchestrator = class extends EventEmitter {
|
|
|
3362
3963
|
}
|
|
3363
3964
|
scheduleTokenBump() {
|
|
3364
3965
|
this.tokenTimer = setTimeout(() => {
|
|
3966
|
+
if (this.state.gracefulStopRequested) {
|
|
3967
|
+
this.stop();
|
|
3968
|
+
return;
|
|
3969
|
+
}
|
|
3365
3970
|
this.state.totalInputTokens += randInt(4e4, 18e4);
|
|
3366
3971
|
this.state.totalOutputTokens += randInt(200, 2e3);
|
|
3367
3972
|
this.emit("state", this.getState());
|
|
@@ -3371,12 +3976,21 @@ var MockOrchestrator = class extends EventEmitter {
|
|
|
3371
3976
|
scheduleNextMessage() {
|
|
3372
3977
|
const delay = randInt(3e3, 7e3);
|
|
3373
3978
|
this.messageTimer = setTimeout(() => {
|
|
3979
|
+
if (this.state.gracefulStopRequested) {
|
|
3980
|
+
this.stop();
|
|
3981
|
+
return;
|
|
3982
|
+
}
|
|
3374
3983
|
this.messageIndex = (this.messageIndex + 1) % AGENT_MESSAGES.length;
|
|
3375
3984
|
this.state.lastMessage = AGENT_MESSAGES[this.messageIndex];
|
|
3376
3985
|
this.emit("state", this.getState());
|
|
3377
3986
|
this.scheduleNextMessage();
|
|
3378
3987
|
}, delay);
|
|
3379
3988
|
}
|
|
3989
|
+
emitStopped() {
|
|
3990
|
+
if (this.stoppedEventEmitted) return;
|
|
3991
|
+
this.stoppedEventEmitted = true;
|
|
3992
|
+
this.emit("stopped");
|
|
3993
|
+
}
|
|
3380
3994
|
};
|
|
3381
3995
|
//#endregion
|
|
3382
3996
|
//#region src/utils/stars.ts
|
|
@@ -3666,6 +4280,7 @@ const MOON_PHASE_PERIOD = 1600;
|
|
|
3666
4280
|
const MAX_MSG_LINES = 3;
|
|
3667
4281
|
const MAX_MSG_LINE_LEN = CONTENT_WIDTH;
|
|
3668
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]";
|
|
3669
4284
|
const DONE_HINT = "[ctrl+c to exit]";
|
|
3670
4285
|
function spacedLabel(text) {
|
|
3671
4286
|
return text.split("").join(" ");
|
|
@@ -3804,8 +4419,8 @@ function centerLineCells(content, width) {
|
|
|
3804
4419
|
...emptyCells(rightPad)
|
|
3805
4420
|
];
|
|
3806
4421
|
}
|
|
3807
|
-
function renderResumeHintCells(width,
|
|
3808
|
-
return centerLineCells(textToCells(
|
|
4422
|
+
function renderResumeHintCells(width, interruptHint) {
|
|
4423
|
+
return centerLineCells(textToCells(interruptHint === "exit" ? DONE_HINT : interruptHint === "force-stop" ? GRACEFUL_STOP_HINT : RESUME_HINT, "dim"), width);
|
|
3809
4424
|
}
|
|
3810
4425
|
/**
|
|
3811
4426
|
* Builds the centered content viewport for the renderer.
|
|
@@ -3913,8 +4528,7 @@ function buildFrameCells(prompt, agentName, state, topStars, bottomStars, sideSt
|
|
|
3913
4528
|
]);
|
|
3914
4529
|
}
|
|
3915
4530
|
for (let y = 0; y < bottomHeight; y++) frame.push(renderStarLineCells(bottomStars, terminalWidth, y, now));
|
|
3916
|
-
|
|
3917
|
-
frame.push(renderResumeHintCells(terminalWidth, isDone));
|
|
4531
|
+
frame.push(renderResumeHintCells(terminalWidth, state.interruptHint));
|
|
3918
4532
|
frame.push(emptyCells(terminalWidth));
|
|
3919
4533
|
return frame;
|
|
3920
4534
|
}
|
|
@@ -3938,6 +4552,7 @@ var Renderer = class {
|
|
|
3938
4552
|
seedTop;
|
|
3939
4553
|
seedBottom;
|
|
3940
4554
|
seedSide;
|
|
4555
|
+
onInterrupt;
|
|
3941
4556
|
handleState = (newState) => {
|
|
3942
4557
|
this.state = {
|
|
3943
4558
|
...newState,
|
|
@@ -3948,10 +4563,11 @@ var Renderer = class {
|
|
|
3948
4563
|
handleStopped = () => {
|
|
3949
4564
|
this.stop("stopped");
|
|
3950
4565
|
};
|
|
3951
|
-
constructor(orchestrator, prompt, agentName) {
|
|
4566
|
+
constructor(orchestrator, prompt, agentName, onInterrupt) {
|
|
3952
4567
|
this.orchestrator = orchestrator;
|
|
3953
4568
|
this.prompt = prompt;
|
|
3954
4569
|
this.agentName = agentName;
|
|
4570
|
+
this.onInterrupt = onInterrupt;
|
|
3955
4571
|
this.state = orchestrator.getState();
|
|
3956
4572
|
this.seedTop = Math.floor(Math.random() * 2147483646) + 1;
|
|
3957
4573
|
this.seedBottom = Math.floor(Math.random() * 2147483646) + 1;
|
|
@@ -3967,10 +4583,7 @@ var Renderer = class {
|
|
|
3967
4583
|
process$1.stdin.setRawMode(true);
|
|
3968
4584
|
process$1.stdin.resume();
|
|
3969
4585
|
process$1.stdin.on("data", (data) => {
|
|
3970
|
-
if (data[0] === 3)
|
|
3971
|
-
this.stop("interrupted");
|
|
3972
|
-
this.orchestrator.stop();
|
|
3973
|
-
}
|
|
4586
|
+
if (data[0] === 3) this.onInterrupt();
|
|
3974
4587
|
});
|
|
3975
4588
|
}
|
|
3976
4589
|
this.interval = setInterval(() => this.render(), TICK_MS);
|
|
@@ -4067,6 +4680,8 @@ const GNHF_REEXEC_STDIN_PROMPT = "GNHF_REEXEC_STDIN_PROMPT";
|
|
|
4067
4680
|
const GNHF_REEXEC_STDIN_PROMPT_FILE = "GNHF_REEXEC_STDIN_PROMPT_FILE";
|
|
4068
4681
|
const GNHF_REEXEC_STDIN_PROMPT_DIR_PREFIX = "gnhf-stdin-";
|
|
4069
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]}"`;
|
|
4070
4685
|
var PromptSignalError = class extends Error {
|
|
4071
4686
|
constructor(signal) {
|
|
4072
4687
|
super(signal);
|
|
@@ -4088,6 +4703,22 @@ function humanizeErrorMessage(message) {
|
|
|
4088
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.";
|
|
4089
4704
|
return message;
|
|
4090
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
|
+
}
|
|
4091
4722
|
function initializeNewBranch(prompt, cwd, schemaOptions) {
|
|
4092
4723
|
ensureCleanWorkingTree(cwd);
|
|
4093
4724
|
const baseCommit = getHeadCommit(cwd);
|
|
@@ -4102,11 +4733,27 @@ function initializeWorktreeRun(prompt, cwd, schemaOptions) {
|
|
|
4102
4733
|
const branchName = slugifyPrompt(prompt);
|
|
4103
4734
|
const runId = branchName.split("/")[1];
|
|
4104
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
|
+
}
|
|
4105
4751
|
createWorktree(repoRoot, worktreePath, branchName);
|
|
4106
4752
|
return {
|
|
4107
4753
|
runInfo: setupRun(runId, prompt, baseCommit, worktreePath, schemaOptions),
|
|
4108
4754
|
worktreePath,
|
|
4109
|
-
effectiveCwd: worktreePath
|
|
4755
|
+
effectiveCwd: worktreePath,
|
|
4756
|
+
resumed: false
|
|
4110
4757
|
};
|
|
4111
4758
|
}
|
|
4112
4759
|
function openPromptTerminal() {
|
|
@@ -4232,11 +4879,13 @@ function readReexecStdinPrompt(env) {
|
|
|
4232
4879
|
}
|
|
4233
4880
|
}
|
|
4234
4881
|
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>",
|
|
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) => {
|
|
4236
4883
|
if (options.mock) {
|
|
4237
4884
|
const mock = new MockOrchestrator();
|
|
4238
4885
|
enterAltScreen();
|
|
4239
|
-
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
|
+
});
|
|
4240
4889
|
renderer.start();
|
|
4241
4890
|
mock.start();
|
|
4242
4891
|
await renderer.waitUntilExit();
|
|
@@ -4248,16 +4897,16 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4248
4897
|
let prompt = promptArg;
|
|
4249
4898
|
let promptFromStdin = false;
|
|
4250
4899
|
const agentName = options.agent;
|
|
4251
|
-
if (agentName !== void 0 && agentName
|
|
4252
|
-
console.error(`Unknown agent: ${options.agent}. Use
|
|
4900
|
+
if (agentName !== void 0 && !isAgentName(agentName)) {
|
|
4901
|
+
console.error(`Unknown agent: ${options.agent}. Use ${AGENT_NAME_LIST}.`);
|
|
4253
4902
|
process$1.exit(1);
|
|
4254
4903
|
}
|
|
4255
4904
|
const config = {
|
|
4256
4905
|
...loadConfig(agentName ? { agent: agentName } : {}),
|
|
4257
4906
|
...options.preventSleep === void 0 ? {} : { preventSleep: options.preventSleep }
|
|
4258
4907
|
};
|
|
4259
|
-
if (config.agent
|
|
4260
|
-
console.error(`Unknown agent: ${config.agent}. Use
|
|
4908
|
+
if (!isAgentName(config.agent)) {
|
|
4909
|
+
console.error(`Unknown agent: ${config.agent}. Use ${AGENT_NAME_LIST}.`);
|
|
4261
4910
|
process$1.exit(1);
|
|
4262
4911
|
}
|
|
4263
4912
|
if (!prompt && process$1.env.GNHF_SLEEP_INHIBITED === "1") prompt = readReexecStdinPrompt(process$1.env);
|
|
@@ -4271,7 +4920,9 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4271
4920
|
let worktreeCleanup = null;
|
|
4272
4921
|
const currentBranch = getCurrentBranch(cwd);
|
|
4273
4922
|
const onGnhfBranch = currentBranch.startsWith("gnhf/");
|
|
4274
|
-
const
|
|
4923
|
+
const cliStopWhen = options.stopWhen === "" ? void 0 : options.stopWhen;
|
|
4924
|
+
let effectiveStopWhen = cliStopWhen;
|
|
4925
|
+
let schemaOptions = buildSchemaOptions(effectiveStopWhen);
|
|
4275
4926
|
let runInfo;
|
|
4276
4927
|
let startIteration = 0;
|
|
4277
4928
|
if (options.worktree) {
|
|
@@ -4287,31 +4938,49 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4287
4938
|
runInfo = wt.runInfo;
|
|
4288
4939
|
effectiveCwd = wt.effectiveCwd;
|
|
4289
4940
|
worktreePath = wt.worktreePath;
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
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
|
+
}
|
|
4299
4955
|
} else if (onGnhfBranch) {
|
|
4300
4956
|
const existingRunId = currentBranch.slice(5);
|
|
4301
|
-
|
|
4957
|
+
let existing = resumeRun(existingRunId, cwd, { includeStopField: false });
|
|
4302
4958
|
const existingPrompt = readFileSync(existing.promptPath, "utf-8");
|
|
4303
4959
|
if (!prompt || prompt === existingPrompt) {
|
|
4960
|
+
existing = resumeRun(existingRunId, cwd, buildResumeSchemaOptions(options.stopWhen));
|
|
4961
|
+
const resumeStopWhen = existing.stopWhen;
|
|
4962
|
+
const resumeSchemaOptions = buildSchemaOptions(resumeStopWhen);
|
|
4304
4963
|
prompt = existingPrompt;
|
|
4305
4964
|
runInfo = existing;
|
|
4965
|
+
effectiveStopWhen = resumeStopWhen;
|
|
4966
|
+
schemaOptions = resumeSchemaOptions;
|
|
4306
4967
|
startIteration = getLastIterationNumber(existing);
|
|
4307
4968
|
} else {
|
|
4308
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.");
|
|
4309
4970
|
if (answer === "o") {
|
|
4310
4971
|
ensureCleanWorkingTree(cwd);
|
|
4311
|
-
|
|
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;
|
|
4312
4978
|
startIteration = getLastIterationNumber(existing);
|
|
4313
|
-
} else if (answer === "n")
|
|
4314
|
-
|
|
4979
|
+
} else if (answer === "n") {
|
|
4980
|
+
effectiveStopWhen = cliStopWhen;
|
|
4981
|
+
schemaOptions = buildSchemaOptions(effectiveStopWhen);
|
|
4982
|
+
runInfo = initializeNewBranch(prompt, cwd, schemaOptions);
|
|
4983
|
+
} else process$1.exit(0);
|
|
4315
4984
|
}
|
|
4316
4985
|
} else {
|
|
4317
4986
|
if (!prompt) {
|
|
@@ -4346,7 +5015,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4346
5015
|
startIteration,
|
|
4347
5016
|
maxIterations: options.maxIterations,
|
|
4348
5017
|
maxTokens: options.maxTokens,
|
|
4349
|
-
stopWhen:
|
|
5018
|
+
stopWhen: effectiveStopWhen,
|
|
4350
5019
|
preventSleep: config.preventSleep,
|
|
4351
5020
|
agentArgsOverride: config.agentArgsOverride?.[config.agent],
|
|
4352
5021
|
worktree: options.worktree,
|
|
@@ -4358,21 +5027,39 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4358
5027
|
const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent], config.agentArgsOverride?.[config.agent], schemaOptions), runInfo, prompt, effectiveCwd, startIteration, {
|
|
4359
5028
|
maxIterations: options.maxIterations,
|
|
4360
5029
|
maxTokens: options.maxTokens,
|
|
4361
|
-
stopWhen:
|
|
5030
|
+
stopWhen: effectiveStopWhen
|
|
4362
5031
|
});
|
|
4363
5032
|
let shutdownSignal = null;
|
|
4364
|
-
|
|
4365
|
-
const
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
if (shutdownSignal) return;
|
|
5033
|
+
let forceShutdownRequested = false;
|
|
5034
|
+
const requestForceShutdown = (signal) => {
|
|
5035
|
+
if (forceShutdownRequested) return;
|
|
5036
|
+
forceShutdownRequested = true;
|
|
4369
5037
|
shutdownSignal = signal;
|
|
4370
5038
|
appendDebugLog(`signal:${signal}`);
|
|
4371
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 = () => {
|
|
4372
5057
|
orchestrator.stop();
|
|
5058
|
+
requestForceShutdown("SIGTERM");
|
|
4373
5059
|
};
|
|
4374
|
-
|
|
4375
|
-
const
|
|
5060
|
+
enterAltScreen();
|
|
5061
|
+
const renderer = new Renderer(orchestrator, prompt, config.agent, handleSigInt);
|
|
5062
|
+
renderer.start();
|
|
4376
5063
|
process$1.on("SIGINT", handleSigInt);
|
|
4377
5064
|
process$1.on("SIGTERM", handleSigTerm);
|
|
4378
5065
|
const orchestratorPromise = orchestrator.start().finally(() => {
|