opencode-gitbutler 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@ This plugin bridges the gap by bringing GitButler's virtual branch power directl
13
13
  ### What This Plugin Does Differently
14
14
 
15
15
  - Only tool that combines automatic branch creation, LLM commits, file assignment, and context injection.
16
- - Zero-config setup. Just run `bun add` and add it to your plugins.
16
+ - Zero-config setup. Just add it to your plugins and go.
17
17
  - Works with GitButler virtual branches to avoid worktree overhead.
18
18
  - Impersonates Cursor for full GitButler CLI compatibility.
19
19
  - Unique multi-agent session mapping.
@@ -21,37 +21,29 @@ This plugin bridges the gap by bringing GitButler's virtual branch power directl
21
21
 
22
22
  ## Installation
23
23
 
24
- Install the package from npm:
24
+ ### 1. Add plugin to OpenCode config
25
25
 
26
- ```bash
27
- bun add opencode-gitbutler
28
- ```
29
-
30
- Or add it to your plugins array in `.opencode/config.json`:
26
+ Add to your `opencode.json` (global or project-level):
31
27
 
32
28
  ```json
33
29
  {
34
- "plugins": ["opencode-gitbutler"]
30
+ "plugin": [
31
+ "opencode-gitbutler@latest"
32
+ ]
35
33
  }
36
34
  ```
37
35
 
38
- ## Prerequisites
36
+ OpenCode will install the plugin automatically on next launch.
39
37
 
