clawt 3.10.5 → 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 (38) hide show
  1. package/AGENTS.md +16 -0
  2. package/dist/index.js +163 -89
  3. package/docs/create.md +1 -0
  4. package/docs/list.md +21 -10
  5. package/docs/merge.md +1 -0
  6. package/docs/remove.md +2 -0
  7. package/docs/spec.md +4 -1
  8. package/docs/status.md +9 -1
  9. package/docs/superpowers/findings/2026-06-09-worktree-base-branch-findings.md +58 -0
  10. package/docs/superpowers/plans/2026-06-09-worktree-base-branch.md +386 -0
  11. package/docs/superpowers/specs/2026-06-09-worktree-base-branch-design.md +169 -0
  12. package/package.json +1 -1
  13. package/src/commands/list.ts +5 -3
  14. package/src/commands/merge.ts +1 -1
  15. package/src/commands/remove.ts +3 -0
  16. package/src/commands/status.ts +5 -0
  17. package/src/types/status.ts +2 -0
  18. package/src/types/worktree.ts +12 -0
  19. package/src/utils/formatter.ts +22 -0
  20. package/src/utils/index.ts +2 -1
  21. package/src/utils/interactive-panel-render.ts +6 -3
  22. package/src/utils/worktree-metadata.ts +82 -0
  23. package/src/utils/worktree.ts +29 -10
  24. package/tests/helpers/fixtures.ts +1 -0
  25. package/tests/unit/commands/cover-validate.test.ts +4 -4
  26. package/tests/unit/commands/create.test.ts +3 -3
  27. package/tests/unit/commands/list.test.ts +66 -3
  28. package/tests/unit/commands/merge.test.ts +1 -1
  29. package/tests/unit/commands/remove.test.ts +24 -18
  30. package/tests/unit/commands/resume.test.ts +21 -21
  31. package/tests/unit/commands/run.test.ts +17 -17
  32. package/tests/unit/commands/status.test.ts +85 -10
  33. package/tests/unit/commands/sync.test.ts +4 -4
  34. package/tests/unit/commands/validate.test.ts +1 -1
  35. package/tests/unit/utils/interactive-panel-render.test.ts +124 -0
  36. package/tests/unit/utils/worktree-matcher.test.ts +2 -2
  37. package/tests/unit/utils/worktree-metadata.test.ts +91 -0
  38. package/tests/unit/utils/worktree.test.ts +65 -0
package/AGENTS.md ADDED
@@ -0,0 +1,16 @@
1
+ # Project Memory
2
+
3
+ ## 代码阅读规范(必须严格遵守的三步阅读流程)
4
+
5
+ 每次接到任务时,**必须**按以下顺序渐进式地理解项目,禁止跳步:
6
+
7
+ 1. **第一步:阅读 `docs/spec.md`** — 全面了解项目整体架构、核心概念与模块划分,建立全局认知。
8
+ 2. **第二步:根据用户问题,阅读 `docs/` 下对应模块的详细文档** — 例如用户问 merge 相关功能,则阅读 `docs/merge.md`;问 config 相关则阅读 `docs/config.md` 和 `docs/config-file.md`,以此类推。
9
+ 3. **第三步:阅读具体的源代码** — 在前两步建立充分上下文后,再深入代码实现,确保方案设计不会因阅读代码不全面而产生缺陷。
10
+
11
+ > **重要提示:文档可能滞后于代码。当文档描述与实际代码实现存在冲突时,以代码为准。**
12
+
13
+ ## 编码规范
14
+
15
+ - JSON 序列化必须使用项目封装的 `safeStringify`(位于 `src/utils/json.ts`),禁止直接使用原生 `JSON.stringify`。`safeStringify` 已通过 `src/utils/index.js` 统一导出。
16
+ - 构造输出对象时,使用 `{ ...obj }` 解构展开而非逐字段列举,避免源类型新增字段后遗漏。
package/dist/index.js CHANGED
@@ -3067,6 +3067,16 @@ function formatLocalISOString(date) {
3067
3067
  const minutes = String(absMinutes % 60).padStart(2, "0");
3068
3068
  return `${iso}${sign}${hours}:${minutes}`;
3069
3069
  }
