clawt 3.10.4 → 3.10.6

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.
Files changed (48) hide show
  1. package/AGENTS.md +16 -0
  2. package/dist/index.js +228 -86
  3. package/dist/postinstall.js +27 -0
  4. package/docs/create.md +1 -0
  5. package/docs/list.md +21 -10
  6. package/docs/merge.md +1 -0
  7. package/docs/remove.md +2 -0
  8. package/docs/spec.md +4 -1
  9. package/docs/status.md +9 -1
  10. package/docs/superpowers/findings/2026-06-01-sync-validate-diverged-findings.md +203 -0
  11. package/docs/superpowers/findings/2026-06-09-worktree-base-branch-findings.md +58 -0
  12. package/docs/superpowers/plans/2026-06-01-validate-ignored-files-conflict.md +412 -0
  13. package/docs/superpowers/plans/2026-06-09-worktree-base-branch.md +386 -0
  14. package/docs/superpowers/specs/2026-06-01-validate-ignored-files-conflict-design.md +76 -0
  15. package/docs/superpowers/specs/2026-06-09-worktree-base-branch-design.md +169 -0
  16. package/docs/validate.md +42 -5
  17. package/package.json +1 -1
  18. package/src/commands/list.ts +5 -3
  19. package/src/commands/merge.ts +1 -1
  20. package/src/commands/remove.ts +3 -0
  21. package/src/commands/status.ts +5 -0
  22. package/src/constants/messages/validate.ts +17 -0
  23. package/src/types/status.ts +2 -0
  24. package/src/types/worktree.ts +12 -0
  25. package/src/utils/formatter.ts +22 -0
  26. package/src/utils/git-core.ts +23 -0
  27. package/src/utils/index.ts +4 -2
  28. package/src/utils/interactive-panel-render.ts +6 -3
  29. package/src/utils/validate-core.ts +52 -0
  30. package/src/utils/worktree-metadata.ts +82 -0
  31. package/src/utils/worktree.ts +29 -10
  32. package/tests/helpers/fixtures.ts +1 -0
  33. package/tests/unit/commands/cover-validate.test.ts +4 -4
  34. package/tests/unit/commands/create.test.ts +3 -3
  35. package/tests/unit/commands/list.test.ts +66 -3
  36. package/tests/unit/commands/merge.test.ts +1 -1
  37. package/tests/unit/commands/remove.test.ts +24 -18
  38. package/tests/unit/commands/resume.test.ts +21 -21
  39. package/tests/unit/commands/run.test.ts +17 -17
  40. package/tests/unit/commands/status.test.ts +85 -10
  41. package/tests/unit/commands/sync.test.ts +4 -4
  42. package/tests/unit/commands/validate.test.ts +1 -1
  43. package/tests/unit/utils/git-core.test.ts +43 -0
  44. package/tests/unit/utils/interactive-panel-render.test.ts +124 -0
  45. package/tests/unit/utils/validate-core.test.ts +60 -0
  46. package/tests/unit/utils/worktree-matcher.test.ts +2 -2
  47. package/tests/unit/utils/worktree-metadata.test.ts +91 -0
  48. package/tests/unit/utils/worktree.test.ts +65 -0
