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