@senad-d/branchme 0.1.7 → 0.1.9
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 +10 -5
- package/README.md +116 -18
- package/SECURITY.md +34 -6
- package/docs/PROJECT_DEFINITION_BRIEF.md +129 -77
- package/docs/SMOKE_TEST.md +24 -12
- package/docs/STRUCTURE.md +30 -20
- package/docs/TUI_CAPTURE.md +109 -28
- package/package.json +9 -5
- package/src/commands/branchme-command.ts +16 -2
- package/src/constants.ts +19 -0
- package/src/git-context.ts +81 -16
- package/src/git-integration.ts +673 -0
- package/src/git.ts +1175 -2
- package/src/tools/branchme-tools.ts +296 -4
- package/src/types.ts +226 -0
- package/src/ui/branchme-panel.ts +27 -3
|
@@ -1,32 +1,43 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
1
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { Type } from "typebox";
|
|
3
4
|
import {
|
|
4
5
|
BRANCH_STATUS_TOOL_NAME,
|
|
5
6
|
CHANGE_BRANCH_TOOL_NAME,
|
|
6
7
|
CREATE_BRANCH_TOOL_NAME,
|
|
8
|
+
CREATE_WORKTREE_TOOL_NAME,
|
|
7
9
|
FETCH_BRANCH_TOOL_NAME,
|
|
10
|
+
GIT_INTEGRATION_SUMMARY_LIMIT_CHARS,
|
|
11
|
+
GIT_WORKTREE_SUMMARY_LIMIT_CHARS,
|
|
12
|
+
INTEGRATE_BRANCH_TOOL_NAME,
|
|
13
|
+
LIST_WORKTREES_TOOL_NAME,
|
|
8
14
|
PULL_BRANCH_TOOL_NAME,
|
|
9
15
|
PULL_REQUEST_TOOL_NAME,
|
|
10
16
|
PUSH_BRANCH_TOOL_NAME,
|
|
11
17
|
REBASE_BRANCH_TOOL_NAME,
|
|
18
|
+
REMOVE_WORKTREE_TOOL_NAME,
|
|
12
19
|
} from "../constants.ts";
|
|
13
20
|
import {
|
|
14
21
|
changeExistingLocalBranch,
|
|
15
22
|
createLocalBranch,
|
|
23
|
+
createWorktree,
|
|
16
24
|
fetchCurrentBranch,
|
|
17
25
|
getGitRoot,
|
|
18
26
|
getLocalBranchCommit,
|
|
19
27
|
getPullRequestCommitSubjects,
|
|
20
28
|
inferPullRequestBaseBranch,
|
|
29
|
+
listWorktrees,
|
|
21
30
|
localBranchExists,
|
|
22
31
|
pullCurrentBranch,
|
|
23
32
|
pushCurrentBranch,
|
|
24
33
|
rebaseCurrentBranch,
|
|
34
|
+
removeWorktree,
|
|
25
35
|
requireCurrentBranch,
|
|
26
36
|
validateBranchName,
|
|
27
37
|
withRepositoryMutationQueue,
|
|
28
38
|
} from "../git.ts";
|
|
29
39
|
import { collectGitContext, formatGitContext } from "../git-context.ts";
|
|
40
|
+
import { integrateBranch } from "../git-integration.ts";
|
|
30
41
|
import {
|
|
31
42
|
createGitHubPullRequest,
|
|
32
43
|
ensureGitHubBranchExists,
|
|
@@ -39,16 +50,44 @@ import {
|
|
|
39
50
|
} from "../github.ts";
|
|
40
51
|
import type {
|
|
41
52
|
ChangeBranchDetails,
|
|
53
|
+
CreateWorktreeDetails,
|
|
42
54
|
GitContextDetails,
|
|
55
|
+
IntegrateBranchDetails,
|
|
56
|
+
ListWorktreesDetails,
|
|
43
57
|
PullRequestDetails,
|
|
44
58
|
PullRequestInput,
|
|
45
59
|
PullRequestInputField,
|
|
46
60
|
PullRequestToolDetails,
|
|
47
61
|
PullRequestToolInput,
|
|
62
|
+
RemoveWorktreeDetails,
|
|
63
|
+
WorktreeEntry,
|
|
48
64
|
} from "../types.ts";
|
|
49
65
|
|
|
50
66
|
const EmptyParametersSchema = Type.Object({}, { additionalProperties: false });
|
|
51
67
|
|
|
68
|
+
const BranchStatusParametersSchema = Type.Object(
|
|
69
|
+
{
|
|
70
|
+
ancestry: Type.Optional(
|
|
71
|
+
Type.Object(
|
|
72
|
+
{
|
|
73
|
+
sourceBranch: Type.String({ minLength: 1, description: "Exact existing local source branch to verify." }),
|
|
74
|
+
targetBranch: Type.String({ minLength: 1, description: "Exact existing local target branch to verify." }),
|
|
75
|
+
},
|
|
76
|
+
{ additionalProperties: false },
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
},
|
|
80
|
+
{ additionalProperties: false },
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const IntegrateBranchParametersSchema = Type.Object(
|
|
84
|
+
{
|
|
85
|
+
sourceBranch: Type.String({ minLength: 1, description: "Exact existing local source branch to integrate." }),
|
|
86
|
+
targetBranch: Type.String({ minLength: 1, description: "Exact existing local target branch already checked out in the clean control worktree." }),
|
|
87
|
+
},
|
|
88
|
+
{ additionalProperties: false },
|
|
89
|
+
);
|
|
90
|
+
|
|
52
91
|
const CreateBranchParametersSchema = Type.Object(
|
|
53
92
|
{
|
|
54
93
|
branchName: Type.String({ minLength: 1, description: "Name of the new branch to create from current HEAD." }),
|
|
@@ -63,6 +102,24 @@ const ChangeBranchParametersSchema = Type.Object(
|
|
|
63
102
|
{ additionalProperties: false },
|
|
64
103
|
);
|
|
65
104
|
|
|
105
|
+
const CreateWorktreeParametersSchema = Type.Object(
|
|
106
|
+
{
|
|
107
|
+
worktreePath: Type.String({ minLength: 1, description: "Explicit absolute destination path for the linked worktree." }),
|
|
108
|
+
branchName: Type.String({ minLength: 1, description: "New or existing local branch name for the linked worktree." }),
|
|
109
|
+
branchMode: StringEnum(["new", "existing"] as const, {
|
|
110
|
+
description: "Whether create_worktree creates a new local branch from current HEAD or uses an existing local branch.",
|
|
111
|
+
}),
|
|
112
|
+
},
|
|
113
|
+
{ additionalProperties: false },
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const RemoveWorktreeParametersSchema = Type.Object(
|
|
117
|
+
{
|
|
118
|
+
worktreePath: Type.String({ minLength: 1, description: "Explicit absolute path of the linked worktree to remove." }),
|
|
119
|
+
},
|
|
120
|
+
{ additionalProperties: false },
|
|
121
|
+
);
|
|
122
|
+
|
|
66
123
|
const PullRequestParametersSchema = Type.Object(
|
|
67
124
|
{
|
|
68
125
|
headBranch: Type.Optional(
|
|
@@ -100,6 +157,150 @@ export function formatPullRequest(details: PullRequestDetails, autofilledFields:
|
|
|
100
157
|
return `Created pull request #${details.number} (${details.state}) for ${repositoryLabel(details.repository)}: ${details.url}.${autofill}`;
|
|
101
158
|
}
|
|
102
159
|
|
|
160
|
+
const WORKTREE_FORMAT_PATH_LIMIT_CHARS = 512;
|
|
161
|
+
const WORKTREE_FORMAT_BRANCH_LIMIT_CHARS = 256;
|
|
162
|
+
|
|
163
|
+
function truncateWorktreeFormatValue(value: string, limit: number): string {
|
|
164
|
+
if (value.length <= limit) return value;
|
|
165
|
+
let end = limit - 1;
|
|
166
|
+
const lastCodePoint = value.codePointAt(end - 1);
|
|
167
|
+
if (lastCodePoint !== undefined && lastCodePoint > 0xffff) end -= 1;
|
|
168
|
+
return `${value.slice(0, end)}…`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function escapeWorktreeFormatControlCharacter(character: string): string {
|
|
172
|
+
const codePoint = character.codePointAt(0);
|
|
173
|
+
return codePoint === undefined ? "" : String.raw`\u${codePoint.toString(16).padStart(4, "0")}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function safeWorktreeFormatValue(value: string, limit: number): string {
|
|
177
|
+
const redacted = redactSecrets(value);
|
|
178
|
+
const escaped = redacted.replace(
|
|
179
|
+
/[\p{Cc}\p{Cf}\u2028\u2029]/gu,
|
|
180
|
+
escapeWorktreeFormatControlCharacter,
|
|
181
|
+
);
|
|
182
|
+
return truncateWorktreeFormatValue(escaped, limit);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function worktreeDisplayState(worktree: WorktreeEntry): string {
|
|
186
|
+
if (worktree.bare) return "bare";
|
|
187
|
+
if (worktree.detached) return "detached";
|
|
188
|
+
const branch = worktree.branch ?? "unknown";
|
|
189
|
+
return `branch ${safeWorktreeFormatValue(branch, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS)}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function worktreeDisplayIndicators(worktree: WorktreeEntry): string[] {
|
|
193
|
+
const indicators: string[] = [];
|
|
194
|
+
if (worktree.main) indicators.push("main");
|
|
195
|
+
if (worktree.current) indicators.push("current");
|
|
196
|
+
if (worktree.locked) indicators.push("locked");
|
|
197
|
+
if (worktree.prunable) indicators.push("prunable");
|
|
198
|
+
return indicators;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function formatWorktreeEntry(worktree: WorktreeEntry): string {
|
|
202
|
+
const path = safeWorktreeFormatValue(worktree.path, WORKTREE_FORMAT_PATH_LIMIT_CHARS);
|
|
203
|
+
const head = worktree.head === null ? "none" : shortCommit(worktree.head);
|
|
204
|
+
const indicators = worktreeDisplayIndicators(worktree);
|
|
205
|
+
const indicatorText = indicators.length === 0 ? "" : ` | ${indicators.join(",")}`;
|
|
206
|
+
return `- ${path} | ${worktreeDisplayState(worktree)} | HEAD ${head}${indicatorText}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function worktreeOmissionLine(omitted: number): string {
|
|
210
|
+
return `${omitted} worktree entr${omitted === 1 ? "y" : "ies"} omitted.`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function formatListWorktrees(details: ListWorktreesDetails): string {
|
|
214
|
+
const repoRoot = safeWorktreeFormatValue(details.repoRoot, WORKTREE_FORMAT_PATH_LIMIT_CHARS);
|
|
215
|
+
const lines = [`Worktrees for ${repoRoot}:`];
|
|
216
|
+
let omitted = details.omitted;
|
|
217
|
+
|
|
218
|
+
for (const [index, worktree] of details.worktrees.entries()) {
|
|
219
|
+
const line = formatWorktreeEntry(worktree);
|
|
220
|
+
const remaining = details.worktrees.length - index - 1;
|
|
221
|
+
const candidateOmitted = details.omitted + remaining;
|
|
222
|
+
const candidate = [
|
|
223
|
+
...lines,
|
|
224
|
+
line,
|
|
225
|
+
...(candidateOmitted > 0 ? [worktreeOmissionLine(candidateOmitted)] : []),
|
|
226
|
+
].join("\n");
|
|
227
|
+
if (candidate.length > GIT_WORKTREE_SUMMARY_LIMIT_CHARS) {
|
|
228
|
+
omitted += details.worktrees.length - index;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
lines.push(line);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (omitted > 0) lines.push(worktreeOmissionLine(omitted));
|
|
235
|
+
return lines.join("\n");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function formatCreateWorktree(details: CreateWorktreeDetails): string {
|
|
239
|
+
const cwd = safeWorktreeFormatValue(details.handoff.cwd, WORKTREE_FORMAT_PATH_LIMIT_CHARS);
|
|
240
|
+
const branch = safeWorktreeFormatValue(details.handoff.branch, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS);
|
|
241
|
+
const mode = details.request.branchMode === "new" ? "new local branch" : "existing local branch";
|
|
242
|
+
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.`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function formatRemoveWorktree(details: RemoveWorktreeDetails): string {
|
|
246
|
+
const path = safeWorktreeFormatValue(
|
|
247
|
+
details.verified.before.worktree.path,
|
|
248
|
+
WORKTREE_FORMAT_PATH_LIMIT_CHARS,
|
|
249
|
+
);
|
|
250
|
+
const branch = safeWorktreeFormatValue(details.handoff.branch, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS);
|
|
251
|
+
return `Removed linked worktree directory ${path}. Verified it is no longer registered and retained local branch ${branch} at HEAD ${shortCommit(details.handoff.head)}; the removed cwd is not ready for handoff.`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function integrationConflictOmissionLine(omitted: number): string {
|
|
255
|
+
return `${omitted} conflict path${omitted === 1 ? "" : "s"} omitted.`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function formatIntegrationConflict(details: Extract<IntegrateBranchDetails, { status: "conflict" }>): string {
|
|
259
|
+
const source = safeWorktreeFormatValue(details.request.sourceBranch, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS);
|
|
260
|
+
const target = safeWorktreeFormatValue(details.request.targetBranch, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS);
|
|
261
|
+
const lines = [
|
|
262
|
+
`integrate_branch found conflicts while integrating ${source} into ${target}; the merge was automatically aborted and exact restoration was verified at target HEAD ${shortCommit(details.verified.heads.after.targetHead)}.`,
|
|
263
|
+
"Conflict paths:",
|
|
264
|
+
];
|
|
265
|
+
let omitted = details.conflict.omitted;
|
|
266
|
+
|
|
267
|
+
for (const [index, entry] of details.conflict.paths.entries()) {
|
|
268
|
+
const path = safeWorktreeFormatValue(entry.path, WORKTREE_FORMAT_PATH_LIMIT_CHARS);
|
|
269
|
+
const line = `- ${path}`;
|
|
270
|
+
const remaining = details.conflict.paths.length - index - 1;
|
|
271
|
+
const candidateOmitted = details.conflict.omitted + remaining;
|
|
272
|
+
const candidate = [
|
|
273
|
+
...lines,
|
|
274
|
+
line,
|
|
275
|
+
...(candidateOmitted > 0 ? [integrationConflictOmissionLine(candidateOmitted)] : []),
|
|
276
|
+
].join("\n");
|
|
277
|
+
if (candidate.length > GIT_INTEGRATION_SUMMARY_LIMIT_CHARS) {
|
|
278
|
+
omitted += details.conflict.paths.length - index;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
lines.push(line);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (omitted > 0) lines.push(integrationConflictOmissionLine(omitted));
|
|
285
|
+
return lines.join("\n");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function formatIntegrateBranch(details: IntegrateBranchDetails): string {
|
|
289
|
+
if (details.status === "conflict") return formatIntegrationConflict(details);
|
|
290
|
+
|
|
291
|
+
const source = safeWorktreeFormatValue(details.request.sourceBranch, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS);
|
|
292
|
+
const target = safeWorktreeFormatValue(details.request.targetBranch, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS);
|
|
293
|
+
const beforeTarget = shortCommit(details.verified.heads.before.targetHead);
|
|
294
|
+
const afterTarget = shortCommit(details.verified.heads.after.targetHead);
|
|
295
|
+
if (details.status === "already_integrated") {
|
|
296
|
+
return `integrate_branch verified ${source} was already integrated into ${target}; no merge ran and target HEAD remained ${afterTarget}.`;
|
|
297
|
+
}
|
|
298
|
+
if (details.status === "fast_forward") {
|
|
299
|
+
return `integrate_branch fast-forwarded ${target} from ${beforeTarget} to ${afterTarget}, integrating ${source}.`;
|
|
300
|
+
}
|
|
301
|
+
return `integrate_branch created and verified a merge commit ${afterTarget} on ${target} from previous target ${beforeTarget} and source ${source}.`;
|
|
302
|
+
}
|
|
303
|
+
|
|
103
304
|
const PULL_REQUEST_INPUT_FIELDS: PullRequestInputField[] = ["headBranch", "baseBranch", "title", "body", "draft"];
|
|
104
305
|
|
|
105
306
|
interface ResolvedPullRequestBranches {
|
|
@@ -257,16 +458,18 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
|
|
|
257
458
|
pi.registerTool({
|
|
258
459
|
name: BRANCH_STATUS_TOOL_NAME,
|
|
259
460
|
label: "Branch Status",
|
|
260
|
-
description: "branch_status explicitly refreshes the current Git repository snapshot
|
|
261
|
-
promptSnippet: "branch_status: explicitly refresh current-repository
|
|
461
|
+
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.",
|
|
462
|
+
promptSnippet: "branch_status: explicitly refresh current-repository Git state and optionally verify targeted local-branch ancestry without mutation",
|
|
262
463
|
promptGuidelines: [
|
|
263
464
|
"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.",
|
|
264
465
|
"Use branch_status as a read-only refresh; branch_status never mutates files, Git state, or GitHub state.",
|
|
466
|
+
"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.",
|
|
265
467
|
],
|
|
266
|
-
parameters:
|
|
267
|
-
async execute(_toolCallId,
|
|
468
|
+
parameters: BranchStatusParametersSchema,
|
|
469
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
268
470
|
const details = await collectGitContext(pi, ctx, {
|
|
269
471
|
signal,
|
|
472
|
+
ancestry: params.ancestry,
|
|
270
473
|
env: options.env,
|
|
271
474
|
fetchImpl: options.fetchImpl,
|
|
272
475
|
});
|
|
@@ -390,6 +593,29 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
|
|
|
390
593
|
},
|
|
391
594
|
});
|
|
392
595
|
|
|
596
|
+
pi.registerTool({
|
|
597
|
+
name: INTEGRATE_BRANCH_TOOL_NAME,
|
|
598
|
+
label: "Integrate Branch",
|
|
599
|
+
description: "integrate_branch verifies and merges one exact existing local source branch into one exact existing local target branch from the current clean control worktree, returning an already-integrated, fast-forward, merge-commit, or automatically aborted conflict result. integrate_branch never fetches or pushes.",
|
|
600
|
+
promptSnippet: "integrate_branch: merge an exact local source branch into the checked-out clean local target with verified outcomes and automatic conflict abort",
|
|
601
|
+
promptGuidelines: [
|
|
602
|
+
"Use integrate_branch only when the user explicitly wants one exact existing local source branch integrated into one exact existing local target branch.",
|
|
603
|
+
"Before integrate_branch, require the current clean control worktree to already have targetBranch checked out; integrate_branch never switches branches, stashes, or discards changes.",
|
|
604
|
+
"Use integrate_branch only after local refs already contain the commits to integrate; integrate_branch never fetches or pushes.",
|
|
605
|
+
"Call integrate_branch by itself and wait for it to complete; never batch integrate_branch with other Git mutations.",
|
|
606
|
+
"After integrate_branch completes, run any targeted branch_status ancestry proof separately; never place integrate_branch and branch_status in the same parallel tool batch.",
|
|
607
|
+
"A conflict result from integrate_branch means the merge was automatically aborted and restoration was verified; semantic conflict analysis belongs to a separate delegated workflow, not integrate_branch.",
|
|
608
|
+
],
|
|
609
|
+
parameters: IntegrateBranchParametersSchema,
|
|
610
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
611
|
+
const details = await integrateBranch(pi, ctx, params, signal);
|
|
612
|
+
return {
|
|
613
|
+
content: [{ type: "text", text: formatIntegrateBranch(details) }],
|
|
614
|
+
details,
|
|
615
|
+
};
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
|
|
393
619
|
pi.registerTool({
|
|
394
620
|
name: PUSH_BRANCH_TOOL_NAME,
|
|
395
621
|
label: "Push Branch",
|
|
@@ -464,4 +690,70 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
|
|
|
464
690
|
});
|
|
465
691
|
},
|
|
466
692
|
});
|
|
693
|
+
|
|
694
|
+
pi.registerTool({
|
|
695
|
+
name: LIST_WORKTREES_TOOL_NAME,
|
|
696
|
+
label: "List Worktrees",
|
|
697
|
+
description: "list_worktrees reads the current repository's bounded linked-worktree inventory without changing Git or filesystem state. list_worktrees accepts no parameters and reports paths, local branches or detached state, HEADs, and safety indicators.",
|
|
698
|
+
promptSnippet: "list_worktrees: explicitly inspect the bounded current-repository worktree inventory without mutation",
|
|
699
|
+
promptGuidelines: [
|
|
700
|
+
"Use list_worktrees when the user or task explicitly needs the current repository's worktree inventory; list_worktrees is read-only.",
|
|
701
|
+
"Use list_worktrees explicitly because automatic Git context remains focused on the active worktree and does not include worktree inventory.",
|
|
702
|
+
],
|
|
703
|
+
parameters: EmptyParametersSchema,
|
|
704
|
+
async execute(_toolCallId, _params, signal, _onUpdate, ctx) {
|
|
705
|
+
const details = await listWorktrees(pi, ctx, signal);
|
|
706
|
+
return {
|
|
707
|
+
content: [{ type: "text", text: formatListWorktrees(details) }],
|
|
708
|
+
details,
|
|
709
|
+
};
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
pi.registerTool({
|
|
714
|
+
name: CREATE_WORKTREE_TOOL_NAME,
|
|
715
|
+
label: "Create Worktree",
|
|
716
|
+
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.",
|
|
717
|
+
promptSnippet: "create_worktree: create and verify a linked worktree at an explicit absolute path with a ready handoff cwd",
|
|
718
|
+
promptGuidelines: [
|
|
719
|
+
"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.",
|
|
720
|
+
"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.",
|
|
721
|
+
"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.",
|
|
722
|
+
],
|
|
723
|
+
parameters: CreateWorktreeParametersSchema,
|
|
724
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
725
|
+
const details = await createWorktree(
|
|
726
|
+
pi,
|
|
727
|
+
ctx,
|
|
728
|
+
params.worktreePath,
|
|
729
|
+
params.branchName,
|
|
730
|
+
params.branchMode,
|
|
731
|
+
signal,
|
|
732
|
+
);
|
|
733
|
+
return {
|
|
734
|
+
content: [{ type: "text", text: formatCreateWorktree(details) }],
|
|
735
|
+
details,
|
|
736
|
+
};
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
pi.registerTool({
|
|
741
|
+
name: REMOVE_WORKTREE_TOOL_NAME,
|
|
742
|
+
label: "Remove Worktree",
|
|
743
|
+
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.",
|
|
744
|
+
promptSnippet: "remove_worktree: force-free removal of an explicitly selected clean linked worktree while retaining its branch",
|
|
745
|
+
promptGuidelines: [
|
|
746
|
+
"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.",
|
|
747
|
+
"Use remove_worktree with exactly worktreePath; remove_worktree never accepts force, move, prune, repair, lock, unlock, branch deletion, remote, or refspec parameters.",
|
|
748
|
+
"Do not batch remove_worktree with dependent worktree mutations; wait for remove_worktree to complete and verify its non-ready handoff before continuing.",
|
|
749
|
+
],
|
|
750
|
+
parameters: RemoveWorktreeParametersSchema,
|
|
751
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
752
|
+
const details = await removeWorktree(pi, ctx, params.worktreePath, signal);
|
|
753
|
+
return {
|
|
754
|
+
content: [{ type: "text", text: formatRemoveWorktree(details) }],
|
|
755
|
+
details,
|
|
756
|
+
};
|
|
757
|
+
},
|
|
758
|
+
});
|
|
467
759
|
}
|
package/src/types.ts
CHANGED
|
@@ -20,6 +20,131 @@ export interface WorkingTreeDetails {
|
|
|
20
20
|
untracked: number;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export interface BranchStatusAncestryQuery {
|
|
24
|
+
sourceBranch: string;
|
|
25
|
+
targetBranch: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface BranchStatusToolInput {
|
|
29
|
+
ancestry?: BranchStatusAncestryQuery;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface IntegrateBranchToolInput {
|
|
33
|
+
sourceBranch: string;
|
|
34
|
+
targetBranch: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type CreateWorktreeMode = "new" | "existing";
|
|
38
|
+
|
|
39
|
+
export type ListWorktreesToolInput = Record<string, never>;
|
|
40
|
+
|
|
41
|
+
export interface CreateWorktreeToolInput {
|
|
42
|
+
worktreePath: string;
|
|
43
|
+
branchName: string;
|
|
44
|
+
branchMode: CreateWorktreeMode;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RemoveWorktreeToolInput {
|
|
48
|
+
worktreePath: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface WorktreeEntry {
|
|
52
|
+
path: string;
|
|
53
|
+
head: string | null;
|
|
54
|
+
branch: string | null;
|
|
55
|
+
detached: boolean;
|
|
56
|
+
bare: boolean;
|
|
57
|
+
locked: boolean;
|
|
58
|
+
lockReason: string | null;
|
|
59
|
+
prunable: boolean;
|
|
60
|
+
pruneReason: string | null;
|
|
61
|
+
main: boolean;
|
|
62
|
+
current: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface VerifiedLinkedWorktreeEntry extends Omit<WorktreeEntry, "path" | "branch"> {
|
|
66
|
+
/** Exact machine-readable identity validated to require no BranchMe display transformation. */
|
|
67
|
+
path: string;
|
|
68
|
+
head: string;
|
|
69
|
+
/** Exact machine-readable identity validated to require no BranchMe display transformation. */
|
|
70
|
+
branch: string;
|
|
71
|
+
detached: false;
|
|
72
|
+
bare: false;
|
|
73
|
+
locked: false;
|
|
74
|
+
lockReason: null;
|
|
75
|
+
prunable: false;
|
|
76
|
+
pruneReason: null;
|
|
77
|
+
main: false;
|
|
78
|
+
current: false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ListWorktreesDetails {
|
|
82
|
+
action: "list_worktrees";
|
|
83
|
+
repoRoot: string;
|
|
84
|
+
worktrees: WorktreeEntry[];
|
|
85
|
+
omitted: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ReadyWorktreeHandoffDetails {
|
|
89
|
+
cwd: string;
|
|
90
|
+
branch: string;
|
|
91
|
+
head: string;
|
|
92
|
+
ready: true;
|
|
93
|
+
summary: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface RetainedBranchHandoffDetails {
|
|
97
|
+
cwd: null;
|
|
98
|
+
branch: string;
|
|
99
|
+
head: string;
|
|
100
|
+
ready: false;
|
|
101
|
+
summary: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type WorktreeHandoffDetails = ReadyWorktreeHandoffDetails | RetainedBranchHandoffDetails;
|
|
105
|
+
|
|
106
|
+
export interface CreateWorktreeDetails {
|
|
107
|
+
action: "create_worktree";
|
|
108
|
+
repoRoot: string;
|
|
109
|
+
request: CreateWorktreeToolInput;
|
|
110
|
+
verified: {
|
|
111
|
+
before: {
|
|
112
|
+
sourcePath: string;
|
|
113
|
+
sourceBranch: string | null;
|
|
114
|
+
sourceDetached: boolean;
|
|
115
|
+
sourceHead: string;
|
|
116
|
+
canonicalWorktreePath: string;
|
|
117
|
+
branchExisted: boolean;
|
|
118
|
+
destinationRegistered: false;
|
|
119
|
+
};
|
|
120
|
+
after: {
|
|
121
|
+
worktreePresent: true;
|
|
122
|
+
worktree: VerifiedLinkedWorktreeEntry;
|
|
123
|
+
workingTree: WorkingTreeDetails;
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
handoff: ReadyWorktreeHandoffDetails;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface RemoveWorktreeDetails {
|
|
130
|
+
action: "remove_worktree";
|
|
131
|
+
repoRoot: string;
|
|
132
|
+
request: RemoveWorktreeToolInput;
|
|
133
|
+
verified: {
|
|
134
|
+
before: {
|
|
135
|
+
worktree: VerifiedLinkedWorktreeEntry;
|
|
136
|
+
workingTree: WorkingTreeDetails;
|
|
137
|
+
};
|
|
138
|
+
after: {
|
|
139
|
+
worktreePresent: false;
|
|
140
|
+
branchRetained: true;
|
|
141
|
+
branch: string;
|
|
142
|
+
head: string;
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
handoff: RetainedBranchHandoffDetails;
|
|
146
|
+
}
|
|
147
|
+
|
|
23
148
|
export interface GitFileChange {
|
|
24
149
|
status: string;
|
|
25
150
|
path: string;
|
|
@@ -54,6 +179,12 @@ export type RelatedPullRequest =
|
|
|
54
179
|
| { status: "none" }
|
|
55
180
|
| { status: "unavailable"; reason: string };
|
|
56
181
|
|
|
182
|
+
export interface BranchAncestryDetails extends BranchStatusAncestryQuery {
|
|
183
|
+
sourceHead: string;
|
|
184
|
+
targetHead: string;
|
|
185
|
+
isAncestor: boolean;
|
|
186
|
+
}
|
|
187
|
+
|
|
57
188
|
export interface BranchStatusDetails {
|
|
58
189
|
repoRoot: string;
|
|
59
190
|
currentBranch: string | null;
|
|
@@ -62,6 +193,7 @@ export interface BranchStatusDetails {
|
|
|
62
193
|
hasChanges: boolean;
|
|
63
194
|
ahead: number | null;
|
|
64
195
|
behind: number | null;
|
|
196
|
+
ancestry?: BranchAncestryDetails;
|
|
65
197
|
warnings?: string[];
|
|
66
198
|
githubRepository?: GitHubRepository;
|
|
67
199
|
}
|
|
@@ -128,6 +260,100 @@ export interface PushBranchDetails {
|
|
|
128
260
|
output: string;
|
|
129
261
|
}
|
|
130
262
|
|
|
263
|
+
export type IntegrateBranchStatus = "already_integrated" | "fast_forward" | "merge_commit" | "conflict";
|
|
264
|
+
|
|
265
|
+
export interface IntegrateBranchHeads {
|
|
266
|
+
sourceHead: string;
|
|
267
|
+
targetHead: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface IntegrateBranchRepositoryVerification {
|
|
271
|
+
worktreeRoot: string;
|
|
272
|
+
canonicalCommonGitDirectory: string;
|
|
273
|
+
identityPreserved: true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export interface IntegrateBranchControlWorktreeVerification {
|
|
277
|
+
targetBranch: string;
|
|
278
|
+
currentBranchPreserved: true;
|
|
279
|
+
cleanBefore: true;
|
|
280
|
+
cleanAfter: true;
|
|
281
|
+
operationStateAbsentBefore: true;
|
|
282
|
+
operationStateAbsentAfter: true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface IntegrateBranchFinalAncestryProof {
|
|
286
|
+
sourceHead: string;
|
|
287
|
+
previousTargetHead: string;
|
|
288
|
+
targetHead: string;
|
|
289
|
+
sourceIsAncestorOfTarget: boolean;
|
|
290
|
+
previousTargetIsAncestorOfTarget: boolean;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export interface IntegrateBranchVerification {
|
|
294
|
+
repository: IntegrateBranchRepositoryVerification;
|
|
295
|
+
controlWorktree: IntegrateBranchControlWorktreeVerification;
|
|
296
|
+
heads: {
|
|
297
|
+
before: IntegrateBranchHeads;
|
|
298
|
+
after: IntegrateBranchHeads;
|
|
299
|
+
};
|
|
300
|
+
finalAncestry: IntegrateBranchFinalAncestryProof;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export interface IntegrateBranchDetailsBase {
|
|
304
|
+
action: "integrate_branch";
|
|
305
|
+
request: IntegrateBranchToolInput;
|
|
306
|
+
verified: IntegrateBranchVerification;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export interface IntegrateBranchAlreadyIntegratedDetails extends IntegrateBranchDetailsBase {
|
|
310
|
+
status: "already_integrated";
|
|
311
|
+
mergeExecuted: false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export interface IntegrateBranchFastForwardDetails extends IntegrateBranchDetailsBase {
|
|
315
|
+
status: "fast_forward";
|
|
316
|
+
mergeExecuted: true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface IntegrateBranchMergeCommitDetails extends IntegrateBranchDetailsBase {
|
|
320
|
+
status: "merge_commit";
|
|
321
|
+
mergeExecuted: true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export interface IntegrateBranchConflictPathEntry {
|
|
325
|
+
/** Exact repository-relative identity; human-readable renderers must sanitize a separate display copy. */
|
|
326
|
+
path: string;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export interface IntegrateBranchConflictDetails extends IntegrateBranchDetailsBase {
|
|
330
|
+
status: "conflict";
|
|
331
|
+
mergeExecuted: true;
|
|
332
|
+
conflict: {
|
|
333
|
+
paths: IntegrateBranchConflictPathEntry[];
|
|
334
|
+
omitted: number;
|
|
335
|
+
abort: {
|
|
336
|
+
attempted: true;
|
|
337
|
+
succeeded: true;
|
|
338
|
+
};
|
|
339
|
+
restoration: {
|
|
340
|
+
verified: true;
|
|
341
|
+
repositoryIdentityPreserved: true;
|
|
342
|
+
controlWorktreeBranchPreserved: true;
|
|
343
|
+
sourceHeadPreserved: true;
|
|
344
|
+
targetHeadRestored: true;
|
|
345
|
+
operationStateCleared: true;
|
|
346
|
+
cleanControlWorktree: true;
|
|
347
|
+
};
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export type IntegrateBranchDetails =
|
|
352
|
+
| IntegrateBranchAlreadyIntegratedDetails
|
|
353
|
+
| IntegrateBranchFastForwardDetails
|
|
354
|
+
| IntegrateBranchMergeCommitDetails
|
|
355
|
+
| IntegrateBranchConflictDetails;
|
|
356
|
+
|
|
131
357
|
export interface PullRequestDetails {
|
|
132
358
|
repository: GitHubRepository;
|
|
133
359
|
number: number;
|