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.
Files changed (3) hide show
  1. package/README.md +18 -2
  2. package/dist/cli.mjs +247 -42
  3. 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
- function normalizeConfig(config) {
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
- return `# Agent to use by default
44
- agent: ${config.agent}
45
-
46
- # Abort after this many consecutive failures
47
- maxConsecutiveFailures: ${config.maxConsecutiveFailures}
48
-
49
- # Prevent the machine from sleeping during a run
50
- preventSleep: ${config.preventSleep}
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
- child.kill("SIGTERM");
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("claude", [
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("codex", [
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("opencode", [
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 = process.platform !== "win32";
1568
- const child = this.spawnFn("acli", [
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 = shutdownChildProcess(server.child, {
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
- }).finally(() => {
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 on an objective given below.
1890
- This is iteration ${params.n} of an ongoing loop to fully accomplish the objective.
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. Focus on the next smallest logical unit of work that's individually testable and would make incremental progress towards the objective - that's the scope of this iteration.
1896
- 3. If you made code changes, run build/tests/linters/formatters if available to validate your work.
1897
- 4. Do NOT make any git commits. Commits will be handled automatically by the gnhf orchestrator.
1898
- 5. When you are done, respond with a JSON object according to the provided schema.
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 complete your iteration. set to false only if something made it impossible for you to do your work
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 and weren't captured by previous notes
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
- frame.push(renderResumeHintCells(terminalWidth));
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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnhf",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Before I go to bed, I tell my agents: good night, have fun",
5
5
  "type": "module",
6
6
  "bin": {