gnhf 0.1.8 → 0.1.10
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 +27 -10
- package/dist/cli.mjs +837 -114
- package/package.json +5 -4
package/dist/cli.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { appendFileSync, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { appendFileSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
5
|
import process$1 from "node:process";
|
|
4
6
|
import { createInterface } from "node:readline";
|
|
5
7
|
import { Command, InvalidArgumentError } from "commander";
|
|
6
|
-
import { dirname, isAbsolute, join } from "node:path";
|
|
7
|
-
import { homedir } from "node:os";
|
|
8
8
|
import yaml from "js-yaml";
|
|
9
9
|
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
10
10
|
import { createServer } from "node:net";
|
|
@@ -13,8 +13,28 @@ import { createHash } from "node:crypto";
|
|
|
13
13
|
//#region src/core/config.ts
|
|
14
14
|
const DEFAULT_CONFIG = {
|
|
15
15
|
agent: "claude",
|
|
16
|
-
maxConsecutiveFailures: 3
|
|
16
|
+
maxConsecutiveFailures: 3,
|
|
17
|
+
preventSleep: true
|
|
17
18
|
};
|
|
19
|
+
var InvalidConfigError = class extends Error {};
|
|
20
|
+
function normalizePreventSleep(value) {
|
|
21
|
+
if (typeof value === "boolean") return value;
|
|
22
|
+
if (typeof value !== "string") return void 0;
|
|
23
|
+
if (value === "true") return true;
|
|
24
|
+
if (value === "false") return false;
|
|
25
|
+
if (value === "on") return true;
|
|
26
|
+
if (value === "off") return false;
|
|
27
|
+
}
|
|
28
|
+
function normalizeConfig(config) {
|
|
29
|
+
const normalized = { ...config };
|
|
30
|
+
const hasPreventSleep = Object.prototype.hasOwnProperty.call(config, "preventSleep");
|
|
31
|
+
const preventSleep = normalizePreventSleep(config.preventSleep);
|
|
32
|
+
if (preventSleep === void 0) {
|
|
33
|
+
if (hasPreventSleep && config.preventSleep !== void 0) throw new InvalidConfigError(`Invalid config value for preventSleep: ${String(config.preventSleep)}`);
|
|
34
|
+
delete normalized.preventSleep;
|
|
35
|
+
} else normalized.preventSleep = preventSleep;
|
|
36
|
+
return normalized;
|
|
37
|
+
}
|
|
18
38
|
function isMissingConfigError(error) {
|
|
19
39
|
if (!(error instanceof Error)) return false;
|
|
20
40
|
return "code" in error ? error.code === "ENOENT" : error.message.includes("ENOENT");
|
|
@@ -25,6 +45,9 @@ agent: ${config.agent}
|
|
|
25
45
|
|
|
26
46
|
# Abort after this many consecutive failures
|
|
27
47
|
maxConsecutiveFailures: ${config.maxConsecutiveFailures}
|
|
48
|
+
|
|
49
|
+
# Prevent the machine from sleeping during a run
|
|
50
|
+
preventSleep: ${config.preventSleep}
|
|
28
51
|
`;
|
|
29
52
|
}
|
|
30
53
|
function loadConfig(overrides) {
|
|
@@ -34,14 +57,15 @@ function loadConfig(overrides) {
|
|
|
34
57
|
let shouldBootstrapConfig = false;
|
|
35
58
|
try {
|
|
36
59
|
const raw = readFileSync(configPath, "utf-8");
|
|
37
|
-
fileConfig = yaml.load(raw) ?? {};
|
|
60
|
+
fileConfig = normalizeConfig(yaml.load(raw) ?? {});
|
|
38
61
|
} catch (error) {
|
|
62
|
+
if (error instanceof InvalidConfigError) throw error;
|
|
39
63
|
if (isMissingConfigError(error)) shouldBootstrapConfig = true;
|
|
40
64
|
}
|
|
41
65
|
const resolvedConfig = {
|
|
42
66
|
...DEFAULT_CONFIG,
|
|
43
67
|
...fileConfig,
|
|
44
|
-
...overrides
|
|
68
|
+
...normalizeConfig(overrides ?? {})
|
|
45
69
|
};
|
|
46
70
|
if (shouldBootstrapConfig) try {
|
|
47
71
|
mkdirSync(configDir, { recursive: true });
|
|
@@ -50,6 +74,20 @@ function loadConfig(overrides) {
|
|
|
50
74
|
return resolvedConfig;
|
|
51
75
|
}
|
|
52
76
|
//#endregion
|
|
77
|
+
//#region src/core/debug-log.ts
|
|
78
|
+
function appendDebugLog(event, details = {}) {
|
|
79
|
+
const logPath = process.env.GNHF_DEBUG_LOG_PATH;
|
|
80
|
+
if (!logPath) return;
|
|
81
|
+
try {
|
|
82
|
+
appendFileSync(logPath, `${JSON.stringify({
|
|
83
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
84
|
+
pid: process.pid,
|
|
85
|
+
event,
|
|
86
|
+
...details
|
|
87
|
+
})}\n`, "utf-8");
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
53
91
|
//#region src/core/git.ts
|
|
54
92
|
const NOT_GIT_REPOSITORY_MESSAGE = "This command must be run inside a Git repository. Change into a repo or run \"git init\" first.";
|
|
55
93
|
function translateGitError(error) {
|
|
@@ -250,6 +288,417 @@ function appendNotes(notesPath, iteration, summary, changes, learnings) {
|
|
|
250
288
|
].join("\n"), "utf-8");
|
|
251
289
|
}
|
|
252
290
|
//#endregion
|
|
291
|
+
//#region src/core/stdin.ts
|
|
292
|
+
async function readStdinText(input) {
|
|
293
|
+
const chunks = [];
|
|
294
|
+
for await (const chunk of input) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
295
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
296
|
+
}
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region src/core/agents/managed-process.ts
|
|
299
|
+
const POST_SIGKILL_GRACE_MS = 100;
|
|
300
|
+
function signalChildProcess(child, options) {
|
|
301
|
+
const killProcess = options.killProcess ?? process.kill.bind(process);
|
|
302
|
+
if (options.detached && child.pid) try {
|
|
303
|
+
killProcess(-child.pid, options.signal);
|
|
304
|
+
return;
|
|
305
|
+
} catch {}
|
|
306
|
+
child.kill(options.signal);
|
|
307
|
+
}
|
|
308
|
+
async function shutdownChildProcess(child, options) {
|
|
309
|
+
if (child.exitCode != null || child.signalCode != null) return;
|
|
310
|
+
const timeoutMs = options.timeoutMs ?? 3e3;
|
|
311
|
+
await new Promise((resolve) => {
|
|
312
|
+
let forceKillTimer = null;
|
|
313
|
+
let hardDeadlineTimer = null;
|
|
314
|
+
let settled = false;
|
|
315
|
+
const settle = () => {
|
|
316
|
+
if (settled) return;
|
|
317
|
+
settled = true;
|
|
318
|
+
if (forceKillTimer) {
|
|
319
|
+
clearTimeout(forceKillTimer);
|
|
320
|
+
forceKillTimer = null;
|
|
321
|
+
}
|
|
322
|
+
if (hardDeadlineTimer) {
|
|
323
|
+
clearTimeout(hardDeadlineTimer);
|
|
324
|
+
hardDeadlineTimer = null;
|
|
325
|
+
}
|
|
326
|
+
child.off("close", handleClose);
|
|
327
|
+
resolve();
|
|
328
|
+
};
|
|
329
|
+
const handleClose = () => {
|
|
330
|
+
settle();
|
|
331
|
+
};
|
|
332
|
+
child.on("close", handleClose);
|
|
333
|
+
try {
|
|
334
|
+
signalChildProcess(child, {
|
|
335
|
+
...options,
|
|
336
|
+
signal: "SIGTERM"
|
|
337
|
+
});
|
|
338
|
+
} catch {}
|
|
339
|
+
forceKillTimer = setTimeout(() => {
|
|
340
|
+
try {
|
|
341
|
+
signalChildProcess(child, {
|
|
342
|
+
...options,
|
|
343
|
+
signal: "SIGKILL"
|
|
344
|
+
});
|
|
345
|
+
} catch {}
|
|
346
|
+
hardDeadlineTimer = setTimeout(() => {
|
|
347
|
+
settle();
|
|
348
|
+
}, POST_SIGKILL_GRACE_MS);
|
|
349
|
+
hardDeadlineTimer.unref?.();
|
|
350
|
+
}, timeoutMs);
|
|
351
|
+
forceKillTimer.unref?.();
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
//#endregion
|
|
355
|
+
//#region src/core/sleep.ts
|
|
356
|
+
const SYSTEMD_INHIBIT_READY_TIMEOUT_MS = 5e3;
|
|
357
|
+
const SYSTEMD_INHIBIT_READY_POLL_MS = 25;
|
|
358
|
+
const GNHF_SLEEP_REEXEC_READY_PATH = "GNHF_SLEEP_REEXEC_READY_PATH";
|
|
359
|
+
const GNHF_SLEEP_REEXEC_READY_DIR_PREFIX = "gnhf-sleep-";
|
|
360
|
+
const GNHF_SLEEP_REEXEC_READY_FILENAME = "reexec-ready";
|
|
361
|
+
const HELPER_STARTUP_GRACE_MS = 100;
|
|
362
|
+
function getSignalExitCode$1(signal) {
|
|
363
|
+
if (signal === "SIGINT") return 130;
|
|
364
|
+
if (signal === "SIGTERM") return 143;
|
|
365
|
+
return 1;
|
|
366
|
+
}
|
|
367
|
+
async function waitForSpawn(child) {
|
|
368
|
+
return await new Promise((resolve) => {
|
|
369
|
+
child.once("spawn", () => resolve(true));
|
|
370
|
+
child.once("error", () => resolve(false));
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
async function waitForHelperStability(child, timeoutMs) {
|
|
374
|
+
return await new Promise((resolve) => {
|
|
375
|
+
let settled = false;
|
|
376
|
+
let timer = null;
|
|
377
|
+
const settle = (value) => {
|
|
378
|
+
if (settled) return;
|
|
379
|
+
settled = true;
|
|
380
|
+
if (timer) {
|
|
381
|
+
clearTimeout(timer);
|
|
382
|
+
timer = null;
|
|
383
|
+
}
|
|
384
|
+
resolve(value);
|
|
385
|
+
};
|
|
386
|
+
child.once("exit", () => {
|
|
387
|
+
settle(false);
|
|
388
|
+
});
|
|
389
|
+
child.once("error", () => {
|
|
390
|
+
settle(false);
|
|
391
|
+
});
|
|
392
|
+
if (child.exitCode != null || child.signalCode != null) {
|
|
393
|
+
settle(false);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
timer = setTimeout(() => {
|
|
397
|
+
settle(true);
|
|
398
|
+
}, timeoutMs);
|
|
399
|
+
timer.unref?.();
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
function isTrustedLinuxReexecReadyPath(readyPath) {
|
|
403
|
+
const resolvedReadyPath = resolve(readyPath);
|
|
404
|
+
const readyDir = dirname(resolvedReadyPath);
|
|
405
|
+
return basename(resolvedReadyPath) === GNHF_SLEEP_REEXEC_READY_FILENAME && dirname(readyDir) === resolve(tmpdir()) && basename(readyDir).startsWith(GNHF_SLEEP_REEXEC_READY_DIR_PREFIX);
|
|
406
|
+
}
|
|
407
|
+
function signalLinuxReexecReady(env) {
|
|
408
|
+
const readyPath = env[GNHF_SLEEP_REEXEC_READY_PATH];
|
|
409
|
+
if (!readyPath) return;
|
|
410
|
+
if (!isTrustedLinuxReexecReadyPath(readyPath)) {
|
|
411
|
+
appendDebugLog("sleep:ready-signal-failed", {
|
|
412
|
+
command: "systemd-inhibit",
|
|
413
|
+
error: "untrusted ready path"
|
|
414
|
+
});
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
writeFileSync(readyPath, "ready\n", {
|
|
419
|
+
encoding: "utf-8",
|
|
420
|
+
flag: "wx"
|
|
421
|
+
});
|
|
422
|
+
} catch (error) {
|
|
423
|
+
appendDebugLog("sleep:ready-signal-failed", {
|
|
424
|
+
command: "systemd-inhibit",
|
|
425
|
+
error: error instanceof Error ? error.message : String(error)
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async function waitForLinuxReexecReady(readyPath, exitStatePromise, timeoutMs) {
|
|
430
|
+
if (existsSync(readyPath)) return { type: "ready" };
|
|
431
|
+
return await new Promise((resolve) => {
|
|
432
|
+
let settled = false;
|
|
433
|
+
const settle = (result) => {
|
|
434
|
+
if (settled) return;
|
|
435
|
+
settled = true;
|
|
436
|
+
clearInterval(poller);
|
|
437
|
+
clearTimeout(timeout);
|
|
438
|
+
resolve(result);
|
|
439
|
+
};
|
|
440
|
+
const poller = setInterval(() => {
|
|
441
|
+
if (existsSync(readyPath)) settle({ type: "ready" });
|
|
442
|
+
}, SYSTEMD_INHIBIT_READY_POLL_MS);
|
|
443
|
+
poller.unref?.();
|
|
444
|
+
const timeout = setTimeout(() => {
|
|
445
|
+
settle({ type: "timeout" });
|
|
446
|
+
}, timeoutMs);
|
|
447
|
+
timeout.unref?.();
|
|
448
|
+
exitStatePromise.then(({ exitCode, signal }) => {
|
|
449
|
+
settle({
|
|
450
|
+
type: "exit",
|
|
451
|
+
exitCode,
|
|
452
|
+
signal
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
function forwardTerminationSignalsToChild(child, detached, killProcess, processOn, processOff) {
|
|
458
|
+
const listeners = [];
|
|
459
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
460
|
+
const listener = () => {
|
|
461
|
+
try {
|
|
462
|
+
signalChildProcess(child, {
|
|
463
|
+
detached,
|
|
464
|
+
killProcess,
|
|
465
|
+
signal
|
|
466
|
+
});
|
|
467
|
+
} catch {}
|
|
468
|
+
};
|
|
469
|
+
processOn(signal, listener);
|
|
470
|
+
listeners.push([signal, listener]);
|
|
471
|
+
}
|
|
472
|
+
return () => {
|
|
473
|
+
for (const [signal, listener] of listeners) processOff(signal, listener);
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function buildPowerShellCommand(parentPid) {
|
|
477
|
+
return [
|
|
478
|
+
"Add-Type @'",
|
|
479
|
+
"using System;",
|
|
480
|
+
"using System.Runtime.InteropServices;",
|
|
481
|
+
"public static class SleepBlock {",
|
|
482
|
+
" [DllImport(\"kernel32.dll\")]",
|
|
483
|
+
" public static extern uint SetThreadExecutionState(uint flags);",
|
|
484
|
+
"}",
|
|
485
|
+
"'@;",
|
|
486
|
+
"$ES_CONTINUOUS = 0x80000000;",
|
|
487
|
+
"$ES_SYSTEM_REQUIRED = 0x00000001;",
|
|
488
|
+
"[SleepBlock]::SetThreadExecutionState($ES_CONTINUOUS -bor $ES_SYSTEM_REQUIRED) | Out-Null;",
|
|
489
|
+
`try { Wait-Process -Id ${parentPid} } catch { } finally { [SleepBlock]::SetThreadExecutionState($ES_CONTINUOUS) | Out-Null }`
|
|
490
|
+
].join("\n");
|
|
491
|
+
}
|
|
492
|
+
async function startHelperProcess(command, args, spawnFn, env) {
|
|
493
|
+
const child = spawnFn(command, args, {
|
|
494
|
+
env,
|
|
495
|
+
stdio: "ignore"
|
|
496
|
+
});
|
|
497
|
+
if (!await waitForSpawn(child)) {
|
|
498
|
+
appendDebugLog("sleep:unavailable", { command });
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
if (!await waitForHelperStability(child, HELPER_STARTUP_GRACE_MS)) {
|
|
502
|
+
appendDebugLog("sleep:unavailable", {
|
|
503
|
+
command,
|
|
504
|
+
reason: "early-exit"
|
|
505
|
+
});
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
return child;
|
|
509
|
+
}
|
|
510
|
+
async function startSleepPrevention(argv, deps = {}) {
|
|
511
|
+
const env = deps.env ?? process.env;
|
|
512
|
+
const killProcess = deps.killProcess ?? process.kill.bind(process);
|
|
513
|
+
const pid = deps.pid ?? process.pid;
|
|
514
|
+
const platform = deps.platform ?? process.platform;
|
|
515
|
+
const processExecArgv = deps.processExecArgv ?? process.execArgv;
|
|
516
|
+
const processArgv1 = deps.processArgv1 ?? process.argv[1];
|
|
517
|
+
const processExecPath = deps.processExecPath ?? process.execPath;
|
|
518
|
+
const processOn = deps.processOn ?? process.on.bind(process);
|
|
519
|
+
const processOff = deps.processOff ?? process.off.bind(process);
|
|
520
|
+
const reexecEnv = deps.reexecEnv ?? {};
|
|
521
|
+
const spawnFn = deps.spawn ?? spawn;
|
|
522
|
+
if (platform === "linux") {
|
|
523
|
+
if (env.GNHF_SLEEP_INHIBITED === "1") {
|
|
524
|
+
signalLinuxReexecReady(env);
|
|
525
|
+
return {
|
|
526
|
+
type: "skipped",
|
|
527
|
+
reason: "already-inhibited"
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
const readyDir = mkdtempSync(join(tmpdir(), GNHF_SLEEP_REEXEC_READY_DIR_PREFIX));
|
|
531
|
+
const readyPath = join(readyDir, GNHF_SLEEP_REEXEC_READY_FILENAME);
|
|
532
|
+
const child = spawnFn("systemd-inhibit", [
|
|
533
|
+
"--what=idle:sleep",
|
|
534
|
+
"--mode=block",
|
|
535
|
+
"--who=gnhf",
|
|
536
|
+
"--why=Prevent sleep while gnhf is running",
|
|
537
|
+
processExecPath,
|
|
538
|
+
...processExecArgv,
|
|
539
|
+
processArgv1,
|
|
540
|
+
...argv
|
|
541
|
+
], {
|
|
542
|
+
detached: true,
|
|
543
|
+
env: {
|
|
544
|
+
...env,
|
|
545
|
+
...reexecEnv,
|
|
546
|
+
GNHF_SLEEP_INHIBITED: "1",
|
|
547
|
+
[GNHF_SLEEP_REEXEC_READY_PATH]: readyPath
|
|
548
|
+
},
|
|
549
|
+
stdio: "inherit"
|
|
550
|
+
});
|
|
551
|
+
const exitStatePromise = new Promise((resolve) => {
|
|
552
|
+
child.once("exit", (code, signal) => {
|
|
553
|
+
resolve({
|
|
554
|
+
exitCode: signal ? getSignalExitCode$1(signal) : code ?? 1,
|
|
555
|
+
signal
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
const stopForwardingSignals = forwardTerminationSignalsToChild(child, true, killProcess, processOn, processOff);
|
|
560
|
+
if (!await waitForSpawn(child)) {
|
|
561
|
+
stopForwardingSignals();
|
|
562
|
+
rmSync(readyDir, {
|
|
563
|
+
recursive: true,
|
|
564
|
+
force: true
|
|
565
|
+
});
|
|
566
|
+
appendDebugLog("sleep:unavailable", { command: "systemd-inhibit" });
|
|
567
|
+
return {
|
|
568
|
+
type: "skipped",
|
|
569
|
+
reason: "unavailable"
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
try {
|
|
573
|
+
const readyState = await waitForLinuxReexecReady(readyPath, exitStatePromise, SYSTEMD_INHIBIT_READY_TIMEOUT_MS);
|
|
574
|
+
try {
|
|
575
|
+
if (readyState.type === "ready") {
|
|
576
|
+
appendDebugLog("sleep:reexec", { command: "systemd-inhibit" });
|
|
577
|
+
const { exitCode } = await exitStatePromise;
|
|
578
|
+
return {
|
|
579
|
+
type: "reexeced",
|
|
580
|
+
exitCode
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
if (readyState.type === "exit") {
|
|
584
|
+
if (readyState.signal === "SIGINT" || readyState.signal === "SIGTERM") {
|
|
585
|
+
appendDebugLog("sleep:reexec", {
|
|
586
|
+
command: "systemd-inhibit",
|
|
587
|
+
signal: readyState.signal
|
|
588
|
+
});
|
|
589
|
+
return {
|
|
590
|
+
type: "reexeced",
|
|
591
|
+
exitCode: readyState.exitCode
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
if (readyState.exitCode !== 0) {
|
|
595
|
+
if (existsSync(readyPath)) {
|
|
596
|
+
appendDebugLog("sleep:reexec", {
|
|
597
|
+
command: "systemd-inhibit",
|
|
598
|
+
exitCode: readyState.exitCode,
|
|
599
|
+
readySignal: "late"
|
|
600
|
+
});
|
|
601
|
+
return {
|
|
602
|
+
type: "reexeced",
|
|
603
|
+
exitCode: readyState.exitCode
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
appendDebugLog("sleep:unavailable", {
|
|
607
|
+
command: "systemd-inhibit",
|
|
608
|
+
exitCode: readyState.exitCode
|
|
609
|
+
});
|
|
610
|
+
return {
|
|
611
|
+
type: "skipped",
|
|
612
|
+
reason: "unavailable"
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
appendDebugLog("sleep:reexec", {
|
|
616
|
+
command: "systemd-inhibit",
|
|
617
|
+
readySignal: false
|
|
618
|
+
});
|
|
619
|
+
return {
|
|
620
|
+
type: "reexeced",
|
|
621
|
+
exitCode: readyState.exitCode
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
appendDebugLog("sleep:unavailable", {
|
|
625
|
+
command: "systemd-inhibit",
|
|
626
|
+
reason: "timeout",
|
|
627
|
+
timeoutMs: SYSTEMD_INHIBIT_READY_TIMEOUT_MS
|
|
628
|
+
});
|
|
629
|
+
await shutdownChildProcess(child, {
|
|
630
|
+
detached: true,
|
|
631
|
+
killProcess,
|
|
632
|
+
timeoutMs: 1e3
|
|
633
|
+
});
|
|
634
|
+
return {
|
|
635
|
+
type: "skipped",
|
|
636
|
+
reason: "unavailable"
|
|
637
|
+
};
|
|
638
|
+
} finally {
|
|
639
|
+
stopForwardingSignals();
|
|
640
|
+
}
|
|
641
|
+
} finally {
|
|
642
|
+
rmSync(readyDir, {
|
|
643
|
+
recursive: true,
|
|
644
|
+
force: true
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (platform === "darwin") {
|
|
649
|
+
const child = await startHelperProcess("caffeinate", [
|
|
650
|
+
"-i",
|
|
651
|
+
"-w",
|
|
652
|
+
String(pid)
|
|
653
|
+
], spawnFn, env);
|
|
654
|
+
if (!child) return {
|
|
655
|
+
type: "skipped",
|
|
656
|
+
reason: "unavailable"
|
|
657
|
+
};
|
|
658
|
+
appendDebugLog("sleep:active", { command: "caffeinate" });
|
|
659
|
+
return {
|
|
660
|
+
type: "active",
|
|
661
|
+
cleanup: async () => {
|
|
662
|
+
appendDebugLog("sleep:cleanup", { command: "caffeinate" });
|
|
663
|
+
await shutdownChildProcess(child, {
|
|
664
|
+
detached: false,
|
|
665
|
+
timeoutMs: 1e3
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
if (platform === "win32") {
|
|
671
|
+
const child = await startHelperProcess("powershell.exe", [
|
|
672
|
+
"-NoLogo",
|
|
673
|
+
"-NoProfile",
|
|
674
|
+
"-NonInteractive",
|
|
675
|
+
"-ExecutionPolicy",
|
|
676
|
+
"Bypass",
|
|
677
|
+
"-Command",
|
|
678
|
+
buildPowerShellCommand(pid)
|
|
679
|
+
], spawnFn, env);
|
|
680
|
+
if (!child) return {
|
|
681
|
+
type: "skipped",
|
|
682
|
+
reason: "unavailable"
|
|
683
|
+
};
|
|
684
|
+
appendDebugLog("sleep:active", { command: "powershell.exe" });
|
|
685
|
+
return {
|
|
686
|
+
type: "active",
|
|
687
|
+
cleanup: async () => {
|
|
688
|
+
appendDebugLog("sleep:cleanup", { command: "powershell.exe" });
|
|
689
|
+
await shutdownChildProcess(child, {
|
|
690
|
+
detached: false,
|
|
691
|
+
timeoutMs: 1e3
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
return {
|
|
697
|
+
type: "skipped",
|
|
698
|
+
reason: "unsupported"
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
//#endregion
|
|
253
702
|
//#region src/core/agents/stream-utils.ts
|
|
254
703
|
/**
|
|
255
704
|
* Wire stderr collection, spawn-error handling, and the common close-handler
|
|
@@ -486,6 +935,21 @@ function buildPrompt(prompt) {
|
|
|
486
935
|
`The JSON must match this schema exactly: ${JSON.stringify(AGENT_OUTPUT_SCHEMA)}`
|
|
487
936
|
].join("\n");
|
|
488
937
|
}
|
|
938
|
+
/**
|
|
939
|
+
* On Windows with `shell: true`, `child.pid` is the `cmd.exe` wrapper, not
|
|
940
|
+
* the actual server process. `taskkill /T` terminates the entire process
|
|
941
|
+
* tree rooted at that PID so the real server doesn't survive shutdown.
|
|
942
|
+
*/
|
|
943
|
+
async function killWindowsProcessTree(pid) {
|
|
944
|
+
try {
|
|
945
|
+
execFileSync("taskkill", [
|
|
946
|
+
"/T",
|
|
947
|
+
"/F",
|
|
948
|
+
"/PID",
|
|
949
|
+
String(pid)
|
|
950
|
+
], { stdio: "ignore" });
|
|
951
|
+
} catch {}
|
|
952
|
+
}
|
|
489
953
|
function createAbortError$1() {
|
|
490
954
|
return /* @__PURE__ */ new Error("Agent was aborted");
|
|
491
955
|
}
|
|
@@ -557,6 +1021,7 @@ var OpenCodeAgent = class {
|
|
|
557
1021
|
fetchFn;
|
|
558
1022
|
getPortFn;
|
|
559
1023
|
killProcessFn;
|
|
1024
|
+
platform;
|
|
560
1025
|
spawnFn;
|
|
561
1026
|
server = null;
|
|
562
1027
|
closingPromise = null;
|
|
@@ -564,6 +1029,7 @@ var OpenCodeAgent = class {
|
|
|
564
1029
|
this.fetchFn = deps.fetch ?? fetch;
|
|
565
1030
|
this.getPortFn = deps.getPort ?? getAvailablePort$1;
|
|
566
1031
|
this.killProcessFn = deps.killProcess ?? process.kill.bind(process);
|
|
1032
|
+
this.platform = deps.platform ?? process.platform;
|
|
567
1033
|
this.spawnFn = deps.spawn ?? spawn;
|
|
568
1034
|
}
|
|
569
1035
|
async run(prompt, cwd, options) {
|
|
@@ -609,7 +1075,8 @@ var OpenCodeAgent = class {
|
|
|
609
1075
|
return this.server;
|
|
610
1076
|
}
|
|
611
1077
|
const port = await this.getPortFn();
|
|
612
|
-
const
|
|
1078
|
+
const isWindows = this.platform === "win32";
|
|
1079
|
+
const detached = !isWindows;
|
|
613
1080
|
const child = this.spawnFn("opencode", [
|
|
614
1081
|
"serve",
|
|
615
1082
|
"--hostname",
|
|
@@ -620,6 +1087,7 @@ var OpenCodeAgent = class {
|
|
|
620
1087
|
], {
|
|
621
1088
|
cwd,
|
|
622
1089
|
detached,
|
|
1090
|
+
shell: isWindows,
|
|
623
1091
|
stdio: [
|
|
624
1092
|
"ignore",
|
|
625
1093
|
"pipe",
|
|
@@ -652,6 +1120,11 @@ var OpenCodeAgent = class {
|
|
|
652
1120
|
if (this.server === server) this.server = null;
|
|
653
1121
|
});
|
|
654
1122
|
this.server = server;
|
|
1123
|
+
appendDebugLog("opencode:spawn", {
|
|
1124
|
+
cwd,
|
|
1125
|
+
port,
|
|
1126
|
+
detached
|
|
1127
|
+
});
|
|
655
1128
|
server.readyPromise = this.waitForHealthy(server, signal).catch(async (error) => {
|
|
656
1129
|
await this.shutdownServer();
|
|
657
1130
|
throw error;
|
|
@@ -934,37 +1407,20 @@ var OpenCodeAgent = class {
|
|
|
934
1407
|
return;
|
|
935
1408
|
}
|
|
936
1409
|
const server = this.server;
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
return;
|
|
941
|
-
}
|
|
942
|
-
server.child.once("close", () => resolve());
|
|
943
|
-
});
|
|
944
|
-
try {
|
|
945
|
-
this.signalServer(server, "SIGTERM");
|
|
946
|
-
} catch {}
|
|
947
|
-
const forceKill = new Promise((resolve) => {
|
|
948
|
-
setTimeout(() => {
|
|
949
|
-
if (!server.closed) try {
|
|
950
|
-
this.signalServer(server, "SIGKILL");
|
|
951
|
-
} catch {}
|
|
952
|
-
resolve();
|
|
953
|
-
}, 3e3).unref?.();
|
|
1410
|
+
appendDebugLog("opencode:shutdown", {
|
|
1411
|
+
cwd: server.cwd,
|
|
1412
|
+
port: server.port
|
|
954
1413
|
});
|
|
955
|
-
this.closingPromise =
|
|
1414
|
+
this.closingPromise = (this.platform === "win32" && server.child.pid ? killWindowsProcessTree(server.child.pid) : shutdownChildProcess(server.child, {
|
|
1415
|
+
detached: server.detached,
|
|
1416
|
+
killProcess: this.killProcessFn,
|
|
1417
|
+
timeoutMs: 3e3
|
|
1418
|
+
})).finally(() => {
|
|
956
1419
|
if (this.server === server) this.server = null;
|
|
957
1420
|
this.closingPromise = null;
|
|
958
1421
|
});
|
|
959
1422
|
await this.closingPromise;
|
|
960
1423
|
}
|
|
961
|
-
signalServer(server, signal) {
|
|
962
|
-
if (server.detached && server.child.pid) try {
|
|
963
|
-
this.killProcessFn(-server.child.pid, signal);
|
|
964
|
-
return;
|
|
965
|
-
} catch {}
|
|
966
|
-
server.child.kill(signal);
|
|
967
|
-
}
|
|
968
1424
|
async requestJSON(server, path, options) {
|
|
969
1425
|
const body = await this.requestText(server, path, options);
|
|
970
1426
|
return JSON.parse(body);
|
|
@@ -1149,6 +1605,11 @@ var RovoDevAgent = class {
|
|
|
1149
1605
|
if (this.server === server) this.server = null;
|
|
1150
1606
|
});
|
|
1151
1607
|
this.server = server;
|
|
1608
|
+
appendDebugLog("rovodev:spawn", {
|
|
1609
|
+
cwd,
|
|
1610
|
+
port,
|
|
1611
|
+
detached
|
|
1612
|
+
});
|
|
1152
1613
|
server.readyPromise = this.waitForHealthy(server, signal).catch(async (error) => {
|
|
1153
1614
|
await this.shutdownServer();
|
|
1154
1615
|
throw error;
|
|
@@ -1158,13 +1619,13 @@ var RovoDevAgent = class {
|
|
|
1158
1619
|
}
|
|
1159
1620
|
async waitForHealthy(server, signal) {
|
|
1160
1621
|
const deadline = Date.now() + 3e4;
|
|
1161
|
-
let
|
|
1622
|
+
let spawnErrorMessage = null;
|
|
1162
1623
|
server.child.once("error", (error) => {
|
|
1163
|
-
|
|
1624
|
+
spawnErrorMessage = error.message;
|
|
1164
1625
|
});
|
|
1165
1626
|
while (Date.now() < deadline) {
|
|
1166
1627
|
if (signal?.aborted) throw createAbortError();
|
|
1167
|
-
if (
|
|
1628
|
+
if (spawnErrorMessage) throw new Error(`Failed to spawn rovodev: ${spawnErrorMessage}`);
|
|
1168
1629
|
if (server.closed) {
|
|
1169
1630
|
const output = server.stderr.trim() || server.stdout.trim();
|
|
1170
1631
|
throw new Error(output ? `rovodev exited before becoming ready: ${output}` : "rovodev exited before becoming ready");
|
|
@@ -1372,37 +1833,20 @@ var RovoDevAgent = class {
|
|
|
1372
1833
|
return;
|
|
1373
1834
|
}
|
|
1374
1835
|
const server = this.server;
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
return;
|
|
1379
|
-
}
|
|
1380
|
-
server.child.once("close", () => resolve());
|
|
1381
|
-
});
|
|
1382
|
-
try {
|
|
1383
|
-
this.signalServer(server, "SIGTERM");
|
|
1384
|
-
} catch {}
|
|
1385
|
-
const forceKill = new Promise((resolve) => {
|
|
1386
|
-
setTimeout(() => {
|
|
1387
|
-
if (!server.closed) try {
|
|
1388
|
-
this.signalServer(server, "SIGKILL");
|
|
1389
|
-
} catch {}
|
|
1390
|
-
resolve();
|
|
1391
|
-
}, 3e3).unref?.();
|
|
1836
|
+
appendDebugLog("rovodev:shutdown", {
|
|
1837
|
+
cwd: server.cwd,
|
|
1838
|
+
port: server.port
|
|
1392
1839
|
});
|
|
1393
|
-
this.closingPromise =
|
|
1840
|
+
this.closingPromise = shutdownChildProcess(server.child, {
|
|
1841
|
+
detached: server.detached,
|
|
1842
|
+
killProcess: this.killProcessFn,
|
|
1843
|
+
timeoutMs: 3e3
|
|
1844
|
+
}).finally(() => {
|
|
1394
1845
|
if (this.server === server) this.server = null;
|
|
1395
1846
|
this.closingPromise = null;
|
|
1396
1847
|
});
|
|
1397
1848
|
await this.closingPromise;
|
|
1398
1849
|
}
|
|
1399
|
-
signalServer(server, signal) {
|
|
1400
|
-
if (server.detached && server.child.pid) try {
|
|
1401
|
-
this.killProcessFn(-server.child.pid, signal);
|
|
1402
|
-
return;
|
|
1403
|
-
} catch {}
|
|
1404
|
-
server.child.kill(signal);
|
|
1405
|
-
}
|
|
1406
1850
|
async requestJSON(server, path, options) {
|
|
1407
1851
|
return await (await this.request(server, path, options)).json();
|
|
1408
1852
|
}
|
|
@@ -1466,6 +1910,7 @@ ${params.prompt}`;
|
|
|
1466
1910
|
}
|
|
1467
1911
|
//#endregion
|
|
1468
1912
|
//#region src/core/orchestrator.ts
|
|
1913
|
+
const STOP_CLOSE_AGENT_GRACE_MS = 250;
|
|
1469
1914
|
var Orchestrator = class extends EventEmitter {
|
|
1470
1915
|
config;
|
|
1471
1916
|
agent;
|
|
@@ -1475,6 +1920,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
1475
1920
|
limits;
|
|
1476
1921
|
stopRequested = false;
|
|
1477
1922
|
stopPromise = null;
|
|
1923
|
+
activeIterationPromise = null;
|
|
1478
1924
|
activeAbortController = null;
|
|
1479
1925
|
pendingAbortReason = null;
|
|
1480
1926
|
state = {
|
|
@@ -1510,7 +1956,23 @@ var Orchestrator = class extends EventEmitter {
|
|
|
1510
1956
|
this.activeAbortController?.abort();
|
|
1511
1957
|
if (this.stopPromise) return;
|
|
1512
1958
|
this.stopPromise = (async () => {
|
|
1513
|
-
|
|
1959
|
+
if (this.activeIterationPromise) {
|
|
1960
|
+
const iterationPromise = this.activeIterationPromise.catch(() => void 0);
|
|
1961
|
+
await new Promise((resolve) => {
|
|
1962
|
+
let settled = false;
|
|
1963
|
+
const settle = () => {
|
|
1964
|
+
if (settled) return;
|
|
1965
|
+
settled = true;
|
|
1966
|
+
clearTimeout(timer);
|
|
1967
|
+
resolve();
|
|
1968
|
+
};
|
|
1969
|
+
const timer = setTimeout(settle, STOP_CLOSE_AGENT_GRACE_MS);
|
|
1970
|
+
timer.unref?.();
|
|
1971
|
+
iterationPromise.finally(settle);
|
|
1972
|
+
});
|
|
1973
|
+
await this.closeAgent();
|
|
1974
|
+
await iterationPromise;
|
|
1975
|
+
} else await this.closeAgent();
|
|
1514
1976
|
resetHard(this.cwd);
|
|
1515
1977
|
this.state.status = "stopped";
|
|
1516
1978
|
this.emit("state", this.getState());
|
|
@@ -1537,7 +1999,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
1537
1999
|
runId: this.runInfo.runId,
|
|
1538
2000
|
prompt: this.prompt
|
|
1539
2001
|
});
|
|
1540
|
-
|
|
2002
|
+
this.activeIterationPromise = this.runIteration(iterationPrompt);
|
|
2003
|
+
const result = await this.activeIterationPromise;
|
|
2004
|
+
this.activeIterationPromise = null;
|
|
2005
|
+
if (result.type === "stopped") break;
|
|
1541
2006
|
if (result.type === "aborted") {
|
|
1542
2007
|
this.abort(result.reason);
|
|
1543
2008
|
break;
|
|
@@ -1569,6 +2034,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
1569
2034
|
}
|
|
1570
2035
|
}
|
|
1571
2036
|
} finally {
|
|
2037
|
+
this.activeIterationPromise = null;
|
|
1572
2038
|
if (this.stopPromise) await this.stopPromise;
|
|
1573
2039
|
else await this.closeAgent();
|
|
1574
2040
|
}
|
|
@@ -1600,6 +2066,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
1600
2066
|
signal: this.activeAbortController.signal,
|
|
1601
2067
|
logPath
|
|
1602
2068
|
});
|
|
2069
|
+
if (this.stopRequested) return { type: "stopped" };
|
|
1603
2070
|
if (result.output.success) return {
|
|
1604
2071
|
type: "completed",
|
|
1605
2072
|
record: this.recordSuccess(result.output)
|
|
@@ -1616,6 +2083,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
1616
2083
|
reason: this.pendingAbortReason
|
|
1617
2084
|
};
|
|
1618
2085
|
}
|
|
2086
|
+
if (this.stopRequested) return { type: "stopped" };
|
|
1619
2087
|
const summary = err instanceof Error ? err.message : String(err);
|
|
1620
2088
|
return {
|
|
1621
2089
|
type: "completed",
|
|
@@ -1888,7 +2356,68 @@ function formatTokens(count) {
|
|
|
1888
2356
|
return String(count);
|
|
1889
2357
|
}
|
|
1890
2358
|
//#endregion
|
|
2359
|
+
//#region src/utils/terminal-width.ts
|
|
2360
|
+
const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
2361
|
+
const MARK_REGEX = /\p{Mark}/u;
|
|
2362
|
+
const REGIONAL_INDICATOR_REGEX = /\p{Regional_Indicator}/u;
|
|
2363
|
+
const EXTENDED_PICTOGRAPHIC_REGEX = /\p{Extended_Pictographic}/u;
|
|
2364
|
+
function isFullWidthCodePoint(codePoint) {
|
|
2365
|
+
return codePoint >= 4352 && (codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 12871 && codePoint !== 12351 || codePoint >= 12880 && codePoint <= 19903 || codePoint >= 19968 && codePoint <= 42182 || codePoint >= 43360 && codePoint <= 43388 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65040 && codePoint <= 65049 || codePoint >= 65072 && codePoint <= 65131 || codePoint >= 65281 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 110592 && codePoint <= 110593 || codePoint >= 127488 && codePoint <= 127569 || codePoint >= 131072 && codePoint <= 262141);
|
|
2366
|
+
}
|
|
2367
|
+
function codePointWidth(codePoint) {
|
|
2368
|
+
if (codePoint === 0 || codePoint === 8204 || codePoint === 8205 || codePoint === 65038 || codePoint === 65039) return 0;
|
|
2369
|
+
if (MARK_REGEX.test(String.fromCodePoint(codePoint))) return 0;
|
|
2370
|
+
return isFullWidthCodePoint(codePoint) ? 2 : 1;
|
|
2371
|
+
}
|
|
2372
|
+
function isWideEmojiGrapheme(grapheme) {
|
|
2373
|
+
return grapheme.includes("") || grapheme.includes("️") || grapheme.includes("⃣") || REGIONAL_INDICATOR_REGEX.test(grapheme) || Array.from(grapheme).some((char) => EXTENDED_PICTOGRAPHIC_REGEX.test(char));
|
|
2374
|
+
}
|
|
2375
|
+
function splitGraphemes(text) {
|
|
2376
|
+
return Array.from(graphemeSegmenter.segment(text), ({ segment }) => segment);
|
|
2377
|
+
}
|
|
2378
|
+
function graphemeWidth(grapheme) {
|
|
2379
|
+
if (!grapheme) return 0;
|
|
2380
|
+
if (isWideEmojiGrapheme(grapheme)) return 2;
|
|
2381
|
+
let width = 0;
|
|
2382
|
+
for (const char of grapheme) width += codePointWidth(char.codePointAt(0) ?? 0);
|
|
2383
|
+
return width;
|
|
2384
|
+
}
|
|
2385
|
+
function stringWidth(text) {
|
|
2386
|
+
let width = 0;
|
|
2387
|
+
for (const grapheme of splitGraphemes(text)) width += graphemeWidth(grapheme);
|
|
2388
|
+
return width;
|
|
2389
|
+
}
|
|
2390
|
+
//#endregion
|
|
1891
2391
|
//#region src/utils/wordwrap.ts
|
|
2392
|
+
function sliceToWidth(text, width) {
|
|
2393
|
+
let result = "";
|
|
2394
|
+
let currentWidth = 0;
|
|
2395
|
+
for (const grapheme of splitGraphemes(text)) {
|
|
2396
|
+
const nextWidth = currentWidth + graphemeWidth(grapheme);
|
|
2397
|
+
if (nextWidth > width) break;
|
|
2398
|
+
result += grapheme;
|
|
2399
|
+
currentWidth = nextWidth;
|
|
2400
|
+
}
|
|
2401
|
+
return result;
|
|
2402
|
+
}
|
|
2403
|
+
function splitByWidth(text, width) {
|
|
2404
|
+
const lines = [];
|
|
2405
|
+
let current = "";
|
|
2406
|
+
let currentWidth = 0;
|
|
2407
|
+
for (const grapheme of splitGraphemes(text)) {
|
|
2408
|
+
const glyphWidth = graphemeWidth(grapheme);
|
|
2409
|
+
if (current && currentWidth + glyphWidth > width) {
|
|
2410
|
+
lines.push(current);
|
|
2411
|
+
current = grapheme;
|
|
2412
|
+
currentWidth = glyphWidth;
|
|
2413
|
+
continue;
|
|
2414
|
+
}
|
|
2415
|
+
current += grapheme;
|
|
2416
|
+
currentWidth += glyphWidth;
|
|
2417
|
+
}
|
|
2418
|
+
if (current) lines.push(current);
|
|
2419
|
+
return lines;
|
|
2420
|
+
}
|
|
1892
2421
|
function wordWrap(text, width, maxLines) {
|
|
1893
2422
|
if (!text) return [];
|
|
1894
2423
|
const lines = [];
|
|
@@ -1899,26 +2428,34 @@ function wordWrap(text, width, maxLines) {
|
|
|
1899
2428
|
continue;
|
|
1900
2429
|
}
|
|
1901
2430
|
let current = "";
|
|
2431
|
+
let currentWidth = 0;
|
|
1902
2432
|
for (const word of words) {
|
|
1903
|
-
|
|
2433
|
+
const wordWidth = stringWidth(word);
|
|
2434
|
+
if (wordWidth > width) {
|
|
1904
2435
|
if (current) {
|
|
1905
2436
|
lines.push(current);
|
|
1906
2437
|
current = "";
|
|
2438
|
+
currentWidth = 0;
|
|
1907
2439
|
}
|
|
1908
|
-
for (
|
|
2440
|
+
for (const slice of splitByWidth(word, width)) lines.push(slice);
|
|
1909
2441
|
continue;
|
|
1910
2442
|
}
|
|
1911
|
-
|
|
2443
|
+
const nextWidth = current ? currentWidth + 1 + wordWidth : wordWidth;
|
|
2444
|
+
if (current && nextWidth > width) {
|
|
1912
2445
|
lines.push(current);
|
|
1913
2446
|
current = word;
|
|
1914
|
-
|
|
2447
|
+
currentWidth = wordWidth;
|
|
2448
|
+
} else {
|
|
2449
|
+
current = current ? current + " " + word : word;
|
|
2450
|
+
currentWidth = nextWidth;
|
|
2451
|
+
}
|
|
1915
2452
|
}
|
|
1916
2453
|
if (current) lines.push(current);
|
|
1917
2454
|
}
|
|
1918
2455
|
if (maxLines && lines.length > maxLines) {
|
|
1919
2456
|
const capped = lines.slice(0, maxLines);
|
|
1920
2457
|
const last = capped[maxLines - 1];
|
|
1921
|
-
capped[maxLines - 1] = last
|
|
2458
|
+
capped[maxLines - 1] = stringWidth(last) >= width ? sliceToWidth(last, width - 1) + "…" : last + "…";
|
|
1922
2459
|
return capped;
|
|
1923
2460
|
}
|
|
1924
2461
|
return lines;
|
|
@@ -1934,13 +2471,13 @@ function makeCell(char, style) {
|
|
|
1934
2471
|
return {
|
|
1935
2472
|
char,
|
|
1936
2473
|
style,
|
|
1937
|
-
width: (char
|
|
2474
|
+
width: graphemeWidth(char)
|
|
1938
2475
|
};
|
|
1939
2476
|
}
|
|
1940
2477
|
function textToCells(text, style) {
|
|
1941
2478
|
const cells = [];
|
|
1942
|
-
for (const
|
|
1943
|
-
const cell = makeCell(
|
|
2479
|
+
for (const grapheme of splitGraphemes(text)) {
|
|
2480
|
+
const cell = makeCell(grapheme, style);
|
|
1944
2481
|
cells.push(cell);
|
|
1945
2482
|
if (cell.width === 2) cells.push({
|
|
1946
2483
|
char: "",
|
|
@@ -2022,7 +2559,7 @@ const TICK_MS = 200;
|
|
|
2022
2559
|
const MOONS_PER_ROW = 30;
|
|
2023
2560
|
const MOON_PHASE_PERIOD = 1600;
|
|
2024
2561
|
const MAX_MSG_LINES = 3;
|
|
2025
|
-
const MAX_MSG_LINE_LEN =
|
|
2562
|
+
const MAX_MSG_LINE_LEN = CONTENT_WIDTH;
|
|
2026
2563
|
const RESUME_HINT = "[ctrl+c to stop, gnhf again to resume]";
|
|
2027
2564
|
function spacedLabel(text) {
|
|
2028
2565
|
return text.split("").join(" ");
|
|
@@ -2116,52 +2653,124 @@ function renderSideStarsCells(stars, rowIndex, xOffset, sideWidth, now) {
|
|
|
2116
2653
|
placeStarsInCells(cells, stars, rowIndex, xOffset, xOffset + sideWidth, xOffset, now);
|
|
2117
2654
|
return cells;
|
|
2118
2655
|
}
|
|
2656
|
+
function clampCellsToWidth(content, width) {
|
|
2657
|
+
if (content.length <= width) return content;
|
|
2658
|
+
const clamped = [];
|
|
2659
|
+
let remaining = width;
|
|
2660
|
+
for (let i = 0; i < content.length && remaining > 0; i++) {
|
|
2661
|
+
const cell = content[i];
|
|
2662
|
+
if (cell.width === 0) continue;
|
|
2663
|
+
if (cell.width > remaining) break;
|
|
2664
|
+
clamped.push(cell);
|
|
2665
|
+
remaining -= cell.width;
|
|
2666
|
+
if (cell.width === 2 && content[i + 1]?.width === 0) {
|
|
2667
|
+
clamped.push(content[i + 1]);
|
|
2668
|
+
i += 1;
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
return clamped;
|
|
2672
|
+
}
|
|
2119
2673
|
function centerLineCells(content, width) {
|
|
2120
|
-
const
|
|
2674
|
+
const clamped = clampCellsToWidth(content, width);
|
|
2675
|
+
const w = clamped.length;
|
|
2121
2676
|
const pad = Math.max(0, Math.floor((width - w) / 2));
|
|
2122
2677
|
const rightPad = Math.max(0, width - w - pad);
|
|
2123
2678
|
return [
|
|
2124
2679
|
...emptyCells(pad),
|
|
2125
|
-
...
|
|
2680
|
+
...clamped,
|
|
2126
2681
|
...emptyCells(rightPad)
|
|
2127
2682
|
];
|
|
2128
2683
|
}
|
|
2129
2684
|
function renderResumeHintCells(width) {
|
|
2130
2685
|
return centerLineCells(textToCells(RESUME_HINT, "dim"), width);
|
|
2131
2686
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
return fitted.length > maxRows ? fitted.slice(fitted.length - maxRows) : fitted;
|
|
2141
|
-
}
|
|
2142
|
-
function buildContentCells(prompt, agentName, state, elapsed, now) {
|
|
2143
|
-
const rows = [];
|
|
2687
|
+
/**
|
|
2688
|
+
* Builds the centered content viewport for the renderer.
|
|
2689
|
+
*
|
|
2690
|
+
* When `availableHeight` is constrained, the layout drops optional sections in
|
|
2691
|
+
* priority order (ASCII art, eyebrow, agent message, then prompt) so the stats
|
|
2692
|
+
* row remains visible and any remaining space is used for the newest moon rows.
|
|
2693
|
+
*/
|
|
2694
|
+
function buildContentCells(prompt, agentName, state, elapsed, now, availableHeight) {
|
|
2144
2695
|
const isRunning = state.status === "running" || state.status === "waiting";
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2696
|
+
const moonRows = renderMoonStripCells(state.iterations, isRunning, now);
|
|
2697
|
+
const maxRows = availableHeight ?? Infinity;
|
|
2698
|
+
if (maxRows <= 0) return [];
|
|
2699
|
+
const titleCells = renderTitleCells(agentName);
|
|
2700
|
+
const titleSpacer = titleCells[1] ?? [];
|
|
2148
2701
|
const promptLines = wordWrap(prompt, CONTENT_WIDTH, MAX_PROMPT_LINES);
|
|
2702
|
+
const promptRows = [];
|
|
2149
2703
|
for (let i = 0; i < MAX_PROMPT_LINES; i++) {
|
|
2150
2704
|
const pl = promptLines[i] ?? "";
|
|
2151
|
-
|
|
2152
|
-
}
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2705
|
+
promptRows.push(pl ? textToCells(pl, "dim") : []);
|
|
2706
|
+
}
|
|
2707
|
+
const sections = {
|
|
2708
|
+
top: [[]],
|
|
2709
|
+
eyebrow: [
|
|
2710
|
+
titleCells[0],
|
|
2711
|
+
[],
|
|
2712
|
+
[]
|
|
2713
|
+
],
|
|
2714
|
+
art: titleCells.slice(2),
|
|
2715
|
+
prompt: [
|
|
2716
|
+
titleSpacer,
|
|
2717
|
+
...promptRows,
|
|
2718
|
+
[],
|
|
2719
|
+
[]
|
|
2720
|
+
],
|
|
2721
|
+
stats: [renderStatsCells(elapsed, state.totalInputTokens, state.totalOutputTokens, state.commitCount)],
|
|
2722
|
+
agent: [
|
|
2723
|
+
[],
|
|
2724
|
+
[],
|
|
2725
|
+
...renderAgentMessageCells(state.lastMessage, state.status)
|
|
2726
|
+
],
|
|
2727
|
+
moon: [
|
|
2728
|
+
[],
|
|
2729
|
+
[],
|
|
2730
|
+
...moonRows
|
|
2731
|
+
]
|
|
2732
|
+
};
|
|
2733
|
+
const flattenSections = () => [
|
|
2734
|
+
...sections.top,
|
|
2735
|
+
...sections.eyebrow,
|
|
2736
|
+
...sections.art,
|
|
2737
|
+
...sections.prompt,
|
|
2738
|
+
...sections.stats,
|
|
2739
|
+
...sections.agent,
|
|
2740
|
+
...sections.moon
|
|
2741
|
+
];
|
|
2742
|
+
const optionalSections = [
|
|
2743
|
+
"art",
|
|
2744
|
+
"eyebrow",
|
|
2745
|
+
"agent",
|
|
2746
|
+
"prompt"
|
|
2747
|
+
];
|
|
2748
|
+
let rows = flattenSections();
|
|
2749
|
+
for (const section of optionalSections) {
|
|
2750
|
+
if (rows.length <= maxRows) break;
|
|
2751
|
+
sections[section] = [];
|
|
2752
|
+
rows = flattenSections();
|
|
2753
|
+
}
|
|
2754
|
+
if (rows.length > maxRows) rows = rows.filter((row) => row.length > 0);
|
|
2755
|
+
if (rows.length > maxRows) {
|
|
2756
|
+
const nonMoonRows = [
|
|
2757
|
+
...sections.top,
|
|
2758
|
+
...sections.eyebrow,
|
|
2759
|
+
...sections.art,
|
|
2760
|
+
...sections.prompt,
|
|
2761
|
+
...sections.stats,
|
|
2762
|
+
...sections.agent
|
|
2763
|
+
].filter((row) => row.length > 0);
|
|
2764
|
+
const allowedMoonRows = Math.max(0, maxRows - nonMoonRows.length);
|
|
2765
|
+
const visibleMoonRows = allowedMoonRows === 0 ? [] : moonRows.filter((row) => row.length > 0).slice(-allowedMoonRows);
|
|
2766
|
+
rows = [...nonMoonRows, ...visibleMoonRows];
|
|
2767
|
+
}
|
|
2159
2768
|
return rows;
|
|
2160
2769
|
}
|
|
2161
2770
|
function buildFrameCells(prompt, agentName, state, topStars, bottomStars, sideStars, now, terminalWidth, terminalHeight) {
|
|
2162
2771
|
const elapsed = formatElapsed(now - state.startTime.getTime());
|
|
2163
2772
|
const availableHeight = Math.max(0, terminalHeight - 2);
|
|
2164
|
-
const contentRows =
|
|
2773
|
+
const contentRows = buildContentCells(prompt, agentName, state, elapsed, now, availableHeight);
|
|
2165
2774
|
while (contentRows.length < Math.min(BASE_CONTENT_ROWS, availableHeight)) contentRows.push([]);
|
|
2166
2775
|
const contentCount = contentRows.length;
|
|
2167
2776
|
const remaining = Math.max(0, availableHeight - contentCount);
|
|
@@ -2200,11 +2809,17 @@ var Renderer = class {
|
|
|
2200
2809
|
cachedHeight = 0;
|
|
2201
2810
|
prevCells = [];
|
|
2202
2811
|
isFirstFrame = true;
|
|
2812
|
+
seedTop;
|
|
2813
|
+
seedBottom;
|
|
2814
|
+
seedSide;
|
|
2203
2815
|
constructor(orchestrator, prompt, agentName) {
|
|
2204
2816
|
this.orchestrator = orchestrator;
|
|
2205
2817
|
this.prompt = prompt;
|
|
2206
2818
|
this.agentName = agentName;
|
|
2207
2819
|
this.state = orchestrator.getState();
|
|
2820
|
+
this.seedTop = Math.floor(Math.random() * 2147483646) + 1;
|
|
2821
|
+
this.seedBottom = Math.floor(Math.random() * 2147483646) + 1;
|
|
2822
|
+
this.seedSide = Math.floor(Math.random() * 2147483646) + 1;
|
|
2208
2823
|
this.exitPromise = new Promise((resolve) => {
|
|
2209
2824
|
this.exitResolve = resolve;
|
|
2210
2825
|
});
|
|
@@ -2217,14 +2832,14 @@ var Renderer = class {
|
|
|
2217
2832
|
};
|
|
2218
2833
|
});
|
|
2219
2834
|
this.orchestrator.on("stopped", () => {
|
|
2220
|
-
this.stop();
|
|
2835
|
+
this.stop("stopped");
|
|
2221
2836
|
});
|
|
2222
2837
|
if (process$1.stdin.isTTY) {
|
|
2223
2838
|
process$1.stdin.setRawMode(true);
|
|
2224
2839
|
process$1.stdin.resume();
|
|
2225
2840
|
process$1.stdin.on("data", (data) => {
|
|
2226
2841
|
if (data[0] === 3) {
|
|
2227
|
-
this.stop();
|
|
2842
|
+
this.stop("interrupted");
|
|
2228
2843
|
this.orchestrator.stop();
|
|
2229
2844
|
}
|
|
2230
2845
|
});
|
|
@@ -2232,7 +2847,7 @@ var Renderer = class {
|
|
|
2232
2847
|
this.interval = setInterval(() => this.render(), TICK_MS);
|
|
2233
2848
|
this.render();
|
|
2234
2849
|
}
|
|
2235
|
-
stop() {
|
|
2850
|
+
stop(reason = "stopped") {
|
|
2236
2851
|
if (this.interval) {
|
|
2237
2852
|
clearInterval(this.interval);
|
|
2238
2853
|
this.interval = null;
|
|
@@ -2242,7 +2857,7 @@ var Renderer = class {
|
|
|
2242
2857
|
process$1.stdin.pause();
|
|
2243
2858
|
process$1.stdin.removeAllListeners("data");
|
|
2244
2859
|
}
|
|
2245
|
-
this.exitResolve();
|
|
2860
|
+
this.exitResolve(reason);
|
|
2246
2861
|
}
|
|
2247
2862
|
waitUntilExit() {
|
|
2248
2863
|
return this.exitPromise;
|
|
@@ -2268,9 +2883,9 @@ var Renderer = class {
|
|
|
2268
2883
|
rest: "dim"
|
|
2269
2884
|
} : star;
|
|
2270
2885
|
};
|
|
2271
|
-
this.topStars = generateStarField(w, h, STAR_DENSITY,
|
|
2272
|
-
this.bottomStars = generateStarField(w, h, STAR_DENSITY,
|
|
2273
|
-
this.sideStars = generateStarField(w, Math.max(BASE_CONTENT_ROWS, availableHeight), STAR_DENSITY,
|
|
2886
|
+
this.topStars = generateStarField(w, h, STAR_DENSITY, this.seedTop).map((s) => shrinkBig(s, s.y >= topHeight - proximityRows));
|
|
2887
|
+
this.bottomStars = generateStarField(w, h, STAR_DENSITY, this.seedBottom).map((s) => shrinkBig(s, s.y < proximityRows));
|
|
2888
|
+
this.sideStars = generateStarField(w, Math.max(BASE_CONTENT_ROWS, availableHeight), STAR_DENSITY, this.seedSide);
|
|
2274
2889
|
return true;
|
|
2275
2890
|
}
|
|
2276
2891
|
return false;
|
|
@@ -2300,12 +2915,21 @@ function slugifyPrompt(prompt) {
|
|
|
2300
2915
|
//#region src/cli.ts
|
|
2301
2916
|
const packageVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8")).version;
|
|
2302
2917
|
const FORCE_EXIT_TIMEOUT_MS = 5e3;
|
|
2918
|
+
const GNHF_REEXEC_STDIN_PROMPT = "GNHF_REEXEC_STDIN_PROMPT";
|
|
2919
|
+
const GNHF_REEXEC_STDIN_PROMPT_FILE = "GNHF_REEXEC_STDIN_PROMPT_FILE";
|
|
2920
|
+
const GNHF_REEXEC_STDIN_PROMPT_DIR_PREFIX = "gnhf-stdin-";
|
|
2921
|
+
const GNHF_REEXEC_STDIN_PROMPT_FILENAME = "prompt.txt";
|
|
2303
2922
|
function parseNonNegativeInteger(value) {
|
|
2304
2923
|
if (!/^\d+$/.test(value)) throw new InvalidArgumentError("must be a non-negative integer");
|
|
2305
2924
|
const parsed = Number.parseInt(value, 10);
|
|
2306
2925
|
if (!Number.isSafeInteger(parsed)) throw new InvalidArgumentError("must be a safe integer");
|
|
2307
2926
|
return parsed;
|
|
2308
2927
|
}
|
|
2928
|
+
function parseOnOffBoolean(value) {
|
|
2929
|
+
if (value === "on" || value === "true") return true;
|
|
2930
|
+
if (value === "off" || value === "false") return false;
|
|
2931
|
+
throw new InvalidArgumentError("must be one of: \"on\", \"off\", \"true\", \"false\"");
|
|
2932
|
+
}
|
|
2309
2933
|
function humanizeErrorMessage(message) {
|
|
2310
2934
|
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.";
|
|
2311
2935
|
return message;
|
|
@@ -2330,8 +2954,57 @@ function ask(question) {
|
|
|
2330
2954
|
});
|
|
2331
2955
|
});
|
|
2332
2956
|
}
|
|
2957
|
+
function getSignalExitCode(signal) {
|
|
2958
|
+
return signal === "SIGINT" ? 130 : 143;
|
|
2959
|
+
}
|
|
2960
|
+
function persistStdinPromptForReexec(prompt) {
|
|
2961
|
+
const promptDir = mkdtempSync(join(tmpdir(), GNHF_REEXEC_STDIN_PROMPT_DIR_PREFIX));
|
|
2962
|
+
const promptPath = join(promptDir, GNHF_REEXEC_STDIN_PROMPT_FILENAME);
|
|
2963
|
+
writeFileSync(promptPath, prompt, {
|
|
2964
|
+
encoding: "utf-8",
|
|
2965
|
+
mode: 384
|
|
2966
|
+
});
|
|
2967
|
+
return {
|
|
2968
|
+
path: promptPath,
|
|
2969
|
+
cleanup: () => {
|
|
2970
|
+
rmSync(promptDir, {
|
|
2971
|
+
recursive: true,
|
|
2972
|
+
force: true
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2977
|
+
function isTrustedReexecPromptPath(promptPath) {
|
|
2978
|
+
const resolvedPromptPath = resolve(promptPath);
|
|
2979
|
+
const promptDir = dirname(resolvedPromptPath);
|
|
2980
|
+
return basename(resolvedPromptPath) === GNHF_REEXEC_STDIN_PROMPT_FILENAME && dirname(promptDir) === resolve(tmpdir()) && basename(promptDir).startsWith(GNHF_REEXEC_STDIN_PROMPT_DIR_PREFIX);
|
|
2981
|
+
}
|
|
2982
|
+
function cleanupTrustedReexecPromptPath(promptPath) {
|
|
2983
|
+
if (!isTrustedReexecPromptPath(promptPath)) return;
|
|
2984
|
+
const resolvedPromptPath = resolve(promptPath);
|
|
2985
|
+
rmSync(resolvedPromptPath, { force: true });
|
|
2986
|
+
try {
|
|
2987
|
+
rmdirSync(dirname(resolvedPromptPath));
|
|
2988
|
+
} catch {}
|
|
2989
|
+
}
|
|
2990
|
+
function readReexecStdinPrompt(env) {
|
|
2991
|
+
const promptPath = env[GNHF_REEXEC_STDIN_PROMPT_FILE];
|
|
2992
|
+
if (promptPath !== void 0) {
|
|
2993
|
+
delete env[GNHF_REEXEC_STDIN_PROMPT_FILE];
|
|
2994
|
+
try {
|
|
2995
|
+
return readFileSync(promptPath, "utf-8");
|
|
2996
|
+
} finally {
|
|
2997
|
+
cleanupTrustedReexecPromptPath(promptPath);
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
const prompt = env[GNHF_REEXEC_STDIN_PROMPT];
|
|
3001
|
+
if (prompt !== void 0) {
|
|
3002
|
+
delete env[GNHF_REEXEC_STDIN_PROMPT];
|
|
3003
|
+
return prompt;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
2333
3006
|
const program = new Command();
|
|
2334
|
-
program.name("gnhf").description("Before I go to bed, I tell my agents: good night, have fun").version(packageVersion).argument("[prompt]", "The objective for the coding agent").option("--agent <agent>", "Agent to use (claude, codex, rovodev, or opencode)").option("--max-iterations <n>", "Abort after N total iterations", parseNonNegativeInteger).option("--max-tokens <n>", "Abort after N total input+output tokens", parseNonNegativeInteger).option("--mock", "", false).action(async (promptArg, options) => {
|
|
3007
|
+
program.name("gnhf").description("Before I go to bed, I tell my agents: good night, have fun").version(packageVersion).argument("[prompt]", "The objective for the coding agent").option("--agent <agent>", "Agent to use (claude, codex, rovodev, or opencode)").option("--max-iterations <n>", "Abort after N total iterations", parseNonNegativeInteger).option("--max-tokens <n>", "Abort after N total input+output tokens", parseNonNegativeInteger).option("--prevent-sleep <mode>", "Prevent system sleep during the run (\"on\" or \"off\")", parseOnOffBoolean).option("--mock", "", false).action(async (promptArg, options) => {
|
|
2335
3008
|
if (options.mock) {
|
|
2336
3009
|
const mock = new MockOrchestrator();
|
|
2337
3010
|
enterAltScreen();
|
|
@@ -2342,18 +3015,28 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
2342
3015
|
exitAltScreen();
|
|
2343
3016
|
return;
|
|
2344
3017
|
}
|
|
3018
|
+
let initialSleepPrevention = null;
|
|
3019
|
+
if (process$1.env.GNHF_SLEEP_INHIBITED === "1") initialSleepPrevention = await startSleepPrevention(process$1.argv.slice(2));
|
|
2345
3020
|
let prompt = promptArg;
|
|
2346
|
-
|
|
3021
|
+
let promptFromStdin = false;
|
|
2347
3022
|
const agentName = options.agent;
|
|
2348
3023
|
if (agentName !== void 0 && agentName !== "claude" && agentName !== "codex" && agentName !== "rovodev" && agentName !== "opencode") {
|
|
2349
3024
|
console.error(`Unknown agent: ${options.agent}. Use "claude", "codex", "rovodev", or "opencode".`);
|
|
2350
3025
|
process$1.exit(1);
|
|
2351
3026
|
}
|
|
2352
|
-
const config =
|
|
3027
|
+
const config = {
|
|
3028
|
+
...loadConfig(agentName ? { agent: agentName } : {}),
|
|
3029
|
+
...options.preventSleep === void 0 ? {} : { preventSleep: options.preventSleep }
|
|
3030
|
+
};
|
|
2353
3031
|
if (config.agent !== "claude" && config.agent !== "codex" && config.agent !== "rovodev" && config.agent !== "opencode") {
|
|
2354
3032
|
console.error(`Unknown agent: ${config.agent}. Use "claude", "codex", "rovodev", or "opencode".`);
|
|
2355
3033
|
process$1.exit(1);
|
|
2356
3034
|
}
|
|
3035
|
+
if (!prompt && process$1.env.GNHF_SLEEP_INHIBITED === "1") prompt = readReexecStdinPrompt(process$1.env);
|
|
3036
|
+
if (!prompt && !process$1.stdin.isTTY) {
|
|
3037
|
+
prompt = await readStdinText(process$1.stdin);
|
|
3038
|
+
promptFromStdin = true;
|
|
3039
|
+
}
|
|
2357
3040
|
const cwd = process$1.cwd();
|
|
2358
3041
|
const currentBranch = getCurrentBranch(cwd);
|
|
2359
3042
|
const onGnhfBranch = currentBranch.startsWith("gnhf/");
|
|
@@ -2382,27 +3065,67 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
2382
3065
|
}
|
|
2383
3066
|
runInfo = initializeNewBranch(prompt, cwd);
|
|
2384
3067
|
}
|
|
3068
|
+
let sleepPreventionCleanup = null;
|
|
3069
|
+
if (config.preventSleep) {
|
|
3070
|
+
const persistedPrompt = promptFromStdin && prompt !== void 0 ? persistStdinPromptForReexec(prompt) : null;
|
|
3071
|
+
let reexeced = false;
|
|
3072
|
+
try {
|
|
3073
|
+
const sleepPrevention = initialSleepPrevention ?? await startSleepPrevention(process$1.argv.slice(2), { reexecEnv: persistedPrompt ? { [GNHF_REEXEC_STDIN_PROMPT_FILE]: persistedPrompt.path } : void 0 });
|
|
3074
|
+
if (sleepPrevention.type === "reexeced") {
|
|
3075
|
+
reexeced = true;
|
|
3076
|
+
process$1.exit(sleepPrevention.exitCode);
|
|
3077
|
+
}
|
|
3078
|
+
if (sleepPrevention.type === "active") sleepPreventionCleanup = sleepPrevention.cleanup;
|
|
3079
|
+
} finally {
|
|
3080
|
+
if (!reexeced) persistedPrompt?.cleanup();
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
appendDebugLog("run:start", { args: process$1.argv.slice(2) });
|
|
2385
3084
|
const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo), runInfo, prompt, cwd, startIteration, {
|
|
2386
3085
|
maxIterations: options.maxIterations,
|
|
2387
3086
|
maxTokens: options.maxTokens
|
|
2388
3087
|
});
|
|
3088
|
+
let shutdownSignal = null;
|
|
2389
3089
|
enterAltScreen();
|
|
2390
3090
|
const renderer = new Renderer(orchestrator, prompt, config.agent);
|
|
2391
3091
|
renderer.start();
|
|
3092
|
+
const requestShutdown = (signal) => {
|
|
3093
|
+
if (shutdownSignal) return;
|
|
3094
|
+
shutdownSignal = signal;
|
|
3095
|
+
appendDebugLog(`signal:${signal}`);
|
|
3096
|
+
renderer.stop();
|
|
3097
|
+
orchestrator.stop();
|
|
3098
|
+
};
|
|
3099
|
+
const handleSigInt = () => requestShutdown("SIGINT");
|
|
3100
|
+
const handleSigTerm = () => requestShutdown("SIGTERM");
|
|
3101
|
+
process$1.on("SIGINT", handleSigInt);
|
|
3102
|
+
process$1.on("SIGTERM", handleSigTerm);
|
|
2392
3103
|
const orchestratorPromise = orchestrator.start().finally(() => {
|
|
2393
3104
|
renderer.stop();
|
|
2394
3105
|
}).catch((err) => {
|
|
2395
3106
|
exitAltScreen();
|
|
2396
3107
|
die(err instanceof Error ? err.message : String(err));
|
|
2397
3108
|
});
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
3109
|
+
try {
|
|
3110
|
+
if (await renderer.waitUntilExit() === "interrupted" && !shutdownSignal) {
|
|
3111
|
+
shutdownSignal = "SIGINT";
|
|
3112
|
+
appendDebugLog("signal:SIGINT");
|
|
3113
|
+
}
|
|
3114
|
+
exitAltScreen();
|
|
3115
|
+
if (await Promise.race([orchestratorPromise.then(() => "done"), new Promise((resolve) => {
|
|
3116
|
+
setTimeout(() => resolve("timeout"), FORCE_EXIT_TIMEOUT_MS).unref();
|
|
3117
|
+
})]) === "timeout") {
|
|
3118
|
+
appendDebugLog("run:shutdown-timeout", { timeoutMs: FORCE_EXIT_TIMEOUT_MS });
|
|
3119
|
+
console.error(`\n gnhf: shutdown timed out after ${FORCE_EXIT_TIMEOUT_MS / 1e3}s, forcing exit\n`);
|
|
3120
|
+
process$1.exit(getSignalExitCode(shutdownSignal ?? "SIGINT"));
|
|
3121
|
+
}
|
|
3122
|
+
} finally {
|
|
3123
|
+
process$1.off("SIGINT", handleSigInt);
|
|
3124
|
+
process$1.off("SIGTERM", handleSigTerm);
|
|
3125
|
+
await sleepPreventionCleanup?.();
|
|
2405
3126
|
}
|
|
3127
|
+
appendDebugLog("run:complete", { signal: shutdownSignal });
|
|
3128
|
+
if (shutdownSignal) process$1.exit(getSignalExitCode(shutdownSignal));
|
|
2406
3129
|
});
|
|
2407
3130
|
function enterAltScreen() {
|
|
2408
3131
|
process$1.stdout.write("\x1B[?1049h");
|