@senad-d/branchme 0.3.1 → 0.3.2

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,13 +1,17 @@
1
1
  # Changelog
2
2
 
3
- ## 0.3.1 - Unreleased
3
+ ## 0.3.2 - Unreleased
4
4
 
5
+ - Allowed missing worktree parent directories, resolved through the nearest existing directory ancestor without writing during validation. Non-directory and dangling-symlink ancestors remain rejected; Git creates missing parents only after all preflight checks pass.
6
+ - Parallelized independent read-only worktree-path, Git-operation-marker, and upstream-configuration lookups while preserving result order and validation.
7
+ - Updated the Pi development packages to `1.0.2`, replacing vulnerable transitive `brace-expansion` and `undici` versions with `5.0.12` and `8.10.2`. Updated the Git-context smoke verifier to use host-resolved imports and Pi's transcript APIs instead of the old nested dependency layout.
5
8
  - Added bounded `list_branches` discovery with local/remote-tracking identity, cached upstream ahead/behind counts, symbolic refs, and worktree occupancy.
6
9
  - Added `track_branch` to fetch and verify a new local tracking checkout, and `update_from_base` to merge a freshly fetched base into the current feature without rewriting published history. Both require clean idle checkouts and narrow fetch scope.
7
10
  - Added `pull_request_status` for validated same-repository open/closed/merged PR lifecycle and commit identities. PR creation now returns `created` or `existing`, preserves existing PR fields, and rechecks HTTP 422 creation races without repeating the POST.
8
11
  - Added optional `land_branch.pullRequestNumber` for squash/rebase cleanup: exact merged head/base and merge-commit containment are verified against the configured GitHub remote while retaining local deletion leases and truthful graph ancestry.
9
12
  - Corrected stale fetch, ancestry, worktree base-ref, tool-count, and merge-policy documentation; expanded strict schema, real-Git, mocked GitHub, and runtime smoke coverage.
10
13
  - Hardened tracking/base-update/landing fetches against symbolic destinations, preserved upstream configuration checks when missing cached refs are restored, blocked unsafe initialization environments and inconclusive discovery, retained inspection guidance after initialization verification failures, and redacted PR cancellation errors before truncation.
14
+ - Aligned current and historical documentation with version `0.3.1`, the nineteen-tool inventory, GitHub token use across PR status/landing/creation, and the implemented workflow module layout.
11
15
 
12
16
  - Added `init_repository` for verified initialization of pi's exact current directory as a non-bare Git repository with an optional initial branch (default `main`). It rejects reinitialization and nested repositories, accepts no path or repository-mode controls, creates no commit or project files, and verifies the in-place `.git` directory and unborn branch.
13
17
  - Added `land_branch` for deterministic post-host-merge cleanup in one queued call: explicit cwd-independent Git routing from the primary repository root, targeted remote fetch and captured ancestry proof, force-free linked-worktree removal including ignored residue, expected-HEAD-leased local source deletion against the fetched remote target, and independent final target sync (`fetch-refspec`, `pull-ff`, `skipped-dirty`, or `noop`).
package/README.md CHANGED
@@ -91,9 +91,9 @@ A typical BranchMe flow is:
91
91
  4. Update it from its configured upstream with `pull_branch`, which requires a clean worktree and uses fast-forward-only semantics.
92
92
  5. Create from the updated `HEAD` with `create_branch`.
93
93
  6. Make edits and commit outside BranchMe.
94
- 7. Push the current branch with `push_branch`.
95
- 8. After `push_branch` completes and GitHub can see the branches, create or reuse a matching open pull request with `pull_request`.
96
- 9. While working, use `update_from_base({ baseBranch: "main" })` to fetch and merge the remote base into the clean feature branch without rewriting published history or changing its upstream.
94
+ 7. While working, use `update_from_base({ baseBranch: "main" })` to fetch and merge the remote base into the clean feature branch without rewriting published history or changing its upstream.
95
+ 8. Push the current branch with `push_branch`.
96
+ 9. After `push_branch` completes and GitHub can see the branches, create or reuse a matching open pull request with `pull_request`.
97
97
  10. Inspect the PR with `pull_request_status({ number: 123 })`. Merge it on GitHub outside BranchMe, then use `land_branch` from the primary checkout. Supply `pullRequestNumber: 123` for squash/rebase merge evidence.
98
98
 
99
99
  For isolated work, a specialized Git subagent can use the explicit worktree workflow:
@@ -175,7 +175,7 @@ git remote set-url origin git@github.com:OWNER/REPO.git
175
175
  export GITHUB_REPOSITORY=OWNER/REPO
176
176
  ```
177
177
 
178
- For automatic related-PR lookup and `pull_request`, set a token in the process environment before starting pi:
178
+ For automatic related-PR lookup, `pull_request_status`, PR-aware `land_branch`, and `pull_request`, set a token in the process environment before starting pi:
179
179
 
180
180
  ```bash
181
181
  export GITHUB_TOKEN=github_pat_...
@@ -205,8 +205,8 @@ BranchMe has no separate project config file. It reads process environment varia
205
205
 
