@senad-d/branchme 0.3.13 → 0.3.15

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.15 - Unreleased
4
+
5
+ - `integrate_branch`, `conclude_merge` and `branch_status` accept optional `worktreePath`, so one git agent session can act on several worktrees of the same repository. It must be the absolute root of the primary checkout or a registered linked worktree with the same Git common directory; relative, missing, subdirectory, bare and foreign paths are refused. The tool then runs as if the session cwd were that worktree (control worktree, clean checks, `keepConflicts`, and the per-worktree mutation queue). A `conflict_kept` result now names its `worktreeRoot`.
6
+
7
+ ## 0.3.14 - Unreleased
8
+
9
+ - A Git mutation that fails because another git process holds `index.lock`, `HEAD.lock` or a ref lock is retried every 250 ms for up to 10 seconds, then fails with git's error plus "The lock was still held after waiting 10 s."; lock files are never removed, and cancellation interrupts the wait without another mutation attempt. Local Git reads (status and the other snapshot reads) time out after 30 seconds instead of 5, for slow or emulated git under parallel agents.
10
+
3
11
  ## 0.3.13 - Unreleased
4
12
 
5
13
  - Added optional `integrate_branch.keepConflicts` (default `false`). When true, a conflicted local integration returns `conflict_kept` with the merge left in progress for `conclude_merge` to finish after resolution or abort; default conflict handling still aborts and verifies restoration. Updated schemas, prompt guidance, documentation, and real-Git regression coverage.
package/README.md CHANGED
@@ -256,14 +256,14 @@ Commands are informational only. BranchMe actions are performed by agent-callabl
256
256
  | `fetch_remote` | `{ "remote"?: string, "prune"?: boolean }` | Refresh all heads for one configured remote (default `origin`) into its remote-tracking namespace. Prune defaults to false; true deletes only that remote's stale cached branch refs. Atomic ref updates, no configured refmap, tags, tag pruning, or submodules. Never changes local refs, checkout, upstream settings, or working-tree files. Files ref backend required for dangling-alias safety. |
257
257
  | `track_branch` | `{ "branchName": string, "remote"?: string, "remoteBranch"?: string }` | Fetch one exact remote branch and create/check out a new local tracking branch. Remote defaults to `origin`; remoteBranch defaults to branchName. Requires a clean idle checkout; verifies HEAD and upstream. |
258
258
  | `update_from_base` | `{ "baseBranch": string, "remote"?: string, "keepConflicts"?: boolean }` | Fetch the exact remote base (remote defaults to `origin`) and merge its captured commit into the clean current feature. Uses verified normal-merge policy; a `conflict` is automatically aborted with its paths listed, or with `keepConflicts: true` reported as `conflict_kept` and left in progress (MERGE_HEAD set, conflicted paths unmerged) for `conclude_merge`. Never rebases, pushes, or changes upstream configuration. |
259
- | `conclude_merge` | `{ "action": "conclude" \| "abort" }` | Finish or abandon the in-progress merge on the current checkout. `conclude` refuses default/custom-sized `<<<<<<<`, `|||||||`, `=======`, or `>>>>>>>` marker lines in unmerged working-tree paths and staged blobs (including CRLF and binary content, naming the paths), stages only the currently unmerged paths, rechecks the candidate index, commits with `git commit --no-edit`, and verifies MERGE_HEAD is gone and HEAD is a two-parent merge of the previous HEAD and MERGE_HEAD; unrelated unstaged changes stay unstaged. `abort` runs `git merge --abort` and verifies the restored HEAD, cleared operation state, and clean tree. Refuses when no merge is in progress. |
259
+ | `conclude_merge` | `{ "action": "conclude" \| "abort", "worktreePath"?: string }` | Finish or abandon the in-progress merge on the current checkout. `conclude` refuses default/custom-sized `<<<<<<<`, `|||||||`, `=======`, or `>>>>>>>` marker lines in unmerged working-tree paths and staged blobs (including CRLF and binary content, naming the paths), stages only the currently unmerged paths, rechecks the candidate index, commits with `git commit --no-edit`, and verifies MERGE_HEAD is gone and HEAD is a two-parent merge of the previous HEAD and MERGE_HEAD; unrelated unstaged changes stay unstaged. `abort` runs `git merge --abort` and verifies the restored HEAD, cleared operation state, and clean tree. Refuses when no merge is in progress. `worktreePath` (an absolute worktree root of the same repository) lets one session act on another worktree; omit it for the current one. |
260
260
  | `pull_request_files` | `{ "number": integer }` | List a pull request's first 100 changed files (path, change type, additions, deletions; the rest counted) and its current labels. Read-only. |
261
261
  | `label_pull_request` | `{ "number": integer, "labels": string[] }` | Add 1 to 5 labels that already exist in the repository to a pull request; an unknown label is refused before anything changes, existing labels stay. |
262
262
  | `reply_pull_request_feedback` | `{ "number": integer, "body": string, "threadId"?: string, "resolve"?: boolean }` | Answer PR feedback on an open, merged or closed PR of this repository (forks included). With `threadId` (from `pull_request_feedback`) it replies in that review thread after verifying the thread belongs to the PR, and `resolve: true` then resolves it; without `threadId` it posts one conversation comment after confirming the number is a pull request, not an issue. Body is Markdown, at most 4,000 characters. Never edits, deletes, approves, merges, or closes. |
