gnhf 0.1.25 → 0.1.26

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 CHANGED
@@ -138,6 +138,7 @@ npm link
138
138
  - **Incremental commits** — each successful iteration is a separate git commit, so you can cherry-pick or revert individual changes
139
139
  - **Failure handling** - all failed iterations are rolled back with `git reset --hard`; agent-reported failures proceed to the next iteration immediately, while hard agent errors use exponential backoff
140
140
  - **Runtime caps** - `--max-iterations` stops before the next iteration begins, `--max-tokens` can abort mid-iteration once reported usage reaches the cap, and `--stop-when` ends the loop after an iteration whose agent output reports the natural-language condition is met; 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
141
+ - **Iteration finalization** - agents are expected to finish validation, stop any background processes they started, and only then emit the final JSON result for the iteration
141
142
  - **Shared memory** — the agent reads `notes.md` (built up from prior iterations) to communicate across iterations
142
143
  - **Local run metadata** — gnhf stores prompt, notes, and resume metadata under `.gnhf/runs/` and ignores it locally, so your branch only contains intentional work
143
144
  - **Resume support** — run `gnhf` while on an existing `gnhf/` branch to pick up where a previous run left off; if you provide a different prompt, gnhf asks whether to update the saved prompt and continue with the existing history, start a new branch, or quit
@@ -243,12 +244,12 @@ Including a snippet of `gnhf.log` is the single most useful thing you can attach
243
244
 
244
245
  `gnhf` supports four agents:
245
246
 
246
- | Agent | Flag | Requirements | Notes |
247
- | ----------- | ------------------ | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
248
- | Claude Code | `--agent claude` | Install Anthropic's `claude` CLI and sign in first. | `gnhf` invokes `claude` directly in non-interactive mode. |
249
- | Codex | `--agent codex` | Install OpenAI's `codex` CLI and sign in first. | `gnhf` invokes `codex exec` directly in non-interactive mode. |
250
- | Rovo Dev | `--agent rovodev` | Install Atlassian's `acli` and authenticate it with Rovo Dev first. | `gnhf` starts a local `acli rovodev serve --disable-session-token <port>` process automatically in the repo workspace. |
251
- | OpenCode | `--agent opencode` | Install `opencode` and configure at least one usable model provider first. | `gnhf` starts a local `opencode serve --hostname 127.0.0.1 --port <port> --print-logs` process automatically, creates a per-run session, and applies a blanket allow rule so tool calls do not block on prompts. |
247
+ | Agent | Flag | Requirements | Notes |
248
+ | ----------- | ------------------ | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
249
+ | Claude Code | `--agent claude` | Install Anthropic's `claude` CLI and sign in first. | `gnhf` invokes `claude` directly in non-interactive mode. After Claude emits a successful structured result, `gnhf` treats that result as final and shuts down any lingering Claude process tree after a short grace period. |
250
+ | Codex | `--agent codex` | Install OpenAI's `codex` CLI and sign in first. | `gnhf` invokes `codex exec` directly in non-interactive mode. |
251
+ | Rovo Dev | `--agent rovodev` | Install Atlassian's `acli` and authenticate it with Rovo Dev first. | `gnhf` starts a local `acli rovodev serve --disable-session-token <port>` process automatically in the repo workspace. |
252
+ | OpenCode | `--agent opencode` | Install `opencode` and configure at least one usable model provider first. | `gnhf` starts a local `opencode serve --hostname 127.0.0.1 --port <port> --print-logs` process automatically, creates a per-run session, and applies a blanket allow rule so tool calls do not block on prompts. |
252
253
 
253
254
  ## Development
254
255
 