206
206
  | Variable | Meaning |
207
207
  | --- | --- |
208
- | `GITHUB_TOKEN` | Preferred token for automatic related-PR lookup and `pull_request`; process environment first, then local `.env` fallback. |
209
- | `GH_TOKEN` | Fallback token for automatic related-PR lookup and `pull_request`; process environment first, then local `.env` fallback. |
208
+ | `GITHUB_TOKEN` | Preferred token for automatic related-PR lookup, `pull_request_status`, PR-aware `land_branch`, and `pull_request`; process environment first, then local `.env` fallback. |
209
+ | `GH_TOKEN` | Fallback token for the same GitHub API operations; process environment first, then local `.env` fallback. |
210
210
  | `BRANCHME_PR_AUTOFILL=true` | Allow `pull_request` to fill omitted PR fields. Accepts `true`/`false`, `1`/`0`, `yes`/`no`, or `on`/`off`; disabled by default. |
211
211
  | `GITHUB_REPOSITORY=owner/repo` | Optional CI fallback and boundary check for the current GitHub repository; process environment only. |
212
212
 
@@ -261,7 +261,7 @@ Commands are informational only. BranchMe actions are performed by agent-callabl
261
261
  | `integrate_branch` | `{ "sourceBranch": string, "targetBranch": 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. It never fetches or pushes. |
262
262
  | `create_branch` | `{ "branchName": string }` | Validates `branchName`, rejects existing local branches, and runs `git switch -c <branchName>` from current `HEAD`. |
263
263
  | `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. |