263
263
  | `pull_request_feedback` | `{ "number"?: integer, "headBranch"?: string }` | Read what a same-repository PR asks to be fixed in one GraphQL request: unresolved, current review threads with their comments (resolved and outdated threads are counted only), review summaries with a body or requested changes, the latest 50 conversation comments, and the head commit's failing checks with up to 20 failure and warning annotations each (`path:line LEVEL: message`). Threads and conversation comments carry their GraphQL ids, every comment its author and time, and comments by the token's own user are marked `(you)`, so a caller can reply to a thread and tell new comments from its own replies. For up to three failing GitHub Actions jobs it adds the log lines leading to the last `##[error]`. A job log that cannot be read is reported per check. No mutations. |
264
264
  | `pull_request_status` | `{ "number"?: integer, "headBranch"?: string }` | Read exact same-repository PR lifecycle and head/base/merge identities. Number and headBranch are mutually exclusive. Without number, returns the most recently updated PR for headBranch or the current branch. Does not certify CI checks or review approvals. |
265
265
  | `init_repository` | `{ "initialBranch"?: string }` | Initializes only pi's exact current working directory as a verified non-bare Git repository with an unborn branch (default `main`). Rejects existing or nested repositories and accepts no path, bare, template, shared, remote, commit, or project-file controls. |
266
- | `branch_status` | `{ "ancestry"?: { "sourceBranch": string, "targetBranch": string } }` | Explicitly refreshes the same bounded context used at agent start. An optional strict ancestry query captures both exact HEADs and reports whether the source commit is an ancestor of the target commit; each endpoint may be an exact local branch or a remote-tracking ref such as `origin/main` (a local branch of the same name takes precedence). It is read-only and never checks out or resets remote-tracking refs; automatic Git context does not run ancestry queries. |
266
+ | `branch_status` | `{ "ancestry"?: { "sourceBranch": string, "targetBranch": string }, "worktreePath"?: string }` | Explicitly refreshes the same bounded context used at agent start. An optional strict ancestry query captures both exact HEADs and reports whether the source commit is an ancestor of the target commit; each endpoint may be an exact local branch or a remote-tracking ref such as `origin/main` (a local branch of the same name takes precedence). It is read-only and never checks out or resets remote-tracking refs; automatic Git context does not run ancestry queries. `worktreePath` (an absolute worktree root of the same repository) lets one session act on another worktree; omit it for the current one. |
267
267
  | `list_worktrees` | `{}` | Runs a bounded, read-only inventory of the current repository's main and linked worktrees, including path, branch/detached state, `HEAD`, current/main, locked, prunable, and omitted-entry details. Automatic Git context does not include this inventory. |
268
268
  | `create_worktree` | `{ "worktreePath": string, "branchName": string, "branchMode": "new" \| "existing", "baseRef"?: string }` | Creates and verifies a linked worktree at an explicitly approved absolute path. `new` creates a local branch from current `HEAD`, or from the optional read-only `baseRef` (an exact local branch, remote-tracking ref such as `origin/main`, or full commit) regardless of the current checkout's branch, dirt, or staleness; `existing` requires an existing local branch not checked out elsewhere and rejects `baseRef`. It returns a ready handoff with the exact canonical absolute cwd and local branch identity. |
269
269
  | `remove_worktree` | `{ "worktreePath": string, "deleteIgnored"?: boolean, "discardChanges"?: boolean }` | Force-free removal of an explicitly selected, verified clean linked worktree. Ignored residue is refused by default; `deleteIgnored: true` explicitly deletes it and reports its top-level paths. The local branch remains at the same commit. `discardChanges: true` explicitly discards uncommitted changes and lists them in `discardedPaths`; a dirty worktree is refused without it. |
@@ -273,7 +273,7 @@ Commands are informational only. BranchMe actions are performed by agent-callabl
273
273
  | `fetch_branch` | `{ "remote"?: string, "branch"?: string }` | With no arguments it requires a current branch with a configured upstream and runs `git fetch --no-tags --no-recurse-submodules <upstream-remote> <upstream-branch-ref>:<remote-tracking-ref>`. With `branch` (and optional `remote`, default `origin`; `remote` requires `branch`) it fetches that exact remote branch into `refs/remotes/<remote>/<branch>` instead. Either way only that tracking ref is refreshed without changing local branches, working-tree files, or upstream configuration. |
274
274
  | `pull_branch` | `{}` | Requires a clean current branch with a configured upstream and runs `git pull --ff-only --no-rebase --no-autostash <upstream-remote> <upstream-branch-ref>`; divergence fails without rebasing or creating a merge commit. |
275
275
  | `rebase_branch` | `{}` | Requires a clean current branch with a configured upstream and runs `git rebase --no-autostash --no-update-refs <upstream>`; it rewrites local commits and automatically attempts `git rebase --abort` on failure. |
276
- | `integrate_branch` | `{ "sourceBranch": string, "targetBranch": string, "keepConflicts"?: boolean }` | Integrates one exact existing local source branch into one distinct existing local target branch. The clean active control worktree must already have the target checked out. It returns `already_integrated`, `fast_forward`, `merge_commit`, or a `conflict` only after automatic abort and verified restoration; with `keepConflicts: true` a conflict is reported as `conflict_kept` and left in progress (MERGE_HEAD set, conflicted paths unmerged) for `conclude_merge`. It never fetches or pushes. |
276
+ | `integrate_branch` | `{ "sourceBranch": string, "targetBranch": string, "keepConflicts"?: boolean, "worktreePath"?: string }` | Integrates one exact existing local source branch into one distinct existing local target branch. The clean active control worktree must already have the target checked out. It returns `already_integrated`, `fast_forward`, `merge_commit`, or a `conflict` only after automatic abort and verified restoration; with `keepConflicts: true` a conflict is reported as `conflict_kept` and left in progress (MERGE_HEAD set, conflicted paths unmerged) for `conclude_merge`. It never fetches or pushes. `worktreePath` (an absolute worktree root of the same repository) lets one session act on another worktree; omit it for the current one. |
277
277
  | `create_branch` | `{ "branchName": string }` | Validates `branchName`, rejects existing local branches, and runs `git switch -c <branchName>` from current `HEAD`. |
