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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/dist/cli.mjs +62 -20
  3. 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 worktreeExists(baseCwd, worktreePath) {
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 false;
457
+ return /* @__PURE__ */ new Set();
458
458
  }
459
- const target = resolve(worktreePath);
460
- for (const line of output.split("\n")) if (line.startsWith("worktree ") && resolve(line.slice(9)) === target) return true;
461
- return false;
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 branchName = slugifyPrompt(prompt);
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 = join(dirname(repoRoot), `${basename(repoRoot)}-gnhf-worktrees`, runId);
4768
- if (worktreeExists(repoRoot, worktreePath) && existsSync(join(worktreePath, ".gnhf", "runs", runId))) {
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(worktreePath);
4791
+ worktreeBranch = getCurrentBranch(candidateWorktreePath);
4772
4792
  } catch (error) {
4773
- throw new Error(`Preserved worktree at ${worktreePath} is in an unexpected state (${error instanceof Error ? error.message : String(error)}). Fix the worktree manually or remove it with "git worktree remove ${worktreePath}" before re-running.`);
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 !== branchName) throw new Error(`Preserved worktree at ${worktreePath} is on branch "${worktreeBranch}" rather than "${branchName}". Restore it to "${branchName}" with "git -C ${worktreePath} checkout ${branchName}", or remove the worktree with "git worktree remove ${worktreePath}" to start fresh.`);
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(runId, worktreePath, schemaOptions),
4778
- worktreePath,
4779
- effectiveCwd: worktreePath,
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(runId, prompt, baseCommit, worktreePath, schemaOptions),
4786
- worktreePath,
4787
- effectiveCwd: worktreePath,
4827
+ runInfo: setupRun(createdRunId, prompt, baseCommit, createdWorktreePath, schemaOptions),
4828
+ worktreePath: createdWorktreePath,
4829
+ effectiveCwd: createdWorktreePath,
4788
4830
  resumed: false
4789
4831
  };
4790
4832
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnhf",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "Before I go to bed, I tell my agents: good night, have fun",
5
5
  "type": "module",
6
6
  "bin": {