264
- | `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 creates a pull request in the resolved current repository. Omitted fields require `BRANCHME_PR_AUTOFILL=true`; branch refs must be distinct, exist locally, and cannot use `owner:branch`. |
264
+ | `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`. |
265
265
 
266
266
  All schemas reject additional properties. `init_repository` accepts only optional `initialBranch`, never a path or repository-mode controls. `change_branch` never accepts `baseRef`, `force`, `stash`, `discard`, `create`, `owner`, `repo`, or path inputs. `fetch_branch` accepts only the optional `remote` and `branch` pair for a targeted remote-tracking refresh (`remote` requires `branch`) and never accepts refspec, tags, prune, or force controls. `pull_branch` and `rebase_branch` have strict empty schemas and never accept a branch, remote, refspec, force, autostash, or arbitrary rebase target. `integrate_branch` requires exactly `sourceBranch` and `targetBranch`; it accepts no repository, path, remote, strategy, message, squash, signing, commit, continuation, abort, force, fetch, push, deletion, or worktree controls. `create_worktree` requires exactly `worktreePath`, `branchName`, and `branchMode`, plus an optional read-only `baseRef` start point for `branchMode: "new"`; `remove_worktree` requires `worktreePath` and accepts optional boolean `deleteIgnored`. No worktree tool accepts force, move, prune, repair, lock, unlock, detached, orphan, remote, or refspec controls. `retire_branch` requires exactly `branchName`, a full 40- or 64-hex-character `expectedHead`, a distinct local `targetBranch`, and the boolean `force` decision; it accepts no repository, path, remote, refspec, pattern, branch list, prune, remote-delete, worktree-removal, or inferred-target control. `pull_request` never accepts `owner`, `repo`, or owner-prefixed branch refs; BranchMe resolves the repository from local `origin` and/or matching `GITHUB_REPOSITORY`. `continue_merge` and `abort_merge` are not available.
267
267
 
@@ -339,7 +339,7 @@ with `branch_status` only after `integrate_branch` has returned. Do not batch th
339
339
 
340
340
  ### Linked worktree verification and handoff
341
341
 
342
- `create_worktree` requires a non-blank absolute path with no control characters. Its immediate parent must already be a directory. BranchMe resolves that parent to build a canonical destination, rejects any existing file, directory, or symlink there, and rejects destinations inside a registered worktree or the repository's common Git directory. Before mutation, the canonical path and local branch must be returnable without redaction, escaping, Unicode alteration, or truncation: canonical paths are limited to 4,096 characters and branch identities to 512 characters, and credential-like token text is rejected. A dirty source worktree is allowed because creation does not switch or overwrite it.
342
+ `create_worktree` requires a non-blank absolute path with no control characters. Missing parent directories are allowed: BranchMe resolves the nearest existing directory ancestor to build a canonical destination, rejecting non-directory or dangling-symlink ancestors. Git creates missing directories only after all preflight checks pass. BranchMe rejects any existing file, directory, or symlink there, and rejects destinations inside a registered worktree or the repository's common Git directory. Before mutation, the canonical path and local branch must be returnable without redaction, escaping, Unicode alteration, or truncation: canonical paths are limited to 4,096 characters and branch identities to 512 characters, and credential-like token text is rejected. A dirty source worktree is allowed because creation does not switch or overwrite it.
343
343
 
344
344
  For `branchMode: "new"`, BranchMe creates the requested local branch from the current `HEAD`, or — when the optional `baseRef` is supplied — from that exact existing local branch, remote-tracking ref (for example `origin/main`), or full commit. `baseRef` is resolved to a commit before mutation and is only read: it is never checked out, reset, or given upstream configuration, and the current checkout's branch, dirt, or staleness never blocks it. For `branchMode: "existing"`, it uses only an existing local branch that is not checked out in another worktree and rejects `baseRef`; it never infers a local branch from a remote. After Git succeeds, BranchMe re-lists worktrees and verifies the canonical path, local branch, `HEAD`, and clean checkout. A representative structured result subset is:
345
345
 
@@ -509,7 +509,7 @@ Ensure the token and Git credentials have permission for the branch and pull req
509
509
  | Detached `HEAD` | Use `change_branch` to switch to an existing local branch, or checkout a branch before `fetch_branch`, `pull_branch`, `rebase_branch`, `create_branch`, or `push_branch`. |
510
510
  | Branch already exists | Choose a new local branch name for `create_branch`, or use `change_branch` to switch to it. |
511
511
  | Branch does not exist locally | Use `track_branch` for an existing remote branch; `change_branch` and `create_worktree` existing mode still require local branches. |
512
- | Worktree destination rejected | Provide an exact absolute path whose immediate parent exists; the destination must not exist or be inside another registered worktree or the repository's common Git directory. Its canonical path and branch identity must also fit the documented limits without credential-like token text or characters that require escaping. |
512
+ | Worktree destination rejected | Provide an exact absolute path with a resolvable directory ancestor (missing parents are allowed); the destination must not exist or be inside another registered worktree or the repository's common Git directory. Its canonical path and branch identity must also fit the documented limits without credential-like token text or characters that require escaping. |
513
513
  | Existing worktree branch is occupied | Choose another existing local branch or remove its other linked checkout after cleaning it; BranchMe does not force multiple checkouts. |
514
514
  | Worktree removal rejected | Use `list_worktrees` and select a non-main/non-current linked worktree. Remove or preserve staged, unstaged, untracked, and unmerged files. Preserve valuable ignored files, then retry with `deleteIgnored: true` if deleting the remaining ignored residue is intended. Locked, detached, prunable/missing, bare, and foreign paths are not removable. |
515
515
  | Linked-worktree agent cannot find credentials | Pass credentials through the process environment. BranchMe does not copy repository-root `.env` or other ignored/untracked files. |
package/SECURITY.md CHANGED
@@ -20,7 +20,7 @@ Implemented git mutations are limited to:
20
20
  - `create_branch`: `git switch -c <branchName>` from current `HEAD` after branch-name validation and existing-branch checks.
21
21
  - `track_branch`: narrow fetch followed by `git switch --no-overwrite-ignore --track=direct -c <branchName> refs/remotes/<remote>/<remoteBranch>`. Requires a new local branch, clean idle checkout, direct fetched commit ref, and verified final HEAD/upstream.
22
22
  - `update_from_base`: narrow fetch followed by the fixed integration merge policy against a captured remote-base commit. Reuses verified outcomes and automatic conflict abort; does not rewrite published history or change upstream. Both new fetch workflows explicitly disable pruning, extra ref mappings, tags, and submodule recursion, and reject symbolic remote-tracking destinations before fetching so Git cannot dereference them into unrelated refs. Base updates compare stored upstream configuration, not whether cached upstream refs happen to resolve.
23
- - `fetch_branch`: `git fetch --no-tags --no-recurse-submodules <upstreamRemote> <upstreamBranchRef>:<remoteTrackingRef>` after validating the current branch's configured upstream target. The explicit destination is limited to that upstream's remote-tracking ref, so local branches and working-tree files are not changed.
23
+ - `fetch_branch`: `git fetch --no-tags --no-recurse-submodules <remote> <remoteBranchRef>:<remoteTrackingRef>`. With no arguments it validates and fetches the current branch's configured upstream; with explicit `branch` and optional configured `remote` (default `origin`) it fetches that exact remote branch. The explicit destination is limited to the selected remote-tracking ref, so local branches and working-tree files are not changed.
24
24
  - `pull_branch`: `git pull --ff-only --no-rebase --no-autostash <upstreamRemote> <upstreamBranchRef>` for the clean current branch after validating its configured upstream target.
25
25
  - `rebase_branch`: `git rebase --no-autostash --no-update-refs <upstream>` for the clean current branch after validating its configured upstream target. It rewrites local commits and automatically attempts `git rebase --abort` without the cancelled caller signal if the rebase fails or is killed.
26
26
  - `integrate_branch`: after rejecting a non-empty `branch.<targetBranch>.mergeOptions` setting, runs `git -c rerere.enabled=false merge --ff --no-edit --no-autostash --no-rerere-autoupdate --no-overwrite-ignore refs/heads/<sourceBranch>` from the verified clean control worktree, which must already have the distinct existing local `targetBranch` checked out. It uses normal merge semantics: no-op, fast-forward, or a Git-generated standard two-parent merge commit for divergent histories.
@@ -55,7 +55,7 @@ GET https://api.github.com/repos/{owner}/{repo}/pulls/{number}
55
55
  GET https://api.github.com/repos/{owner}/{repo}/pulls?state={open|all}&head={owner}:{branch}&sort=updated&direction=desc&per_page={1|2}
56
56
  ```