278
278
  | `push_branch` | `{}` | Pushes the current branch to its configured upstream remote with an explicit `HEAD:<upstream-branch-ref>` refspec, or publishes it with `git push --set-upstream origin <currentBranch>` when no upstream exists. A branch whose upstream is a differently named `main`, `master`, `trunk`, `develop`, or origin default branch is published to its own name instead and tracks it. |
279
279
  | `pull_request` | `{ "headBranch"?: string, "baseBranch"?: string, "title"?: string, "body"?: string, "draft"?: boolean }` | Preflights GitHub branch visibility and verifies the GitHub `headBranch` commit matches the local branch, then reuses an exact matching open PR or creates one in the resolved current repository. Existing PR metadata is preserved. Omitted fields require `BRANCHME_PR_AUTOFILL=true`; branch refs must be distinct, exist locally, and cannot use `owner:branch`. |
@@ -303,7 +303,7 @@ Before each agent run, BranchMe appends an **Automatic Git Context** snapshot to
303
303
  - whether pull request field autofill is enabled;
304
304
  - up to 5 recent commits with short hash, date, and subject.
305
305
 
306
- Collection defaults are a 5-second timeout per local Git command, a 4-second related-PR lookup timeout, at most 512 characters per metadata value, and at most 4,000 characters for the rendered snapshot. GitHub response bodies are limited to 64 KiB. The formatter can further shorten values or omit entries to stay within the total limit.
306
+ Collection defaults are a 30-second timeout per local Git read, a 4-second related-PR lookup timeout, at most 512 characters per metadata value, and at most 4,000 characters for the rendered snapshot. GitHub response bodies are limited to 64 KiB. The formatter can further shorten values or omit entries to stay within the total limit.
307
307
 
308
308
  The snapshot is fresh at agent start but is not live. A fetch, branch switch, pull, rebase, integration, commit, file change, push, or other mutation later in the same run can make it stale. `branch_status` performs an explicit current-state refresh through the same shared collector and remains read-only; it does not mutate files, Git state, or GitHub state. Its optional `ancestry` object requires exact `sourceBranch` and `targetBranch` fields together, captures both commit IDs, and reports `isAncestor`. Each endpoint may be an exact local branch or a remote-tracking ref such as `origin/main` (local branches take precedence), so a fetched `origin/main` can serve as a read-only containment proof after a pull request merges. This targeted proof is explicit only: automatic Git context remains unchanged. Run it after `integrate_branch` or `fetch_branch` completes, never in the same parallel tool batch.
309
309
 
@@ -325,7 +325,7 @@ If pull request field autofill is enabled, after push_branch completes create a
325
325
 
326
326
  ### Verified local branch integration
327
327
 
328
- `integrate_branch` requires explicit intent and exactly two distinct existing local branch names: `sourceBranch` and `targetBranch`. The active Pi worktree is the control worktree; it must be clean, have no merge/rebase/cherry-pick/revert/sequencer operation in progress, and already have `targetBranch` checked out. BranchMe never switches to the target or infers one. A source checked out in another dirty linked worktree is allowed because integration reads only its committed local ref. Remote-only refs, commit IDs, paths, repositories, remotes, and owner-prefixed refs are rejected.
328
+ `integrate_branch` requires explicit intent and exactly two distinct existing local branch names: `sourceBranch` and `targetBranch`. The active Pi worktree is the control worktree; it must be clean, have no merge/rebase/cherry-pick/revert/sequencer operation in progress, and already have `targetBranch` checked out. BranchMe never switches to the target or infers one. A source checked out in another dirty linked worktree is allowed because integration reads only its committed local ref. Remote-only refs, commit IDs, paths, repositories, remotes, and owner-prefixed refs are rejected. Optional `worktreePath` makes another worktree of the same repository the control worktree: it must be the absolute root of the primary checkout or a registered linked worktree with the same Git common directory; relative paths, subdirectories, missing, bare, and foreign paths are refused before integration starts.
329
329
 
330
330
  For a source not already reachable from the target, BranchMe runs this fixed normal-merge policy from the verified worktree root:
331
331
 
