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.
Files changed (3) hide show
  1. package/README.md +27 -9
  2. package/dist/cli.mjs +198 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -45,7 +45,7 @@ gnhf is a [ralph](https://ghuntley.com/ralph/), [autoresearch](https://github.co
45
45
  You wake up to a branch full of clean work and a log of everything that happened.
46
46
 
47
47
  - **Dead simple** — one command starts an autonomous loop that runs until you 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 gnhf/ branch │
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 │ │ git reset │ │
125
- │ append │ │ --hard │ │
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** - 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. Complete no-op iterations are reported as failures and count toward the consecutive-failure abort limit.
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 in either case, and in the interactive TUI the final state remains visible until you press Ctrl+C to exit
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 the loop when the agent reports this condition; persists across resume | unlimited |
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
- "-c",
569
- "commit.gpgsign=false",
570
- "-c",
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 iterationPrompt = buildIterationPrompt({
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) return {
16663
- type: "completed",
16664
- record: this.recordSuccess(result.output),
16665
- shouldFullyStop
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
- appendNotes(this.runInfo.notesPath, this.state.currentIteration, output.summary, toStringArray(output.key_changes_made), toStringArray(output.key_learnings));
16723
- commitAll(buildCommitMessage(this.config.commitMessage, output, { iteration: this.state.currentIteration }), this.cwd);
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: toStringArray(output.key_changes_made),
16734
- keyLearnings: toStringArray(output.key_learnings),
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 rolledBack = `${options.failCount} rolled back`;
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(rolledBack) : s.yellow(rolledBack)
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnhf",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "Before I go to bed, I tell my agents: good night, have fun",
5
5
  "type": "module",
6
6
  "bin": {