vde-worktree 0.0.15 → 0.0.17

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/dist/index.mjs CHANGED
@@ -617,7 +617,8 @@ const hasStateDirectory = async (repoRoot) => {
617
617
  const parseLifecycle = (content) => {
618
618
  try {
619
619
  const parsed = JSON.parse(content);
620
- if (parsed.schemaVersion !== 1 || typeof parsed.branch !== "string" || typeof parsed.worktreeId !== "string" || typeof parsed.baseBranch !== "string" || typeof parsed.createdHead !== "string" || parsed.createdHead.length === 0 || typeof parsed.createdAt !== "string" || typeof parsed.updatedAt !== "string") return {
620
+ const isLastDivergedHeadValid = parsed.lastDivergedHead === null || typeof parsed.lastDivergedHead === "string" && parsed.lastDivergedHead.length > 0;
621
+ if (parsed.schemaVersion !== 2 || typeof parsed.branch !== "string" || typeof parsed.worktreeId !== "string" || typeof parsed.baseBranch !== "string" || typeof parsed.everDiverged !== "boolean" || isLastDivergedHeadValid !== true || typeof parsed.createdAt !== "string" || typeof parsed.updatedAt !== "string") return {
621
622
  valid: false,
622
623
  record: null
623
624
  };
@@ -665,15 +666,17 @@ const readWorktreeMergeLifecycle = async ({ repoRoot, branch }) => {
665
666
  };
666
667
  }
667
668
  };
668
- const upsertWorktreeMergeLifecycle = async ({ repoRoot, branch, baseBranch, createdHead }) => {
669
+ const upsertWorktreeMergeLifecycle = async ({ repoRoot, branch, baseBranch, observedDivergedHead }) => {
670
+ const normalizedObservedHead = typeof observedDivergedHead === "string" && observedDivergedHead.length > 0 ? observedDivergedHead : null;
669
671
  if (await hasStateDirectory(repoRoot) !== true) {
670
672
  const now = (/* @__PURE__ */ new Date()).toISOString();
671
673
  return {
672
- schemaVersion: 1,
674
+ schemaVersion: 2,
673
675
  branch,
674
676
  worktreeId: branchToWorktreeId(branch),
675
677
  baseBranch,
676
- createdHead,
678
+ everDiverged: normalizedObservedHead !== null,
679
+ lastDivergedHead: normalizedObservedHead,
677
680
  createdAt: now,
678
681
  updatedAt: now
679
682
  };
@@ -682,14 +685,17 @@ const upsertWorktreeMergeLifecycle = async ({ repoRoot, branch, baseBranch, crea
682
685
  repoRoot,
683
686
  branch
684
687
  });
685
- if (current.valid && current.record !== null && current.record.baseBranch === baseBranch) return current.record;
688
+ if (current.valid && current.record !== null && current.record.baseBranch === baseBranch && normalizedObservedHead === null) return current.record;
686
689
  const now = (/* @__PURE__ */ new Date()).toISOString();
690
+ const everDiverged = current.record?.everDiverged === true || normalizedObservedHead !== null;
691
+ const lastDivergedHead = normalizedObservedHead ?? current.record?.lastDivergedHead ?? null;
687
692
  const next = {
688
- schemaVersion: 1,
693
+ schemaVersion: 2,
689
694
  branch,
690
695
  worktreeId: branchToWorktreeId(branch),
691
696
  baseBranch,
692
- createdHead: current.record?.createdHead ?? createdHead,
697
+ everDiverged,
698
+ lastDivergedHead,
693
699
  createdAt: current.record?.createdAt ?? now,
694
700
  updatedAt: now
695
701
  };
@@ -699,15 +705,17 @@ const upsertWorktreeMergeLifecycle = async ({ repoRoot, branch, baseBranch, crea
699
705
  });
700
706
  return next;
701
707
  };
702
- const moveWorktreeMergeLifecycle = async ({ repoRoot, fromBranch, toBranch, baseBranch, createdHead }) => {
708
+ const moveWorktreeMergeLifecycle = async ({ repoRoot, fromBranch, toBranch, baseBranch, observedDivergedHead }) => {
709
+ const normalizedObservedHead = typeof observedDivergedHead === "string" && observedDivergedHead.length > 0 ? observedDivergedHead : null;
703
710
  if (await hasStateDirectory(repoRoot) !== true) {
704
711
  const now = (/* @__PURE__ */ new Date()).toISOString();
705
712
  return {
706
- schemaVersion: 1,
713
+ schemaVersion: 2,
707
714
  branch: toBranch,
708
715
  worktreeId: branchToWorktreeId(toBranch),
709
716
  baseBranch,
710
- createdHead,
717
+ everDiverged: normalizedObservedHead !== null,
718
+ lastDivergedHead: normalizedObservedHead,
711
719
  createdAt: now,
712
720
  updatedAt: now
713
721
  };
@@ -718,12 +726,15 @@ const moveWorktreeMergeLifecycle = async ({ repoRoot, fromBranch, toBranch, base
718
726
  });
719
727
  const targetPath = lifecycleFilePath(repoRoot, toBranch);
720
728
  const now = (/* @__PURE__ */ new Date()).toISOString();
729
+ const everDiverged = source.record?.everDiverged === true || normalizedObservedHead !== null;
730
+ const lastDivergedHead = normalizedObservedHead ?? source.record?.lastDivergedHead ?? null;
721
731
  const next = {
722
- schemaVersion: 1,
732
+ schemaVersion: 2,
723
733
  branch: toBranch,
724
734
  worktreeId: branchToWorktreeId(toBranch),
725
735
  baseBranch,
726
- createdHead: source.record?.createdHead ?? createdHead,
736
+ everDiverged,
737
+ lastDivergedHead,
727
738
  createdAt: source.record?.createdAt ?? now,
728
739
  updatedAt: now
729
740
  };
@@ -842,43 +853,68 @@ const toTargetBranches = ({ branches, baseBranch }) => {
842
853
  }
843
854
  return [...uniqueBranches];
844
855
  };
845
- const buildUnknownBranchMap = (branches) => {
846
- return new Map(branches.map((branch) => [branch, null]));
856
+ const buildUnknownPrStatusMap = (branches) => {
857
+ return new Map(branches.map((branch) => [branch, "unknown"]));
858
+ };
859
+ const parseUpdatedAtMillis = (value) => {
860
+ if (typeof value !== "string" || value.length === 0) return Number.NEGATIVE_INFINITY;
861
+ const parsed = Date.parse(value);
862
+ if (Number.isNaN(parsed)) return Number.NEGATIVE_INFINITY;
863
+ return parsed;
847
864
  };
848
- const parseMergedBranches = ({ raw, targetBranches }) => {
865
+ const toPrStatus = (record) => {
866
+ if (typeof record.mergedAt === "string" && record.mergedAt.length > 0) return "merged";
867
+ const state = typeof record.state === "string" ? record.state.toUpperCase() : "";
868
+ if (state === "MERGED") return "merged";
869
+ if (state === "OPEN") return "open";
870
+ if (state === "CLOSED") return "closed_unmerged";
871
+ return "unknown";
872
+ };
873
+ const parsePrStatusByBranch = ({ raw, targetBranches }) => {
849
874
  try {
850
875
  const parsed = JSON.parse(raw);
851
876
  if (Array.isArray(parsed) !== true) return null;
877
+ const targetBranchSet = new Set(targetBranches);
852
878
  const records = parsed;
853
- const mergedBranches = /* @__PURE__ */ new Set();
854
- for (const record of records) {
879
+ const latestByBranch = /* @__PURE__ */ new Map();
880
+ for (const [index, record] of records.entries()) {
855
881
  if (typeof record?.headRefName !== "string" || record.headRefName.length === 0) continue;
856
- if (targetBranches.has(record.headRefName) !== true) continue;
857
- if (typeof record.mergedAt !== "string" || record.mergedAt.length === 0) continue;
858
- mergedBranches.add(record.headRefName);
882
+ if (targetBranchSet.has(record.headRefName) !== true) continue;
883
+ const updatedAtMillis = parseUpdatedAtMillis(record.updatedAt);
884
+ const status = toPrStatus(record);
885
+ const current = latestByBranch.get(record.headRefName);
886
+ if (current === void 0 || updatedAtMillis > current.updatedAtMillis || updatedAtMillis === current.updatedAtMillis && index > current.index) latestByBranch.set(record.headRefName, {
887
+ updatedAtMillis,
888
+ index,
889
+ status
890
+ });
891
+ }
892
+ const result = /* @__PURE__ */ new Map();
893
+ for (const branch of targetBranches) {
894
+ const latest = latestByBranch.get(branch);
895
+ result.set(branch, latest?.status ?? "none");
859
896
  }
860
- return mergedBranches;
897
+ return result;
861
898
  } catch {
862
899
  return null;
863
900
  }
864
901
  };
865
- const resolveMergedByPrBatch = async ({ repoRoot, baseBranch, branches, enabled = true, runGh = defaultRunGh }) => {
866
- if (enabled !== true) return /* @__PURE__ */ new Map();
902
+ const resolvePrStatusByBranchBatch = async ({ repoRoot, baseBranch, branches, enabled = true, runGh = defaultRunGh }) => {
867
903
  if (baseBranch === null) return /* @__PURE__ */ new Map();
868
904
  const targetBranches = toTargetBranches({
869
905
  branches,
870
906
  baseBranch
871
907
  });
872
908
  if (targetBranches.length === 0) return /* @__PURE__ */ new Map();
909
+ if (enabled !== true) return buildUnknownPrStatusMap(targetBranches);
873
910
  try {
874
- const targetBranchSet = new Set(targetBranches);
875
911
  const result = await runGh({
876
912
  cwd: repoRoot,
877
913
  args: [
878
914
  "pr",
879
915
  "list",
880
916
  "--state",
881
- "merged",
917
+ "all",
882
918
  "--base",
883
919
  baseBranch,
884
920
  "--search",
@@ -886,21 +922,19 @@ const resolveMergedByPrBatch = async ({ repoRoot, baseBranch, branches, enabled
886
922
  "--limit",
887
923
  "1000",
888
924
  "--json",
889
- "headRefName,mergedAt"
925
+ "headRefName,state,mergedAt,updatedAt"
890
926
  ]
891
927
  });
892
- if (result.exitCode !== 0) return buildUnknownBranchMap(targetBranches);
893
- const mergedBranches = parseMergedBranches({
928
+ if (result.exitCode !== 0) return buildUnknownPrStatusMap(targetBranches);
929
+ const prStatusByBranch = parsePrStatusByBranch({
894
930
  raw: result.stdout,
895
- targetBranches: targetBranchSet
931
+ targetBranches
896
932
  });
897
- if (mergedBranches === null) return buildUnknownBranchMap(targetBranches);
898
- return new Map(targetBranches.map((branch) => {
899
- return [branch, mergedBranches.has(branch)];
900
- }));
933
+ if (prStatusByBranch === null) return buildUnknownPrStatusMap(targetBranches);
934
+ return prStatusByBranch;
901
935
  } catch (error) {
902
- if (error.code === "ENOENT") return buildUnknownBranchMap(targetBranches);
903
- return buildUnknownBranchMap(targetBranches);
936
+ if (error.code === "ENOENT") return buildUnknownPrStatusMap(targetBranches);
937
+ return buildUnknownPrStatusMap(targetBranches);
904
938
  }
905
939
  };
906
940
 
@@ -1049,7 +1083,57 @@ const resolveLockState = async ({ repoRoot, branch }) => {
1049
1083
  };
1050
1084
  }
1051
1085
  };
1052
- const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, mergedByPrByBranch }) => {
1086
+ const WORK_REFLOG_MESSAGE_PATTERN = /^(commit(?: \([^)]*\))?|cherry-pick|revert|rebase \(pick\)|merge):/;
1087
+ const resolveLifecycleFromReflog = async ({ repoRoot, branch, baseBranch }) => {
1088
+ const reflog = await runGitCommand({
1089
+ cwd: repoRoot,
1090
+ args: [
1091
+ "reflog",
1092
+ "show",
1093
+ "--format=%H%x09%gs",
1094
+ branch
1095
+ ],
1096
+ reject: false
1097
+ });
1098
+ if (reflog.exitCode !== 0) return {
1099
+ merged: null,
1100
+ divergedHead: null
1101
+ };
1102
+ let latestWorkHead = null;
1103
+ for (const line of reflog.stdout.split("\n")) {
1104
+ const trimmed = line.trim();
1105
+ if (trimmed.length === 0) continue;
1106
+ const separatorIndex = trimmed.indexOf(" ");
1107
+ if (separatorIndex <= 0) continue;
1108
+ const head = trimmed.slice(0, separatorIndex).trim();
1109
+ const message = trimmed.slice(separatorIndex + 1).trim();
1110
+ if (head.length === 0 || WORK_REFLOG_MESSAGE_PATTERN.test(message) !== true) continue;
1111
+ if (latestWorkHead === null) latestWorkHead = head;
1112
+ const result = await runGitCommand({
1113
+ cwd: repoRoot,
1114
+ args: [
1115
+ "merge-base",
1116
+ "--is-ancestor",
1117
+ head,
1118
+ baseBranch
1119
+ ],
1120
+ reject: false
1121
+ });
1122
+ if (result.exitCode === 0) return {
1123
+ merged: true,
1124
+ divergedHead: head
1125
+ };
1126
+ if (result.exitCode !== 1) return {
1127
+ merged: null,
1128
+ divergedHead: latestWorkHead
1129
+ };
1130
+ }
1131
+ return {
1132
+ merged: false,
1133
+ divergedHead: latestWorkHead
1134
+ };
1135
+ };
1136
+ const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, prStatusByBranch }) => {
1053
1137
  if (branch === null) return {
1054
1138
  byAncestry: null,
1055
1139
  byPR: null,
@@ -1070,17 +1154,49 @@ const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, mergedBy
1070
1154
  if (result.exitCode === 0) byAncestry = true;
1071
1155
  else if (result.exitCode === 1) byAncestry = false;
1072
1156
  }
1073
- const byPR = branch === baseBranch ? null : mergedByPrByBranch.get(branch) ?? null;
1157
+ const prStatus = branch === baseBranch ? null : prStatusByBranch.get(branch) ?? null;
1158
+ let byPR = null;
1159
+ if (prStatus === "merged") byPR = true;
1160
+ else if (prStatus === "none" || prStatus === "open" || prStatus === "closed_unmerged") byPR = false;
1074
1161
  let byLifecycle = null;
1075
1162
  if (baseBranch !== null) {
1076
1163
  const lifecycle = await upsertWorktreeMergeLifecycle({
1077
1164
  repoRoot,
1078
1165
  branch,
1079
1166
  baseBranch,
1080
- createdHead: head
1167
+ observedDivergedHead: byAncestry === false ? head : null
1081
1168
  });
1082
- if (byAncestry === true) byLifecycle = lifecycle.createdHead !== head;
1083
- else if (byAncestry === false) byLifecycle = false;
1169
+ if (byAncestry === false) byLifecycle = false;
1170
+ else if (byAncestry === true) if (lifecycle.everDiverged !== true || lifecycle.lastDivergedHead === null) if (byPR === true) byLifecycle = null;
1171
+ else {
1172
+ const probe = await resolveLifecycleFromReflog({
1173
+ repoRoot,
1174
+ branch,
1175
+ baseBranch
1176
+ });
1177
+ byLifecycle = probe.merged;
1178
+ if (probe.divergedHead !== null) await upsertWorktreeMergeLifecycle({
1179
+ repoRoot,
1180
+ branch,
1181
+ baseBranch,
1182
+ observedDivergedHead: probe.divergedHead
1183
+ });
1184
+ }
1185
+ else {
1186
+ const lifecycleResult = await runGitCommand({
1187
+ cwd: repoRoot,
1188
+ args: [
1189
+ "merge-base",
1190
+ "--is-ancestor",
1191
+ lifecycle.lastDivergedHead,
1192
+ baseBranch
1193
+ ],
1194
+ reject: false
1195
+ });
1196
+ if (lifecycleResult.exitCode === 0) byLifecycle = true;
1197
+ else if (lifecycleResult.exitCode === 1) byLifecycle = false;
1198
+ else byLifecycle = null;
1199
+ }
1084
1200
  }
1085
1201
  return {
1086
1202
  byAncestry,
@@ -1092,6 +1208,10 @@ const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, mergedBy
1092
1208
  })
1093
1209
  };
1094
1210
  };
1211
+ const resolvePrState = ({ branch, baseBranch, prStatusByBranch }) => {
1212
+ if (branch === null || branch === baseBranch) return { status: null };
1213
+ return { status: prStatusByBranch.get(branch) ?? null };
1214
+ };
1095
1215
  const resolveMergedOverall = ({ byAncestry, byPR, byLifecycle }) => {
1096
1216
  if (byPR === true || byLifecycle === true) return true;
1097
1217
  if (byAncestry === false) return false;
@@ -1138,7 +1258,7 @@ const resolveUpstreamState = async (worktreePath) => {
1138
1258
  remote: upstreamRef.stdout.trim()
1139
1259
  };
1140
1260
  };
1141
- const enrichWorktree = async ({ repoRoot, worktree, baseBranch, mergedByPrByBranch }) => {
1261
+ const enrichWorktree = async ({ repoRoot, worktree, baseBranch, prStatusByBranch }) => {
1142
1262
  const [dirty, locked, merged, upstream] = await Promise.all([
1143
1263
  resolveDirty(worktree.path),
1144
1264
  resolveLockState({
@@ -1150,10 +1270,15 @@ const enrichWorktree = async ({ repoRoot, worktree, baseBranch, mergedByPrByBran
1150
1270
  branch: worktree.branch,
1151
1271
  head: worktree.head,
1152
1272
  baseBranch,
1153
- mergedByPrByBranch
1273
+ prStatusByBranch
1154
1274
  }),
1155
1275
  resolveUpstreamState(worktree.path)
1156
1276
  ]);
1277
+ const pr = resolvePrState({
1278
+ branch: worktree.branch,
1279
+ baseBranch,
1280
+ prStatusByBranch
1281
+ });
1157
1282
  return {
1158
1283
  branch: worktree.branch,
1159
1284
  path: worktree.path,
@@ -1161,6 +1286,7 @@ const enrichWorktree = async ({ repoRoot, worktree, baseBranch, mergedByPrByBran
1161
1286
  dirty,
1162
1287
  locked,
1163
1288
  merged,
1289
+ pr,
1164
1290
  upstream
1165
1291
  };
1166
1292
  };
@@ -1170,7 +1296,7 @@ const collectWorktreeSnapshot = async (repoRoot, { noGh = false } = {}) => {
1170
1296
  listGitWorktrees(repoRoot),
1171
1297
  resolveEnableGh(repoRoot)
1172
1298
  ]);
1173
- const mergedByPrByBranch = await resolveMergedByPrBatch({
1299
+ const prStatusByBranch = await resolvePrStatusByBranchBatch({
1174
1300
  repoRoot,
1175
1301
  baseBranch,
1176
1302
  branches: worktrees.map((worktree) => worktree.branch),
@@ -1184,7 +1310,7 @@ const collectWorktreeSnapshot = async (repoRoot, { noGh = false } = {}) => {
1184
1310
  repoRoot,
1185
1311
  worktree,
1186
1312
  baseBranch,
1187
- mergedByPrByBranch
1313
+ prStatusByBranch
1188
1314
  });
1189
1315
  }))
1190
1316
  };
@@ -1440,9 +1566,9 @@ const colorizeListTableLine = ({ line, theme }) => {
1440
1566
  const segments = line.split("│");
1441
1567
  if (segments.length < 3) return line;
1442
1568
  const cells = segments.slice(1, -1);
1443
- if (cells.length !== 7) return line;
1569
+ if (cells.length !== 8) return line;
1444
1570
  const headers = cells.map((cell) => cell.trim());
1445
- if (headers[0] === "branch" && headers[1] === "dirty" && headers[2] === "merged" && headers[3] === "locked" && headers[4] === "ahead" && headers[5] === "behind" && headers[6] === "path") {
1571
+ if (headers[0] === "branch" && headers[1] === "dirty" && headers[2] === "merged" && headers[3] === "pr" && headers[4] === "locked" && headers[5] === "ahead" && headers[6] === "behind" && headers[7] === "path") {
1446
1572
  const nextCells = cells.map((cell) => colorizeCellContent({
1447
1573
  cell,
1448
1574
  color: theme.header
@@ -1456,15 +1582,18 @@ const colorizeListTableLine = ({ line, theme }) => {
1456
1582
  const branchCell = cells[0];
1457
1583
  const dirtyCell = cells[1];
1458
1584
  const mergedCell = cells[2];
1459
- const lockedCell = cells[3];
1460
- const aheadCell = cells[4];
1461
- const behindCell = cells[5];
1462
- const pathCell = cells[6];
1585
+ const prCell = cells[3];
1586
+ const lockedCell = cells[4];
1587
+ const aheadCell = cells[5];
1588
+ const behindCell = cells[6];
1589
+ const pathCell = cells[7];
1463
1590
  const branchColor = branchCell.includes("(detached)") === true ? theme.branchDetached : branchCell.trimStart().startsWith("*") ? theme.branchCurrent : theme.branch;
1464
1591
  const dirtyTrimmed = dirtyCell.trim();
1465
1592
  const dirtyColor = dirtyTrimmed === "dirty" ? theme.dirty : dirtyTrimmed === "clean" ? theme.clean : theme.value;
1466
1593
  const mergedTrimmed = mergedCell.trim();
1467
1594
  const mergedColor = mergedTrimmed === "merged" ? theme.merged : mergedTrimmed === "unmerged" ? theme.unmerged : mergedTrimmed === "-" ? theme.base : theme.unknown;
1595
+ const prTrimmed = prCell.trim();
1596
+ const prColor = prTrimmed === "merged" ? theme.merged : prTrimmed === "open" ? theme.value : prTrimmed === "closed_unmerged" ? theme.unmerged : prTrimmed === "none" ? theme.muted : prTrimmed === "-" ? theme.base : theme.unknown;
1468
1597
  const lockedColor = lockedCell.trim() === "locked" ? theme.locked : theme.muted;
1469
1598
  const aheadTrimmed = aheadCell.trim();
1470
1599
  const aheadValue = Number.parseInt(aheadTrimmed, 10);
@@ -1485,6 +1614,10 @@ const colorizeListTableLine = ({ line, theme }) => {
1485
1614
  cell: mergedCell,
1486
1615
  color: mergedColor
1487
1616
  }),
1617
+ colorizeCellContent({
1618
+ cell: prCell,
1619
+ color: prColor
1620
+ }),
1488
1621
  colorizeCellContent({
1489
1622
  cell: lockedCell,
1490
1623
  color: lockedColor
@@ -1525,7 +1658,7 @@ const commandHelpEntries = [
1525
1658
  name: "list",
1526
1659
  usage: "vw list [--json]",
1527
1660
  summary: "List worktrees with status metadata.",
1528
- details: ["Table output includes branch, path, dirty, lock, merged, and ahead/behind vs base branch.", "JSON output includes upstream metadata fields."]
1661
+ details: ["Table output includes branch, path, dirty, lock, merged, PR state, and ahead/behind vs base branch.", "JSON output includes PR and upstream metadata fields."]
1529
1662
  },
1530
1663
  {
1531
1664
  name: "status",
@@ -1897,22 +2030,6 @@ const resolveBaseBranch = async (repoRoot) => {
1897
2030
  for (const candidate of ["main", "master"]) if (await doesGitRefExist(repoRoot, `refs/heads/${candidate}`)) return candidate;
1898
2031
  throw createCliError("INVALID_ARGUMENT", { message: "Unable to resolve base branch. Configure vde-worktree.baseBranch." });
1899
2032
  };
1900
- const resolveBranchHead = async ({ repoRoot, branch }) => {
1901
- const resolved = await runGitCommand({
1902
- cwd: repoRoot,
1903
- args: [
1904
- "rev-parse",
1905
- "--verify",
1906
- branch
1907
- ],
1908
- reject: false
1909
- });
1910
- if (resolved.exitCode !== 0 || resolved.stdout.trim().length === 0) throw createCliError("INVALID_ARGUMENT", {
1911
- message: `Failed to resolve branch head: ${branch}`,
1912
- details: { branch }
1913
- });
1914
- return resolved.stdout.trim();
1915
- };
1916
2033
  const ensureTargetPathWritable = async (targetPath) => {
1917
2034
  try {
1918
2035
  await access(targetPath, constants.F_OK);
@@ -2358,6 +2475,11 @@ const formatMergedColor = ({ mergedState, theme }) => {
2358
2475
  if (normalized === "base") return theme.base(mergedState);
2359
2476
  return theme.unknown(mergedState);
2360
2477
  };
2478
+ const formatPrDisplayState = ({ prStatus, isBaseBranch }) => {
2479
+ if (isBaseBranch || prStatus === null) return "-";
2480
+ if (prStatus === "none" || prStatus === "open" || prStatus === "merged" || prStatus === "closed_unmerged" || prStatus === "unknown") return prStatus;
2481
+ return "unknown";
2482
+ };
2361
2483
  const formatListUpstreamCount = (value) => {
2362
2484
  if (value === null) return "-";
2363
2485
  return String(value);
@@ -2519,7 +2641,7 @@ const renderGeneralHelpText = ({ version }) => {
2519
2641
  " --json Output machine-readable JSON.",
2520
2642
  " --verbose Enable verbose logs.",
2521
2643
  " --no-hooks Disable hooks for this run (requires --allow-unsafe).",
2522
- " --no-gh Disable GitHub CLI based PR merge checks for this run.",
2644
+ " --no-gh Disable GitHub CLI based PR status checks for this run.",
2523
2645
  " --allow-unsafe Explicitly allow unsafe behavior in non-TTY mode.",
2524
2646
  " --hook-timeout-ms <ms> Override hook timeout.",
2525
2647
  " --lock-timeout-ms <ms> Override repository lock timeout.",
@@ -2622,7 +2744,7 @@ const createCli = (options = {}) => {
2622
2744
  },
2623
2745
  gh: {
2624
2746
  type: "boolean",
2625
- description: "Enable GitHub CLI based PR merge checks (disable with --no-gh)",
2747
+ description: "Enable GitHub CLI based PR status checks (disable with --no-gh)",
2626
2748
  default: true
2627
2749
  },
2628
2750
  allowUnsafe: {
@@ -2956,6 +3078,7 @@ const createCli = (options = {}) => {
2956
3078
  "branch",
2957
3079
  "dirty",
2958
3080
  "merged",
3081
+ "pr",
2959
3082
  "locked",
2960
3083
  "ahead",
2961
3084
  "behind",
@@ -2966,11 +3089,17 @@ const createCli = (options = {}) => {
2966
3089
  baseBranch: snapshot.baseBranch,
2967
3090
  worktree
2968
3091
  });
2969
- const mergedState = (worktree.branch !== null && snapshot.baseBranch !== null && worktree.branch === snapshot.baseBranch) === true ? "-" : worktree.merged.overall === true ? "merged" : worktree.merged.overall === false ? "unmerged" : "unknown";
3092
+ const isBaseBranch = worktree.branch !== null && snapshot.baseBranch !== null && worktree.branch === snapshot.baseBranch;
3093
+ const mergedState = isBaseBranch === true ? "-" : worktree.merged.overall === true ? "merged" : worktree.merged.overall === false ? "unmerged" : "unknown";
3094
+ const prState = formatPrDisplayState({
3095
+ prStatus: worktree.pr.status,
3096
+ isBaseBranch
3097
+ });
2970
3098
  return [
2971
3099
  `${worktree.path === repoContext.currentWorktreeRoot ? "*" : " "} ${worktree.branch ?? "(detached)"}`,
2972
3100
  worktree.dirty ? "dirty" : "clean",
2973
3101
  mergedState,
3102
+ prState,
2974
3103
  worktree.locked.value ? "locked" : "-",
2975
3104
  formatListUpstreamCount(distanceFromBase.ahead),
2976
3105
  formatListUpstreamCount(distanceFromBase.behind),
@@ -3095,10 +3224,7 @@ const createCli = (options = {}) => {
3095
3224
  repoRoot,
3096
3225
  branch,
3097
3226
  baseBranch,
3098
- createdHead: await resolveBranchHead({
3099
- repoRoot,
3100
- branch
3101
- })
3227
+ observedDivergedHead: null
3102
3228
  });
3103
3229
  await runPostHook({
3104
3230
  name: "new",
@@ -3133,18 +3259,12 @@ const createCli = (options = {}) => {
3133
3259
  const snapshot = await collectWorktreeSnapshot$1(repoRoot);
3134
3260
  const existing = snapshot.worktrees.find((worktree) => worktree.branch === branch);
3135
3261
  if (existing !== void 0) {
3136
- if (snapshot.baseBranch !== null) {
3137
- const branchHead = await resolveBranchHead({
3138
- repoRoot,
3139
- branch
3140
- });
3141
- await upsertWorktreeMergeLifecycle({
3142
- repoRoot,
3143
- branch,
3144
- baseBranch: snapshot.baseBranch,
3145
- createdHead: branchHead
3146
- });
3147
- }
3262
+ if (snapshot.baseBranch !== null) await upsertWorktreeMergeLifecycle({
3263
+ repoRoot,
3264
+ branch,
3265
+ baseBranch: snapshot.baseBranch,
3266
+ observedDivergedHead: null
3267
+ });
3148
3268
  return {
3149
3269
  status: "existing",
3150
3270
  branch,
@@ -3190,18 +3310,12 @@ const createCli = (options = {}) => {
3190
3310
  ]
3191
3311
  });
3192
3312
  }
3193
- if (lifecycleBaseBranch !== null) {
3194
- const branchHead = await resolveBranchHead({
3195
- repoRoot,
3196
- branch
3197
- });
3198
- await upsertWorktreeMergeLifecycle({
3199
- repoRoot,
3200
- branch,
3201
- baseBranch: lifecycleBaseBranch,
3202
- createdHead: branchHead
3203
- });
3204
- }
3313
+ if (lifecycleBaseBranch !== null) await upsertWorktreeMergeLifecycle({
3314
+ repoRoot,
3315
+ branch,
3316
+ baseBranch: lifecycleBaseBranch,
3317
+ observedDivergedHead: null
3318
+ });
3205
3319
  await runPostHook({
3206
3320
  name: "switch",
3207
3321
  context: hookContext
@@ -3301,19 +3415,13 @@ const createCli = (options = {}) => {
3301
3415
  newPath
3302
3416
  ]
3303
3417
  });
3304
- if (snapshot.baseBranch !== null) {
3305
- const branchHead = await resolveBranchHead({
3306
- repoRoot,
3307
- branch: newBranch
3308
- });
3309
- await moveWorktreeMergeLifecycle({
3310
- repoRoot,
3311
- fromBranch: oldBranch,
3312
- toBranch: newBranch,
3313
- baseBranch: snapshot.baseBranch,
3314
- createdHead: branchHead
3315
- });
3316
- }
3418
+ if (snapshot.baseBranch !== null) await moveWorktreeMergeLifecycle({
3419
+ repoRoot,
3420
+ fromBranch: oldBranch,
3421
+ toBranch: newBranch,
3422
+ baseBranch: snapshot.baseBranch,
3423
+ observedDivergedHead: null
3424
+ });
3317
3425
  await runPostHook({
3318
3426
  name: "mv",
3319
3427
  context: hookContext
@@ -3582,10 +3690,7 @@ const createCli = (options = {}) => {
3582
3690
  repoRoot,
3583
3691
  branch,
3584
3692
  baseBranch: lifecycleBaseBranch,
3585
- createdHead: await resolveBranchHead({
3586
- repoRoot,
3587
- branch
3588
- })
3693
+ observedDivergedHead: null
3589
3694
  });
3590
3695
  await runPostHook({
3591
3696
  name: "get",
@@ -3612,10 +3717,7 @@ const createCli = (options = {}) => {
3612
3717
  repoRoot,
3613
3718
  branch,
3614
3719
  baseBranch: lifecycleBaseBranch,
3615
- createdHead: await resolveBranchHead({
3616
- repoRoot,
3617
- branch
3618
- })
3720
+ observedDivergedHead: null
3619
3721
  });
3620
3722
  await runPostHook({
3621
3723
  name: "get",
@@ -3727,10 +3829,7 @@ const createCli = (options = {}) => {
3727
3829
  repoRoot,
3728
3830
  branch,
3729
3831
  baseBranch,
3730
- createdHead: await resolveBranchHead({
3731
- repoRoot,
3732
- branch
3733
- })
3832
+ observedDivergedHead: null
3734
3833
  });
3735
3834
  if (stashOid !== null) {
3736
3835
  if ((await runGitCommand({