@@ -466,7 +466,7 @@ Repository-configured `reference-transaction` hooks can run during `git update-r
466
466
  BranchMe operates only on the repository where pi is running:
467
467
 
468
468
  - Automatic collection and `branch_status` run bounded, read-only Git commands from the verified git root.
469
- - Git commands use `pi.exec("git", args, { cwd, signal, timeout })` with argv arrays; repository mutations run from the verified git root.
469
+ - Git commands use `pi.exec("git", args, { cwd, signal, timeout })` with argv arrays; repository mutations run from the verified git root. A command that fails because another git process holds a lock (`index.lock`, `HEAD.lock`, a ref lock) is retried every 250 ms for up to 10 seconds, then fails with git's error plus a note that the lock was still held; BranchMe never removes a lock file.
470
470
  - `change_branch` switches only to existing local branches and has no `force`, `stash`, `discard`, remote, or path input.
471
471
  - `fetch_branch` uses an explicit source-to-remote-tracking refspec with tags and submodule recursion disabled and does not change local branches or working-tree files. Without arguments it requires a configured upstream; with an explicit `branch` (and optional configured `remote`, default `origin`) it refreshes only `refs/remotes/<remote>/<branch>` and never touches upstream configuration.
472
472
  - `pull_branch` requires a clean worktree and configured upstream, updates only the current branch with `git pull --ff-only --no-rebase --no-autostash`, and has no branch, remote, force, or rebase input.
@@ -141,13 +141,14 @@ Standalone removal retains its branch and requires explicit `deleteIgnored: true
141
141
 
142
142
  ## 8. Branch integration contract
143
143
 
144
- - `integrate_branch` accepts exactly `sourceBranch` and `targetBranch`, plus optional boolean `keepConflicts`. They must identify distinct existing local refs in the current repository; remote-only refs, full refs, commit IDs, paths, repositories, remotes, and owner-prefixed refs are rejected.
144
+ - `integrate_branch` accepts exactly `sourceBranch` and `targetBranch`, plus optional boolean `keepConflicts` and optional `worktreePath`. They must identify distinct existing local refs in the current repository; remote-only refs, full refs, commit IDs, paths, repositories, remotes, and owner-prefixed refs are rejected.
145
145
  - The active Pi worktree is the control worktree and must already have the target checked out, be clean, and have no merge/rebase/cherry-pick/revert/sequencer state. The source may be checked out in another dirty worktree because only its captured commit ref is read.
146
146
  - Before mutation, BranchMe rejects a non-empty `branch.<targetBranch>.mergeOptions` setting so branch-specific defaults cannot change the policy. The fixed command is `git -c rerere.enabled=false merge --ff --no-edit --no-autostash --no-rerere-autoupdate --no-overwrite-ignore refs/heads/<sourceBranch>`. It does not fetch or push, disables autostash and rerere, protects ignored files, and preserves hooks, custom merge drivers, clean/smudge filters, and signing policy.
147
147
  - Results distinguish `already_integrated`, `fast_forward`, `merge_commit`, and `conflict`. Before/after source and target commit IDs, repository/control-worktree identity, clean/operation state, and source/prior-target ancestry are verified. Merge-commit classification requires exact captured target/source parents.
148
148
  - Conflict paths are exact bounded repository-relative identities. `conflict` is returned only after `git merge --abort` succeeds and exact ref, repository, branch, operation-state, and clean-worktree restoration is verified. Failed non-conflict merges remain errors.
149
149
  - There is no reset-based rollback, `continue_merge`, `abort_merge`, custom merge message, strategy, squash, unrelated-history, signing, force, or commit control. Unexpected ref movement or inconclusive cleanup produces an uncertain error and manual inspection guidance.
150
150
  - The mutation queue covers the complete operation but is process-local. BranchMe neither starts agents nor resolves semantic conflicts; Mission or another orchestrator may separately decide whether to delegate conflict analysis to an integration agent or ask the developer about intent.
151
+ - `integrate_branch`, `conclude_merge`, and `branch_status` accept optional `worktreePath`, the absolute root of another worktree of the same repository (same Git common directory); the tool then runs as if the session cwd were that worktree. Relative, missing, subdirectory, bare, and foreign paths are refused.
151
152
  - `branch_status` may receive `{ "ancestry": { "sourceBranch": string, "targetBranch": string } }` for an independent read-only proof. It must run after integration completes, not in the same parallel tool batch; automatic context remains unchanged.
152
153
 
153
154
  ## 9. Local branch-retirement contract
@@ -24,7 +24,7 @@ pi --no-extensions -e .
24
24
 
25
25
  - `npm run smoke:pi` first runs isolated checkout Pi processes from a temporary non-Git working directory: one with `pi --no-extensions -e <package> -e <temporary verifier>` and `/branchmeverify verify`, then one with `pi --no-extensions -e <package>` and `/branchme help`.
26
26
  - The temporary command verifier calls `pi.getAllTools()` after BranchMe loads and confirms exactly twenty-five tools—`fetch_remote`, `list_branches`, `track_branch`, `update_from_base`, `conclude_merge`, `pull_request_status`, `pull_request_feedback`, `reply_pull_request_feedback`, `pull_request_files`, `label_pull_request`, `branch_status`, `init_repository`, `change_branch`, `create_branch`, `create_worktree`, `fetch_branch`, `integrate_branch`, `land_branch`, `list_worktrees`, `pull_branch`, `pull_request`, `push_branch`, `rebase_branch`, `remove_worktree`, and `retire_branch`—are each registered exactly once and active with strict schemas, named prompt guidelines, descriptions, and extension source metadata. It also proves `continue_merge` and `abort_merge` are absent.