57
57
 
58
- Explicit `pull_request_status`, idempotent `pull_request` lookup, and PR-aware `land_branch` use the last two read-only endpoints. Each request has a 10-second transport/body deadline and 64 KiB response limit. Exact PR number, repository, branch identities, and commit IDs are validated. Status does not certify CI checks or review approvals. Idempotent creation preserves existing PR fields and reuses only an exact head-SHA/base match; HTTP 422 races get one read-only recheck.
58
+ Explicit `pull_request_status`, idempotent `pull_request` lookup, and PR-aware `land_branch` use the last two read-only endpoints. Each request has a 10-second transport/body deadline and 64 KiB response limit. Exact PR number, repository, branch identities, and commit IDs are validated. Status does not certify CI checks or review approvals. Idempotent creation preserves existing PR fields and reuses only an exact head-SHA/base match; HTTP 422 races get one read-only recheck. All of these operations require `GITHUB_TOKEN` or `GH_TOKEN` from the process environment or verified-root `.env` fallback.
59
59
 
60
60
  `list_branches` is an explicit read-only local inventory, limited to 200 returned refs, 128 KiB raw ref output, and 4,000 characters of text. Paths and names are display-safe metadata, not executable handoffs. Upstream counts reflect cached refs, not a fresh remote fetch.
61
61
 
@@ -86,7 +86,7 @@ The branch preflight requests have no body. BranchMe uses the resolved `headBran
86
86
 
87
87
  ## Worktree filesystem boundary
88
88
 
89
- Linked worktree management expands the mutation boundary beyond the active checkout. `create_worktree` may create a directory anywhere the user can write when the explicitly supplied destination passes all checks. BranchMe requires a non-blank absolute path without control characters, requires the immediate parent to exist as a directory, resolves that parent with `realpath`, and rejects any existing destination—including a symlink. It also rejects a destination inside any registered worktree or inside the current repository's common Git directory. Before mutation, the resulting canonical path and local branch must remain exactly identical as JavaScript strings after BranchMe's redaction, control/format escaping, Unicode-safe truncation, and size checks; the path limit is 4,096 characters and the branch limit is 512 characters.
89
+ Linked worktree management expands the mutation boundary beyond the active checkout. `create_worktree` may create a directory anywhere the user can write when the explicitly supplied destination passes all checks. BranchMe requires a non-blank absolute path without control characters, resolves the nearest existing directory ancestor with `realpath` without writing, rejects non-directory and dangling-symlink ancestors, and rejects any existing destination—including a symlink. Git creates any missing parent directories only after path, repository-boundary, branch, and base-ref validation succeeds. It also rejects a destination inside any registered worktree or inside the current repository's common Git directory. Before mutation, the resulting canonical path and local branch must remain exactly identical as JavaScript strings after BranchMe's redaction, control/format escaping, Unicode-safe truncation, and size checks; the path limit is 4,096 characters and the branch limit is 512 characters.
90
90
 
91
91
  `remove_worktree` never passes an unverified user path to Git. It canonicalizes the supplied absolute path, requires an exact match in a fresh inventory belonging to the current repository, applies the same lossless checks to the canonical path and retained branch, and rejects the main worktree, the worktree containing the active Pi session, bare, detached, locked, prunable/missing, and dirty worktrees. A separate bounded porcelain scan detects ignored files and directories. They block removal by default without path disclosure; explicit `deleteIgnored: true` authorizes deleting all of them with the worktree and reports only redacted top-level paths, never contents. After force-free removal, BranchMe verifies the worktree is no longer registered and that its local branch remains at the captured `HEAD`.
92
92
 
@@ -126,7 +126,7 @@ Only the local branch ref is deleted. Local `branch.<branchName>.*` configuratio
126
126
 
127
127
  Git fetch, pull, and push authentication is handled by the user's configured Git credential and transport setup. BranchMe never passes GitHub API tokens to Git commands.
128
128
 
129
- `pull_request` and related-PR lookup check `process.env.GITHUB_TOKEN`, then `process.env.GH_TOKEN`. If neither process token is set, BranchMe reads a local `.env` file from the verified git root and checks:
129
+ Automatic related-PR lookup, `pull_request_status`, PR-aware `land_branch`, and `pull_request` check `process.env.GITHUB_TOKEN`, then `process.env.GH_TOKEN`. If neither process token is set, BranchMe reads a local `.env` file from the verified git root and checks:
130
130
 
131
131
  - `GITHUB_TOKEN` (preferred)
132
132
  - `GH_TOKEN` (fallback)
@@ -143,7 +143,7 @@ The automatic snapshot can become stale after a Git or filesystem mutation durin
143
143
 
144
144
  ## Telemetry
145
145
 
146
- BranchMe does not collect telemetry. Related-PR lookup sends only the resolved repository owner/name and current branch in the authenticated GitHub API URL. PR creation sends only the resolved GitHub pull request fields described above. When autofill supplies title or body, those fields can contain bounded, redacted local commit subjects. BranchMe does not send diff contents, filenames, or local file contents to GitHub during context collection.
146
+ BranchMe does not collect telemetry. Related-PR lookup and explicit PR lifecycle reads send only the resolved repository owner/name plus the current/requested branch or PR number in authenticated GitHub API URLs. PR-aware landing additionally reads the supplied PR number to verify host-merge evidence. PR creation sends only the resolved GitHub pull request fields described above. When autofill supplies title or body, those fields can contain bounded, redacted local commit subjects. BranchMe does not send diff contents, filenames, or local file contents to GitHub during context collection or PR lifecycle verification.
147
147
 
148
148
  ## Reporting vulnerabilities
149
149
 
@@ -1,6 +1,6 @@
1
1
  # Project Definition Brief
2
2
 
3
- Originally approved on 2026-06-30. Updated to describe the implemented `0.3.0` package.
3
+ Originally approved on 2026-06-30. Updated to describe the implemented `0.3.1` package.
4
4
 
5
5
  ## 1. Bootstrap history
6
6
 
@@ -83,7 +83,9 @@ Originally approved on 2026-06-30. Updated to describe the implemented `0.3.0` p
83
83
  - `src/git-context.ts`
84
84
  - `src/commands/branchme-command.ts`
85
85
  - `src/tools/branchme-tools.ts`
86
+ - `src/tools/workflow-tools.ts`
86
87
  - `src/git.ts`
88
+ - `src/git-workflow.ts`
87
89
  - `src/git-integration.ts`
88
90
  - `src/git-retirement.ts`
89
91
  - `src/git-landing.ts`
@@ -98,7 +100,7 @@ Originally approved on 2026-06-30. Updated to describe the implemented `0.3.0` p
98
100
  - The general Git helper owns verified current-directory initialization plus reusable argv-style current-repository inspection, branch/ref/ancestry and operation-state primitives, branch/upstream workflows, worktree parsing/path validation/create/remove verification, and process-local same-repository mutation serialization.
99
101
  - The integration module owns the clean-control preflight, fixed merge mutation, automatic conflict abort, outcome classification, and repository/ref/worktree/ancestry verification without absorbing that state machine into the general helper.
100
102
  - The retirement module owns exact request validation, direct-ref and expected-`HEAD` preflight, complete worktree occupancy checks, target ancestry, leased local-ref deletion, cancellation-safe verification, and bounded uncertain outcomes without absorbing that state machine into the general helper.
101
- - The GitHub helper owns repository resolution, token/autofill configuration, related-PR lookup, branch visibility and commit preflight, and pull request REST calls.
103
+ - The GitHub helper owns repository resolution, token/autofill configuration, related-PR and PR-lifecycle lookup, branch visibility and commit preflight, idempotent pull request creation/reuse, and merged-PR evidence used by landing.
102
104
  - The redaction module owns shared credential redaction for display and prompt-bound metadata.
103
105
  - Shared public details remain JSON-serializable and contain no runtime objects or abort signals.
104
106
  - Dependencies:
@@ -109,7 +111,7 @@ Originally approved on 2026-06-30. Updated to describe the implemented `0.3.0` p
109
111
 
110
112
  ## 6. Configuration, state, and filesystem boundary
111
113
 
112
- - Config source: no separate BranchMe config file. `GITHUB_TOKEN`, `GH_TOKEN`, and `BRANCHME_PR_AUTOFILL` use process-environment values first and may fall back to supported keys in a hardened regular `.env` file at the verified Git root.
114
+ - Config source: no separate BranchMe config file. `GITHUB_TOKEN`, `GH_TOKEN`, and `BRANCHME_PR_AUTOFILL` use process-environment values first and may fall back to supported keys in a hardened regular `.env` file at the verified Git root. Tokens authenticate automatic related-PR lookup, explicit PR status, PR-aware landing evidence, and PR creation/reuse.
113
115
  - Session state: no persisted BranchMe state. Tool calls return serializable details, and mutation/PR coordination is in-memory and process-local only; other Pi sessions and external Git processes are not locked.
114
116
  - Repository initialization: `init_repository` creates in-place `.git` metadata only in pi's canonical current directory after rejecting existing or nested repositories; it creates an unborn branch but no commit or project file.
115
117
  - Active-checkout mutations: explicit branch switching, creation, pull, rebase, and integration operations can update Git metadata and working-tree files through Git. Explicit retirement can delete one exact local branch ref. Push and fetch operations can update remote or remote-tracking refs through Git.
@@ -121,7 +123,7 @@ Standalone removal retains its branch and requires explicit `deleteIgnored: true
121
123
  ## 7. Worktree handoff contract
122
124
 
123
125
  - `list_worktrees` reads bounded NUL-delimited porcelain inventory and keeps worktree discovery out of automatic active-worktree context.
