@senad-d/branchme 0.1.8 → 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/src/git.ts CHANGED
@@ -24,6 +24,8 @@ import {
24
24
  import { redactSecrets } from "./redaction.ts";
25
25
  import type {
26
26
  AheadBehindCount,
27
+ BranchAncestryDetails,
28
+ BranchStatusAncestryQuery,
27
29
  BranchStatusDetails,
28
30
  ChangeBranchDetails,
29
31
  CreateBranchDetails,
@@ -157,6 +159,23 @@ export async function getGitRoot(
157
159
  return root;
158
160
  }
159
161
 
162
+ export async function getCanonicalGitWorktreeRoot(
163
+ pi: Pick<ExtensionAPI, "exec">,
164
+ ctx: GitCommandContext,
165
+ signal?: AbortSignal,
166
+ ): Promise<string> {
167
+ const repoRoot = await getGitRoot(pi, ctx, signal);
168
+ if (!isAbsolute(repoRoot) || /[\p{Cc}\p{Cf}\u2028\u2029]/u.test(repoRoot)) {
169
+ throw new Error("Unable to verify the active worktree root: Git returned an invalid path.");
170
+ }
171
+
172
+ try {
173
+ return await realpath(repoRoot);
174
+ } catch {
175
+ throw new Error("Unable to verify the active worktree root: the path could not be resolved.");
176
+ }
177
+ }
178
+
160
179
  export async function getCurrentBranch(
161
180
  pi: Pick<ExtensionAPI, "exec">,
162
181
  ctx: GitCommandContext,
@@ -667,12 +686,16 @@ function safeWorktreeBranchLabel(branchName: string): string {
667
686
  return JSON.stringify(safeWorktreeValue(branchName, GIT_CONTEXT_VALUE_LIMIT_CHARS));
668
687
  }
669
688
 
689
+ export function isLosslessGitMetadata(value: string, limit: number): boolean {
690
+ return safeWorktreeValue(value, limit) === value;
691
+ }
692
+
670
693
  export function requireLosslessWorktreeIdentity(
671
694
  value: string,
672
695
  identity: "cwd" | "branch",
673
696
  ): string {
674
697
  const limit = identity === "cwd" ? GIT_WORKTREE_PATH_LIMIT_CHARS : GIT_CONTEXT_VALUE_LIMIT_CHARS;
675
- if (safeWorktreeValue(value, limit) === value) return value;
698
+ if (isLosslessGitMetadata(value, limit)) return value;
676
699
 
677
700
  const label = identity === "cwd" ? "canonical worktree path" : "local branch name";
678
701
  throw new Error(
@@ -775,13 +798,13 @@ function stripSingleLineTerminator(value: string): string {
775
798
  return value;
776
799
  }
777
800
 
778
- async function getCommonGitDirectory(
801
+ export async function getCanonicalCommonGitDirectory(
779
802
  pi: Pick<ExtensionAPI, "exec">,
780
- repoRoot: string,
803
+ ctx: GitCommandContext,
781
804
  signal?: AbortSignal,
782
805
  ): Promise<string> {
783
806
  const args = ["rev-parse", "--path-format=absolute", "--git-common-dir"];
784
- const result = await runGit(pi, { cwd: repoRoot }, args, {
807
+ const result = await runGit(pi, ctx, args, {
785
808
  signal,
786
809
  timeout: GIT_STATUS_TIMEOUT_MS,
787
810
  });
@@ -791,14 +814,77 @@ async function getCommonGitDirectory(
791
814
  !isAbsolute(commonGitDir) ||
792
815
  /[\p{Cc}\p{Cf}\u2028\u2029]/u.test(commonGitDir)
793
816
  ) {
794
- throw new Error("Unable to validate worktree path: Git returned an invalid common directory.");
817
+ throw new Error("Unable to verify repository identity: Git returned an invalid common directory.");
795
818
  }
796
819
 
797
820
  try {
798
821
  return await realpath(commonGitDir);
799
822
  } catch {
800
- throw new Error("Unable to validate worktree path: Git common directory could not be resolved.");
823
+ throw new Error("Unable to verify repository identity: the common Git directory could not be resolved.");
824
+ }
825
+ }
826
+
827
+ export type GitOperationKind = "merge" | "rebase" | "cherry-pick" | "revert" | "sequencer";
828
+
829
+ export interface GitOperationState {
830
+ active: GitOperationKind[];
831
+ mergeHeadPresent: boolean;
832
+ autoMergePresent: boolean;
833
+ }
834
+
835
+ const GIT_OPERATION_MARKERS: ReadonlyArray<readonly [path: string, operation: GitOperationKind]> = [
836
+ ["MERGE_HEAD", "merge"],
837
+ ["AUTO_MERGE", "merge"],
838
+ ["rebase-merge", "rebase"],
839
+ ["rebase-apply", "rebase"],
840
+ ["CHERRY_PICK_HEAD", "cherry-pick"],
841
+ ["REVERT_HEAD", "revert"],
842
+ ["sequencer", "sequencer"],
843
+ ];
844
+
845
+ async function gitOperationMarkerExists(path: string): Promise<boolean> {
846
+ try {
847
+ await lstat(path);
848
+ return true;
849
+ } catch (error) {
850
+ if (isMissingFilesystemPath(error)) return false;
851
+ throw new Error("Unable to inspect repository operation state: a Git state path could not be inspected.");
852
+ }
853
+ }
854
+
855
+ export async function getGitOperationState(
856
+ pi: Pick<ExtensionAPI, "exec">,
857
+ ctx: GitCommandContext,
858
+ signal?: AbortSignal,
859
+ ): Promise<GitOperationState> {
860
+ const args = ["rev-parse", "--path-format=absolute"];
861
+ for (const [path] of GIT_OPERATION_MARKERS) args.push("--git-path", path);
862
+ const result = await runGit(pi, ctx, args, {
863
+ signal,
864
+ timeout: GIT_STATUS_TIMEOUT_MS,
865
+ });
866
+ const paths = stripSingleLineTerminator(result.stdout).split(/\r?\n/u);
867
+ if (
868
+ paths.length !== GIT_OPERATION_MARKERS.length ||
869
+ paths.some((path) => !isAbsolute(path) || /[\p{Cc}\p{Cf}\u2028\u2029]/u.test(path))
870
+ ) {
871
+ throw new Error("Unable to inspect repository operation state: Git returned invalid state paths.");
801
872
  }
873
+
874
+ const active: GitOperationKind[] = [];
875
+ const presentMarkers: boolean[] = [];
876
+ for (const [index, path] of paths.entries()) {
877
+ const present = await gitOperationMarkerExists(path);
878
+ presentMarkers.push(present);
879
+ if (!present) continue;
880
+ const operation = GIT_OPERATION_MARKERS[index][1];
881
+ if (!active.includes(operation)) active.push(operation);
882
+ }
883
+ return {
884
+ active,
885
+ mergeHeadPresent: presentMarkers[0] === true,
886
+ autoMergePresent: presentMarkers[1] === true,
887
+ };
802
888
  }
803
889
 
804
890
  async function prepareWorktreeCreation(
@@ -820,7 +906,11 @@ async function prepareWorktreeCreation(
820
906
  }
821
907
  }
822
908
 
823
- const commonGitDir = await getCommonGitDirectory(pi, inventory.repoRoot, signal);
909
+ const commonGitDir = await getCanonicalCommonGitDirectory(
910
+ pi,
911
+ { cwd: inventory.repoRoot },
912
+ signal,
913
+ );
824
914
  if (pathIsInsideOrEqual(canonicalPath, commonGitDir)) {
825
915
  throw new Error("worktreePath destination cannot be inside the repository common Git directory.");
826
916
  }
@@ -1764,6 +1854,64 @@ export async function getLocalBranchCommit(
1764
1854
  return commit;
1765
1855
  }
1766
1856
 
1857
+ export async function requireExistingLocalBranch(
1858
+ pi: Pick<ExtensionAPI, "exec">,
1859
+ ctx: GitCommandContext,
1860
+ branchName: string,
1861
+ label: "Source" | "Target",
1862
+ signal?: AbortSignal,
1863
+ ): Promise<void> {
1864
+ if (await localBranchExists(pi, ctx, branchName, signal)) return;
1865
+ throw new Error(`${label} local branch '${safeGitContextValue(branchName)}' does not exist.`);
1866
+ }
1867
+
1868
+ export async function isCommitAncestor(
1869
+ pi: Pick<ExtensionAPI, "exec">,
1870
+ ctx: GitCommandContext,
1871
+ ancestorCommit: string,
1872
+ descendantCommit: string,
1873
+ signal?: AbortSignal,
1874
+ ): Promise<boolean> {
1875
+ if (!/^[0-9a-f]{40,64}$/iu.test(ancestorCommit) || !/^[0-9a-f]{40,64}$/iu.test(descendantCommit)) {
1876
+ throw new Error("Ancestry verification requires two valid full commit identities.");
1877
+ }
1878
+
1879
+ const args = ["merge-base", "--is-ancestor", ancestorCommit, descendantCommit];
1880
+ const result = await runGit(pi, ctx, args, {
1881
+ signal,
1882
+ timeout: GIT_STATUS_TIMEOUT_MS,
1883
+ allowFailure: true,
1884
+ });
1885
+ if (result.code !== 0 && result.code !== 1) throw new Error(formatGitFailure(args, result));
1886
+ return result.code === 0;
1887
+ }
1888
+
1889
+ export async function getLocalBranchAncestry(
1890
+ pi: Pick<ExtensionAPI, "exec">,
1891
+ ctx: GitCommandContext,
1892
+ query: BranchStatusAncestryQuery,
1893
+ signal?: AbortSignal,
1894
+ ): Promise<BranchAncestryDetails> {
1895
+ validateBranchNameInput(query.sourceBranch, "Source branch");
1896
+ validateBranchNameInput(query.targetBranch, "Target branch");
1897
+ await validateBranchName(pi, ctx, query.sourceBranch, signal);
1898
+ await validateBranchName(pi, ctx, query.targetBranch, signal);
1899
+ await requireExistingLocalBranch(pi, ctx, query.sourceBranch, "Source", signal);
1900
+ await requireExistingLocalBranch(pi, ctx, query.targetBranch, "Target", signal);
1901
+
1902
+ const sourceHead = await getLocalBranchCommit(pi, ctx, query.sourceBranch, signal);
1903
+ const targetHead = await getLocalBranchCommit(pi, ctx, query.targetBranch, signal);
1904
+ const isAncestor = await isCommitAncestor(pi, ctx, sourceHead, targetHead, signal);
1905
+
1906
+ return {
1907
+ sourceBranch: query.sourceBranch,
1908
+ targetBranch: query.targetBranch,
1909
+ sourceHead,
1910
+ targetHead,
1911
+ isAncestor,
1912
+ };
1913
+ }
1914
+
1767
1915
  export async function createLocalBranch(
1768
1916
  pi: Pick<ExtensionAPI, "exec">,
1769
1917
  ctx: GitCommandContext,
@@ -7,7 +7,9 @@ import {
7
7
  CREATE_BRANCH_TOOL_NAME,
8
8
  CREATE_WORKTREE_TOOL_NAME,
9
9
  FETCH_BRANCH_TOOL_NAME,
10
+ GIT_INTEGRATION_SUMMARY_LIMIT_CHARS,
10
11
  GIT_WORKTREE_SUMMARY_LIMIT_CHARS,
12
+ INTEGRATE_BRANCH_TOOL_NAME,
11
13
  LIST_WORKTREES_TOOL_NAME,
12
14
  PULL_BRANCH_TOOL_NAME,
13
15
  PULL_REQUEST_TOOL_NAME,
@@ -35,6 +37,7 @@ import {
35
37
  withRepositoryMutationQueue,
36
38
  } from "../git.ts";
37
39
  import { collectGitContext, formatGitContext } from "../git-context.ts";
40
+ import { integrateBranch } from "../git-integration.ts";
38
41
  import {
39
42
  createGitHubPullRequest,
40
43
  ensureGitHubBranchExists,
@@ -49,6 +52,7 @@ import type {
49
52
  ChangeBranchDetails,
50
53
  CreateWorktreeDetails,
51
54
  GitContextDetails,
55
+ IntegrateBranchDetails,
52
56
  ListWorktreesDetails,
53
57
  PullRequestDetails,
54
58
  PullRequestInput,
@@ -61,6 +65,29 @@ import type {
61
65
 
62
66
  const EmptyParametersSchema = Type.Object({}, { additionalProperties: false });
63
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
+
64
91
  const CreateBranchParametersSchema = Type.Object(
65
92
  {
66
93
  branchName: Type.String({ minLength: 1, description: "Name of the new branch to create from current HEAD." }),
@@ -224,6 +251,56 @@ export function formatRemoveWorktree(details: RemoveWorktreeDetails): string {
224
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.`;
225
252
  }
226
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
+
227
304
  const PULL_REQUEST_INPUT_FIELDS: PullRequestInputField[] = ["headBranch", "baseBranch", "title", "body", "draft"];
228
305
 
229
306
  interface ResolvedPullRequestBranches {
@@ -381,16 +458,18 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
381
458
  pi.registerTool({
382
459
  name: BRANCH_STATUS_TOOL_NAME,
383
460
  label: "Branch Status",
384
- description: "branch_status explicitly refreshes the current Git repository snapshot, including branch, working tree, unstaged changes, related pull request, and recent commits. branch_status is read-only and never mutates files, Git state, or GitHub state.",
385
- promptSnippet: "branch_status: explicitly refresh current-repository branch, working tree, changes, related PR, and recent commits without mutation",
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",
386
463
  promptGuidelines: [
387
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.",
388
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.",
389
467
  ],
390
- parameters: EmptyParametersSchema,
391
- async execute(_toolCallId, _params, signal, _onUpdate, ctx) {
468
+ parameters: BranchStatusParametersSchema,
469
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
392
470
  const details = await collectGitContext(pi, ctx, {
393
471
  signal,
472
+ ancestry: params.ancestry,
394
473
  env: options.env,
395
474
  fetchImpl: options.fetchImpl,
396
475
  });
@@ -514,6 +593,29 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
514
593
  },
515
594
  });
516
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
+
517
619
  pi.registerTool({
518
620
  name: PUSH_BRANCH_TOOL_NAME,
519
621
  label: "Push Branch",
package/src/types.ts CHANGED
@@ -20,6 +20,20 @@ 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
+
23
37
  export type CreateWorktreeMode = "new" | "existing";
24
38
 
25
39
  export type ListWorktreesToolInput = Record<string, never>;
@@ -165,6 +179,12 @@ export type RelatedPullRequest =
165
179
  | { status: "none" }
166
180
  | { status: "unavailable"; reason: string };
167
181
 
182
+ export interface BranchAncestryDetails extends BranchStatusAncestryQuery {
183
+ sourceHead: string;
184
+ targetHead: string;
185
+ isAncestor: boolean;
186
+ }
187
+
168
188
  export interface BranchStatusDetails {
169
189
  repoRoot: string;
170
190
  currentBranch: string | null;
@@ -173,6 +193,7 @@ export interface BranchStatusDetails {
173
193
  hasChanges: boolean;
174
194
  ahead: number | null;
175
195
  behind: number | null;
196
+ ancestry?: BranchAncestryDetails;
176
197
  warnings?: string[];
177
198
  githubRepository?: GitHubRepository;
178
199
  }
@@ -239,6 +260,100 @@ export interface PushBranchDetails {
239
260
  output: string;
240
261
  }
241
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
+
242
357
  export interface PullRequestDetails {
243
358
  repository: GitHubRepository;
244
359
  number: number;
@@ -24,14 +24,14 @@ type PanelCell =
24
24
  preserveLeading?: boolean;
25
25
  };
26
26
 
27
- export type BranchMePanelSection = "status" | "workflow" | "worktrees";
27
+ export type BranchMePanelSection = "status" | "workflow" | "integration" | "worktrees";
28
28
 
29
29
  const NARROW_MIN_WIDTH = 24;
30
30
  const WIDE_MIN_WIDTH = 72;
31
31
  const MAX_PANEL_WIDTH = 96;
32
32
  const MAX_PANEL_HEIGHT = 16;
33
33
  const BODY_HEIGHT = 9;
34
- const PANEL_SECTIONS: BranchMePanelSection[] = ["status", "workflow", "worktrees"];
34
+ const PANEL_SECTIONS: BranchMePanelSection[] = ["status", "workflow", "integration", "worktrees"];
35
35
  const ANSI_RESET = "\u001b[0m";
36
36
 
37
37
  function truncateVisible(value: string, width: number, ellipsis = "…", pad = false): string {
@@ -144,7 +144,7 @@ function statusDetailRow(label: string, value: string): PanelCell {
144
144
  }
145
145
 
146
146
  function workflowDetailRow(tool: string, description: string): PanelCell {
147
- return layoutCell(` ${tool.padEnd(16, " ")}-> ${sanitize(description)}`);
147
+ return layoutCell(` ${tool.padEnd(17, " ")}-> ${sanitize(description)}`);
148
148
  }
149
149
 
150
150
  function sectionTitle(section: BranchMePanelSection): string {
@@ -153,6 +153,8 @@ function sectionTitle(section: BranchMePanelSection): string {
153
153
  return "STATUS";
154
154
  case "workflow":
155
155
  return "WORKFLOW";
156
+ case "integration":
157
+ return "INTEGRATION";
156
158
  case "worktrees":
157
159
  return "WORKTREES";
158
160
  }
@@ -180,6 +182,8 @@ function sectionFooter(section: BranchMePanelSection, data: BranchMePanelData):
180
182
  return "status • current repository only • tools perform actions";
181
183
  case "workflow":
182
184
  return "workflow • inspect → change → fetch/pull/rebase → create → push → PR";
185
+ case "integration":
186
+ return "integration • local refs • clean target • conflicts auto-abort";
183
187
  case "worktrees":
184
188
  return "worktrees • create → handoff cwd → next session • remove retains branch";
185
189
  }
@@ -208,6 +212,15 @@ function sectionRows(section: BranchMePanelSection, data: BranchMePanelData): Pa
208
212
  workflowDetailRow("push_branch", "current branch"),
209
213
  workflowDetailRow("pull_request", "after push"),
210
214
  ];
215
+ case "integration":
216
+ return [
217
+ heading("INTEGRATION"),
218
+ workflowDetailRow("integrate_branch", "exact local source -> target"),
219
+ workflowDetailRow("control target", "checked out + clean"),
220
+ workflowDetailRow("remote effects", "never fetch or push"),
221
+ workflowDetailRow("conflict", "automatic verified abort"),
222
+ workflowDetailRow("semantic intent", "separate developer workflow"),
223
+ ];
211
224
  case "worktrees":
212
225
  return [
213
226
  heading("WORKTREES"),