27
- - Runtime schema inspection verifies that `integrate_branch` has exactly the required `sourceBranch` and `targetBranch` fields plus optional `keepConflicts` and that `retire_branch` has exactly the required `branchName`, `expectedHead`, `targetBranch`, and `force` fields with `additionalProperties: false`. It also verifies that `branch_status` has only an optional top-level `ancestry` object whose required nested `sourceBranch` and `targetBranch` fields reject additional nested or top-level properties.
27
+ - Runtime schema inspection verifies that `integrate_branch` has exactly the required `sourceBranch` and `targetBranch` fields plus optional `keepConflicts` and `worktreePath` and that `retire_branch` has exactly the required `branchName`, `expectedHead`, `targetBranch`, and `force` fields with `additionalProperties: false`. It also verifies that `branch_status` has only optional top-level `worktreePath` and `ancestry`, the `ancestry` object's required nested `sourceBranch` and `targetBranch` fields reject additional nested or top-level properties.
28
28
  - A second temporary verifier registers a deterministic local smoke model, blocks `fetch`, and runs normal prompts through real Pi lifecycle handling. It verifies automatic no-tool Git context in a temporary repository, one real `branch_status` tool refresh after a verifier-created local change, safe credential-free related-PR status with no request, and non-Git startup fallback. It expects all twenty-five tools but forbids `land_branch`, `retire_branch`, `integrate_branch`, `conclude_merge`, and every remote or worktree mutation tool (including `fetch_remote`) from executing.
29
29
  - The Pi runtime smoke validates worktree, integration, and retirement tool registration, strict schemas, and prompt contracts only; it never creates or removes a worktree, runs a merge, or retires a branch. Real-Git lifecycle coverage runs under `npm run test` in isolated temporary local repositories with no remote contact.
30
30
  - Isolated real-Git integration tests cover `already_integrated`, `fast_forward`, exact two-parent `merge_commit`, and conflict-path capture followed by verified automatic abort/restoration. They also cover target mismatch, dirty control state, rejection of branch-specific target merge options, unrelated histories, ignored-file overwrite protection, preserved repository hooks, and a committed source ref checked out in another dirty linked worktree. The recorded merge argv proves autostash and rerere are disabled, ignored-file protection is enabled, and `--no-verify` is absent.
package/docs/STRUCTURE.md CHANGED
@@ -53,9 +53,9 @@ src/
53
53
  - No long-lived processes, watchers, timers, sockets, or background jobs start in the extension factory.
54
54
  - A single `before_agent_start` handler synchronously collects a fresh snapshot for each agent run and appends it to the existing system prompt; failures degrade to bounded unavailable context rather than blocking startup.
55
55
  - Slash commands are informational; tools perform repository initialization, branch, integration, retirement, worktree, push, and PR actions. Commands never initialize repositories, create/remove worktrees, merge or retire branches, change cwd, or start Pi sessions. There is no context command.
56
- - Every tool uses a strict TypeBox object schema with `additionalProperties: false`; `init_repository` accepts only optional `initialBranch`; `integrate_branch` requires exactly `sourceBranch` and `targetBranch` with optional boolean `keepConflicts`; `retire_branch` requires exactly `branchName`, full commit `expectedHead`, distinct `targetBranch`, and boolean `force`; `update_from_base` accepts optional boolean `keepConflicts`; `conclude_merge` requires exactly `action` (`conclude` or `abort`); worktree creation requires `worktreePath`, `branchName`, and `branchMode` with optional new-mode `baseRef`, listing is empty, and removal requires `worktreePath` with optional boolean `deleteIgnored`.
56
+ - Every tool uses a strict TypeBox object schema with `additionalProperties: false`; `init_repository` accepts only optional `initialBranch`; `integrate_branch` requires exactly `sourceBranch` and `targetBranch` with optional boolean `keepConflicts` and optional `worktreePath`; `retire_branch` requires exactly `branchName`, full commit `expectedHead`, distinct `targetBranch`, and boolean `force`; `update_from_base` accepts optional boolean `keepConflicts`; `conclude_merge` requires exactly `action` (`conclude` or `abort`) with optional `worktreePath`; `branch_status` accepts optional `ancestry` and `worktreePath` (an absolute root of another worktree of the same repository, resolved by `resolveRepositoryWorktreeCwd`); worktree creation requires `worktreePath`, `branchName`, and `branchMode` with optional new-mode `baseRef`, listing is empty, and removal requires `worktreePath` with optional boolean `deleteIgnored`.
57
57
  - Every tool defines a description, `promptSnippet`, and tool-specific `promptGuidelines` that explicitly name the tool.
58
- - Git commands use argv arrays with cwd, signal, and timeout; network calls use the transport wrapper's absolute executable and child environment, while other commands use `pi.exec("git", args, { cwd, signal, timeout })`; `init_repository` uses Pi's file-mutation queue for the new `.git` entry and queues on the canonical target directory, and repository mutations run from verified locations and same-repository mutation/PR windows are serialized per repository. Retirement uses one canonical-active-worktree-keyed queue window from preflight through final verification. The queue is process-local and does not lock a different active worktree, another Pi process, or an external Git process.
58
+ - Git commands use argv arrays with cwd, signal, and timeout; network calls use the transport wrapper's absolute executable and child environment, while other commands use `pi.exec("git", args, { cwd, signal, timeout })`; `runGit` retries a command that failed on another process's `*.lock` every 250 ms for up to 10 s and never touches the lock; `init_repository` uses Pi's file-mutation queue for the new `.git` entry and queues on the canonical target directory, and repository mutations run from verified locations and same-repository mutation/PR windows are serialized per repository. Retirement uses one canonical-active-worktree-keyed queue window from preflight through final verification. The queue is process-local and does not lock a different active worktree, another Pi process, or an external Git process.
59
59
  - Worktree results expose serializable requested and verified before/after state. Create returns `handoff: { cwd: <absolute>, branch, head, ready: true, summary }`; remove returns the retained branch/HEAD with `cwd: null` and `ready: false`.