@@ -45,8 +45,8 @@ vi.mock('../../../src/utils/index.js', () => ({
45
45
  getProjectName: vi.fn().mockReturnValue('test-project'),
46
46
  getGitTopLevel: vi.fn().mockReturnValue('/repo'),
47
47
  getCurrentBranch: vi.fn().mockReturnValue('clawt-validate-feature'),
48
- getProjectWorktrees: vi.fn().mockReturnValue([{ path: '/path/feature', branch: 'feature' }]),
49
- findExactMatch: vi.fn().mockReturnValue({ path: '/path/feature', branch: 'feature' }),
48
+ getProjectWorktrees: vi.fn().mockReturnValue([{ path: '/path/feature', branch: 'feature', baseBranch: null }]),
49
+ findExactMatch: vi.fn().mockReturnValue({ path: '/path/feature', branch: 'feature', baseBranch: null }),
50
50
  hasSnapshot: vi.fn().mockReturnValue(true),
51
51
  readSnapshot: vi.fn().mockReturnValue({ treeHash: 'snapshot-tree-hash', headCommitHash: '', stagedTreeHash: '' }),
52
52
  writeSnapshot: vi.fn(),
@@ -103,8 +103,8 @@ beforeEach(() => {
103
103
  vi.clearAllMocks();
104
104
  // 恢复默认 mock 值
105
105
  mockedGetCurrentBranch.mockReturnValue('clawt-validate-feature');
106
- mockedGetProjectWorktrees.mockReturnValue([{ path: '/path/feature', branch: 'feature' }]);
107
- mockedFindExactMatch.mockReturnValue({ path: '/path/feature', branch: 'feature' });
106
+ mockedGetProjectWorktrees.mockReturnValue([{ path: '/path/feature', branch: 'feature', baseBranch: null }]);
107
+ mockedFindExactMatch.mockReturnValue({ path: '/path/feature', branch: 'feature', baseBranch: null });
108
108
  mockedHasSnapshot.mockReturnValue(true);
109
109
  mockedReadSnapshot.mockReturnValue({ treeHash: 'snapshot-tree-hash', headCommitHash: '', stagedTreeHash: '' });
110
110
  mockedIsWorkingDirClean.mockReturnValue(false);
@@ -67,7 +67,7 @@ describe('registerCreateCommand', () => {
67
67
  describe('handleCreate', () => {
68
68
  it('成功创建 worktree', async () => {
69
69
  mockedCreateWorktrees.mockReturnValue([
70
- { path: '/path/feature', branch: 'feature' },
70
+ { path: '/path/feature', branch: 'feature', baseBranch: null },
71
71
  ]);
72
72
 
73
73
  const program = new Command();
@@ -82,8 +82,8 @@ describe('handleCreate', () => {
82
82
 
83
83
  it('支持 -n 指定创建数量', async () => {
84
84
  mockedCreateWorktrees.mockReturnValue([
85
- { path: '/path/feature-1', branch: 'feature-1' },
86
- { path: '/path/feature-2', branch: 'feature-2' },
85
+ { path: '/path/feature-1', branch: 'feature-1', baseBranch: null },
86
+ { path: '/path/feature-2', branch: 'feature-2', baseBranch: null },
87
87
  ]);
88
88
 
89
89
  const program = new Command();
@@ -24,8 +24,17 @@ vi.mock('../../../src/utils/index.js', () => ({
24
24
  formatWorktreeStatus: vi.fn(),
25
25
  isWorktreeIdle: vi.fn(),
26
26
  printInfo: vi.fn(),
27
+ formatBaseBranchInline: vi.fn((baseBranch: string | null | undefined) => `<- ${baseBranch ?? '未记录'}`),
27
28
  }));
28
29
 
30
+ vi.mock('../../../src/utils/i18n.js', async (importOriginal) => {
31
+ const actual = await importOriginal<typeof import('../../../src/utils/i18n.js')>();
32
+ return {
33
+ ...actual,
34
+ getCurrentLanguage: vi.fn(() => 'zh'),
35
+ };
36
+ });
37
+
29
38
  import { registerListCommand } from '../../../src/commands/list.js';
30
39
  import { runPreChecks, getProjectName, getProjectWorktrees, getWorktreeStatus, printInfo } from '../../../src/utils/index.js';
31
40
 
@@ -69,7 +78,7 @@ describe('handleList', () => {
69
78
  it('有 worktree 时文本输出', async () => {
70
79
  mockedGetProjectName.mockReturnValue('test-project');
71
80
  mockedGetProjectWorktrees.mockReturnValue([
72
- { path: '/path/feature', branch: 'feature' },
81
+ { path: '/path/feature', branch: 'feature', baseBranch: null },
73
82
  ]);
74
83
  mockedGetWorktreeStatus.mockReturnValue({
75
84
  commitCount: 3, insertions: 10, deletions: 5, hasDirtyFiles: false,
@@ -86,7 +95,7 @@ describe('handleList', () => {
86
95
  it('--json 输出 JSON 格式', async () => {
87
96
  mockedGetProjectName.mockReturnValue('test-project');
88
97
  mockedGetProjectWorktrees.mockReturnValue([
89
- { path: '/path/feature', branch: 'feature' },
98
+ { path: '/path/feature', branch: 'feature', baseBranch: null },
90
99
  ]);
91
100
  const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
92
101
 
@@ -108,7 +117,7 @@ describe('handleList', () => {
108
117
  it('worktree 状态不可用时显示提示', async () => {
109
118
  mockedGetProjectName.mockReturnValue('test-project');
110
119
  mockedGetProjectWorktrees.mockReturnValue([
111
- { path: '/path/feature', branch: 'feature' },
120
+ { path: '/path/feature', branch: 'feature', baseBranch: null },
112
121
  ]);
113
122
  mockedGetWorktreeStatus.mockReturnValue(null);
114
123
 
@@ -119,4 +128,58 @@ describe('handleList', () => {
119
128
 
120
129
  expect(mockedGetWorktreeStatus).toHaveBeenCalled();
121
130
  });
131
+
132
+ it('--json 输出包含 baseBranch 字段', async () => {
133
+ mockedGetProjectName.mockReturnValue('test-project');
134
+ mockedGetProjectWorktrees.mockReturnValue([
135
+ { path: '/path/feature', branch: 'feature', baseBranch: 'test' },
136
+ ]);
137
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
138
+
139
+ const program = new Command();
140
+ program.exitOverride();
141
+ registerListCommand(program);
142
+ await program.parseAsync(['list', '--json'], { from: 'user' });
143
+
144
+ const jsonCall = consoleSpy.mock.calls.find((call) => {
145
+ try { JSON.parse(call[0]); return true; } catch { return false; }
146
+ });
147
+ expect(jsonCall).toBeDefined();
148
+ const parsed = JSON.parse(jsonCall![0]);
149
+ expect(parsed.worktrees[0].baseBranch).toBe('test');
150
+ });
151
+
152
+ it('文本输出包含来源分支(有元数据时)', async () => {
153
+ mockedGetProjectName.mockReturnValue('test-project');
154
+ mockedGetProjectWorktrees.mockReturnValue([
155
+ { path: '/path/feature', branch: 'feature', baseBranch: 'test' },
156
+ ]);
157
+ mockedGetWorktreeStatus.mockReturnValue({
158
+ commitCount: 3, insertions: 10, deletions: 5, hasDirtyFiles: false,
159
+ });
160
+
161
+ const program = new Command();
162
+ program.exitOverride();
163
+ registerListCommand(program);
164
+ await program.parseAsync(['list'], { from: 'user' });
165
+
166
+ expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('<- test'));
167
+ });
168
+
169
+ it('文本输出包含"未记录"(无元数据时)', async () => {
170
+ mockedGetProjectName.mockReturnValue('test-project');
171
+ mockedGetProjectWorktrees.mockReturnValue([
172
+ { path: '/path/feature', branch: 'feature', baseBranch: null },
173
+ ]);
174
+ mockedGetWorktreeStatus.mockReturnValue({
175
+ commitCount: 3, insertions: 10, deletions: 5, hasDirtyFiles: false,
176
+ });
177
+
178
+ const program = new Command();
179
+ program.exitOverride();
180
+ registerListCommand(program);
181
+ await program.parseAsync(['list'], { from: 'user' });
182
+
183
+ expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('未记录'));
184
+ });
122
185
  });
@@ -149,7 +149,7 @@ const mockedHandleMergeConflict = vi.mocked(handleMergeConflict);
149
149
  const mockedPromptCommitMessage = vi.mocked(promptCommitMessage);
150
150
  const mockedIsNonInteractive = vi.mocked(isNonInteractive);
151
151
 
152
- const worktree = { path: '/path/feature', branch: 'feature' };
152
+ const worktree = { path: '/path/feature', branch: 'feature', baseBranch: null };
153
153
 
154
154
  beforeEach(() => {
155
155
  mockedGetGitTopLevel.mockReturnValue('/repo');
@@ -57,6 +57,7 @@ vi.mock('../../../src/utils/index.js', () => ({
57
57
  guardMainWorkBranch: vi.fn().mockResolvedValue(undefined),
58
58
  guardMainWorkBranchExists: vi.fn(),
59
59
  isNonInteractive: vi.fn().mockReturnValue(false),
60
+ removeWorktreeMetadata: vi.fn(),
60
61
  }));
61
62
 
62
63
  import { registerRemoveCommand } from '../../../src/commands/remove.js';
@@ -75,6 +76,7 @@ import {
75
76
  printHint,
76
77
  resolveTargetWorktrees,
77
78
  getCurrentBranch,
79
+ removeWorktreeMetadata,
78
80
  } from '../../../src/utils/index.js';
79
81
 
80
82
  const mockedGetProjectName = vi.mocked(getProjectName);
@@ -90,6 +92,7 @@ const mockedPrintError = vi.mocked(printError);
90
92
  const mockedPrintHint = vi.mocked(printHint);
91
93
  const mockedResolveTargetWorktrees = vi.mocked(resolveTargetWorktrees);
92
94
  const mockedGetCurrentBranch = vi.mocked(getCurrentBranch);
95
+ const mockedRemoveWorktreeMetadata = vi.mocked(removeWorktreeMetadata);
93
96
 
94
97
  beforeEach(() => {
95
98
  vi.mocked(runPreChecks).mockReset();
@@ -107,6 +110,7 @@ beforeEach(() => {
107
110
  mockedResolveTargetWorktrees.mockReset();
108
111
  mockedGetCurrentBranch.mockReset();
109
112
  mockedGetCurrentBranch.mockReturnValue('main');
113
+ mockedRemoveWorktreeMetadata.mockReset();
110
114
  });
111
115
 
112
116
  describe('registerRemoveCommand', () => {
@@ -121,8 +125,8 @@ describe('registerRemoveCommand', () => {
121
125
  describe('handleRemove', () => {
122
126
  it('--all 移除所有 worktree(autoDeleteBranch=true)', async () => {
123
127
  mockedGetProjectWorktrees.mockReturnValue([
124
- { path: '/path/feature-1', branch: 'feature-1' },
125
- { path: '/path/feature-2', branch: 'feature-2' },
128
+ { path: '/path/feature-1', branch: 'feature-1', baseBranch: null },
129
+ { path: '/path/feature-2', branch: 'feature-2', baseBranch: null },
126
130
  ]);
127
131
  mockedGetConfigValue.mockReturnValue(true);
128
132
 
@@ -133,16 +137,18 @@ describe('handleRemove', () => {
133
137
 
134
138
  expect(mockedRemoveWorktreeByPath).toHaveBeenCalledTimes(2);
135
139
  expect(mockedDeleteBranch).toHaveBeenCalledTimes(2);
140
+ expect(mockedRemoveWorktreeMetadata).toHaveBeenCalledWith('test-project', 'feature-1');
141
+ expect(mockedRemoveWorktreeMetadata).toHaveBeenCalledWith('test-project', 'feature-2');
136
142
  expect(mockedRemoveProjectSnapshots).toHaveBeenCalledWith('test-project');
137
143
  });
138
144
 
139
145
  it('-b 精确匹配时通过 resolveTargetWorktrees 解析并移除', async () => {
140
146
  mockedGetProjectWorktrees.mockReturnValue([
141
- { path: '/path/feature', branch: 'feature' },
142
- { path: '/path/other', branch: 'other' },
147
+ { path: '/path/feature', branch: 'feature', baseBranch: null },
148
+ { path: '/path/other', branch: 'other', baseBranch: null },
143
149
  ]);
144
150
  mockedResolveTargetWorktrees.mockResolvedValue([
145
- { path: '/path/feature', branch: 'feature' },
151
+ { path: '/path/feature', branch: 'feature', baseBranch: null },
146
152
  ]);
147
153
  mockedGetConfigValue.mockReturnValue(true);
148
154
 
@@ -158,14 +164,14 @@ describe('handleRemove', () => {
158
164
 
159
165
  it('-b 模糊匹配多个时通过 resolveTargetWorktrees 解析并批量移除', async () => {
160
166
  mockedGetProjectWorktrees.mockReturnValue([
161
- { path: '/path/feature-1', branch: 'feature-1' },
162
- { path: '/path/feature-2', branch: 'feature-2' },
163
- { path: '/path/other', branch: 'other' },
167
+ { path: '/path/feature-1', branch: 'feature-1', baseBranch: null },
168
+ { path: '/path/feature-2', branch: 'feature-2', baseBranch: null },
169
+ { path: '/path/other', branch: 'other', baseBranch: null },
164
170
  ]);
165
171
  // 模拟用户多选了两个
166
172
  mockedResolveTargetWorktrees.mockResolvedValue([
167
- { path: '/path/feature-1', branch: 'feature-1' },
168
- { path: '/path/feature-2', branch: 'feature-2' },
173
+ { path: '/path/feature-1', branch: 'feature-1', baseBranch: null },
174
+ { path: '/path/feature-2', branch: 'feature-2', baseBranch: null },
169
175
  ]);
170
176
  mockedGetConfigValue.mockReturnValue(true);
171
177
 
@@ -179,11 +185,11 @@ describe('handleRemove', () => {
179
185
 
180
186
  it('未指定 --all 或 -b 时通过 resolveTargetWorktrees 展示多选列表', async () => {
181
187
  mockedGetProjectWorktrees.mockReturnValue([
182
- { path: '/path/feature-1', branch: 'feature-1' },
183
- { path: '/path/feature-2', branch: 'feature-2' },
188
+ { path: '/path/feature-1', branch: 'feature-1', baseBranch: null },
189
+ { path: '/path/feature-2', branch: 'feature-2', baseBranch: null },
184
190
  ]);
185
191
  mockedResolveTargetWorktrees.mockResolvedValue([
186
- { path: '/path/feature-1', branch: 'feature-1' },
192
+ { path: '/path/feature-1', branch: 'feature-1', baseBranch: null },
187
193
  ]);
188
194
  mockedGetConfigValue.mockReturnValue(true);
189
195
 
@@ -203,10 +209,10 @@ describe('handleRemove', () => {
203
209
 
204
210
  it('autoDeleteBranch=false 时询问用户是否删除分支', async () => {
205
211
  mockedGetProjectWorktrees.mockReturnValue([
206
- { path: '/path/feature', branch: 'feature' },
212
+ { path: '/path/feature', branch: 'feature', baseBranch: null },
207
213
  ]);
208
214
  mockedResolveTargetWorktrees.mockResolvedValue([
209
- { path: '/path/feature', branch: 'feature' },
215
+ { path: '/path/feature', branch: 'feature', baseBranch: null },
210
216
  ]);
211
217
  mockedGetConfigValue.mockReturnValue(false);
212
218
  mockedConfirmAction.mockResolvedValue(false);
@@ -225,7 +231,7 @@ describe('handleRemove', () => {
225
231
 
226
232
  it('-b 指定不存在的分支时 resolveTargetWorktrees 抛出错误', async () => {
227
233
  mockedGetProjectWorktrees.mockReturnValue([
228
- { path: '/path/other', branch: 'other' },
234
+ { path: '/path/other', branch: 'other', baseBranch: null },
229
235
  ]);
230
236
  mockedResolveTargetWorktrees.mockRejectedValue(new Error('未找到与 "nonexistent" 匹配的分支'));
231
237
 
@@ -240,8 +246,8 @@ describe('handleRemove', () => {
240
246
 
241
247
  it('移除过程中部分失败时汇报并抛出错误', async () => {
242
248
  mockedGetProjectWorktrees.mockReturnValue([
243
- { path: '/path/feature-1', branch: 'feature-1' },
244
- { path: '/path/feature-2', branch: 'feature-2' },
249
+ { path: '/path/feature-1', branch: 'feature-1', baseBranch: null },
250
+ { path: '/path/feature-2', branch: 'feature-2', baseBranch: null },
245
251
  ]);
246
252
  mockedGetConfigValue.mockReturnValue(true);
247
253
  // 第一个成功,第二个失败
@@ -125,7 +125,7 @@ describe('registerResumeCommand', () => {
125
125
 
126
126
  describe('handleResume', () => {
127
127
  it('传 -b 时走标准解析流程', async () => {
128
- const worktree = { path: '/path/feature', branch: 'feature' };
128
+ const worktree = { path: '/path/feature', branch: 'feature', baseBranch: null };
129
129
  mockedGetProjectWorktrees.mockReturnValue([worktree]);
130
130
  mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
131
131
  mockedGetConfigValue.mockReturnValue(true);
@@ -143,8 +143,8 @@ describe('handleResume', () => {
143
143
 
144
144
  it('不传 -b 且多个 worktree 时默认使用分组多选', async () => {
145
145
  const worktrees = [
146
- { path: '/path/feature-a', branch: 'feature-a' },
147
- { path: '/path/feature-b', branch: 'feature-b' },
146
+ { path: '/path/feature-a', branch: 'feature-a', baseBranch: null },
147
+ { path: '/path/feature-b', branch: 'feature-b', baseBranch: null },
148
148
  ];
149
149
  mockedGetProjectWorktrees.mockReturnValue(worktrees);
150
150
  mockedPromptGroupedMultiSelectBranches.mockResolvedValue([worktrees[0]]);
@@ -163,7 +163,7 @@ describe('handleResume', () => {
163
163
  });
164
164
 
165
165
  it('仅 1 个 worktree 时走标准流程', async () => {
166
- const worktree = { path: '/path/feature', branch: 'feature' };
166
+ const worktree = { path: '/path/feature', branch: 'feature', baseBranch: null };
167
167
  mockedGetProjectWorktrees.mockReturnValue([worktree]);
168
168
  mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
169
169
  mockedGetConfigValue.mockReturnValue(true);
@@ -180,7 +180,7 @@ describe('handleResume', () => {
180
180
 
181
181
  describe('handleResume — resumeInPlace 配置', () => {
182
182
  it('resumeInPlace 为 true 时,单选在当前终端就地恢复', async () => {
183
- const worktree = { path: '/path/feature', branch: 'feature' };
183
+ const worktree = { path: '/path/feature', branch: 'feature', baseBranch: null };
184
184
  mockedGetProjectWorktrees.mockReturnValue([worktree]);
185
185
  mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
186
186
  mockedGetConfigValue.mockReturnValue(true);
@@ -196,7 +196,7 @@ describe('handleResume — resumeInPlace 配置', () => {
196
196
  });
197
197
 
198
198
  it('resumeInPlace 为 false 时,单选在新终端 Tab 中恢复', async () => {
199
- const worktree = { path: '/path/feature', branch: 'feature' };
199
+ const worktree = { path: '/path/feature', branch: 'feature', baseBranch: null };
200
200
  mockedGetProjectWorktrees.mockReturnValue([worktree]);
201
201
  mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
202
202
  mockedGetConfigValue.mockReturnValue(false);
@@ -214,7 +214,7 @@ describe('handleResume — resumeInPlace 配置', () => {
214
214
  });
215
215
 
216
216
  it('resumeInPlace 为 false 且无历史会话时,传 false 给新终端启动', async () => {
217
- const worktree = { path: '/path/feature', branch: 'feature' };
217
+ const worktree = { path: '/path/feature', branch: 'feature', baseBranch: null };
218
218
  mockedGetProjectWorktrees.mockReturnValue([worktree]);
219
219
  mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
220
220
  mockedGetConfigValue.mockReturnValue(false);
@@ -230,8 +230,8 @@ describe('handleResume — resumeInPlace 配置', () => {
230
230
 
231
231
  it('多选时不受 resumeInPlace 影响,始终在新 Tab 中打开', async () => {
232
232
  const worktrees = [
233
- { path: '/path/feature-a', branch: 'feature-a' },
234
- { path: '/path/feature-b', branch: 'feature-b' },
233
+ { path: '/path/feature-a', branch: 'feature-a', baseBranch: null },
234
+ { path: '/path/feature-b', branch: 'feature-b', baseBranch: null },
235
235
  ];
236
236
  mockedGetProjectWorktrees.mockReturnValue(worktrees);
237
237
  mockedPromptGroupedMultiSelectBranches.mockResolvedValue(worktrees);
@@ -251,8 +251,8 @@ describe('handleResume — resumeInPlace 配置', () => {
251
251
 
252
252
  it('用户未选择任何分支时直接退出', async () => {
253
253
  const worktrees = [
254
- { path: '/path/feature-a', branch: 'feature-a' },
255
- { path: '/path/feature-b', branch: 'feature-b' },
254
+ { path: '/path/feature-a', branch: 'feature-a', baseBranch: null },
255
+ { path: '/path/feature-b', branch: 'feature-b', baseBranch: null },
256
256
  ];
257
257
  mockedGetProjectWorktrees.mockReturnValue(worktrees);
258
258
  mockedPromptGroupedMultiSelectBranches.mockResolvedValue([]);
@@ -270,7 +270,7 @@ describe('handleResume — resumeInPlace 配置', () => {
270
270
 
271
271
  describe('handleResume — 非交互式追问', () => {
272
272
  it('--prompt + -b 有历史会话时传 [true]', async () => {
273
- const worktree = { path: '/path/feature', branch: 'feature' };
273
+ const worktree = { path: '/path/feature', branch: 'feature', baseBranch: null };
274
274
  mockedGetProjectWorktrees.mockReturnValue([worktree]);
275
275
  mockedFindExactMatch.mockReturnValue(worktree);
276
276
  mockedHasClaudeSessionHistory.mockReturnValue(true);
@@ -296,7 +296,7 @@ describe('handleResume — 非交互式追问', () => {
296
296
  });
297
297
 
298
298
  it('--prompt + -b 无历史会话时传 [false]', async () => {
299
- const worktree = { path: '/path/feature', branch: 'feature' };
299
+ const worktree = { path: '/path/feature', branch: 'feature', baseBranch: null };
300
300
  mockedGetProjectWorktrees.mockReturnValue([worktree]);
301
301
  mockedFindExactMatch.mockReturnValue(worktree);
302
302
  mockedHasClaudeSessionHistory.mockReturnValue(false);
@@ -339,7 +339,7 @@ describe('handleResume — 非交互式追问', () => {
339
339
 
340
340
  it('--prompt 指定的分支不存在时报错', async () => {
341
341
  mockedGetProjectWorktrees.mockReturnValue([
342
- { path: '/path/other', branch: 'other' },
342
+ { path: '/path/other', branch: 'other', baseBranch: null },
343
343
  ]);
344
344
  mockedFindExactMatch.mockReturnValue(undefined);
345
345
 
@@ -354,8 +354,8 @@ describe('handleResume — 非交互式追问', () => {
354
354
 
355
355
  it('-f 批量追问模式', async () => {
356
356
  const worktrees = [
357
- { path: '/path/feat-a', branch: 'feat-a' },
358
- { path: '/path/feat-b', branch: 'feat-b' },
357
+ { path: '/path/feat-a', branch: 'feat-a', baseBranch: null },
358
+ { path: '/path/feat-b', branch: 'feat-b', baseBranch: null },
359
359
  ];
360
360
  mockedLoadTaskFile.mockReturnValue([
361
361
  { branch: 'feat-a', task: '追问任务A' },
@@ -390,7 +390,7 @@ describe('handleResume — 非交互式追问', () => {
390
390
  { branch: 'nonexistent', task: '追问任务' },
391
391
  ]);
392
392
  mockedGetProjectWorktrees.mockReturnValue([
393
- { path: '/path/feat-a', branch: 'feat-a' },
393
+ { path: '/path/feat-a', branch: 'feat-a', baseBranch: null },
394
394
  ]);
395
395
  mockedFindExactMatch.mockReturnValue(undefined);
396
396
 
@@ -404,7 +404,7 @@ describe('handleResume — 非交互式追问', () => {
404
404
  });
405
405
 
406
406
  it('-f + -c 传递并发数', async () => {
407
- const worktree = { path: '/path/feat-a', branch: 'feat-a' };
407
+ const worktree = { path: '/path/feat-a', branch: 'feat-a', baseBranch: null };
408
408
  mockedLoadTaskFile.mockReturnValue([
409
409
  { branch: 'feat-a', task: '追问' },
410
410
  ]);
@@ -429,9 +429,9 @@ describe('handleResume — 非交互式追问', () => {
429
429
 
430
430
  it('-f 批量追问按 worktree 独立检查会话历史', async () => {
431
431
  const worktrees = [
432
- { path: '/path/feat-a', branch: 'feat-a' },
433
- { path: '/path/feat-b', branch: 'feat-b' },
434
- { path: '/path/feat-c', branch: 'feat-c' },
432
+ { path: '/path/feat-a', branch: 'feat-a', baseBranch: null },
433
+ { path: '/path/feat-b', branch: 'feat-b', baseBranch: null },
434
+ { path: '/path/feat-c', branch: 'feat-c', baseBranch: null },
435
435
  ];
436
436
  mockedLoadTaskFile.mockReturnValue([
437
437
  { branch: 'feat-a', task: '任务A' },
@@ -263,7 +263,7 @@ describe('handleRun', () => {
263
263
  it('未传 --tasks 时创建单个 worktree 并打开交互式界面', async () => {
264
264
  mockedSanitizeBranchName.mockReturnValue('feature');
265
265
  mockedCheckBranchExists.mockReturnValue(false);
266
- const worktree = { path: '/path/feature', branch: 'feature' };
266
+ const worktree = { path: '/path/feature', branch: 'feature', baseBranch: null };
267
267
  mockedCreateWorktrees.mockReturnValue([worktree]);
268
268
 
269
269
  const program = new Command();
@@ -290,8 +290,8 @@ describe('handleRun', () => {
290
290
 
291
291
  it('传 --tasks 时创建对应数量 worktree 并并行执行', async () => {
292
292
  const worktrees = [
293
- { path: '/path/feat-1', branch: 'feat-1' },
294
- { path: '/path/feat-2', branch: 'feat-2' },
293
+ { path: '/path/feat-1', branch: 'feat-1', baseBranch: null },
294
+ { path: '/path/feat-2', branch: 'feat-2', baseBranch: null },
295
295
  ];
296
296
  mockedCreateWorktrees.mockReturnValue(worktrees);
297
297
 
@@ -314,7 +314,7 @@ describe('handleRun', () => {
314
314
  });
315
315
 
316
316
  it('任务执行失败时在通知中报告', async () => {
317
- const worktrees = [{ path: '/path/feat-1', branch: 'feat-1' }];
317
+ const worktrees = [{ path: '/path/feat-1', branch: 'feat-1', baseBranch: null }];
318
318
  mockedCreateWorktrees.mockReturnValue(worktrees);
319
319
 
320
320
  const jsonOutput = JSON.stringify({
@@ -334,7 +334,7 @@ describe('handleRun', () => {
334
334
  });
335
335
 
336
336
  it('子进程发生错误时返回失败结果', async () => {
337
- const worktrees = [{ path: '/path/feat-1', branch: 'feat-1' }];
337
+ const worktrees = [{ path: '/path/feat-1', branch: 'feat-1', baseBranch: null }];
338
338
  mockedCreateWorktrees.mockReturnValue(worktrees);
339
339
 
340
340
  // 创建会触发 error 事件的子进程
@@ -359,9 +359,9 @@ describe('handleRun', () => {
359
359
  it('传 --concurrency 限制并发数', async () => {
360
360
  mockedParseConcurrency.mockReturnValue(1);
361
361
  const worktrees = [
362
- { path: '/path/feat-1', branch: 'feat-1' },
363
- { path: '/path/feat-2', branch: 'feat-2' },
364
- { path: '/path/feat-3', branch: 'feat-3' },
362
+ { path: '/path/feat-1', branch: 'feat-1', baseBranch: null },
363
+ { path: '/path/feat-2', branch: 'feat-2', baseBranch: null },
364
+ { path: '/path/feat-3', branch: 'feat-3', baseBranch: null },
365
365
  ];
366
366
  mockedCreateWorktrees.mockReturnValue(worktrees);
367
367
 
@@ -389,8 +389,8 @@ describe('handleRun', () => {
389
389
 
390
390
  it('--concurrency 为 0 时不限制并发', async () => {
391
391
  const worktrees = [
392
- { path: '/path/feat-1', branch: 'feat-1' },
393
- { path: '/path/feat-2', branch: 'feat-2' },
392
+ { path: '/path/feat-1', branch: 'feat-1', baseBranch: null },
393
+ { path: '/path/feat-2', branch: 'feat-2', baseBranch: null },
394
394
  ];
395
395
  mockedCreateWorktrees.mockReturnValue(worktrees);
396
396
 
@@ -418,9 +418,9 @@ describe('handleRun', () => {
418
418
  mockedParseConcurrency.mockReturnValue(2);
419
419
 
420
420
  const worktrees = [
421
- { path: '/path/feat-1', branch: 'feat-1' },
422
- { path: '/path/feat-2', branch: 'feat-2' },
423
- { path: '/path/feat-3', branch: 'feat-3' },
421
+ { path: '/path/feat-1', branch: 'feat-1', baseBranch: null },
422
+ { path: '/path/feat-2', branch: 'feat-2', baseBranch: null },
423
+ { path: '/path/feat-3', branch: 'feat-3', baseBranch: null },
424
424
  ];
425
425
  mockedCreateWorktrees.mockReturnValue(worktrees);
426
426
 
@@ -450,8 +450,8 @@ describe('handleRun', () => {
450
450
  { branch: 'fix-bug', task: '修复问题' },
451
451
  ]);
452
452
  const worktrees = [
453
- { path: '/path/feat-login', branch: 'feat-login' },
454
- { path: '/path/fix-bug', branch: 'fix-bug' },
453
+ { path: '/path/feat-login', branch: 'feat-login', baseBranch: null },
454
+ { path: '/path/fix-bug', branch: 'fix-bug', baseBranch: null },
455
455
  ];
456
456
  mockedCreateWorktreesByBranches.mockReturnValue(worktrees);
457
457
 
@@ -480,8 +480,8 @@ describe('handleRun', () => {
480
480
  { task: '任务2' },
481
481
  ]);
482
482
  const worktrees = [
483
- { path: '/path/feat-1', branch: 'feat-1' },
484
- { path: '/path/feat-2', branch: 'feat-2' },
483
+ { path: '/path/feat-1', branch: 'feat-1', baseBranch: null },
484
+ { path: '/path/feat-2', branch: 'feat-2', baseBranch: null },
485
485
  ];
486
486
  mockedCreateWorktrees.mockReturnValue(worktrees);
487
487