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