60
60
  - Tool details avoid token values, abort signals, runtime objects, and unbounded raw command/API output.
61
61
  - Automatic and explicit context include branch/upstream/ahead-behind state, working-tree counts, up to 20 unstaged/untracked entries, related open PR state, and up to 5 recent commits. Explicit `branch_status` may additionally verify one strict local or remote-tracking source/target ancestry query; automatic context never does. Metadata values default to 512 characters and rendered context to 4,000 characters.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senad-d/branchme",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "type": "module",
5
5
  "description": "Pi extension for verified Git repository initialization, branch, worktree, integration, retirement, push, and pull request workflows.",
6
6
  "license": "MIT",
package/src/constants.ts CHANGED
@@ -56,7 +56,11 @@ export const BRANCHME_TOOL_NAMES = [
56
56
  REMOVE_WORKTREE_TOOL_NAME,
57
57
  ] as const;
58
58
 
59
- export const GIT_STATUS_TIMEOUT_MS = 5_000;
59
+ // Emulated git (e.g. Rosetta in a container) under parallel agents needs far more than 5 s for a status read.
60
+ export const GIT_STATUS_TIMEOUT_MS = 30_000;
61
+ // A mutation that finds another git process's *.lock retries every interval until the wait ends.
62
+ export const GIT_LOCK_RETRY_INTERVAL_MS = 250;
63
+ export const GIT_LOCK_WAIT_MS = 10_000;
60
64
  export const GIT_MUTATION_TIMEOUT_MS = 30_000;
61
65
  export const GIT_FETCH_TIMEOUT_MS = 120_000;
62
66
  export const GIT_PULL_TIMEOUT_MS = 120_000;
