@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.
@@ -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 source branch to verify." }),
78
- targetBranch: Type.String({ minLength: 1, description: "Exact existing local target branch to verify." }),
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
- return `Created linked worktree ${cwd} for ${mode} ${branch}. Verified its canonical path, local branch, HEAD ${shortCommit(details.handoff.head)}, clean working tree, and ready handoff.`;
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 local branch commit is an ancestor of another. branch_status is read-only and never mutates files, Git state, or GitHub state.",
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 the current branch's configured upstream branch into its remote-tracking ref with an explicit git fetch --no-tags --no-recurse-submodules refspec. fetch_branch does not change local branches or working-tree files and never prunes, rebases, merges, stashes, stages, commits, or pushes.",
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 only when the user explicitly wants to fetch the configured upstream branch for the current branch.",
571
- "Use fetch_branch only on a current branch with an upstream; fetch_branch has no branchName, remote, tags, prune, force, or refspec parameters.",
572
- "Call fetch_branch and wait for it to complete before rebase_branch when the user wants the latest upstream state; do not batch fetch_branch with 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: EmptyParametersSchema,
575
- async execute(_toolCallId, _params, signal, _onUpdate, ctx) {
576
- const details = await fetchCurrentBranch(pi, ctx, signal);
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 configured upstream remote ${details.remote} for current branch ${details.currentBranch}.` }],
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 exactly worktreePath, branchName, and branchMode; create_worktree never accepts force, baseRef, remote, detach, orphan, move, prune, repair, lock, or unlock parameters.",
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;