borgmcp 0.9.36 → 0.9.38

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.
@@ -0,0 +1,173 @@
1
+ /**
2
+ * gh#33 — worktree lifecycle as product behavior.
3
+ *
4
+ * Pure git-decision helpers behind an injected `runSync` seam (matching
5
+ * the `AssimilateDeps.runSync` shape), so every branch is unit-testable
6
+ * without a live repo. This module DECIDES + emits git command sequences;
7
+ * it never launches agents and never touches the cube API.
8
+ *
9
+ * Design spec: docs/superpowers/specs/2026-05-29-worktree-lifecycle-design.md
10
+ * Q-resolutions baked in (SPEC-APPROVED 3a80412d):
11
+ * Q1 branch naming — `wt-<suffix>` prefix-stripped, full-basename fallback.
12
+ * Q2 idle-sync — ff-only, clean-gated; never merge/rebase; never over dirty.
13
+ * Q3 post-merge — auto-return to wt-<basename>; ANNOUNCE the prunable
14
+ * merged branch, prune only when explicitly requested.
15
+ * Q4 uniform — no primary-worktree carve-out; main is never a working branch.
16
+ */
17
+ /**
18
+ * Per-worktree branch name (Q1). Strips the repo basename prefix from the
19
+ * worktree dir basename for readability (`borg-mcp-codex-builder` ->
20
+ * `wt-codex-builder`); falls back to the full dir basename when there is
21
+ * no shared prefix (`myrepo-feature` under repo `otherrepo` ->
22
+ * `wt-myrepo-feature`).
23
+ */
24
+ export function perWorktreeBranchName(worktreeBasename, repoBasename) {
25
+ const prefix = `${repoBasename}-`;
26
+ const suffix = worktreeBasename.startsWith(prefix)
27
+ ? worktreeBasename.slice(prefix.length)
28
+ : worktreeBasename;
29
+ return `wt-${suffix}`;
30
+ }
31
+ /** True iff the working tree is clean (`git status --porcelain` empty). */
32
+ export function isCleanTree(runSync, cwd) {
33
+ const r = runSync('git', ['status', '--porcelain'], cwd);
34
+ return r.status === 0 && r.stdout.trim() === '';
35
+ }
36
+ const LOCAL_CONFIG_RE = /^\.claude\//;
37
+ /**
38
+ * Classify a dirty tree into staged / unstaged / untracked buckets, and
39
+ * flag local-config files separately. The STAGED bucket is load-bearing:
40
+ * the live UNBLOCK case (b15894be) had a *staged* leftover diff that
41
+ * blocked `pull --ff-only`, which an unstaged-only check would miss.
42
+ */
43
+ export function classifyDirty(runSync, cwd) {
44
+ const r = runSync('git', ['status', '--porcelain'], cwd);
45
+ const out = { staged: [], unstaged: [], untracked: [], localConfig: [] };
46
+ if (r.status !== 0)
47
+ return out;
48
+ for (const line of r.stdout.split('\n')) {
49
+ if (!line.trim())
50
+ continue;
51
+ const path = line.slice(3);
52
+ if (line.startsWith('??')) {
53
+ out.untracked.push(path);
54
+ }
55
+ else {
56
+ const x = line[0]; // staged (index) column
57
+ const y = line[1]; // unstaged (work-tree) column
58
+ if (x !== ' ' && x !== '?')
59
+ out.staged.push(path);
60
+ if (y !== ' ' && y !== '?')
61
+ out.unstaged.push(path);
62
+ }
63
+ if (LOCAL_CONFIG_RE.test(path))
64
+ out.localConfig.push(path);
65
+ }
66
+ return out;
67
+ }
68
+ /** True iff `branch` is an ancestor of `ref` — i.e. a clean fast-forward target. */
69
+ export function isFastForward(runSync, cwd, branch, ref) {
70
+ return runSync('git', ['merge-base', '--is-ancestor', branch, ref], cwd).status === 0;
71
+ }
72
+ /** True iff `branch`'s tip is an ancestor of `ref` — i.e. fully merged into it. */
73
+ export function isMerged(runSync, cwd, branch, ref) {
74
+ return runSync('git', ['merge-base', '--is-ancestor', branch, ref], cwd).status === 0;
75
+ }
76
+ /**
77
+ * Idle-sync the current per-worktree branch to `ref` (Q2). NEVER discards
78
+ * work: dirty -> skipped-dirty (no mutation). Only fast-forwards (no
79
+ * merge/rebase): diverged -> skipped-diverged. The caller fetches first.
80
+ *
81
+ * `already-current` when the branch tip already equals `ref` (the common
82
+ * no-op case on every launch).
83
+ */
84
+ export function syncWorktree(runSync, cwd, branch, ref) {
85
+ if (!isCleanTree(runSync, cwd)) {
86
+ return {
87
+ action: 'skipped-dirty',
88
+ message: 'uncommitted changes present; sync skipped (nothing discarded)',
89
+ };
90
+ }
91
+ if (!isFastForward(runSync, cwd, branch, ref)) {
92
+ return {
93
+ action: 'skipped-diverged',
94
+ message: `${branch} has diverged from ${ref}; resolve manually (no auto-merge/rebase)`,
95
+ };
96
+ }
97
+ // Already at ref? merge --ff-only is a no-op but we report it distinctly
98
+ // so callers can stay quiet on the common case.
99
+ const ahead = runSync('git', ['rev-list', '--count', `${branch}..${ref}`], cwd);
100
+ if (ahead.status === 0 && ahead.stdout.trim() === '0') {
101
+ return { action: 'already-current' };
102
+ }
103
+ const ff = runSync('git', ['merge', '--ff-only', ref], cwd);
104
+ if (ff.status !== 0) {
105
+ return { action: 'skipped-diverged', message: 'ff-only merge unexpectedly failed' };
106
+ }
107
+ return { action: 'fast-forwarded' };
108
+ }
109
+ /** True iff a local branch named `branch` already exists. */
110
+ export function localBranchExists(runSync, cwd, branch) {
111
+ return runSync('git', ['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`], cwd).status === 0;
112
+ }
113
+ /**
114
+ * Migration (Q4/Q5/§4.5): bring a detached/stale worktree onto
115
+ * `wt-<basename>` at `ref`. Idempotent: re-running on an already-adopted
116
+ * clean worktree is a lossless reset to `ref`. Never discards:
117
+ * - dirty work tree -> skipped-dirty (surface)
118
+ * - current HEAD unmerged -> blocked-unmerged (surface)
119
+ * - TARGET `branch` exists with commits not on `ref` -> blocked-target-
120
+ * unmerged (surface). This is load-bearing: the switch uses `-C`
121
+ * (force-create/reset), which would ORPHAN commits on a pre-existing
122
+ * `wt-` branch. The HEAD-merged check alone misses this when the
123
+ * target branch != HEAD (e.g. on `main` while a prior `wt-x` holds
124
+ * committed-but-unmerged work). gh#33 CR-v2 blocker 078d1630.
125
+ */
126
+ export function adoptWorktree(runSync, cwd, branch, ref) {
127
+ if (!isCleanTree(runSync, cwd)) {
128
+ return {
129
+ action: 'skipped-dirty',
130
+ message: 'uncommitted changes present; not switching (nothing discarded)',
131
+ };
132
+ }
133
+ if (!isMerged(runSync, cwd, 'HEAD', ref)) {
134
+ return {
135
+ action: 'blocked-unmerged',
136
+ message: `current HEAD has commits not on ${ref}; commit/push or set aside before adopting`,
137
+ };
138
+ }
139
+ // Guard the TARGET branch ref before the `switch -C` force-reset. If
140
+ // `branch` already exists and is NOT an ancestor of `ref`, it carries
141
+ // committed-unmerged work that `-C` would discard — block instead.
142
+ // (Absent, or an ancestor of `ref` = a clean reset target → proceed.)
143
+ if (localBranchExists(runSync, cwd, branch) && !isMerged(runSync, cwd, branch, ref)) {
144
+ return {
145
+ action: 'blocked-target-unmerged',
146
+ message: `branch ${branch} exists with commits not on ${ref}; resolve before adopting (a force-switch would discard them)`,
147
+ };
148
+ }
149
+ runSync('git', ['switch', '-C', branch, ref], cwd);
150
+ return { action: 'adopted' };
151
+ }
152
+ /**
153
+ * Post-merge cleanup (Q3): when `feature` is fully merged into `ref`,
154
+ * either ANNOUNCE it as prunable (default) or actually prune it with the
155
+ * safe `git branch -d` (which itself refuses to delete an unmerged
156
+ * branch — defense in depth against a stale local ref). Unmerged ->
157
+ * not-merged (never touched).
158
+ */
159
+ export function cleanupMerged(runSync, cwd, feature, ref, opts = { prune: false }) {
160
+ if (!isMerged(runSync, cwd, feature, ref)) {
161
+ return { action: 'not-merged', branch: feature };
162
+ }
163
+ if (!opts.prune) {
164
+ return {
165
+ action: 'announced',
166
+ branch: feature,
167
+ message: `${feature} is merged into ${ref} and can be pruned: \`git branch -d ${feature}\` (or re-run with --prune)`,
168
+ };
169
+ }
170
+ runSync('git', ['branch', '-d', feature], cwd);
171
+ return { action: 'pruned', branch: feature };
172
+ }
173
+ //# sourceMappingURL=worktree-lifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree-lifecycle.js","sourceRoot":"","sources":["../src/worktree-lifecycle.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "0.9.36",
3
+ "version": "0.9.38",
4
4
  "description": "Coordinate AI coding agents in shared cubes. Works with Claude Code and Codex. Create projects, assign roles, and share a live activity log.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",