@senad-d/branchme 0.3.8 → 0.3.10
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 +14 -0
- package/README.md +8 -7
- package/SECURITY.md +7 -7
- package/docs/PROJECT_DEFINITION_BRIEF.md +7 -6
- package/docs/SMOKE_TEST.md +2 -2
- package/docs/STRUCTURE.md +10 -4
- package/docs/TUI_CAPTURE.md +1 -0
- package/package.json +1 -1
- package/src/blocker-guidance.ts +154 -0
- package/src/commands/branchme-command.ts +1 -0
- package/src/constants.ts +3 -0
- package/src/extension.ts +2 -1
- package/src/git-integration.ts +10 -2
- package/src/git-landing.ts +67 -8
- package/src/git-transport.ts +133 -0
- package/src/git-workflow.ts +8 -10
- package/src/git.ts +31 -9
- package/src/github-feedback.ts +169 -27
- package/src/tools/branchme-tools.ts +8 -1
- package/src/tools/workflow-tools.ts +30 -5
- package/src/types.ts +36 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.10 - Unreleased
|
|
4
|
+
|
|
5
|
+
- Added `reply_pull_request_feedback`: one reply in a review thread of a PR, optionally resolving it, or one PR conversation comment, on open, merged and closed PRs of the repository (forks included). The thread is verified to belong to the PR, and a conversation comment's number to be a pull request, before anything is posted. Agents that address PR feedback can now answer every comment on GitHub, so the reviewer, and the next automated check, see what was handled.
|
|
6
|
+
- `pull_request_feedback` returns what replying and deduplication need: review thread ids, conversation comment ids, the author and time of every comment and review, the token's own login (`viewer`) with its comments marked `(you)`, and the pull request's node id. Failing check runs list up to 20 failure and warning annotations (`path:line LEVEL: message`) before their log tail, the most precise pointer for lint and test failures.
|
|
7
|
+
|
|
8
|
+
## 0.3.9 - Unreleased
|
|
9
|
+
|
|
10
|
+
- Git network commands (`fetch`, `pull`, `push`, `ls-remote`) run with terminal prompts disabled: missing credentials fail at once with git's own message instead of waiting on an unanswerable prompt until the timeout reported only "killed". For `https://github.com` the `GITHUB_TOKEN`/`GH_TOKEN` BranchMe already uses for the API now also answers git's credential request (after the user's own helpers), through the child environment, never argv. A token alone is enough for push and pull.
|
|
11
|
+
- `change_branch`, `pull_branch`, `rebase_branch`, `track_branch` and `update_from_base` refuse only on staged, unstaged or unmerged tracked changes and name the paths; untracked files such as a local `.env` no longer block them (git still refuses an overwrite). `land_branch` target sync treats a checkout with only untracked files as clean.
|
|
12
|
+
- `land_branch` releases a merged source branch checked out in the primary checkout by switching that checkout to the target (new `checkout` receipt step) before retiring it. Its text names the sync result plainly (`main fast-forwarded 79cf046 -> cd4f9ef`) instead of the mode name.
|
|
13
|
+
- Every tool error with a known cause ends with `Blocked:` (why) and `Resolve:` (the next call to make) lines: credentials, unreachable remote, timeout, rejected or diverged push and pull, missing remote branch, overwritten files, tracked changes, branch occupancy, in-progress operations, detached HEAD, missing upstream, GitHub token and HTTP errors, and concurrent moves. Worktree-removal guidance distinguishes untracked changes from tracked-only checkout blockers.
|
|
14
|
+
- Resolved Sonar findings by extracting landing guidance formatting, emitting credential fields from the runtime token, and using fixed absolute system Git executables rather than `PATH` resolution. Added timeout/cancellation and PATH-hijacking regression coverage.
|
|
15
|
+
- Applied untracked-file tolerance through base-update verification and merge aborts, and protected ignored-file collisions when landing switches the primary checkout. Updated security and architecture documentation for the transport and checkout boundaries.
|
|
16
|
+
|
|
3
17
|
## 0.3.8 - Unreleased
|
|
4
18
|
|
|
5
19
|
- Resolved four Sonar maintainability findings: fetch the bounded failing-job logs concurrently and simplify feedback formatting by extracting nested template literals.
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
BranchMe is a Pi extension for safe Git repository, branch, and worktree workflow automation. Before each agent run, it appends a bounded, read-only snapshot of the current Git repository to the system prompt. It also adds an informational `/branchme` command and twenty-
|
|
19
|
+
BranchMe is a Pi extension for safe Git repository, branch, and worktree workflow automation. Before each agent run, it appends a bounded, read-only snapshot of the current Git repository to the system prompt. It also adds an informational `/branchme` command and twenty-three agent-callable tools that initialize a repository, refresh state, manage, integrate, and retire local branches, conclude a kept merge, inspect/create/remove linked worktrees, push the current branch, create GitHub pull requests, and read and answer PR feedback.
|
|
20
20
|
|
|
21
21
|
<table align="center">
|
|
22
22
|
<tr>
|
|
@@ -202,7 +202,7 @@ $EDITOR .env
|
|
|
202
202
|
pi
|
|
203
203
|
```
|
|
204
204
|
|
|
205
|
-
`fetch_remote`, `fetch_branch`, `pull_branch`, and `
|
|
205
|
+
`fetch_remote`, `fetch_branch`, `pull_branch`, `push_branch`, and `land_branch` run git with terminal prompts disabled, so missing credentials fail at once instead of hanging. Your normal Git credentials are tried first; for `https://github.com` remotes the same `GITHUB_TOKEN`/`GH_TOKEN` (process environment or repository `.env`) then answers as a credential helper, passed to git through its environment and never on the command line. A token alone is enough. `rebase_branch` operates on the locally available configured upstream ref and makes no network request itself.
|
|
206
206
|
When the current branch already has an upstream, BranchMe pushes an explicit `HEAD:<upstream-branch-ref>` refspec to the configured upstream remote instead of relying on a bare `git push`.
|
|
207
207
|
Run `pull_request` only after `push_branch` has completed; `pull_request` preflights the GitHub `headBranch` and `baseBranch` before creating the PR and fails with retry guidance if a branch is not visible yet or the GitHub `headBranch` commit does not match the local branch.
|
|
208
208
|
|
|
@@ -257,7 +257,8 @@ Commands are informational only. BranchMe actions are performed by agent-callabl
|
|
|
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
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. |
|
|
260
|
-
| `
|
|
260
|
+
| `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. |
|
|
261
|
+
| `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. |
|
|
261
262
|
| `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. |
|
|
262
263
|
| `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. |
|
|
263
264
|
| `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,7 +267,7 @@ Commands are informational only. BranchMe actions are performed by agent-callabl
|
|
|
266
267
|
| `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. |
|
|
267
268
|
| `retire_branch` | `{ "branchName": string, "expectedHead": string, "targetBranch": string, "force": boolean }` | Deletes only the exact unoccupied local branch ref when its direct ref matches the supplied full commit ID and its relationship to the exact local target has been verified. Unmerged retirement requires explicit `force: true`; remote and remote-tracking refs are untouched. |
|
|
268
269
|
| `land_branch` | `{ "sourceBranch": string, "targetBranch": string, "remote"?: string, "worktreePath"?: string, "pullRequestNumber"?: integer }` | Post-merge fetch, ancestry or exact merged-PR proof, linked-worktree removal including ignored residue, leased local source deletion, and independent fast-forward target sync. Default remote `origin`; omitted path finds the source's linked worktree. Returns per-step receipts. |
|
|
269
|
-
| `change_branch` | `{ "branchName": string }` | Validates `branchName`, requires `refs/heads/<branchName>` to exist locally, rejects
|
|
270
|
+
| `change_branch` | `{ "branchName": string }` | Validates `branchName`, requires `refs/heads/<branchName>` to exist locally, rejects staged, unstaged, or unmerged tracked changes (naming the paths), and runs `git switch <branchName>`. Untracked files such as `.env` do not block; git itself refuses a switch that would overwrite one. |
|
|
270
271
|
| `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. |
|
|
271
272
|
| `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. |
|
|
272
273
|
| `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. |
|
|
@@ -412,7 +413,7 @@ After the pull request **merged on the host**, run from the repository root:
|
|
|
412
413
|
}
|
|
413
414
|
```
|
|
414
415
|
|
|
415
|
-
`remote` defaults to `origin`. Omit `worktreePath` to find the linked worktree holding `sourceBranch`, if any. An explicit path can also select a worktree incorrectly parked on `targetBranch`; unrelated branches, primary/root checkouts, detached, locked, dirty, or foreign worktrees are not removed. Invocation from inside the removal directory (including a nested or symlinked cwd) is refused: **run from the repository root**. Other caller directories in the same repository are supported; every Git command has an explicit `-C` directory, and cleanup runs through the primary checkout
|
|
416
|
+
`remote` defaults to `origin`. Omit `worktreePath` to find the linked worktree holding `sourceBranch`, if any. An explicit path can also select a worktree incorrectly parked on `targetBranch`; unrelated branches, primary/root checkouts, detached, locked, dirty, or foreign worktrees are not removed. Invocation from inside the removal directory (including a nested or symlinked cwd) is refused: **run from the repository root**. Other caller directories in the same repository are supported; every Git command has an explicit `-C` directory, and cleanup runs through the primary checkout. When the merged source branch is checked out in the primary checkout and its tracked files are clean, `land_branch` switches that checkout to the target branch first (`checkout: switched`) so the source can be retired; untracked files stay in place.
|
|
416
417
|
|
|
417
418
|
The tool fetches the exact remote target and by default requires the captured source tip to be its ancestor before cleanup. For squash/rebase merges, supply `pullRequestNumber`. BranchMe verifies a closed, merged PR in the same GitHub repository as the configured landing remote, exact head/base branch names, the source SHA, and containment of the reported merge commit in the fetched target. Supplying a PR number requires valid evidence even when ordinary ancestry succeeds. There is no general force escape hatch. This host-merge proof authorizes retiring the original rewritten source history; those original commits may eventually become unreachable. An existing selected source checkout must match the proven PR head exactly; a checkout parked on the target still needs target ancestry. Tracked/staged changes and non-ignored untracked files block removal. **Ignored `.env`, `.pi/`, `node_modules/`, `dist/`, and other ignored residue are deleted with the worktree**; preserve anything needed first. Only their top-level path entries, never contents, appear in `deletedIgnoredPaths`. Source retirement uses the captured expected-HEAD lease and the **remote-tracking target**, not local `HEAD` or a stale local target.
|
|
418
419
|
|
|
@@ -422,7 +423,7 @@ Target sync is last, even when worktree removal or branch retirement is refused:
|
|
|
422
423
|
| --- | --- |
|
|
423
424
|
| `fetch-refspec` | Target is not checked out anywhere: non-forced `fetch <remote> refs/heads/<target>:refs/heads/<target>`. Git enforces fast-forward-only updates. |
|
|
424
425
|
| `pull-ff` | Target is checked out clean: explicit `pull --ff-only --no-rebase --no-autostash <remote> refs/heads/<target>` in that worktree. |
|
|
425
|
-
| `skipped-dirty` | Target checkout
|
|
426
|
+
| `skipped-dirty` | Target checkout has tracked changes: leave it untouched and report its path and ahead/behind counts against the captured remote target. |
|
|
426
427
|
| `noop` | Local target already equals the captured remote head; no sync mutation. |
|
|
427
428
|
| `not-run` / `failed` | Initial safety/ancestry refusal or sync failure; a successful fast-forward is never inferred from Git's prose. |
|
|
428
429
|
|
|
@@ -565,7 +566,7 @@ npm run check:pack
|
|
|
565
566
|
printf '/branchme help\n/quit\n' | pi --no-extensions -e .
|
|
566
567
|
```
|
|
567
568
|
|
|
568
|
-
Validation covers TypeScript typechecking, formatting checks, automatic context collection and prompt injection, mocked GitHub lookup, isolated real-Git worktree, branch-integration, and leased branch-retirement lifecycle tests, package checks, checkout Pi runtime smoke, and package-content verification. The checkout smoke loads BranchMe through Pi, then uses a temporary verifier command to confirm all twenty-
|
|
569
|
+
Validation covers TypeScript typechecking, formatting checks, automatic context collection and prompt injection, mocked GitHub lookup, isolated real-Git worktree, branch-integration, and leased branch-retirement lifecycle tests, package checks, checkout Pi runtime smoke, and package-content verification. The checkout smoke loads BranchMe through Pi, then uses a temporary verifier command to confirm all twenty-three BranchMe tools are visible through `pi.getAllTools()` with strict schemas and prompt metadata, including `integrate_branch`, `retire_branch`, `conclude_merge`, and targeted `branch_status.ancestry`, with no generic `continue_merge` or `abort_merge` tool. Runtime smoke inspects retirement and landing registration/schema but never executes either cleanup tool. Smoke-test notes are recorded in [`docs/SMOKE_TEST.md`](docs/SMOKE_TEST.md), and TUI/help captures are stored in [`docs/TUI_CAPTURE.md`](docs/TUI_CAPTURE.md).
|
|
569
570
|
|
|
570
571
|
Refresh TUI captures intentionally with:
|
|
571
572
|
|
package/SECURITY.md
CHANGED
|
@@ -11,7 +11,7 @@ pi install git:https://github.com/senad-d/branchme@<tag>
|
|
|
11
11
|
|
|
12
12
|
## Git behavior
|
|
13
13
|
|
|
14
|
-
BranchMe runs local `git` commands through Pi's extension API
|
|
14
|
+
BranchMe runs local `git` commands with argv-style arguments through Pi's extension API, except network commands, which use a child process to set a non-interactive credential environment. Network Git uses fixed absolute system-install locations (`/usr/bin/git`, `/usr/local/bin/git`, `/opt/homebrew/bin/git`, `/bin/git`, or Git for Windows under `C:\Program Files\Git`), never a `PATH` lookup. `init_repository` first canonicalizes its non-repository target directory; other repository mutations resolve the Git root and run from that verified root.
|
|
15
15
|
|
|
16
16
|
Implemented git mutations are limited to:
|
|
17
17
|
|
|
@@ -29,20 +29,20 @@ Implemented git mutations are limited to:
|
|
|
29
29
|
- `push_branch`: `git push <upstreamRemote> HEAD:<upstreamBranchRef>` for the current branch when an upstream exists, or `git push --set-upstream origin <currentBranch>` when no upstream exists.
|
|
30
30
|
- `create_worktree`: `git worktree add -b <branchName> <canonicalPath> <capturedCommit>` for a new local branch from current HEAD or optional read-only `baseRef`, or `git worktree add <canonicalPath> <existingLocalBranch>` for an existing unoccupied local branch, after destination and repository-boundary validation.
|
|
31
31
|
- `remove_worktree`: `git worktree remove <verifiedCanonicalPath>` without force, only after fresh repository-membership, safety-state, path, tracked/untracked status, and ignored-entry checks. The local branch is retained and verified at the same commit.
|
|
32
|
-
- `land_branch`: explicit post-host-merge targeted fetch, linked-worktree removal including ignored residue, leased source-local-ref deletion against the fetched remote target, and independent final target sync. Every Git command uses an explicit `-C` directory; unoccupied targets use a non-forced fetch refspec,
|
|
32
|
+
- `land_branch`: explicit post-host-merge targeted fetch, linked-worktree removal including ignored residue, leased source-local-ref deletion against the fetched remote target, and independent final target sync. Every Git command uses an explicit `-C` directory; unoccupied targets use a non-forced fetch refspec, occupied targets without tracked changes use an explicit ff-only pull, and tracked-dirty targets are left untouched. A merged source in the primary checkout is released with `git switch --no-overwrite-ignore <targetBranch>` only after idle and tracked-clean checks, preserving untracked and ignored files or refusing a collision. No remote mutation, prune, stash, or reset is attempted.
|
|
33
33
|
- `retire_branch`: `git update-ref --no-deref -d refs/heads/<branchName> <capturedRetiringHead>` only for one exact unoccupied direct local ref whose commit matches the required full `expectedHead`. The exact local target commit and ancestry are captured first; unmerged retirement requires explicit `force: true` authorization.
|
|
34
34
|
|
|
35
35
|
Before each agent run, BranchMe also runs bounded, read-only Git commands to collect branch/upstream/ahead-behind state, working-tree counts, up to 20 unstaged or untracked path entries, and up to 5 recent commits. The same collector runs when `branch_status` explicitly refreshes context. An explicit optional `branch_status.ancestry` query captures exact local or remote-tracking source/target commit IDs and uses `git merge-base --is-ancestor` against those commits; automatic Git context never runs this query and remains unchanged. Collection does not run `fetch`, `switch`, `pull`, `rebase`, `merge`, `push`, `add`, `commit`, or any other mutation, and it never reads diffs or file contents.
|
|
36
36
|
|
|
37
37
|
Repository initialization creates `.git` metadata in the current directory. Branch switching, worktree creation/removal, fast-forward pulls, successful rebases, and branch integration can update or remove filesystem content as normal Git behavior; fetch updates one validated remote-tracking ref without changing local branches or the working tree, and retirement deletes one verified local branch ref. Mutating operations for the same repository are serialized to avoid same-turn races; initialization uses Pi's file-mutation queue for the new `.git` entry and is serialized by its canonical target directory. `integrate_branch` holds one queue window across preflight, merge, cleanup, and final verification; `retire_branch` holds one active-worktree-keyed window across preflight, immediate reinspection, leased deletion, and postcondition verification; `pull_request` uses the queue around PR preflight and creation. This in-memory queue is process-local: it coordinates BranchMe calls using the same active checkout but does not lock a different active worktree, another Pi process, or an external Git process. Integration refs are captured and re-verified. Retirement additionally uses an expected-old-value ref lease and rechecks its captured target and complete worktree occupancy. Unexpected or inconclusive movement produces a bounded uncertain error with manual inspection guidance and no reset-based rollback.
|
|
38
38
|
|
|
39
|
-
BranchMe rejects
|
|
39
|
+
BranchMe rejects staged, unstaged, or unmerged tracked changes before `change_branch`, `pull_branch`, `rebase_branch`, `track_branch`, and `update_from_base`, naming the affected paths. Untracked files do not block these preflights; Git still refuses non-ignored file collisions, and tracking/base-update/landing operations also protect ignored files. Standalone `integrate_branch` still requires a fully clean control worktree, including untracked files. Standalone `remove_worktree` rejects any staged, unstaged, untracked, or unmerged entry in a linked worktree. Standalone removal protects ignored residue by default and deletes it only with explicit `deleteIgnored: true` authorization. `land_branch` also allows ignored residue to be deleted, but still refuses tracked/staged or non-ignored untracked changes. Retirement does not require an unrelated active worktree to be clean, but every registered worktree record is inspected and any occupancy of the retiring branch blocks deletion. Integration also rejects an existing merge, rebase, cherry-pick, revert, or sequencer state and any non-empty target-branch `mergeOptions` setting that could alter the fixed command policy. After merge or retirement mutation begins, cleanup/postcondition inspection ignores caller cancellation and uses bounded timeouts. A `conflict` result is returned only after exact repository-relative conflict paths are captured, `git merge --abort` succeeds, source and target refs are restored, repository/control-worktree identity is preserved, operation state is cleared, and the control worktree is clean. Failed non-conflict merges remain errors; inconclusive cleanup or verification is never reported as success. After retirement is attempted, contradictory or inconclusive repository, target-ref, retiring-ref, or occupancy state is an uncertain error stating that retirement may have completed and requiring manual inspection before retry.
|
|
40
40
|
|
|
41
41
|
BranchMe does not force checkout/removal, stash, create user-authored commits, accept commit messages, reset, force-push, or edit project files directly. Rebase-driven rewriting occurs only through explicit `rebase_branch`. Explicit `integrate_branch` or `update_from_base` may let Git create a standard merge commit for divergent histories, but BranchMe exposes no strategy, squash, unrelated-history, signing, force, or commit-message control and no generic `continue_merge` or `abort_merge` tool. The only staging surface is `conclude_merge`, which adds exactly the formerly unmerged paths of an in-progress merge after proving no conflict marker line remains in them. Explicit `retire_branch` and merged-only `land_branch` are the local branch-deletion surfaces. Standalone retirement has no bulk, pattern, inferred-target, remote-delete, remote-tracking-delete, automatic worktree-removal, or rollback-ref control.
|
|
42
42
|
|
|
43
43
|
## Network behavior
|
|
44
44
|
|
|
45
|
-
`track_branch`, `update_from_base`, `fetch_remote`, `fetch_branch`, `pull_branch`, `land_branch`, and `push_branch` contact the configured Git remote through the user's normal Git transport and credentials.
|
|
45
|
+
`track_branch`, `update_from_base`, `fetch_remote`, `fetch_branch`, `pull_branch`, `land_branch`, and `push_branch` contact the configured Git remote through the user's normal Git transport and credentials. Terminal prompts are disabled (`GIT_TERMINAL_PROMPT=0`, `GCM_INTERACTIVE=never`). Existing credential helpers are tried first; for `https://github.com`, an appended helper can answer with BranchMe's `GITHUB_TOKEN`/`GH_TOKEN` from the process environment or repository-root `.env`. The token is passed only in the child environment, never in argv or stored Git configuration. Other hosts and SSH transports receive no credentials from this helper. `rebase_branch`, `integrate_branch`, and `retire_branch` use locally available refs; BranchMe's direct retirement argv never fetches, pulls, pushes, or names a remote or remote-tracking ref for deletion.
|
|
46
46
|
|
|
47
47
|
Git extension points are a separate trust boundary. `integrate_branch` preserves repository-configured hooks, custom merge drivers, clean/smudge filters, and signature policy; it does not pass `--no-verify`. Retirement's `git update-ref` can invoke repository-configured `reference-transaction` hooks. Those configurations may execute arbitrary local commands, mutate other state, or contact networks under the user's identity. BranchMe's fixed argv, direct no-network contract, and direct no-remote-delete guarantees cannot constrain hook behavior.
|
|
48
48
|
|
|
@@ -103,7 +103,7 @@ BranchMe does not copy ignored or untracked files, including repository-root `.e
|
|
|
103
103
|
|
|
104
104
|
The remote-tracking fetch destination is checked for symbolic indirection before fetching. The captured source tip must be an ancestor of the freshly fetched remote-tracking target before any deletion, unless explicit `pullRequestNumber` evidence proves an exact merged GitHub PR. PR evidence requires matching resolved repository and landing remote, closed/merged state, exact source branch/SHA and target branch, and containment of the reported merge commit in the fetched target. The receipt preserves actual graph ancestry (which can remain false) separately from `mergeProof: "pull-request"`. This proof authorizes internal non-ancestor local retirement using the existing expected-HEAD lease; no public force option is added. Original pre-squash/rebase commits may eventually become unreachable. The primary checkout is never removed. The selected linked checkout must be on the source or target branch, clean of tracked/staged/non-ignored untracked changes, unlocked, attached, and part of the same repository. Its HEAD must also be contained in the captured target, or be the exact proven merged PR head on the source branch. A checkout parked on the target still requires graph ancestry. Ignored files and directories, including `.env` and `.pi/`, are intentionally deleted by force-free `git worktree remove`; preserve needed files first. The receipt lists redacted top-level ignored paths, never file contents. The source branch is then retired with the same atomic expected-old-value lease and full occupancy checks as standalone retirement, but ancestry is against the fetched remote-tracking ref. No remote or remote-tracking ref is deleted.
|
|
105
105
|
|
|
106
|
-
Target sync always follows attempted cleanup, including cleanup refusals, unless initial cwd/fetch/ancestry checks refuse the whole operation.
|
|
106
|
+
After worktree cleanup, a merged source checked out in the primary checkout may be switched to the local target, with source-tip, idle-state, tracked-clean, and ignored-file collision checks; the `checkout` receipt records this separately. The primary checkout is never removed. Target sync always follows attempted cleanup, including cleanup refusals, unless initial cwd/fetch/ancestry checks refuse the whole operation. Untracked-only target checkouts may be fast-forwarded; tracked changes cause a skip. No stash, reset, force update, or prune occurs. Explicit fetch mappings and prune-disabling flags/config prevent Git's configured extra fetch mappings and pruning from expanding the landing scope. Actual local target refs are read before/after sync; errors, dirty skips, and unattempted steps are reported, not described as successful fast-forwards. After possible sync mutation, final ref verification ignores cancellation. Missing branches/worktrees are idempotent; a leftover missing-worktree registration is not pruned and can still block retirement. Repository hooks and external concurrent mutations remain within the existing trust/uncertainty boundary.
|
|
107
107
|
|
|
108
108
|
## Local branch-retirement boundary
|
|
109
109
|
|
|
@@ -127,7 +127,7 @@ Only the local branch ref is deleted. Local `branch.<branchName>.*` configuratio
|
|
|
127
127
|
|
|
128
128
|
## Credentials
|
|
129
129
|
|
|
130
|
-
Git fetch, pull, and push
|
|
130
|
+
Git fetch, pull, and push first use the user's configured Git credentials. For GitHub HTTPS requests, BranchMe additionally supplies the API token through a host-scoped credential helper and child environment. Git and any configured helpers/hooks inherit that environment and are trusted code; the helper ignores credential store/erase requests. Tokens are never interpolated into command arguments, helper source, or persistent Git configuration.
|
|
131
131
|
|
|
132
132
|
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:
|
|
133
133
|
|
|
@@ -162,7 +162,7 @@ Do not open public issues for security-sensitive reports that include exploit de
|
|
|
162
162
|
|
|
163
163
|
- Do not commit secrets, tokens, local `.env`, local `.pi/` state, or generated artifacts.
|
|
164
164
|
- Keep tool schemas strict and reject unsupported fields.
|
|
165
|
-
- Keep
|
|
165
|
+
- Keep Git calls argv-style; network transport must use a fixed absolute executable with `shell: false`, disable terminal prompts, and keep credential values in the child environment only.
|
|
166
166
|
- Preserve the fixed `integrate_branch` merge policy, verified automatic abort/restoration contract, and process-local queue caveat; never add reset-based rollback. Keep `conclude_merge` limited to marker-free formerly unmerged paths, `git commit --no-edit`, and verified two-parent postconditions; never add `-a`, path, message, strategy, or `--ours`/`--theirs` controls.
|
|
167
167
|
- Preserve `retire_branch` expected-`HEAD` leasing, complete occupancy checks, exact target ancestry, explicit unmerged force authorization, local-only ref scope, cancellation-safe final inspection, and uncertain-error behavior; never add bulk, inferred, remote, remote-tracking, or automatic worktree deletion.
|
|
168
168
|
- Treat `reference-transaction` hooks as arbitrary repository-controlled code outside BranchMe's direct retirement argv guarantees.
|
|
@@ -15,7 +15,7 @@ Originally approved on 2026-06-30. Updated to describe the implemented `0.3.1` p
|
|
|
15
15
|
- Exported extension function: `branchMeExtension`
|
|
16
16
|
- Repository URL: `https://github.com/senad-d/branchme`
|
|
17
17
|
- One-sentence pitch: Verified Pi tools for Git repository initialization plus current-repository branch, integration, retirement, linked-worktree, push, and GitHub pull request workflows.
|
|
18
|
-
- Tool count: twenty-
|
|
18
|
+
- Tool count: twenty-three strict agent-callable tools.
|
|
19
19
|
|
|
20
20
|
## 3. Users and use cases
|
|
21
21
|
|
|
@@ -30,7 +30,7 @@ Originally approved on 2026-06-30. Updated to describe the implemented `0.3.1` p
|
|
|
30
30
|
- Create and verify a linked worktree from current `HEAD`, optional read-only `baseRef`, or an unoccupied existing local branch.
|
|
31
31
|
- Return an exact, absolute, machine-readable worktree handoff for a caller-managed separate Pi session or subagent.
|
|
32
32
|
- Remove an exact verified clean linked worktree while retaining its local branch; protect ignored residue by default and delete it only with explicit `deleteIgnored: true` authorization.
|
|
33
|
-
- Land a host-merged feature in one cwd-independent call: fetch/prove remote ancestry, remove a clean linked checkout including ignored residue, lease-delete the source branch, and sync the target independently without touching
|
|
33
|
+
- Land a host-merged feature in one cwd-independent call: fetch/prove remote ancestry, remove a clean linked checkout including ignored residue, release a tracked-clean primary source checkout with an ignored-file-protecting switch, lease-delete the source branch, and sync the target independently without touching tracked changes. Untracked-only target checkouts are permitted.
|
|
34
34
|
- Retire one exact unoccupied local branch ref only when it matches a full expected `HEAD` and its relationship to one exact local target has been verified; unmerged retirement requires explicit force authorization.
|
|
35
35
|
- Switch to an existing local branch after a clean-worktree preflight or create a new branch from current `HEAD`.
|
|
36
36
|
- Fetch a configured upstream tracking ref, fast-forward the current branch, or explicitly rebase it.
|
|
@@ -56,7 +56,8 @@ Originally approved on 2026-06-30. Updated to describe the implemented `0.3.1` p
|
|
|
56
56
|
| Tool | `update_from_base` | Fetch and merge an explicit remote base | Preserves published history; fixed merge policy, listed conflict paths, automatic abort unless `keepConflicts` |
|
|
57
57
|
| Tool | `conclude_merge` | Commit or abort the in-progress kept merge | Refuses remaining markers; stages only formerly unmerged paths; `commit --no-edit`; verified two-parent result or verified abort |
|
|
58
58
|
| Tool | `pull_request_status` | Read exact PR or latest PR for a head | Open/closed/merged state and commit identities; not a CI/review verdict |
|
|
59
|
-
| Tool | `pull_request_feedback` | Read what a PR asks to be fixed | Unresolved review threads, review summaries, conversation comments, failing checks with Actions log tails; read-only |
|
|
59
|
+
| Tool | `pull_request_feedback` | Read what a PR asks to be fixed | Unresolved review threads, review summaries, conversation comments, failing checks with annotations and Actions log tails; read-only |
|
|
60
|
+
| Tool | `reply_pull_request_feedback` | Answer PR feedback | One reply in a review thread of the PR (optionally resolving it) or one conversation comment; the thread must belong to the PR; never edits, deletes, approves, merges or closes |
|
|
60
61
|
| Tool | `init_repository` | Initialize the exact current directory as a new non-bare Git repository | Optional initial branch; rejects reinitialization/nesting; no path, commit, or project-file controls |
|
|
61
62
|
| Tool | `branch_status` | Refresh bounded current-worktree Git and related-PR context; optionally prove captured local or remote-tracking ancestry | Read-only; targeted ancestry is absent from automatic context |
|
|
62
63
|
| Tool | `integrate_branch` | Integrate one exact local source into the already-current clean local target | Fixed normal-merge policy; verified automatic abort/restoration on conflict |
|
|
@@ -96,7 +97,7 @@ Originally approved on 2026-06-30. Updated to describe the implemented `0.3.1` p
|
|
|
96
97
|
- `src/github.ts`
|
|
97
98
|
- `src/ui/branchme-panel.ts`
|
|
98
99
|
- Module boundaries:
|
|
99
|
-
- The extension entry point registers the informational command, twenty-
|
|
100
|
+
- The extension entry point registers the informational command, twenty-three tools, and automatic context hook.
|
|
100
101
|
- `src/git-workflow.ts` and `src/tools/workflow-tools.ts` own verified remote tracking, base updates, and the new PR status registration.
|
|
101
102
|
- The context module owns bounded read-only collection, prompt formatting, targeted ancestry rendering, and the `before_agent_start` hook; automatic context never runs ancestry queries.
|
|
102
103
|
- The command and UI modules own mode-safe informational help/status behavior and never invoke mutations.
|
|
@@ -159,14 +160,14 @@ Standalone removal retains its branch and requires explicit `deleteIgnored: true
|
|
|
159
160
|
|
|
160
161
|
## 10. Security and privacy
|
|
161
162
|
|
|
162
|
-
- Every Git command uses `pi.exec("git", args, { cwd, signal, timeout })
|
|
163
|
+
- Every Git command uses an argv array rather than shell interpolation. Network commands use a fixed absolute Git executable with cwd, signal, timeout, and a non-interactive child environment; other commands use `pi.exec("git", args, { cwd, signal, timeout })`.
|
|
163
164
|
- Paths, branch names, Git output, commit subjects, and pull request metadata are treated as untrusted. Display and prompt surfaces are escaped, redacted, and bounded separately from prevalidated exact handoff identities.
|
|
164
165
|
- Worktree paths are canonicalized and checked against a fresh current-repository inventory or the common Git directory before mutation. User-supplied removal paths are never passed directly to Git.
|
|
165
166
|
- Worktree removal has no force path and rejects dirty, detached, locked, prunable/missing, bare, main, current, and foreign entries before mutation. Ignored entries require explicit `deleteIgnored: true` authorization.
|
|
166
167
|
- Worktree tools expose no force, move, prune, repair, lock, unlock, detached, orphan, remote-inference, or arbitrary refspec controls; new-mode `baseRef` is explicitly read-only. Git's incomplete submodule worktree support is not bypassed with force cleanup.
|
|
167
168
|
- Retirement requires exact direct local refs, full expected-`HEAD` leasing, complete worktree occupancy inspection, captured target ancestry, and explicit unmerged force authorization. Final verification is cancellation-safe and bounded; uncertain state requires manual inspection and no rollback mutation.
|
|
168
169
|
- Automatic context and `branch_status` never collect diffs or file contents. Related-PR lookup may make a bounded authenticated GitHub request only when credentials and repository identity resolve; there is no unauthenticated fallback.
|
|
169
|
-
- `pull_request` uses the resolved current GitHub repository, preflights local and GitHub branch identity, and rejects cross-repository head refs. Git fetch/pull/push
|
|
170
|
+
- `pull_request` uses the resolved current GitHub repository, preflights local and GitHub branch identity, and rejects cross-repository head refs. Git fetch/pull/push try the user's normal Git credentials first, then a GitHub-HTTPS-only helper using BranchMe's token from the child environment (never argv or persistent configuration).
|
|
170
171
|
- BranchMe's direct integration and retirement commands are local-only, but repository-configured hooks—including `reference-transaction` hooks during retirement—merge drivers, filters, and signing policy may execute arbitrary commands or network operations under the user's identity outside BranchMe's argv guarantees.
|
|
171
172
|
- Tokens are resolved from supported process or verified-root `.env` keys and redacted from prompts, errors, content, and details. BranchMe collects no telemetry.
|
|
172
173
|
- Creation and removal require explicit user intent and an exact approved path. BranchMe does not silently infer worktree destinations or start a separate agent session.
|
package/docs/SMOKE_TEST.md
CHANGED
|
@@ -23,9 +23,9 @@ pi --no-extensions -e .
|
|
|
23
23
|
## Automated smoke behavior
|
|
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
|
-
- The temporary command verifier calls `pi.getAllTools()` after BranchMe loads and confirms exactly twenty-
|
|
26
|
+
- The temporary command verifier calls `pi.getAllTools()` after BranchMe loads and confirms exactly twenty-three tools—`fetch_remote`, `list_branches`, `track_branch`, `update_from_base`, `conclude_merge`, `pull_request_status`, `pull_request_feedback`, `reply_pull_request_feedback`, `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
27
|
- Runtime schema inspection verifies that `integrate_branch` has exactly the required `sourceBranch` and `targetBranch` fields 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.
|
|
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-
|
|
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-three 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.
|
|
31
31
|
- Isolated real-Git retirement tests cover merged and explicitly forced-unmerged local-ref deletion, stale expected-`HEAD` rejection, current and linked-worktree occupancy, dirty unrelated worktrees, non-current targets, and preservation of target refs, remote-tracking refs, worktrees, working-tree files, and `branch.<name>.*` configuration without contacting a remote. Recorded argv proves retirement uses `update-ref --no-deref -d` with the captured expected commit and never uses `git branch -d/-D`, fetch, push, or a remote ref target.
|
package/docs/STRUCTURE.md
CHANGED
|
@@ -15,6 +15,8 @@ src/
|
|
|
15
15
|
├── tools/
|
|
16
16
|
│ ├── branchme-tools.ts # main registration, including branch discovery
|
|
17
17
|
│ └── workflow-tools.ts # tracking, base update, and PR status registration
|
|
18
|
+
├── blocker-guidance.ts # known tool blockers and actionable recovery guidance
|
|
19
|
+
├── git-transport.ts # absolute network Git executable, child-env credentials, cancellation
|
|
18
20
|
├── git.ts # shared argv-style Git primitives and per-repo mutation queue
|
|
19
21
|
├── git-discovery.ts # bounded remote cache refresh, optional pruning, symbolic-ref safety
|
|
20
22
|
├── git-workflow.ts # narrow fetch, tracking checkout, and feature-base update
|
|
@@ -28,7 +30,7 @@ src/
|
|
|
28
30
|
|
|
29
31
|
## Module boundaries
|
|
30
32
|
|
|
31
|
-
1. `src/extension.ts` stays small and registers the command, twenty-
|
|
33
|
+
1. `src/extension.ts` stays small and registers the command, twenty-three tools, and one `before_agent_start` context hook.
|
|
32
34
|
2. `src/git-context.ts` owns the shared read-only collector, escaped/bounded formatter, automatic system-prompt append, and the current-state output used by `branch_status`.
|
|
33
35
|
3. `src/commands/branchme-command.ts` parses `/branchme`, `/branchme help`, `--help`, and `-h`; it never performs git or GitHub mutations and avoids raw stdout in JSON mode.
|
|
34
36
|
4. `src/tools/branchme-tools.ts` owns strict TypeBox schemas, prompt metadata, bounded tool content, and safe structured details. `branch_status` delegates to the shared context collector and optional ancestry verifier; `integrate_branch` and `retire_branch` delegate to focused mutation state machines; worktree tools expose explicit inventory and verified handoff operations.
|
|
@@ -43,6 +45,8 @@ src/
|
|
|
43
45
|
13. `src/git-landing.ts` owns execution-only `land_branch` orchestration and receipt types. It resolves the primary checkout, routes every Git call with `-C`, and reuses queue-free targeted fetch/removal/retirement helpers under one primary-root mutation window. Remote ancestry or exact merged-PR evidence gates cleanup; final target sync independently reports exact ref observations. No startup work or persistent landing state is added.
|
|
44
46
|
|
|
45
47
|
14. `src/git-discovery.ts` owns configured-remote-wide branch-cache refresh and optional stale tracking-ref pruning with explicit internal mappings, atomic ref updates, bounded files-backend destination inspection (including dangling symrefs), and active-root/common-directory queues. It never checks out or changes local refs or upstream settings.
|
|
48
|
+
15. `src/git-transport.ts` wraps tool execution only: network Git uses fixed absolute system-install locations, disabled terminal prompts, and a host-scoped GitHub token helper through the child environment. Other commands delegate to Pi's exec; no process starts during registration.
|
|
49
|
+
16. `src/blocker-guidance.ts` appends `Blocked:`/`Resolve:` lines for known tool errors without changing cancellation, and supplies the same guidance for landing receipt refusals.
|
|
46
50
|
|
|
47
51
|
## Pi extension conventions
|
|
48
52
|
|
|
@@ -51,7 +55,7 @@ src/
|
|
|
51
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.
|
|
52
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`; `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`.
|
|
53
57
|
- Every tool defines a description, `promptSnippet`, and tool-specific `promptGuidelines` that explicitly name the tool.
|
|
54
|
-
- Git commands use `pi.exec("git", args, { cwd, signal, timeout })
|
|
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.
|
|
55
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`.
|
|
56
60
|
- Tool details avoid token values, abort signals, runtime objects, and unbounded raw command/API output.
|
|
57
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.
|
|
@@ -64,7 +68,7 @@ src/
|
|
|
64
68
|
- Related-PR lookup may issue an authenticated `GET /pulls` before every agent run and on explicit refresh. It has a 4-second timeout and 64 KiB response limit, and makes no unauthenticated fallback request.
|
|
65
69
|
- The start-of-run snapshot may be stale after a mutation in that same run; `branch_status` is the explicit read-only refresh. Its optional targeted ancestry proof accepts exact local branches or remote-tracking refs such as `origin/main` as read-only endpoints and must run after `integrate_branch` or `fetch_branch`, not in the same parallel tool batch.
|
|
66
70
|
- `init_repository` canonicalizes pi's exact current directory, rejects filesystem root, existing `.git` entries, reinitialization, and nesting inside another repository, then runs `git init --no-template --initial-branch <name>`. It verifies the exact non-bare repository root, in-place `.git` directory, requested unborn branch, and absence of a commit. It accepts no path or repository-mode controls and performs no automatic cleanup after an uncertain failure.
|
|
67
|
-
- `change_branch` mutates local HEAD and working-tree files only through `git switch <branchName>` for existing local branches after a clean-worktree
|
|
71
|
+
- `change_branch` mutates local HEAD and working-tree files only through `git switch <branchName>` for existing local branches after a tracked-clean preflight. `change_branch`, `pull_branch`, `rebase_branch`, `track_branch`, and `update_from_base` name blocking tracked paths and permit untracked-only checkouts; standalone integration and worktree removal retain their stricter clean preflights.
|
|
68
72
|
- `fetch_remote` refreshes all heads for one configured remote into only its tracking namespace. Pruning is opt-in, and configured refmaps, tags, tag pruning, submodules, and maintenance are disabled. Conventional same-namespace HEAD aliases are protected by excluding the remote branch named HEAD; unsafe or dangling symbolic destinations are refused, and non-files ref backends fail closed. Active-root locking interoperates with existing same-checkout tools; a common-directory lock additionally serializes `fetch_remote` calls across linked trees. External Git processes can still race preflight.
|
|
69
73
|
- `fetch_branch` runs `git fetch --no-tags --no-recurse-submodules <remote> <remote-ref>:<remote-tracking-ref>`; its explicit refspec updates only that tracking ref without changing 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.
|
|
70
74
|
- `pull_branch` requires a clean worktree and configured upstream, then updates only the current branch with an explicit `git pull --ff-only --no-rebase --no-autostash <remote> <remote-ref>` command; divergence fails without a rebase or merge commit.
|
|
@@ -80,7 +84,7 @@ src/
|
|
|
80
84
|
- `list_worktrees` reads a bounded `git worktree list --porcelain -z` inventory and remains explicit rather than expanding automatic active-worktree context.
|
|
81
85
|
- `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.
|
|
82
86
|
- `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.
|
|
83
|
-
- `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
|
|
87
|
+
- `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. A merged source in the primary checkout is released with a tracked-clean, idle, ignored-file-protecting switch to the target and a separate `checkout` receipt. Target sync is last and uses a non-forced fetch refspec, a tracked-clean ff-only pull, tracked-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.
|
|
84
88
|
- `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.
|
|
85
89
|
- `pull_request` requires resolved `headBranch` and `baseBranch` values to be distinct and exist locally, requires `headBranch` to match the GitHub-visible branch commit, queues behind already-started same-repository git mutation windows, makes GitHub REST API calls for the resolved current repository only, and rejects owner-prefixed or unsafe branch refs before the request. Omitted fields require configured autofill.
|
|
86
90
|
- `pull_request` reads `GITHUB_TOKEN` or `GH_TOKEN` from process environment first; only when neither process token is set does it read those token keys from a small regular `.env` file in the verified git root as a fallback. `BRANCHME_PR_AUTOFILL` uses the same process-first, `.env`-fallback precedence and defaults off.
|
|
@@ -101,6 +105,8 @@ src/
|
|
|
101
105
|
test/
|
|
102
106
|
├── command.test.mjs # /branchme parsing, help, fallback, panel width
|
|
103
107
|
├── git-context.test.mjs # collection, prompt hook, formatting, safety, and output bounds
|
|
108
|
+
├── blocker-guidance.test.mjs # actionable blocker messages and cancellation preservation
|
|
109
|
+
├── git-transport.test.mjs # credential protocol, absolute Git resolution, non-interactive execution and cancellation
|
|
104
110
|
├── git.test.mjs # branch/worktree parsing, validation, command construction, postconditions, failures
|
|
105
111
|
├── git-integration.test.mjs # isolated real-Git context, branch integration, and worktree lifecycle coverage
|
|
106
112
|
├── git-retirement.test.mjs # mocked retirement preflight, lease, postconditions, and failures
|
package/docs/TUI_CAPTURE.md
CHANGED
|
@@ -43,6 +43,7 @@ Commands only show info; BranchMe tools perform actions.
|
|
|
43
43
|
- `conclude_merge` — `action: "conclude"` commits the kept merge once every conflict marker is removed; `action: "abort"` restores the branch.
|
|
44
44
|
- `pull_request_status` — inspect a PR by number, or the latest PR for a head branch; not a CI/review verdict.
|
|
45
45
|
- `pull_request_feedback` — read a PR's unresolved review comments and failing checks with log tails.
|
|
46
|
+
- `reply_pull_request_feedback` — reply to a PR review thread (optionally resolving it) or post a PR comment.
|
|
46
47
|
- `pull_request` — reuses an exact matching open PR without changing its title, body, or draft state.
|
|
47
48
|
- `land_branch` — after host merge, clean up from the primary checkout; use `pullRequestNumber` for squash/rebase evidence.
|
|
48
49
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@senad-d/branchme",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
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",
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every BranchMe tool error reaches the agent as text. Known blockers get two extra
|
|
3
|
+
* lines: why the call was blocked and the next call that resolves it, so an agent
|
|
4
|
+
* without a shell can recover instead of stopping at the first refusal.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
interface BlockerRule {
|
|
8
|
+
pattern: RegExp;
|
|
9
|
+
why: string;
|
|
10
|
+
resolve: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// First match wins: specific Git and GitHub causes before generic ones.
|
|
14
|
+
const BLOCKER_RULES: readonly BlockerRule[] = [
|
|
15
|
+
{
|
|
16
|
+
pattern: /could not read (Username|Password)|terminal prompts disabled|Authentication failed|Invalid username or (password|token)|The requested URL returned error: 40[13]|Permission to \S+ denied/iu,
|
|
17
|
+
why: "the remote rejected the request or received no credentials.",
|
|
18
|
+
resolve: "set GITHUB_TOKEN or GH_TOKEN (process environment or the repository .env) to a token with read and write access to this repository's contents, or configure a git credential helper, then retry the same call once.",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
pattern: /Could not resolve host|Failed to connect|Connection (timed out|refused)|unable to access/iu,
|
|
22
|
+
why: "the remote host could not be reached.",
|
|
23
|
+
resolve: "retry once; if it fails again, report the remote as unreachable and stop.",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
pattern: /\(killed\)|was killed or timed out/iu,
|
|
27
|
+
why: "git did not finish within the time limit (slow or unreachable network).",
|
|
28
|
+
resolve: "retry the same call once; if it times out again, report the remote as unreachable and stop.",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
pattern: /\[rejected\]|non-fast-forward|\(fetch first\)|Updates were rejected/iu,
|
|
32
|
+
why: "the remote branch has commits the local branch does not.",
|
|
33
|
+
resolve: "fetch_branch, then pull_branch (or rebase_branch when history rewriting is authorized), then push_branch again; never force-push without explicit authorization.",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
pattern: /Not possible to fast-forward|diverg/iu,
|
|
37
|
+
why: "the local and remote branches have diverged; pull_branch only fast-forwards.",
|
|
38
|
+
resolve: "rebase_branch when history rewriting is authorized, or update_from_base to merge the base; otherwise report the divergence.",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
pattern: /couldn't find remote ref/iu,
|
|
42
|
+
why: "the remote has no branch of that name (a new or empty repository, or a wrong name).",
|
|
43
|
+
resolve: "fetch_remote, then list_branches to see the remote branches; push_branch publishes the current branch and creates it on the remote.",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
pattern: /would be overwritten by (checkout|merge)|untracked working tree files would be/iu,
|
|
47
|
+
why: "local files collide with files on the other branch.",
|
|
48
|
+
resolve: "the files git names must be committed, moved, or removed by the step that owns them; then retry.",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
pattern: /Tracked files have uncommitted changes/iu,
|
|
52
|
+
why: "the named tracked files have uncommitted changes.",
|
|
53
|
+
resolve: "commit them with commitme when they are approved for this step, or hand them to the step that owns them; then retry. Untracked files are never the blocker.",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
pattern: /has staged, unstaged, untracked, or unmerged changes/iu,
|
|
57
|
+
why: "the selected worktree has uncommitted changes, including possible untracked files.",
|
|
58
|
+
resolve: "commit approved changes with commitme or preserve them outside the worktree, then retry. remove_worktree with discardChanges: true requires explicit authorization to discard them; land_branch never discards uncommitted changes.",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
pattern: /occupied by \d+ registered worktree|already checked out in a worktree|occupies multiple worktrees|became occupied/iu,
|
|
62
|
+
why: "the branch is checked out in a worktree.",
|
|
63
|
+
resolve: "change_branch that checkout to another branch, or remove a linked worktree with remove_worktree, then retry. land_branch switches the primary checkout itself when it has no tracked changes.",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
pattern: /Target checkout has tracked changes/iu,
|
|
67
|
+
why: "the target branch's checkout has tracked changes, so it was not updated.",
|
|
68
|
+
resolve: "commit or hand off those changes, then pull_branch in that checkout.",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
pattern: /in-progress Git operation|must be completed or aborted|merge is in progress/iu,
|
|
72
|
+
why: "a merge, rebase, or cherry-pick is in progress.",
|
|
73
|
+
resolve: "finish or abort a merge with conclude_merge; any other operation needs a person, so report it.",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
pattern: /HEAD is detached|while HEAD is detached/iu,
|
|
77
|
+
why: "HEAD is not on a branch.",
|
|
78
|
+
resolve: "change_branch to a local branch, then retry.",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
pattern: /no upstream is configured/iu,
|
|
82
|
+
why: "the current branch has no upstream.",
|
|
83
|
+
resolve: "push_branch publishes the branch and sets its upstream; track_branch sets one for an existing remote branch.",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
pattern: /GitHub token is required/iu,
|
|
87
|
+
why: "no GitHub token was found.",
|
|
88
|
+
resolve: "set GITHUB_TOKEN or GH_TOKEN in the process environment or the repository .env, then retry.",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
pattern: /HTTP 401/iu,
|
|
92
|
+
why: "GitHub rejected the token.",
|
|
93
|
+
resolve: "replace GITHUB_TOKEN or GH_TOKEN with a valid token, then retry.",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
pattern: /HTTP 40[34]/iu,
|
|
97
|
+
why: "the token cannot see or change this repository (GitHub answers 404 for a private repository it cannot see).",
|
|
98
|
+
resolve: "use a token with access to this repository (contents and pull requests), then retry.",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
pattern: /Already on branch/iu,
|
|
102
|
+
why: "the checkout is already on that branch.",
|
|
103
|
+
resolve: "nothing to do; continue with the next step.",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
pattern: /Local branch .* already exists/iu,
|
|
107
|
+
why: "a local branch with that name exists.",
|
|
108
|
+
resolve: "change_branch to use it, or pick another name.",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
pattern: /Local branch .* does not exist|local branch '.*' does not exist|Local target branch (does not exist|is absent)/iu,
|
|
112
|
+
why: "no local branch has that name.",
|
|
113
|
+
resolve: "list_branches to check names (fetch_remote first for remote branches); track_branch creates a local branch from a remote one.",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
pattern: /moved after|changed while|moved concurrently|changed during/iu,
|
|
117
|
+
why: "repository state changed while the call ran.",
|
|
118
|
+
resolve: "refresh with branch_status or list_branches and retry once with the new values.",
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
/** "Blocked: ...\nResolve: ..." for a known blocker in `message`, otherwise null. */
|
|
123
|
+
export function blockerGuidance(message: string): string | null {
|
|
124
|
+
const rule = BLOCKER_RULES.find((candidate) => candidate.pattern.test(message));
|
|
125
|
+
return rule ? `Blocked: ${rule.why}\nResolve: ${rule.resolve}` : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function guidedError(error: unknown): unknown {
|
|
129
|
+
if (!(error instanceof Error)) return error;
|
|
130
|
+
const guidance = blockerGuidance(error.message);
|
|
131
|
+
if (!guidance || error.message.includes("\nResolve: ")) return error;
|
|
132
|
+
return new Error(`${error.message}\n${guidance}`, { cause: error });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function executeWithGuidance<TArgs extends unknown[], TResult>(
|
|
136
|
+
execute: (...args: TArgs) => Promise<TResult>,
|
|
137
|
+
signalIndex: number,
|
|
138
|
+
...args: TArgs
|
|
139
|
+
): Promise<TResult> {
|
|
140
|
+
try {
|
|
141
|
+
return await execute(...args);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
const signal = args[signalIndex] as AbortSignal | undefined;
|
|
144
|
+
if (signal?.aborted) throw error;
|
|
145
|
+
throw guidedError(error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Wraps a Pi tool so known blockers in its errors carry Blocked/Resolve lines. */
|
|
150
|
+
export function withBlockerGuidance<TTool extends { execute: (...args: never[]) => Promise<unknown> }>(tool: TTool): TTool {
|
|
151
|
+
// Pi calls execute(toolCallId, params, signal, onUpdate, ctx).
|
|
152
|
+
const execute = tool.execute as unknown as (...args: unknown[]) => Promise<unknown>;
|
|
153
|
+
return { ...tool, execute: executeWithGuidance.bind(undefined, execute, 2) as unknown as TTool["execute"] };
|
|
154
|
+
}
|
|
@@ -52,6 +52,7 @@ export function getBranchMeHelpText(): string {
|
|
|
52
52
|
"- `conclude_merge` — `action: \"conclude\"` commits the kept merge once every conflict marker is removed; `action: \"abort\"` restores the branch.",
|
|
53
53
|
"- `pull_request_status` — inspect a PR by number, or the latest PR for a head branch; not a CI/review verdict.",
|
|
54
54
|
"- `pull_request_feedback` — read a PR's unresolved review comments and failing checks with log tails.",
|
|
55
|
+
"- `reply_pull_request_feedback` — reply to a PR review thread (optionally resolving it) or post a PR comment.",
|
|
55
56
|
"- `pull_request` — reuses an exact matching open PR without changing its title, body, or draft state.",
|
|
56
57
|
"- `land_branch` — after host merge, clean up from the primary checkout; use `pullRequestNumber` for squash/rebase evidence.",
|
|
57
58
|
"",
|
package/src/constants.ts
CHANGED
|
@@ -17,6 +17,7 @@ export const PUSH_BRANCH_TOOL_NAME = "push_branch";
|
|
|
17
17
|
export const PULL_REQUEST_TOOL_NAME = "pull_request";
|
|
18
18
|
export const PULL_REQUEST_STATUS_TOOL_NAME = "pull_request_status";
|
|
19
19
|
export const PULL_REQUEST_FEEDBACK_TOOL_NAME = "pull_request_feedback";
|
|
20
|
+
export const REPLY_PULL_REQUEST_FEEDBACK_TOOL_NAME = "reply_pull_request_feedback";
|
|
20
21
|
export const LIST_WORKTREES_TOOL_NAME = "list_worktrees";
|
|
21
22
|
export const CREATE_WORKTREE_TOOL_NAME = "create_worktree";
|
|
22
23
|
export const REMOVE_WORKTREE_TOOL_NAME = "remove_worktree";
|
|
@@ -45,6 +46,7 @@ export const BRANCHME_TOOL_NAMES = [
|
|
|
45
46
|
PULL_REQUEST_TOOL_NAME,
|
|
46
47
|
PULL_REQUEST_STATUS_TOOL_NAME,
|
|
47
48
|
PULL_REQUEST_FEEDBACK_TOOL_NAME,
|
|
49
|
+
REPLY_PULL_REQUEST_FEEDBACK_TOOL_NAME,
|
|
48
50
|
LIST_WORKTREES_TOOL_NAME,
|
|
49
51
|
CREATE_WORKTREE_TOOL_NAME,
|
|
50
52
|
REMOVE_WORKTREE_TOOL_NAME,
|
|
@@ -95,5 +97,6 @@ export const PULL_REQUEST_FEEDBACK_LOG_JOB_LIMIT = 3;
|
|
|
95
97
|
export const PULL_REQUEST_FEEDBACK_BODY_LIMIT_CHARS = 2_000;
|
|
96
98
|
export const PULL_REQUEST_FEEDBACK_LOG_LIMIT_CHARS = 6_000;
|
|
97
99
|
export const PULL_REQUEST_FEEDBACK_SUMMARY_LIMIT_CHARS = 24_000;
|
|
100
|
+
export const PULL_REQUEST_REPLY_BODY_LIMIT_CHARS = 4_000;
|
|
98
101
|
export const GITHUB_GRAPHQL_TIMEOUT_MS = 15_000;
|
|
99
102
|
export const GITHUB_JOB_LOG_TIMEOUT_MS = 30_000;
|
package/src/extension.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { registerBranchMeCommand } from "./commands/branchme-command.ts";
|
|
3
3
|
import { registerGitContextAwareness } from "./git-context.ts";
|
|
4
|
+
import { withGitTransport } from "./git-transport.ts";
|
|
4
5
|
import { registerBranchMeTools } from "./tools/branchme-tools.ts";
|
|
5
6
|
|
|
6
7
|
export function branchMeExtension(pi: ExtensionAPI): void {
|
|
7
8
|
registerBranchMeCommand(pi);
|
|
8
|
-
registerBranchMeTools(pi);
|
|
9
|
+
registerBranchMeTools({ registerTool: pi.registerTool.bind(pi), exec: withGitTransport(pi).exec });
|
|
9
10
|
registerGitContextAwareness(pi);
|
|
10
11
|
}
|
|
11
12
|
|