gnhf 0.1.27 → 0.1.29
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 +15 -13
- package/dist/cli.mjs +42 -9
- 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 request stop or a configured runtime cap is reached
|
|
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
|
|
48
|
+
- **Long running** — each iteration is committed on success, rolled back on failure, with sensible retries; retryable 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, OpenCode, GitHub Copilot CLI, or Pi out of the box
|
|
51
51
|
|
|
@@ -129,14 +129,16 @@ npm link
|
|
|
129
129
|
▼ ▼ │
|
|
130
130
|
┌────────────┐ yes ┌──────────┐ │
|
|
131
131
|
│ 3 consec. ├─────────►│ abort │ │
|
|
132
|
-
│ failures
|
|
132
|
+
│ failures │ └────▲─────┘ │
|
|
133
|
+
│ or perm. ├───────────────┘ │
|
|
134
|
+
│ error? │ │
|
|
133
135
|
└─────┬──────┘ │
|
|
134
136
|
no │ │
|
|
135
137
|
└──────────────────────────────────────┘
|
|
136
138
|
```
|
|
137
139
|
|
|
138
140
|
- **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,
|
|
141
|
+
- **Failure handling** - all failed iterations are rolled back with `git reset --hard`; agent-reported failures proceed to the next iteration immediately, retryable hard agent errors use exponential backoff, and permanent agent errors such as Claude low credit balance abort immediately and print the run log path
|
|
140
142
|
- **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; resumed runs reuse the saved stop condition unless you pass a new value, or `--stop-when ""` to clear it; 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
143
|
- **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
|
|
142
144
|
- **Graceful interrupts** - in the interactive TUI, the first Ctrl+C requests a graceful stop and lets the current iteration finish (or ends backoff early), the second Ctrl+C force-stops immediately, and `SIGTERM` also force-stops immediately
|
|
@@ -172,15 +174,15 @@ If you run `gnhf` on an existing `gnhf/` branch with a different prompt, gnhf as
|
|
|
172
174
|
|
|
173
175
|
### Flags
|
|
174
176
|
|
|
175
|
-
| Flag | Description
|
|
176
|
-
| ------------------------ |
|
|
177
|
+
| Flag | Description | Default |
|
|
178
|
+
| ------------------------ | --------------------------------------------------------------------------- | ---------------------- |
|
|
177
179
|
| `--agent <agent>` | Agent to use (`claude`, `codex`, `rovodev`, `opencode`, `copilot`, or `pi`) | config file (`claude`) |
|
|
178
|
-
| `--max-iterations <n>` | Abort after `n` total iterations
|
|
179
|
-
| `--max-tokens <n>` | Abort after `n` total input+output tokens
|
|
180
|
-
| `--stop-when <cond>` | End the loop when the agent reports this condition; persists across resume
|
|
181
|
-
| `--prevent-sleep <mode>` | Prevent system sleep during the run (`on`/`off` or `true`/`false`)
|
|
182
|
-
| `--worktree` | Run in a separate git worktree (enables multiple agents concurrently)
|
|
183
|
-
| `--version` | Show version
|
|
180
|
+
| `--max-iterations <n>` | Abort after `n` total iterations | unlimited |
|
|
181
|
+
| `--max-tokens <n>` | Abort after `n` total input+output tokens | unlimited |
|
|
182
|
+
| `--stop-when <cond>` | End the loop when the agent reports this condition; persists across resume | unlimited |
|
|
183
|
+
| `--prevent-sleep <mode>` | Prevent system sleep during the run (`on`/`off` or `true`/`false`) | config file (`on`) |
|
|
184
|
+
| `--worktree` | Run in a separate git worktree (enables multiple agents concurrently) | `false` |
|
|
185
|
+
| `--version` | Show version | |
|
|
184
186
|
|
|
185
187
|
## Configuration
|
|
186
188
|
|
|
@@ -263,8 +265,8 @@ Including a snippet of `gnhf.log` is the single most useful thing you can attach
|
|
|
263
265
|
| ------------------ | ------------------ | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
264
266
|
| 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. |
|
|
265
267
|
| Codex | `--agent codex` | Install OpenAI's `codex` CLI and sign in first. | `gnhf` invokes `codex exec` directly in non-interactive mode. |
|
|
266
|
-
| GitHub Copilot CLI | `--agent copilot` | Install GitHub Copilot CLI and sign in first. | `gnhf` invokes `copilot` directly in non-interactive JSONL mode. Copilot currently exposes assistant output tokens, but not full input/cache token totals; see https://github.com/github/copilot-cli/issues/1152.
|
|
267
|
-
| Pi | `--agent pi` | Install the `pi` CLI and configure a usable provider/model first. | `gnhf` invokes `pi` directly in JSON mode, appends the final output schema to the prompt, and disables Pi session persistence with `--no-session`.
|
|
268
|
+
| GitHub Copilot CLI | `--agent copilot` | Install GitHub Copilot CLI and sign in first. | `gnhf` invokes `copilot` directly in non-interactive JSONL mode. Copilot currently exposes assistant output tokens, but not full input/cache token totals; see https://github.com/github/copilot-cli/issues/1152. |
|
|
269
|
+
| Pi | `--agent pi` | Install the `pi` CLI and configure a usable provider/model first. | `gnhf` invokes `pi` directly in JSON mode, appends the final output schema to the prompt, and disables Pi session persistence with `--no-session`. |
|
|
268
270
|
| 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. |
|
|
269
271
|
| 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. |
|
|
270
272
|
|
package/dist/cli.mjs
CHANGED
|
@@ -492,6 +492,14 @@ function buildAgentOutputSchema(opts) {
|
|
|
492
492
|
required
|
|
493
493
|
};
|
|
494
494
|
}
|
|
495
|
+
var PermanentAgentError = class extends Error {
|
|
496
|
+
detail;
|
|
497
|
+
constructor(message, detail) {
|
|
498
|
+
super(message, { cause: detail });
|
|
499
|
+
this.name = "PermanentAgentError";
|
|
500
|
+
this.detail = detail;
|
|
501
|
+
}
|
|
502
|
+
};
|
|
495
503
|
//#endregion
|
|
496
504
|
//#region src/core/run.ts
|
|
497
505
|
const LOG_FILENAME = "gnhf.log";
|
|
@@ -1177,6 +1185,9 @@ function isSameUsage$1(a, b) {
|
|
|
1177
1185
|
function extendsUsage(next, previous) {
|
|
1178
1186
|
return next.inputTokens >= previous.inputTokens && next.outputTokens >= previous.outputTokens && next.cacheReadTokens >= previous.cacheReadTokens && next.cacheCreationTokens >= previous.cacheCreationTokens && !isSameUsage$1(next, previous);
|
|
1179
1187
|
}
|
|
1188
|
+
function isPermanentClaudeError(stderr) {
|
|
1189
|
+
return /credit balance\s+is\s+too\s+low/i.test(stderr);
|
|
1190
|
+
}
|
|
1180
1191
|
var ClaudeAgent = class {
|
|
1181
1192
|
name = "claude";
|
|
1182
1193
|
bin;
|
|
@@ -1305,7 +1316,8 @@ var ClaudeAgent = class {
|
|
|
1305
1316
|
if (finalResultCleanupTimer) clearTimeout(finalResultCleanupTimer);
|
|
1306
1317
|
logStream?.end();
|
|
1307
1318
|
if (code !== 0 && !closedAfterFinalCleanup) {
|
|
1308
|
-
|
|
1319
|
+
const detail = `claude exited with code ${code}: ${stderr}`;
|
|
1320
|
+
reject(isPermanentClaudeError(stderr) ? new PermanentAgentError("claude credit balance too low - see gnhf.log", detail) : new Error(detail));
|
|
1309
1321
|
return;
|
|
1310
1322
|
}
|
|
1311
1323
|
const terminalResultEvent = finalStructuredResultEvent ?? resultEvent;
|
|
@@ -2978,7 +2990,7 @@ var RovoDevAgent = class {
|
|
|
2978
2990
|
return server;
|
|
2979
2991
|
}
|
|
2980
2992
|
async waitForHealthy(server, signal) {
|
|
2981
|
-
const deadline = Date.now() +
|
|
2993
|
+
const deadline = Date.now() + 9e4;
|
|
2982
2994
|
let spawnErrorMessage = null;
|
|
2983
2995
|
server.child.once("error", (error) => {
|
|
2984
2996
|
spawnErrorMessage = error.message;
|
|
@@ -3436,7 +3448,8 @@ var Orchestrator = class extends EventEmitter {
|
|
|
3436
3448
|
consecutiveErrors: 0,
|
|
3437
3449
|
startTime: /* @__PURE__ */ new Date(),
|
|
3438
3450
|
waitingUntil: null,
|
|
3439
|
-
lastMessage: null
|
|
3451
|
+
lastMessage: null,
|
|
3452
|
+
lastAgentError: null
|
|
3440
3453
|
};
|
|
3441
3454
|
constructor(config, agent, runInfo, prompt, cwd, startIteration = 0, limits = {}) {
|
|
3442
3455
|
super();
|
|
@@ -3727,6 +3740,14 @@ var Orchestrator = class extends EventEmitter {
|
|
|
3727
3740
|
elapsedMs,
|
|
3728
3741
|
error: serializeError(err)
|
|
3729
3742
|
});
|
|
3743
|
+
if (err instanceof PermanentAgentError) {
|
|
3744
|
+
resetHard(this.cwd);
|
|
3745
|
+
this.state.lastAgentError = err.detail;
|
|
3746
|
+
return {
|
|
3747
|
+
type: "aborted",
|
|
3748
|
+
reason: err.message
|
|
3749
|
+
};
|
|
3750
|
+
}
|
|
3730
3751
|
const summary = err instanceof Error ? err.message : String(err);
|
|
3731
3752
|
return {
|
|
3732
3753
|
type: "completed",
|
|
@@ -3745,6 +3766,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
3745
3766
|
this.state.successCount++;
|
|
3746
3767
|
this.state.consecutiveFailures = 0;
|
|
3747
3768
|
this.state.consecutiveErrors = 0;
|
|
3769
|
+
this.state.lastAgentError = null;
|
|
3748
3770
|
return {
|
|
3749
3771
|
number: this.state.currentIteration,
|
|
3750
3772
|
success: true,
|
|
@@ -3759,8 +3781,13 @@ var Orchestrator = class extends EventEmitter {
|
|
|
3759
3781
|
resetHard(this.cwd);
|
|
3760
3782
|
this.state.failCount++;
|
|
3761
3783
|
this.state.consecutiveFailures++;
|
|
3762
|
-
if (kind === "error")
|
|
3763
|
-
|
|
3784
|
+
if (kind === "error") {
|
|
3785
|
+
this.state.consecutiveErrors++;
|
|
3786
|
+
this.state.lastAgentError = recordSummary;
|
|
3787
|
+
} else {
|
|
3788
|
+
this.state.consecutiveErrors = 0;
|
|
3789
|
+
this.state.lastAgentError = null;
|
|
3790
|
+
}
|
|
3764
3791
|
return {
|
|
3765
3792
|
number: this.state.currentIteration,
|
|
3766
3793
|
success: false,
|
|
@@ -4334,10 +4361,15 @@ function renderStatsCells(elapsed, inputTokens, outputTokens, commitCount) {
|
|
|
4334
4361
|
...textToCells(formatCommitCount(commitCount), "normal")
|
|
4335
4362
|
];
|
|
4336
4363
|
}
|
|
4337
|
-
function renderAgentMessageCells(message, status) {
|
|
4364
|
+
function renderAgentMessageCells(message, status, lastAgentError) {
|
|
4338
4365
|
const lines = [];
|
|
4339
|
-
if (status === "waiting")
|
|
4340
|
-
|
|
4366
|
+
if (status === "waiting") {
|
|
4367
|
+
lines.push("waiting (backoff)...");
|
|
4368
|
+
if (lastAgentError) lines.push(...wordWrap(lastAgentError, MAX_MSG_LINE_LEN, 2));
|
|
4369
|
+
} else if (status === "aborted" && lastAgentError) {
|
|
4370
|
+
lines.push(...wordWrap(message ?? "max consecutive failures reached", MAX_MSG_LINE_LEN, 1));
|
|
4371
|
+
lines.push(...wordWrap(lastAgentError, MAX_MSG_LINE_LEN, 2));
|
|
4372
|
+
} else if (status === "aborted" && !message) lines.push("max consecutive failures reached");
|
|
4341
4373
|
else if (!message) lines.push("working...");
|
|
4342
4374
|
else {
|
|
4343
4375
|
const wrapped = wordWrap(message, MAX_MSG_LINE_LEN, MAX_MSG_LINES);
|
|
@@ -4460,7 +4492,7 @@ function buildContentCells(prompt, agentName, state, elapsed, now, availableHeig
|
|
|
4460
4492
|
agent: [
|
|
4461
4493
|
[],
|
|
4462
4494
|
[],
|
|
4463
|
-
...renderAgentMessageCells(state.lastMessage, state.status)
|
|
4495
|
+
...renderAgentMessageCells(state.lastMessage, state.status, state.lastAgentError)
|
|
4464
4496
|
],
|
|
4465
4497
|
moon: [
|
|
4466
4498
|
[],
|
|
@@ -5100,6 +5132,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
5100
5132
|
commitCount: finalState.commitCount,
|
|
5101
5133
|
worktreePath
|
|
5102
5134
|
});
|
|
5135
|
+
if (finalState.status === "aborted") console.error(`\n gnhf: Run log: ${runInfo.logPath}\n`);
|
|
5103
5136
|
if (worktreePath) if (finalState.commitCount > 0) {
|
|
5104
5137
|
worktreeCleanup = null;
|
|
5105
5138
|
console.error(`\n gnhf: worktree preserved at ${worktreePath}\n gnhf: merge the branch and remove with: git worktree remove "${worktreePath}"\n`);
|