gnhf 0.1.34 → 0.1.36
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 +27 -9
- package/dist/cli.mjs +198 -28
- 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; retryable 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 except commit failures preserved for repair, 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 clear or restore it on exit depending on terminal support; token totals prefixed with `~` are estimates
|
|
50
50
|
- **Exit summary**: every run ends with a permanent summary covering elapsed time, branch, iterations, tokens, branch diff stats, local notes/log paths, and review commands
|
|
51
51
|
- **Agent-agnostic**: works with Claude Code, Codex, Rovo Dev, OpenCode, GitHub Copilot CLI, Pi, or ACP targets out of the box
|
|
@@ -71,6 +71,11 @@ $ gnhf --worktree "add tests for module Y" &
|
|
|
71
71
|
$ gnhf --worktree "refactor the API layer" &
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
```sh
|
|
75
|
+
# Commit directly on the current branch and push after each successful iteration
|
|
76
|
+
$ gnhf --current-branch --push "keep improving this app"
|
|
77
|
+
```
|
|
78
|
+
|
|
74
79
|
Run `gnhf` from inside a Git repository with a clean working tree. If you are starting from a plain directory, run `git init` first.
|
|
75
80
|
`gnhf` supports macOS, Linux, and Windows.
|
|
76
81
|
|
|
@@ -101,7 +106,7 @@ npm link
|
|
|
101
106
|
▼
|
|
102
107
|
┌──────────────────────┐
|
|
103
108
|
│ validate clean git │
|
|
104
|
-
│ create
|
|
109
|
+
│ create or use branch │
|
|
105
110
|
│ write prompt.md │
|
|
106
111
|
└──────────┬───────────┘
|
|
107
112
|
▼
|
|
@@ -121,8 +126,8 @@ npm link
|
|
|
121
126
|
yes │ │ no │
|
|
122
127
|
▼ ▼ │
|
|
123
128
|
┌──────────┐ ┌───────────┐ │
|
|
124
|
-
│ commit │ │
|
|
125
|
-
│ append │ │
|
|
129
|
+
│ commit │ │ reset or │ │
|
|
130
|
+
│ append │ │ repair │ │
|
|
126
131
|
│ notes.md │ │ maybe wait│ │
|
|
127
132
|
└────┬─────┘ └─────┬─────┘ │
|
|
128
133
|
│ │ │
|
|
@@ -138,9 +143,9 @@ npm link
|
|
|
138
143
|
└──────────────────────────────────────┘
|
|
139
144
|
```
|
|
140
145
|
|
|
141
|
-
- **Incremental commits** - each successful iteration is a separate unsigned git commit, so you can cherry-pick or revert individual changes without GPG or SSH signing prompts blocking the run
|
|
142
|
-
- **Failure handling** -
|
|
143
|
-
- **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
|
|
146
|
+
- **Incremental commits** - each successful iteration is a separate unsigned git commit, so you can cherry-pick or revert individual changes without GPG or SSH signing prompts blocking the run; if `git commit` fails, gnhf preserves the uncommitted work and asks the next agent iteration to repair it
|
|
147
|
+
- **Failure handling** - failed iterations are rolled back with `git reset --hard` except commit failures, which preserve uncommitted work for repair; 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. Complete no-op iterations are reported as failures and count toward the consecutive-failure abort limit. If the run exits with a pending commit failure, the exit summary warns that uncommitted changes were left for repair.
|
|
148
|
+
- **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 unless a commit failure needs repair first; resumed runs reuse the saved stop condition unless you pass a new value, or `--stop-when ""` to clear it; pending commit-failure repair work is preserved and other uncommitted work is rolled back, and in the interactive TUI the final state remains visible until you press Ctrl+C to exit
|
|
144
149
|
- **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
|
|
145
150
|
- **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
|
|
146
151
|
- **Exit summary** - after shutdown cleanup, gnhf prints a permanent stdout summary with the final branch, elapsed time, iteration and token totals, branch diff stats, notes/debug-log paths, and review commands
|
|
@@ -148,6 +153,17 @@ npm link
|
|
|
148
153
|
- **Local run metadata** — gnhf stores prompt, notes, stop conditions, and commit-message convention metadata under `.gnhf/runs/` and ignores it locally, so your branch only contains intentional work
|
|
149
154
|
- **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. New runs whose generated branch already exists use a numeric suffix such as `gnhf/<slug>-1`.
|
|
150
155
|
|
|
156
|
+
### Live Branch Mode
|
|
157
|
+
|
|
158
|
+
Pass `--current-branch` to run on the branch you are already on instead of creating a `gnhf/` branch.
|
|
159
|
+
Pass `--push` to push the current branch after each successful iteration.
|
|
160
|
+
Together, `--current-branch --push` is useful for loose projects where you want a deployed or locally watched branch to update throughout the run.
|
|
161
|
+
|
|
162
|
+
- Push failures abort the run after preserving the successful local commit.
|
|
163
|
+
- gnhf never force-pushes or auto-pulls for this mode.
|
|
164
|
+
- `--push` also works with the default `gnhf/` branch mode and sets `origin` as the upstream when needed.
|
|
165
|
+
- Do not combine `--current-branch` with `--worktree`; gnhf exits with an error because those modes choose different working directories.
|
|
166
|
+
|
|
151
167
|
### Worktree Mode
|
|
152
168
|
|
|
153
169
|
Pass `--worktree` to run each agent in an isolated [git worktree](https://git-scm.com/docs/git-worktree). This lets you launch multiple agents on the same repo simultaneously — each gets its own working directory and branch without interfering with the others or your main checkout.
|
|
@@ -161,7 +177,7 @@ Pass `--worktree` to run each agent in an isolated [git worktree](https://git-sc
|
|
|
161
177
|
|
|
162
178
|
- Worktrees with commits are **preserved** after the run so you can review, merge, or cherry-pick the work. gnhf prints the path and cleanup command.
|
|
163
179
|
- Re-running the same prompt with `--worktree` resumes a preserved matching worktree when possible; otherwise gnhf creates a suffixed worktree such as `<run-slug>-1` if the original name is unavailable.
|
|
164
|
-
- Worktrees with **no commits** are automatically removed on exit.
|
|
180
|
+
- Worktrees with **no commits** are automatically removed on exit unless a pending commit failure left uncommitted work to inspect or repair.
|
|
165
181
|
- `--worktree` must be run from a non-gnhf branch (typically `main`).
|
|
166
182
|
|
|
167
183
|
## CLI Reference
|
|
@@ -182,9 +198,11 @@ If you run `gnhf` on an existing `gnhf/` branch with a different prompt, gnhf as
|
|
|
182
198
|
| `--agent <agent>` | Agent to use (`claude`, `codex`, `rovodev`, `opencode`, `copilot`, `pi`, or `acp:<target-or-command>`) | config file (`claude`) |
|
|
183
199
|
| `--max-iterations <n>` | Abort after `n` total iterations | unlimited |
|
|
184
200
|
| `--max-tokens <n>` | Abort after `n` total input+output tokens | unlimited |
|
|
185
|
-
| `--stop-when <cond>` | End
|
|
201
|
+
| `--stop-when <cond>` | End when the agent reports this condition, after any commit-failure repair; persists across resume | unlimited |
|
|
186
202
|
| `--prevent-sleep <mode>` | Prevent system sleep during the run (`on`/`off` or `true`/`false`) | config file (`on`) |
|
|
187
203
|
| `--worktree` | Run in a separate git worktree (enables multiple agents concurrently) | `false` |
|
|
204
|
+
| `--current-branch` | Run on the current branch instead of creating a `gnhf/` branch | `false` |
|
|
205
|
+
| `--push` | Push the current branch after each successful iteration | `false` |
|
|
188
206
|
| `--version` | Show version | |
|
|
189
207
|
|
|
190
208
|
## Configuration
|
package/dist/cli.mjs
CHANGED
|
@@ -416,6 +416,33 @@ const NOT_GIT_REPOSITORY_MESSAGE = "This command must be run inside a Git reposi
|
|
|
416
416
|
function translateGitError(error) {
|
|
417
417
|
return error instanceof Error ? error : new Error(String(error));
|
|
418
418
|
}
|
|
419
|
+
function outputText(value) {
|
|
420
|
+
if (typeof value === "string") return value;
|
|
421
|
+
if (Buffer.isBuffer(value)) return value.toString("utf-8");
|
|
422
|
+
if (value instanceof Uint8Array) return Buffer.from(value).toString("utf-8");
|
|
423
|
+
return "";
|
|
424
|
+
}
|
|
425
|
+
function formatGitError(error) {
|
|
426
|
+
const parts = [
|
|
427
|
+
error instanceof Error ? error.message : String(error),
|
|
428
|
+
outputText(error.stdout),
|
|
429
|
+
outputText(error.stderr)
|
|
430
|
+
].map((part) => part.trim()).filter(Boolean);
|
|
431
|
+
return [...new Set(parts)].join("\n");
|
|
432
|
+
}
|
|
433
|
+
function firstLine(text) {
|
|
434
|
+
return text.split("\n").find((line) => line.trim())?.trim() ?? text;
|
|
435
|
+
}
|
|
436
|
+
var CommitFailedError = class extends Error {
|
|
437
|
+
detail;
|
|
438
|
+
constructor(error) {
|
|
439
|
+
const detail = formatGitError(error);
|
|
440
|
+
super(`git commit failed: ${firstLine(detail)}`);
|
|
441
|
+
this.name = "CommitFailedError";
|
|
442
|
+
this.detail = detail;
|
|
443
|
+
this.cause = error;
|
|
444
|
+
}
|
|
445
|
+
};
|
|
419
446
|
function git(args, cwd, options = {}) {
|
|
420
447
|
const env = {
|
|
421
448
|
...options.env ?? process.env,
|
|
@@ -562,18 +589,62 @@ function getBranchDiffStats(baseCommit, cwd) {
|
|
|
562
589
|
return stats;
|
|
563
590
|
}
|
|
564
591
|
function commitAll(message, cwd) {
|
|
592
|
+
const commitArgs = [
|
|
593
|
+
"-c",
|
|
594
|
+
"commit.gpgsign=false",
|
|
595
|
+
"-c",
|
|
596
|
+
"tag.gpgsign=false",
|
|
597
|
+
"commit",
|
|
598
|
+
"-m",
|
|
599
|
+
message
|
|
600
|
+
];
|
|
565
601
|
git(["add", "-A"], cwd);
|
|
566
602
|
try {
|
|
567
603
|
git([
|
|
568
|
-
"
|
|
569
|
-
"
|
|
570
|
-
"
|
|
571
|
-
"tag.gpgsign=false",
|
|
572
|
-
"commit",
|
|
573
|
-
"-m",
|
|
574
|
-
message
|
|
604
|
+
"diff",
|
|
605
|
+
"--cached",
|
|
606
|
+
"--quiet"
|
|
575
607
|
], cwd);
|
|
608
|
+
return;
|
|
576
609
|
} catch {}
|
|
610
|
+
try {
|
|
611
|
+
git(commitArgs, cwd);
|
|
612
|
+
} catch (error) {
|
|
613
|
+
const commitError = new CommitFailedError(error);
|
|
614
|
+
appendDebugLog("git:commit:failed", {
|
|
615
|
+
error: serializeError(error),
|
|
616
|
+
detail: commitError.detail
|
|
617
|
+
});
|
|
618
|
+
throw commitError;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function pushCurrentBranch(cwd) {
|
|
622
|
+
let hasUpstream = true;
|
|
623
|
+
try {
|
|
624
|
+
git([
|
|
625
|
+
"rev-parse",
|
|
626
|
+
"--abbrev-ref",
|
|
627
|
+
"--symbolic-full-name",
|
|
628
|
+
"@{upstream}"
|
|
629
|
+
], cwd);
|
|
630
|
+
} catch {
|
|
631
|
+
hasUpstream = false;
|
|
632
|
+
}
|
|
633
|
+
if (hasUpstream) {
|
|
634
|
+
git(["push"], cwd);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
git([
|
|
638
|
+
"remote",
|
|
639
|
+
"get-url",
|
|
640
|
+
"origin"
|
|
641
|
+
], cwd);
|
|
642
|
+
git([
|
|
643
|
+
"push",
|
|
644
|
+
"-u",
|
|
645
|
+
"origin",
|
|
646
|
+
"HEAD"
|
|
647
|
+
], cwd);
|
|
577
648
|
}
|
|
578
649
|
function resetHard(cwd) {
|
|
579
650
|
git([
|
|
@@ -16377,6 +16448,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16377
16448
|
activeIterationPromise = null;
|
|
16378
16449
|
activeAbortController = null;
|
|
16379
16450
|
pendingAbortReason = null;
|
|
16451
|
+
pendingCommitFailure = null;
|
|
16380
16452
|
activeIterationTokensEstimated = false;
|
|
16381
16453
|
loopDone = false;
|
|
16382
16454
|
stoppedEventEmitted = false;
|
|
@@ -16413,7 +16485,8 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16413
16485
|
return {
|
|
16414
16486
|
...this.state,
|
|
16415
16487
|
tokensEstimated: this.state.tokensEstimated || this.activeIterationTokensEstimated,
|
|
16416
|
-
interruptHint: getInterruptHint(this.state)
|
|
16488
|
+
interruptHint: getInterruptHint(this.state),
|
|
16489
|
+
hasPendingCommitFailure: this.pendingCommitFailure !== null
|
|
16417
16490
|
};
|
|
16418
16491
|
}
|
|
16419
16492
|
requestGracefulStop() {
|
|
@@ -16466,6 +16539,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16466
16539
|
await iterationPromise;
|
|
16467
16540
|
} else await this.closeAgent();
|
|
16468
16541
|
resetHard(this.cwd);
|
|
16542
|
+
this.pendingCommitFailure = null;
|
|
16469
16543
|
this.state.status = "stopped";
|
|
16470
16544
|
this.emit("state", this.getState());
|
|
16471
16545
|
this.emitStopped();
|
|
@@ -16481,6 +16555,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16481
16555
|
startIteration: this.state.currentIteration,
|
|
16482
16556
|
maxIterations: this.limits.maxIterations,
|
|
16483
16557
|
maxTokens: this.limits.maxTokens,
|
|
16558
|
+
push: this.limits.push === true,
|
|
16484
16559
|
maxConsecutiveFailures: this.config.maxConsecutiveFailures,
|
|
16485
16560
|
baseCommit: this.runInfo.baseCommit,
|
|
16486
16561
|
initialCommitCount: this.state.commitCount
|
|
@@ -16497,13 +16572,14 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16497
16572
|
this.state.status = "running";
|
|
16498
16573
|
this.emit("iteration:start", this.state.currentIteration);
|
|
16499
16574
|
this.emit("state", this.getState());
|
|
16500
|
-
const
|
|
16575
|
+
const baseIterationPrompt = buildIterationPrompt({
|
|
16501
16576
|
n: this.state.currentIteration,
|
|
16502
16577
|
runId: this.runInfo.runId,
|
|
16503
16578
|
prompt: this.prompt,
|
|
16504
16579
|
stopWhen: this.limits.stopWhen,
|
|
16505
16580
|
commitMessage: this.config.commitMessage
|
|
16506
16581
|
});
|
|
16582
|
+
const iterationPrompt = this.pendingCommitFailure ? this.buildCommitRepairPrompt(baseIterationPrompt) : baseIterationPrompt;
|
|
16507
16583
|
appendDebugLog("iteration:start", {
|
|
16508
16584
|
iteration: this.state.currentIteration,
|
|
16509
16585
|
promptLength: iterationPrompt.length,
|
|
@@ -16550,6 +16626,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16550
16626
|
tokensEstimated: this.state.tokensEstimated,
|
|
16551
16627
|
commitCount: this.state.commitCount
|
|
16552
16628
|
});
|
|
16629
|
+
if (result.abortReason) {
|
|
16630
|
+
this.abort(result.abortReason);
|
|
16631
|
+
break;
|
|
16632
|
+
}
|
|
16553
16633
|
if (this.stopForGracefulShutdown()) break;
|
|
16554
16634
|
if (this.limits.stopWhen !== void 0 && result.shouldFullyStop) {
|
|
16555
16635
|
this.abort("stop condition met");
|
|
@@ -16659,11 +16739,16 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16659
16739
|
});
|
|
16660
16740
|
if (this.stopRequested) return { type: "stopped" };
|
|
16661
16741
|
const shouldFullyStop = result.output.should_fully_stop === true;
|
|
16662
|
-
if (result.output.success)
|
|
16663
|
-
|
|
16664
|
-
record
|
|
16665
|
-
|
|
16666
|
-
|
|
16742
|
+
if (result.output.success) {
|
|
16743
|
+
const record = this.recordSuccess(result.output);
|
|
16744
|
+
const abortReason = record.success && this.limits.push === true ? this.pushAfterSuccess() : void 0;
|
|
16745
|
+
return {
|
|
16746
|
+
type: "completed",
|
|
16747
|
+
record,
|
|
16748
|
+
shouldFullyStop: record.success ? shouldFullyStop : false,
|
|
16749
|
+
...abortReason === void 0 ? {} : { abortReason }
|
|
16750
|
+
};
|
|
16751
|
+
}
|
|
16667
16752
|
return {
|
|
16668
16753
|
type: "completed",
|
|
16669
16754
|
record: this.recordFailure(`[FAIL] ${result.output.summary}`, result.output.summary, toStringArray(result.output.key_learnings), "reported"),
|
|
@@ -16681,7 +16766,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16681
16766
|
elapsedMs,
|
|
16682
16767
|
reason: this.pendingAbortReason
|
|
16683
16768
|
});
|
|
16684
|
-
resetHard(this.cwd);
|
|
16769
|
+
if (this.pendingCommitFailure === null) resetHard(this.cwd);
|
|
16685
16770
|
return {
|
|
16686
16771
|
type: "aborted",
|
|
16687
16772
|
reason: this.pendingAbortReason
|
|
@@ -16700,7 +16785,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16700
16785
|
error: serializeError(err)
|
|
16701
16786
|
});
|
|
16702
16787
|
if (err instanceof PermanentAgentError) {
|
|
16703
|
-
resetHard(this.cwd);
|
|
16788
|
+
if (this.pendingCommitFailure === null) resetHard(this.cwd);
|
|
16704
16789
|
this.state.lastAgentError = err.detail;
|
|
16705
16790
|
return {
|
|
16706
16791
|
type: "aborted",
|
|
@@ -16719,8 +16804,16 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16719
16804
|
}
|
|
16720
16805
|
}
|
|
16721
16806
|
recordSuccess(output) {
|
|
16722
|
-
|
|
16723
|
-
|
|
16807
|
+
const keyChanges = toStringArray(output.key_changes_made);
|
|
16808
|
+
const keyLearnings = toStringArray(output.key_learnings);
|
|
16809
|
+
try {
|
|
16810
|
+
commitAll(buildCommitMessage(this.config.commitMessage, output, { iteration: this.state.currentIteration }), this.cwd);
|
|
16811
|
+
} catch (error) {
|
|
16812
|
+
if (error instanceof CommitFailedError) return this.recordCommitFailure(error);
|
|
16813
|
+
throw error;
|
|
16814
|
+
}
|
|
16815
|
+
this.pendingCommitFailure = null;
|
|
16816
|
+
appendNotes(this.runInfo.notesPath, this.state.currentIteration, output.summary, keyChanges, keyLearnings);
|
|
16724
16817
|
this.state.commitCount = getBranchCommitCount(this.runInfo.baseCommit, this.cwd);
|
|
16725
16818
|
this.state.successCount++;
|
|
16726
16819
|
this.state.consecutiveFailures = 0;
|
|
@@ -16730,14 +16823,60 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16730
16823
|
number: this.state.currentIteration,
|
|
16731
16824
|
success: true,
|
|
16732
16825
|
summary: output.summary,
|
|
16733
|
-
keyChanges
|
|
16734
|
-
keyLearnings
|
|
16826
|
+
keyChanges,
|
|
16827
|
+
keyLearnings,
|
|
16735
16828
|
timestamp: /* @__PURE__ */ new Date()
|
|
16736
16829
|
};
|
|
16737
16830
|
}
|
|
16831
|
+
buildCommitRepairPrompt(basePrompt) {
|
|
16832
|
+
return `${basePrompt}
|
|
16833
|
+
|
|
16834
|
+
## Previous Commit Failure
|
|
16835
|
+
|
|
16836
|
+
The previous iteration made workspace changes, but gnhf could not commit them because git commit failed.
|
|
16837
|
+
Do not start unrelated work.
|
|
16838
|
+
Inspect and fix the existing uncommitted changes so the commit can pass, then report success.
|
|
16839
|
+
|
|
16840
|
+
Git commit output:
|
|
16841
|
+
|
|
16842
|
+
\`\`\`
|
|
16843
|
+
${this.pendingCommitFailure}
|
|
16844
|
+
\`\`\``;
|
|
16845
|
+
}
|
|
16846
|
+
recordCommitFailure(error) {
|
|
16847
|
+
this.pendingCommitFailure = error.detail;
|
|
16848
|
+
const summary = "git commit failed; asking agent to repair the workspace";
|
|
16849
|
+
appendNotes(this.runInfo.notesPath, this.state.currentIteration, `[ERROR] ${summary}`, [], [error.detail]);
|
|
16850
|
+
this.state.failCount++;
|
|
16851
|
+
this.state.consecutiveFailures++;
|
|
16852
|
+
this.state.consecutiveErrors = 0;
|
|
16853
|
+
this.state.lastAgentError = error.detail;
|
|
16854
|
+
return {
|
|
16855
|
+
number: this.state.currentIteration,
|
|
16856
|
+
success: false,
|
|
16857
|
+
summary,
|
|
16858
|
+
keyChanges: [],
|
|
16859
|
+
keyLearnings: [error.detail],
|
|
16860
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
16861
|
+
};
|
|
16862
|
+
}
|
|
16863
|
+
pushAfterSuccess() {
|
|
16864
|
+
try {
|
|
16865
|
+
pushCurrentBranch(this.cwd);
|
|
16866
|
+
appendDebugLog("git:push:success", { iteration: this.state.currentIteration });
|
|
16867
|
+
return;
|
|
16868
|
+
} catch (err) {
|
|
16869
|
+
appendDebugLog("git:push:error", {
|
|
16870
|
+
iteration: this.state.currentIteration,
|
|
16871
|
+
error: serializeError(err)
|
|
16872
|
+
});
|
|
16873
|
+
return `push failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
16874
|
+
}
|
|
16875
|
+
}
|
|
16738
16876
|
recordFailure(notesSummary, recordSummary, learnings, kind) {
|
|
16877
|
+
const hadPendingCommitFailure = this.pendingCommitFailure !== null;
|
|
16739
16878
|
appendNotes(this.runInfo.notesPath, this.state.currentIteration, notesSummary, [], toStringArray(learnings));
|
|
16740
|
-
resetHard(this.cwd);
|
|
16879
|
+
if (!hadPendingCommitFailure) resetHard(this.cwd);
|
|
16741
16880
|
this.state.failCount++;
|
|
16742
16881
|
this.state.consecutiveFailures++;
|
|
16743
16882
|
if (kind === "error") {
|
|
@@ -16951,7 +17090,7 @@ function renderExitSummary(options) {
|
|
|
16951
17090
|
const title = stopped ? `${s.red("×")} ${s.bold("gnhf stopped")}` : `${s.cyan("✦")} ${s.bold("gnhf wrapped")}`;
|
|
16952
17091
|
const subtitle = stopped ? `${s.cyan(options.agentName)} ran for ${s.yellow(elapsed)} before: ${options.abortReason ?? options.status}` : `${s.cyan(options.agentName)} worked for ${s.yellow(elapsed)} on ${s.magenta(options.branchName)}`;
|
|
16953
17092
|
const cardWidth = resolveCardWidth([title, ` ${subtitle}`], options.terminalColumns);
|
|
16954
|
-
const
|
|
17093
|
+
const failed = `${options.failCount} failed`;
|
|
16955
17094
|
const inputTokens = formatTokenCount$1(options.totalInputTokens, "in", options.tokensEstimated);
|
|
16956
17095
|
const outputTokens = formatTokenCount$1(options.totalOutputTokens, "out", options.tokensEstimated);
|
|
16957
17096
|
const commits = plural(options.commitCount, "commit");
|
|
@@ -16966,7 +17105,7 @@ function renderExitSummary(options) {
|
|
|
16966
17105
|
metricLine(s.dim("iterations"), [
|
|
16967
17106
|
`${s.bold(String(options.iterations))} total`,
|
|
16968
17107
|
s.green(`${options.successCount} good`),
|
|
16969
|
-
stopped ? s.red(
|
|
17108
|
+
stopped ? s.red(failed) : s.yellow(failed)
|
|
16970
17109
|
]),
|
|
16971
17110
|
metricLine(s.dim("tokens"), [s.bold(inputTokens), s.bold(outputTokens)]),
|
|
16972
17111
|
metricLine(s.dim("branch diff"), [
|
|
@@ -16982,6 +17121,7 @@ function renderExitSummary(options) {
|
|
|
16982
17121
|
"",
|
|
16983
17122
|
commandLine(s.dim("notes"), options.notesPath),
|
|
16984
17123
|
commandLine(s.dim("debug log"), options.logPath),
|
|
17124
|
+
...options.hasPendingCommitFailure ? [commandLine(s.yellow("uncommitted"), "commit failed; changes were left for repair")] : [],
|
|
16985
17125
|
"",
|
|
16986
17126
|
commandLine(s.dim("next steps"), s.cyan(`git log --oneline ${options.baseRef}..HEAD`)),
|
|
16987
17127
|
continuationLine(s.cyan(`git diff --stat ${options.baseRef}..HEAD`)),
|
|
@@ -17913,6 +18053,11 @@ function initializeNewBranch(prompt, cwd, schemaOptions) {
|
|
|
17913
18053
|
const runId = createBranchWithSuffix(slugifyPrompt(prompt), cwd).split("/")[1];
|
|
17914
18054
|
return setupRun(runId, prompt, baseCommit, cwd, schemaOptions);
|
|
17915
18055
|
}
|
|
18056
|
+
function initializeCurrentBranchRun(prompt, cwd, schemaOptions) {
|
|
18057
|
+
ensureCleanWorkingTree(cwd);
|
|
18058
|
+
const baseCommit = getHeadCommit(cwd);
|
|
18059
|
+
return setupRun(createRunIdWithSuffix(slugifyPrompt(prompt).split("/")[1], cwd), prompt, baseCommit, cwd, schemaOptions);
|
|
18060
|
+
}
|
|
17916
18061
|
function branchNameWithSuffix(branchName, suffix) {
|
|
17917
18062
|
return suffix === 0 ? branchName : `${branchName}-${suffix}`;
|
|
17918
18063
|
}
|
|
@@ -17932,6 +18077,16 @@ function createBranchWithSuffix(branchName, cwd) {
|
|
|
17932
18077
|
}
|
|
17933
18078
|
throw new Error(`Unable to create a unique branch name for ${branchName}`);
|
|
17934
18079
|
}
|
|
18080
|
+
function runIdWithSuffix(runId, suffix) {
|
|
18081
|
+
return suffix === 0 ? runId : `${runId}-${suffix}`;
|
|
18082
|
+
}
|
|
18083
|
+
function createRunIdWithSuffix(runId, cwd) {
|
|
18084
|
+
for (let suffix = 0; suffix < 100; suffix += 1) {
|
|
18085
|
+
const candidate = runIdWithSuffix(runId, suffix);
|
|
18086
|
+
if (!existsSync(join(cwd, ".gnhf", "runs", candidate))) return candidate;
|
|
18087
|
+
}
|
|
18088
|
+
throw new Error(`Unable to create a unique run id for ${runId}`);
|
|
18089
|
+
}
|
|
17935
18090
|
function initializeWorktreeRun(prompt, cwd, schemaOptions, resumeSchemaOptions) {
|
|
17936
18091
|
const repoRoot = getRepoRootDir(cwd);
|
|
17937
18092
|
const baseCommit = getHeadCommit(cwd);
|
|
@@ -18109,7 +18264,7 @@ function readReexecStdinPrompt(env) {
|
|
|
18109
18264
|
}
|
|
18110
18265
|
}
|
|
18111
18266
|
const program = new Command();
|
|
18112
|
-
program.name("gnhf").description("Before I go to bed, I tell my agents: good night, have fun").version(packageVersion).argument("[prompt]", "The objective for the coding agent").option("--agent <agent>", `Agent to use (${AGENT_NAMES.join(", ")}, or acp:<target-or-command>)`).option("--max-iterations <n>", "Abort after N total iterations", parseNonNegativeInteger).option("--max-tokens <n>", "Abort after N total input+output tokens", parseNonNegativeInteger).option("--stop-when <condition>", "End when the agent reports this condition; resumes reuse it, pass a new value to overwrite or \"\" to clear").option("--prevent-sleep <mode>", "Prevent system sleep during the run (\"on\" or \"off\")", parseOnOffBoolean).option("--worktree", "Run in a separate git worktree (enables multiple agents on the same repo)", false).option("--mock", "", false).action(async (promptArg, options) => {
|
|
18267
|
+
program.name("gnhf").description("Before I go to bed, I tell my agents: good night, have fun").version(packageVersion).argument("[prompt]", "The objective for the coding agent").option("--agent <agent>", `Agent to use (${AGENT_NAMES.join(", ")}, or acp:<target-or-command>)`).option("--max-iterations <n>", "Abort after N total iterations", parseNonNegativeInteger).option("--max-tokens <n>", "Abort after N total input+output tokens", parseNonNegativeInteger).option("--stop-when <condition>", "End when the agent reports this condition, after any commit-failure repair; resumes reuse it, pass a new value to overwrite or \"\" to clear").option("--prevent-sleep <mode>", "Prevent system sleep during the run (\"on\" or \"off\")", parseOnOffBoolean).option("--worktree", "Run in a separate git worktree (enables multiple agents on the same repo)", false).option("--current-branch", "Run on the current branch instead of creating a gnhf branch", false).option("--push", "Push the current branch after each successful iteration", false).option("--mock", "", false).action(async (promptArg, options) => {
|
|
18113
18268
|
if (options.mock) {
|
|
18114
18269
|
const mock = new MockOrchestrator();
|
|
18115
18270
|
enterAltScreen();
|
|
@@ -18158,6 +18313,10 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
18158
18313
|
let worktreeCleanup = null;
|
|
18159
18314
|
const currentBranch = getCurrentBranch(cwd);
|
|
18160
18315
|
const onGnhfBranch = currentBranch.startsWith("gnhf/");
|
|
18316
|
+
if (options.currentBranch && options.worktree) {
|
|
18317
|
+
console.error("Cannot combine --current-branch and --worktree.");
|
|
18318
|
+
process$1.exit(1);
|
|
18319
|
+
}
|
|
18161
18320
|
const cliStopWhen = options.stopWhen === "" ? void 0 : options.stopWhen;
|
|
18162
18321
|
let effectiveStopWhen = cliStopWhen;
|
|
18163
18322
|
let effectiveCommitMessage = config.commitMessage;
|
|
@@ -18194,6 +18353,12 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
18194
18353
|
if (worktreeCleanup === exitCleanup) exitCleanup();
|
|
18195
18354
|
});
|
|
18196
18355
|
}
|
|
18356
|
+
} else if (options.currentBranch) {
|
|
18357
|
+
if (!prompt) {
|
|
18358
|
+
program.help();
|
|
18359
|
+
return;
|
|
18360
|
+
}
|
|
18361
|
+
runInfo = initializeCurrentBranchRun(prompt, cwd, schemaOptions);
|
|
18197
18362
|
} else if (onGnhfBranch) {
|
|
18198
18363
|
const existingRunId = currentBranch.slice(5);
|
|
18199
18364
|
const existingMetadata = peekRunMetadata(existingRunId, cwd);
|
|
@@ -18250,7 +18415,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
18250
18415
|
if (!reexeced) persistedPrompt?.cleanup();
|
|
18251
18416
|
}
|
|
18252
18417
|
}
|
|
18253
|
-
const runMode = options.worktree ? "worktree" : startIteration > 0 ? "resume" : "new";
|
|
18418
|
+
const runMode = options.worktree ? "worktree" : options.currentBranch ? "current-branch" : startIteration > 0 ? "resume" : "new";
|
|
18254
18419
|
const telemetryAgent = getTelemetryAgent(config.agent);
|
|
18255
18420
|
telemetry.pageview("/run", {
|
|
18256
18421
|
agent: telemetryAgent,
|
|
@@ -18273,6 +18438,8 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
18273
18438
|
agentArgsOverride: getNativeAgentName(config.agent) ? config.agentArgsOverride?.[getNativeAgentName(config.agent)] : void 0,
|
|
18274
18439
|
worktree: options.worktree,
|
|
18275
18440
|
worktreePath,
|
|
18441
|
+
currentBranch: options.currentBranch,
|
|
18442
|
+
push: options.push,
|
|
18276
18443
|
platform: process$1.platform,
|
|
18277
18444
|
nodeVersion: process$1.version,
|
|
18278
18445
|
gnhfVersion: packageVersion
|
|
@@ -18288,7 +18455,8 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
18288
18455
|
}, agent, runInfo, prompt, effectiveCwd, startIteration, {
|
|
18289
18456
|
maxIterations: options.maxIterations,
|
|
18290
18457
|
maxTokens: options.maxTokens,
|
|
18291
|
-
stopWhen: effectiveStopWhen
|
|
18458
|
+
stopWhen: effectiveStopWhen,
|
|
18459
|
+
...options.push ? { push: true } : {}
|
|
18292
18460
|
});
|
|
18293
18461
|
let shutdownSignal = null;
|
|
18294
18462
|
let forceShutdownRequested = false;
|
|
@@ -18380,7 +18548,8 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
18380
18548
|
baseRef: runInfo.baseCommit.slice(0, 12) || runInfo.baseCommit,
|
|
18381
18549
|
diffStats,
|
|
18382
18550
|
color: shouldUseColor(),
|
|
18383
|
-
terminalColumns: process$1.stdout.columns
|
|
18551
|
+
terminalColumns: process$1.stdout.columns,
|
|
18552
|
+
hasPendingCommitFailure: finalState.hasPendingCommitFailure
|
|
18384
18553
|
});
|
|
18385
18554
|
appendDebugLog("run:complete", {
|
|
18386
18555
|
signal: shutdownSignal,
|
|
@@ -18406,12 +18575,13 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
18406
18575
|
total_output_tokens: finalState.totalOutputTokens,
|
|
18407
18576
|
duration_ms: Date.now() - runStartedAt,
|
|
18408
18577
|
prevent_sleep: config.preventSleep === true,
|
|
18578
|
+
push_each_iteration: options.push === true,
|
|
18409
18579
|
commit_message_preset: effectiveCommitMessage?.preset ?? "default",
|
|
18410
18580
|
stop_when_set: effectiveStopWhen !== void 0
|
|
18411
18581
|
});
|
|
18412
18582
|
await telemetry.close(1e3);
|
|
18413
18583
|
if (finalState.status === "aborted") console.error(`\n gnhf: Run log: ${runInfo.logPath}\n`);
|
|
18414
|
-
if (worktreePath) if (finalState.commitCount > 0) {
|
|
18584
|
+
if (worktreePath) if (finalState.commitCount > 0 || finalState.hasPendingCommitFailure) {
|
|
18415
18585
|
worktreeCleanup = null;
|
|
18416
18586
|
console.error(`\n gnhf: worktree preserved at ${worktreePath}\n gnhf: merge the branch and remove with: git worktree remove "${worktreePath}"\n`);
|
|
18417
18587
|
} else {
|