124
- - `create_worktree` requires an explicitly approved absolute destination whose immediate parent exists. It rejects existing destinations and locations inside registered worktrees or the repository's common Git directory.
126
+ - `create_worktree` requires an explicitly approved absolute destination with a resolvable directory ancestor. Missing parents are created by Git only after validation. It rejects non-directory or dangling-symlink ancestors, existing destinations, and locations inside registered worktrees or the repository's common Git directory.
125
127
  - New mode creates a local branch from current `HEAD` or an explicit read-only `baseRef` (local branch, remote-tracking ref, or full commit). Existing mode accepts only an existing local branch not checked out in another worktree; no remote branch is inferred.
126
128
  - Before mutation, canonical cwd and branch identity must fit documented limits and remain unchanged by redaction, escaping, Unicode handling, or truncation.
127
129
  - Successful creation verifies canonical path, local branch, full `HEAD`, and clean state, then returns the exact canonical absolute cwd and local branch in `handoff: { cwd, branch, head, ready: true, summary }`.
@@ -171,7 +173,7 @@ Standalone removal retains its branch and requires explicit `deleteIgnored: true
171
173
  - `SECURITY.md` documents local filesystem, Git, GitHub, credential, and prompt-insertion boundaries.
172
174
  - `docs/STRUCTURE.md` describes the implemented source and test layout.
173
175
  - `docs/SMOKE_TEST.md` records isolated checkout, handoff, and installed-package smoke behavior.
174
- - `CHANGELOG.md` tracks the active `0.3.0` unreleased changes.
176
+ - `CHANGELOG.md` tracks the active `0.3.1` unreleased changes.
175
177
  - npm distribution uses package `@senad-d/branchme`; package-content checks exclude private specs, credentials, generated files, caches, and local state.
176
178
 
177
179
  ## 12. Validation plan
package/docs/STRUCTURE.md CHANGED
@@ -34,7 +34,7 @@ src/
34
34
  5. `src/git.ts` owns verified current-directory repository initialization plus reusable current-repository Git primitives: root and canonical common-Git-directory identity, strict direct local-ref inspection, branch/ref validation, local commit ancestry, operation-state and working-tree inspection, complete bounded worktree occupancy, branch/upstream workflows, worktree parsing/path validation/create/remove verification, and the process-local per-repository mutation queue.
35
35
  6. `src/git-integration.ts` owns the integration preflight, one-window merge mutation, conflict-path capture, automatic abort, outcome classification, and repository/ref/worktree/ancestry postcondition verification. It never fetches, pushes, resets, switches branches, or continues merges.
36
36
  7. `src/git-retirement.ts` owns strict runtime input validation, expected-`HEAD` and target-ancestry preflight, complete worktree-occupancy rejection, expected-old-value `update-ref` deletion, cancellation-safe final inspection, merged/forced-unmerged classification, and bounded uncertain errors. It never deletes remote or remote-tracking refs, removes worktrees, edits branch configuration, or performs reset rollback.
37
- 8. `src/github.ts` owns GitHub `owner/repo` parsing, repository boundary checks, `GITHUB_TOKEN`/`GH_TOKEN` and `BRANCHME_PR_AUTOFILL` process-env/hardened git-root `.env` resolution, authenticated related-open-PR lookup, PR branch-name syntax validation, GitHub branch visibility/commit preflight, PR REST calls, bounded response validation, and redacted errors.
37
+ 8. `src/github.ts` owns GitHub `owner/repo` parsing, repository boundary checks, `GITHUB_TOKEN`/`GH_TOKEN` and `BRANCHME_PR_AUTOFILL` process-env/hardened git-root `.env` resolution, authenticated related/open/all PR lookup, PR lifecycle and merged-evidence validation, PR branch-name syntax validation, GitHub branch visibility/commit preflight, idempotent PR creation/reuse, bounded response validation, and redacted errors.
38
38
  9. `src/redaction.ts` owns shared credential redaction for Git, GitHub, and prompt-bound metadata.
39
39
  10. `src/types.ts` keeps serializable details shared by helpers, context, and tools.
40
40
  11. `src/ui/branchme-panel.ts` renders a compact status panel and clips lines to terminal width.
@@ -73,7 +73,7 @@ src/
73
73
  - Git hooks, custom merge drivers, clean/smudge filters, signing policy, and `reference-transaction` hooks stay active where Git invokes them and may execute commands or network operations outside BranchMe's direct argv boundary.
74
74
  - `create_branch` mutates local branch/HEAD only with `git switch -c`.
75
75
  - `list_worktrees` reads a bounded `git worktree list --porcelain -z` inventory and remains explicit rather than expanding automatic active-worktree context.
76
- - `create_worktree` requires an explicitly approved absolute destination, canonicalizes its existing parent, rejects existing or nested/common-Git-directory destinations, and creates from current `HEAD`, from an explicit read-only `baseRef` (exact local branch, remote-tracking ref, or full commit resolved to a commit before mutation and never checked out or reset), or from an unoccupied existing local branch. It verifies canonical path, branch, `HEAD`, and cleanliness before returning a ready handoff.
76
+ - `create_worktree` requires an explicitly approved absolute destination, canonicalizes its nearest existing directory ancestor, rejects non-directory/dangling ancestors and existing or nested/common-Git-directory destinations, lets Git create missing parents after validation, and creates from current `HEAD`, from an explicit read-only `baseRef` (exact local branch, remote-tracking ref, or full commit resolved to a commit before mutation and never checked out or reset), or from an unoccupied existing local branch. It verifies canonical path, branch, `HEAD`, and cleanliness before returning a ready handoff.
77
77
  - `remove_worktree` resolves an exact fresh current-repository inventory match, rejects main/current/dirty/detached/locked/prunable/missing/bare entries, and performs a bounded ignored-entry scan. Ignored residue is refused by default; optional `deleteIgnored: true` explicitly authorizes deleting it and reports redacted top-level paths. Removal remains force-free and confirms that the local branch remains at the same commit.
78
78
  - `land_branch` fetches the exact remote target, requires source ancestry or exact merged GitHub evidence via `pullRequestNumber`, refuses a cwd inside the removal target, removes only a safe linked source/target checkout (including ignored residue), and lease-deletes only the source local ref against the fetched remote-tracking target. Target sync is last and uses a non-forced fetch refspec, a clean-worktree ff-only pull, dirty-skip, or no-op. Failed/unattempted sync is explicit; receipts include actual before/after refs and top-level deleted ignored paths. Standalone removal/retirement defaults are unchanged.
79
79
  - `push_branch` mutates remote refs only for the current branch and uses an explicit upstream remote/refspec instead of bare `git push` when an upstream exists.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senad-d/branchme",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
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",
@@ -74,11 +74,16 @@
74
74
  "typebox": "*"
75
75
  },
76
76
  "devDependencies": {
77
- "@earendil-works/pi-ai": "^0.84.1",
78
- "@earendil-works/pi-coding-agent": "^0.84.1",
79
- "@earendil-works/pi-tui": "^0.84.1",
77
+ "@earendil-works/pi-ai": "^1.0.2",
78
+ "@earendil-works/pi-coding-agent": "^1.0.2",
79
+ "@earendil-works/pi-tui": "^1.0.2",
80
80
  "@types/node": "^26.0.1",
81
81
  "typebox": "^1.2.18",
82
82
  "typescript": "^7.0.2"
83
+ },
84
+ "allowScripts": {
85
+ "@google/genai@2.21.0": true,
86
+ "esbuild@0.28.2": true,
87
+ "protobufjs@7.6.6": true
83
88
  }
84
89
  }
@@ -90,15 +90,23 @@ export interface UpdateFromBaseInput {
90
90
  remote?: string;
91
91
  }
92
92
 
93
- async function captureUpstreamConfiguration(pi: GitAPI, ctx: GitCommandContext, branch: string, signal?: AbortSignal): Promise<string> {
94
- const values: string[] = [];
95
- for (const key of ["remote", "merge"]) {
96
- const result = await runGit(pi, ctx, ["config", "--null", "--get-all", `branch.${branch}.${key}`], { signal, allowFailure: true });
97
- if (result.code !== 0 && (result.code !== 1 || result.stdout || result.stderr)) {
98
- throw new Error("Unable to inspect upstream configuration.");
99
- }
100
- values.push(result.stdout);
93
+ async function readUpstreamConfigurationValue(
94
+ pi: GitAPI,
95
+ ctx: GitCommandContext,
96
+ branch: string,
97
+ signal: AbortSignal | undefined,
98
+ key: string,
99
+ ): Promise<string> {
100
+ const result = await runGit(pi, ctx, ["config", "--null", "--get-all", `branch.${branch}.${key}`], { signal, allowFailure: true });
101
+ if (result.code !== 0 && (result.code !== 1 || result.stdout || result.stderr)) {
102
+ throw new Error("Unable to inspect upstream configuration.");
101
103
  }
104
+ return result.stdout;
105
+ }
106
+
107
+ async function captureUpstreamConfiguration(pi: GitAPI, ctx: GitCommandContext, branch: string, signal?: AbortSignal): Promise<string> {
108
+ const readValue = readUpstreamConfigurationValue.bind(undefined, pi, ctx, branch, signal);
109
+ const values = await Promise.all(["remote", "merge"].map(readValue));
102
110
  return JSON.stringify(values);
103
111
  }
104
112
 
package/src/git.ts CHANGED
@@ -954,6 +954,14 @@ export async function canonicalizePathAllowMissing(path: string): Promise<string
954
954
  }
955
955
  }
956
956
 
957
+ async function resolveWorktreeInventoryEntry(
958
+ record: ParsedWorktreeRecord,
959
+ index: number,
960
+ ): Promise<WorktreeInventoryEntry> {
961
+ const canonicalPath = await canonicalizePathAllowMissing(record.rawPath);
962
+ return { record, index, canonicalPath };
963
+ }
964
+
957
965
  export async function collectWorktreeInventory(
958
966
  pi: Pick<ExtensionAPI, "exec">,
959
967
  ctx: GitCommandContext,
@@ -971,10 +979,11 @@ export async function collectWorktreeInventory(
971
979
  throw new Error("Unable to inspect worktrees: current repository path could not be resolved.");
972
980
  }
973
981
 
974
- const entries: WorktreeInventoryEntry[] = [];
982
+ const entryPromises: Promise<WorktreeInventoryEntry>[] = [];
975
983
  for (const [index, record] of records.entries()) {
976
- entries.push({ record, index, canonicalPath: await canonicalizePathAllowMissing(record.rawPath) });
984
+ entryPromises.push(resolveWorktreeInventoryEntry(record, index));
977
985
  }
986
+ const entries = await Promise.all(entryPromises);
978
987
  return { repoRoot, canonicalCurrentPath, entries };
979
988
  }
980
989
 
@@ -983,16 +992,20 @@ export function pathIsInsideOrEqual(candidatePath: string, boundaryPath: string)
983
992
  return relation === "" || (relation !== ".." && !relation.startsWith(`..${sep}`) && !isAbsolute(relation));
984
993
  }
985
994
 
986
- async function requireCanonicalCreationPath(worktreePath: string): Promise<string> {
987
- const parentPath = dirname(worktreePath);
988
- let parentStats;
995
+ async function requireCanonicalCreationParent(parentPath: string): Promise<string> {
989
996
  try {
990
- parentStats = await stat(parentPath);
991
- } catch {
992
- throw new Error(`worktreePath parent ${safeWorktreePathLabel(parentPath)} must exist as a directory.`);
993
- }
994
- if (!parentStats.isDirectory()) {
995
- throw new Error(`worktreePath parent ${safeWorktreePathLabel(parentPath)} must be a directory.`);
997
+ // lstat distinguishes a missing directory from a dangling symlink.
998
+ await lstat(parentPath);
999
+ } catch (error) {
1000
+ if (filesystemErrorCode(error) === "ENOTDIR") {
1001
+ throw new Error(`worktreePath parent ${safeWorktreePathLabel(parentPath)} must be a directory.`);
1002
+ }
1003
+ const ancestorPath = dirname(parentPath);
1004
+ if (filesystemErrorCode(error) !== "ENOENT" || ancestorPath === parentPath) {
1005
+ throw new Error(`worktreePath parent ${safeWorktreePathLabel(parentPath)} could not be inspected.`);
1006
+ }
1007
+ // Resolve missing ancestors without writing; Git creates them after preflight.
1008
+ return join(await requireCanonicalCreationParent(ancestorPath), basename(parentPath));
996
1009
  }
997
1010
 
998
1011
  let canonicalParent: string;
@@ -1001,12 +1014,25 @@ async function requireCanonicalCreationPath(worktreePath: string): Promise<strin
1001
1014
  } catch {
1002
1015
  throw new Error(`worktreePath parent ${safeWorktreePathLabel(parentPath)} could not be resolved.`);
1003
1016
  }
1017
+ let parentStats;
1018
+ try {
1019
+ parentStats = await stat(canonicalParent);
1020
+ } catch {
1021
+ throw new Error(`worktreePath parent ${safeWorktreePathLabel(parentPath)} could not be inspected.`);
1022
+ }
1023
+ if (!parentStats.isDirectory()) {
1024
+ throw new Error(`worktreePath parent ${safeWorktreePathLabel(parentPath)} must be a directory.`);
1025
+ }
1026
+ return canonicalParent;
1027
+ }
1004
1028
 
1029
+ async function requireCanonicalCreationPath(worktreePath: string): Promise<string> {
1030
+ const canonicalParent = await requireCanonicalCreationParent(dirname(worktreePath));
1005
1031
  const canonicalPath = join(canonicalParent, basename(worktreePath));
1006
1032
  try {
1007
1033
  await lstat(canonicalPath);
1008
1034
  } catch (error) {
1009
- if (isMissingFilesystemPath(error)) return canonicalPath;
1035
+ if (filesystemErrorCode(error) === "ENOENT") return canonicalPath;
1010
1036
  throw new Error(`worktreePath destination ${safeWorktreePathLabel(canonicalPath)} could not be inspected.`);
1011
1037
  }
1012
1038
  throw new Error(`worktreePath destination ${safeWorktreePathLabel(canonicalPath)} already exists.`);
@@ -1092,10 +1118,8 @@ export async function getGitOperationState(
1092
1118
  }
1093
1119
 
1094
1120
  const active: GitOperationKind[] = [];
1095
- const presentMarkers: boolean[] = [];
1096
- for (const [index, path] of paths.entries()) {
1097
- const present = await gitOperationMarkerExists(path);
1098
- presentMarkers.push(present);
1121
+ const presentMarkers = await Promise.all(paths.map(gitOperationMarkerExists));
1122
+ for (const [index, present] of presentMarkers.entries()) {
1099
1123
  if (!present) continue;
1100
1124
  const operation = GIT_OPERATION_MARKERS[index][1];
1101
1125
  if (!active.includes(operation)) active.push(operation);