gnhf 0.1.24 → 0.1.25

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 +5 -4
  2. package/dist/cli.mjs +21 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -45,7 +45,7 @@ gnhf is a [ralph](https://ghuntley.com/ralph/), [autoresearch](https://github.co
45
45
  You wake up to a branch full of clean work and a log of everything that happened.
46
46
 
47
47
  - **Dead simple** — one command starts an autonomous loop that runs until you Ctrl+C or a configured runtime cap is reached
48
- - **Long running** — each iteration is committed on success, rolled back on failure, with sensible retries and exponential backoff
48
+ - **Long running** — each iteration is committed on success, rolled back on failure, with sensible retries; hard agent errors back off exponentially while agent-reported failures continue immediately
49
49
  - **Live terminal title** — interactive runs keep your terminal title updated with live status, token totals, and commit count, then restore the previous title on exit
50
50
  - **Agent-agnostic** — works with Claude Code, Codex, Rovo Dev, or OpenCode out of the box
51
51
 
@@ -122,7 +122,7 @@ npm link
122
122
  ┌──────────┐ ┌───────────┐ │
123
123
  │ commit │ │ git reset │ │
124
124
  │ append │ │ --hard │ │
125
- │ notes.md │ │ backoff │ │
125
+ │ notes.md │ │ maybe wait│ │
126
126
  └────┬─────┘ └─────┬─────┘ │
127
127
  │ │ │
128
128
  │ ┌──────────┘ │
@@ -136,10 +136,11 @@ npm link
136
136
  ```
137
137
 
138
138
  - **Incremental commits** — each successful iteration is a separate git commit, so you can cherry-pick or revert individual changes
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
139
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
140
141
  - **Shared memory** — the agent reads `notes.md` (built up from prior iterations) to communicate across iterations
141
142
  - **Local run metadata** — gnhf stores prompt, notes, and resume metadata under `.gnhf/runs/` and ignores it locally, so your branch only contains intentional work
142
- - **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 overwrite the saved prompt, start a new branch, or quit
143
+ - **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
143
144
 
144
145
  ### Worktree Mode
145
146
 
@@ -165,7 +166,7 @@ Pass `--worktree` to run each agent in an isolated [git worktree](https://git-sc
165
166
  | `echo "<prompt>" \| gnhf` | Pipe prompt via stdin |
166
167
  | `cat prd.md \| gnhf` | Pipe a large spec or PRD via stdin |
167
168
 
168
- If you run `gnhf` on an existing `gnhf/` branch with a different prompt, gnhf asks whether to overwrite the saved prompt, start a new branch, or quit. When the prompt came from stdin, that confirmation is read from the controlling terminal, so it must be available.
169
+ If you run `gnhf` on an existing `gnhf/` branch with a different prompt, gnhf asks whether to update `prompt.md` and continue the existing run history, start a new branch, or quit. When the prompt came from stdin, that confirmation is read from the controlling terminal, so it must be available.
169
170
 
170
171
  ### Flags
171
172
 
package/dist/cli.mjs CHANGED
@@ -488,7 +488,7 @@ function setupRun(runId, prompt, baseCommit, cwd, schemaOptions) {
488
488
  const promptPath = join(runDir, "prompt.md");
489
489
  writeFileSync(promptPath, prompt, "utf-8");
490
490
  const notesPath = join(runDir, "notes.md");
491
- writeFileSync(notesPath, `# gnhf run: ${runId}\n\nObjective: ${prompt}\n\n## Iteration Log\n`, "utf-8");
491
+ if (!existsSync(notesPath)) writeFileSync(notesPath, `# gnhf run: ${runId}\n\nObjective: see .gnhf/runs/${runId}/prompt.md\n\n## Iteration Log\n`, "utf-8");
492
492
  const schemaPath = join(runDir, "output-schema.json");
493
493
  writeSchemaFile(schemaPath, schemaOptions.includeStopField);
494
494
  const logPath = join(runDir, LOG_FILENAME);
@@ -1133,6 +1133,7 @@ var ClaudeAgent = class {
1133
1133
  });
