gnhf 0.1.35 → 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 +188 -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; if the first commit attempt fails, gnhf re-stages changes and retries with `--no-verify` so hook-mutated work is not stranded
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,
@@ -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(commitArgs, cwd);
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
- firstError = error;
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
- git(["add", "-A"], cwd);
620
+ }
621
+ function pushCurrentBranch(cwd) {
622
+ let hasUpstream = true;
583
623
  try {
584
- git([...commitArgs, "--no-verify"], cwd);
585
- appendDebugLog("git:commit:no-verify-fallback", { firstError: serializeError(firstError) });
586
- } catch {}
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 iterationPrompt = buildIterationPrompt({
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) return {
16673
- type: "completed",
16674
- record: this.recordSuccess(result.output),
16675
- shouldFullyStop
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
- appendNotes(this.runInfo.notesPath, this.state.currentIteration, output.summary, toStringArray(output.key_changes_made), toStringArray(output.key_learnings));
16733
- 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);
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: toStringArray(output.key_changes_made),
16744
- keyLearnings: toStringArray(output.key_learnings),
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 rolledBack = `${options.failCount} rolled back`;
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(rolledBack) : s.yellow(rolledBack)
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`)),
@@ -17923,6 +18053,11 @@ function initializeNewBranch(prompt, cwd, schemaOptions) {
17923
18053
  const runId = createBranchWithSuffix(slugifyPrompt(prompt), cwd).split("/")[1];
17924
18054
  return setupRun(runId, prompt, baseCommit, cwd, schemaOptions);
17925
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
+ }
17926
18061
  function branchNameWithSuffix(branchName, suffix) {
17927
18062
  return suffix === 0 ? branchName : `${branchName}-${suffix}`;
17928
18063
  }
@@ -17942,6 +18077,16 @@ function createBranchWithSuffix(branchName, cwd) {
17942
18077
  }
17943
18078
  throw new Error(`Unable to create a unique branch name for ${branchName}`);
17944
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
+ }
17945
18090
  function initializeWorktreeRun(prompt, cwd, schemaOptions, resumeSchemaOptions) {
17946
18091
  const repoRoot = getRepoRootDir(cwd);
17947
18092
  const baseCommit = getHeadCommit(cwd);
@@ -18119,7 +18264,7 @@ function readReexecStdinPrompt(env) {
18119
18264
  }
18120
18265
  }
18121
18266
  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) => {
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) => {
18123
18268
  if (options.mock) {
18124
18269
  const mock = new MockOrchestrator();
18125
18270
  enterAltScreen();
@@ -18168,6 +18313,10 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
18168
18313
  let worktreeCleanup = null;
18169
18314
  const currentBranch = getCurrentBranch(cwd);
18170
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
+ }
18171
18320
  const cliStopWhen = options.stopWhen === "" ? void 0 : options.stopWhen;
18172
18321
  let effectiveStopWhen = cliStopWhen;
18173
18322
  let effectiveCommitMessage = config.commitMessage;
@@ -18204,6 +18353,12 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
18204
18353
  if (worktreeCleanup === exitCleanup) exitCleanup();
18205
18354
  });
18206
18355
  }
18356
+ } else if (options.currentBranch) {
18357
+ if (!prompt) {
18358
+ program.help();
18359
+ return;
18360
+ }
18361
+ runInfo = initializeCurrentBranchRun(prompt, cwd, schemaOptions);
18207
18362
  } else if (onGnhfBranch) {
18208
18363
  const existingRunId = currentBranch.slice(5);
18209
18364
  const existingMetadata = peekRunMetadata(existingRunId, cwd);
@@ -18260,7 +18415,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
18260
18415
  if (!reexeced) persistedPrompt?.cleanup();
18261
18416
  }
18262
18417
  }
18263
- const runMode = options.worktree ? "worktree" : startIteration > 0 ? "resume" : "new";
18418
+ const runMode = options.worktree ? "worktree" : options.currentBranch ? "current-branch" : startIteration > 0 ? "resume" : "new";
18264
18419
  const telemetryAgent = getTelemetryAgent(config.agent);
18265
18420
  telemetry.pageview("/run", {
18266
18421
  agent: telemetryAgent,
@@ -18283,6 +18438,8 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
18283
18438
  agentArgsOverride: getNativeAgentName(config.agent) ? config.agentArgsOverride?.[getNativeAgentName(config.agent)] : void 0,
18284
18439
  worktree: options.worktree,
18285
18440
  worktreePath,
18441
+ currentBranch: options.currentBranch,
18442
+ push: options.push,
18286
18443
  platform: process$1.platform,
18287
18444
  nodeVersion: process$1.version,
18288
18445
  gnhfVersion: packageVersion
@@ -18298,7 +18455,8 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
18298
18455
  }, agent, runInfo, prompt, effectiveCwd, startIteration, {
18299
18456
  maxIterations: options.maxIterations,
18300
18457
  maxTokens: options.maxTokens,
18301
- stopWhen: effectiveStopWhen
18458
+ stopWhen: effectiveStopWhen,
18459
+ ...options.push ? { push: true } : {}
18302
18460
  });
18303
18461
  let shutdownSignal = null;
18304
18462
  let forceShutdownRequested = false;
@@ -18390,7 +18548,8 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
18390
18548
  baseRef: runInfo.baseCommit.slice(0, 12) || runInfo.baseCommit,
18391
18549
  diffStats,
18392
18550
  color: shouldUseColor(),
18393
- terminalColumns: process$1.stdout.columns
18551
+ terminalColumns: process$1.stdout.columns,
18552
+ hasPendingCommitFailure: finalState.hasPendingCommitFailure
18394
18553
  });
18395
18554
  appendDebugLog("run:complete", {
18396
18555
  signal: shutdownSignal,
@@ -18416,12 +18575,13 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
18416
18575
  total_output_tokens: finalState.totalOutputTokens,
18417
18576
  duration_ms: Date.now() - runStartedAt,
18418
18577
  prevent_sleep: config.preventSleep === true,
18578
+ push_each_iteration: options.push === true,
18419
18579
  commit_message_preset: effectiveCommitMessage?.preset ?? "default",
18420
18580
  stop_when_set: effectiveStopWhen !== void 0
18421
18581
  });
18422
18582
  await telemetry.close(1e3);
18423
18583
  if (finalState.status === "aborted") console.error(`\n gnhf: Run log: ${runInfo.logPath}\n`);
18424
- if (worktreePath) if (finalState.commitCount > 0) {
18584
+ if (worktreePath) if (finalState.commitCount > 0 || finalState.hasPendingCommitFailure) {
18425
18585
  worktreeCleanup = null;
18426
18586
  console.error(`\n gnhf: worktree preserved at ${worktreePath}\n gnhf: merge the branch and remove with: git worktree remove "${worktreePath}"\n`);
18427
18587
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnhf",
3
- "version": "0.1.35",
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": {