gnhf 0.1.17 → 0.1.18

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 +66 -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 = {
@@ -3856,6 +3869,19 @@ function initializeNewBranch(prompt, cwd) {
3856
3869
  const runId = branchName.split("/")[1];
3857
3870
  return setupRun(runId, prompt, baseCommit, cwd);
3858
3871
  }
3872
+ function initializeWorktreeRun(prompt, cwd) {
3873
+ const repoRoot = getRepoRootDir(cwd);
3874
+ const baseCommit = getHeadCommit(cwd);
3875
+ const branchName = slugifyPrompt(prompt);
3876
+ const runId = branchName.split("/")[1];
3877
+ const worktreePath = join(dirname(repoRoot), `${basename(repoRoot)}-gnhf-worktrees`, runId);
3878
+ createWorktree(repoRoot, worktreePath, branchName);
3879
+ return {
3880
+ runInfo: setupRun(runId, prompt, baseCommit, worktreePath),
3881
+ worktreePath,
3882
+ effectiveCwd: worktreePath
3883
+ };
3884
+ }
3859
3885
  function ask(question) {
3860
3886
  const rl = createInterface({
3861
3887
  input: process$1.stdin,
@@ -3918,7 +3944,7 @@ function readReexecStdinPrompt(env) {
3918
3944
  }
3919
3945
  }
3920
3946
  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) => {
3947
+ 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
3948
  if (options.mock) {
3923
3949
  const mock = new MockOrchestrator();
3924
3950
  enterAltScreen();
@@ -3952,11 +3978,36 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
3952
3978
  promptFromStdin = true;
3953
3979
  }
3954
3980
  const cwd = process$1.cwd();
3981
+ let effectiveCwd = cwd;
3982
+ let worktreePath = null;
3983
+ let worktreeCleanup = null;
3955
3984
  const currentBranch = getCurrentBranch(cwd);
3956
3985
  const onGnhfBranch = currentBranch.startsWith("gnhf/");
3957
3986
  let runInfo;
3958
3987
  let startIteration = 0;
3959
- if (onGnhfBranch) {
3988
+ if (options.worktree) {
3989
+ if (!prompt) {
3990
+ program.help();
3991
+ return;
3992
+ }
3993
+ if (onGnhfBranch) {
3994
+ console.error("Cannot use --worktree from a gnhf branch. Switch to the base branch first.");
3995
+ process$1.exit(1);
3996
+ }
3997
+ const wt = initializeWorktreeRun(prompt, cwd);
3998
+ runInfo = wt.runInfo;
3999
+ effectiveCwd = wt.effectiveCwd;
4000
+ worktreePath = wt.worktreePath;
4001
+ worktreeCleanup = () => {
4002
+ try {
4003
+ removeWorktree(cwd, wt.worktreePath);
4004
+ } catch {}
4005
+ };
4006
+ const exitCleanup = worktreeCleanup;
4007
+ process$1.on("exit", () => {
4008
+ if (worktreeCleanup === exitCleanup) exitCleanup();
4009
+ });
4010
+ } else if (onGnhfBranch) {
3960
4011
  const existingRunId = currentBranch.slice(5);
3961
4012
  const existing = resumeRun(existingRunId, cwd);
3962
4013
  const existingPrompt = readFileSync(existing.promptPath, "utf-8");
@@ -4007,11 +4058,13 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
4007
4058
  maxTokens: options.maxTokens,
4008
4059
  preventSleep: config.preventSleep,
4009
4060
  agentArgsOverride: config.agentArgsOverride?.[config.agent],
4061
+ worktree: options.worktree,
4062
+ worktreePath,
4010
4063
  platform: process$1.platform,
4011
4064
  nodeVersion: process$1.version,
4012
4065
  gnhfVersion: packageVersion
4013
4066
  });
4014
- const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent], config.agentArgsOverride?.[config.agent]), runInfo, prompt, cwd, startIteration, {
4067
+ const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent], config.agentArgsOverride?.[config.agent]), runInfo, prompt, effectiveCwd, startIteration, {
4015
4068
  maxIterations: options.maxIterations,
4016
4069
  maxTokens: options.maxTokens
4017
4070
  });
@@ -4065,8 +4118,17 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
4065
4118
  failCount: finalState.failCount,
4066
4119
  totalInputTokens: finalState.totalInputTokens,
4067
4120
  totalOutputTokens: finalState.totalOutputTokens,
4068
- commitCount: finalState.commitCount
4121
+ commitCount: finalState.commitCount,
4122
+ worktreePath
4069
4123
  });
4124
+ if (worktreePath) if (finalState.commitCount > 0) {
4125
+ worktreeCleanup = null;
4126
+ console.error(`\n gnhf: worktree preserved at ${worktreePath}\n gnhf: merge the branch and remove with: git worktree remove "${worktreePath}"\n`);
4127
+ } else {
4128
+ worktreeCleanup?.();
4129
+ worktreeCleanup = null;
4130
+ appendDebugLog("worktree:cleaned-up", { worktreePath });
4131
+ }
4070
4132
  }
4071
4133
  if (shutdownSignal) process$1.exit(getSignalExitCode(shutdownSignal));
4072
4134
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnhf",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Before I go to bed, I tell my agents: good night, have fun",
5
5
  "type": "module",
6
6
  "bin": {