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