@@ -642,6 +642,7 @@ async function verifyKeptConflict(
642
642
  status: "conflict_kept",
643
643
  mergeExecuted: true,
644
644
  request: { sourceBranch: prepared.sourceBranch, targetBranch: prepared.targetBranch },
645
+ worktreeRoot: prepared.worktreeRoot,
645
646
  heads: { sourceHead: prepared.sourceHead, targetHead: prepared.targetHead },
646
647
  conflict: {
647
648
  paths: conflictPaths.paths,
package/src/git.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { lstat, readFile, realpath, stat } from "node:fs/promises";
2
2
  import { basename, dirname, isAbsolute, join, normalize, relative, sep } from "node:path";
3
+ import { setTimeout as sleep } from "node:timers/promises";
3
4
  import { withFileMutationQueue, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
5
  import {
5
6
  GIT_BRANCH_ENTRY_LIMIT,
@@ -9,6 +10,8 @@ import {
9
10
  GIT_CONTEXT_RECENT_COMMIT_LIMIT,
10
11
  GIT_CONTEXT_VALUE_LIMIT_CHARS,
11
12
  GIT_FETCH_TIMEOUT_MS,
13
+ GIT_LOCK_RETRY_INTERVAL_MS,
14
+ GIT_LOCK_WAIT_MS,
12
15
  GIT_MUTATION_TIMEOUT_MS,
13
16
  GIT_PULL_TIMEOUT_MS,
14
17
  GIT_PUSH_TIMEOUT_MS,
@@ -128,17 +131,41 @@ export function formatGitFailure(args: readonly string[], result: GitExecResult,
128
131
  return `${safeCommandLabel(args, tokens)} failed${killed}: ${reason}`;
129
132
  }
130
133
 
131
- export async function runGit(
134
+ // git's "Unable to create '<path>.lock': File exists" for index.lock, HEAD.lock
135
+ // and ref locks: another git process holds the lock.
136
+ const GIT_LOCK_HELD_PATTERN = /Unable to create '[^'\n]*\.lock': File exists/u;
137
+
138
+ async function runGitWithLockRetry(
132
139
  pi: Pick<ExtensionAPI, "exec">,
133
140
  ctx: GitCommandContext,
134
141
  args: string[],
135
- options: GitRunOptions = {},
142
+ options: GitRunOptions,
143
+ deadline: number,
136
144
  ): Promise<GitExecResult> {
137
145
  const result = await pi.exec("git", args, {
138
146
  cwd: ctx.cwd,
139
147
  signal: options.signal,
140
148
  timeout: options.timeout ?? GIT_STATUS_TIMEOUT_MS,
141
149
  });
150
+ // Retries depend on the previous failure and must never run concurrently.
151
+ // The lock belongs to another git process and is never touched here.
152
+ if (result.killed || result.code === 0 || !GIT_LOCK_HELD_PATTERN.test(result.stderr) || options.signal?.aborted) {
153
+ return result;
154
+ }
155
+ if (Date.now() >= deadline) {
156
+ return { ...result, stderr: `${result.stderr.trimEnd()}\nThe lock was still held after waiting ${GIT_LOCK_WAIT_MS / 1000} s.` };
157
+ }
158
+ await sleep(GIT_LOCK_RETRY_INTERVAL_MS, undefined, { signal: options.signal });
159
+ return runGitWithLockRetry(pi, ctx, args, options, deadline);
160
+ }
161
+
162
+ export async function runGit(
163
+ pi: Pick<ExtensionAPI, "exec">,
164
+ ctx: GitCommandContext,
165
+ args: string[],
166
+ options: GitRunOptions = {},
167
+ ): Promise<GitExecResult> {
168
+ const result = await runGitWithLockRetry(pi, ctx, args, options, Date.now() + GIT_LOCK_WAIT_MS);
142
169
 
143
170
  if (result.killed) {
144
171
  throw new Error(formatGitFailure(args, result, options.tokens));
@@ -1283,6 +1310,25 @@ export async function validateWorktreeRemovalPath(
1283
1310
  };
1284
1311
  }
1285
1312
 
1313
+ /** Resolves an explicit worktreePath to the root of a live, non-bare worktree of the repository at ctx.cwd. */
1314
+ export async function resolveRepositoryWorktreeCwd(
1315
+ pi: Pick<ExtensionAPI, "exec">,
1316
+ ctx: GitCommandContext,
1317
+ worktreePath: unknown,
1318
+ signal?: AbortSignal,
1319
+ ): Promise<string> {
1320
+ const { canonicalPath, worktree } = await validateWorktreeRemovalPath(pi, ctx, worktreePath, signal);
1321
+ if (worktree.bare || worktree.prunable) {
1322
+ throw new Error("worktreePath must be an existing, non-bare worktree of the current repository.");
1323
+ }
1324
+ const targetCtx = { cwd: canonicalPath };
1325
+ if (await getCanonicalGitWorktreeRoot(pi, targetCtx, signal) !== canonicalPath ||
1326
+ await getCanonicalCommonGitDirectory(pi, targetCtx, signal) !== await getCanonicalCommonGitDirectory(pi, ctx, signal)) {
1327
+ throw new Error("worktreePath must be the root of a worktree of the current repository.");
1328
+ }
1329
+ return requireLosslessWorktreeIdentity(canonicalPath, "cwd");
1330
+ }
1331
+
1286
1332
  const GIT_BRANCH_FORMAT = "%(refname)%00%(objectname)%00%(HEAD)%00%(upstream)%00%(symref)%00%(upstream:track)";
1287
1333
 
1288
1334
  function branchTrackingCounts(upstream: string, track: string): AheadBehindCount {
@@ -45,6 +45,7 @@ import {
45
45
  rebaseCurrentBranch,
46
46
  removeWorktree,
47
47
  requireCurrentBranch,
48
+ resolveRepositoryWorktreeCwd,
48
49
  validateBranchName,
49
50
  withRepositoryMutationQueue,
50
51
  } from "../git.ts";
@@ -125,6 +126,7 @@ const BranchStatusParametersSchema = Type.Object(
125
126
  { additionalProperties: false },
126
127
  ),
127
128
  ),
129
+ worktreePath: Type.Optional(Type.String({ minLength: 1, description: "Absolute root path of another worktree of the same repository to act on; omit for the current one." })),
128
130
  },
129
131
  { additionalProperties: false },
130
132
  );
@@ -134,6 +136,7 @@ const IntegrateBranchParametersSchema = Type.Object(
134
136
  sourceBranch: Type.String({ minLength: 1, description: "Exact existing local source branch to integrate." }),
135
137
  targetBranch: Type.String({ minLength: 1, description: "Exact existing local target branch already checked out in the clean control worktree." }),
136
138
  keepConflicts: Type.Optional(Type.Boolean({ description: "When true, a conflicted merge is left in progress (MERGE_HEAD set, conflicted paths unmerged) for conclude_merge instead of being aborted; defaults to false." })),
139
+ worktreePath: Type.Optional(Type.String({ minLength: 1, description: "Absolute root path of another worktree of the same repository to act on; omit for the current one." })),
137
140
  },
138
141
  { additionalProperties: false },
139
142
  );
@@ -588,10 +591,12 @@ export function registerBranchMeTools(host: Pick<ExtensionAPI, "registerTool" |
588
591
  "Use branch_status as a read-only refresh; branch_status never mutates files, Git state, or GitHub state.",
589
592
  "Use targeted branch_status ancestry verification only after integrate_branch completes; do not issue branch_status in the same parallel tool batch as integrate_branch.",
590
593
  "Use branch_status ancestry with a remote-tracking target such as origin/main after fetch_branch completes to prove a branch is contained in the fetched baseline; branch_status never checks out or resets remote-tracking refs.",
594
+ "branch_status worktreePath lets one session act on another worktree of the same repository; omit it for the current one.",
591
595
  ],
592
596
  parameters: BranchStatusParametersSchema,
593
597
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
594
- const details = await collectGitContext(pi, ctx, {
598
+ const target = params.worktreePath === undefined ? ctx : { cwd: await resolveRepositoryWorktreeCwd(pi, ctx, params.worktreePath, signal) };
599
+ const details = await collectGitContext(pi, target, {
595
600
  signal,
596
601
  ancestry: params.ancestry,
597
602
  env: options.env,
@@ -599,7 +604,7 @@ export function registerBranchMeTools(host: Pick<ExtensionAPI, "registerTool" |
599
604
  });
600
605
 
601
606
  try {
602
- details.githubRepository ??= await resolveGitHubRepository(pi, ctx, signal, options.env);
607
+ details.githubRepository ??= await resolveGitHubRepository(pi, target, signal, options.env);
603
608
  } catch (error) {
604
609
  if (signal?.aborted) throw error;
605
610
  // Repository resolution is optional compatibility metadata; context collection is authoritative.
@@ -783,10 +788,12 @@ export function registerBranchMeTools(host: Pick<ExtensionAPI, "registerTool" |
783
788
  "After integrate_branch completes, run any targeted branch_status ancestry proof separately; never place integrate_branch and branch_status in the same parallel tool batch.",
784
789
  "A conflict result from integrate_branch means the merge was automatically aborted and restoration was verified unless keepConflicts is true; semantic conflict analysis belongs to a separate delegated workflow, not integrate_branch.",
785
790
  "Pass keepConflicts: true to integrate_branch only when a file-editing step will resolve the listed paths; the result conflict_kept leaves the merge in progress with MERGE_HEAD set, and conclude_merge must then conclude or abort it before any other Git mutation.",
791
+ "integrate_branch worktreePath lets one session act on another worktree of the same repository; omit it for the current one.",
786
792
  ],
787
793
  parameters: IntegrateBranchParametersSchema,
788
794
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
789
- const details = await integrateBranch(pi, ctx, params, signal);
795
+ const target = params.worktreePath === undefined ? ctx : { cwd: await resolveRepositoryWorktreeCwd(pi, ctx, params.worktreePath, signal) };
796
+ const details = await integrateBranch(pi, target, params, signal);
790
797
  return {
791
798
  content: [{ type: "text", text: formatIntegrateBranch(details) }],
792
799
  details,
@@ -6,7 +6,7 @@ import {
6
6
  LABEL_PULL_REQUEST_TOOL_NAME, PULL_REQUEST_FILES_TOOL_NAME, PULL_REQUEST_LABEL_LIMIT, PULL_REQUEST_STATUS_TOOL_NAME,
7
7
  REPLY_PULL_REQUEST_FEEDBACK_TOOL_NAME, TRACK_BRANCH_TOOL_NAME, UPDATE_FROM_BASE_TOOL_NAME,
8
8
  } from "../constants.ts";
9
- import { getGitRoot, requireCurrentBranch, safeWorktreeValue } from "../git.ts";
9
+ import { getGitRoot, requireCurrentBranch, resolveRepositoryWorktreeCwd, safeWorktreeValue } from "../git.ts";
10
10
  import { concludeMerge, formatConflictPathList } from "../git-integration.ts";
11
11
  import { findGitHubPullRequest, getGitHubPullRequest, resolveGitHubRepository, resolveGitHubToken } from "../github.ts";
12
12
  import {
@@ -222,14 +222,17 @@ export function registerWorkflowTools(pi: Pick<ExtensionAPI, "registerTool" | "e
222
222
  "Before conclude_merge with action conclude, a file-editing step must remove every <<<<<<<, |||||||, =======, and >>>>>>> marker line from the listed paths; conclude_merge refuses and names the paths that still contain markers.",
223
223
  "conclude_merge with action conclude stages only the currently unmerged paths and uses Git's prepared merge message; unrelated unstaged edits stay unstaged and are not part of the merge commit. Git commits the entire index, including previously staged entries: do not stage unrelated edits during the merge.",
224
224
  "conclude_merge with action abort restores the branch with git merge --abort and verifies a clean idle checkout; conclude_merge refuses when no merge is in progress.",
225
+ "conclude_merge worktreePath lets one session act on another worktree of the same repository; omit it for the current one.",
225
226
  ],
226
227
  parameters: Type.Object({
227
228
  action: StringEnum(["conclude", "abort"] as const, {
228
229
  description: "conclude stages the resolved formerly unmerged paths and commits the merge; abort runs git merge --abort and verifies restoration.",
229
230
  }),
231
+ worktreePath: Type.Optional(Type.String({ minLength: 1, description: "Absolute root path of another worktree of the same repository to act on; omit for the current one." })),
230
232
  }, { additionalProperties: false }),
231
233
  async execute(_id, params, signal, _update, ctx) {
232
- const details = await concludeMerge(pi, ctx, params, signal);
234
+ const target = params.worktreePath === undefined ? ctx : { cwd: await resolveRepositoryWorktreeCwd(pi, ctx, params.worktreePath, signal) };
235
+ const details = await concludeMerge(pi, target, params, signal);
233
236
  return { content: [{ type: "text", text: formatConcludeMerge(details) }], details };
234
237
  },
235
238
  });
package/src/types.ts CHANGED
@@ -88,6 +88,8 @@ export interface IntegrateBranchToolInput {
88
88
  targetBranch: string;
89
89
  /** Leave a conflicted merge in progress for conclude_merge instead of aborting it. */
90
90
  keepConflicts?: boolean;
91
+ /** Absolute root of another worktree of the same repository; omitted means the current one. */
92
+ worktreePath?: string;
91
93
  }
92
94
 
93
95
  export interface RetireBranchToolInput {
@@ -448,6 +450,8 @@ export interface IntegrateBranchConflictKeptDetails {
448
450
  status: "conflict_kept";
449
451
  mergeExecuted: true;
450
452
  request: IntegrateBranchToolInput;
453
+ /** Canonical root of the worktree holding the kept merge. */
454
+ worktreeRoot: string;
451
455
  /** targetHead is the unchanged HEAD; sourceHead is the commit recorded in MERGE_HEAD. */
452
456
  heads: IntegrateBranchHeads;
453
457
  conflict: {
@@ -469,6 +473,8 @@ export type ConcludeMergeAction = "conclude" | "abort";
469
473
 
470
474
  export interface ConcludeMergeToolInput {
471
475
  action: ConcludeMergeAction;
476
+ /** Absolute root of another worktree of the same repository; omitted means the current one. */
477
+ worktreePath?: string;
472
478
  }
473
479
 
474
480
  export interface ConcludeMergeConcludedDetails {