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.
- package/README.md +23 -0
- package/dist/cli.mjs +72 -4
- 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 (
|
|
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,
|
|
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
|
});
|