@senad-d/branchme 0.1.9 → 0.2.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 +6 -4
- package/README.md +46 -10
- package/SECURITY.md +29 -5
- package/docs/PROJECT_DEFINITION_BRIEF.md +30 -12
- package/docs/SMOKE_TEST.md +9 -8
- package/docs/STRUCTURE.md +22 -15
- package/docs/TUI_CAPTURE.md +34 -26
- package/package.json +2 -2
- package/src/commands/branchme-command.ts +9 -1
- package/src/constants.ts +4 -0
- package/src/git-retirement.ts +681 -0
- package/src/git.ts +158 -0
- package/src/tools/branchme-tools.ts +55 -1
- package/src/types.ts +76 -0
- package/src/ui/branchme-panel.ts +7 -4
package/src/git.ts
CHANGED
|
@@ -629,6 +629,30 @@ export function parseWorktreePorcelain(output: string): Pick<ListWorktreesDetail
|
|
|
629
629
|
return { worktrees, omitted: records.length - returnedCount };
|
|
630
630
|
}
|
|
631
631
|
|
|
632
|
+
export interface LocalBranchWorktreeOccupancy {
|
|
633
|
+
branchName: string;
|
|
634
|
+
completeInventoryInspected: true;
|
|
635
|
+
occupied: boolean;
|
|
636
|
+
matchingWorktreeCount: number;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export interface PresentDirectLocalBranchRef {
|
|
640
|
+
status: "present";
|
|
641
|
+
branchName: string;
|
|
642
|
+
fullRef: string;
|
|
643
|
+
objectId: string;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export interface AbsentDirectLocalBranchRef {
|
|
647
|
+
status: "absent";
|
|
648
|
+
branchName: string;
|
|
649
|
+
fullRef: string;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
export type DirectLocalBranchRefInspection =
|
|
653
|
+
| PresentDirectLocalBranchRef
|
|
654
|
+
| AbsentDirectLocalBranchRef;
|
|
655
|
+
|
|
632
656
|
interface WorktreeInventoryEntry {
|
|
633
657
|
record: ParsedWorktreeRecord;
|
|
634
658
|
index: number;
|
|
@@ -998,6 +1022,33 @@ export async function listWorktrees(
|
|
|
998
1022
|
};
|
|
999
1023
|
}
|
|
1000
1024
|
|
|
1025
|
+
export async function inspectLocalBranchWorktreeOccupancy(
|
|
1026
|
+
pi: Pick<ExtensionAPI, "exec">,
|
|
1027
|
+
ctx: GitCommandContext,
|
|
1028
|
+
branchName: string,
|
|
1029
|
+
signal?: AbortSignal,
|
|
1030
|
+
): Promise<LocalBranchWorktreeOccupancy> {
|
|
1031
|
+
validateBranchNameInput(branchName);
|
|
1032
|
+
requireLosslessWorktreeIdentity(branchName, "branch");
|
|
1033
|
+
const repoRoot = await getGitRoot(pi, ctx, signal);
|
|
1034
|
+
const args = ["worktree", "list", "--porcelain", "-z"];
|
|
1035
|
+
const result = await runGit(pi, { cwd: repoRoot }, args, {
|
|
1036
|
+
signal,
|
|
1037
|
+
timeout: GIT_STATUS_TIMEOUT_MS,
|
|
1038
|
+
});
|
|
1039
|
+
const records = parseWorktreeRecords(result.stdout);
|
|
1040
|
+
let matchingWorktreeCount = 0;
|
|
1041
|
+
for (const record of records) {
|
|
1042
|
+
if (record.branch === branchName) matchingWorktreeCount += 1;
|
|
1043
|
+
}
|
|
1044
|
+
return {
|
|
1045
|
+
branchName,
|
|
1046
|
+
completeInventoryInspected: true,
|
|
1047
|
+
occupied: matchingWorktreeCount > 0,
|
|
1048
|
+
matchingWorktreeCount,
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1001
1052
|
function validateCreateWorktreeMode(branchMode: unknown): asserts branchMode is CreateWorktreeMode {
|
|
1002
1053
|
if (branchMode !== "new" && branchMode !== "existing") {
|
|
1003
1054
|
throw new Error("branchMode must be either 'new' or 'existing'.");
|
|
@@ -1836,6 +1887,113 @@ export async function localBranchExists(
|
|
|
1836
1887
|
return result.code === 0;
|
|
1837
1888
|
}
|
|
1838
1889
|
|
|
1890
|
+
function requireLosslessLocalRef(branchName: string): string {
|
|
1891
|
+
requireLosslessWorktreeIdentity(branchName, "branch");
|
|
1892
|
+
const fullRef = `refs/heads/${branchName}`;
|
|
1893
|
+
if (!isLosslessGitMetadata(fullRef, GIT_CONTEXT_VALUE_LIMIT_CHARS)) {
|
|
1894
|
+
throw new Error(
|
|
1895
|
+
"The full local branch ref cannot be returned safely and losslessly. " +
|
|
1896
|
+
`Use a branch name short enough to keep the full ref within ${GIT_CONTEXT_VALUE_LIMIT_CHARS} characters.`,
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1899
|
+
return fullRef;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
interface ExactLocalRefListing {
|
|
1903
|
+
exactObjectId: string | null;
|
|
1904
|
+
exactRefFound: boolean;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
function parseExactLocalRefListing(
|
|
1908
|
+
branchName: string,
|
|
1909
|
+
fullRef: string,
|
|
1910
|
+
output: string,
|
|
1911
|
+
): ExactLocalRefListing {
|
|
1912
|
+
const row = stripSingleLineTerminator(output);
|
|
1913
|
+
if (!row) return { exactObjectId: null, exactRefFound: false };
|
|
1914
|
+
|
|
1915
|
+
const fields = row.split(NUL_SEPARATOR);
|
|
1916
|
+
if (fields.length !== 2 || !fields[0] || !fields[1]) {
|
|
1917
|
+
throw new Error(
|
|
1918
|
+
`Unable to inspect local branch ${safeWorktreeBranchLabel(branchName)}: ` +
|
|
1919
|
+
"Git returned a malformed exact-ref listing.",
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
const [observedRef, objectId] = fields;
|
|
1923
|
+
if (observedRef !== fullRef) {
|
|
1924
|
+
if (observedRef.startsWith(`${fullRef}/`)) {
|
|
1925
|
+
return { exactObjectId: null, exactRefFound: false };
|
|
1926
|
+
}
|
|
1927
|
+
throw new Error(
|
|
1928
|
+
`Unable to inspect local branch ${safeWorktreeBranchLabel(branchName)}: ` +
|
|
1929
|
+
"Git returned a different ref identity for the requested exact local branch.",
|
|
1930
|
+
);
|
|
1931
|
+
}
|
|
1932
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(objectId)) {
|
|
1933
|
+
throw new Error(
|
|
1934
|
+
`Unable to inspect local branch ${safeWorktreeBranchLabel(branchName)}: ` +
|
|
1935
|
+
"Git returned a malformed full object identity.",
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
1938
|
+
return { exactObjectId: objectId, exactRefFound: true };
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
async function rejectSymbolicLocalBranchRef(
|
|
1942
|
+
pi: Pick<ExtensionAPI, "exec">,
|
|
1943
|
+
ctx: GitCommandContext,
|
|
1944
|
+
branchName: string,
|
|
1945
|
+
fullRef: string,
|
|
1946
|
+
signal?: AbortSignal,
|
|
1947
|
+
): Promise<void> {
|
|
1948
|
+
const symbolicRefArgs = ["symbolic-ref", "--quiet", fullRef];
|
|
1949
|
+
const symbolicRef = await runGit(pi, ctx, symbolicRefArgs, {
|
|
1950
|
+
signal,
|
|
1951
|
+
timeout: GIT_STATUS_TIMEOUT_MS,
|
|
1952
|
+
allowFailure: true,
|
|
1953
|
+
});
|
|
1954
|
+
if (symbolicRef.code === 0) {
|
|
1955
|
+
throw new Error(
|
|
1956
|
+
`Local branch ${safeWorktreeBranchLabel(branchName)} is symbolic; ` +
|
|
1957
|
+
"retirement requires a verified direct local branch ref.",
|
|
1958
|
+
);
|
|
1959
|
+
}
|
|
1960
|
+
if (symbolicRef.code !== 1 || symbolicRef.stdout.length > 0 || symbolicRef.stderr.length > 0) {
|
|
1961
|
+
throw new Error(formatGitFailure(symbolicRefArgs, symbolicRef));
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
export async function inspectDirectLocalBranchRef(
|
|
1966
|
+
pi: Pick<ExtensionAPI, "exec">,
|
|
1967
|
+
ctx: GitCommandContext,
|
|
1968
|
+
branchName: string,
|
|
1969
|
+
signal?: AbortSignal,
|
|
1970
|
+
): Promise<DirectLocalBranchRefInspection> {
|
|
1971
|
+
validateBranchNameInput(branchName);
|
|
1972
|
+
const fullRef = requireLosslessLocalRef(branchName);
|
|
1973
|
+
const listArgs = [
|
|
1974
|
+
"for-each-ref",
|
|
1975
|
+
"--count=1",
|
|
1976
|
+
"--sort=refname",
|
|
1977
|
+
"--format=%(refname)%00%(objectname)",
|
|
1978
|
+
fullRef,
|
|
1979
|
+
];
|
|
1980
|
+
const listing = await runGit(pi, ctx, listArgs, {
|
|
1981
|
+
signal,
|
|
1982
|
+
timeout: GIT_STATUS_TIMEOUT_MS,
|
|
1983
|
+
});
|
|
1984
|
+
const parsed = parseExactLocalRefListing(branchName, fullRef, listing.stdout);
|
|
1985
|
+
await rejectSymbolicLocalBranchRef(pi, ctx, branchName, fullRef, signal);
|
|
1986
|
+
if (!parsed.exactRefFound || parsed.exactObjectId === null) {
|
|
1987
|
+
return { status: "absent", branchName, fullRef };
|
|
1988
|
+
}
|
|
1989
|
+
return {
|
|
1990
|
+
status: "present",
|
|
1991
|
+
branchName,
|
|
1992
|
+
fullRef,
|
|
1993
|
+
objectId: parsed.exactObjectId,
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1839
1997
|
export async function getLocalBranchCommit(
|
|
1840
1998
|
pi: Pick<ExtensionAPI, "exec">,
|
|
1841
1999
|
ctx: GitCommandContext,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
CREATE_WORKTREE_TOOL_NAME,
|
|
9
9
|
FETCH_BRANCH_TOOL_NAME,
|
|
10
10
|
GIT_INTEGRATION_SUMMARY_LIMIT_CHARS,
|
|
11
|
+
GIT_RETIREMENT_SUMMARY_LIMIT_CHARS,
|
|
11
12
|
GIT_WORKTREE_SUMMARY_LIMIT_CHARS,
|
|
12
13
|
INTEGRATE_BRANCH_TOOL_NAME,
|
|
13
14
|
LIST_WORKTREES_TOOL_NAME,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
PUSH_BRANCH_TOOL_NAME,
|
|
17
18
|
REBASE_BRANCH_TOOL_NAME,
|
|
18
19
|
REMOVE_WORKTREE_TOOL_NAME,
|
|
20
|
+
RETIRE_BRANCH_TOOL_NAME,
|
|
19
21
|
} from "../constants.ts";
|
|
20
22
|
import {
|
|
21
23
|
changeExistingLocalBranch,
|
|
@@ -38,6 +40,7 @@ import {
|
|
|
38
40
|
} from "../git.ts";
|
|
39
41
|
import { collectGitContext, formatGitContext } from "../git-context.ts";
|
|
40
42
|
import { integrateBranch } from "../git-integration.ts";
|
|
43
|
+
import { retireBranch } from "../git-retirement.ts";
|
|
41
44
|
import {
|
|
42
45
|
createGitHubPullRequest,
|
|
43
46
|
ensureGitHubBranchExists,
|
|
@@ -60,6 +63,7 @@ import type {
|
|
|
60
63
|
PullRequestToolDetails,
|
|
61
64
|
PullRequestToolInput,
|
|
62
65
|
RemoveWorktreeDetails,
|
|
66
|
+
RetireBranchDetails,
|
|
63
67
|
WorktreeEntry,
|
|
64
68
|
} from "../types.ts";
|
|
65
69
|
|
|
@@ -88,6 +92,19 @@ const IntegrateBranchParametersSchema = Type.Object(
|
|
|
88
92
|
{ additionalProperties: false },
|
|
89
93
|
);
|
|
90
94
|
|
|
95
|
+
const RetireBranchParametersSchema = Type.Object(
|
|
96
|
+
{
|
|
97
|
+
branchName: Type.String({ minLength: 1, description: "Exact existing local branch to retire." }),
|
|
98
|
+
expectedHead: Type.String({
|
|
99
|
+
pattern: "^(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})$",
|
|
100
|
+
description: "Full 40- or 64-character hexadecimal commit identity expected for branchName.",
|
|
101
|
+
}),
|
|
102
|
+
targetBranch: Type.String({ minLength: 1, description: "Exact existing local branch used for ancestry verification." }),
|
|
103
|
+
force: Type.Boolean({ description: "Explicit authorization for unmerged retirement; false for merged retirement." }),
|
|
104
|
+
},
|
|
105
|
+
{ additionalProperties: false },
|
|
106
|
+
);
|
|
107
|
+
|
|
91
108
|
const CreateBranchParametersSchema = Type.Object(
|
|
92
109
|
{
|
|
93
110
|
branchName: Type.String({ minLength: 1, description: "Name of the new branch to create from current HEAD." }),
|
|
@@ -301,6 +318,18 @@ export function formatIntegrateBranch(details: IntegrateBranchDetails): string {
|
|
|
301
318
|
return `integrate_branch created and verified a merge commit ${afterTarget} on ${target} from previous target ${beforeTarget} and source ${source}.`;
|
|
302
319
|
}
|
|
303
320
|
|
|
321
|
+
export function formatRetireBranch(details: RetireBranchDetails): string {
|
|
322
|
+
const branch = safeWorktreeFormatValue(details.request.branchName, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS);
|
|
323
|
+
const target = safeWorktreeFormatValue(details.request.targetBranch, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS);
|
|
324
|
+
const retiredHead = shortCommit(details.verified.refs.before.retiring.head);
|
|
325
|
+
const targetHead = shortCommit(details.verified.refs.after.target.head);
|
|
326
|
+
const localOnly = "No remote or remote-tracking branch was deleted.";
|
|
327
|
+
const text = details.mode === "merged"
|
|
328
|
+
? `retire_branch retired merged local branch ${branch} at HEAD ${retiredHead} after verifying ancestry to ${target} at ${targetHead}. ${localOnly}`
|
|
329
|
+
: `retire_branch force-retired unmerged local branch ${branch} at HEAD ${retiredHead} against ${target} at ${targetHead}. The retired commit may lose its remaining local branch reference and can eventually become unreachable. ${localOnly}`;
|
|
330
|
+
return text.slice(0, GIT_RETIREMENT_SUMMARY_LIMIT_CHARS);
|
|
331
|
+
}
|
|
332
|
+
|
|
304
333
|
const PULL_REQUEST_INPUT_FIELDS: PullRequestInputField[] = ["headBranch", "baseBranch", "title", "body", "draft"];
|
|
305
334
|
|
|
306
335
|
interface ResolvedPullRequestBranches {
|
|
@@ -616,6 +645,30 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
|
|
|
616
645
|
},
|
|
617
646
|
});
|
|
618
647
|
|
|
648
|
+
pi.registerTool({
|
|
649
|
+
name: RETIRE_BRANCH_TOOL_NAME,
|
|
650
|
+
label: "Retire Branch",
|
|
651
|
+
description: "retire_branch deletes one exact unoccupied local branch only when its current commit matches expectedHead and its ancestry to one exact local targetBranch is verified. retire_branch requires force: true for explicitly authorized unmerged retirement, never directly deletes remote or remote-tracking branches, and leaves local branch configuration untouched.",
|
|
652
|
+
promptSnippet: "retire_branch: delete one exact local branch with an expected-HEAD lease, target ancestry verification, worktree-occupancy rejection, and explicit unmerged force authorization",
|
|
653
|
+
promptGuidelines: [
|
|
654
|
+
"Use retire_branch only when the user explicitly intends to delete one exact local branch.",
|
|
655
|
+
"Before retire_branch, obtain a fresh expected commit and ancestry proof with branch_status unless the exact commit was already supplied.",
|
|
656
|
+
"Call branch_status and retire_branch sequentially; never place branch_status and retire_branch in the same parallel tool batch.",
|
|
657
|
+
"Call retire_branch by itself; never run retire_branch beside another Git mutation.",
|
|
658
|
+
"Use force: true with retire_branch only after explicit user authorization for unmerged data loss.",
|
|
659
|
+
"Treat remove_worktree and retire_branch as separate sequential operations: removing a linked worktree retains its branch, which may be retired only afterward.",
|
|
660
|
+
"retire_branch never directly deletes a remote or remote-tracking branch.",
|
|
661
|
+
],
|
|
662
|
+
parameters: RetireBranchParametersSchema,
|
|
663
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
664
|
+
const details = await retireBranch(pi, ctx, params, signal);
|
|
665
|
+
return {
|
|
666
|
+
content: [{ type: "text", text: formatRetireBranch(details) }],
|
|
667
|
+
details,
|
|
668
|
+
};
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
|
|
619
672
|
pi.registerTool({
|
|
620
673
|
name: PUSH_BRANCH_TOOL_NAME,
|
|
621
674
|
label: "Push Branch",
|
|
@@ -643,7 +696,8 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
|
|
|
643
696
|
description: "pull_request creates a GitHub pull request in the resolved current repository. PR fields are optional only when BRANCHME_PR_AUTOFILL=true; pull_request then infers branches, derives title/body from commit subjects, and defaults draft to false. Branches must exist locally and on GitHub, and headBranch must match the GitHub-visible commit. Owner-prefixed refs, owner, and repo are never accepted as inputs.",
|
|
644
697
|
promptSnippet: "pull_request: create a GitHub pull request with explicit fields or optional configured PR field autofill",
|
|
645
698
|
promptGuidelines: [
|
|
646
|
-
"Use pull_request
|
|
699
|
+
"Use pull_request whenever creating a pull request is appropriate for the active workflow; authorization may come from user prompts, system or developer instructions, AGENTS.md, skills, automation, or delegated/subagent prompts.",
|
|
700
|
+
"Agents may invoke pull_request without a separate end-user confirmation when the active workflow makes pull request creation appropriate.",
|
|
647
701
|
"When automatic Git context reports pull request field autofill disabled, use pull_request only with explicit headBranch, baseBranch, title, body, and draft values.",
|
|
648
702
|
"When automatic Git context reports pull request field autofill enabled, pull_request may omit fields the user did not provide; prefer the user's explicit values whenever present.",
|
|
649
703
|
"Use pull_request only with existing local branches for headBranch and baseBranch; headBranch must match the GitHub-visible branch commit.",
|
package/src/types.ts
CHANGED
|
@@ -34,6 +34,13 @@ export interface IntegrateBranchToolInput {
|
|
|
34
34
|
targetBranch: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export interface RetireBranchToolInput {
|
|
38
|
+
branchName: string;
|
|
39
|
+
expectedHead: string;
|
|
40
|
+
targetBranch: string;
|
|
41
|
+
force: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
export type CreateWorktreeMode = "new" | "existing";
|
|
38
45
|
|
|
39
46
|
export type ListWorktreesToolInput = Record<string, never>;
|
|
@@ -354,6 +361,75 @@ export type IntegrateBranchDetails =
|
|
|
354
361
|
| IntegrateBranchMergeCommitDetails
|
|
355
362
|
| IntegrateBranchConflictDetails;
|
|
356
363
|
|
|
364
|
+
export type RetireBranchMode = "merged" | "forced_unmerged";
|
|
365
|
+
|
|
366
|
+
export interface RetireBranchRepositoryIdentity {
|
|
367
|
+
worktreeRoot: string;
|
|
368
|
+
canonicalCommonGitDirectory: string;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export interface RetireBranchRefIdentity {
|
|
372
|
+
branchName: string;
|
|
373
|
+
fullRef: string;
|
|
374
|
+
head: string;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export interface RetireBranchAbsentRefProof {
|
|
378
|
+
branchName: string;
|
|
379
|
+
fullRef: string;
|
|
380
|
+
absent: true;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export interface RetireBranchWorktreeOccupancyProof {
|
|
384
|
+
branchName: string;
|
|
385
|
+
completeInventoryInspected: true;
|
|
386
|
+
occupied: false;
|
|
387
|
+
matchingWorktreeCount: 0;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export interface RetireBranchDetails {
|
|
391
|
+
action: "retire_branch";
|
|
392
|
+
status: "retired";
|
|
393
|
+
mode: RetireBranchMode;
|
|
394
|
+
/** Exact normalized request values used for the verified retirement. */
|
|
395
|
+
request: RetireBranchToolInput;
|
|
396
|
+
verified: {
|
|
397
|
+
repository: {
|
|
398
|
+
before: RetireBranchRepositoryIdentity;
|
|
399
|
+
after: RetireBranchRepositoryIdentity;
|
|
400
|
+
identityPreserved: true;
|
|
401
|
+
};
|
|
402
|
+
refs: {
|
|
403
|
+
before: {
|
|
404
|
+
retiring: RetireBranchRefIdentity;
|
|
405
|
+
target: RetireBranchRefIdentity;
|
|
406
|
+
expectedHead: string;
|
|
407
|
+
expectedHeadMatches: true;
|
|
408
|
+
};
|
|
409
|
+
after: {
|
|
410
|
+
retiring: RetireBranchAbsentRefProof;
|
|
411
|
+
target: RetireBranchRefIdentity;
|
|
412
|
+
targetHeadPreserved: true;
|
|
413
|
+
};
|
|
414
|
+
};
|
|
415
|
+
ancestry: {
|
|
416
|
+
retiringHead: string;
|
|
417
|
+
targetHead: string;
|
|
418
|
+
retiringIsAncestorOfTarget: boolean;
|
|
419
|
+
};
|
|
420
|
+
worktreeOccupancy: {
|
|
421
|
+
before: RetireBranchWorktreeOccupancyProof;
|
|
422
|
+
after: RetireBranchWorktreeOccupancyProof;
|
|
423
|
+
};
|
|
424
|
+
mutation: {
|
|
425
|
+
exactLocalRefDeletionAttempted: true;
|
|
426
|
+
localBranchAbsentAfterDeletion: true;
|
|
427
|
+
directRemoteDeletionAttempted: false;
|
|
428
|
+
remoteTrackingRefDeletionAttempted: false;
|
|
429
|
+
};
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
357
433
|
export interface PullRequestDetails {
|
|
358
434
|
repository: GitHubRepository;
|
|
359
435
|
number: number;
|
package/src/ui/branchme-panel.ts
CHANGED
|
@@ -154,7 +154,7 @@ function sectionTitle(section: BranchMePanelSection): string {
|
|
|
154
154
|
case "workflow":
|
|
155
155
|
return "WORKFLOW";
|
|
156
156
|
case "integration":
|
|
157
|
-
return "
|
|
157
|
+
return "LIFECYCLE";
|
|
158
158
|
case "worktrees":
|
|
159
159
|
return "WORKTREES";
|
|
160
160
|
}
|
|
@@ -183,7 +183,7 @@ function sectionFooter(section: BranchMePanelSection, data: BranchMePanelData):
|
|
|
183
183
|
case "workflow":
|
|
184
184
|
return "workflow • inspect → change → fetch/pull/rebase → create → push → PR";
|
|
185
185
|
case "integration":
|
|
186
|
-
return "
|
|
186
|
+
return "lifecycle • integrate → remove worktree separately → retire local ref";
|
|
187
187
|
case "worktrees":
|
|
188
188
|
return "worktrees • create → handoff cwd → next session • remove retains branch";
|
|
189
189
|
}
|
|
@@ -214,12 +214,15 @@ function sectionRows(section: BranchMePanelSection, data: BranchMePanelData): Pa
|
|
|
214
214
|
];
|
|
215
215
|
case "integration":
|
|
216
216
|
return [
|
|
217
|
-
heading("
|
|
217
|
+
heading("LIFECYCLE"),
|
|
218
218
|
workflowDetailRow("integrate_branch", "exact local source -> target"),
|
|
219
219
|
workflowDetailRow("control target", "checked out + clean"),
|
|
220
|
-
workflowDetailRow("remote effects", "never fetch or push"),
|
|
221
220
|
workflowDetailRow("conflict", "automatic verified abort"),
|
|
222
221
|
workflowDetailRow("semantic intent", "separate developer workflow"),
|
|
222
|
+
workflowDetailRow("remove_worktree", "separate; branch retained"),
|
|
223
|
+
workflowDetailRow("retire_branch", "leased local ref deletion"),
|
|
224
|
+
workflowDetailRow("retirement guard", "unoccupied + target ancestry"),
|
|
225
|
+
workflowDetailRow("remote effects", "never delete remote refs"),
|
|
223
226
|
];
|
|
224
227
|
case "worktrees":
|
|
225
228
|
return [
|