@senad-d/branchme 0.2.1 → 0.3.0
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 +9 -2
- package/README.md +47 -13
- package/SECURITY.md +14 -5
- package/docs/PROJECT_DEFINITION_BRIEF.md +8 -3
- package/docs/SMOKE_TEST.md +3 -3
- package/docs/STRUCTURE.md +10 -6
- package/docs/TUI_CAPTURE.md +2 -2
- package/package.json +1 -1
- package/src/commands/branchme-command.ts +2 -2
- package/src/constants.ts +2 -0
- package/src/git-landing.ts +449 -0
- package/src/git-retirement.ts +34 -11
- package/src/git.ts +250 -28
- package/src/tools/branchme-tools.ts +88 -18
- package/src/types.ts +12 -0
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
GIT_RETIREMENT_SUMMARY_LIMIT_CHARS,
|
|
12
12
|
GIT_WORKTREE_SUMMARY_LIMIT_CHARS,
|
|
13
13
|
INTEGRATE_BRANCH_TOOL_NAME,
|
|
14
|
+
LAND_BRANCH_TOOL_NAME,
|
|
14
15
|
LIST_WORKTREES_TOOL_NAME,
|
|
15
16
|
PULL_BRANCH_TOOL_NAME,
|
|
16
17
|
PULL_REQUEST_TOOL_NAME,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
createLocalBranch,
|
|
25
26
|
createWorktree,
|
|
26
27
|
fetchCurrentBranch,
|
|
28
|
+
fetchRemoteBranch,
|
|
27
29
|
getGitRoot,
|
|
28
30
|
getLocalBranchCommit,
|
|
29
31
|
getPullRequestCommitSubjects,
|
|
@@ -41,6 +43,7 @@ import {
|
|
|
41
43
|
import { collectGitContext, formatGitContext } from "../git-context.ts";
|
|
42
44
|
import { integrateBranch } from "../git-integration.ts";
|
|
43
45
|
import { retireBranch } from "../git-retirement.ts";
|
|
46
|
+
import { formatLandBranch, landBranch } from "../git-landing.ts";
|
|
44
47
|
import {
|
|
45
48
|
createGitHubPullRequest,
|
|
46
49
|
ensureGitHubBranchExists,
|
|
@@ -74,8 +77,8 @@ const BranchStatusParametersSchema = Type.Object(
|
|
|
74
77
|
ancestry: Type.Optional(
|
|
75
78
|
Type.Object(
|
|
76
79
|
{
|
|
77
|
-
sourceBranch: Type.String({ minLength: 1, description: "Exact existing local
|
|
78
|
-
targetBranch: Type.String({ minLength: 1, description: "Exact existing local
|
|
80
|
+
sourceBranch: Type.String({ minLength: 1, description: "Exact existing local branch or remote-tracking ref (for example origin/main) whose commit is verified as the ancestor." }),
|
|
81
|
+
targetBranch: Type.String({ minLength: 1, description: "Exact existing local branch or remote-tracking ref (for example origin/main) whose commit is verified as the descendant." }),
|
|
79
82
|
},
|
|
80
83
|
{ additionalProperties: false },
|
|
81
84
|
),
|
|
@@ -105,6 +108,16 @@ const RetireBranchParametersSchema = Type.Object(
|
|
|
105
108
|
{ additionalProperties: false },
|
|
106
109
|
);
|
|
107
110
|
|
|
111
|
+
const LandBranchParametersSchema = Type.Object(
|
|
112
|
+
{
|
|
113
|
+
sourceBranch: Type.String({ minLength: 1, description: "Merged local feature branch to delete, or its name on an idempotent retry." }),
|
|
114
|
+
targetBranch: Type.String({ minLength: 1, description: "Local default branch to fast-forward after cleanup." }),
|
|
115
|
+
remote: Type.Optional(Type.String({ minLength: 1, description: "Configured remote name; defaults to origin." })),
|
|
116
|
+
worktreePath: Type.Optional(Type.String({ minLength: 1, description: "Absolute linked-worktree path to remove, including one parked on targetBranch. Omit to find the source branch's worktree." })),
|
|
117
|
+
},
|
|
118
|
+
{ additionalProperties: false },
|
|
119
|
+
);
|
|
120
|
+
|
|
108
121
|
const CreateBranchParametersSchema = Type.Object(
|
|
109
122
|
{
|
|
110
123
|
branchName: Type.String({ minLength: 1, description: "Name of the new branch to create from current HEAD." }),
|
|
@@ -124,8 +137,26 @@ const CreateWorktreeParametersSchema = Type.Object(
|
|
|
124
137
|
worktreePath: Type.String({ minLength: 1, description: "Explicit absolute destination path for the linked worktree." }),
|
|
125
138
|
branchName: Type.String({ minLength: 1, description: "New or existing local branch name for the linked worktree." }),
|
|
126
139
|
branchMode: StringEnum(["new", "existing"] as const, {
|
|
127
|
-
description: "Whether create_worktree creates a new local branch from current HEAD or uses an existing local branch.",
|
|
140
|
+
description: "Whether create_worktree creates a new local branch (from current HEAD, or from baseRef when provided) or uses an existing local branch.",
|
|
128
141
|
}),
|
|
142
|
+
baseRef: Type.Optional(Type.String({
|
|
143
|
+
minLength: 1,
|
|
144
|
+
description: "Optional read-only start point for branchMode 'new': an exact existing local branch, remote-tracking ref (for example origin/main), or full commit that the new branch starts from instead of current HEAD. Never checked out or mutated.",
|
|
145
|
+
})),
|
|
146
|
+
},
|
|
147
|
+
{ additionalProperties: false },
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const FetchBranchParametersSchema = Type.Object(
|
|
151
|
+
{
|
|
152
|
+
remote: Type.Optional(Type.String({
|
|
153
|
+
minLength: 1,
|
|
154
|
+
description: "Configured remote name for a targeted fetch; defaults to origin and requires branch. Omit together with branch to fetch the current branch's configured upstream.",
|
|
155
|
+
})),
|
|
156
|
+
branch: Type.Optional(Type.String({
|
|
157
|
+
minLength: 1,
|
|
158
|
+
description: "Exact branch name on the remote (for example main) to fetch into its remote-tracking ref without touching local branches or the current branch's upstream. Omit to fetch the current branch's configured upstream.",
|
|
159
|
+
})),
|
|
129
160
|
},
|
|
130
161
|
{ additionalProperties: false },
|
|
131
162
|
);
|
|
@@ -256,7 +287,10 @@ export function formatCreateWorktree(details: CreateWorktreeDetails): string {
|
|
|
256
287
|
const cwd = safeWorktreeFormatValue(details.handoff.cwd, WORKTREE_FORMAT_PATH_LIMIT_CHARS);
|
|
257
288
|
const branch = safeWorktreeFormatValue(details.handoff.branch, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS);
|
|
258
289
|
const mode = details.request.branchMode === "new" ? "new local branch" : "existing local branch";
|
|
259
|
-
|
|
290
|
+
const base = details.request.baseRef === undefined
|
|
291
|
+
? ""
|
|
292
|
+
: ` from base ${safeWorktreeFormatValue(details.request.baseRef, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS)}`;
|
|
293
|
+
return `Created linked worktree ${cwd} for ${mode} ${branch}${base}. Verified its canonical path, local branch, HEAD ${shortCommit(details.handoff.head)}, clean working tree, and ready handoff.`;
|
|
260
294
|
}
|
|
261
295
|
|
|
262
296
|
export function formatRemoveWorktree(details: RemoveWorktreeDetails): string {
|
|
@@ -487,12 +521,13 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
|
|
|
487
521
|
pi.registerTool({
|
|
488
522
|
name: BRANCH_STATUS_TOOL_NAME,
|
|
489
523
|
label: "Branch Status",
|
|
490
|
-
description: "branch_status explicitly refreshes the current Git repository snapshot and can optionally verify whether one captured
|
|
491
|
-
promptSnippet: "branch_status: explicitly refresh current-repository Git state and optionally verify targeted local-branch ancestry without mutation",
|
|
524
|
+
description: "branch_status explicitly refreshes the current Git repository snapshot and can optionally verify whether one captured branch commit is an ancestor of another; ancestry endpoints may be exact local branches or remote-tracking refs such as origin/main. branch_status is read-only and never mutates files, Git state, or GitHub state.",
|
|
525
|
+
promptSnippet: "branch_status: explicitly refresh current-repository Git state and optionally verify targeted local-branch or remote-tracking ancestry without mutation",
|
|
492
526
|
promptGuidelines: [
|
|
493
527
|
"Use the automatic Git context for start-of-run questions; call branch_status only for an explicit refresh or after Git state changes during the current run.",
|
|
494
528
|
"Use branch_status as a read-only refresh; branch_status never mutates files, Git state, or GitHub state.",
|
|
495
529
|
"Use targeted branch_status ancestry verification only after integrate_branch completes; do not issue branch_status in the same parallel tool batch as integrate_branch.",
|
|
530
|
+
"Use branch_status ancestry with a remote-tracking target such as origin/main after fetch_branch completes to prove a branch is contained in the fetched baseline; branch_status never checks out or resets remote-tracking refs.",
|
|
496
531
|
],
|
|
497
532
|
parameters: BranchStatusParametersSchema,
|
|
498
533
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -564,18 +599,29 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
|
|
|
564
599
|
pi.registerTool({
|
|
565
600
|
name: FETCH_BRANCH_TOOL_NAME,
|
|
566
601
|
label: "Fetch Branch",
|
|
567
|
-
description: "fetch_branch fetches
|
|
568
|
-
promptSnippet: "fetch_branch: fetch the current branch's configured upstream into its remote-tracking ref without changing local branches or files",
|
|
602
|
+
description: "fetch_branch fetches one branch into its remote-tracking ref with an explicit git fetch --no-tags --no-recurse-submodules refspec. Without arguments fetch_branch fetches the current branch's configured upstream; with branch (and optional remote, default origin) it fetches that exact remote branch without touching local branches, the working tree, or the current branch's upstream configuration. fetch_branch never prunes, rebases, merges, stashes, stages, commits, or pushes.",
|
|
603
|
+
promptSnippet: "fetch_branch: fetch the current branch's configured upstream, or an explicit remote branch, into its remote-tracking ref without changing local branches or files",
|
|
569
604
|
promptGuidelines: [
|
|
570
|
-
"Use fetch_branch
|
|
571
|
-
"Use fetch_branch
|
|
572
|
-
"Call fetch_branch and wait for it to complete before rebase_branch
|
|
605
|
+
"Use fetch_branch without arguments to fetch the configured upstream branch for the current branch; that default requires a current branch with an upstream.",
|
|
606
|
+
"Use fetch_branch with branch (and optional remote, default origin) to refresh another remote-tracking ref such as origin/main; fetch_branch never changes local branches, working-tree files, or upstream configuration, and remote requires branch.",
|
|
607
|
+
"Call fetch_branch and wait for it to complete before rebase_branch or a targeted branch_status ancestry proof against the fetched remote-tracking ref; do not batch fetch_branch with dependent calls.",
|
|
573
608
|
],
|
|
574
|
-
parameters:
|
|
575
|
-
async execute(_toolCallId,
|
|
576
|
-
|
|
609
|
+
parameters: FetchBranchParametersSchema,
|
|
610
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
611
|
+
if (params.branch === undefined) {
|
|
612
|
+
if (params.remote !== undefined) {
|
|
613
|
+
throw new Error("fetch_branch remote requires branch; omit both to fetch the current branch's configured upstream.");
|
|
614
|
+
}
|
|
615
|
+
const details = await fetchCurrentBranch(pi, ctx, signal);
|
|
616
|
+
return {
|
|
617
|
+
content: [{ type: "text", text: `Fetched configured upstream remote ${details.remote} for current branch ${details.currentBranch}.` }],
|
|
618
|
+
details,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const details = await fetchRemoteBranch(pi, ctx, params.remote ?? "origin", params.branch, signal);
|
|
577
623
|
return {
|
|
578
|
-
content: [{ type: "text", text: `Fetched
|
|
624
|
+
content: [{ type: "text", text: `Fetched ${details.remote}/${details.branch} into remote-tracking ref ${details.remoteTrackingRef} without changing local branches.` }],
|
|
579
625
|
details,
|
|
580
626
|
};
|
|
581
627
|
},
|
|
@@ -669,6 +715,28 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
|
|
|
669
715
|
},
|
|
670
716
|
});
|
|
671
717
|
|
|
718
|
+
pi.registerTool({
|
|
719
|
+
name: LAND_BRANCH_TOOL_NAME,
|
|
720
|
+
label: "Land Branch",
|
|
721
|
+
description: "land_branch performs deterministic post-merge cleanup: fetch the remote target, prove source ancestry, remove its clean linked worktree (including ignored residue), lease-delete the local source branch, then fast-forward the local target without switching branches. Returns a structured per-step receipt and one-line summary. No remote mutation, force, stash, reset, checkout, or prune; retries are idempotent. Diagnostics are redacted and bounded to 4000 characters each.",
|
|
722
|
+
promptSnippet: "land_branch: post-merge linked-worktree cleanup, leased source retirement, and independent fast-forward target sync with verified receipts",
|
|
723
|
+
promptGuidelines: [
|
|
724
|
+
"Use land_branch after the pull request merged on the host; the source tip must be an ancestor of the fetched remote target (squash/rebase merges may not satisfy this).",
|
|
725
|
+
"land_branch is cwd-independent; run from the repository root, never inside the worktree being removed.",
|
|
726
|
+
"land_branch deletes ignored files such as .env, .pi/, node_modules/, and dist/ with the worktree and lists their top-level entries in deletedIgnoredPaths; preserve anything needed beforehand.",
|
|
727
|
+
"land_branch target sync never touches a dirty checkout; skipped-dirty reports its path and ahead/behind counts, independently of cleanup outcomes.",
|
|
728
|
+
"Call land_branch by itself and inspect each receipt outcome; do not batch it with other Git mutations. It never switches branches or mutates remote branches.",
|
|
729
|
+
],
|
|
730
|
+
parameters: LandBranchParametersSchema,
|
|
731
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
732
|
+
const details = await landBranch(pi, ctx, params, signal);
|
|
733
|
+
return {
|
|
734
|
+
content: [{ type: "text", text: formatLandBranch(details) }],
|
|
735
|
+
details,
|
|
736
|
+
};
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
|
|
672
740
|
pi.registerTool({
|
|
673
741
|
name: PUSH_BRANCH_TOOL_NAME,
|
|
674
742
|
label: "Push Branch",
|
|
@@ -767,11 +835,12 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
|
|
|
767
835
|
pi.registerTool({
|
|
768
836
|
name: CREATE_WORKTREE_TOOL_NAME,
|
|
769
837
|
label: "Create Worktree",
|
|
770
|
-
description: "create_worktree creates and verifies one linked worktree at an explicit absolute worktreePath for either a new local branch from current HEAD or an existing unoccupied local branch. create_worktree returns exact lossless handoff identity fields and never infers paths, remotes, base refs, detached or orphan modes, or force behavior.",
|
|
771
|
-
promptSnippet: "create_worktree: create and verify a linked worktree at an explicit absolute path with a ready handoff cwd",
|
|
838
|
+
description: "create_worktree creates and verifies one linked worktree at an explicit absolute worktreePath for either a new local branch (from current HEAD, or from an explicit read-only baseRef such as origin/main or a full commit) or an existing unoccupied local branch. create_worktree returns exact lossless handoff identity fields and never infers paths, remotes, base refs, detached or orphan modes, or force behavior.",
|
|
839
|
+
promptSnippet: "create_worktree: create and verify a linked worktree at an explicit absolute path, optionally starting its new branch from an explicit read-only baseRef, with a ready handoff cwd",
|
|
772
840
|
promptGuidelines: [
|
|
773
841
|
"Use create_worktree only when the user explicitly requests worktree creation and provides or approves the exact absolute worktreePath; create_worktree must never infer a filesystem path silently.",
|
|
774
|
-
"Use create_worktree with
|
|
842
|
+
"Use create_worktree with worktreePath, branchName, branchMode, and optionally baseRef for branchMode 'new'; create_worktree never accepts force, remote, detach, orphan, move, prune, repair, lock, or unlock parameters.",
|
|
843
|
+
"Use create_worktree with an explicit baseRef such as origin/main to start a new branch from a fetched remote baseline regardless of the current checkout's branch, dirt, or staleness; create_worktree only reads baseRef and never checks out or resets it.",
|
|
775
844
|
"Do not batch create_worktree with dependent worktree mutations; wait for create_worktree to complete and verify its ready exact handoff.cwd before passing that cwd to another agent or session.",
|
|
776
845
|
],
|
|
777
846
|
parameters: CreateWorktreeParametersSchema,
|
|
@@ -783,6 +852,7 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
|
|
|
783
852
|
params.branchName,
|
|
784
853
|
params.branchMode,
|
|
785
854
|
signal,
|
|
855
|
+
params.baseRef,
|
|
786
856
|
);
|
|
787
857
|
return {
|
|
788
858
|
content: [{ type: "text", text: formatCreateWorktree(details) }],
|
package/src/types.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface CreateWorktreeToolInput {
|
|
|
49
49
|
worktreePath: string;
|
|
50
50
|
branchName: string;
|
|
51
51
|
branchMode: CreateWorktreeMode;
|
|
52
|
+
/** Optional read-only start point for branchMode "new": local branch, remote-tracking ref, or full commit. */
|
|
53
|
+
baseRef?: string;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export interface RemoveWorktreeToolInput {
|
|
@@ -238,6 +240,16 @@ export interface FetchBranchDetails {
|
|
|
238
240
|
output: string;
|
|
239
241
|
}
|
|
240
242
|
|
|
243
|
+
export interface FetchRemoteBranchDetails {
|
|
244
|
+
repoRoot: string;
|
|
245
|
+
remote: string;
|
|
246
|
+
branch: string;
|
|
247
|
+
remoteRef: string;
|
|
248
|
+
remoteTrackingRef: string;
|
|
249
|
+
refspec: string;
|
|
250
|
+
output: string;
|
|
251
|
+
}
|
|
252
|
+
|
|
241
253
|
export interface PullBranchDetails {
|
|
242
254
|
repoRoot: string;
|
|
243
255
|
currentBranch: string;
|