@senad-d/branchme 0.2.2 → 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.
- package/CHANGELOG.md +15 -3
- package/README.md +93 -21
- package/SECURITY.md +34 -15
- package/docs/PROJECT_DEFINITION_BRIEF.md +34 -18
- package/docs/SMOKE_TEST.md +13 -4
- package/docs/STRUCTURE.md +22 -10
- package/docs/TUI_CAPTURE.md +41 -31
- package/package.json +3 -2
- package/src/commands/branchme-command.ts +13 -3
- package/src/constants.ts +15 -0
- package/src/git-integration.ts +27 -6
- package/src/git-landing.ts +501 -0
- package/src/git-retirement.ts +34 -11
- package/src/git-workflow.ts +136 -0
- package/src/git.ts +353 -20
- package/src/github.ts +115 -0
- package/src/tools/branchme-tools.ts +115 -9
- package/src/tools/workflow-tools.ts +86 -0
- package/src/types.ts +55 -0
- package/src/ui/branchme-panel.ts +3 -2
|
@@ -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,10 +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,
|
|
17
|
+
LAND_BRANCH_TOOL_NAME,
|
|
18
|
+
LIST_BRANCHES_TOOL_NAME,
|
|
14
19
|
LIST_WORKTREES_TOOL_NAME,
|
|
15
20
|
PULL_BRANCH_TOOL_NAME,
|
|
16
21
|
PULL_REQUEST_TOOL_NAME,
|
|
@@ -29,6 +34,8 @@ import {
|
|
|
29
34
|
getLocalBranchCommit,
|
|
30
35
|
getPullRequestCommitSubjects,
|
|
31
36
|
inferPullRequestBaseBranch,
|
|
37
|
+
initializeRepository,
|
|
38
|
+
listBranches,
|
|
32
39
|
listWorktrees,
|
|
33
40
|
localBranchExists,
|
|
34
41
|
pullCurrentBranch,
|
|
@@ -42,8 +49,9 @@ import {
|
|
|
42
49
|
import { collectGitContext, formatGitContext } from "../git-context.ts";
|
|
43
50
|
import { integrateBranch } from "../git-integration.ts";
|
|
44
51
|
import { retireBranch } from "../git-retirement.ts";
|
|
52
|
+
import { formatLandBranch, landBranch } from "../git-landing.ts";
|
|
45
53
|
import {
|
|
46
|
-
|
|
54
|
+
createOrReuseGitHubPullRequest,
|
|
47
55
|
ensureGitHubBranchExists,
|
|
48
56
|
redactSecrets,
|
|
49
57
|
repositoryLabel,
|
|
@@ -56,9 +64,9 @@ import type {
|
|
|
56
64
|
ChangeBranchDetails,
|
|
57
65
|
CreateWorktreeDetails,
|
|
58
66
|
GitContextDetails,
|
|
67
|
+
InitRepositoryDetails,
|
|
59
68
|
IntegrateBranchDetails,
|
|
60
69
|
ListWorktreesDetails,
|
|
61
|
-
PullRequestDetails,
|
|
62
70
|
PullRequestInput,
|
|
63
71
|
PullRequestInputField,
|
|
64
72
|
PullRequestToolDetails,
|
|
@@ -70,6 +78,16 @@ import type {
|
|
|
70
78
|
|
|
71
79
|
const EmptyParametersSchema = Type.Object({}, { additionalProperties: false });
|
|
72
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
|
+
|
|
73
91
|
const BranchStatusParametersSchema = Type.Object(
|
|
74
92
|
{
|
|
75
93
|
ancestry: Type.Optional(
|
|
@@ -106,6 +124,17 @@ const RetireBranchParametersSchema = Type.Object(
|
|
|
106
124
|
{ additionalProperties: false },
|
|
107
125
|
);
|
|
108
126
|
|
|
127
|
+
const LandBranchParametersSchema = Type.Object(
|
|
128
|
+
{
|
|
129
|
+
sourceBranch: Type.String({ minLength: 1, description: "Merged local feature branch to delete, or its name on an idempotent retry." }),
|
|
130
|
+
targetBranch: Type.String({ minLength: 1, description: "Local default branch to fast-forward after cleanup." }),
|
|
131
|
+
remote: Type.Optional(Type.String({ minLength: 1, description: "Configured remote name; defaults to origin." })),
|
|
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." })),
|
|
134
|
+
},
|
|
135
|
+
{ additionalProperties: false },
|
|
136
|
+
);
|
|
137
|
+
|
|
109
138
|
const CreateBranchParametersSchema = Type.Object(
|
|
110
139
|
{
|
|
111
140
|
branchName: Type.String({ minLength: 1, description: "Name of the new branch to create from current HEAD." }),
|
|
@@ -152,6 +181,9 @@ const FetchBranchParametersSchema = Type.Object(
|
|
|
152
181
|
const RemoveWorktreeParametersSchema = Type.Object(
|
|
153
182
|
{
|
|
154
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
|
+
})),
|
|
155
187
|
},
|
|
156
188
|
{ additionalProperties: false },
|
|
157
189
|
);
|
|
@@ -188,9 +220,9 @@ export function formatChangeBranch(details: ChangeBranchDetails): string {
|
|
|
188
220
|
return `Changed branch from ${previous} to ${details.currentBranch}.`;
|
|
189
221
|
}
|
|
190
222
|
|
|
191
|
-
export function formatPullRequest(details:
|
|
223
|
+
export function formatPullRequest(details: PullRequestToolDetails, autofilledFields: PullRequestInputField[] = []): string {
|
|
192
224
|
const autofill = autofilledFields.length > 0 ? ` Autofilled fields: ${autofilledFields.join(", ")}.` : "";
|
|
193
|
-
return
|
|
225
|
+
return `${details.outcome === "existing" ? "Reused existing" : "Created"} pull request #${details.number} (${details.state}) for ${repositoryLabel(details.repository)}: ${details.url}.${autofill}`;
|
|
194
226
|
}
|
|
195
227
|
|
|
196
228
|
const WORKTREE_FORMAT_PATH_LIMIT_CHARS = 512;
|
|
@@ -271,6 +303,12 @@ export function formatListWorktrees(details: ListWorktreesDetails): string {
|
|
|
271
303
|
return lines.join("\n");
|
|
272
304
|
}
|
|
273
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
|
+
|
|
274
312
|
export function formatCreateWorktree(details: CreateWorktreeDetails): string {
|
|
275
313
|
const cwd = safeWorktreeFormatValue(details.handoff.cwd, WORKTREE_FORMAT_PATH_LIMIT_CHARS);
|
|
276
314
|
const branch = safeWorktreeFormatValue(details.handoff.branch, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS);
|
|
@@ -506,6 +544,28 @@ async function requireGitHubHeadMatchesLocalBranch(
|
|
|
506
544
|
}
|
|
507
545
|
|
|
508
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
|
+
});
|
|
509
569
|
pi.registerTool({
|
|
510
570
|
name: BRANCH_STATUS_TOOL_NAME,
|
|
511
571
|
label: "Branch Status",
|
|
@@ -540,6 +600,27 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
|
|
|
540
600
|
},
|
|
541
601
|
});
|
|
542
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
|
+
|
|
543
624
|
pi.registerTool({
|
|
544
625
|
name: CREATE_BRANCH_TOOL_NAME,
|
|
545
626
|
label: "Create Branch",
|
|
@@ -703,6 +784,29 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
|
|
|
703
784
|
},
|
|
704
785
|
});
|
|
705
786
|
|
|
787
|
+
pi.registerTool({
|
|
788
|
+
name: LAND_BRANCH_TOOL_NAME,
|
|
789
|
+
label: "Land Branch",
|
|
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.",
|
|
791
|
+
promptSnippet: "land_branch: post-merge linked-worktree cleanup, leased source retirement, and independent fast-forward target sync with verified receipts",
|
|
792
|
+
promptGuidelines: [
|
|
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.",
|
|
795
|
+
"land_branch is cwd-independent; run from the repository root, never inside the worktree being removed.",
|
|
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.",
|
|
797
|
+
"land_branch target sync never touches a dirty checkout; skipped-dirty reports its path and ahead/behind counts, independently of cleanup outcomes.",
|
|
798
|
+
"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.",
|
|
799
|
+
],
|
|
800
|
+
parameters: LandBranchParametersSchema,
|
|
801
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
802
|
+
const details = await landBranch(pi, ctx, params, signal, options);
|
|
803
|
+
return {
|
|
804
|
+
content: [{ type: "text", text: formatLandBranch(details) }],
|
|
805
|
+
details,
|
|
806
|
+
};
|
|
807
|
+
},
|
|
808
|
+
});
|
|
809
|
+
|
|
706
810
|
pi.registerTool({
|
|
707
811
|
name: PUSH_BRANCH_TOOL_NAME,
|
|
708
812
|
label: "Push Branch",
|
|
@@ -738,6 +842,7 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
|
|
|
738
842
|
"Do not call push_branch and pull_request in the same tool batch; call pull_request only after push_branch has completed.",
|
|
739
843
|
"Use pull_request only for the resolved current repository; pull_request never accepts owner, repo, or owner-prefixed branch refs.",
|
|
740
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.",
|
|
741
846
|
],
|
|
742
847
|
parameters: PullRequestParametersSchema,
|
|
743
848
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -760,7 +865,7 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
|
|
|
760
865
|
fetchImpl: options.fetchImpl,
|
|
761
866
|
signal,
|
|
762
867
|
});
|
|
763
|
-
const details = await
|
|
868
|
+
const details = await createOrReuseGitHubPullRequest(repository, resolved.input, headBranch.commitSha, token, {
|
|
764
869
|
fetchImpl: options.fetchImpl,
|
|
765
870
|
signal,
|
|
766
871
|
});
|
|
@@ -830,16 +935,17 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
|
|
|
830
935
|
pi.registerTool({
|
|
831
936
|
name: REMOVE_WORKTREE_TOOL_NAME,
|
|
832
937
|
label: "Remove Worktree",
|
|
833
|
-
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.",
|
|
834
|
-
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",
|
|
835
940
|
promptGuidelines: [
|
|
836
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.",
|
|
837
|
-
"Use remove_worktree with
|
|
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.",
|
|
838
944
|
"Do not batch remove_worktree with dependent worktree mutations; wait for remove_worktree to complete and verify its non-ready handoff before continuing.",
|
|
839
945
|
],
|
|
840
946
|
parameters: RemoveWorktreeParametersSchema,
|
|
841
947
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
842
|
-
const details = await removeWorktree(pi, ctx, params.worktreePath, signal);
|
|
948
|
+
const details = await removeWorktree(pi, ctx, params.worktreePath, signal, params.deleteIgnored === true);
|
|
843
949
|
return {
|
|
844
950
|
content: [{ type: "text", text: formatRemoveWorktree(details) }],
|
|
845
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
|
|
package/src/ui/branchme-panel.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
];
|