package/dist/cli.mjs CHANGED
@@ -1043,6 +1043,7 @@ function setupAbortHandler(signal, child, reject, abortChild = () => {
1043
1043
  }
1044
1044
  //#endregion
1045
1045
  //#region src/core/agents/claude.ts
1046
+ const DEFAULT_FINAL_RESULT_EXIT_GRACE_MS = 15e3;
1046
1047
  function shouldUseWindowsShell$2(bin, platform) {
1047
1048
  if (platform !== "win32") return false;
1048
1049
  if (/\.(cmd|bat)$/i.test(bin)) return true;
@@ -1073,8 +1074,22 @@ function terminateClaudeProcess(child, platform) {
1073
1074
  } catch {}
1074
1075
  return;
1075
1076
  }
1077
+ if (child.pid) try {
1078
+ process.kill(-child.pid, "SIGTERM");
1079
+ return;
1080
+ } catch {}
1076
1081
  child.kill("SIGTERM");
1077
1082
  }
1083
+ async function shutdownClaudeProcess(child, platform) {
1084
+ if (platform === "win32") {
1085
+ terminateClaudeProcess(child, platform);
1086
+ return;
1087
+ }
1088
+ await shutdownChildProcess(child, { detached: true });
1089
+ }
1090
+ function isFinalStructuredResult(event) {
1091
+ return !event.is_error && event.subtype === "success" && !!event.structured_output;
1092
+ }
1078
1093
  function buildClaudeArgs(prompt, schema, extraArgs) {
1079
1094
  const userArgs = extraArgs ?? [];
1080
1095
  const userSpecifiedPermissionMode = userArgs.some((arg) => arg === "--dangerously-skip-permissions" || arg === "--permission-mode" || arg.startsWith("--permission-mode=") || arg === "--permission-prompt-tool" || arg.startsWith("--permission-prompt-tool="));
@@ -1108,12 +1123,14 @@ var ClaudeAgent = class {
1108
1123
  name = "claude";
1109
1124
  bin;
1110
1125
  extraArgs;
1126
+ finalResultGraceMs;
1111
1127
  platform;
1112
1128
  schema;
1113
1129
  constructor(binOrDeps = {}) {
1114
1130
  const deps = typeof binOrDeps === "string" ? { bin: binOrDeps } : binOrDeps;
1115
1131
  this.bin = deps.bin ?? "claude";
1116
1132
  this.extraArgs = deps.extraArgs;
1133
+ this.finalResultGraceMs = deps.finalResultGraceMs ?? DEFAULT_FINAL_RESULT_EXIT_GRACE_MS;
1117
1134
  this.platform = deps.platform ?? process.platform;
1118
1135
  this.schema = deps.schema ?? buildAgentOutputSchema({ includeStopField: false });
1119
1136
  }
@@ -1123,6 +1140,7 @@ var ClaudeAgent = class {
1123
1140
  const logStream = logPath ? createWriteStream(logPath) : null;
1124
1141
  const child = spawn(this.bin, buildClaudeArgs(prompt, this.schema, this.extraArgs), {
1125
1142
  cwd,
1143
+ detached: this.platform !== "win32",
1126
1144
  shell: shouldUseWindowsShell$2(this.bin, this.platform),
1127
1145
  stdio: [
1128
1146
  "ignore",
@@ -1133,7 +1151,11 @@ var ClaudeAgent = class {
1133
1151
  });
1134
1152
  if (setupAbortHandler(signal, child, reject, () => terminateClaudeProcess(child, this.platform))) return;
1135
1153
  let resultEvent = null;
1154
+ let finalStructuredResultEvent = null;
1136
1155
  let latestResultUsage = null;
1156
+ let finalResultCleanupTimer = null;
1157
+ let closedAfterFinalCleanup = false;
1158
+ let stderr = "";
1137
1159
  const cumulative = {
1138
1160
  inputTokens: 0,
1139
1161
  outputTokens: 0,
@@ -1145,6 +1167,12 @@ var ClaudeAgent = class {
1145
1167
  let lastAnonymousAssistantId = null;
1146
1168
  let lastAnonymousAssistantUsage = null;
1147
1169
  let pendingAnonymousAssistantUsage = null;
1170
+ child.stderr.on("data", (data) => {
1171
+ stderr += data.toString();
1172
+ });
1173
+ child.on("error", (err) => {
1174
+ reject(/* @__PURE__ */ new Error(`Failed to spawn claude: ${err.message}`));
1175
+ });
1148
1176
  parseJSONLStream(child.stdout, logStream, (event) => {
1149
1177
  if (event.type === "assistant") {
1150
1178
  const msg = event.message;
@@ -1205,24 +1233,38 @@ var ClaudeAgent = class {
1205
1233
  if (event.type === "result") {
1206
1234
  const next = event;
1207
1235
  latestResultUsage = next.usage;
1208
- if (next.is_error || next.subtype !== "success" || next.structured_output || !resultEvent) resultEvent = next;
1236
+ if (isFinalStructuredResult(next)) {
1237
+ finalStructuredResultEvent = next;
1238
+ if (finalResultCleanupTimer) clearTimeout(finalResultCleanupTimer);
1239
+ finalResultCleanupTimer = setTimeout(() => {
1240
+ closedAfterFinalCleanup = true;
1241
+ shutdownClaudeProcess(child, this.platform);
1242
+ }, this.finalResultGraceMs);
1243
+ } else if (!finalStructuredResultEvent && (next.is_error || next.subtype !== "success" || next.structured_output || !resultEvent)) resultEvent = next;
1209
1244
  }
1210
1245
  });
1211
- setupChildProcessHandlers(child, "claude", logStream, reject, () => {
1212
- if (!resultEvent) {
1246
+ child.on("close", (code) => {
1247
+ if (finalResultCleanupTimer) clearTimeout(finalResultCleanupTimer);
1248
+ logStream?.end();
1249
+ if (code !== 0 && !closedAfterFinalCleanup) {
1250
+ reject(/* @__PURE__ */ new Error(`claude exited with code ${code}: ${stderr}`));
1251
+ return;
1252
+ }
1253
+ const terminalResultEvent = finalStructuredResultEvent ?? resultEvent;
1254
+ if (!terminalResultEvent) {
1213
1255
  reject(/* @__PURE__ */ new Error("claude returned no result event"));
1214
1256
  return;
1215
1257
  }
1216
- if (resultEvent.is_error || resultEvent.subtype !== "success") {
1217
- reject(/* @__PURE__ */ new Error(`claude reported error: ${JSON.stringify(resultEvent)}`));
1258
+ if (terminalResultEvent.is_error || terminalResultEvent.subtype !== "success") {
1259
+ reject(/* @__PURE__ */ new Error(`claude reported error: ${JSON.stringify(terminalResultEvent)}`));
1218
1260
  return;
1219
1261
  }
1220
- if (!resultEvent.structured_output) {
1262
+ if (!terminalResultEvent.structured_output) {
1221
1263
  reject(/* @__PURE__ */ new Error("claude returned no structured_output"));
1222
1264
  return;
1223
1265
  }
1224
- const output = resultEvent.structured_output;
1225
- const usage = toTokenUsage(latestResultUsage ?? resultEvent.usage);
1266
+ const output = terminalResultEvent.structured_output;
1267
+ const usage = toTokenUsage(latestResultUsage ?? terminalResultEvent.usage);
1226
1268
  onUsage?.(usage);
1227
1269
  resolve({
1228
1270
  output,
@@ -2818,7 +2860,8 @@ This is iteration ${params.n}. Each iteration aims to make an incremental step f
2818
2860
  2. Identify the next smallest logical unit of work that's individually verifiable and would make incremental progress towards the objective, and treat that as the scope of this iteration
2819
2861
  3. If you attempted a solution and it didn't end up moving the needle on the objective, document learnings and record success=false, then conclude the iteration rather than continuously pivoting
2820
2862
  4. If you made code changes, run build/tests/linters/formatters if available to validate your work. Do NOT make any git commits - that will be handled automatically by the gnhf orchestrator
2821
- 6. Finally, respond with a JSON object according to the provided schema
2863
+ 5. If you started any long-running background processes (dev servers, browsers, watchers, Electron, etc.), stop them before finishing the iteration
2864
+ 6. Only submit the final JSON object after the result is final: your work is complete, validation is done, and you have stopped any background processes you started
2822
2865
 
2823
2866
  ## Output
2824
2867
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnhf",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "Before I go to bed, I tell my agents: good night, have fun",
5
5
  "type": "module",
6
6
  "bin": {