@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.
- package/CHANGELOG.md +11 -3
- package/README.md +62 -24
- package/SECURITY.md +27 -17
- package/docs/PROJECT_DEFINITION_BRIEF.md +30 -19
- package/docs/SMOKE_TEST.md +13 -4
- package/docs/STRUCTURE.md +20 -12
- package/docs/TUI_CAPTURE.md +41 -31
- package/package.json +3 -2
- package/src/commands/branchme-command.ts +13 -3
- package/src/constants.ts +13 -0
- package/src/git-integration.ts +27 -6
- package/src/git-landing.ts +63 -11
- package/src/git-workflow.ts +136 -0
- package/src/git.ts +279 -3
- package/src/github.ts +115 -0
- package/src/tools/branchme-tools.ts +84 -12
- 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,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
|
-
|
|
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:
|
|
223
|
+
export function formatPullRequest(details: PullRequestToolDetails, autofilledFields: PullRequestInputField[] = []): string {
|
|
204
224
|
const autofill = autofilledFields.length > 0 ? ` Autofilled fields: ${autofilledFields.join(", ")}.` : "";
|
|
205
|
-
return
|
|
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;
|
|
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
|
|
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
|
|
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
|
|
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
|
];
|