gnhf 0.1.29 → 0.1.30
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 +2 -1
- package/dist/cli.mjs +62 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -144,7 +144,7 @@ npm link
|
|
|
144
144
|
- **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
|
|
145
145
|
- **Shared memory** — the agent reads `notes.md` (built up from prior iterations) to communicate across iterations
|
|
146
146
|
- **Local run metadata** — gnhf stores prompt, notes, and resume metadata under `.gnhf/runs/` and ignores it locally, so your branch only contains intentional work
|
|
147
|
-
- **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
|
|
147
|
+
- **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`.
|
|
148
148
|
|
|
149
149
|
### Worktree Mode
|
|
150
150
|
|
|
@@ -158,6 +158,7 @@ Pass `--worktree` to run each agent in an isolated [git worktree](https://git-sc
|
|
|
158
158
|
```
|
|
159
159
|
|
|
160
160
|
- 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.
|
|
161
|
+
- 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.
|
|
161
162
|
- Worktrees with **no commits** are automatically removed on exit.
|
|
162
163
|
- `--worktree` must be run from a non-gnhf branch (typically `main`).
|
|
163
164
|
|
package/dist/cli.mjs
CHANGED
|
@@ -445,7 +445,7 @@ function removeWorktree(baseCwd, worktreePath) {
|
|
|
445
445
|
worktreePath
|
|
446
446
|
], baseCwd);
|
|
447
447
|
}
|
|
448
|
-
function
|
|
448
|
+
function listWorktreePaths(baseCwd) {
|
|
449
449
|
let output;
|
|
450
450
|
try {
|
|
451
451
|
output = git([
|
|
@@ -454,11 +454,11 @@ function worktreeExists(baseCwd, worktreePath) {
|
|
|
454
454
|
"--porcelain"
|
|
455
455
|
], baseCwd);
|
|
456
456
|
} catch {
|
|
457
|
-
return
|
|
457
|
+
return /* @__PURE__ */ new Set();
|
|
458
458
|
}
|
|
459
|
-
const
|
|
460
|
-
for (const line of output.split("\n")) if (line.startsWith("worktree ")
|
|
461
|
-
return
|
|
459
|
+
const paths = /* @__PURE__ */ new Set();
|
|
460
|
+
for (const line of output.split("\n")) if (line.startsWith("worktree ")) paths.add(resolve(line.slice(9)));
|
|
461
|
+
return paths;
|
|
462
462
|
}
|
|
463
463
|
//#endregion
|
|
464
464
|
//#region src/core/agents/types.ts
|
|
@@ -4754,37 +4754,79 @@ function buildResumeSchemaOptions(stopWhen) {
|
|
|
4754
4754
|
function initializeNewBranch(prompt, cwd, schemaOptions) {
|
|
4755
4755
|
ensureCleanWorkingTree(cwd);
|
|
4756
4756
|
const baseCommit = getHeadCommit(cwd);
|
|
4757
|
-
const
|
|
4758
|
-
createBranch(branchName, cwd);
|
|
4759
|
-
const runId = branchName.split("/")[1];
|
|
4757
|
+
const runId = createBranchWithSuffix(slugifyPrompt(prompt), cwd).split("/")[1];
|
|
4760
4758
|
return setupRun(runId, prompt, baseCommit, cwd, schemaOptions);
|
|
4761
4759
|
}
|
|
4760
|
+
function branchNameWithSuffix(branchName, suffix) {
|
|
4761
|
+
return suffix === 0 ? branchName : `${branchName}-${suffix}`;
|
|
4762
|
+
}
|
|
4763
|
+
function isCollisionError(error) {
|
|
4764
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4765
|
+
return /already exists|exists already|would be overwritten/i.test(message);
|
|
4766
|
+
}
|
|
4767
|
+
function createBranchWithSuffix(branchName, cwd) {
|
|
4768
|
+
for (let suffix = 0; suffix < 100; suffix += 1) {
|
|
4769
|
+
const candidate = branchNameWithSuffix(branchName, suffix);
|
|
4770
|
+
try {
|
|
4771
|
+
createBranch(candidate, cwd);
|
|
4772
|
+
return candidate;
|
|
4773
|
+
} catch (error) {
|
|
4774
|
+
if (!isCollisionError(error)) throw error;
|
|
4775
|
+
}
|
|
4776
|
+
}
|
|
4777
|
+
throw new Error(`Unable to create a unique branch name for ${branchName}`);
|
|
4778
|
+
}
|
|
4762
4779
|
function initializeWorktreeRun(prompt, cwd, schemaOptions) {
|
|
4763
4780
|
const repoRoot = getRepoRootDir(cwd);
|
|
4764
4781
|
const baseCommit = getHeadCommit(cwd);
|
|
4765
4782
|
const branchName = slugifyPrompt(prompt);
|
|
4783
|
+
const makeWorktreePath = (runId) => join(dirname(repoRoot), `${basename(repoRoot)}-gnhf-worktrees`, runId);
|
|
4766
4784
|
const runId = branchName.split("/")[1];
|
|
4767
|
-
const worktreePath =
|
|
4768
|
-
|
|
4785
|
+
const worktreePath = makeWorktreePath(runId);
|
|
4786
|
+
const registeredWorktreePaths = listWorktreePaths(repoRoot);
|
|
4787
|
+
const resumePreservedWorktree = (candidateBranchName, candidateRunId, candidateWorktreePath) => {
|
|
4788
|
+
if (!registeredWorktreePaths.has(resolve(candidateWorktreePath)) || !existsSync(join(candidateWorktreePath, ".gnhf", "runs", candidateRunId))) return null;
|
|
4769
4789
|
let worktreeBranch;
|
|
4770
4790
|
try {
|
|
4771
|
-
worktreeBranch = getCurrentBranch(
|
|
4791
|
+
worktreeBranch = getCurrentBranch(candidateWorktreePath);
|
|
4772
4792
|
} catch (error) {
|
|
4773
|
-
throw new Error(`Preserved worktree at ${
|
|
4793
|
+
throw new Error(`Preserved worktree at ${candidateWorktreePath} is in an unexpected state (${error instanceof Error ? error.message : String(error)}). Fix the worktree manually or remove it with "git worktree remove ${candidateWorktreePath}" before re-running.`);
|
|
4774
4794
|
}
|
|
4775
|
-
if (worktreeBranch !==
|
|
4795
|
+
if (worktreeBranch !== candidateBranchName) throw new Error(`Preserved worktree at ${candidateWorktreePath} is on branch "${worktreeBranch}" rather than "${candidateBranchName}". Restore it to "${candidateBranchName}" with "git -C ${candidateWorktreePath} checkout ${candidateBranchName}", or remove the worktree with "git worktree remove ${candidateWorktreePath}" to start fresh.`);
|
|
4776
4796
|
return {
|
|
4777
|
-
runInfo: resumeRun(
|
|
4778
|
-
worktreePath,
|
|
4779
|
-
effectiveCwd:
|
|
4797
|
+
runInfo: resumeRun(candidateRunId, candidateWorktreePath, schemaOptions),
|
|
4798
|
+
worktreePath: candidateWorktreePath,
|
|
4799
|
+
effectiveCwd: candidateWorktreePath,
|
|
4780
4800
|
resumed: true
|
|
4781
4801
|
};
|
|
4802
|
+
};
|
|
4803
|
+
let createdBranchName = branchName;
|
|
4804
|
+
let createdRunId = runId;
|
|
4805
|
+
let createdWorktreePath = worktreePath;
|
|
4806
|
+
for (let suffix = 0; suffix < 100; suffix += 1) {
|
|
4807
|
+
const candidateBranchName = branchNameWithSuffix(branchName, suffix);
|
|
4808
|
+
const candidateRunId = candidateBranchName.split("/")[1];
|
|
4809
|
+
const resumed = resumePreservedWorktree(candidateBranchName, candidateRunId, makeWorktreePath(candidateRunId));
|
|
4810
|
+
if (resumed) return resumed;
|
|
4811
|
+
}
|
|
4812
|
+
for (let suffix = 0; suffix < 100; suffix += 1) {
|
|
4813
|
+
createdBranchName = branchNameWithSuffix(branchName, suffix);
|
|
4814
|
+
createdRunId = createdBranchName.split("/")[1];
|
|
4815
|
+
createdWorktreePath = makeWorktreePath(createdRunId);
|
|
4816
|
+
const resumed = resumePreservedWorktree(createdBranchName, createdRunId, createdWorktreePath);
|
|
4817
|
+
if (resumed) return resumed;
|
|
4818
|
+
try {
|
|
4819
|
+
createWorktree(repoRoot, createdWorktreePath, createdBranchName);
|
|
4820
|
+
break;
|
|
4821
|
+
} catch (error) {
|
|
4822
|
+
if (!isCollisionError(error)) throw error;
|
|
4823
|
+
if (suffix === 99) throw new Error(`Unable to create a unique worktree for ${branchName}`);
|
|
4824
|
+
}
|
|
4782
4825
|
}
|
|
4783
|
-
createWorktree(repoRoot, worktreePath, branchName);
|
|
4784
4826
|
return {
|
|
4785
|
-
runInfo: setupRun(
|
|
4786
|
-
worktreePath,
|
|
4787
|
-
effectiveCwd:
|
|
4827
|
+
runInfo: setupRun(createdRunId, prompt, baseCommit, createdWorktreePath, schemaOptions),
|
|
4828
|
+
worktreePath: createdWorktreePath,
|
|
4829
|
+
effectiveCwd: createdWorktreePath,
|
|
4788
4830
|
resumed: false
|
|
4789
4831
|
};
|
|
4790
4832
|
}
|