gnhf 0.1.17 → 0.1.19

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 +23 -0
  2. package/dist/cli.mjs +72 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -62,6 +62,13 @@ $ gnhf "reduce complexity of the codebase without changing functionality" \
62
62
  # have a good nap
63
63
  ```
64
64
 
65
+ ```sh
66
+ # Run multiple agents on the same repo simultaneously using worktrees
67
+ $ gnhf --worktree "implement feature X" &
68
+ $ gnhf --worktree "add tests for module Y" &
69
+ $ gnhf --worktree "refactor the API layer" &
70
+ ```
71
+
65
72
  Run `gnhf` from inside a Git repository with a clean working tree. If you are starting from a plain directory, run `git init` first.
66
73
  `gnhf` supports macOS, Linux, and Windows.
67
74
 
@@ -133,6 +140,21 @@ npm link
133
140
  - **Local run metadata** — gnhf stores prompt, notes, and resume metadata under `.gnhf/runs/` and ignores it locally, so your branch only contains intentional work
134
141
  - **Resume support** — run `gnhf` while on an existing `gnhf/` branch to pick up where a previous run left off
135
142
 
143
+ ### Worktree Mode
144
+
145
+ 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.
146
+
147
+ ```
148
+ <repo>/ ← your repo (unchanged)
149
+ <repo>-gnhf-worktrees/
150
+ ├── <run-slug-1>/ ← worktree for agent 1
151
+ └── <run-slug-2>/ ← worktree for agent 2
152
+ ```
153
+
154
+ - 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.
155
+ - Worktrees with **no commits** are automatically removed on exit.
156
+ - `--worktree` must be run from a non-gnhf branch (typically `main`).
157
+
136
158
  ## CLI Reference
137
159
 
138
160
  | Command | Description |
@@ -150,6 +172,7 @@ npm link
150
172
  | `--max-iterations <n>` | Abort after `n` total iterations | unlimited |
151
173
  | `--max-tokens <n>` | Abort after `n` total input+output tokens | unlimited |
152
174
  | `--prevent-sleep <mode>` | Prevent system sleep during the run (`on`/`off` or `true`/`false`) | config file (`on`) |
175
+ | `--worktree` | Run in a separate git worktree (enables multiple agents concurrently) | `false` |
153
176
  | `--version` | Show version | |
154
177
 
155
178
  ## Configuration
package/dist/cli.mjs CHANGED
@@ -313,6 +313,10 @@ function git(args, cwd) {
313
313
  throw translateGitError(error);
314
314
  }
315
315
  }
316
+ /** Wrap a value in single quotes, escaping embedded single quotes for POSIX shells. */
317
+ function shellEscape(value) {
318
+ return `'${value.replace(/'/g, "'\\''")}'`;
319
+ }
316
320
  function isGitRepository(cwd) {
317
321
  try {
318
322
  execSync("git rev-parse --git-dir", {
@@ -378,6 +382,15 @@ function resetHard(cwd) {
378
382
  git("reset --hard HEAD", cwd);
379
383
  git("clean -fd", cwd);
380
384
  }
385
+ function getRepoRootDir(cwd) {
386
+ return git("rev-parse --show-toplevel", cwd);
387
+ }
388
+ function createWorktree(baseCwd, worktreePath, branchName) {
389
+ git(`worktree add -b ${shellEscape(branchName)} ${shellEscape(worktreePath)}`, baseCwd);
390
+ }
391
+ function removeWorktree(baseCwd, worktreePath) {
392
+ git(`worktree remove --force ${shellEscape(worktreePath)}`, baseCwd);
393
+ }
381
394
  //#endregion
382
395
  //#region src/core/agents/types.ts
383
396
  const AGENT_OUTPUT_SCHEMA = {
@@ -2885,6 +2898,12 @@ var Orchestrator = class extends EventEmitter {
2885
2898
  }
2886
2899
  }
2887
2900
  }
2901
+ } catch (err) {
2902
+ appendDebugLog("orchestrator:loop-error", {
2903
+ iteration: this.state.currentIteration,
2904
+ error: serializeError(err)
2905
+ });
2906
+ throw err;
2888
2907
  } finally {
2889
2908
  this.activeIterationPromise = null;
2890
2909
  if (this.stopPromise) await this.stopPromise;
@@ -3856,6 +3875,19 @@ function initializeNewBranch(prompt, cwd) {
3856
3875
  const runId = branchName.split("/")[1];
3857
3876
  return setupRun(runId, prompt, baseCommit, cwd);
3858
3877
  }
3878
+ function initializeWorktreeRun(prompt, cwd) {
3879
+ const repoRoot = getRepoRootDir(cwd);
3880
+ const baseCommit = getHeadCommit(cwd);
3881
+ const branchName = slugifyPrompt(prompt);
3882
+ const runId = branchName.split("/")[1];
3883
+ const worktreePath = join(dirname(repoRoot), `${basename(repoRoot)}-gnhf-worktrees`, runId);
3884
+ createWorktree(repoRoot, worktreePath, branchName);
3885
+ return {
3886
+ runInfo: setupRun(runId, prompt, baseCommit, worktreePath),
3887
+ worktreePath,
3888
+ effectiveCwd: worktreePath
3889
+ };
3890
+ }
3859
3891
  function ask(question) {
3860
3892
  const rl = createInterface({
3861
3893
  input: process$1.stdin,
@@ -3918,7 +3950,7 @@ function readReexecStdinPrompt(env) {
3918
3950
  }
3919
3951
  }
3920
3952
  const program = new Command();
3921
- 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 (claude, codex, rovodev, or opencode)").option("--max-iterations <n>", "Abort after N total iterations", parseNonNegativeInteger).option("--max-tokens <n>", "Abort after N total input+output tokens", parseNonNegativeInteger).option("--prevent-sleep <mode>", "Prevent system sleep during the run (\"on\" or \"off\")", parseOnOffBoolean).option("--mock", "", false).action(async (promptArg, options) => {
3953
+ 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 (claude, codex, rovodev, or opencode)").option("--max-iterations <n>", "Abort after N total iterations", parseNonNegativeInteger).option("--max-tokens <n>", "Abort after N total input+output tokens", parseNonNegativeInteger).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) => {
3922
3954
  if (options.mock) {
3923
3955
  const mock = new MockOrchestrator();
3924
3956
  enterAltScreen();
@@ -3952,11 +3984,36 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
3952
3984
  promptFromStdin = true;
3953
3985
  }
3954
3986
  const cwd = process$1.cwd();
3987
+ let effectiveCwd = cwd;
3988
+ let worktreePath = null;
3989
+ let worktreeCleanup = null;
3955
3990
  const currentBranch = getCurrentBranch(cwd);
3956
3991
  const onGnhfBranch = currentBranch.startsWith("gnhf/");
3957
3992
  let runInfo;
3958
3993
  let startIteration = 0;
3959
- if (onGnhfBranch) {
3994
+ if (options.worktree) {
3995
+ if (!prompt) {
3996
+ program.help();
3997
+ return;
3998
+ }
3999
+ if (onGnhfBranch) {
4000
+ console.error("Cannot use --worktree from a gnhf branch. Switch to the base branch first.");
4001
+ process$1.exit(1);
4002
+ }
4003
+ const wt = initializeWorktreeRun(prompt, cwd);
4004
+ runInfo = wt.runInfo;
4005
+ effectiveCwd = wt.effectiveCwd;
4006
+ worktreePath = wt.worktreePath;
4007
+ worktreeCleanup = () => {
4008
+ try {
4009
+ removeWorktree(cwd, wt.worktreePath);
4010
+ } catch {}
4011
+ };
4012
+ const exitCleanup = worktreeCleanup;
4013
+ process$1.on("exit", () => {
4014
+ if (worktreeCleanup === exitCleanup) exitCleanup();
4015
+ });
4016
+ } else if (onGnhfBranch) {
3960
4017
  const existingRunId = currentBranch.slice(5);
3961
4018
  const existing = resumeRun(existingRunId, cwd);
3962
4019
  const existingPrompt = readFileSync(existing.promptPath, "utf-8");
@@ -4007,11 +4064,13 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
4007
4064
  maxTokens: options.maxTokens,
4008
4065
  preventSleep: config.preventSleep,
4009
4066
  agentArgsOverride: config.agentArgsOverride?.[config.agent],
4067
+ worktree: options.worktree,
4068
+ worktreePath,
4010
4069
  platform: process$1.platform,
4011
4070
  nodeVersion: process$1.version,
4012
4071
  gnhfVersion: packageVersion
4013
4072
  });
4014
- const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent], config.agentArgsOverride?.[config.agent]), runInfo, prompt, cwd, startIteration, {
4073
+ const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent], config.agentArgsOverride?.[config.agent]), runInfo, prompt, effectiveCwd, startIteration, {
4015
4074
  maxIterations: options.maxIterations,
4016
4075
  maxTokens: options.maxTokens
4017
4076
  });
@@ -4065,8 +4124,17 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
4065
4124
  failCount: finalState.failCount,
4066
4125
  totalInputTokens: finalState.totalInputTokens,
4067
4126
  totalOutputTokens: finalState.totalOutputTokens,
4068
- commitCount: finalState.commitCount
4127
+ commitCount: finalState.commitCount,
4128
+ worktreePath
4069
4129
  });
4130
+ if (worktreePath) if (finalState.commitCount > 0) {
4131
+ worktreeCleanup = null;
4132
+ console.error(`\n gnhf: worktree preserved at ${worktreePath}\n gnhf: merge the branch and remove with: git worktree remove "${worktreePath}"\n`);
4133
+ } else {
4134
+ worktreeCleanup?.();
4135
+ worktreeCleanup = null;
4136
+ appendDebugLog("worktree:cleaned-up", { worktreePath });
4137
+ }
4070
4138
  }
4071
4139
  if (shutdownSignal) process$1.exit(getSignalExitCode(shutdownSignal));
4072
4140
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnhf",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Before I go to bed, I tell my agents: good night, have fun",
5
5
  "type": "module",
6
6
  "bin": {