gnhf 0.1.35 → 0.1.37
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 +28 -9
- package/dist/cli.mjs +285 -40
- 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; if
|
|
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,12 @@ 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` |
|
|
206
|
+
| `--meteor-frequency <n>` | Set TUI meteor frequency from 0 to 5 (`0` disables meteors) | `3` |
|
|
188
207
|
| `--version` | Show version | |
|
|
189
208
|
|
|
190
209
|
## 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,
|
|
@@ -572,18 +599,52 @@ function commitAll(message, cwd) {
|
|
|
572
599
|
message
|
|
573
600
|
];
|
|
574
601
|
git(["add", "-A"], cwd);
|
|
575
|
-
let firstError;
|
|
576
602
|
try {
|
|
577
|
-
git(
|
|
603
|
+
git([
|
|
604
|
+
"diff",
|
|
605
|
+
"--cached",
|
|
606
|
+
"--quiet"
|
|
607
|
+
], cwd);
|
|
578
608
|
return;
|
|
609
|
+
} catch {}
|
|
610
|
+
try {
|
|
611
|
+
git(commitArgs, cwd);
|
|
579
612
|
} catch (error) {
|
|
580
|
-
|
|
613
|
+
const commitError = new CommitFailedError(error);
|
|
614
|
+
appendDebugLog("git:commit:failed", {
|
|
615
|
+
error: serializeError(error),
|
|
616
|
+
detail: commitError.detail
|
|
617
|
+
});
|
|
618
|
+
throw commitError;
|
|
581
619
|
}
|
|
582
|
-
|
|
620
|
+
}
|
|
621
|
+
function pushCurrentBranch(cwd) {
|
|
622
|
+
let hasUpstream = true;
|
|
583
623
|
try {
|
|
584
|
-
git([
|
|
585
|
-
|
|
586
|
-
|
|
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);
|
|
587
648
|
}
|
|
588
649
|
function resetHard(cwd) {
|
|
589
650
|
git([
|
|
@@ -16387,6 +16448,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16387
16448
|
activeIterationPromise = null;
|
|
16388
16449
|
activeAbortController = null;
|
|
16389
16450
|
pendingAbortReason = null;
|
|
16451
|
+
pendingCommitFailure = null;
|
|
16390
16452
|
activeIterationTokensEstimated = false;
|
|
16391
16453
|
loopDone = false;
|
|
16392
16454
|
stoppedEventEmitted = false;
|
|
@@ -16423,7 +16485,8 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16423
16485
|
return {
|
|
16424
16486
|
...this.state,
|
|
16425
16487
|
tokensEstimated: this.state.tokensEstimated || this.activeIterationTokensEstimated,
|
|
16426
|
-
interruptHint: getInterruptHint(this.state)
|
|
16488
|
+
interruptHint: getInterruptHint(this.state),
|
|
16489
|
+
hasPendingCommitFailure: this.pendingCommitFailure !== null
|
|
16427
16490
|
};
|
|
16428
16491
|
}
|
|
16429
16492
|
requestGracefulStop() {
|
|
@@ -16476,6 +16539,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16476
16539
|
await iterationPromise;
|
|
16477
16540
|
} else await this.closeAgent();
|
|
16478
16541
|
resetHard(this.cwd);
|
|
16542
|
+
this.pendingCommitFailure = null;
|
|
16479
16543
|
this.state.status = "stopped";
|
|
16480
16544
|
this.emit("state", this.getState());
|
|
16481
16545
|
this.emitStopped();
|
|
@@ -16491,6 +16555,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16491
16555
|
startIteration: this.state.currentIteration,
|
|
16492
16556
|
maxIterations: this.limits.maxIterations,
|
|
16493
16557
|
maxTokens: this.limits.maxTokens,
|
|
16558
|
+
push: this.limits.push === true,
|
|
16494
16559
|
maxConsecutiveFailures: this.config.maxConsecutiveFailures,
|
|
16495
16560
|
baseCommit: this.runInfo.baseCommit,
|
|
16496
16561
|
initialCommitCount: this.state.commitCount
|
|
@@ -16507,13 +16572,14 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16507
16572
|
this.state.status = "running";
|
|
16508
16573
|
this.emit("iteration:start", this.state.currentIteration);
|
|
16509
16574
|
this.emit("state", this.getState());
|
|
16510
|
-
const
|
|
16575
|
+
const baseIterationPrompt = buildIterationPrompt({
|
|
16511
16576
|
n: this.state.currentIteration,
|
|
16512
16577
|
runId: this.runInfo.runId,
|
|
16513
16578
|
prompt: this.prompt,
|
|
16514
16579
|
stopWhen: this.limits.stopWhen,
|
|
16515
16580
|
commitMessage: this.config.commitMessage
|
|
16516
16581
|
});
|
|
16582
|
+
const iterationPrompt = this.pendingCommitFailure ? this.buildCommitRepairPrompt(baseIterationPrompt) : baseIterationPrompt;
|
|
16517
16583
|
appendDebugLog("iteration:start", {
|
|
16518
16584
|
iteration: this.state.currentIteration,
|
|
16519
16585
|
promptLength: iterationPrompt.length,
|
|
@@ -16560,6 +16626,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16560
16626
|
tokensEstimated: this.state.tokensEstimated,
|
|
16561
16627
|
commitCount: this.state.commitCount
|
|
16562
16628
|
});
|
|
16629
|
+
if (result.abortReason) {
|
|
16630
|
+
this.abort(result.abortReason);
|
|
16631
|
+
break;
|
|
16632
|
+
}
|
|
16563
16633
|
if (this.stopForGracefulShutdown()) break;
|
|
16564
16634
|
if (this.limits.stopWhen !== void 0 && result.shouldFullyStop) {
|
|
16565
16635
|
this.abort("stop condition met");
|
|
@@ -16669,11 +16739,16 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16669
16739
|
});
|
|
16670
16740
|
if (this.stopRequested) return { type: "stopped" };
|
|
16671
16741
|
const shouldFullyStop = result.output.should_fully_stop === true;
|
|
16672
|
-
if (result.output.success)
|
|
16673
|
-
|
|
16674
|
-
record
|
|
16675
|
-
|
|
16676
|
-
|
|
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
|
+
}
|
|
16677
16752
|
return {
|
|
16678
16753
|
type: "completed",
|
|
16679
16754
|
record: this.recordFailure(`[FAIL] ${result.output.summary}`, result.output.summary, toStringArray(result.output.key_learnings), "reported"),
|
|
@@ -16691,7 +16766,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16691
16766
|
elapsedMs,
|
|
16692
16767
|
reason: this.pendingAbortReason
|
|
16693
16768
|
});
|
|
16694
|
-
resetHard(this.cwd);
|
|
16769
|
+
if (this.pendingCommitFailure === null) resetHard(this.cwd);
|
|
16695
16770
|
return {
|
|
16696
16771
|
type: "aborted",
|
|
16697
16772
|
reason: this.pendingAbortReason
|
|
@@ -16710,7 +16785,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16710
16785
|
error: serializeError(err)
|
|
16711
16786
|
});
|
|
16712
16787
|
if (err instanceof PermanentAgentError) {
|
|
16713
|
-
resetHard(this.cwd);
|
|
16788
|
+
if (this.pendingCommitFailure === null) resetHard(this.cwd);
|
|
16714
16789
|
this.state.lastAgentError = err.detail;
|
|
16715
16790
|
return {
|
|
16716
16791
|
type: "aborted",
|
|
@@ -16729,8 +16804,16 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16729
16804
|
}
|
|
16730
16805
|
}
|
|
16731
16806
|
recordSuccess(output) {
|
|
16732
|
-
|
|
16733
|
-
|
|
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);
|
|
16734
16817
|
this.state.commitCount = getBranchCommitCount(this.runInfo.baseCommit, this.cwd);
|
|
16735
16818
|
this.state.successCount++;
|
|
16736
16819
|
this.state.consecutiveFailures = 0;
|
|
@@ -16740,14 +16823,60 @@ var Orchestrator = class extends EventEmitter {
|
|
|
16740
16823
|
number: this.state.currentIteration,
|
|
16741
16824
|
success: true,
|
|
16742
16825
|
summary: output.summary,
|
|
16743
|
-
keyChanges
|
|
16744
|
-
keyLearnings
|
|
16826
|
+
keyChanges,
|
|
16827
|
+
keyLearnings,
|
|
16745
16828
|
timestamp: /* @__PURE__ */ new Date()
|
|
16746
16829
|
};
|
|
16747
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
|
+
}
|
|
16748
16876
|
recordFailure(notesSummary, recordSummary, learnings, kind) {
|
|
16877
|
+
const hadPendingCommitFailure = this.pendingCommitFailure !== null;
|
|
16749
16878
|
appendNotes(this.runInfo.notesPath, this.state.currentIteration, notesSummary, [], toStringArray(learnings));
|
|
16750
|
-
resetHard(this.cwd);
|
|
16879
|
+
if (!hadPendingCommitFailure) resetHard(this.cwd);
|
|
16751
16880
|
this.state.failCount++;
|
|
16752
16881
|
this.state.consecutiveFailures++;
|
|
16753
16882
|
if (kind === "error") {
|
|
@@ -16961,7 +17090,7 @@ function renderExitSummary(options) {
|
|
|
16961
17090
|
const title = stopped ? `${s.red("×")} ${s.bold("gnhf stopped")}` : `${s.cyan("✦")} ${s.bold("gnhf wrapped")}`;
|
|
16962
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)}`;
|
|
16963
17092
|
const cardWidth = resolveCardWidth([title, ` ${subtitle}`], options.terminalColumns);
|
|
16964
|
-
const
|
|
17093
|
+
const failed = `${options.failCount} failed`;
|
|
16965
17094
|
const inputTokens = formatTokenCount$1(options.totalInputTokens, "in", options.tokensEstimated);
|
|
16966
17095
|
const outputTokens = formatTokenCount$1(options.totalOutputTokens, "out", options.tokensEstimated);
|
|
16967
17096
|
const commits = plural(options.commitCount, "commit");
|
|
@@ -16976,7 +17105,7 @@ function renderExitSummary(options) {
|
|
|
16976
17105
|
metricLine(s.dim("iterations"), [
|
|
16977
17106
|
`${s.bold(String(options.iterations))} total`,
|
|
16978
17107
|
s.green(`${options.successCount} good`),
|
|
16979
|
-
stopped ? s.red(
|
|
17108
|
+
stopped ? s.red(failed) : s.yellow(failed)
|
|
16980
17109
|
]),
|
|
16981
17110
|
metricLine(s.dim("tokens"), [s.bold(inputTokens), s.bold(outputTokens)]),
|
|
16982
17111
|
metricLine(s.dim("branch diff"), [
|
|
@@ -16992,6 +17121,7 @@ function renderExitSummary(options) {
|
|
|
16992
17121
|
"",
|
|
16993
17122
|
commandLine(s.dim("notes"), options.notesPath),
|
|
16994
17123
|
commandLine(s.dim("debug log"), options.logPath),
|
|
17124
|
+
...options.hasPendingCommitFailure ? [commandLine(s.yellow("uncommitted"), "commit failed; changes were left for repair")] : [],
|
|
16995
17125
|
"",
|
|
16996
17126
|
commandLine(s.dim("next steps"), s.cyan(`git log --oneline ${options.baseRef}..HEAD`)),
|
|
16997
17127
|
continuationLine(s.cyan(`git diff --stat ${options.baseRef}..HEAD`)),
|
|
@@ -17157,6 +17287,7 @@ const STAR_CHARS = [
|
|
|
17157
17287
|
"°",
|
|
17158
17288
|
"°"
|
|
17159
17289
|
];
|
|
17290
|
+
const MIN_METEOR_START_GAP_MS = 500;
|
|
17160
17291
|
function generateStarField(width, height, density, seed) {
|
|
17161
17292
|
const stars = [];
|
|
17162
17293
|
let s = seed;
|
|
@@ -17191,6 +17322,47 @@ function getStarState(star, now) {
|
|
|
17191
17322
|
if (t > .025) return "bright";
|
|
17192
17323
|
return "dim";
|
|
17193
17324
|
}
|
|
17325
|
+
function generateMeteorShower(width, height, count, seed) {
|
|
17326
|
+
if (width <= 0 || height <= 0 || count <= 0) return [];
|
|
17327
|
+
const meteors = [];
|
|
17328
|
+
let s = seed % 2147483647;
|
|
17329
|
+
if (s <= 0) s += 2147483646;
|
|
17330
|
+
const rand = () => {
|
|
17331
|
+
s = s * 16807 % 2147483647;
|
|
17332
|
+
return s / 2147483647;
|
|
17333
|
+
};
|
|
17334
|
+
for (let i = 0; i < count; i++) {
|
|
17335
|
+
const length = Math.min(height, 2 + (i + Math.floor(rand() * 6)) % 6);
|
|
17336
|
+
const xMax = Math.max(1, width - length);
|
|
17337
|
+
const yMin = Math.max(0, length - 1);
|
|
17338
|
+
const yMaxExclusive = Math.max(yMin + 1, Math.ceil(height * .75));
|
|
17339
|
+
const ySpan = Math.max(1, yMaxExclusive - yMin);
|
|
17340
|
+
const period = 16e3 + rand() * 2e4;
|
|
17341
|
+
const duration = count >= 8 ? 3e3 + rand() * 600 : 900 + rand() * 500;
|
|
17342
|
+
meteors.push({
|
|
17343
|
+
x: Math.floor(rand() * xMax),
|
|
17344
|
+
y: yMin + Math.floor(rand() * ySpan),
|
|
17345
|
+
length,
|
|
17346
|
+
phase: i === 0 ? 0 : i * (MIN_METEOR_START_GAP_MS + 80) + rand() * 60,
|
|
17347
|
+
period,
|
|
17348
|
+
duration
|
|
17349
|
+
});
|
|
17350
|
+
}
|
|
17351
|
+
return meteors;
|
|
17352
|
+
}
|
|
17353
|
+
function getMeteorTrail(meteor, now) {
|
|
17354
|
+
const cycleTime = ((now - meteor.phase) % meteor.period + meteor.period) % meteor.period;
|
|
17355
|
+
if (cycleTime >= meteor.duration) return [];
|
|
17356
|
+
const step = Math.floor(cycleTime / 120);
|
|
17357
|
+
const cells = [];
|
|
17358
|
+
for (let i = 0; i < meteor.length; i++) cells.push({
|
|
17359
|
+
x: meteor.x - step + i,
|
|
17360
|
+
y: meteor.y + step - i,
|
|
17361
|
+
state: i === 0 ? "bright" : "dim",
|
|
17362
|
+
char: "╱"
|
|
17363
|
+
});
|
|
17364
|
+
return cells;
|
|
17365
|
+
}
|
|
17194
17366
|
//#endregion
|
|
17195
17367
|
//#region src/utils/moon.ts
|
|
17196
17368
|
const MOON_PHASES = [
|
|
@@ -17414,6 +17586,8 @@ const CONTENT_WIDTH = 63;
|
|
|
17414
17586
|
const MAX_PROMPT_LINES = 3;
|
|
17415
17587
|
const BASE_CONTENT_ROWS = 24;
|
|
17416
17588
|
const STAR_DENSITY = .035;
|
|
17589
|
+
const DEFAULT_METEOR_FREQUENCY = 3;
|
|
17590
|
+
const METEOR_SEED_OFFSET = 101;
|
|
17417
17591
|
const TICK_MS = 200;
|
|
17418
17592
|
const MOONS_PER_ROW = 30;
|
|
17419
17593
|
const MOON_PHASE_PERIOD = 1600;
|
|
@@ -17517,6 +17691,17 @@ function starStyle(state) {
|
|
|
17517
17691
|
if (state === "dim") return "dim";
|
|
17518
17692
|
return "normal";
|
|
17519
17693
|
}
|
|
17694
|
+
function meteorCountForFrequency(frequency) {
|
|
17695
|
+
if (frequency <= 0) return 0;
|
|
17696
|
+
if (frequency === 1) return 1;
|
|
17697
|
+
if (frequency === 2) return 2;
|
|
17698
|
+
if (frequency === 3) return 4;
|
|
17699
|
+
if (frequency === 4) return 6;
|
|
17700
|
+
return 28;
|
|
17701
|
+
}
|
|
17702
|
+
function meteorsStartingBefore(meteors, rowOffset, maxStartRow) {
|
|
17703
|
+
return meteors.filter((meteor) => rowOffset + meteor.y < maxStartRow);
|
|
17704
|
+
}
|
|
17520
17705
|
function placeStarsInCells(cells, stars, row, xMin, xMax, xOffset, now) {
|
|
17521
17706
|
for (const star of stars) {
|
|
17522
17707
|
if (star.y !== row || star.x < xMin || star.x >= xMax) continue;
|
|
@@ -17533,15 +17718,28 @@ function placeStarsInCells(cells, stars, row, xMin, xMax, xOffset, now) {
|
|
|
17533
17718
|
};
|
|
17534
17719
|
}
|
|
17535
17720
|
}
|
|
17536
|
-
function
|
|
17721
|
+
function placeMeteorsInCells(cells, meteors, row, xMin, xMax, xOffset, now) {
|
|
17722
|
+
for (const meteor of meteors) for (const trail of getMeteorTrail(meteor, now)) {
|
|
17723
|
+
if (trail.y !== row || trail.x < xMin || trail.x >= xMax) continue;
|
|
17724
|
+
const localX = trail.x - xOffset;
|
|
17725
|
+
cells[localX] = {
|
|
17726
|
+
char: trail.char,
|
|
17727
|
+
style: trail.state === "bright" ? "bold" : "dim",
|
|
17728
|
+
width: 1
|
|
17729
|
+
};
|
|
17730
|
+
}
|
|
17731
|
+
}
|
|
17732
|
+
function renderStarLineCells(stars, meteors, width, y, now) {
|
|
17537
17733
|
const cells = emptyCells(width);
|
|
17538
17734
|
placeStarsInCells(cells, stars, y, 0, width, 0, now);
|
|
17735
|
+
placeMeteorsInCells(cells, meteors, y, 0, width, 0, now);
|
|
17539
17736
|
return cells;
|
|
17540
17737
|
}
|
|
17541
|
-
function renderSideStarsCells(stars, rowIndex, xOffset, sideWidth, now) {
|
|
17738
|
+
function renderSideStarsCells(stars, meteors, rowIndex, xOffset, sideWidth, now) {
|
|
17542
17739
|
if (sideWidth <= 0) return [];
|
|
17543
17740
|
const cells = emptyCells(sideWidth);
|
|
17544
17741
|
placeStarsInCells(cells, stars, rowIndex, xOffset, xOffset + sideWidth, xOffset, now);
|
|
17742
|
+
placeMeteorsInCells(cells, meteors, rowIndex, xOffset, xOffset + sideWidth, xOffset, now);
|
|
17545
17743
|
return cells;
|
|
17546
17744
|
}
|
|
17547
17745
|
function clampCellsToWidth(content, width) {
|
|
@@ -17658,7 +17856,7 @@ function buildContentCells(prompt, agentName, state, elapsed, now, availableHeig
|
|
|
17658
17856
|
}
|
|
17659
17857
|
return rows;
|
|
17660
17858
|
}
|
|
17661
|
-
function buildFrameCells(prompt, agentName, state, topStars, bottomStars, sideStars, now, terminalWidth, terminalHeight) {
|
|
17859
|
+
function buildFrameCells(prompt, agentName, state, topStars, bottomStars, sideStars, now, terminalWidth, terminalHeight, topMeteors = [], bottomMeteors = [], sideMeteors = []) {
|
|
17662
17860
|
const elapsed = formatElapsed(now - state.startTime.getTime());
|
|
17663
17861
|
const availableHeight = Math.max(0, terminalHeight - 2);
|
|
17664
17862
|
const contentRows = buildContentCells(prompt, agentName, state, elapsed, now, availableHeight);
|
|
@@ -17667,20 +17865,24 @@ function buildFrameCells(prompt, agentName, state, topStars, bottomStars, sideSt
|
|
|
17667
17865
|
const remaining = Math.max(0, availableHeight - contentCount);
|
|
17668
17866
|
const topHeight = Math.max(0, Math.ceil(remaining / 2));
|
|
17669
17867
|
const bottomHeight = remaining - topHeight;
|
|
17868
|
+
const maxMeteorStartRow = Math.ceil(availableHeight * .75);
|
|
17869
|
+
const visibleTopMeteors = meteorsStartingBefore(topMeteors, 0, maxMeteorStartRow);
|
|
17870
|
+
const visibleSideMeteors = meteorsStartingBefore(sideMeteors, topHeight, maxMeteorStartRow);
|
|
17871
|
+
const visibleBottomMeteors = meteorsStartingBefore(bottomMeteors, topHeight + contentCount, maxMeteorStartRow);
|
|
17670
17872
|
const sideWidth = Math.max(0, Math.floor((terminalWidth - CONTENT_WIDTH) / 2));
|
|
17671
17873
|
const frame = [];
|
|
17672
|
-
for (let y = 0; y < topHeight; y++) frame.push(renderStarLineCells(topStars, terminalWidth, y, now));
|
|
17874
|
+
for (let y = 0; y < topHeight; y++) frame.push(renderStarLineCells(topStars, visibleTopMeteors, terminalWidth, y, now));
|
|
17673
17875
|
for (let i = 0; i < contentRows.length; i++) {
|
|
17674
|
-
const left = renderSideStarsCells(sideStars, i, 0, sideWidth, now);
|
|
17876
|
+
const left = renderSideStarsCells(sideStars, visibleSideMeteors, i, 0, sideWidth, now);
|
|
17675
17877
|
const center = centerLineCells(contentRows[i], CONTENT_WIDTH);
|
|
17676
|
-
const right = renderSideStarsCells(sideStars, i, terminalWidth - sideWidth, sideWidth, now);
|
|
17878
|
+
const right = renderSideStarsCells(sideStars, visibleSideMeteors, i, terminalWidth - sideWidth, sideWidth, now);
|
|
17677
17879
|
frame.push([
|
|
17678
17880
|
...left,
|
|
17679
17881
|
...center,
|
|
17680
17882
|
...right
|
|
17681
17883
|
]);
|
|
17682
17884
|
}
|
|
17683
|
-
for (let y = 0; y < bottomHeight; y++) frame.push(renderStarLineCells(bottomStars, terminalWidth, y, now));
|
|
17885
|
+
for (let y = 0; y < bottomHeight; y++) frame.push(renderStarLineCells(bottomStars, visibleBottomMeteors, terminalWidth, y, now));
|
|
17684
17886
|
frame.push(renderResumeHintCells(terminalWidth, state.interruptHint));
|
|
17685
17887
|
frame.push(emptyCells(terminalWidth));
|
|
17686
17888
|
return frame;
|
|
@@ -17696,8 +17898,11 @@ var Renderer = class {
|
|
|
17696
17898
|
topStars = [];
|
|
17697
17899
|
bottomStars = [];
|
|
17698
17900
|
sideStars = [];
|
|
17901
|
+
topMeteors = [];
|
|
17902
|
+
bottomMeteors = [];
|
|
17699
17903
|
cachedWidth = 0;
|
|
17700
17904
|
cachedHeight = 0;
|
|
17905
|
+
meteorFrequency;
|
|
17701
17906
|
prevCells = [];
|
|
17702
17907
|
prevTitle = null;
|
|
17703
17908
|
titleSaved = false;
|
|
@@ -17716,11 +17921,12 @@ var Renderer = class {
|
|
|
17716
17921
|
handleStopped = () => {
|
|
17717
17922
|
this.stop("stopped");
|
|
17718
17923
|
};
|
|
17719
|
-
constructor(orchestrator, prompt, agentName, onInterrupt) {
|
|
17924
|
+
constructor(orchestrator, prompt, agentName, onInterrupt, options = {}) {
|
|
17720
17925
|
this.orchestrator = orchestrator;
|
|
17721
17926
|
this.prompt = prompt;
|
|
17722
17927
|
this.agentName = agentName;
|
|
17723
17928
|
this.onInterrupt = onInterrupt;
|
|
17929
|
+
this.meteorFrequency = Math.max(0, Math.floor(options.meteorFrequency ?? DEFAULT_METEOR_FREQUENCY));
|
|
17724
17930
|
this.state = orchestrator.getState();
|
|
17725
17931
|
this.seedTop = Math.floor(Math.random() * 2147483646) + 1;
|
|
17726
17932
|
this.seedBottom = Math.floor(Math.random() * 2147483646) + 1;
|
|
@@ -17773,6 +17979,7 @@ var Renderer = class {
|
|
|
17773
17979
|
const availableHeight = Math.max(0, h - 2);
|
|
17774
17980
|
const remaining = Math.max(0, availableHeight - BASE_CONTENT_ROWS);
|
|
17775
17981
|
const topHeight = Math.max(0, Math.ceil(remaining / 2));
|
|
17982
|
+
const bottomHeight = Math.max(0, remaining - topHeight);
|
|
17776
17983
|
const proximityRows = 8;
|
|
17777
17984
|
const shrinkBig = (s, nearContentRow) => {
|
|
17778
17985
|
if (!nearContentRow || s.x < contentStart || s.x >= contentEnd) return s;
|
|
@@ -17788,6 +17995,8 @@ var Renderer = class {
|
|
|
17788
17995
|
this.topStars = generateStarField(w, h, STAR_DENSITY, this.seedTop).map((s) => shrinkBig(s, s.y >= topHeight - proximityRows));
|
|
17789
17996
|
this.bottomStars = generateStarField(w, h, STAR_DENSITY, this.seedBottom).map((s) => shrinkBig(s, s.y < proximityRows));
|
|
17790
17997
|
this.sideStars = generateStarField(w, Math.max(BASE_CONTENT_ROWS, availableHeight), STAR_DENSITY, this.seedSide);
|
|
17998
|
+
this.topMeteors = generateMeteorShower(w, topHeight, topHeight > 0 ? meteorCountForFrequency(this.meteorFrequency) : 0, this.seedTop + METEOR_SEED_OFFSET);
|
|
17999
|
+
this.bottomMeteors = generateMeteorShower(w, bottomHeight, bottomHeight > 0 ? meteorCountForFrequency(this.meteorFrequency) : 0, this.seedBottom + METEOR_SEED_OFFSET);
|
|
17791
18000
|
return true;
|
|
17792
18001
|
}
|
|
17793
18002
|
return false;
|
|
@@ -17798,7 +18007,7 @@ var Renderer = class {
|
|
|
17798
18007
|
const h = process$1.stdout.rows || 24;
|
|
17799
18008
|
const resized = this.ensureStarFields(w, h);
|
|
17800
18009
|
this.updateTerminalTitle(now);
|
|
17801
|
-
const nextCells = buildFrameCells(this.prompt, this.agentName, this.state, this.topStars, this.bottomStars, this.sideStars, now, w, h);
|
|
18010
|
+
const nextCells = buildFrameCells(this.prompt, this.agentName, this.state, this.topStars, this.bottomStars, this.sideStars, now, w, h, this.topMeteors, this.bottomMeteors);
|
|
17802
18011
|
if (this.isFirstFrame || resized) {
|
|
17803
18012
|
process$1.stdout.write("\x1B[H" + nextCells.map(rowToString).join("\n"));
|
|
17804
18013
|
this.isFirstFrame = false;
|
|
@@ -17829,6 +18038,7 @@ function slugifyPrompt(prompt) {
|
|
|
17829
18038
|
//#region src/cli.ts
|
|
17830
18039
|
const packageVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8")).version;
|
|
17831
18040
|
const FORCE_EXIT_TIMEOUT_MS = 5e3;
|
|
18041
|
+
const MAX_METEOR_FREQUENCY = 5;
|
|
17832
18042
|
const GNHF_REEXEC_STDIN_PROMPT = "GNHF_REEXEC_STDIN_PROMPT";
|
|
17833
18043
|
const GNHF_REEXEC_STDIN_PROMPT_FILE = "GNHF_REEXEC_STDIN_PROMPT_FILE";
|
|
17834
18044
|
const GNHF_REEXEC_STDIN_PROMPT_DIR_PREFIX = "gnhf-stdin-";
|
|
@@ -17847,6 +18057,11 @@ function parseNonNegativeInteger(value) {
|
|
|
17847
18057
|
if (!Number.isSafeInteger(parsed)) throw new InvalidArgumentError("must be a safe integer");
|
|
17848
18058
|
return parsed;
|
|
17849
18059
|
}
|
|
18060
|
+
function parseMeteorFrequency(value) {
|
|
18061
|
+
const parsed = parseNonNegativeInteger(value);
|
|
18062
|
+
if (parsed > MAX_METEOR_FREQUENCY) throw new InvalidArgumentError(`must be between 0 and ${MAX_METEOR_FREQUENCY}`);
|
|
18063
|
+
return parsed;
|
|
18064
|
+
}
|
|
17850
18065
|
function parseOnOffBoolean(value) {
|
|
17851
18066
|
if (value === "on" || value === "true") return true;
|
|
17852
18067
|
if (value === "off" || value === "false") return false;
|
|
@@ -17923,6 +18138,11 @@ function initializeNewBranch(prompt, cwd, schemaOptions) {
|
|
|
17923
18138
|
const runId = createBranchWithSuffix(slugifyPrompt(prompt), cwd).split("/")[1];
|
|
17924
18139
|
return setupRun(runId, prompt, baseCommit, cwd, schemaOptions);
|
|
17925
18140
|
}
|
|
18141
|
+
function initializeCurrentBranchRun(prompt, cwd, schemaOptions) {
|
|
18142
|
+
ensureCleanWorkingTree(cwd);
|
|
18143
|
+
const baseCommit = getHeadCommit(cwd);
|
|
18144
|
+
return setupRun(createRunIdWithSuffix(slugifyPrompt(prompt).split("/")[1], cwd), prompt, baseCommit, cwd, schemaOptions);
|
|
18145
|
+
}
|
|
17926
18146
|
function branchNameWithSuffix(branchName, suffix) {
|
|
17927
18147
|
return suffix === 0 ? branchName : `${branchName}-${suffix}`;
|
|
17928
18148
|
}
|
|
@@ -17942,6 +18162,16 @@ function createBranchWithSuffix(branchName, cwd) {
|
|
|
17942
18162
|
}
|
|
17943
18163
|
throw new Error(`Unable to create a unique branch name for ${branchName}`);
|
|
17944
18164
|
}
|
|
18165
|
+
function runIdWithSuffix(runId, suffix) {
|
|
18166
|
+
return suffix === 0 ? runId : `${runId}-${suffix}`;
|
|
18167
|
+
}
|
|
18168
|
+
function createRunIdWithSuffix(runId, cwd) {
|
|
18169
|
+
for (let suffix = 0; suffix < 100; suffix += 1) {
|
|
18170
|
+
const candidate = runIdWithSuffix(runId, suffix);
|
|
18171
|
+
if (!existsSync(join(cwd, ".gnhf", "runs", candidate))) return candidate;
|
|
18172
|
+
}
|
|
18173
|
+
throw new Error(`Unable to create a unique run id for ${runId}`);
|
|
18174
|
+
}
|
|
17945
18175
|
function initializeWorktreeRun(prompt, cwd, schemaOptions, resumeSchemaOptions) {
|
|
17946
18176
|
const repoRoot = getRepoRootDir(cwd);
|
|
17947
18177
|
const baseCommit = getHeadCommit(cwd);
|
|
@@ -18119,13 +18349,13 @@ function readReexecStdinPrompt(env) {
|
|
|
18119
18349
|
}
|
|
18120
18350
|
}
|
|
18121
18351
|
const program = new Command();
|
|
18122
|
-
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) => {
|
|
18352
|
+
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("--meteor-frequency <n>", "Meteor frequency from 0 to 5 (0 disables, 3 is default)", parseMeteorFrequency, 3).option("--mock", "", false).action(async (promptArg, options) => {
|
|
18123
18353
|
if (options.mock) {
|
|
18124
18354
|
const mock = new MockOrchestrator();
|
|
18125
18355
|
enterAltScreen();
|
|
18126
|
-
const renderer = new Renderer(mock, "let's minimize app startup latency without sacrificing any functionality", "
|
|
18356
|
+
const renderer = new Renderer(mock, "let's minimize app startup latency without sacrificing any functionality", "codex", () => {
|
|
18127
18357
|
mock.handleInterrupt();
|
|
18128
|
-
});
|
|
18358
|
+
}, { meteorFrequency: options.meteorFrequency });
|
|
18129
18359
|
renderer.start();
|
|
18130
18360
|
mock.start();
|
|
18131
18361
|
await renderer.waitUntilExit();
|
|
@@ -18168,6 +18398,10 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
18168
18398
|
let worktreeCleanup = null;
|
|
18169
18399
|
const currentBranch = getCurrentBranch(cwd);
|
|
18170
18400
|
const onGnhfBranch = currentBranch.startsWith("gnhf/");
|
|
18401
|
+
if (options.currentBranch && options.worktree) {
|
|
18402
|
+
console.error("Cannot combine --current-branch and --worktree.");
|
|
18403
|
+
process$1.exit(1);
|
|
18404
|
+
}
|
|
18171
18405
|
const cliStopWhen = options.stopWhen === "" ? void 0 : options.stopWhen;
|
|
18172
18406
|
let effectiveStopWhen = cliStopWhen;
|
|
18173
18407
|
let effectiveCommitMessage = config.commitMessage;
|
|
@@ -18204,6 +18438,12 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
18204
18438
|
if (worktreeCleanup === exitCleanup) exitCleanup();
|
|
18205
18439
|
});
|
|
18206
18440
|
}
|
|
18441
|
+
} else if (options.currentBranch) {
|
|
18442
|
+
if (!prompt) {
|
|
18443
|
+
program.help();
|
|
18444
|
+
return;
|
|
18445
|
+
}
|
|
18446
|
+
runInfo = initializeCurrentBranchRun(prompt, cwd, schemaOptions);
|
|
18207
18447
|
} else if (onGnhfBranch) {
|
|
18208
18448
|
const existingRunId = currentBranch.slice(5);
|
|
18209
18449
|
const existingMetadata = peekRunMetadata(existingRunId, cwd);
|
|
@@ -18260,7 +18500,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
18260
18500
|
if (!reexeced) persistedPrompt?.cleanup();
|
|
18261
18501
|
}
|
|
18262
18502
|
}
|
|
18263
|
-
const runMode = options.worktree ? "worktree" : startIteration > 0 ? "resume" : "new";
|
|
18503
|
+
const runMode = options.worktree ? "worktree" : options.currentBranch ? "current-branch" : startIteration > 0 ? "resume" : "new";
|
|
18264
18504
|
const telemetryAgent = getTelemetryAgent(config.agent);
|
|
18265
18505
|
telemetry.pageview("/run", {
|
|
18266
18506
|
agent: telemetryAgent,
|
|
@@ -18283,6 +18523,8 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
18283
18523
|
agentArgsOverride: getNativeAgentName(config.agent) ? config.agentArgsOverride?.[getNativeAgentName(config.agent)] : void 0,
|
|
18284
18524
|
worktree: options.worktree,
|
|
18285
18525
|
worktreePath,
|
|
18526
|
+
currentBranch: options.currentBranch,
|
|
18527
|
+
push: options.push,
|
|
18286
18528
|
platform: process$1.platform,
|
|
18287
18529
|
nodeVersion: process$1.version,
|
|
18288
18530
|
gnhfVersion: packageVersion
|
|
@@ -18298,7 +18540,8 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
18298
18540
|
}, agent, runInfo, prompt, effectiveCwd, startIteration, {
|
|
18299
18541
|
maxIterations: options.maxIterations,
|
|
18300
18542
|
maxTokens: options.maxTokens,
|
|
18301
|
-
stopWhen: effectiveStopWhen
|
|
18543
|
+
stopWhen: effectiveStopWhen,
|
|
18544
|
+
...options.push ? { push: true } : {}
|
|
18302
18545
|
});
|
|
18303
18546
|
let shutdownSignal = null;
|
|
18304
18547
|
let forceShutdownRequested = false;
|
|
@@ -18329,7 +18572,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
18329
18572
|
requestForceShutdown("SIGTERM");
|
|
18330
18573
|
};
|
|
18331
18574
|
enterAltScreen();
|
|
18332
|
-
const renderer = new Renderer(orchestrator, prompt, config.agent, handleSigInt);
|
|
18575
|
+
const renderer = new Renderer(orchestrator, prompt, config.agent, handleSigInt, { meteorFrequency: options.meteorFrequency });
|
|
18333
18576
|
renderer.start();
|
|
18334
18577
|
process$1.on("SIGINT", handleSigInt);
|
|
18335
18578
|
process$1.on("SIGTERM", handleSigTerm);
|
|
@@ -18390,7 +18633,8 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
18390
18633
|
baseRef: runInfo.baseCommit.slice(0, 12) || runInfo.baseCommit,
|
|
18391
18634
|
diffStats,
|
|
18392
18635
|
color: shouldUseColor(),
|
|
18393
|
-
terminalColumns: process$1.stdout.columns
|
|
18636
|
+
terminalColumns: process$1.stdout.columns,
|
|
18637
|
+
hasPendingCommitFailure: finalState.hasPendingCommitFailure
|
|
18394
18638
|
});
|
|
18395
18639
|
appendDebugLog("run:complete", {
|
|
18396
18640
|
signal: shutdownSignal,
|
|
@@ -18416,12 +18660,13 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
18416
18660
|
total_output_tokens: finalState.totalOutputTokens,
|
|
18417
18661
|
duration_ms: Date.now() - runStartedAt,
|
|
18418
18662
|
prevent_sleep: config.preventSleep === true,
|
|
18663
|
+
push_each_iteration: options.push === true,
|
|
18419
18664
|
commit_message_preset: effectiveCommitMessage?.preset ?? "default",
|
|
18420
18665
|
stop_when_set: effectiveStopWhen !== void 0
|
|
18421
18666
|
});
|
|
18422
18667
|
await telemetry.close(1e3);
|
|
18423
18668
|
if (finalState.status === "aborted") console.error(`\n gnhf: Run log: ${runInfo.logPath}\n`);
|
|
18424
|
-
if (worktreePath) if (finalState.commitCount > 0) {
|
|
18669
|
+
if (worktreePath) if (finalState.commitCount > 0 || finalState.hasPendingCommitFailure) {
|
|
18425
18670
|
worktreeCleanup = null;
|
|
18426
18671
|
console.error(`\n gnhf: worktree preserved at ${worktreePath}\n gnhf: merge the branch and remove with: git worktree remove "${worktreePath}"\n`);
|
|
18427
18672
|
} else {
|