clawt 1.2.0 → 1.4.0

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/docs/spec.md CHANGED
@@ -23,6 +23,7 @@
23
23
  - [5.9 日志系统](#59-日志系统)
24
24
  - [5.10 查看全局配置](#510-查看全局配置)
25
25
  - [5.11 在已有 Worktree 中恢复会话](#511-在已有-worktree-中恢复会话)
26
+ - [5.12 将主分支代码同步到目标 Worktree](#512-将主分支代码同步到目标-worktree)
26
27
  - [6. 错误处理规范](#6-错误处理规范)
27
28
  - [7. 非功能性需求](#7-非功能性需求)
28
29
 
@@ -137,6 +138,11 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
137
138
  ├── logs/ # 日志目录
138
139
  │ ├── clawt-2025-02-06.log
139
140
  │ └── ...
141
+ ├── validate-snapshots/ # validate 快照目录
142
+ │ └── <project-name>/ # 以项目名分组
143
+ │ ├── <branchName>.patch # 每个分支一个 patch 快照文件
144
+ │ ├── <branchName>.head # 对应的主分支 HEAD commit hash(用于增量 validate 一致性校验)
145
+ │ └── ...
140
146
  └── worktrees/ # 所有 worktree 的统一存放目录
141
147
  └── <project-name>/ # 以项目名分组
142
148
  ├── <branchName>/ # n=1 时直接使用分支名
@@ -161,6 +167,7 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
161
167
  | `clawt list` | 列出当前项目所有 worktree | 5.8 |
162
168
  | `clawt config` | 查看全局配置 | 5.10 |
163
169
  | `clawt resume` | 在已有 worktree 中恢复 Claude Code 交互式会话 | 5.11 |
170
+ | `clawt sync` | 将主分支最新代码同步到目标 worktree | 5.12 |
164
171
 
165
172
  所有命令执行前,都必须先执行**主 worktree 校验**(见 [2.1](#21-主-worktree-的定义与定位规则))。
166
173
 
@@ -336,24 +343,40 @@ Claude Code CLI 以 `--output-format json` 运行时,退出后会在 stdout
336
343
  **命令:**
337
344
 
338
345
  ```bash
339
- clawt validate -b <branchName>
346
+ clawt validate -b <branchName> [--clean]
340
347
  ```
341
348
 
342
349
  **参数:**
343
350
 
344
- | 参数 | 必填 | 说明 |
345
- | ---- | ---- | ------------------------------------------------------------------------ |
346
- | `-b` | 是 | 要验证的 worktree 分支名(例如 `feature-scheme-1`) |
351
+ | 参数 | 必填 | 说明 |
352
+ | --------- | ---- | ------------------------------------------------------------------------ |
353
+ | `-b` | 是 | 要验证的 worktree 分支名(例如 `feature-scheme-1`) |
354
+ | `--clean` | 否 | 清理 validate 状态(重置主 worktree 并删除快照) |
347
355
 
348
356
  > **限制:** 单次只能验证一个分支,不支持批量验证。
349
357
 
350
358
  **背景说明:**
351
359
 
352
- Git worktree 不会包含 `node_modules`、`.venv` 等依赖文件,每次安装依赖耗时较长。利用 `git stash` 可以在所有 worktree 间共享的特性,将目标 worktree 的变更迁移到主 worktree 进行测试,无需重新安装依赖。
360
+ Git worktree 不会包含 `node_modules`、`.venv` 等依赖文件,每次安装依赖耗时较长。利用 `git diff HEAD...branch --binary`(三点 diff)可以获取目标分支自分叉点以来的全量变更(包含已提交和未提交的修改),将其作为 patch 应用到主 worktree 中进行测试,无需重新安装依赖。
361
+
362
+ **快照机制:**
363
+
364
+ validate 命令引入了**快照(snapshot)机制**来支持增量对比。每次 validate 执行成功后,会将当前全量变更保存为 patch 文件(`~/.clawt/validate-snapshots/<project>/<branchName>.patch`),同时保存主分支 HEAD commit hash 到 `.head` 文件(用于一致性校验)。当再次执行 validate 时,先校验主分支 HEAD 是否与快照记录一致(不一致则清除旧快照降级为首次模式),然后通过对比新旧快照,将上次快照应用到暂存区、最新变更保留在工作目录,用户可通过 `git diff` 直接查看两次 validate 之间的增量差异。
353
365
 
354
366
  **运行流程:**
355
367
 
356
- #### 步骤 1:检测主 worktree 工作区状态
368
+ #### `--clean` 模式
369
+
370
+ 当指定 `--clean` 选项时,执行清理逻辑后直接返回,不进入常规 validate 流程:
371
+
372
+ 1. **主 worktree 校验** (2.1)
373
+ 2. 如果主 worktree 有未提交更改,执行 `git reset --hard` + `git clean -fd` 清空
374
+ 3. 删除对应分支的 patch 快照文件
375
+ 4. 输出清理成功提示
376
+
377
+ #### 首次 validate(无历史快照)
378
+
379
+ ##### 步骤 1:检测主 worktree 工作区状态
357
380
 
358
381
  执行 `git status --porcelain`,判断主 worktree 是否有未提交的更改。
359
382
 
@@ -376,50 +399,110 @@ Git worktree 不会包含 `node_modules`、`.venv` 等依赖文件,每次安
376
399
 
377
400
  执行完毕后,通过 `git status --porcelain` 再次检测状态,确保工作区干净。如果仍然不干净,报错退出。
378
401
 
379
- #### 步骤 2:在目标 worktree 中创建 stash
402
+ ##### 步骤 2:检测目标分支变更
403
+
404
+ 统一检测目标 worktree 的未提交修改和已提交 commit:
380
405
 
381
406
  ```bash
382
- # 定位目标 worktree
407
+ # 检测未提交修改
383
408
  cd ~/.clawt/worktrees/<project>/<branchName>
384
-
385
- # 校验目标 worktree 是否有更改
386
409
  git status --porcelain
410
+
411
+ # 检测已提交 commit(在主 worktree 中执行)
412
+ cd <主 worktree 路径>
413
+ git log HEAD..<branchName> --oneline
387
414
  ```
388
415
 
389
- - **无更改** → 输出提示 `该 worktree 的分支上没有任何更改,无需验证`,退出
390
- - **有更改** → 继续
416
+ - **两者均无** → 输出提示 `该 worktree 的分支上没有任何更改,无需验证`,退出
417
+ - **至少有一项** → 继续
418
+
419
+ ##### 步骤 3:通过 patch 迁移目标分支全量变更
420
+
421
+ 使用三点 diff(`git diff HEAD...branchName --binary`)获取目标分支自分叉点以来的全量变更。如果目标 worktree 有未提交修改,先做临时 commit 以便 diff 能捕获全部变更,diff 完成后撤销临时 commit 恢复原状。
391
422
 
392
423
  ```bash
424
+ # 如果有未提交修改,先临时提交
425
+ cd ~/.clawt/worktrees/<project>/<branchName>
393
426
  git add .
394
- git stash push -m "clawt:validate:<branchName>"
395
- git stash apply
427
+ git commit -m "clawt:temp-commit-for-validate"
428
+
429
+ # 在主 worktree 中执行三点 diff
430
+ cd <主 worktree 路径>
431
+ git diff HEAD...<branchName> --binary | git apply
432
+
433
+ # 撤销临时 commit,恢复目标 worktree 原状
434
+ cd ~/.clawt/worktrees/<project>/<branchName>
435
+ git reset --soft HEAD~1
396
436
  git restore --staged .
397
437
  ```
398
438
 
399
- > 此步骤结束后,目标 worktree 的代码保持原样(变更仍然存在于工作区),同时变更已被记录到共享的 stash 中。
439
+ > 此步骤结束后,目标 worktree 的代码保持原样,主 worktree 工作目录包含目标分支的全量变更。
440
+ > 如果 patch apply 失败(目标分支与主分支差异过大),会提示用户先执行 `clawt sync -b <branchName>` 同步主分支后重试。
400
441
 
401
- #### 步骤 3:在主 worktree 应用 stash
442
+ ##### 步骤 4:保存纯净快照
443
+
444
+ 将主 worktree 工作目录的全量变更保存为 patch 文件,同时记录主分支 HEAD commit hash:
402
445
 
403
446
  ```bash
404
- # 回到主 worktree
405
- cd <主 worktree 路径>
447
+ git add .
448
+ git diff --cached --binary > ~/.clawt/validate-snapshots/<project>/<branchName>.patch
449
+ git rev-parse HEAD > ~/.clawt/validate-snapshots/<project>/<branchName>.head
450
+ git restore --staged .
451
+ ```
406
452
 
407
- # 校验 stash@{0} 是否为我们创建的
408
- git stash list
453
+ > 结果:暂存区=空,工作目录=全量变更。
454
+
455
+ ##### 步骤 5:输出成功提示
456
+
457
+ ```
458
+ ✓ 已将分支 feature-scheme-1 的变更应用到主 worktree
459
+ 可以开始验证了
409
460
  ```
410
461
 
411
- 检查 `stash@{0}` 的消息是否包含 `clawt:validate:<branchName>`:
462
+ #### 增量 validate(存在历史快照)
463
+
464
+ 当 `~/.clawt/validate-snapshots/<project>/<branchName>.patch` 存在时,自动进入增量模式:
465
+
466
+ ##### 步骤 1:校验主分支 HEAD 一致性
467
+
468
+ 读取 `.head` 文件中保存的主分支 HEAD hash,与当前主分支 HEAD 对比:
469
+
470
+ - **不一致或 `.head` 文件不存在** → 清除旧快照(`.patch` + `.head`),降级为首次 validate 模式
471
+ - **一致** → 继续增量流程
472
+
473
+ ##### 步骤 2:读取旧 patch
474
+
475
+ 在清空主 worktree 之前,读取上次保存的快照 patch 内容。
476
+
477
+ ##### 步骤 3:确保主 worktree 干净
478
+
479
+ 如果主 worktree 有残留状态,让用户选择处理方式(同首次 validate 步骤 1 的交互),做兜底清理。
412
480
 
413
- - **不包含** → 报错:`git stash list 已变更,请重新执行`,退出
414
- - **包含** → 继续
481
+ ##### 步骤 4:从目标分支获取最新全量变更
482
+
483
+ 通过 patch 方式从目标分支获取最新全量变更(流程同首次 validate 的步骤 3)。
484
+
485
+ ##### 步骤 5:保存最新快照
486
+
487
+ 将最新全量变更保存为新的 patch 文件 + HEAD hash(覆盖旧快照,流程同首次 validate 的步骤 4)。
488
+
489
+ ##### 步骤 6:将旧 patch 应用到暂存区
415
490
 
416
491
  ```bash
417
- git stash pop stash@{0}
492
+ git apply --cached < <旧 patch 内容>
418
493
  ```
419
494
 
420
- #### 步骤 4:输出成功提示
495
+ - **应用成功** → 结果:暂存区=上次快照,工作目录=最新全量变更(用户可通过 `git diff` 查看增量差异)
496
+ - **应用失败**(文件结构变化过大)→ 降级为全量模式,暂存区保持为空,等同于首次 validate 的结果
497
+
498
+ ##### 步骤 7:输出成功提示
421
499
 
422
500
  ```
501
+ # 增量模式成功
502
+ ✓ 已将分支 feature-scheme-1 的最新变更应用到主 worktree(增量模式)
503
+ 暂存区 = 上次快照,工作目录 = 最新变更
504
+
505
+ # 增量降级为全量
423
506
  ✓ 已将分支 feature-scheme-1 的变更应用到主 worktree
424
507
  可以开始验证了
425
508
  ```
@@ -503,7 +586,9 @@ clawt merge -b <branchName> [-m <commitMessage>]
503
586
  1. **主 worktree 校验** (2.1)
504
587
  2. **主 worktree 状态检测**
505
588
  - 执行 `git status --porcelain`
506
- - 如果有更改 → 提示 `主 worktree 有未提交的更改,请先处理`,退出
589
+ - 如果有更改:
590
+ - 如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.patch`),额外输出警告提示用户可先执行 `clawt validate -b <branchName> --clean` 清理
591
+ - 提示 `主 worktree 有未提交的更改,请先处理`,退出
507
592
  - 无更改 → 继续
508
593
  3. **根据目标 worktree 状态决定是否需要提交**
509
594
  - 检测目标 worktree 工作区是否干净(`git status --porcelain`)
@@ -561,6 +646,9 @@ clawt merge -b <branchName> [-m <commitMessage>]
561
646
  ```
562
647
  - 输出清理成功提示:`✓ 已清理 worktree 和分支: <branchName>`
563
648
 
649
+ 9. **清理 validate 快照**
650
+ - merge 成功后,如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.patch`),自动删除该快照文件(merge 成功后快照已无意义)
651
+
564
652
  > **注意:** 清理确认在 merge 操作之前询问(避免 merge 成功后因交互中断而遗留未清理的 worktree),但清理操作在 merge 成功后才执行。
565
653
 
566
654
  ---
@@ -739,6 +827,54 @@ clawt resume -b <branchName>
739
827
 
740
828
  ---
741
829
 
830
+ ### 5.12 将主分支代码同步到目标 Worktree
831
+
832
+ **命令:**
833
+
834
+ ```bash
835
+ clawt sync -b <branchName>
836
+ ```
837
+
838
+ **参数:**
839
+
840
+ | 参数 | 必填 | 说明 |
841
+ | ---- | ---- | ----------------------------------------------------- |
842
+ | `-b` | 是 | 要同步的分支名(对应已有 worktree 的分支) |
843
+
844
+ **使用场景:**
845
+
846
+ 当目标 worktree 的分支与主分支差异过大(例如主分支有了新的提交),导致 `clawt validate` 的 patch apply 失败时,可以通过 `clawt sync` 将主分支最新代码合并到目标 worktree,使其保持与主分支同步。
847
+
848
+ **运行流程:**
849
+
850
+ 1. **主 worktree 校验** (2.1)
851
+ 2. **检查目标 worktree 是否存在**:确认 `~/.clawt/worktrees/<project>/<branchName>` 目录存在
852
+ - 不存在 → 报错退出
853
+ 3. **获取主分支名**:通过 `git rev-parse --abbrev-ref HEAD` 获取主 worktree 当前分支名(不硬编码 main/master)
854
+ 4. **自动保存未提交变更**:检查目标 worktree 是否有未提交修改
855
+ - 有修改 → 自动执行 `git add . && git commit -m "chore: auto-save before sync"` 保存变更
856
+ - 无修改 → 跳过
857
+ 5. **在目标 worktree 中合并主分支**:
858
+ ```bash
859
+ cd ~/.clawt/worktrees/<project>/<branchName>
860
+ git merge <mainBranch>
861
+ ```
862
+ 6. **冲突处理**:
863
+ - **有冲突** → 输出警告,提示用户进入目标 worktree 手动解决:
864
+ ```
865
+ 合并存在冲突,请进入目标 worktree 手动解决:
866
+ cd ~/.clawt/worktrees/<project>/<branchName>
867
+ 解决冲突后执行 git add . && git merge --continue
868
+ ```
869
+ - **无冲突** → 继续
870
+ 7. **清除 validate 快照**:合并成功后,如果该分支存在 validate 快照(`.patch` + `.head`),自动删除(代码基础已变化,旧快照无效)
871
+ 8. **输出成功提示**:
872
+ ```
873
+ ✓ 已将 <mainBranch> 的最新代码同步到 <branchName>
874
+ ```
875
+
876
+ ---
877
+
742
878
  ## 6. 错误处理规范
743
879
 
744
880
  ### 6.1 通用错误处理
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -7,6 +7,7 @@ import { MESSAGES } from '../constants/index.js';
7
7
  import type { MergeOptions } from '../types/index.js';
8
8
  import {
9
9
  validateMainWorktree,
10
+ getProjectName,
10
11
  getGitTopLevel,
11
12
  getProjectWorktreeDir,
12
13
  isWorkingDirClean,
@@ -17,8 +18,11 @@ import {
17
18
  gitPull,
18
19
  gitPush,
19
20
  hasLocalCommits,
21
+ hasSnapshot,
22
+ removeSnapshot,
20
23
  printSuccess,
21
24
  printInfo,
25
+ printWarning,
22
26
  getConfigValue,
23
27
  confirmAction,
24
28
  cleanupWorktrees,
@@ -82,8 +86,14 @@ async function handleMerge(options: MergeOptions): Promise<void> {
82
86
  throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
83
87
  }
84
88
 
89
+ const projectName = getProjectName();
90
+
85
91
  // 步骤 3:主 worktree 状态检测
86
92
  if (!isWorkingDirClean(mainWorktreePath)) {
93
+ // 如果存在 validate 快照状态,提示用户先清理
94
+ if (hasSnapshot(projectName, options.branch)) {
95
+ printWarning(MESSAGES.MERGE_VALIDATE_STATE_HINT(options.branch));
96
+ }
87
97
  throw new ClawtError(MESSAGES.MAIN_WORKTREE_DIRTY);
88
98
  }
89
99
 
@@ -143,4 +153,9 @@ async function handleMerge(options: MergeOptions): Promise<void> {
143
153
  if (shouldCleanup) {
144
154
  cleanupWorktreeAndBranch(targetWorktreePath, options.branch);
145
155
  }
156
+
157
+ // 步骤 10:清理 validate 快照(merge 成功后快照已无意义)
158
+ if (hasSnapshot(projectName, options.branch)) {
159
+ removeSnapshot(projectName, options.branch);
160
+ }
146
161
  }
@@ -0,0 +1,116 @@
1
+ import { existsSync } from 'node:fs';
2
+ import type { Command } from 'commander';
3
+ import { logger } from '../logger/index.js';
4
+ import { ClawtError } from '../errors/index.js';
5
+ import { MESSAGES } from '../constants/index.js';
6
+ import type { SyncOptions } from '../types/index.js';
7
+ import {
8
+ validateMainWorktree,
9
+ getGitTopLevel,
10
+ getProjectName,
11
+ getProjectWorktreeDir,
12
+ isWorkingDirClean,
13
+ gitAddAll,
14
+ gitCommit,
15
+ gitMerge,
16
+ hasMergeConflict,
17
+ getCurrentBranch,
18
+ hasSnapshot,
19
+ removeSnapshot,
20
+ printSuccess,
21
+ printInfo,
22
+ printWarning,
23
+ } from '../utils/index.js';
24
+
25
+ /**
26
+ * 注册 sync 命令:将主分支最新代码同步到目标 worktree
27
+ * @param {Command} program - Commander 实例
28
+ */
29
+ export function registerSyncCommand(program: Command): void {
30
+ program
31
+ .command('sync')
32
+ .description('将主分支最新代码同步到目标 worktree')
33
+ .requiredOption('-b, --branch <branchName>', '要同步的分支名')
34
+ .action(async (options: SyncOptions) => {
35
+ await handleSync(options);
36
+ });
37
+ }
38
+
39
+ /**
40
+ * 自动保存目标 worktree 中的未提交变更
41
+ * @param {string} worktreePath - 目标 worktree 路径
42
+ * @param {string} branch - 分支名
43
+ */
44
+ function autoSaveChanges(worktreePath: string, branch: string): void {
45
+ gitAddAll(worktreePath);
46
+ gitCommit('chore: auto-save before sync', worktreePath);
47
+ printInfo(MESSAGES.SYNC_AUTO_COMMITTED(branch));
48
+ logger.info(`已自动保存 ${branch} 分支的未提交变更`);
49
+ }
50
+
51
+ /**
52
+ * 在目标 worktree 中合并主分支
53
+ * @param {string} worktreePath - 目标 worktree 路径
54
+ * @param {string} mainBranch - 主分支名
55
+ * @returns {boolean} 是否存在冲突(true 表示有冲突)
56
+ */
57
+ function mergeMainBranch(worktreePath: string, mainBranch: string): boolean {
58
+ try {
59
+ gitMerge(mainBranch, worktreePath);
60
+ return false;
61
+ } catch {
62
+ // 合并失败时检查是否为冲突
63
+ if (hasMergeConflict(worktreePath)) {
64
+ return true;
65
+ }
66
+ // 非冲突错误则向上抛出
67
+ throw new ClawtError(`合并 ${mainBranch} 失败`);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * 执行 sync 命令的核心逻辑
73
+ * 将主分支最新代码同步到目标 worktree
74
+ * @param {SyncOptions} options - 命令选项
75
+ */
76
+ async function handleSync(options: SyncOptions): Promise<void> {
77
+ validateMainWorktree();
78
+
79
+ const { branch } = options;
80
+ logger.info(`sync 命令执行,分支: ${branch}`);
81
+
82
+ // 检查目标 worktree 是否存在
83
+ const projectWorktreeDir = getProjectWorktreeDir();
84
+ const targetWorktreePath = `${projectWorktreeDir}/${branch}`;
85
+
86
+ if (!existsSync(targetWorktreePath)) {
87
+ throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(branch));
88
+ }
89
+
90
+ // 获取主分支名(不硬编码 main/master)
91
+ const mainWorktreePath = getGitTopLevel();
92
+ const mainBranch = getCurrentBranch(mainWorktreePath);
93
+
94
+ // 检查目标 worktree 是否有未提交变更,有则自动保存
95
+ if (!isWorkingDirClean(targetWorktreePath)) {
96
+ autoSaveChanges(targetWorktreePath, branch);
97
+ }
98
+
99
+ // 在目标 worktree 中合并主分支
100
+ printInfo(MESSAGES.SYNC_MERGING(branch, mainBranch));
101
+ const hasConflict = mergeMainBranch(targetWorktreePath, mainBranch);
102
+
103
+ if (hasConflict) {
104
+ printWarning(MESSAGES.SYNC_CONFLICT(targetWorktreePath));
105
+ return;
106
+ }
107
+
108
+ // 合并成功后清除该分支的 validate 快照(代码基础已变化,旧快照无效)
109
+ const projectName = getProjectName();
110
+ if (hasSnapshot(projectName, branch)) {
111
+ removeSnapshot(projectName, branch);
112
+ logger.info(`已清除分支 ${branch} 的 validate 快照`);
113
+ }
114
+
115
+ printSuccess(MESSAGES.SYNC_SUCCESS(branch, mainBranch));
116
+ }