40
- - **GitButler CLI** (`but`) — [Install via Homebrew](https://docs.gitbutler.com/installation)
41
- - **OpenCode** — v1.1.0 or later
42
- - **Bun** — v1.0.0 or later (plugin runtime)
38
+ ### 2. Install GitButler CLI
43
39
 
44
- The postinstall script checks for the GitButler CLI and warns if missing (install never fails).
45
-
46
- ## Quick Start
40
+ ```bash
41
+ brew install gitbutler
42
+ ```
47
43
 
48
- Add to your OpenCode config (`.opencode/config.json`):
44
+ See [GitButler installation docs](https://docs.gitbutler.com/installation) for other methods.
49
45
 
50
- ```json
51
- {
52
- "plugins": ["opencode-gitbutler"]
53
- }
54
- ```
46
+ ### 3. Restart OpenCode
55
47
 
56
48
  The plugin automatically:
57
49
  - Creates and renames branches based on your prompts
@@ -6,6 +6,14 @@ description: Clean up empty or orphaned GitButler branches
6
6
 
7
7
  Identify and remove empty or orphaned `ge-branch-*` branches that have accumulated from past sessions.
8
8
 
9
+ ## When to Run
10
+
11
+ - **After multi-session work** — each agent session may create `ge-branch-*` branches that become empty after commits are moved or squashed
12
+ - **When `but status` shows 3+ `ge-branch-*` branches** — workspace clutter slows down branch selection and increases lock contention
13
+ - **After `but cursor stop` failures** — the plugin's auto-cleanup has ~12% failure rate; empty branches may persist
14
+ - **When user reports "too many branches"** — proactively offer to run this command
15
+ - **User-named branches are NEVER auto-cleaned** — only `ge-branch-*` pattern branches are candidates
16
+
9
17
  ## Additional Instructions
10
18
 
11
19
  $ARGUMENTS
package/dist/index.js CHANGED
@@ -892,8 +892,46 @@ function createRewordManager(deps) {
892
892
  let cleanupCount = 0;
893
893
  let failCount = 0;
894
894
  let latestBranchName = null;
895
+ const owner = branchOwnership.get(conversationId);
896
+ const ownershipMatches = !owner || owner.rootSessionID === rootSessionID;
897
+ if (!ownershipMatches && owner) {
898
+ log.warn("branch-ownership-mismatch", {
899
+ conversationId,
900
+ expectedRootSessionID: owner.rootSessionID,
901
+ actualRootSessionID: rootSessionID
902
+ });
903
+ }
904
+ const candidateBranchCliIds = new Set;
905
+ if (ownershipMatches && editedFiles && editedFiles.size > 0) {
906
+ for (const filePath of editedFiles) {
907
+ const branchInfo = cli.findFileBranch(filePath, status);
908
+ if (branchInfo.branchCliId) {
909
+ candidateBranchCliIds.add(branchInfo.branchCliId);
910
+ }
911
+ }
912
+ }
913
+ if (ownershipMatches && owner?.branchName && !owner.branchName.startsWith("conversation-")) {
914
+ for (const stack of status.stacks) {
915
+ for (const branch of stack.branches ?? []) {
916
+ if (branch.name === owner.branchName) {
917
+ candidateBranchCliIds.add(branch.cliId);
918
+ }
919
+ }
920
+ }
921
+ }
922
+ const candidateBranches = status.stacks.flatMap((stack) => stack.branches ?? []).filter((branch) => candidateBranchCliIds.has(branch.cliId));
923
+ if (candidateBranches.length === 0) {
924
+ log.info("post-stop-no-candidate-branches", {
925
+ conversationId,
926
+ rootSessionID,
927
+ editedFiles: editedFiles?.size ?? 0,
928
+ ownerBranchName: owner?.branchName
929
+ });
930
+ }
895
931
  for (const stack of status.stacks) {
896
932
  for (const branch of stack.branches ?? []) {
933
+ if (!candidateBranchCliIds.has(branch.cliId))
934
+ continue;
897
935
  if (branch.branchStatus !== "completelyUnpushed")
898
936
  continue;
899
937
  if (branch.commits.length === 0)
@@ -993,18 +1031,27 @@ function createRewordManager(deps) {
993
1031
  }
994
1032
  }
995
1033
  }
996
- if (!latestBranchName) {
997
- const existing = status.stacks.flatMap((s) => s.branches ?? []).filter((b) => b.commits.length > 0 && !defaultBranchPattern.test(b.name));
998
- if (existing.length > 0) {
999
- latestBranchName = existing[existing.length - 1].name;
1000
- }
1034
+ const shouldSetTitle = candidateBranches.length === 1;
1035
+ if (!shouldSetTitle && candidateBranches.length > 1) {
1036
+ log.info("session-title-skipped-ambiguous", {
1037
+ conversationId,
1038
+ rootSessionID,
1039
+ candidateBranches: candidateBranches.map((b) => ({
1040
+ cliId: b.cliId,
1041
+ name: b.name
1042
+ }))
1043
+ });
1001
1044
  }
1002
- if (latestBranchName) {
1003
- client.session.update({
1004
- path: { id: rootSessionID },
1005
- body: { title: latestBranchName }
1006
- }).catch(() => {});
1007
- addNotification(sessionID, `Session title updated to \`${latestBranchName}\``);
1045
+ if (shouldSetTitle) {
1046
+ const singleBranch = candidateBranches[0];
1047
+ const titleToSet = latestBranchName ?? singleBranch.name;
1048
+ if (titleToSet) {
1049
+ client.session.update({
1050
+ path: { id: rootSessionID },
1051
+ body: { title: titleToSet }
1052
+ }).catch(() => {});
1053
+ addNotification(sessionID, `Session title updated to \`${titleToSet}\``);
1054
+ }
1008
1055
  }
1009
1056
  for (const stack of status.stacks) {
1010
1057
  for (const branch of stack.branches ?? []) {
@@ -1 +1 @@
1
- {"version":3,"file":"reword.d.ts","sourceRoot":"","sources":["../src/reword.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"reword.d.ts","sourceRoot":"","sources":["../src/reword.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-gitbutler",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for GitButler integration",
6
6
  "main": "dist/index.js",
@@ -60,6 +60,9 @@ You can batch multiple file edits before committing - no need to commit after ev
60
60
  - After edits, run `but status --json` and move your file/hunk IDs to the correct branch via `but stage` or `but commit --changes`.
61
61
  3. **Validate branch ownership before commit.**
62
62
  - Confirm each changed file/hunk belongs to the intended branch/task, then commit only those IDs.
63
+ 4. **Respect branch ownership across sessions.**
64
+ - In multi-agent environments, branches may belong to other agent sessions. Never reword, rename, or push branches you didn't create in this session.
65
+ - If you need to modify another session's branch, ask the user first.
63
66
 
64
67
  ## Quick Start
65
68
 
@@ -257,6 +260,34 @@ but absorb # Absorb any auto-formatted c
257
260
  | Lefthook `pre-commit.old` accumulates | Lefthook creates `pre-commit.old` backup that conflicts on next install | Add `rm -f .git/hooks/pre-commit.old` to `prepare` script in package.json |
258
261
  | `but pull` before unapply | Pulling with merged branches still applied causes orphan errors | **Always** `but unapply <merged-branch>` before `but pull` |
259
262
  | `but unapply` after remote branch deletion | `but unapply` fails with "branch not found" when remote deleted the branch (e.g. `--delete-branch` on merge), and subsequent `but pull` fails with "resolution mismatch" | Use GitButler desktop app to pull, or `but teardown` → `but setup` → `but config target origin/<branch>` |
263
+ | Split-hunk files stuck in `zz` | File has hunks locked to commits on different branches — GitButler sets `stack_id=None`, plugin considers file "handled" via lock reference | Manually commit each hunk: `but diff --json` to get hunk IDs, then `but commit <branch> -m "msg" --changes <hunk-id>` for each |
264
+ | Plugin auto-cleanup misses empty branches | `ge-branch-*` cleanup has ~12% failure rate; user-named empty branches are never auto-cleaned | Run `/b-branch-gc` command or manually `but unapply <branch-id>` |
265
+ | Notifications not reaching agent | Plugin notification delivery is ~55% (259 queued vs 142 delivered in observed sessions) | Always verify state with `but status --json` — don't rely on `<system-reminder>` notifications alone |
266
+
267
+ ### Diagnosing `zz` Stuck Files
268
+
269
+ When files are stuck in `zz` (unassigned) and don't auto-recover:
270
+
271
+ 1. **Identify the cause:**
272
+ ```bash
273
+ but status --json -f # Look for files in zz with [LOCKED] markers
274
+ but diff --json # Get hunk-level IDs and see lock targets
275
+ ```
276
+
277
+ 2. **If hunks are locked to different branches** (split-hunk scenario):
278
+ - Each hunk must be committed to its locked branch individually
279
+ - `but commit <branch> -m "msg" --changes <hunk-id>` for each hunk
280
+ - Or `but rub <hunk-id> <commit-id>` to amend into the locked commit
281
+
282
+ 3. **If files have no locks but are still in `zz`:**
283
+ - Plugin's `after-edit` may have failed silently — stage manually
284
+ - `but stage <file-id> <branch>` or commit with `--changes`
285
+
286
+ 4. **If many files are stuck after `but cursor stop`:**
287
+ - Run `but rub <file-id> <branch-id>` for each file to force assignment
288
+ - This is the most reliable recovery method
289
+
290
+ **Key insight:** Auto-recovery won't fix multi-branch locked files. If you see `[LOCKED]` in `zz`, manual intervention is required.
260
291
 
261
292
  ## Critical Safety Rules
262
293
 
@@ -273,3 +304,27 @@ but absorb # Absorb any auto-formatted c
273
304
  6. Use `--dry-run` flags (push, absorb) when unsure
274
305
  7. **Run `but pull` frequently** — at session start, before creating branches, and before pushing. Stale workspace = merge conflicts
275
306
  8. When updating this skill, use `but skill install --path <known-path>` to avoid prompts
307
+ 9. **Check for `zz` files with locks before finishing work.** Run `but status --json -f` and look for files in `zz` with `[LOCKED]` markers. These won't auto-recover — you must manually commit each hunk to its correct branch using `--changes <hunk-id>`. See [references/concepts.md — Hunk Locking](references/concepts.md) for details.
308
+ 10. **Don't trust notifications alone** — plugin notification delivery is ~55%. Always verify workspace state with `but status --json` before making assumptions about branch assignments or commit status.
309
+
310
+ ## Plugin Auto-Behaviors (What Happens Behind the Scenes)
311
+
312
+ The GitButler plugin performs several actions automatically. **You do NOT need to do these yourself** — but you should know they happen so you don't duplicate work or get confused by unexpected state changes.
313
+
314
+ | Behavior | Trigger | What It Does |
315
+ |----------|---------|--------------|
316
+ | **File auto-assign** | After each file edit | Finds which branch owns the file and runs `but cursor after-edit` or `but rub` to assign it |
317
+ | **Auto-commit** | Session idle (agent stops editing) | Runs `but cursor stop` which commits all uncommitted changes to their assigned branches |
318
+ | **LLM commit reword** | After auto-commit | Rewrites generic commit messages using Claude Haiku based on the actual diff |
319
+ | **Branch rename** | After auto-commit | Renames `ge-branch-*` branches to descriptive names based on user's prompt |
320
+ | **Empty branch cleanup** | After auto-commit | Removes `ge-branch-*` branches with 0 commits (~88% success rate) |
321
+ | **Session title sync** | After auto-commit | Updates session title from branch name |
322
+ | **Context injection** | Before each agent message | Injects `<system-reminder>` with workspace notifications (branch created, commits made, etc.) |
323
+
324
+ ### What This Means for You
325
+
326
+ 1. **Don't manually rename `ge-branch-*` branches** — the plugin will do it after idle
327
+ 2. **Don't reword auto-generated commit messages** — the plugin rewrites them with LLM
328
+ 3. **If you see unexpected commits** — check if the plugin auto-committed during an idle event
329
+ 4. **Notification delivery is ~55%** — not all injected notifications reach you. Always verify state with `but status --json` rather than relying on notifications
330
+ 5. **Empty branch cleanup can fail** — if you see stale `ge-branch-*` branches, run `/b-branch-gc`
@@ -201,6 +201,7 @@ but pull # Then pull merged changes
201
201
  | `M` | Modified |
202
202
  | `D` | Deleted |
203
203
  | `[LOCKED]` | File depends on specific commit (absorb target) |
204
+ | `zz` | Unassigned — file not staged to any branch |
204
205
  | `●` | Commit |
205
206
  | `CONFLICTED` | Needs conflict resolution |
206
207
 
@@ -269,3 +270,16 @@ but pr new feat/my-feature -t
269
270
  but unapply feat/my-feature
270
271
  but pull
271
272
  ```
273
+
274
+ ---
275
+
276
+ ## Troubleshooting Quick Reference
277
+
278
+ | Symptom | Cause | Fix |
279
+ |---|---|---|
280
+ | Files stuck in `zz` with `[LOCKED]` | Hunks locked to commits on different branches | `but diff --json` → commit each hunk individually with `--changes <hunk-id>` |
281
+ | Files in `zz` after edits (no locks) | Plugin `after-edit` didn't auto-assign | `but stage <file-id> <branch>` or `but commit <branch> -m "msg" --changes <id>` |
282
+ | Many empty `ge-branch-*` branches | Plugin auto-cleanup failed (~12% failure rate) | Run `/b-branch-gc` or `but unapply <branch-id>` for each |
283
+ | `but absorb` puts hunk on wrong commit | Hunk locked to commit on different branch | Use `but amend <file-id> <commit-id>` for explicit control |
284
+ | `but pull` fails after PR merge | Merged branch still applied in workspace | `but unapply <merged-branch>` first, then `but pull` |
285
+ | Changes "disappear" after `but cursor stop` | Plugin auto-committed to a `ge-branch-*` | `but status --json -f` — check all branches for your files |
@@ -177,6 +177,40 @@ Prevents you from creating broken states:
177
177
  - Can't stage changes to wrong branches
178
178
  - Ensures each branch remains independently functional
179
179
 
180
+ ## Hunk Locking & Split Files
181
+
182
+ When a file has changes touching lines that belong to commits on **different branches**, GitButler "locks" those hunks.
183
+
184
+ ### How Locking Works
185
+
186
+ ```
187
+ File: src/api.ts
188
+ Line 10-15: depends on commit C1 (branch: feat/auth)
189
+ Line 40-50: depends on commit C3 (branch: fix/validation)
190
+ Line 80-90: new code, no dependency
191
+ ```
192
+
193
+ - Lines 10-15 are **locked to** `feat/auth` — shown as `[LOCKED → C1]`
194
+ - Lines 40-50 are **locked to** `fix/validation` — shown as `[LOCKED → C3]`
195
+ - Lines 80-90 are **free** — can be staged/committed to any branch
196
+
197
+ ### Split Hunk Assignment (`zz` Trap)
198
+
199
+ When a file's hunks are locked to **multiple different branches**, GitButler cannot auto-assign the file to any single branch. The file stays in `zz` (unassigned) with lock markers.
200
+
201
+ **This is the most common cause of files "stuck in `zz`"** — the plugin's `after-edit` hook sees the file is mentioned on a branch (via lock) and considers it "handled", but the file remains unassigned.
202
+
203
+ **Resolution:**
204
+
205
+ 1. Run `but status --json -f` to identify locked files in `zz`
206
+ 2. Use `but diff --json` to see individual hunk IDs and their lock targets
207
+ 3. Commit or amend each hunk individually: `but commit <branch> -m "msg" --changes <hunk-id>`
208
+ 4. Or use `but rub <hunk-id> <commit-id>` to amend specific hunks into their locked commits
209
+
210
+ ### Key Takeaway
211
+
212
+ If files are stuck in `zz` with `[LOCKED]` markers, **don't wait for auto-recovery** — it won't happen for multi-branch locks. Manually assign each hunk to its correct branch.
213
+
180
214
  ## Empty Commits as Placeholders
181
215
 
182
216
  You can create empty commits: