@senad-d/branchme 0.3.0 → 0.3.1

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.
@@ -1,4 +1,5 @@
1
1
  import { StringEnum } from "@earendil-works/pi-ai";
2
+ import { registerWorkflowTools } from "./workflow-tools.ts";
2
3
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
4
  import { Type } from "typebox";
4
5
  import {
@@ -7,11 +8,14 @@ import {
7
8
  CREATE_BRANCH_TOOL_NAME,
8
9
  CREATE_WORKTREE_TOOL_NAME,
9
10
  FETCH_BRANCH_TOOL_NAME,
11
+ GIT_BRANCH_SUMMARY_LIMIT_CHARS,
10
12
  GIT_INTEGRATION_SUMMARY_LIMIT_CHARS,
11
13
  GIT_RETIREMENT_SUMMARY_LIMIT_CHARS,
12
14
  GIT_WORKTREE_SUMMARY_LIMIT_CHARS,
15
+ INIT_REPOSITORY_TOOL_NAME,
13
16
  INTEGRATE_BRANCH_TOOL_NAME,
14
17
  LAND_BRANCH_TOOL_NAME,
18
+ LIST_BRANCHES_TOOL_NAME,
15
19
  LIST_WORKTREES_TOOL_NAME,
16
20
  PULL_BRANCH_TOOL_NAME,
17
21
  PULL_REQUEST_TOOL_NAME,
@@ -30,6 +34,8 @@ import {
30
34
  getLocalBranchCommit,
31
35
  getPullRequestCommitSubjects,
32
36
  inferPullRequestBaseBranch,
37
+ initializeRepository,
38
+ listBranches,
33
39
  listWorktrees,
34
40
  localBranchExists,
35
41
  pullCurrentBranch,
@@ -45,7 +51,7 @@ import { integrateBranch } from "../git-integration.ts";
45
51
  import { retireBranch } from "../git-retirement.ts";
46
52
  import { formatLandBranch, landBranch } from "../git-landing.ts";
47
53
  import {
48
- createGitHubPullRequest,
54
+ createOrReuseGitHubPullRequest,
49
55
  ensureGitHubBranchExists,
50
56
  redactSecrets,
51
57
  repositoryLabel,
@@ -58,9 +64,9 @@ import type {
58
64
  ChangeBranchDetails,
59
65
  CreateWorktreeDetails,
60
66
  GitContextDetails,
67
+ InitRepositoryDetails,
61
68
  IntegrateBranchDetails,
62
69
  ListWorktreesDetails,
63
- PullRequestDetails,
64
70
  PullRequestInput,
65
71
  PullRequestInputField,
66
72
  PullRequestToolDetails,
@@ -72,6 +78,16 @@ import type {
72
78
 
73
79
  const EmptyParametersSchema = Type.Object({}, { additionalProperties: false });
74
80
 
81
+ const InitRepositoryParametersSchema = Type.Object(
82
+ {
83
+ initialBranch: Type.Optional(Type.String({
84
+ minLength: 1,
85
+ description: "Initial unborn branch name; defaults to main.",
86
+ })),
87
+ },
88
+ { additionalProperties: false },
89
+ );
90
+
75
91
  const BranchStatusParametersSchema = Type.Object(
76
92
  {
77
93
  ancestry: Type.Optional(
@@ -114,6 +130,7 @@ const LandBranchParametersSchema = Type.Object(
114
130
  targetBranch: Type.String({ minLength: 1, description: "Local default branch to fast-forward after cleanup." }),
115
131
  remote: Type.Optional(Type.String({ minLength: 1, description: "Configured remote name; defaults to origin." })),
116
132
  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." })),
133
+ pullRequestNumber: Type.Optional(Type.Integer({ minimum: 1, description: "Merged GitHub PR number for exact host-merge evidence, required for squash/rebase merges without source ancestry." })),
117
134
  },
118
135
  { additionalProperties: false },
119
136
  );
@@ -164,6 +181,9 @@ const FetchBranchParametersSchema = Type.Object(
164
181
  const RemoveWorktreeParametersSchema = Type.Object(
165
182
  {
166
183
  worktreePath: Type.String({ minLength: 1, description: "Explicit absolute path of the linked worktree to remove." }),
184
+ deleteIgnored: Type.Optional(Type.Boolean({
185
+ description: "Explicit authorization to delete ignored files and directories with the worktree; defaults to false.",
186
+ })),
167
187
  },
168
188
  { additionalProperties: false },
169
189
  );
@@ -200,9 +220,9 @@ export function formatChangeBranch(details: ChangeBranchDetails): string {
200
220
  return `Changed branch from ${previous} to ${details.currentBranch}.`;
201
221
  }
202
222
 
203
- export function formatPullRequest(details: PullRequestDetails, autofilledFields: PullRequestInputField[] = []): string {
223
+ export function formatPullRequest(details: PullRequestToolDetails, autofilledFields: PullRequestInputField[] = []): string {
204
224
  const autofill = autofilledFields.length > 0 ? ` Autofilled fields: ${autofilledFields.join(", ")}.` : "";
205
- return `Created pull request #${details.number} (${details.state}) for ${repositoryLabel(details.repository)}: ${details.url}.${autofill}`;
225
+ return `${details.outcome === "existing" ? "Reused existing" : "Created"} pull request #${details.number} (${details.state}) for ${repositoryLabel(details.repository)}: ${details.url}.${autofill}`;
206
226
  }
207
227
 
208
228
  const WORKTREE_FORMAT_PATH_LIMIT_CHARS = 512;
@@ -283,6 +303,12 @@ export function formatListWorktrees(details: ListWorktreesDetails): string {
283
303
  return lines.join("\n");
284
304
  }
285
305
 
306
+ export function formatInitRepository(details: InitRepositoryDetails): string {
307
+ const root = safeWorktreeFormatValue(details.repoRoot, WORKTREE_FORMAT_PATH_LIMIT_CHARS);
308
+ const branch = safeWorktreeFormatValue(details.initialBranch, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS);
309
+ return `Initialized non-bare Git repository at ${root} with unborn initial branch ${branch}. No files were staged or committed.`;
310
+ }
311
+
286
312
  export function formatCreateWorktree(details: CreateWorktreeDetails): string {
287
313
  const cwd = safeWorktreeFormatValue(details.handoff.cwd, WORKTREE_FORMAT_PATH_LIMIT_CHARS);
288
314
  const branch = safeWorktreeFormatValue(details.handoff.branch, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS);
@@ -518,6 +544,28 @@ async function requireGitHubHeadMatchesLocalBranch(
518
544
  }
519
545
 
520
546
  export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "exec">, options: BranchMeToolOptions = {}): void {
547
+ registerWorkflowTools(pi, options);
548
+ pi.registerTool({
549
+ name: LIST_BRANCHES_TOOL_NAME,
550
+ label: "List Branches",
551
+ description: "list_branches reads up to 200 local and remote-tracking branches, including commits, upstream counts, symbolic refs, and worktree occupancy. Read-only; cached remote refs are not fetched. Text is bounded to 4000 characters.",
552
+ promptSnippet: "list_branches: discover local and cached remote-tracking branches without mutation",
553
+ promptGuidelines: [
554
+ "Use list_branches to discover names and worktree occupancy before branch operations; it never fetches or mutates Git state.",
555
+ "Treat list_branches names and paths as display metadata; redacted or truncated values are not executable identities.",
556
+ ],
557
+ parameters: EmptyParametersSchema,
558
+ async execute(_toolCallId, _params, signal, _onUpdate, ctx) {
559
+ const details = await listBranches(pi, ctx, signal);
560
+ const lines = details.branches.map((branch) =>
561
+ `${branch.current ? "*" : "-"} ${JSON.stringify(branch.name)} (${branch.kind}) ${shortCommit(branch.head)}; upstream ${JSON.stringify(branch.upstream)}; ahead ${branch.ahead ?? "?"}, behind ${branch.behind ?? "?"}; worktrees ${JSON.stringify(branch.worktreePaths)}`);
562
+ const text = [`Branches: ${details.branches.length}; omitted: ${details.omitted}.`, ...lines].join("\n");
563
+ return {
564
+ content: [{ type: "text", text: text.length <= GIT_BRANCH_SUMMARY_LIMIT_CHARS ? text : `${text.slice(0, GIT_BRANCH_SUMMARY_LIMIT_CHARS - 20)}\n[summary truncated]` }],
565
+ details,
566
+ };
567
+ },
568
+ });
521
569
  pi.registerTool({
522
570
  name: BRANCH_STATUS_TOOL_NAME,
523
571
  label: "Branch Status",
@@ -552,6 +600,27 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
552
600
  },
553
601
  });
554
602
 
603
+ pi.registerTool({
604
+ name: INIT_REPOSITORY_TOOL_NAME,
605
+ label: "Initialize Repository",
606
+ description: "init_repository initializes the exact current working directory as a new non-bare Git repository with an unborn initial branch (default main). It rejects an existing .git entry, any directory already inside a Git repository, nested initialization, paths, bare/separate-Git-dir modes, templates, shared modes, staging, commits, remotes, and file creation outside Git metadata.",
607
+ promptSnippet: "init_repository: initialize the exact current working directory as a verified new non-bare Git repository",
608
+ promptGuidelines: [
609
+ "Use init_repository only when the user explicitly asks to initialize the current working directory as a new Git repository.",
610
+ "Use init_repository with optional initialBranch; it defaults to main and never accepts or infers a path, bare mode, separate Git directory, template, shared mode, remote, initial commit, README, or gitignore content.",
611
+ "init_repository rejects reinitialization and nested repositories; it creates only Git metadata and an unborn branch, then verifies the exact repository root and Git directory.",
612
+ "Call init_repository by itself and wait for verification before using repository-dependent Git tools.",
613
+ ],
614
+ parameters: InitRepositoryParametersSchema,
615
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
616
+ const details = await initializeRepository(pi, ctx, params.initialBranch ?? "main", signal);
617
+ return {
618
+ content: [{ type: "text", text: formatInitRepository(details) }],
619
+ details,
620
+ };
621
+ },
622
+ });
623
+
555
624
  pi.registerTool({
556
625
  name: CREATE_BRANCH_TOOL_NAME,
557
626
  label: "Create Branch",
@@ -718,10 +787,11 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
718
787
  pi.registerTool({
719
788
  name: LAND_BRANCH_TOOL_NAME,
720
789
  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.",
790
+ description: "land_branch performs deterministic post-merge cleanup: fetch the remote target, prove source ancestry or verify exact merged GitHub PR evidence via pullRequestNumber, 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
791
  promptSnippet: "land_branch: post-merge linked-worktree cleanup, leased source retirement, and independent fast-forward target sync with verified receipts",
723
792
  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).",
793
+ "Use land_branch after the pull request merged on the host; supply pullRequestNumber for squash/rebase merges. It verifies exact merged PR head/base identities and containment of the merge commit in the fetched remote target.",
794
+ "Without pullRequestNumber, land_branch still requires source ancestry. PR evidence permits deletion of original source history after a squash/rebase merge; inspect the mergeProof receipt.",
725
795
  "land_branch is cwd-independent; run from the repository root, never inside the worktree being removed.",
726
796
  "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
797
  "land_branch target sync never touches a dirty checkout; skipped-dirty reports its path and ahead/behind counts, independently of cleanup outcomes.",
@@ -729,7 +799,7 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
729
799
  ],
730
800
  parameters: LandBranchParametersSchema,
731
801
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
732
- const details = await landBranch(pi, ctx, params, signal);
802
+ const details = await landBranch(pi, ctx, params, signal, options);
733
803
  return {
734
804
  content: [{ type: "text", text: formatLandBranch(details) }],
735
805
  details,
@@ -772,6 +842,7 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
772
842
  "Do not call push_branch and pull_request in the same tool batch; call pull_request only after push_branch has completed.",
773
843
  "Use pull_request only for the resolved current repository; pull_request never accepts owner, repo, or owner-prefixed branch refs.",
774
844
  "Use pull_request with GITHUB_TOKEN or GH_TOKEN from the process environment or local .env fallback; pull_request must not expose token values.",
845
+ "pull_request reuses an existing open PR only when its exact head commit and base match; it never updates existing title, body, or draft state.",
775
846
  ],
776
847
  parameters: PullRequestParametersSchema,
777
848
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
@@ -794,7 +865,7 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
794
865
  fetchImpl: options.fetchImpl,
795
866
  signal,
796
867
  });
797
- const details = await createGitHubPullRequest(repository, resolved.input, token, {
868
+ const details = await createOrReuseGitHubPullRequest(repository, resolved.input, headBranch.commitSha, token, {
798
869
  fetchImpl: options.fetchImpl,
799
870
  signal,
800
871
  });
@@ -864,16 +935,17 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
864
935
  pi.registerTool({
865
936
  name: REMOVE_WORKTREE_TOOL_NAME,
866
937
  label: "Remove Worktree",
867
- description: "remove_worktree force-free removes one verified clean linked worktree at an explicit absolute worktreePath while retaining and returning its exact local branch identity. remove_worktree rejects main, current, dirty, detached, locked, prunable, missing, and foreign worktrees and never deletes branches.",
868
- promptSnippet: "remove_worktree: force-free removal of an explicitly selected clean linked worktree while retaining its branch",
938
+ description: "remove_worktree force-free removes one verified clean linked worktree at an explicit absolute worktreePath while retaining and returning its exact local branch identity. Ignored residue is refused by default; deleteIgnored: true explicitly authorizes deleting it with the worktree and reports its top-level paths. remove_worktree rejects main, current, dirty, detached, locked, prunable, missing, and foreign worktrees and never deletes branches.",
939
+ promptSnippet: "remove_worktree: force-free removal of an explicitly selected clean linked worktree, with optional explicit ignored-residue deletion, while retaining its branch",
869
940
  promptGuidelines: [
870
941
  "Use remove_worktree only when the user explicitly requests worktree removal and provides or approves the exact absolute worktreePath; remove_worktree must never infer a filesystem path silently.",
871
- "Use remove_worktree with exactly worktreePath; remove_worktree never accepts force, move, prune, repair, lock, unlock, branch deletion, remote, or refspec parameters.",
942
+ "Use remove_worktree with worktreePath and optional deleteIgnored; set deleteIgnored: true only after the user explicitly authorizes deleting all ignored files and directories, including possible .env, .pi/, dependency, and build residue.",
943
+ "remove_worktree never accepts force, move, prune, repair, lock, unlock, branch deletion, remote, or refspec parameters.",
872
944
  "Do not batch remove_worktree with dependent worktree mutations; wait for remove_worktree to complete and verify its non-ready handoff before continuing.",
873
945
  ],
874
946
  parameters: RemoveWorktreeParametersSchema,
875
947
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
876
- const details = await removeWorktree(pi, ctx, params.worktreePath, signal);
948
+ const details = await removeWorktree(pi, ctx, params.worktreePath, signal, params.deleteIgnored === true);
877
949
  return {
878
950
  content: [{ type: "text", text: formatRemoveWorktree(details) }],
879
951
  details,
@@ -0,0 +1,86 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { PULL_REQUEST_STATUS_TOOL_NAME, TRACK_BRANCH_TOOL_NAME, UPDATE_FROM_BASE_TOOL_NAME } from "../constants.ts";
4
+ import { getGitRoot, requireCurrentBranch } from "../git.ts";
5
+ import { findGitHubPullRequest, getGitHubPullRequest, resolveGitHubRepository, resolveGitHubToken } from "../github.ts";
6
+ import type { PullRequestStatusDetails } from "../types.ts";
7
+ import type { BranchMeToolOptions } from "./branchme-tools.ts";
8
+ import { trackBranch, updateFromBase } from "../git-workflow.ts";
9
+
10
+ function formatPullRequestStatus(pullRequest: PullRequestStatusDetails | null): string {
11
+ if (!pullRequest) return "No matching pull request found.";
12
+ const state = pullRequest.merged ? "merged" : pullRequest.state;
13
+ const draft = pullRequest.draft ? " (draft)" : "";
14
+ return `PR #${pullRequest.number}: ${state}${draft}; ${pullRequest.head} -> ${pullRequest.base}. ${pullRequest.url}`;
15
+ }
16
+
17
+ export function registerWorkflowTools(pi: Pick<ExtensionAPI, "registerTool" | "exec">, options: BranchMeToolOptions = {}): void {
18
+ pi.registerTool({
19
+ name: PULL_REQUEST_STATUS_TOOL_NAME,
20
+ label: "Pull Request Status",
21
+ description: "pull_request_status reads one same-repository GitHub PR by number, or the most recently updated PR for a head branch (current branch by default). Reports open/closed/merged state and exact head/base/merge identities. No mutations; not a CI-check or review-approval verdict.",
22
+ promptSnippet: "pull_request_status: inspect open, closed, or merged PR state and immutable commit identities",
23
+ promptGuidelines: [
24
+ "Use pull_request_status with number for exact lifecycle verification; number and headBranch are mutually exclusive.",
25
+ "Without number, pull_request_status returns the most recently updated PR for headBranch or the current branch. Prefer its exact number for subsequent checks.",
26
+ "pull_request_status never merges, edits, closes, or deletes a PR and does not claim CI checks or review requirements passed.",
27
+ ],
28
+ parameters: Type.Object({
29
+ number: Type.Optional(Type.Integer({ minimum: 1, description: "Exact PR number; mutually exclusive with headBranch." })),
30
+ headBranch: Type.Optional(Type.String({ minLength: 1, description: "Head branch to look up; defaults to current branch when number is omitted." })),
31
+ }, { additionalProperties: false }),
32
+ async execute(_id, params, signal, _update, ctx) {
33
+ if (params.number !== undefined && params.headBranch !== undefined) throw new Error("Supply number or headBranch, not both.");
34
+ const rootCtx = { cwd: await getGitRoot(pi, ctx, signal) };
35
+ const repository = await resolveGitHubRepository(pi, rootCtx, signal, options.env);
36
+ const token = (await resolveGitHubToken(options.env, { cwd: rootCtx.cwd, signal })).token;
37
+ const requestOptions = { fetchImpl: options.fetchImpl, signal };
38
+ const pullRequest = params.number === undefined
39
+ ? await findGitHubPullRequest(repository, params.headBranch ?? await requireCurrentBranch(pi, rootCtx, signal), token, requestOptions)
40
+ : await getGitHubPullRequest(repository, params.number, token, requestOptions);
41
+ return {
42
+ content: [{ type: "text", text: formatPullRequestStatus(pullRequest) }],
43
+ details: { action: "pull_request_status", pullRequest },
44
+ };
45
+ },
46
+ });
47
+ pi.registerTool({
48
+ name: UPDATE_FROM_BASE_TOOL_NAME,
49
+ label: "Update From Base",
50
+ description: "update_from_base fetches one remote base and merges its captured commit into the clean current feature branch with verified no-op, fast-forward, merge-commit, or automatically aborted conflict results. Never rebases, pushes, stashes, or changes upstream configuration.",
51
+ promptSnippet: "update_from_base: merge a fresh remote base into the current feature without rewriting published history",
52
+ promptGuidelines: [
53
+ "Use update_from_base only when asked to update the current feature branch from an explicit baseBranch; remote defaults to origin.",
54
+ "Run update_from_base by itself. It fetches the base and uses the fixed normal-merge policy; conflicts are automatically aborted with restoration verified.",
55
+ "update_from_base preserves published history and upstream configuration; committing conflict-resolution changes belongs to a separate workflow.",
56
+ ],
57
+ parameters: Type.Object({
58
+ baseBranch: Type.String({ minLength: 1, description: "Exact base branch on the remote, for example main." }),
59
+ remote: Type.Optional(Type.String({ minLength: 1, description: "Configured remote; defaults to origin." })),
60
+ }, { additionalProperties: false }),
61
+ async execute(_id, params, signal, _update, ctx) {
62
+ const details = await updateFromBase(pi, ctx, params, signal);
63
+ return { content: [{ type: "text", text: `update_from_base: ${details.status}. ${details.status === "conflict" ? "Merge aborted and restoration verified; inspect conflict paths in details." : "Base integration verified without rewriting published history."}` }], details };
64
+ },
65
+ });
66
+ pi.registerTool({
67
+ name: TRACK_BRANCH_TOOL_NAME,
68
+ label: "Track Branch",
69
+ description: "track_branch fetches one exact remote branch, creates and checks out a new local tracking branch, and verifies HEAD/upstream. Requires a clean idle checkout. Never forces, stashes, commits, or pushes.",
70
+ promptSnippet: "track_branch: join an existing remote branch with a verified local tracking checkout",
71
+ promptGuidelines: [
72
+ "Use track_branch only when explicitly asked to create and check out a local branch tracking an existing remote branch.",
73
+ "Run track_branch by itself; it fetches before checkout. Supply branchName and optionally remote (default origin) and remoteBranch (default branchName).",
74
+ "track_branch rejects existing local branches and dirty or in-progress worktrees; it never stashes or discards changes.",
75
+ ],
76
+ parameters: Type.Object({
77
+ branchName: Type.String({ minLength: 1, description: "New local branch name." }),
78
+ remote: Type.Optional(Type.String({ minLength: 1, description: "Configured remote; defaults to origin." })),
79
+ remoteBranch: Type.Optional(Type.String({ minLength: 1, description: "Existing branch on that remote; defaults to branchName." })),
80
+ }, { additionalProperties: false }),
81
+ async execute(_id, params, signal, _update, ctx) {
82
+ const details = await trackBranch(pi, ctx, params, signal);
83
+ return { content: [{ type: "text", text: `Checked out ${details.branchName} tracking ${details.upstream} at ${details.head}.` }], details };
84
+ },
85
+ });
86
+ }
package/src/types.ts CHANGED
@@ -29,6 +29,42 @@ export interface BranchStatusToolInput {
29
29
  ancestry?: BranchStatusAncestryQuery;
30
30
  }
31
31
 
32
+ export type ListBranchesToolInput = Record<string, never>;
33
+
34
+ export interface BranchEntry {
35
+ name: string;
36
+ fullRef: string;
37
+ kind: "local" | "remote-tracking";
38
+ head: string;
39
+ current: boolean;
40
+ upstream: string | null;
41
+ ahead: number | null;
42
+ behind: number | null;
43
+ symbolicTarget: string | null;
44
+ worktreePaths: string[];
45
+ }
46
+
47
+ export interface ListBranchesDetails {
48
+ action: "list_branches";
49
+ repoRoot: string;
50
+ branches: BranchEntry[];
51
+ omitted: number;
52
+ }
53
+
54
+ export interface InitRepositoryToolInput {
55
+ initialBranch?: string;
56
+ }
57
+
58
+ export interface InitRepositoryDetails {
59
+ action: "init_repository";
60
+ request: InitRepositoryToolInput;
61
+ repoRoot: string;
62
+ gitDirectory: string;
63
+ initialBranch: string;
64
+ bare: false;
65
+ unborn: true;
66
+ }
67
+
32
68
  export interface IntegrateBranchToolInput {
33
69
  sourceBranch: string;
34
70
  targetBranch: string;
@@ -55,6 +91,8 @@ export interface CreateWorktreeToolInput {
55
91
 
56
92
  export interface RemoveWorktreeToolInput {
57
93
  worktreePath: string;
94
+ /** Explicitly authorizes deleting ignored files and directories with the worktree. */
95
+ deleteIgnored?: boolean;
58
96
  }
59
97
 
60
98
  export interface WorktreeEntry {
@@ -137,6 +175,7 @@ export interface CreateWorktreeDetails {
137
175
 
138
176
  export interface RemoveWorktreeDetails {
139
177
  action: "remove_worktree";
178
+ deletedIgnoredPaths: string[];
140
179
  repoRoot: string;
141
180
  request: RemoveWorktreeToolInput;
142
181
  verified: {
@@ -452,6 +491,21 @@ export interface PullRequestDetails {
452
491
  draft: boolean;
453
492
  }
454
493
 
494
+ export interface PullRequestStatusInput {
495
+ number?: number;
496
+ headBranch?: string;
497
+ }
498
+
499
+ export interface PullRequestStatusDetails extends PullRequestDetails {
500
+ title: string;
501
+ state: "open" | "closed";
502
+ merged: boolean;
503
+ mergedAt: string | null;
504
+ headSha: string;
505
+ baseSha: string;
506
+ mergeCommitSha: string | null;
507
+ }
508
+
455
509
  export interface PullRequestInput {
456
510
  headBranch: string;
457
511
  baseBranch: string;
@@ -465,6 +519,7 @@ export type PullRequestInputField = keyof PullRequestInput;
465
519
  export type PullRequestToolInput = Partial<PullRequestInput>;
466
520
 
467
521
  export interface PullRequestToolDetails extends PullRequestDetails {
522
+ outcome?: "created" | "existing";
468
523
  autofilledFields?: PullRequestInputField[];
469
524
  }
470
525
 
@@ -179,7 +179,7 @@ function sectionFooter(section: BranchMePanelSection, data: BranchMePanelData):
179
179
 
180
180
  switch (section) {
181
181
  case "status":
182
- return "status • current repository only • tools perform actions";
182
+ return "status • init or current repository • tools perform actions";
183
183
  case "workflow":
184
184
  return "workflow • inspect → change → fetch/pull/rebase → create → push → PR";
185
185
  case "integration":
@@ -196,6 +196,7 @@ function sectionRows(section: BranchMePanelSection, data: BranchMePanelData): Pa
196
196
  case "status":
197
197
  return [
198
198
  heading("STATUS"),
199
+ workflowDetailRow("init_repository", "new current directory"),
199
200
  statusDetailRow("Current branch:", values.branch),
200
201
  statusDetailRow("GitHub repository:", values.repository),
201
202
  statusDetailRow("GitHub token:", values.token),
@@ -258,7 +259,7 @@ function renderNarrow(data: BranchMePanelData, width: number, selectedSection: B
258
259
  const counter = `${sectionIndex(section) + 1}/${PANEL_SECTIONS.length}`;
259
260
  const lines = [
260
261
  titleBorder(width, sectionLabel(section), theme),
261
- framedLine("current repo only • informational", width, theme),
262
+ framedLine("init or current repo • informational", width, theme),
262
263
  framedLine(layoutCell(" ↑↓ section • q quit • /branchme help"), width, theme),
263
264
  horizontal(width, "├", "─", "┤", theme),
264
265
  ];