gnhf 0.1.10 → 0.1.12
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 +18 -2
- package/dist/cli.mjs +247 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,7 +47,6 @@ You wake up to a branch full of clean work and a log of everything that happened
|
|
|
47
47
|
- **Dead simple** — one command starts an autonomous loop that runs until you Ctrl+C or a configured runtime cap is reached
|
|
48
48
|
- **Long running** — each iteration is committed on success, rolled back on failure, with sensible retries and exponential backoff
|
|
49
49
|
- **Agent-agnostic** — works with Claude Code, Codex, Rovo Dev, or OpenCode out of the box
|
|
50
|
-
- **Terminal-safe rendering** — the live UI keeps wide Unicode text such as emoji and CJK glyphs aligned instead of clipping or shifting the frame
|
|
51
50
|
|
|
52
51
|
## Quick Start
|
|
53
52
|
|
|
@@ -129,7 +128,7 @@ npm link
|
|
|
129
128
|
```
|
|
130
129
|
|
|
131
130
|
- **Incremental commits** — each successful iteration is a separate git commit, so you can cherry-pick or revert individual changes
|
|
132
|
-
- **Runtime caps** — `--max-iterations` stops before the next iteration begins, while `--max-tokens` can abort mid-iteration once reported usage reaches the cap; uncommitted work is rolled back in either case
|
|
131
|
+
- **Runtime caps** — `--max-iterations` stops before the next iteration begins, while `--max-tokens` can abort mid-iteration once reported usage reaches the cap; uncommitted work is rolled back in either case, and in the interactive TUI the final state remains visible until you press Ctrl+C to exit
|
|
133
132
|
- **Shared memory** — the agent reads `notes.md` (built up from prior iterations) to communicate across iterations
|
|
134
133
|
- **Local run metadata** — gnhf stores prompt, notes, and resume metadata under `.gnhf/runs/` and ignores it locally, so your branch only contains intentional work
|
|
135
134
|
- **Resume support** — run `gnhf` while on an existing `gnhf/` branch to pick up where a previous run left off
|
|
@@ -161,6 +160,11 @@ Config lives at `~/.gnhf/config.yml`:
|
|
|
161
160
|
# Agent to use by default (claude, codex, rovodev, or opencode)
|
|
162
161
|
agent: claude
|
|
163
162
|
|
|
163
|
+
# Custom paths to agent binaries (optional)
|
|
164
|
+
# agentPathOverride:
|
|
165
|
+
# claude: /path/to/custom-claude
|
|
166
|
+
# codex: /path/to/custom-codex
|
|
167
|
+
|
|
164
168
|
# Abort after this many consecutive failures
|
|
165
169
|
maxConsecutiveFailures: 3
|
|
166
170
|
|
|
@@ -172,6 +176,18 @@ If the file does not exist yet, `gnhf` creates it on first run using the resolve
|
|
|
172
176
|
|
|
173
177
|
CLI flags override config file values. `--prevent-sleep` accepts `on`/`off` as well as `true`/`false`; the config file always uses a boolean.
|
|
174
178
|
The iteration and token caps are runtime-only flags and are not persisted in `config.yml`.
|
|
179
|
+
|
|
180
|
+
### Custom Agent Paths
|
|
181
|
+
|
|
182
|
+
Use `agentPathOverride` to point any agent at a custom binary — useful for wrappers like Claude Code Switch or custom Codex builds that accept the same flags and arguments as the original:
|
|
183
|
+
|
|
184
|
+
```yaml
|
|
185
|
+
agentPathOverride:
|
|
186
|
+
claude: ~/bin/claude-code-switch
|
|
187
|
+
codex: /usr/local/bin/my-codex-wrapper
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Paths may be absolute, bare executable names already on your `PATH`, `~`-prefixed, or relative to the config directory (`~/.gnhf/`). The override replaces only the binary name; all standard arguments are preserved, so the replacement must be CLI-compatible with the original agent. On Windows, `.cmd` and `.bat` wrappers are supported, including bare names resolved from `PATH`. For `rovodev`, the override must point to an `acli`-compatible binary since gnhf invokes it as `<bin> rovodev serve ...`.
|
|
175
191
|
When sleep prevention is enabled, `gnhf` uses the native mechanism for your OS: `caffeinate` on macOS, `systemd-inhibit` on Linux, and a small PowerShell helper backed by `SetThreadExecutionState` on Windows.
|
|
176
192
|
|
|
177
193
|
## Debug Logs
|
package/dist/cli.mjs
CHANGED
|
@@ -11,8 +11,15 @@ import { createServer } from "node:net";
|
|
|
11
11
|
import { EventEmitter } from "node:events";
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
13
|
//#region src/core/config.ts
|
|
14
|
+
const AGENT_NAMES = [
|
|
15
|
+
"claude",
|
|
16
|
+
"codex",
|
|
17
|
+
"rovodev",
|
|
18
|
+
"opencode"
|
|
19
|
+
];
|
|
14
20
|
const DEFAULT_CONFIG = {
|
|
15
21
|
agent: "claude",
|
|
22
|
+
agentPathOverride: {},
|
|
16
23
|
maxConsecutiveFailures: 3,
|
|
17
24
|
preventSleep: true
|
|
18
25
|
};
|
|
@@ -25,7 +32,35 @@ function normalizePreventSleep(value) {
|
|
|
25
32
|
if (value === "on") return true;
|
|
26
33
|
if (value === "off") return false;
|
|
27
34
|
}
|
|
28
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a user-supplied path against the config directory (~/.gnhf).
|
|
37
|
+
* Expands leading `~` or `~/` to the home directory, then resolves relative
|
|
38
|
+
* paths against `baseDir` so that entries like `./bin/codex` work predictably
|
|
39
|
+
* regardless of the repo's cwd. Bare executable names and absolute paths pass
|
|
40
|
+
* through unchanged.
|
|
41
|
+
*/
|
|
42
|
+
function resolveConfigPath(raw, baseDir) {
|
|
43
|
+
if (raw !== "~" && !raw.startsWith("~/") && !raw.startsWith("~\\") && !raw.includes("/") && !raw.includes("\\")) return raw;
|
|
44
|
+
const home = homedir();
|
|
45
|
+
let expanded = raw;
|
|
46
|
+
if (expanded === "~") expanded = home;
|
|
47
|
+
else if (expanded.startsWith("~/") || expanded.startsWith("~\\")) expanded = join(home, expanded.slice(2));
|
|
48
|
+
return resolve(baseDir, expanded);
|
|
49
|
+
}
|
|
50
|
+
function normalizeAgentPathOverride(value, configDir) {
|
|
51
|
+
if (value === void 0 || value === null) return void 0;
|
|
52
|
+
if (typeof value !== "object" || Array.isArray(value)) throw new InvalidConfigError(`Invalid config value for agentPathOverride: expected an object mapping agent names to paths`);
|
|
53
|
+
const validNames = new Set(AGENT_NAMES);
|
|
54
|
+
const result = {};
|
|
55
|
+
for (const [key, val] of Object.entries(value)) {
|
|
56
|
+
if (!validNames.has(key)) throw new InvalidConfigError(`Invalid agent name in agentPathOverride: "${key}". Use "claude", "codex", "rovodev", or "opencode".`);
|
|
57
|
+
if (typeof val !== "string") throw new InvalidConfigError(`Invalid path for agentPathOverride.${key}: expected a string`);
|
|
58
|
+
if (val.trim() === "") throw new InvalidConfigError(`Invalid path for agentPathOverride.${key}: expected a non-empty string`);
|
|
59
|
+
result[key] = resolveConfigPath(val, configDir);
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
function normalizeConfig(config, configDir) {
|
|
29
64
|
const normalized = { ...config };
|
|
30
65
|
const hasPreventSleep = Object.prototype.hasOwnProperty.call(config, "preventSleep");
|
|
31
66
|
const preventSleep = normalizePreventSleep(config.preventSleep);
|
|
@@ -33,22 +68,47 @@ function normalizeConfig(config) {
|
|
|
33
68
|
if (hasPreventSleep && config.preventSleep !== void 0) throw new InvalidConfigError(`Invalid config value for preventSleep: ${String(config.preventSleep)}`);
|
|
34
69
|
delete normalized.preventSleep;
|
|
35
70
|
} else normalized.preventSleep = preventSleep;
|
|
71
|
+
if (Object.prototype.hasOwnProperty.call(config, "agentPathOverride")) {
|
|
72
|
+
const resolveDir = configDir ?? join(homedir(), ".gnhf");
|
|
73
|
+
const agentPathOverride = normalizeAgentPathOverride(config.agentPathOverride, resolveDir);
|
|
74
|
+
if (agentPathOverride === void 0) delete normalized.agentPathOverride;
|
|
75
|
+
else normalized.agentPathOverride = agentPathOverride;
|
|
76
|
+
} else delete normalized.agentPathOverride;
|
|
36
77
|
return normalized;
|
|
37
78
|
}
|
|
38
79
|
function isMissingConfigError(error) {
|
|
39
80
|
if (!(error instanceof Error)) return false;
|
|
40
81
|
return "code" in error ? error.code === "ENOENT" : error.message.includes("ENOENT");
|
|
41
82
|
}
|
|
83
|
+
function serializeAgentPathOverride(agentPathOverride) {
|
|
84
|
+
const serializedOverrides = Object.fromEntries(AGENT_NAMES.flatMap((name) => {
|
|
85
|
+
const value = agentPathOverride[name];
|
|
86
|
+
return value === void 0 ? [] : [[name, value]];
|
|
87
|
+
}));
|
|
88
|
+
if (Object.keys(serializedOverrides).length === 0) return "";
|
|
89
|
+
return yaml.dump({ agentPathOverride: serializedOverrides }, {
|
|
90
|
+
lineWidth: -1,
|
|
91
|
+
noRefs: true,
|
|
92
|
+
sortKeys: false
|
|
93
|
+
}).trimEnd();
|
|
94
|
+
}
|
|
42
95
|
function serializeConfig(config) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
96
|
+
const agentPathOverrideSection = serializeAgentPathOverride(config.agentPathOverride);
|
|
97
|
+
const lines = [
|
|
98
|
+
"# Agent to use by default",
|
|
99
|
+
`agent: ${config.agent}`,
|
|
100
|
+
"",
|
|
101
|
+
"# Custom paths to agent binaries (optional)",
|
|
102
|
+
"# Paths may be absolute, bare executable names on PATH,",
|
|
103
|
+
"# ~-prefixed, or relative to this config directory.",
|
|
104
|
+
"# Note: rovodev overrides must point to an acli-compatible binary.",
|
|
105
|
+
"# agentPathOverride:",
|
|
106
|
+
"# claude: /path/to/custom-claude",
|
|
107
|
+
"# codex: /path/to/custom-codex"
|
|
108
|
+
];
|
|
109
|
+
if (agentPathOverrideSection) lines.push(...agentPathOverrideSection.split("\n"));
|
|
110
|
+
lines.push("", "# Abort after this many consecutive failures", `maxConsecutiveFailures: ${config.maxConsecutiveFailures}`, "", "# Prevent the machine from sleeping during a run", `preventSleep: ${config.preventSleep}`, "");
|
|
111
|
+
return lines.join("\n");
|
|
52
112
|
}
|
|
53
113
|
function loadConfig(overrides) {
|
|
54
114
|
const configDir = join(homedir(), ".gnhf");
|
|
@@ -57,7 +117,7 @@ function loadConfig(overrides) {
|
|
|
57
117
|
let shouldBootstrapConfig = false;
|
|
58
118
|
try {
|
|
59
119
|
const raw = readFileSync(configPath, "utf-8");
|
|
60
|
-
fileConfig = normalizeConfig(yaml.load(raw) ?? {});
|
|
120
|
+
fileConfig = normalizeConfig(yaml.load(raw) ?? {}, configDir);
|
|
61
121
|
} catch (error) {
|
|
62
122
|
if (error instanceof InvalidConfigError) throw error;
|
|
63
123
|
if (isMissingConfigError(error)) shouldBootstrapConfig = true;
|
|
@@ -745,10 +805,12 @@ function parseJSONLStream(stream, logStream, callback) {
|
|
|
745
805
|
* Wire an AbortSignal to kill a child process.
|
|
746
806
|
* Returns true if the signal was already aborted (caller should return early).
|
|
747
807
|
*/
|
|
748
|
-
function setupAbortHandler(signal, child, reject) {
|
|
808
|
+
function setupAbortHandler(signal, child, reject, abortChild = () => {
|
|
809
|
+
child.kill("SIGTERM");
|
|
810
|
+
}) {
|
|
749
811
|
if (!signal) return false;
|
|
750
812
|
const onAbort = () => {
|
|
751
|
-
|
|
813
|
+
abortChild();
|
|
752
814
|
reject(/* @__PURE__ */ new Error("Agent was aborted"));
|
|
753
815
|
};
|
|
754
816
|
if (signal.aborted) {
|
|
@@ -761,13 +823,52 @@ function setupAbortHandler(signal, child, reject) {
|
|
|
761
823
|
}
|
|
762
824
|
//#endregion
|
|
763
825
|
//#region src/core/agents/claude.ts
|
|
826
|
+
function shouldUseWindowsShell$2(bin, platform) {
|
|
827
|
+
if (platform !== "win32") return false;
|
|
828
|
+
if (/\.(cmd|bat)$/i.test(bin)) return true;
|
|
829
|
+
if (/[\\/]/.test(bin)) return false;
|
|
830
|
+
try {
|
|
831
|
+
const firstMatch = execFileSync("where", [bin], {
|
|
832
|
+
encoding: "utf8",
|
|
833
|
+
stdio: [
|
|
834
|
+
"ignore",
|
|
835
|
+
"pipe",
|
|
836
|
+
"ignore"
|
|
837
|
+
]
|
|
838
|
+
}).split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
839
|
+
return firstMatch ? /\.(cmd|bat)$/i.test(firstMatch) : false;
|
|
840
|
+
} catch {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function terminateClaudeProcess(child, platform) {
|
|
845
|
+
if (platform === "win32" && child.pid) {
|
|
846
|
+
try {
|
|
847
|
+
execFileSync("taskkill", [
|
|
848
|
+
"/T",
|
|
849
|
+
"/F",
|
|
850
|
+
"/PID",
|
|
851
|
+
String(child.pid)
|
|
852
|
+
], { stdio: "ignore" });
|
|
853
|
+
} catch {}
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
child.kill("SIGTERM");
|
|
857
|
+
}
|
|
764
858
|
var ClaudeAgent = class {
|
|
765
859
|
name = "claude";
|
|
860
|
+
bin;
|
|
861
|
+
platform;
|
|
862
|
+
constructor(binOrDeps = {}) {
|
|
863
|
+
const deps = typeof binOrDeps === "string" ? { bin: binOrDeps } : binOrDeps;
|
|
864
|
+
this.bin = deps.bin ?? "claude";
|
|
865
|
+
this.platform = deps.platform ?? process.platform;
|
|
866
|
+
}
|
|
766
867
|
run(prompt, cwd, options) {
|
|
767
868
|
const { onUsage, onMessage, signal, logPath } = options ?? {};
|
|
768
869
|
return new Promise((resolve, reject) => {
|
|
769
870
|
const logStream = logPath ? createWriteStream(logPath) : null;
|
|
770
|
-
const child = spawn(
|
|
871
|
+
const child = spawn(this.bin, [
|
|
771
872
|
"-p",
|
|
772
873
|
prompt,
|
|
773
874
|
"--verbose",
|
|
@@ -778,6 +879,7 @@ var ClaudeAgent = class {
|
|
|
778
879
|
"--dangerously-skip-permissions"
|
|
779
880
|
], {
|
|
780
881
|
cwd,
|
|
882
|
+
shell: shouldUseWindowsShell$2(this.bin, this.platform),
|
|
781
883
|
stdio: [
|
|
782
884
|
"ignore",
|
|
783
885
|
"pipe",
|
|
@@ -785,7 +887,7 @@ var ClaudeAgent = class {
|
|
|
785
887
|
],
|
|
786
888
|
env: process.env
|
|
787
889
|
});
|
|
788
|
-
if (setupAbortHandler(signal, child, reject)) return;
|
|
890
|
+
if (setupAbortHandler(signal, child, reject, () => terminateClaudeProcess(child, this.platform))) return;
|
|
789
891
|
let resultEvent = null;
|
|
790
892
|
const cumulative = {
|
|
791
893
|
inputTokens: 0,
|
|
@@ -841,17 +943,54 @@ var ClaudeAgent = class {
|
|
|
841
943
|
};
|
|
842
944
|
//#endregion
|
|
843
945
|
//#region src/core/agents/codex.ts
|
|
946
|
+
function shouldUseWindowsShell$1(bin, platform) {
|
|
947
|
+
if (platform !== "win32") return false;
|
|
948
|
+
if (/\.(cmd|bat)$/i.test(bin)) return true;
|
|
949
|
+
if (/[\\/]/.test(bin)) return false;
|
|
950
|
+
try {
|
|
951
|
+
const firstMatch = execFileSync("where", [bin], {
|
|
952
|
+
encoding: "utf8",
|
|
953
|
+
stdio: [
|
|
954
|
+
"ignore",
|
|
955
|
+
"pipe",
|
|
956
|
+
"ignore"
|
|
957
|
+
]
|
|
958
|
+
}).split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
959
|
+
return firstMatch ? /\.(cmd|bat)$/i.test(firstMatch) : false;
|
|
960
|
+
} catch {
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function terminateCodexProcess(child, platform) {
|
|
965
|
+
if (platform === "win32" && child.pid) {
|
|
966
|
+
try {
|
|
967
|
+
execFileSync("taskkill", [
|
|
968
|
+
"/T",
|
|
969
|
+
"/F",
|
|
970
|
+
"/PID",
|
|
971
|
+
String(child.pid)
|
|
972
|
+
], { stdio: "ignore" });
|
|
973
|
+
} catch {}
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
child.kill("SIGTERM");
|
|
977
|
+
}
|
|
844
978
|
var CodexAgent = class {
|
|
845
979
|
name = "codex";
|
|
980
|
+
bin;
|
|
981
|
+
platform;
|
|
846
982
|
schemaPath;
|
|
847
|
-
constructor(schemaPath) {
|
|
983
|
+
constructor(schemaPath, binOrDeps = {}) {
|
|
984
|
+
const deps = typeof binOrDeps === "string" ? { bin: binOrDeps } : binOrDeps;
|
|
985
|
+
this.bin = deps.bin ?? "codex";
|
|
986
|
+
this.platform = deps.platform ?? process.platform;
|
|
848
987
|
this.schemaPath = schemaPath;
|
|
849
988
|
}
|
|
850
989
|
run(prompt, cwd, options) {
|
|
851
990
|
const { onUsage, onMessage, signal, logPath } = options ?? {};
|
|
852
991
|
return new Promise((resolve, reject) => {
|
|
853
992
|
const logStream = logPath ? createWriteStream(logPath) : null;
|
|
854
|
-
const child = spawn(
|
|
993
|
+
const child = spawn(this.bin, [
|
|
855
994
|
"exec",
|
|
856
995
|
prompt,
|
|
857
996
|
"--json",
|
|
@@ -862,6 +1001,7 @@ var CodexAgent = class {
|
|
|
862
1001
|
"never"
|
|
863
1002
|
], {
|
|
864
1003
|
cwd,
|
|
1004
|
+
shell: shouldUseWindowsShell$1(this.bin, this.platform),
|
|
865
1005
|
stdio: [
|
|
866
1006
|
"ignore",
|
|
867
1007
|
"pipe",
|
|
@@ -869,7 +1009,7 @@ var CodexAgent = class {
|
|
|
869
1009
|
],
|
|
870
1010
|
env: process.env
|
|
871
1011
|
});
|
|
872
|
-
if (setupAbortHandler(signal, child, reject)) return;
|
|
1012
|
+
if (setupAbortHandler(signal, child, reject, () => terminateCodexProcess(child, this.platform))) return;
|
|
873
1013
|
let lastAgentMessage = null;
|
|
874
1014
|
const cumulative = {
|
|
875
1015
|
inputTokens: 0,
|
|
@@ -1018,6 +1158,7 @@ function withTimeoutSignal$1(signal, timeoutMs) {
|
|
|
1018
1158
|
}
|
|
1019
1159
|
var OpenCodeAgent = class {
|
|
1020
1160
|
name = "opencode";
|
|
1161
|
+
bin;
|
|
1021
1162
|
fetchFn;
|
|
1022
1163
|
getPortFn;
|
|
1023
1164
|
killProcessFn;
|
|
@@ -1026,6 +1167,7 @@ var OpenCodeAgent = class {
|
|
|
1026
1167
|
server = null;
|
|
1027
1168
|
closingPromise = null;
|
|
1028
1169
|
constructor(deps = {}) {
|
|
1170
|
+
this.bin = deps.bin ?? "opencode";
|
|
1029
1171
|
this.fetchFn = deps.fetch ?? fetch;
|
|
1030
1172
|
this.getPortFn = deps.getPort ?? getAvailablePort$1;
|
|
1031
1173
|
this.killProcessFn = deps.killProcess ?? process.kill.bind(process);
|
|
@@ -1077,7 +1219,7 @@ var OpenCodeAgent = class {
|
|
|
1077
1219
|
const port = await this.getPortFn();
|
|
1078
1220
|
const isWindows = this.platform === "win32";
|
|
1079
1221
|
const detached = !isWindows;
|
|
1080
|
-
const child = this.spawnFn(
|
|
1222
|
+
const child = this.spawnFn(this.bin, [
|
|
1081
1223
|
"serve",
|
|
1082
1224
|
"--hostname",
|
|
1083
1225
|
"127.0.0.1",
|
|
@@ -1463,6 +1605,38 @@ function createAbortError() {
|
|
|
1463
1605
|
function isAbortError(error) {
|
|
1464
1606
|
return error instanceof Error && error.name === "AbortError";
|
|
1465
1607
|
}
|
|
1608
|
+
function shouldUseWindowsShell(bin, platform) {
|
|
1609
|
+
if (platform !== "win32") return false;
|
|
1610
|
+
if (/\.(cmd|bat)$/i.test(bin)) return true;
|
|
1611
|
+
if (/[\\/]/.test(bin)) return false;
|
|
1612
|
+
try {
|
|
1613
|
+
const firstMatch = execFileSync("where", [bin], {
|
|
1614
|
+
encoding: "utf8",
|
|
1615
|
+
stdio: [
|
|
1616
|
+
"ignore",
|
|
1617
|
+
"pipe",
|
|
1618
|
+
"ignore"
|
|
1619
|
+
]
|
|
1620
|
+
}).split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
1621
|
+
return firstMatch ? /\.(cmd|bat)$/i.test(firstMatch) : false;
|
|
1622
|
+
} catch {
|
|
1623
|
+
return false;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
function terminateRovoDevProcess(child, platform) {
|
|
1627
|
+
if (platform === "win32" && child.pid) {
|
|
1628
|
+
try {
|
|
1629
|
+
execFileSync("taskkill", [
|
|
1630
|
+
"/T",
|
|
1631
|
+
"/F",
|
|
1632
|
+
"/PID",
|
|
1633
|
+
String(child.pid)
|
|
1634
|
+
], { stdio: "ignore" });
|
|
1635
|
+
} catch {}
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
child.kill("SIGTERM");
|
|
1639
|
+
}
|
|
1466
1640
|
function getAvailablePort() {
|
|
1467
1641
|
return new Promise((resolve, reject) => {
|
|
1468
1642
|
const server = createServer();
|
|
@@ -1509,18 +1683,22 @@ async function delay(ms, signal) {
|
|
|
1509
1683
|
}
|
|
1510
1684
|
var RovoDevAgent = class {
|
|
1511
1685
|
name = "rovodev";
|
|
1686
|
+
bin;
|
|
1512
1687
|
schemaPath;
|
|
1513
1688
|
fetchFn;
|
|
1514
1689
|
getPortFn;
|
|
1515
1690
|
killProcessFn;
|
|
1691
|
+
platform;
|
|
1516
1692
|
spawnFn;
|
|
1517
1693
|
server = null;
|
|
1518
1694
|
closingPromise = null;
|
|
1519
1695
|
constructor(schemaPath, deps = {}) {
|
|
1696
|
+
this.bin = deps.bin ?? "acli";
|
|
1520
1697
|
this.schemaPath = schemaPath;
|
|
1521
1698
|
this.fetchFn = deps.fetch ?? fetch;
|
|
1522
1699
|
this.getPortFn = deps.getPort ?? getAvailablePort;
|
|
1523
1700
|
this.killProcessFn = deps.killProcess ?? process.kill.bind(process);
|
|
1701
|
+
this.platform = deps.platform ?? process.platform;
|
|
1524
1702
|
this.spawnFn = deps.spawn ?? spawn;
|
|
1525
1703
|
}
|
|
1526
1704
|
async run(prompt, cwd, options) {
|
|
@@ -1564,8 +1742,8 @@ var RovoDevAgent = class {
|
|
|
1564
1742
|
}
|
|
1565
1743
|
if (this.server && !this.server.closed) await this.shutdownServer();
|
|
1566
1744
|
const port = await this.getPortFn();
|
|
1567
|
-
const detached =
|
|
1568
|
-
const child = this.spawnFn(
|
|
1745
|
+
const detached = this.platform !== "win32";
|
|
1746
|
+
const child = this.spawnFn(this.bin, [
|
|
1569
1747
|
"rovodev",
|
|
1570
1748
|
"serve",
|
|
1571
1749
|
"--disable-session-token",
|
|
@@ -1573,6 +1751,7 @@ var RovoDevAgent = class {
|
|
|
1573
1751
|
], {
|
|
1574
1752
|
cwd,
|
|
1575
1753
|
detached,
|
|
1754
|
+
shell: shouldUseWindowsShell(this.bin, this.platform),
|
|
1576
1755
|
stdio: [
|
|
1577
1756
|
"ignore",
|
|
1578
1757
|
"pipe",
|
|
@@ -1837,11 +2016,29 @@ var RovoDevAgent = class {
|
|
|
1837
2016
|
cwd: server.cwd,
|
|
1838
2017
|
port: server.port
|
|
1839
2018
|
});
|
|
1840
|
-
this.closingPromise =
|
|
2019
|
+
this.closingPromise = this.platform === "win32" ? new Promise((resolve) => {
|
|
2020
|
+
const handleClose = () => {
|
|
2021
|
+
server.child.off("close", handleClose);
|
|
2022
|
+
resolve();
|
|
2023
|
+
};
|
|
2024
|
+
server.child.on("close", handleClose);
|
|
2025
|
+
try {
|
|
2026
|
+
terminateRovoDevProcess(server.child, this.platform);
|
|
2027
|
+
} catch {
|
|
2028
|
+
server.child.off("close", handleClose);
|
|
2029
|
+
resolve();
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
setTimeout(() => {
|
|
2033
|
+
server.child.off("close", handleClose);
|
|
2034
|
+
resolve();
|
|
2035
|
+
}, 100).unref?.();
|
|
2036
|
+
}) : shutdownChildProcess(server.child, {
|
|
1841
2037
|
detached: server.detached,
|
|
1842
2038
|
killProcess: this.killProcessFn,
|
|
1843
2039
|
timeoutMs: 3e3
|
|
1844
|
-
})
|
|
2040
|
+
});
|
|
2041
|
+
this.closingPromise = this.closingPromise.finally(() => {
|
|
1845
2042
|
if (this.server === server) this.server = null;
|
|
1846
2043
|
this.closingPromise = null;
|
|
1847
2044
|
});
|
|
@@ -1875,34 +2072,34 @@ function withTimeoutSignal(signal, timeoutMs) {
|
|
|
1875
2072
|
}
|
|
1876
2073
|
//#endregion
|
|
1877
2074
|
//#region src/core/agents/factory.ts
|
|
1878
|
-
function createAgent(name, runInfo) {
|
|
2075
|
+
function createAgent(name, runInfo, pathOverride) {
|
|
1879
2076
|
switch (name) {
|
|
1880
|
-
case "claude": return new ClaudeAgent();
|
|
1881
|
-
case "codex": return new CodexAgent(runInfo.schemaPath);
|
|
1882
|
-
case "opencode": return new OpenCodeAgent();
|
|
1883
|
-
case "rovodev": return new RovoDevAgent(runInfo.schemaPath);
|
|
2077
|
+
case "claude": return new ClaudeAgent(pathOverride);
|
|
2078
|
+
case "codex": return new CodexAgent(runInfo.schemaPath, pathOverride);
|
|
2079
|
+
case "opencode": return new OpenCodeAgent({ bin: pathOverride });
|
|
2080
|
+
case "rovodev": return new RovoDevAgent(runInfo.schemaPath, { bin: pathOverride });
|
|
1884
2081
|
}
|
|
1885
2082
|
}
|
|
1886
2083
|
//#endregion
|
|
1887
2084
|
//#region src/templates/iteration-prompt.ts
|
|
1888
2085
|
function buildIterationPrompt(params) {
|
|
1889
|
-
return `You are working autonomously
|
|
1890
|
-
This is iteration ${params.n}
|
|
2086
|
+
return `You are working autonomously towards an objective given below.
|
|
2087
|
+
This is iteration ${params.n}. Each iteration aims to make an incremental step forward, not to complete the entire objective.
|
|
1891
2088
|
|
|
1892
2089
|
## Instructions
|
|
1893
2090
|
|
|
1894
|
-
1. Read .gnhf/runs/${params.runId}/notes.md first to understand what has been done in previous iterations
|
|
1895
|
-
2.
|
|
1896
|
-
3. If you
|
|
1897
|
-
4. Do NOT make any git commits
|
|
1898
|
-
|
|
2091
|
+
1. Read .gnhf/runs/${params.runId}/notes.md first to understand what has been done in previous iterations
|
|
2092
|
+
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
|
|
2093
|
+
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
|
|
2094
|
+
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
|
|
2095
|
+
6. Finally, respond with a JSON object according to the provided schema
|
|
1899
2096
|
|
|
1900
2097
|
## Output
|
|
1901
2098
|
|
|
1902
|
-
- success: whether you were able to
|
|
2099
|
+
- success: whether you were able to make a meaningful contribution that got us closer towards the objective. setting this to false means any code change you made should be discarded
|
|
1903
2100
|
- summary: a concise one-sentence summary of the accomplishment in this iteration
|
|
1904
2101
|
- key_changes_made: an array of descriptions for key changes you made. don't group this by file - group by logical units of work. don't describe activities - describe material outcomes
|
|
1905
|
-
- key_learnings: an array of new learnings that were surprising
|
|
2102
|
+
- key_learnings: an array of new learnings that were surprising, weren't captured by previous notes and would be informative for future iterations
|
|
1906
2103
|
|
|
1907
2104
|
## Objective
|
|
1908
2105
|
|
|
@@ -1923,6 +2120,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
1923
2120
|
activeIterationPromise = null;
|
|
1924
2121
|
activeAbortController = null;
|
|
1925
2122
|
pendingAbortReason = null;
|
|
2123
|
+
loopDone = false;
|
|
1926
2124
|
state = {
|
|
1927
2125
|
status: "running",
|
|
1928
2126
|
currentIteration: 0,
|
|
@@ -1954,6 +2152,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
1954
2152
|
stop() {
|
|
1955
2153
|
this.stopRequested = true;
|
|
1956
2154
|
this.activeAbortController?.abort();
|
|
2155
|
+
if (this.loopDone) {
|
|
2156
|
+
this.emit("stopped");
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
1957
2159
|
if (this.stopPromise) return;
|
|
1958
2160
|
this.stopPromise = (async () => {
|
|
1959
2161
|
if (this.activeIterationPromise) {
|
|
@@ -2037,6 +2239,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2037
2239
|
this.activeIterationPromise = null;
|
|
2038
2240
|
if (this.stopPromise) await this.stopPromise;
|
|
2039
2241
|
else await this.closeAgent();
|
|
2242
|
+
this.loopDone = true;
|
|
2040
2243
|
}
|
|
2041
2244
|
}
|
|
2042
2245
|
async runIteration(prompt) {
|
|
@@ -2561,6 +2764,7 @@ const MOON_PHASE_PERIOD = 1600;
|
|
|
2561
2764
|
const MAX_MSG_LINES = 3;
|
|
2562
2765
|
const MAX_MSG_LINE_LEN = CONTENT_WIDTH;
|
|
2563
2766
|
const RESUME_HINT = "[ctrl+c to stop, gnhf again to resume]";
|
|
2767
|
+
const DONE_HINT = "[ctrl+c to exit]";
|
|
2564
2768
|
function spacedLabel(text) {
|
|
2565
2769
|
return text.split("").join(" ");
|
|
2566
2770
|
}
|
|
@@ -2681,8 +2885,8 @@ function centerLineCells(content, width) {
|
|
|
2681
2885
|
...emptyCells(rightPad)
|
|
2682
2886
|
];
|
|
2683
2887
|
}
|
|
2684
|
-
function renderResumeHintCells(width) {
|
|
2685
|
-
return centerLineCells(textToCells(RESUME_HINT, "dim"), width);
|
|
2888
|
+
function renderResumeHintCells(width, done) {
|
|
2889
|
+
return centerLineCells(textToCells(done ? DONE_HINT : RESUME_HINT, "dim"), width);
|
|
2686
2890
|
}
|
|
2687
2891
|
/**
|
|
2688
2892
|
* Builds the centered content viewport for the renderer.
|
|
@@ -2790,7 +2994,8 @@ function buildFrameCells(prompt, agentName, state, topStars, bottomStars, sideSt
|
|
|
2790
2994
|
]);
|
|
2791
2995
|
}
|
|
2792
2996
|
for (let y = 0; y < bottomHeight; y++) frame.push(renderStarLineCells(bottomStars, terminalWidth, y, now));
|
|
2793
|
-
|
|
2997
|
+
const isDone = state.status === "aborted";
|
|
2998
|
+
frame.push(renderResumeHintCells(terminalWidth, isDone));
|
|
2794
2999
|
frame.push(emptyCells(terminalWidth));
|
|
2795
3000
|
return frame;
|
|
2796
3001
|
}
|
|
@@ -3081,7 +3286,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
3081
3286
|
}
|
|
3082
3287
|
}
|
|
3083
3288
|
appendDebugLog("run:start", { args: process$1.argv.slice(2) });
|
|
3084
|
-
const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo), runInfo, prompt, cwd, startIteration, {
|
|
3289
|
+
const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent]), runInfo, prompt, cwd, startIteration, {
|
|
3085
3290
|
maxIterations: options.maxIterations,
|
|
3086
3291
|
maxTokens: options.maxTokens
|
|
3087
3292
|
});
|
|
@@ -3101,7 +3306,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
3101
3306
|
process$1.on("SIGINT", handleSigInt);
|
|
3102
3307
|
process$1.on("SIGTERM", handleSigTerm);
|
|
3103
3308
|
const orchestratorPromise = orchestrator.start().finally(() => {
|
|
3104
|
-
renderer.stop();
|
|
3309
|
+
if (!(orchestrator.getState().status === "aborted" && process$1.stdin.isTTY)) renderer.stop();
|
|
3105
3310
|
}).catch((err) => {
|
|
3106
3311
|
exitAltScreen();
|
|
3107
3312
|
die(err instanceof Error ? err.message : String(err));
|