3070
+ function formatBaseBranchLine(baseBranch) {
3071
+ const lang = getCurrentLanguage();
3072
+ const label = lang === "en" ? "Base branch" : "\u6765\u6E90\u5206\u652F";
3073
+ const fallback = lang === "en" ? "Not recorded" : "\u672A\u8BB0\u5F55";
3074
+ return `${label}: ${baseBranch ?? fallback}`;
3075
+ }
3076
+ function formatBaseBranchInline(baseBranch) {
3077
+ const fallback = getCurrentLanguage() === "en" ? "Not recorded" : "\u672A\u8BB0\u5F55";
3078
+ return `<- ${baseBranch ?? fallback}`;
3079
+ }
3070
3080
  function generateTaskFilename(prefix) {
3071
3081
  const now = /* @__PURE__ */ new Date();
3072
3082
  const pad = (n) => String(n).padStart(2, "0");
@@ -3395,45 +3405,100 @@ async function runPreChecks(options) {
3395
3405
  }
3396
3406
 
3397
3407
  // src/utils/worktree.ts
3398
- import { join as join5 } from "path";
3399
- import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
3408
+ import { join as join6 } from "path";
3409
+ import { existsSync as existsSync6, readdirSync as readdirSync2 } from "fs";
3410
+
3411
+ // src/utils/worktree-metadata.ts
3412
+ import { existsSync as existsSync5, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync3 } from "fs";
3413
+ import { dirname, join as join5 } from "path";
3414
+ function getWorktreeMetadataPath(projectName, branchName) {
3415
+ return join5(PROJECTS_CONFIG_DIR, projectName, "worktrees", `${branchName}.json`);
3416
+ }
3417
+ function saveWorktreeMetadata(projectName, metadata) {
3418
+ const metadataPath = getWorktreeMetadataPath(projectName, metadata.branch);
3419
+ const metadataDir = dirname(metadataPath);
3420
+ try {
3421
+ ensureDir(metadataDir);
3422
+ writeFileSync3(metadataPath, safeStringify(metadata), "utf-8");
3423
+ } catch (error) {
3424
+ logger.error(`\u4FDD\u5B58 worktree \u5143\u6570\u636E\u5931\u8D25: ${metadataPath}`, error);
3425
+ throw error;
3426
+ }
3427
+ }
3428
+ function loadWorktreeMetadata(projectName, branchName) {
3429
+ const metadataPath = getWorktreeMetadataPath(projectName, branchName);
3430
+ if (!existsSync5(metadataPath)) {
3431
+ return null;
3432
+ }
3433
+ try {
3434
+ const content = readFileSync3(metadataPath, "utf-8");
3435
+ const parsed = JSON.parse(content);
3436
+ if (!parsed || typeof parsed !== "object" || !parsed.branch || !parsed.baseBranch) {
3437
+ logger.warn(`worktree \u5143\u6570\u636E\u683C\u5F0F\u65E0\u6548: ${metadataPath}`);
3438
+ return null;
3439
+ }
3440
+ return parsed;
3441
+ } catch (error) {
3442
+ logger.warn(`\u89E3\u6790 worktree \u5143\u6570\u636E\u5931\u8D25: ${metadataPath}`, error);
3443
+ return null;
3444
+ }
3445
+ }
3446
+ function removeWorktreeMetadata(projectName, branchName) {
3447
+ const metadataPath = getWorktreeMetadataPath(projectName, branchName);
3448
+ try {
3449
+ if (existsSync5(metadataPath)) {
3450
+ rmSync(metadataPath);
3451
+ }
3452
+ } catch (error) {
3453
+ logger.error(`\u5220\u9664 worktree \u5143\u6570\u636E\u5931\u8D25: ${metadataPath}`, error);
3454
+ }
3455
+ }
3456
+
3457
+ // src/utils/worktree.ts
3400
3458
  function getProjectWorktreeDir() {
3401
3459
  const projectName = getProjectName();
3402
- return join5(WORKTREES_DIR, projectName);
3460
+ return join6(WORKTREES_DIR, projectName);
3403
3461
  }
3404
3462
  function createWorktrees(branchName, count) {
3405
3463
  const sanitized = sanitizeBranchName(branchName);
3406
3464
  const branchNames = generateBranchNames(sanitized, count);
3407
3465
  validateBranchesNotExist(branchNames);
3408
- const projectDir = getProjectWorktreeDir();
3466
+ const projectName = getProjectName();
3467
+ const projectDir = join6(WORKTREES_DIR, projectName);
3409
3468
  ensureDir(projectDir);
3469
+ const baseBranch = getCurrentBranch();
3410
3470
  const results = [];
3411
3471
  for (const name of branchNames) {
3412
- const worktreePath = join5(projectDir, name);
3472
+ const worktreePath = join6(projectDir, name);
3413
3473
  createWorktree(name, worktreePath);
3414
3474
  createValidateBranch(name);
3415
- results.push({ path: worktreePath, branch: name });
3475
+ saveWorktreeMetadata(projectName, { branch: name, baseBranch, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
3476
+ results.push({ path: worktreePath, branch: name, baseBranch });
3416
3477
  logger.info(`worktree \u521B\u5EFA\u5B8C\u6210: ${worktreePath} (\u5206\u652F: ${name})`);
3417
3478
  }
3418
3479
  return results;
3419
3480
  }
3420
3481
  function createWorktreesByBranches(branchNames) {
3421
3482
  validateBranchesNotExist(branchNames);
3422
- const projectDir = getProjectWorktreeDir();
3483
+ const projectName = getProjectName();
3484
+ const projectDir = join6(WORKTREES_DIR, projectName);
3423
3485
  ensureDir(projectDir);
3486
+ const baseBranch = getCurrentBranch();
3424
3487
  const results = [];
3425
3488
  for (const name of branchNames) {
3426
- const worktreePath = join5(projectDir, name);
3489
+ const worktreePath = join6(projectDir, name);
3427
3490
  createWorktree(name, worktreePath);
3428
3491
  createValidateBranch(name);
3429
- results.push({ path: worktreePath, branch: name });
3492
+ saveWorktreeMetadata(projectName, { branch: name, baseBranch, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
3493
+ results.push({ path: worktreePath, branch: name, baseBranch });
3430
3494
  logger.info(`worktree \u521B\u5EFA\u5B8C\u6210: ${worktreePath} (\u5206\u652F: ${name})`);
3431
3495
  }
3432
3496
  return results;
3433
3497
  }
3434
3498
  function getProjectWorktrees() {
3435
- const projectDir = getProjectWorktreeDir();
3436
- if (!existsSync5(projectDir)) {
3499
+ const projectName = getProjectName();
3500
+ const projectDir = join6(WORKTREES_DIR, projectName);
3501
+ if (!existsSync6(projectDir)) {
3437
3502
  return [];
3438
3503
  }
3439
3504
  const worktreeListOutput = gitWorktreeList();
@@ -3446,29 +3511,33 @@ function getProjectWorktrees() {
3446
3511
  if (!entry.isDirectory()) {
3447
3512
  continue;
3448
3513
  }
3449
- const fullPath = join5(projectDir, entry.name);
3514
+ const fullPath = join6(projectDir, entry.name);
3450
3515
  if (registeredPaths.has(fullPath)) {
3516
+ const metadata = loadWorktreeMetadata(projectName, entry.name);
3451
3517
  worktrees.push({
3452
3518
  path: fullPath,
3453
- branch: entry.name
3519
+ branch: entry.name,
3520
+ baseBranch: metadata?.baseBranch ?? null
3454
3521
  });
3455
3522
  }
3456
3523
  }
3457
3524
  return worktrees;
3458
3525
  }
3459
3526
  function cleanupWorktrees(worktrees) {
3527
+ const projectName = getProjectName();
3460
3528
  for (const wt of worktrees) {
3461
3529
  try {
3462
3530
  removeWorktreeByPath(wt.path);
3463
3531
  deleteBranch(wt.branch);
3464
3532
  deleteValidateBranch(wt.branch);
3533
+ removeWorktreeMetadata(projectName, wt.branch);
3465
3534
  logger.info(`\u5DF2\u6E05\u7406 worktree \u548C\u5206\u652F: ${wt.branch}`);
3466
3535
  } catch (error) {
3467
3536
  logger.error(`\u6E05\u7406 worktree \u5931\u8D25: ${wt.path} - ${error}`);
3468
3537
  }
3469
3538
  }
3470
3539
  gitWorktreePrune();
3471
- const projectDir = getProjectWorktreeDir();
3540
+ const projectDir = join6(WORKTREES_DIR, projectName);
3472
3541
  removeEmptyDir(projectDir);
3473
3542
  }
3474
3543
  function getWorktreeStatus(worktree) {
@@ -3485,7 +3554,7 @@ function getWorktreeStatus(worktree) {
3485
3554
 
3486
3555
  // src/utils/symlink-guard.ts
3487
3556
  import { readdirSync as readdirSync3, lstatSync, unlinkSync, readlinkSync } from "fs";
3488
- import { join as join6, relative, isAbsolute as isAbsolute2, resolve } from "path";
3557
+ import { join as join7, relative, isAbsolute as isAbsolute2, resolve } from "path";
3489
3558
  function isExternalSymlink(linkPath, worktreeRoot) {
3490
3559
  try {
3491
3560
  const target = readlinkSync(linkPath);
@@ -3502,7 +3571,7 @@ function removeExternalSymlinks(dir) {
3502
3571
  const entries = readdirSync3(dir, { withFileTypes: true });
3503
3572
  for (const entry of entries) {
3504
3573
  if (!entry.isSymbolicLink()) continue;
3505
- const fullPath = join6(dir, entry.name);
3574
+ const fullPath = join7(dir, entry.name);
3506
3575
  if (!isExternalSymlink(fullPath, dir)) continue;
3507
3576
  try {
3508
3577
  const stat = lstatSync(fullPath);
@@ -3537,14 +3606,14 @@ async function promptCommitMessage(promptMessage, nonInteractiveErrorMessage) {
3537
3606
 
3538
3607
  // src/utils/claude.ts
3539
3608
  import { spawnSync as spawnSync3 } from "child_process";
3540
- import { existsSync as existsSync7, readdirSync as readdirSync4 } from "fs";
3541
- import { join as join7 } from "path";
3609
+ import { existsSync as existsSync8, readdirSync as readdirSync4 } from "fs";
3610
+ import { join as join8 } from "path";
3542
3611
 
3543
3612
  // src/utils/terminal.ts
3544
3613
  import { execFileSync as execFileSync3 } from "child_process";
3545
- import { existsSync as existsSync6 } from "fs";
3614
+ import { existsSync as existsSync7 } from "fs";
3546
3615
  function isITerm2Installed() {
3547
- return existsSync6(ITERM2_APP_PATH);
3616
+ return existsSync7(ITERM2_APP_PATH);
3548
3617
  }
3549
3618
  function isCmuxEnvironment() {
3550
3619
  return !!process.env.CMUX_WORKSPACE_ID;
@@ -3682,8 +3751,8 @@ function encodeClaudeProjectPath(absolutePath) {
3682
3751
  }
3683
3752
  function hasClaudeSessionHistory(worktreePath) {
3684
3753
  const encodedName = encodeClaudeProjectPath(worktreePath);
3685
- const projectDir = join7(CLAUDE_PROJECTS_DIR, encodedName);
3686
- if (!existsSync7(projectDir)) {
3754
+ const projectDir = join8(CLAUDE_PROJECTS_DIR, encodedName);
3755
+ if (!existsSync8(projectDir)) {
3687
3756
  return false;
3688
3757
  }
3689
3758
  const entries = readdirSync4(projectDir);
@@ -3736,23 +3805,23 @@ function launchInteractiveClaudeInNewTerminal(worktree, hasPreviousSession) {
3736
3805
  }
3737
3806
 
3738
3807
  // src/utils/validate-snapshot.ts
3739
- import { join as join8 } from "path";
3740
- import { existsSync as existsSync8, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, readdirSync as readdirSync5, rmdirSync as rmdirSync2, statSync as statSync2 } from "fs";
3808
+ import { join as join9 } from "path";
3809
+ import { existsSync as existsSync9, readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, readdirSync as readdirSync5, rmdirSync as rmdirSync2, statSync as statSync2 } from "fs";
3741
3810
  function getSnapshotPath(projectName, branchName) {
3742
- return join8(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
3811
+ return join9(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
3743
3812
  }
3744
3813
  function getSnapshotHeadPath(projectName, branchName) {
3745
- return join8(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
3814
+ return join9(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
3746
3815
  }
3747
3816
  function getSnapshotStagedPath(projectName, branchName) {
3748
- return join8(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.staged`);
3817
+ return join9(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.staged`);
3749
3818
  }
3750
3819
  function hasSnapshot(projectName, branchName) {
3751
- return existsSync8(getSnapshotPath(projectName, branchName));
3820
+ return existsSync9(getSnapshotPath(projectName, branchName));
3752
3821
  }
3753
3822
  function getSnapshotModifiedTime(projectName, branchName) {
3754
3823
  const snapshotPath = getSnapshotPath(projectName, branchName);
3755
- if (!existsSync8(snapshotPath)) return null;
3824
+ if (!existsSync9(snapshotPath)) return null;
3756
3825
  const stat = statSync2(snapshotPath);
3757
3826
  return stat.mtime.toISOString();
3758
3827
  }
@@ -3761,22 +3830,22 @@ function readSnapshot(projectName, branchName) {
3761
3830
  const headPath = getSnapshotHeadPath(projectName, branchName);
3762
3831
  const stagedPath = getSnapshotStagedPath(projectName, branchName);
3763
3832
  logger.debug(`\u8BFB\u53D6 validate \u5FEB\u7167: ${snapshotPath}`);
3764
- const treeHash = existsSync8(snapshotPath) ? readFileSync3(snapshotPath, "utf-8").trim() : "";
3765
- const headCommitHash = existsSync8(headPath) ? readFileSync3(headPath, "utf-8").trim() : "";
3766
- const stagedTreeHash = existsSync8(stagedPath) ? readFileSync3(stagedPath, "utf-8").trim() : "";
3833
+ const treeHash = existsSync9(snapshotPath) ? readFileSync4(snapshotPath, "utf-8").trim() : "";
3834
+ const headCommitHash = existsSync9(headPath) ? readFileSync4(headPath, "utf-8").trim() : "";
3835
+ const stagedTreeHash = existsSync9(stagedPath) ? readFileSync4(stagedPath, "utf-8").trim() : "";
3767
3836
  return { treeHash, headCommitHash, stagedTreeHash };
3768
3837
  }
3769
3838
  function writeSnapshot(projectName, branchName, treeHash, headCommitHash, stagedTreeHash) {
3770
- const snapshotDir = join8(VALIDATE_SNAPSHOTS_DIR, projectName);
3839
+ const snapshotDir = join9(VALIDATE_SNAPSHOTS_DIR, projectName);
3771
3840
  ensureDir(snapshotDir);
3772
3841
  if (treeHash !== void 0) {
3773
- writeFileSync3(getSnapshotPath(projectName, branchName), treeHash, "utf-8");
3842
+ writeFileSync4(getSnapshotPath(projectName, branchName), treeHash, "utf-8");
3774
3843
  }
3775
3844
  if (headCommitHash !== void 0) {
3776
- writeFileSync3(getSnapshotHeadPath(projectName, branchName), headCommitHash, "utf-8");
3845
+ writeFileSync4(getSnapshotHeadPath(projectName, branchName), headCommitHash, "utf-8");
3777
3846
  }
3778
3847
  if (stagedTreeHash !== void 0) {
3779
- writeFileSync3(getSnapshotStagedPath(projectName, branchName), stagedTreeHash, "utf-8");
3848
+ writeFileSync4(getSnapshotStagedPath(projectName, branchName), stagedTreeHash, "utf-8");
3780
3849
  }
3781
3850
  logger.info(`\u5DF2\u5199\u5165 validate \u5FEB\u7167 (project=${projectName}, branch=${branchName})`);
3782
3851
  }
@@ -3784,35 +3853,35 @@ function removeSnapshot(projectName, branchName) {
3784
3853
  const snapshotPath = getSnapshotPath(projectName, branchName);
3785
3854
  const headPath = getSnapshotHeadPath(projectName, branchName);
3786
3855
  const stagedPath = getSnapshotStagedPath(projectName, branchName);
3787
- if (existsSync8(snapshotPath)) {
3856
+ if (existsSync9(snapshotPath)) {
3788
3857
  unlinkSync2(snapshotPath);
3789
3858
  logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${snapshotPath}`);
3790
3859
  }
3791
- if (existsSync8(headPath)) {
3860
+ if (existsSync9(headPath)) {
3792
3861
  unlinkSync2(headPath);
3793
3862
  logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${headPath}`);
3794
3863
  }
3795
- if (existsSync8(stagedPath)) {
3864
+ if (existsSync9(stagedPath)) {
3796
3865
  unlinkSync2(stagedPath);
3797
3866
  logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${stagedPath}`);
3798
3867
  }
3799
3868
  }
3800
3869
  function getProjectSnapshotBranches(projectName) {
3801
- const projectDir = join8(VALIDATE_SNAPSHOTS_DIR, projectName);
3802
- if (!existsSync8(projectDir)) {
3870
+ const projectDir = join9(VALIDATE_SNAPSHOTS_DIR, projectName);
3871
+ if (!existsSync9(projectDir)) {
3803
3872
  return [];
3804
3873
  }
3805
3874
  const files = readdirSync5(projectDir);
3806
3875
  return files.filter((f) => f.endsWith(".tree")).map((f) => f.replace(/\.tree$/, ""));
3807
3876
  }
3808
3877
  function removeProjectSnapshots(projectName) {
3809
- const projectDir = join8(VALIDATE_SNAPSHOTS_DIR, projectName);
3810
- if (!existsSync8(projectDir)) {
3878
+ const projectDir = join9(VALIDATE_SNAPSHOTS_DIR, projectName);
3879
+ if (!existsSync9(projectDir)) {
3811
3880
  return;
3812
3881
  }
3813
3882
  const files = readdirSync5(projectDir);
3814
3883
  for (const file of files) {
3815
- unlinkSync2(join8(projectDir, file));
3884
+ unlinkSync2(join9(projectDir, file));
3816
3885
  }
3817
3886
  try {
3818
3887
  rmdirSync2(projectDir);
@@ -4343,7 +4412,7 @@ var ProgressRenderer = class {
4343
4412
 
4344
4413
  // src/utils/task-file.ts
4345
4414
  import { resolve as resolve2 } from "path";
4346
- import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
4415
+ import { existsSync as existsSync10, readFileSync as readFileSync5 } from "fs";
4347
4416
  var TASK_BLOCK_REGEX = /<!-- CLAWT-TASKS:START -->([\s\S]*?)<!-- CLAWT-TASKS:END -->/g;
4348
4417
  var BRANCH_LINE_REGEX = /^#\s*branch:\s*(.+)$/;
4349
4418
  var EMPTY_TASKS_MESSAGE = getCurrentLanguage() === "en" ? "Task list cannot be empty" : "\u4EFB\u52A1\u5217\u8868\u4E0D\u80FD\u4E3A\u7A7A";
@@ -4389,10 +4458,10 @@ function parseTaskFile(content, options) {
4389
4458
  }
4390
4459
  function loadTaskFile(filePath, options) {
4391
4460
  const absolutePath = resolve2(filePath);
4392
- if (!existsSync9(absolutePath)) {
4461
+ if (!existsSync10(absolutePath)) {
4393
4462
  throw new ClawtError(MESSAGES.TASK_FILE_NOT_FOUND(absolutePath));
4394
4463
  }
4395
- const content = readFileSync4(absolutePath, "utf-8");
4464
+ const content = readFileSync5(absolutePath, "utf-8");
4396
4465
  const entries = parseTaskFile(content, options);
4397
4466
  if (entries.length === 0) {
4398
4467
  throw new ClawtError(MESSAGES.TASK_FILE_EMPTY);
@@ -4737,7 +4806,7 @@ async function executeBatchTasks(worktrees, tasks, concurrency, continueFlags) {
4737
4806
 
4738
4807
  // src/utils/dry-run.ts
4739
4808
  import chalk6 from "chalk";
4740
- import { join as join9 } from "path";
4809
+ import { join as join10 } from "path";
4741
4810
  var DRY_RUN_TASK_DESC_MAX_LENGTH = 80;
4742
4811
  function truncateTaskDesc(task) {
4743
4812
  const oneLine = task.replace(/\n/g, " ").trim();
@@ -4765,7 +4834,7 @@ function printDryRunPreview(branchNames, tasks, concurrency) {
4765
4834
  let hasConflict = false;
4766
4835
  for (let i = 0; i < branchNames.length; i++) {
4767
4836
  const branch = branchNames[i];
4768
- const worktreePath = join9(projectDir, branch);
4837
+ const worktreePath = join10(projectDir, branch);
4769
4838
  const exists = checkBranchExists(branch);
4770
4839
  if (exists) hasConflict = true;
4771
4840
  const indexLabel = `[${i + 1}/${branchNames.length}]`;
@@ -4920,14 +4989,14 @@ async function promptStringValue(key, currentValue) {
4920
4989
  }
4921
4990
 
4922
4991
  // src/utils/update-checker.ts
4923
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
4992
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
4924
4993
  import { execSync as execSync4 } from "child_process";
4925
4994
  import { request } from "https";
4926
4995
  import chalk8 from "chalk";
4927
4996
  import stringWidth2 from "string-width";
4928
4997
  function readUpdateCache() {
4929
4998
  try {
4930
- const raw = readFileSync5(UPDATE_CHECK_PATH, "utf-8");
4999
+ const raw = readFileSync6(UPDATE_CHECK_PATH, "utf-8");
4931
5000
  return JSON.parse(raw);
4932
5001
  } catch {
4933
5002
  return null;
@@ -4935,7 +5004,7 @@ function readUpdateCache() {
4935
5004
  }
4936
5005
  function writeUpdateCache(cache) {
4937
5006
  try {
4938
- writeFileSync4(UPDATE_CHECK_PATH, JSON.stringify(cache, null, 2), "utf-8");
5007
+ writeFileSync5(UPDATE_CHECK_PATH, JSON.stringify(cache, null, 2), "utf-8");
4939
5008
  } catch {
4940
5009
  }
4941
5010
  }
@@ -5133,8 +5202,8 @@ async function executeRunCommand(command, mainWorktreePath) {
5133
5202
  }
5134
5203
 
5135
5204
  // src/utils/validate-core.ts
5136
- import { existsSync as existsSync10 } from "fs";
5137
- import { join as join10 } from "path";
5205
+ import { existsSync as existsSync11 } from "fs";
5206
+ import { join as join11 } from "path";
5138
5207
  function buildCleanCommands(files) {
5139
5208
  const dirs = /* @__PURE__ */ new Set();
5140
5209
  for (const file of files) {
@@ -5238,7 +5307,7 @@ function detectIgnoredFilesInPatch(branchName, mainWorktreePath) {
5238
5307
  const output = execCommand(`git diff --name-only HEAD...${branchName}`, { cwd: mainWorktreePath });
5239
5308
  const patchFiles = output.split("\n").filter(Boolean);
5240
5309
  if (patchFiles.length === 0) return [];
5241
- return gitCheckIgnored(patchFiles, mainWorktreePath).filter((file) => existsSync10(join10(mainWorktreePath, file)));
5310
+ return gitCheckIgnored(patchFiles, mainWorktreePath).filter((file) => existsSync11(join11(mainWorktreePath, file)));
5242
5311
  } catch {
5243
5312
  return [];
5244
5313
  }
@@ -5286,7 +5355,7 @@ function buildPanelFrame(statusResult, selectedIndex, scrollOffset, rows, cols,
5286
5355
  return lines;
5287
5356
  }
5288
5357
  function buildDisplayOrder(worktrees) {
5289
- const worktreeInfos = worktrees.map((wt) => ({ path: wt.path, branch: wt.branch }));
5358
+ const worktreeInfos = worktrees.map((wt) => ({ path: wt.path, branch: wt.branch, baseBranch: wt.baseBranch }));
5290
5359
  const groups = groupWorktreesByDate(worktreeInfos);
5291
5360
  const branchIndexMap = /* @__PURE__ */ new Map();
5292
5361
  for (let i = 0; i < worktrees.length; i++) {
@@ -5302,7 +5371,7 @@ function buildDisplayOrder(worktrees) {
5302
5371
  }
5303
5372
  function buildGroupedWorktreeLines(worktrees, selectedIndex) {
5304
5373
  const panelLines = [];
5305
- const worktreeInfos = worktrees.map((wt) => ({ path: wt.path, branch: wt.branch }));
5374
+ const worktreeInfos = worktrees.map((wt) => ({ path: wt.path, branch: wt.branch, baseBranch: wt.baseBranch }));
5306
5375
  const groups = groupWorktreesByDate(worktreeInfos);
5307
5376
  const branchIndexMap = /* @__PURE__ */ new Map();
5308
5377
  for (let i = 0; i < worktrees.length; i++) {
@@ -5360,6 +5429,7 @@ function renderWorktreeBlock(wt, isSelected) {
5360
5429
  } else {
5361
5430
  lines.push(`${indent}${chalk9.green(PANEL_SYNCED_WITH_MAIN)}`);
5362
5431
  }
5432
+ lines.push(`${indent}${chalk9.gray(formatBaseBranchLine(wt.baseBranch))}`);
5363
5433
  if (wt.createdAt) {
5364
5434
  const relativeTime = formatRelativeTime(wt.createdAt);
5365
5435
  if (relativeTime) {
@@ -6035,9 +6105,9 @@ async function handleMergeConflict(currentBranch, incomingBranch, cwd, autoFlag)
6035
6105
  }
6036
6106
 
6037
6107
  // src/hooks/post-create.ts
6038
- import { existsSync as existsSync11, accessSync, chmodSync, constants as fsConstants } from "fs";
6108
+ import { existsSync as existsSync12, accessSync, chmodSync, constants as fsConstants } from "fs";
6039
6109
  import { spawn as spawn2 } from "child_process";
6040
- import { join as join11 } from "path";
6110
+ import { join as join12 } from "path";
6041
6111
  var POST_CREATE_SCRIPT_RELATIVE_PATH = ".clawt/postCreate.sh";
6042
6112
  function isExecutable(filePath) {
6043
6113
  try {
@@ -6069,8 +6139,8 @@ function resolvePostCreateHook() {
6069
6139
  }
6070
6140
  }
6071
6141
  const mainWorktreePath = getMainWorktreePath();
6072
- const scriptPath = join11(mainWorktreePath, POST_CREATE_SCRIPT_RELATIVE_PATH);
6073
- if (existsSync11(scriptPath)) {
6142
+ const scriptPath = join12(mainWorktreePath, POST_CREATE_SCRIPT_RELATIVE_PATH);
6143
+ if (existsSync12(scriptPath)) {
6074
6144
  if (!isExecutable(scriptPath)) {
6075
6145
  autoFixExecutablePermission(scriptPath);
6076
6146
  }
@@ -6169,7 +6239,8 @@ function printListAsJson(projectName, worktrees) {
6169
6239
  total: worktrees.length,
6170
6240
  worktrees: worktrees.map((wt) => ({
6171
6241
  path: wt.path,
6172
- branch: wt.branch
6242
+ branch: wt.branch,
6243
+ baseBranch: wt.baseBranch
6173
6244
  }))
6174
6245
  };
6175
6246
  console.log(JSON.stringify(result, null, 2));
@@ -6185,7 +6256,7 @@ function printListAsText(projectName, worktrees) {
6185
6256
  const status = getWorktreeStatus(wt);
6186
6257
  const isIdle = status ? isWorktreeIdle(status) : false;
6187
6258
  const pathDisplay = isIdle ? chalk10.hex("#FF8C00")(wt.path) : wt.path;
6188
- printInfo(` ${pathDisplay} [${wt.branch}]`);
6259
+ printInfo(` ${pathDisplay} [${wt.branch}] ${formatBaseBranchInline(wt.baseBranch)}`);
6189
6260
  if (status) {
6190
6261
  printInfo(` ${formatWorktreeStatus(status)}`);
6191
6262
  } else {
@@ -6285,6 +6356,7 @@ async function handleRemove(options) {
6285
6356
  }
6286
6357
  deleteValidateBranch(wt.branch);
6287
6358
  removeSnapshot(projectName, wt.branch);
6359
+ removeWorktreeMetadata(projectName, wt.branch);
6288
6360
  printSuccess(MESSAGES.WORKTREE_REMOVED(wt.path));
6289
6361
  } catch (error) {
6290
6362
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -6825,7 +6897,7 @@ async function shouldCleanupAfterMerge(branchName) {
6825
6897
  return confirmAction(MESSAGES.CONFIRM_DELETE_WORKTREE_BRANCH(branchName), true);
6826
6898
  }
6827
6899
  function cleanupWorktreeAndBranch(worktreePath, branchName) {
6828
- cleanupWorktrees([{ path: worktreePath, branch: branchName }]);
6900
+ cleanupWorktrees([{ path: worktreePath, branch: branchName, baseBranch: null }]);
6829
6901
  printSuccess(MESSAGES.WORKTREE_CLEANED(branchName));
6830
6902
  }
6831
6903
  async function handleMerge(options) {
@@ -7100,7 +7172,8 @@ async function collectWorktreeDetailedStatusAsync(worktree, projectName) {
7100
7172
  snapshotTime: resolveSnapshotTime(projectName, worktree.branch),
7101
7173
  insertions: diffStat.insertions,
7102
7174
  deletions: diffStat.deletions,
7103
- createdAt
7175
+ createdAt,
7176
+ baseBranch: worktree.baseBranch
7104
7177
  };
7105
7178
  }
7106
7179
  function detectChangeStatusFromPorcelain(porcelain, commitsAhead) {
@@ -7225,6 +7298,7 @@ function printWorktreeItem(wt) {
7225
7298
  } else {
7226
7299
  printInfo(` ${chalk11.green(lang === "en" ? "In sync with main" : "\u4E0E\u4E3B\u5206\u652F\u540C\u6B65")}`);
7227
7300
  }
7301
+ printInfo(` ${chalk11.gray(formatBaseBranchLine(wt.baseBranch))}`);
7228
7302
  if (wt.createdAt) {
7229
7303
  const relativeTime = formatRelativeTime(wt.createdAt);
7230
7304
  if (relativeTime) {
@@ -7331,8 +7405,8 @@ function registerAliasCommand(program2) {
7331
7405
  }
7332
7406
 
7333
7407
  // src/commands/projects.ts
7334
- import { existsSync as existsSync12, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
7335
- import { join as join12 } from "path";
7408
+ import { existsSync as existsSync13, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
7409
+ import { join as join13 } from "path";
7336
7410
  import chalk13 from "chalk";
7337
7411
  function registerProjectsCommand(program2) {
7338
7412
  program2.command("projects [name]").description(getCurrentLanguage() === "en" ? "Show worktree overview across projects, or view details for a specific project" : "\u5C55\u793A\u6240\u6709\u9879\u76EE\u7684 worktree \u6982\u89C8\uFF0C\u6216\u67E5\u770B\u6307\u5B9A\u9879\u76EE\u7684 worktree \u8BE6\u60C5").option("--json", getCurrentLanguage() === "en" ? "Output in JSON format" : "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA").action((name, options) => {
@@ -7356,8 +7430,8 @@ function handleProjectsOverview(json) {
7356
7430
  printProjectsOverviewAsText(result);
7357
7431
  }
7358
7432
  function handleProjectDetail(name, json) {
7359
- const projectDir = join12(WORKTREES_DIR, name);
7360
- if (!existsSync12(projectDir)) {
7433
+ const projectDir = join13(WORKTREES_DIR, name);
7434
+ if (!existsSync13(projectDir)) {
7361
7435
  printError(MESSAGES.PROJECTS_NOT_FOUND(name));
7362
7436
  process.exit(1);
7363
7437
  }
@@ -7370,7 +7444,7 @@ function handleProjectDetail(name, json) {
7370
7444
  printProjectDetailAsText(result);
7371
7445
  }
7372
7446
  function collectProjectsOverview() {
7373
- if (!existsSync12(WORKTREES_DIR)) {
7447
+ if (!existsSync13(WORKTREES_DIR)) {
7374
7448
  return { projects: [], totalProjects: 0, totalDiskUsage: 0 };
7375
7449
  }
7376
7450
  const entries = readdirSync6(WORKTREES_DIR, { withFileTypes: true });
@@ -7379,7 +7453,7 @@ function collectProjectsOverview() {
7379
7453
  if (!entry.isDirectory()) {
7380
7454
  continue;
7381
7455
  }
7382
- const projectDir = join12(WORKTREES_DIR, entry.name);
7456
+ const projectDir = join13(WORKTREES_DIR, entry.name);
7383
7457
  const overview = collectSingleProjectOverview(entry.name, projectDir);
7384
7458
  projects.push(overview);
7385
7459
  }
@@ -7396,7 +7470,7 @@ function collectSingleProjectOverview(name, projectDir) {
7396
7470
  const worktreeDirs = subEntries.filter((e) => e.isDirectory());
7397
7471
  const worktreeCount = worktreeDirs.length;
7398
7472
  const diskUsage = calculateDirSize(projectDir);
7399
- const lastActiveTime = resolveProjectLastActiveTime(projectDir, worktreeDirs.map((e) => join12(projectDir, e.name)));
7473
+ const lastActiveTime = resolveProjectLastActiveTime(projectDir, worktreeDirs.map((e) => join13(projectDir, e.name)));
7400
7474
  return {
7401
7475
  name,
7402
7476
  worktreeCount,
@@ -7411,7 +7485,7 @@ function collectProjectDetail(name, projectDir) {
7411
7485
  if (!entry.isDirectory()) {
7412
7486
  continue;
7413
7487
  }
7414
- const wtPath = join12(projectDir, entry.name);
7488
+ const wtPath = join13(projectDir, entry.name);
7415
7489
  const detail = collectSingleWorktreeDetail(entry.name, wtPath);
7416
7490
  worktrees.push(detail);
7417
7491
  }
@@ -7511,7 +7585,7 @@ function printWorktreeDetailItem(wt) {
7511
7585
  }
7512
7586
 
7513
7587
  // src/commands/completion.ts
7514
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync14 } from "fs";
7588
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync15 } from "fs";
7515
7589
  import { resolve as resolve3 } from "path";
7516
7590
  import { homedir as homedir2 } from "os";
7517
7591
 
@@ -7562,23 +7636,23 @@ compdef _clawt_completion clawt
7562
7636
  }
7563
7637
 
7564
7638
  // src/utils/completion-engine.ts
7565
- import { existsSync as existsSync13, readdirSync as readdirSync7, statSync as statSync5 } from "fs";
7566
- import { join as join13, dirname, basename as basename2 } from "path";
7639
+ import { existsSync as existsSync14, readdirSync as readdirSync7, statSync as statSync5 } from "fs";
7640
+ import { join as join14, dirname as dirname2, basename as basename2 } from "path";
7567
7641
  function completeFilePath(partial) {
7568
7642
  const cwd = process.cwd();
7569
7643
  const hasDir = partial.includes("/");
7570
- const searchDir = hasDir ? join13(cwd, dirname(partial)) : cwd;
7644
+ const searchDir = hasDir ? join14(cwd, dirname2(partial)) : cwd;
7571
7645
  const prefix = hasDir ? basename2(partial) : partial;
7572
- if (!existsSync13(searchDir)) {
7646
+ if (!existsSync14(searchDir)) {
7573
7647
  return [];
7574
7648
  }
7575
7649
  const entries = readdirSync7(searchDir);
7576
7650
  const results = [];
7577
- const dirPrefix = hasDir ? dirname(partial) + "/" : "";
7651
+ const dirPrefix = hasDir ? dirname2(partial) + "/" : "";
7578
7652
  for (const entry of entries) {
7579
7653
  if (!entry.startsWith(prefix)) continue;
7580
7654
  if (entry.startsWith(".")) continue;
7581
- const fullPath = join13(searchDir, entry);
7655
+ const fullPath = join14(searchDir, entry);
7582
7656
  try {
7583
7657
  const stat = statSync5(fullPath);
7584
7658
  if (stat.isDirectory()) {
@@ -7667,15 +7741,15 @@ function generateCompletions(program2, args) {
7667
7741
 
7668
7742
  // src/commands/completion.ts
7669
7743
  function appendToFile(filePath, content) {
7670
- if (existsSync14(filePath)) {
7671
- const current = readFileSync6(filePath, "utf-8");
7744
+ if (existsSync15(filePath)) {
7745
+ const current = readFileSync7(filePath, "utf-8");
7672
7746
  if (current.includes("clawt completion")) {
7673
7747
  printInfo(MESSAGES.COMPLETION_INSTALL_EXISTS + ": " + filePath);
7674
7748
  return;
7675
7749
  }
7676
7750
  content = current + content;
7677
7751
  }
7678
- writeFileSync5(filePath, content, "utf-8");
7752
+ writeFileSync6(filePath, content, "utf-8");
7679
7753
  printSuccess(MESSAGES.COMPLETION_INSTALL_SUCCESS + ": " + filePath);
7680
7754
  printInfo(MESSAGES.COMPLETION_INSTALL_RESTART(filePath));
7681
7755
  }
@@ -7807,23 +7881,23 @@ async function handleHome() {
7807
7881
  }
7808
7882
 
7809
7883
  // src/commands/tasks.ts
7810
- import { resolve as resolve4, dirname as dirname2, join as join14 } from "path";
7811
- import { existsSync as existsSync15, writeFileSync as writeFileSync6 } from "fs";
7884
+ import { resolve as resolve4, dirname as dirname3, join as join15 } from "path";
7885
+ import { existsSync as existsSync16, writeFileSync as writeFileSync7 } from "fs";
7812
7886
  function registerTasksCommand(program2) {
7813
7887
  const taskCmd = program2.command("tasks").description(getCurrentLanguage() === "en" ? "Task file management" : "\u4EFB\u52A1\u6587\u4EF6\u7BA1\u7406");
7814
7888
  taskCmd.command("init").description(getCurrentLanguage() === "en" ? "Generate task template file" : "\u751F\u6210\u4EFB\u52A1\u6A21\u677F\u6587\u4EF6").argument("[path]", getCurrentLanguage() === "en" ? "Output file path" : "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84").action(async (path2) => {
7815
- const filePath = path2 ?? join14(TASK_TEMPLATE_OUTPUT_DIR, generateTaskFilename(TASK_TEMPLATE_FILENAME_PREFIX));
7889
+ const filePath = path2 ?? join15(TASK_TEMPLATE_OUTPUT_DIR, generateTaskFilename(TASK_TEMPLATE_FILENAME_PREFIX));
7816
7890
  await handleTasksInit(filePath);
7817
7891
  });
7818
7892
  }
7819
7893
  async function handleTasksInit(filePath) {
7820
7894
  const absolutePath = resolve4(filePath);
7821
7895
  logger.info(`tasks init \u547D\u4EE4\u6267\u884C\uFF0C\u76EE\u6807\u6587\u4EF6: ${absolutePath}`);
7822
- if (existsSync15(absolutePath)) {
7896
+ if (existsSync16(absolutePath)) {
7823
7897
  throw new ClawtError(MESSAGES.TASK_INIT_FILE_EXISTS(filePath));
7824
7898
  }
7825
- ensureDir(dirname2(absolutePath));
7826
- writeFileSync6(absolutePath, getTaskTemplateContent(), "utf-8");
7899
+ ensureDir(dirname3(absolutePath));
7900
+ writeFileSync7(absolutePath, getTaskTemplateContent(), "utf-8");
7827
7901
  printSuccess(MESSAGES.TASK_INIT_SUCCESS(filePath));
7828
7902
  printHint(MESSAGES.TASK_INIT_HINT(filePath));
7829
7903
  }
package/docs/create.md CHANGED
@@ -50,6 +50,7 @@ clawt create -b <branchName> [-n <count>] [--no-post-create]
50
50
  - 若 `n > 1`:校验 `branchName-1` 到 `branchName-n`
51
51
  - 所有分支名在创建任何 worktree **之前**完成全部校验
52
52
  8. **批量创建 worktree + 验证分支**
53
+ - 创建前记录当前所在分支为来源分支(`baseBranch`),保存到 `~/.clawt/projects/<projectName>/worktrees/<branchName>.json`
53
54
  - 若 `n = 1`:
54
55
  ```bash
55
56
  git worktree add -b <branchName> ~/.clawt/worktrees/<project>/<branchName>