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.
- package/README.md +5 -4
- package/dist/cli.mjs +21 -10
- 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
|
|
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 │ │
|
|
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
|
|
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
|
|
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:
|
|
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")
|
|
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.
|
|
2997
|
-
const backoffMs = 6e4 * Math.pow(2, this.state.
|
|
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
|
-
|
|
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)
|
|
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
|
}
|