1134
1134
  if (setupAbortHandler(signal, child, reject, () => terminateClaudeProcess(child, this.platform))) return;
1135
1135
  let resultEvent = null;
1136
+ let latestResultUsage = null;
1136
1137
  const cumulative = {
1137
1138
  inputTokens: 0,
1138
1139
  outputTokens: 0,
@@ -1201,7 +1202,11 @@ var ClaudeAgent = class {
1201
1202
  }
1202
1203
  }
1203
1204
  }
1204
- if (event.type === "result") resultEvent = event;
1205
+ if (event.type === "result") {
1206
+ const next = event;
1207
+ latestResultUsage = next.usage;
1208
+ if (next.is_error || next.subtype !== "success" || next.structured_output || !resultEvent) resultEvent = next;
1209
+ }
1205
1210
  });
1206
1211
  setupChildProcessHandlers(child, "claude", logStream, reject, () => {
1207
1212
  if (!resultEvent) {
@@ -1217,7 +1222,7 @@ var ClaudeAgent = class {
1217
1222
  return;
1218
1223
  }
1219
1224
  const output = resultEvent.structured_output;
1220
- const usage = toTokenUsage(resultEvent.usage);
1225
+ const usage = toTokenUsage(latestResultUsage ?? resultEvent.usage);
1221
1226
  onUsage?.(usage);
1222
1227
  resolve({
1223
1228
  output,
@@ -2849,6 +2854,7 @@ var Orchestrator = class extends EventEmitter {
2849
2854
  successCount: 0,
2850
2855
  failCount: 0,
2851
2856
  consecutiveFailures: 0,
2857
+ consecutiveErrors: 0,
2852
2858
  startTime: /* @__PURE__ */ new Date(),
2853
2859
  waitingUntil: null,
2854
2860
  lastMessage: null
@@ -2993,14 +2999,14 @@ var Orchestrator = class extends EventEmitter {
2993
2999
  this.abort(`${this.config.maxConsecutiveFailures} consecutive failures`);
2994
3000
  break;
2995
3001
  }
2996
- if (this.state.consecutiveFailures > 0 && !this.stopRequested) {
2997
- const backoffMs = 6e4 * Math.pow(2, this.state.consecutiveFailures - 1);
3002
+ if (this.state.consecutiveErrors > 0 && !this.stopRequested) {
3003
+ const backoffMs = 6e4 * Math.pow(2, this.state.consecutiveErrors - 1);
2998
3004
  this.state.status = "waiting";
2999
3005
  this.state.waitingUntil = new Date(Date.now() + backoffMs);
3000
3006
  this.emit("state", this.getState());
3001
3007
  appendDebugLog("backoff:start", {
3002
3008
  iteration: this.state.currentIteration,
3003
- consecutiveFailures: this.state.consecutiveFailures,
3009
+ consecutiveErrors: this.state.consecutiveErrors,
3004
3010
  backoffMs
3005
3011
  });
3006
3012
  await this.interruptibleSleep(backoffMs);
@@ -3088,7 +3094,7 @@ var Orchestrator = class extends EventEmitter {
3088
3094
  };
3089
3095
  return {
3090
3096
  type: "completed",
3091
- record: this.recordFailure(`[FAIL] ${result.output.summary}`, result.output.summary, toStringArray(result.output.key_learnings)),
3097
+ record: this.recordFailure(`[FAIL] ${result.output.summary}`, result.output.summary, toStringArray(result.output.key_learnings), "reported"),
3092
3098
  shouldFullyStop
3093
3099
  };
3094
3100
  } catch (err) {
@@ -3120,7 +3126,7 @@ var Orchestrator = class extends EventEmitter {
3120
3126
  const summary = err instanceof Error ? err.message : String(err);
3121
3127
  return {
3122
3128
  type: "completed",
3123
- record: this.recordFailure(`[ERROR] ${summary}`, summary, []),
3129
+ record: this.recordFailure(`[ERROR] ${summary}`, summary, [], "error"),
3124
3130
  shouldFullyStop: false
3125
3131
  };
3126
3132
  } finally {
@@ -3134,6 +3140,7 @@ var Orchestrator = class extends EventEmitter {
3134
3140
  this.state.commitCount = getBranchCommitCount(this.runInfo.baseCommit, this.cwd);
3135
3141
  this.state.successCount++;
3136
3142
  this.state.consecutiveFailures = 0;
3143
+ this.state.consecutiveErrors = 0;
3137
3144
  return {
3138
3145
  number: this.state.currentIteration,
3139
3146
  success: true,
@@ -3143,11 +3150,13 @@ var Orchestrator = class extends EventEmitter {
3143
3150
  timestamp: /* @__PURE__ */ new Date()
3144
3151
  };
3145
3152
  }
3146
- recordFailure(notesSummary, recordSummary, learnings) {
3153
+ recordFailure(notesSummary, recordSummary, learnings, kind) {
3147
3154
  appendNotes(this.runInfo.notesPath, this.state.currentIteration, notesSummary, [], toStringArray(learnings));
3148
3155
  resetHard(this.cwd);
3149
3156
  this.state.failCount++;
3150
3157
  this.state.consecutiveFailures++;
3158
+ if (kind === "error") this.state.consecutiveErrors++;
3159
+ else this.state.consecutiveErrors = 0;
3151
3160
  return {
3152
3161
  number: this.state.currentIteration,
3153
3162
  success: false,
@@ -3280,6 +3289,7 @@ var MockOrchestrator = class extends EventEmitter {
3280
3289
  successCount: 11,
3281
3290
  failCount: 2,
3282
3291
  consecutiveFailures: 0,
3292
+ consecutiveErrors: 0,
3283
3293
  startTime: new Date(Date.now() - INITIAL_ELAPSED_MS),
3284
3294
  waitingUntil: null,
3285
3295
  lastMessage: AGENT_MESSAGES[0]
@@ -4252,10 +4262,11 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
4252
4262
  runInfo = existing;
4253
4263
  startIteration = getLastIterationNumber(existing);
4254
4264
  } else {
4255
- const answer = await ask(`You are on gnhf branch "${currentBranch}".\n (o) Overwrite current run with new prompt\n (n) Start a new branch on top of this one\n (q) Quit\nChoose [o/n/q]: `, "The overwrite prompt closed before a choice was entered. Re-run gnhf from an interactive terminal and choose o, n, or q.", "Cannot show the overwrite prompt because stdin is not interactive. Re-run gnhf from an interactive terminal and choose o, n, or q.");
4265
+ const answer = await ask(`You are on gnhf branch "${currentBranch}".\n (o) Update prompt and continue current run\n (n) Start a new branch on top of this one\n (q) Quit\nChoose [o/n/q]: `, "The overwrite prompt closed before a choice was entered. Re-run gnhf from an interactive terminal and choose o, n, or q.", "Cannot show the overwrite prompt because stdin is not interactive. Re-run gnhf from an interactive terminal and choose o, n, or q.");
4256
4266
  if (answer === "o") {
4257
4267
  ensureCleanWorkingTree(cwd);
4258
4268
  runInfo = setupRun(existingRunId, prompt, existing.baseCommit, cwd, schemaOptions);
4269
+ startIteration = getLastIterationNumber(existing);
4259
4270
  } else if (answer === "n") runInfo = initializeNewBranch(prompt, cwd, schemaOptions);
4260
4271
  else process$1.exit(0);
4261
4272
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnhf",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "Before I go to bed, I tell my agents: good night, have fun",
5
5
  "type": "module",
6
6
  "bin": {