clawt 2.1.0 → 2.3.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.
@@ -4,7 +4,7 @@
4
4
 
5
5
  ### docs/spec.md
6
6
  - 完整的软件规格说明,包含 7 大章节
7
- - 命令流程在 `5. 需求场景详细设计` 下,每个命令一个子章节(5.1-5.11
7
+ - 命令流程在 `5. 需求场景详细设计` 下,每个命令一个子章节(5.1-5.13
8
8
  - run 命令对应 `5.2 批量创建 Worktree + 执行 Claude Code 任务`,流程按步骤编号描述
9
9
  - merge 命令对应 `5.6 合并验证过的分支`,流程按步骤编号描述
10
10
  - config 命令对应 `5.10 查看全局配置`,只读展示配置
@@ -60,24 +60,26 @@ run 命令有两种模式(自 claudeCodeCommand 特性后):
60
60
  - 传 `--tasks`:并行任务模式(多 worktree + `executeClaudeTask` + spawnProcess)
61
61
  - CLAUDE.md 中的核心流程按模式分段描述
62
62
 
63
- ## 命令清单(8 个)
63
+ ## 命令清单(10 个)
64
64
 
65
- `create`、`run`、`resume`、`list`、`remove`、`validate`、`merge`、`config`
65
+ `create`、`run`、`resume`、`list`、`remove`、`validate`、`merge`、`config`、`sync`、`reset`
66
66
 
67
67
  Notes:
68
68
  - resume 和 run(交互式模式)共用 `launchInteractiveClaude()`,该函数从 run.ts 提取到 src/utils/claude.ts
69
69
  - `claudeCodeCommand` 配置项同时影响 run 交互式模式和 resume 命令
70
+ - reset 命令与 validate --clean 的区别:reset 不删除快照文件,validate --clean 会删除快照
70
71
 
71
72
  ## validate 快照机制
72
73
 
73
74
  - validate 命令支持首次/增量两种模式,通过 `hasSnapshot()` 判断
74
- - 快照路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.patch`
75
+ - 快照路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.tree`(存储 git tree 对象 hash)
75
76
  - 常量 `VALIDATE_SNAPSHOTS_DIR` 定义在 `src/constants/paths.ts`
76
77
  - validate 新增 `--clean` 选项(`ValidateOptions.clean?: boolean`)
77
- - 增量模式核心:旧 patch 应用到暂存区 + 新全量变更在工作目录`git diff` 可查看增量差异
78
- - 增量 apply 失败时自动降级为全量模式
79
- - shell 层新增 `execCommandWithInput()`(`execFileSync` + stdin),用于 `gitApplyCachedFromStdin()`
80
- - git 层新增 `gitDiffCachedBinary()`(返回 Buffer)和 `gitApplyCachedFromStdin()`
78
+ - 快照保存:`git add . git write-tree → git restore --staged .`,将 tree hash 写入 `.tree` 文件
79
+ - 增量模式核心:`git read-tree <旧 tree hash>` 将旧快照载入暂存区 + 新全量变更在工作目录 → `git diff` 可查看增量差异
80
+ - tree 对象不依赖主分支 HEAD,无需一致性校验(旧方案需要 `.head` 文件校验 HEAD 一致性)
81
+ - 增量 read-tree 失败时自动降级为全量模式(tree 对象可能被 git gc 回收)
82
+ - git 层有 `gitWriteTree()`(返回 tree hash)和 `gitReadTree()`(载入暂存区)
81
83
  - merge 成功后自动清理对应快照;merge 时主 worktree 脏 + 存在快照会输出警告提示
82
84
  - docs/spec.md 中 validate 章节(5.4)按 `--clean 模式`、`首次 validate`、`增量 validate` 三段描述
83
85
  - CLAUDE.md 中在 validate + merge 工作流章节用缩进列表描述两种模式
package/CLAUDE.md CHANGED
@@ -24,7 +24,7 @@ npm i -g . # 本地全局安装进行测试
24
24
 
25
25
  每个命令为独立文件 `src/commands/<name>.ts`,导出 `registerXxxCommand(program)` 函数,在 `src/index.ts` 中统一注册到 Commander。命令内部逻辑封装在对应的 `handleXxx` 函数中。
26
26
 
27
- 九个命令:`create`、`run`、`resume`、`list`、`remove`、`validate`、`merge`、`config`、`sync`。
27
+ 十个命令:`create`、`run`、`resume`、`list`、`remove`、`validate`、`merge`、`config`、`sync`、`reset`。
28
28
 
29
29
  ### 核心流程(run 命令)
30
30
 
@@ -56,10 +56,11 @@ run 命令有两种模式:
56
56
  ### validate + merge 工作流
57
57
 
58
58
  - `validate`:将目标分支的全量变更(已提交 + 未提交)通过 `git diff HEAD...branch --binary` 的 patch 方式迁移到主 worktree,便于在主 worktree 中测试。支持两种模式:
59
- - **首次 validate**(无历史快照):patch 迁移全量变更 → 保存纯净快照 patch + 主分支 HEAD hash → 结果:暂存区=空,工作目录=全量变更
60
- - **增量 validate**(存在历史快照):校验主分支 HEAD 一致性(不一致则清除旧快照降级为首次模式)→ 读取旧 patch → 确保主 worktree 干净 → patch 迁移最新变更 → 保存新快照 patch 应用到暂存区 → 结果:暂存区=上次快照,工作目录=最新变更(可通过 `git diff` 查看增量差异)
59
+ - **首次 validate**(无历史快照):patch 迁移全量变更 → 通过 `git write-tree` 保存快照为 git tree 对象 → 结果:暂存区=空,工作目录=全量变更
60
+ - **增量 validate**(存在历史快照):读取旧 tree hash → 确保主 worktree 干净 → patch 迁移最新变更 → 保存新 tree 对象快照 `git read-tree` 将旧 tree 载入暂存区 → 结果:暂存区=上次快照,工作目录=最新变更(可通过 `git diff` 查看增量差异)
61
61
  - `--clean` 选项:重置主 worktree + 删除对应快照文件
62
- - 快照存储路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.patch`(patch 文件)+ `<branchName>.head`(主分支 HEAD hash)
62
+ - 快照存储路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.tree`(存储 git tree 对象 hash)
63
+ - tree 对象不依赖主分支 HEAD,无需一致性校验
63
64
  - 变更检测:同时检测目标 worktree 的未提交修改和已提交 commit,两者均无则提示无需验证
64
65
  - 未提交修改处理:有未提交修改时先做临时 commit,diff 完成后通过 `git reset --soft` 撤销恢复原状
65
66
  - `merge`:检测目标 worktree 状态(有修改则需 `-m` 提交,已提交则跳过,无变更则报错)→ **squash 检测**(检查目标分支是否存在 `AUTO_SAVE_COMMIT_MESSAGE` 前缀的 auto-save commit,如有则提示用户是否压缩所有提交:用户确认后通过 `gitMergeBase` 计算分叉点、`gitResetSoftTo` 将所有 commit reset 到暂存区;有 `-m` 则直接提交继续流程,无 `-m` 则提示用户自行提交后退出)→ 合并到主 worktree → pull → push → 可选清理 worktree 和分支(受 `autoDeleteBranch` 配置或交互式确认控制)→ 清理对应的 validate 快照
@@ -75,11 +76,18 @@ run 命令有两种模式:
75
76
  6. 冲突处理:有冲突时提示用户手动解决,无冲突则输出成功
76
77
  7. 合并成功后清除该分支的 validate 快照(代码基础已变化,旧快照无效)
77
78
 
79
+ ### reset 命令流程
80
+
81
+ 1. `validateMainWorktree()` 确认在主 worktree 根目录
82
+ 2. 检测主 worktree 工作区和暂存区是否干净(`isWorkingDirClean()`)
83
+ 3. 不干净 → `gitResetHard()` + `gitCleanForce()` 重置工作区和暂存区(保留 validate 快照)
84
+ 4. 已干净 → 提示无需重置
85
+
78
86
  ### 目录层级
79
87
 
80
88
  - `src/commands/` — 各命令的注册与处理逻辑
81
- - `src/utils/` — 工具函数(git 操作(含三点 diff、分支合并、冲突检测、merge-base 计算、commit message 检测、soft reset 到指定 commit 等)、shell 执行与子进程管理、分支名处理、worktree 管理与批量清理、配置、格式化输出、交互式输入、Claude Code 交互式启动、validate 快照管理(含 HEAD hash 一致性校验))
82
- - `src/constants/` — 常量定义(路径、退出码、消息模板、分支规则、配置默认值、终端控制序列、validate 快照目录、sync 相关消息、git 常量(如 `AUTO_SAVE_COMMIT_MESSAGE`)、squash 相关消息)
89
+ - `src/utils/` — 工具函数(git 操作(含三点 diff、分支合并、冲突检测、merge-base 计算、commit message 检测、soft reset 到指定 commit、write-tree/read-tree 等)、shell 执行与子进程管理、分支名处理、worktree 管理与批量清理、配置、格式化输出、交互式输入、Claude Code 交互式启动、validate 快照管理(基于 git tree 对象))
90
+ - `src/constants/` — 常量定义(路径、退出码、消息模板、分支规则、配置默认值、终端控制序列、validate 快照目录、sync 相关消息、git 常量(如 `AUTO_SAVE_COMMIT_MESSAGE`)、squash 相关消息、reset 相关消息)
83
91
  - `src/types/` — TypeScript 类型定义
84
92
  - `src/errors/` — 自定义 `ClawtError` 错误类(携带退出码)
85
93
  - `src/logger/` — winston 日志(按日期滚动,写入 `~/.clawt/logs/`)
package/README.md CHANGED
@@ -103,7 +103,7 @@ clawt validate -b <branchName> [--clean]
103
103
 
104
104
  将目标 worktree 的变更通过 `git diff`(三点 diff)迁移到主 worktree,方便在主 worktree 中直接测试,无需重新安装依赖。同时检测未提交修改和已提交 commit,确保所有变更都能被捕获。
105
105
 
106
- 支持增量模式:首次 validate 后会自动保存快照(patch + 主分支 HEAD hash),再次 validate 同一分支时会先校验主分支 HEAD 一致性(不一致则降级为首次模式),然后将上次快照应用到暂存区、最新变更保留在工作目录,用户可通过 `git diff` 查看两次 validate 之间的增量差异。使用 `--clean` 可清理 validate 状态(重置主 worktree 并删除快照文件)。
106
+ 支持增量模式:首次 validate 后会自动保存快照(通过 `git write-tree` 将变更存储为 git tree 对象),再次 validate 同一分支时会通过 `git read-tree` 将上次快照载入暂存区、最新变更保留在工作目录,用户可通过 `git diff` 查看两次 validate 之间的增量差异。使用 `--clean` 可清理 validate 状态(重置主 worktree 并删除快照文件)。
107
107
 
108
108
  > **提示:** 如果 validate 时 patch apply 失败(目标分支与主分支差异过大),可先执行 `clawt sync -b <branchName>` 同步主分支后重试。
109
109
 
@@ -187,6 +187,19 @@ clawt list
187
187
 
188
188
  列出当前项目在 `~/.clawt/worktrees/` 下的所有 worktree 及对应分支。
189
189
 
190
+ ### `clawt reset` — 重置主 worktree 工作区和暂存区
191
+
192
+ ```bash
193
+ clawt reset
194
+ ```
195
+
196
+ 重置主 worktree 的工作区和暂存区(`git reset --hard` + `git clean -f`),恢复到干净状态。与 `clawt validate --clean` 不同,`reset` 不会删除 validate 快照文件,适用于只想清空变更而保留快照以便后续增量 validate 的场景。如果工作区和暂存区已是干净状态,会提示无需重置。
197
+
198
+ ```bash
199
+ # 重置主 worktree 工作区和暂存区
200
+ clawt reset
201
+ ```
202
+
190
203
  ### `clawt config` — 查看全局配置
191
204
 
192
205
  ```bash
package/dist/index.js CHANGED
@@ -111,7 +111,11 @@ var MESSAGES = {
111
111
  MERGE_SQUASH_PENDING: (worktreePath, branch) => `\u2713 \u5DF2\u5C06\u6240\u6709\u63D0\u4EA4\u538B\u7F29\u5230\u6682\u5B58\u533A
112
112
  \u8BF7\u5728\u76EE\u6807 worktree \u4E2D\u63D0\u4EA4\u540E\u91CD\u65B0\u6267\u884C merge\uFF1A
113
113
  cd ${worktreePath}
114
- \u63D0\u4EA4\u5B8C\u6210\u540E\u6267\u884C\uFF1Aclawt merge -b ${branch}`
114
+ \u63D0\u4EA4\u5B8C\u6210\u540E\u6267\u884C\uFF1Aclawt merge -b ${branch}`,
115
+ /** reset 成功 */
116
+ RESET_SUCCESS: "\u2713 \u4E3B worktree \u5DE5\u4F5C\u533A\u548C\u6682\u5B58\u533A\u5DF2\u91CD\u7F6E",
117
+ /** reset 时工作区和暂存区已干净 */
118
+ RESET_ALREADY_CLEAN: "\u4E3B worktree \u5DE5\u4F5C\u533A\u548C\u6682\u5B58\u533A\u5DF2\u662F\u5E72\u51C0\u72B6\u6001\uFF0C\u65E0\u9700\u91CD\u7F6E"
115
119
  };
116
120
 
117
121
  // src/constants/exitCodes.ts
@@ -341,22 +345,9 @@ function getDiffStat(worktreePath) {
341
345
  const output = execCommand("git diff --shortstat HEAD", { cwd: worktreePath });
342
346
  return parseShortStat(output);
343
347
  }
344
- function gitDiffCachedBinary(cwd) {
345
- logger.debug(`\u6267\u884C\u547D\u4EE4: git diff --cached --binary${cwd ? ` (cwd: ${cwd})` : ""}`);
346
- return execSync2("git diff --cached --binary", {
347
- cwd,
348
- stdio: ["pipe", "pipe", "pipe"]
349
- });
350
- }
351
- function gitApplyCachedFromStdin(patchContent, cwd) {
352
- execCommandWithInput("git", ["apply", "--cached"], { input: patchContent, cwd });
353
- }
354
348
  function getCurrentBranch(cwd) {
355
349
  return execCommand("git rev-parse --abbrev-ref HEAD", { cwd });
356
350
  }
357
- function getHeadCommitHash(cwd) {
358
- return execCommand("git rev-parse HEAD", { cwd });
359
- }
360
351
  function gitDiffBinaryAgainstBranch(branchName, cwd) {
361
352
  logger.debug(`\u6267\u884C\u547D\u4EE4: git diff HEAD...${branchName} --binary${cwd ? ` (cwd: ${cwd})` : ""}`);
362
353
  return execSync2(`git diff HEAD...${branchName} --binary`, {
@@ -385,6 +376,12 @@ function hasCommitWithMessage(branchName, messagePrefix, cwd) {
385
376
  function gitResetSoftTo(commitHash, cwd) {
386
377
  execCommand(`git reset --soft ${commitHash}`, { cwd });
387
378
  }
379
+ function gitWriteTree(cwd) {
380
+ return execCommand("git write-tree", { cwd });
381
+ }
382
+ function gitReadTree(treeHash, cwd) {
383
+ execCommand(`git read-tree ${treeHash}`, { cwd });
384
+ }
388
385
 
389
386
  // src/utils/formatter.ts
390
387
  import chalk from "chalk";
@@ -638,27 +635,21 @@ function launchInteractiveClaude(worktree) {
638
635
  import { join as join3 } from "path";
639
636
  import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync3, rmdirSync as rmdirSync2 } from "fs";
640
637
  function getSnapshotPath(projectName, branchName) {
641
- return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.patch`);
642
- }
643
- function getSnapshotHeadPath(projectName, branchName) {
644
- return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
638
+ return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
645
639
  }
646
640
  function hasSnapshot(projectName, branchName) {
647
641
  return existsSync5(getSnapshotPath(projectName, branchName));
648
642
  }
649
- function readSnapshot(projectName, branchName) {
643
+ function readSnapshotTreeHash(projectName, branchName) {
650
644
  const snapshotPath = getSnapshotPath(projectName, branchName);
651
645
  logger.debug(`\u8BFB\u53D6 validate \u5FEB\u7167: ${snapshotPath}`);
652
- return readFileSync2(snapshotPath);
646
+ return readFileSync2(snapshotPath, "utf-8").trim();
653
647
  }
654
- function writeSnapshot(projectName, branchName, patch, headHash) {
648
+ function writeSnapshot(projectName, branchName, treeHash) {
655
649
  const snapshotPath = getSnapshotPath(projectName, branchName);
656
650
  const snapshotDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
657
651
  ensureDir(snapshotDir);
658
- writeFileSync2(snapshotPath, patch);
659
- if (headHash) {
660
- writeFileSync2(getSnapshotHeadPath(projectName, branchName), headHash, "utf-8");
661
- }
652
+ writeFileSync2(snapshotPath, treeHash, "utf-8");
662
653
  logger.info(`\u5DF2\u4FDD\u5B58 validate \u5FEB\u7167: ${snapshotPath}`);
663
654
  }
664
655
  function removeSnapshot(projectName, branchName) {
@@ -667,17 +658,6 @@ function removeSnapshot(projectName, branchName) {
667
658
  unlinkSync(snapshotPath);
668
659
  logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${snapshotPath}`);
669
660
  }
670
- const headPath = getSnapshotHeadPath(projectName, branchName);
671
- if (existsSync5(headPath)) {
672
- unlinkSync(headPath);
673
- }
674
- }
675
- function readSnapshotHead(projectName, branchName) {
676
- const headPath = getSnapshotHeadPath(projectName, branchName);
677
- if (!existsSync5(headPath)) {
678
- return null;
679
- }
680
- return readFileSync2(headPath, "utf-8").trim();
681
661
  }
682
662
 
683
663
  // src/commands/list.ts
@@ -1052,13 +1032,12 @@ function migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName
1052
1032
  }
1053
1033
  }
1054
1034
  }
1055
- function saveCurrentSnapshotPatch(mainWorktreePath, projectName, branchName) {
1035
+ function saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName) {
1056
1036
  gitAddAll(mainWorktreePath);
1057
- const patch = gitDiffCachedBinary(mainWorktreePath);
1037
+ const treeHash = gitWriteTree(mainWorktreePath);
1058
1038
  gitRestoreStaged(mainWorktreePath);
1059
- const headHash = getHeadCommitHash(mainWorktreePath);
1060
- writeSnapshot(projectName, branchName, patch, headHash);
1061
- return patch;
1039
+ writeSnapshot(projectName, branchName, treeHash);
1040
+ return treeHash;
1062
1041
  }
1063
1042
  function handleValidateClean(options) {
1064
1043
  validateMainWorktree();
@@ -1074,26 +1053,24 @@ function handleValidateClean(options) {
1074
1053
  }
1075
1054
  function handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
1076
1055
  migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
1077
- saveCurrentSnapshotPatch(mainWorktreePath, projectName, branchName);
1056
+ saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
1078
1057
  printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
1079
1058
  }
1080
1059
  function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
1081
- const oldPatch = readSnapshot(projectName, branchName);
1060
+ const oldTreeHash = readSnapshotTreeHash(projectName, branchName);
1082
1061
  if (!isWorkingDirClean(mainWorktreePath)) {
1083
1062
  gitResetHard(mainWorktreePath);
1084
1063
  gitCleanForce(mainWorktreePath);
1085
1064
  }
1086
1065
  migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
1087
- saveCurrentSnapshotPatch(mainWorktreePath, projectName, branchName);
1088
- if (oldPatch.length > 0) {
1089
- try {
1090
- gitApplyCachedFromStdin(oldPatch, mainWorktreePath);
1091
- } catch (error) {
1092
- logger.warn(`\u589E\u91CF apply \u5931\u8D25: ${error}`);
1093
- printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
1094
- printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
1095
- return;
1096
- }
1066
+ saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
1067
+ try {
1068
+ gitReadTree(oldTreeHash, mainWorktreePath);
1069
+ } catch (error) {
1070
+ logger.warn(`\u589E\u91CF read-tree \u5931\u8D25: ${error}`);
1071
+ printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
1072
+ printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
1073
+ return;
1097
1074
  }
1098
1075
  printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
1099
1076
  }
@@ -1117,16 +1094,7 @@ async function handleValidate(options) {
1117
1094
  printInfo(MESSAGES.TARGET_WORKTREE_CLEAN);
1118
1095
  return;
1119
1096
  }
1120
- let isIncremental = hasSnapshot(projectName, options.branch);
1121
- if (isIncremental) {
1122
- const savedHead = readSnapshotHead(projectName, options.branch);
1123
- const currentHead = getHeadCommitHash(mainWorktreePath);
1124
- if (!savedHead || savedHead !== currentHead) {
1125
- logger.info(`\u4E3B\u5206\u652F HEAD \u4E0D\u5339\u914D (${savedHead ?? "null"} \u2192 ${currentHead})\uFF0C\u6E05\u9664\u65E7\u5FEB\u7167`);
1126
- removeSnapshot(projectName, options.branch);
1127
- isIncremental = false;
1128
- }
1129
- }
1097
+ const isIncremental = hasSnapshot(projectName, options.branch);
1130
1098
  if (isIncremental) {
1131
1099
  if (!isWorkingDirClean(mainWorktreePath)) {
1132
1100
  await handleDirtyMainWorktree(mainWorktreePath);
@@ -1329,6 +1297,25 @@ async function handleSync(options) {
1329
1297
  printSuccess(MESSAGES.SYNC_SUCCESS(branch, mainBranch));
1330
1298
  }
1331
1299
 
1300
+ // src/commands/reset.ts
1301
+ function registerResetCommand(program2) {
1302
+ program2.command("reset").description("\u91CD\u7F6E\u4E3B worktree \u5DE5\u4F5C\u533A\u548C\u6682\u5B58\u533A\uFF08\u4FDD\u7559 validate \u5FEB\u7167\uFF09").action(() => {
1303
+ handleReset();
1304
+ });
1305
+ }
1306
+ function handleReset() {
1307
+ validateMainWorktree();
1308
+ const mainWorktreePath = getGitTopLevel();
1309
+ logger.info("reset \u547D\u4EE4\u6267\u884C");
1310
+ if (!isWorkingDirClean(mainWorktreePath)) {
1311
+ gitResetHard(mainWorktreePath);
1312
+ gitCleanForce(mainWorktreePath);
1313
+ printSuccess(MESSAGES.RESET_SUCCESS);
1314
+ } else {
1315
+ printInfo(MESSAGES.RESET_ALREADY_CLEAN);
1316
+ }
1317
+ }
1318
+
1332
1319
  // src/index.ts
1333
1320
  var require2 = createRequire(import.meta.url);
1334
1321
  var { version } = require2("../package.json");
@@ -1344,6 +1331,7 @@ registerValidateCommand(program);
1344
1331
  registerMergeCommand(program);
1345
1332
  registerConfigCommand(program);
1346
1333
  registerSyncCommand(program);
1334
+ registerResetCommand(program);
1347
1335
  process.on("uncaughtException", (error) => {
1348
1336
  if (error instanceof ClawtError) {
1349
1337
  printError(error.message);
package/docs/spec.md CHANGED
@@ -24,6 +24,7 @@
24
24
  - [5.10 查看全局配置](#510-查看全局配置)
25
25
  - [5.11 在已有 Worktree 中恢复会话](#511-在已有-worktree-中恢复会话)
26
26
  - [5.12 将主分支代码同步到目标 Worktree](#512-将主分支代码同步到目标-worktree)
27
+ - [5.13 重置主 Worktree 工作区和暂存区](#513-重置主-worktree-工作区和暂存区)
27
28
  - [6. 错误处理规范](#6-错误处理规范)
28
29
  - [7. 非功能性需求](#7-非功能性需求)
29
30
 
@@ -140,8 +141,7 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
140
141
  │ └── ...
141
142
  ├── validate-snapshots/ # validate 快照目录
142
143
  │ └── <project-name>/ # 以项目名分组
143
- │ ├── <branchName>.patch # 每个分支一个 patch 快照文件
144
- │ ├── <branchName>.head # 对应的主分支 HEAD commit hash(用于增量 validate 一致性校验)
144
+ │ ├── <branchName>.tree # 每个分支一个 tree hash 快照文件(存储 git tree 对象的 hash)
145
145
  │ └── ...
146
146
  └── worktrees/ # 所有 worktree 的统一存放目录
147
147
  └── <project-name>/ # 以项目名分组
@@ -168,6 +168,7 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
168
168
  | `clawt config` | 查看全局配置 | 5.10 |
169
169
  | `clawt resume` | 在已有 worktree 中恢复 Claude Code 交互式会话 | 5.11 |
170
170
  | `clawt sync` | 将主分支最新代码同步到目标 worktree | 5.12 |
171
+ | `clawt reset` | 重置主 worktree 工作区和暂存区 | 5.13 |
171
172
 
172
173
  所有命令执行前,都必须先执行**主 worktree 校验**(见 [2.1](#21-主-worktree-的定义与定位规则))。
173
174
 
@@ -361,7 +362,7 @@ Git worktree 不会包含 `node_modules`、`.venv` 等依赖文件,每次安
361
362
 
362
363
  **快照机制:**
363
364
 
364
- validate 命令引入了**快照(snapshot)机制**来支持增量对比。每次 validate 执行成功后,会将当前全量变更保存为 patch 文件(`~/.clawt/validate-snapshots/<project>/<branchName>.patch`),同时保存主分支 HEAD commit hash 到 `.head` 文件(用于一致性校验)。当再次执行 validate 时,先校验主分支 HEAD 是否与快照记录一致(不一致则清除旧快照降级为首次模式),然后通过对比新旧快照,将上次快照应用到暂存区、最新变更保留在工作目录,用户可通过 `git diff` 直接查看两次 validate 之间的增量差异。
365
+ validate 命令引入了**快照(snapshot)机制**来支持增量对比。每次 validate 执行成功后,会将当前全量变更通过 `git write-tree` 保存为 git tree 对象,并将 tree hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`)。当再次执行 validate 时,通过 `git read-tree` 将上次快照的 tree 对象载入暂存区、最新变更保留在工作目录,用户可通过 `git diff` 直接查看两次 validate 之间的增量差异。由于 tree 对象存储在 git 对象库中,不依赖主分支 HEAD,无需一致性校验。
365
366
 
366
367
  **运行流程:**
367
368
 
@@ -371,7 +372,7 @@ validate 命令引入了**快照(snapshot)机制**来支持增量对比。
371
372
 
372
373
  1. **主 worktree 校验** (2.1)
373
374
  2. 如果主 worktree 有未提交更改,执行 `git reset --hard` + `git clean -fd` 清空
374
- 3. 删除对应分支的 patch 快照文件
375
+ 3. 删除对应分支的快照文件
375
376
  4. 输出清理成功提示
376
377
 
377
378
  #### 首次 validate(无历史快照)
@@ -439,14 +440,13 @@ git restore --staged .
439
440
  > 此步骤结束后,目标 worktree 的代码保持原样,主 worktree 工作目录包含目标分支的全量变更。
440
441
  > 如果 patch apply 失败(目标分支与主分支差异过大),会提示用户先执行 `clawt sync -b <branchName>` 同步主分支后重试。
441
442
 
442
- ##### 步骤 4:保存纯净快照
443
+ ##### 步骤 4:保存快照为 git tree 对象
443
444
 
444
- 将主 worktree 工作目录的全量变更保存为 patch 文件,同时记录主分支 HEAD commit hash:
445
+ 将主 worktree 工作目录的全量变更保存为 git tree 对象:
445
446
 
446
447
  ```bash
447
448
  git add .
448
- git diff --cached --binary > ~/.clawt/validate-snapshots/<project>/<branchName>.patch
449
- git rev-parse HEAD > ~/.clawt/validate-snapshots/<project>/<branchName>.head
449
+ git write-tree # 返回 tree hash,写入 ~/.clawt/validate-snapshots/<project>/<branchName>.tree
450
450
  git restore --staged .
451
451
  ```
452
452
 
@@ -461,41 +461,34 @@ git restore --staged .
461
461
 
462
462
  #### 增量 validate(存在历史快照)
463
463
 
464
- 当 `~/.clawt/validate-snapshots/<project>/<branchName>.patch` 存在时,自动进入增量模式:
464
+ 当 `~/.clawt/validate-snapshots/<project>/<branchName>.tree` 存在时,自动进入增量模式:
465
465
 
466
- ##### 步骤 1:校验主分支 HEAD 一致性
466
+ ##### 步骤 1:读取旧 tree hash
467
467
 
468
- 读取 `.head` 文件中保存的主分支 HEAD hash,与当前主分支 HEAD 对比:
468
+ 在清空主 worktree 之前,读取上次保存的快照 tree hash
469
469
 
470
- - **不一致或 `.head` 文件不存在** → 清除旧快照(`.patch` + `.head`),降级为首次 validate 模式
471
- - **一致** → 继续增量流程
472
-
473
- ##### 步骤 2:读取旧 patch
474
-
475
- 在清空主 worktree 之前,读取上次保存的快照 patch 内容。
476
-
477
- ##### 步骤 3:确保主 worktree 干净
470
+ ##### 步骤 2:确保主 worktree 干净
478
471
 
479
472
  如果主 worktree 有残留状态,让用户选择处理方式(同首次 validate 步骤 1 的交互),做兜底清理。
480
473
 
481
- ##### 步骤 4:从目标分支获取最新全量变更
474
+ ##### 步骤 3:从目标分支获取最新全量变更
482
475
 
483
476
  通过 patch 方式从目标分支获取最新全量变更(流程同首次 validate 的步骤 3)。
484
477
 
485
- ##### 步骤 5:保存最新快照
478
+ ##### 步骤 4:保存最新快照为 git tree 对象
486
479
 
487
- 将最新全量变更保存为新的 patch 文件 + HEAD hash(覆盖旧快照,流程同首次 validate 的步骤 4)。
480
+ 将最新全量变更保存为新的 tree 对象(覆盖旧快照,流程同首次 validate 的步骤 4)。
488
481
 
489
- ##### 步骤 6:将旧 patch 应用到暂存区
482
+ ##### 步骤 5:将旧 tree 对象载入暂存区
490
483
 
491
484
  ```bash
492
- git apply --cached < <旧 patch 内容>
485
+ git read-tree <旧 tree hash>
493
486
  ```
494
487
 
495
- - **应用成功** → 结果:暂存区=上次快照,工作目录=最新全量变更(用户可通过 `git diff` 查看增量差异)
496
- - **应用失败**(文件结构变化过大)→ 降级为全量模式,暂存区保持为空,等同于首次 validate 的结果
488
+ - **读取成功** → 结果:暂存区=上次快照,工作目录=最新全量变更(用户可通过 `git diff` 查看增量差异)
489
+ - **读取失败**(tree 对象可能被 git gc 回收)→ 降级为全量模式,暂存区保持为空,等同于首次 validate 的结果
497
490
 
498
- ##### 步骤 7:输出成功提示
491
+ ##### 步骤 6:输出成功提示
499
492
 
500
493
  ```
501
494
  # 增量模式成功
@@ -586,7 +579,7 @@ clawt merge -b <branchName> [-m <commitMessage>]
586
579
  2. **主 worktree 状态检测**
587
580
  - 执行 `git status --porcelain`
588
581
  - 如果有更改:
589
- - 如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.patch`),额外输出警告提示用户可先执行 `clawt validate -b <branchName> --clean` 清理
582
+ - 如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`),额外输出警告提示用户可先执行 `clawt validate -b <branchName> --clean` 清理
590
583
  - 提示 `主 worktree 有未提交的更改,请先处理`,退出
591
584
  - 无更改 → 继续
592
585
  3. **Squash 检测与执行(auto-save 临时提交压缩)**
@@ -661,7 +654,7 @@ clawt merge -b <branchName> [-m <commitMessage>]
661
654
  - 输出清理成功提示:`✓ 已清理 worktree 和分支: <branchName>`
662
655
 
663
656
  10. **清理 validate 快照**
664
- - merge 成功后,如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.patch`),自动删除该快照文件(merge 成功后快照已无意义)
657
+ - merge 成功后,如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`),自动删除该快照文件(merge 成功后快照已无意义)
665
658
 
666
659
  > **注意:** 清理确认在 merge 操作之前询问(避免 merge 成功后因交互中断而遗留未清理的 worktree),但清理操作在 merge 成功后才执行。
667
660
 
@@ -881,7 +874,7 @@ clawt sync -b <branchName>
881
874
  解决冲突后执行 git add . && git merge --continue
882
875
  ```
883
876
  - **无冲突** → 继续
884
- 7. **清除 validate 快照**:合并成功后,如果该分支存在 validate 快照(`.patch` + `.head`),自动删除(代码基础已变化,旧快照无效)
877
+ 7. **清除 validate 快照**:合并成功后,如果该分支存在 validate 快照(`.tree` 文件),自动删除(代码基础已变化,旧快照无效)
885
878
  8. **输出成功提示**:
886
879
  ```
887
880
  ✓ 已将 <mainBranch> 的最新代码同步到 <branchName>
@@ -889,6 +882,38 @@ clawt sync -b <branchName>
889
882
 
890
883
  ---
891
884
 
885
+ ### 5.13 重置主 Worktree 工作区和暂存区
886
+
887
+ **命令:**
888
+
889
+ ```bash
890
+ clawt reset
891
+ ```
892
+
893
+ **无参数。**
894
+
895
+ **使用场景:**
896
+
897
+ 当用户通过 `clawt validate` 将分支变更迁移到主 worktree 后,希望快速清除工作区和暂存区的所有修改,恢复到干净状态。与 `clawt validate --clean` 的区别在于:`reset` 仅重置工作区和暂存区,**不删除** validate 快照文件,适用于只想清空变更而保留快照以便后续增量 validate 的场景。
898
+
899
+ **运行流程:**
900
+
901
+ 1. **主 worktree 校验** (2.1)
902
+ 2. **检测工作区状态**:通过 `git status --porcelain` 检测主 worktree 是否有未提交的更改
903
+ - **工作区干净** → 输出提示 `主 worktree 工作区和暂存区已是干净状态,无需重置`,退出
904
+ - **工作区不干净** → 继续
905
+ 3. **重置工作区和暂存区**:
906
+ ```bash
907
+ git reset --hard
908
+ git clean -f
909
+ ```
910
+ 4. **输出成功提示**:
911
+ ```
912
+ ✓ 主 worktree 工作区和暂存区已重置
913
+ ```
914
+
915
+ ---
916
+
892
917
  ## 6. 错误处理规范
893
918
 
894
919
  ### 6.1 通用错误处理
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,42 @@
1
+ import type { Command } from 'commander';
2
+ import { logger } from '../logger/index.js';
3
+ import { MESSAGES } from '../constants/index.js';
4
+ import {
5
+ validateMainWorktree,
6
+ getGitTopLevel,
7
+ isWorkingDirClean,
8
+ gitResetHard,
9
+ gitCleanForce,
10
+ printSuccess,
11
+ printInfo,
12
+ } from '../utils/index.js';
13
+
14
+ /**
15
+ * 注册 reset 命令:重置主 worktree 工作区和暂存区
16
+ * @param {Command} program - Commander 实例
17
+ */
18
+ export function registerResetCommand(program: Command): void {
19
+ program
20
+ .command('reset')
21
+ .description('重置主 worktree 工作区和暂存区(保留 validate 快照)')
22
+ .action(() => {
23
+ handleReset();
24
+ });
25
+ }
26
+
27
+ /**
28
+ * 执行 reset 命令:重置主 worktree 工作区和暂存区
29
+ */
30
+ function handleReset(): void {
31
+ validateMainWorktree();
32
+ const mainWorktreePath = getGitTopLevel();
33
+ logger.info('reset 命令执行');
34
+
35
+ if (!isWorkingDirClean(mainWorktreePath)) {
36
+ gitResetHard(mainWorktreePath);
37
+ gitCleanForce(mainWorktreePath);
38
+ printSuccess(MESSAGES.RESET_SUCCESS);
39
+ } else {
40
+ printInfo(MESSAGES.RESET_ALREADY_CLEAN);
41
+ }
42
+ }
@@ -18,16 +18,14 @@ import {
18
18
  gitRestoreStaged,
19
19
  gitResetHard,
20
20
  gitCleanForce,
21
- gitDiffCachedBinary,
22
- gitApplyCachedFromStdin,
23
21
  gitDiffBinaryAgainstBranch,
24
22
  gitApplyFromStdin,
25
23
  gitResetSoft,
26
- getHeadCommitHash,
24
+ gitWriteTree,
25
+ gitReadTree,
27
26
  hasLocalCommits,
28
27
  hasSnapshot,
29
- readSnapshot,
30
- readSnapshotHead,
28
+ readSnapshotTreeHash,
31
29
  writeSnapshot,
32
30
  removeSnapshot,
33
31
  printSuccess,
@@ -137,21 +135,19 @@ function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: st
137
135
  }
138
136
 
139
137
  /**
140
- * 保存当前主 worktree 工作目录变更为纯净快照 patch
141
- * 操作序列:git add . → git diff --cached --binary → git restore --staged .
142
- * 同时记录主分支 HEAD hash,用于增量 validate 一致性校验
138
+ * 保存当前主 worktree 工作目录变更为 git tree 对象快照
139
+ * 操作序列:git add . → git write-tree → git restore --staged .
143
140
  * @param {string} mainWorktreePath - 主 worktree 路径
144
141
  * @param {string} projectName - 项目名
145
142
  * @param {string} branchName - 分支名
146
- * @returns {Buffer} 生成的 patch 内容
143
+ * @returns {string} 生成的 tree hash
147
144
  */
148
- function saveCurrentSnapshotPatch(mainWorktreePath: string, projectName: string, branchName: string): Buffer {
145
+ function saveCurrentSnapshotTree(mainWorktreePath: string, projectName: string, branchName: string): string {
149
146
  gitAddAll(mainWorktreePath);
150
- const patch = gitDiffCachedBinary(mainWorktreePath);
147
+ const treeHash = gitWriteTree(mainWorktreePath);
151
148
  gitRestoreStaged(mainWorktreePath);
152
- const headHash = getHeadCommitHash(mainWorktreePath);
153
- writeSnapshot(projectName, branchName, patch, headHash);
154
- return patch;
149
+ writeSnapshot(projectName, branchName, treeHash);
150
+ return treeHash;
155
151
  }
156
152
 
157
153
  /**
@@ -172,7 +168,7 @@ function handleValidateClean(options: ValidateOptions): void {
172
168
  gitCleanForce(mainWorktreePath);
173
169
  }
174
170
 
175
- // 删除对应的 patch 文件
171
+ // 删除对应的快照文件
176
172
  removeSnapshot(projectName, options.branch);
177
173
 
178
174
  printSuccess(MESSAGES.VALIDATE_CLEANED(options.branch));
@@ -190,8 +186,8 @@ function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: strin
190
186
  // 通过 patch 迁移目标分支全量变更到主 worktree
191
187
  migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
192
188
 
193
- // 保存纯净快照到 patch 文件
194
- saveCurrentSnapshotPatch(mainWorktreePath, projectName, branchName);
189
+ // 保存快照为 git tree 对象
190
+ saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
195
191
 
196
192
  // 结果:暂存区=空,工作目录=全量变更
197
193
  printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
@@ -206,8 +202,8 @@ function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: strin
206
202
  * @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
207
203
  */
208
204
  function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): void {
209
- // 步骤 1:读取旧 patch(在清空前读取)
210
- const oldPatch = readSnapshot(projectName, branchName);
205
+ // 步骤 1:读取旧 tree hash(在清空前读取)
206
+ const oldTreeHash = readSnapshotTreeHash(projectName, branchName);
211
207
 
212
208
  // 步骤 2:确保主 worktree 干净(调用方已通过 handleDirtyMainWorktree 处理)
213
209
  // 这里做兜底清理,防止 handleDirtyMainWorktree 之后仍有残留
@@ -219,21 +215,19 @@ function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath:
219
215
  // 步骤 3:通过 patch 从目标分支获取最新全量变更
220
216
  migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
221
217
 
222
- // 步骤 4:保存最新快照
223
- saveCurrentSnapshotPatch(mainWorktreePath, projectName, branchName);
224
-
225
- // 步骤 5:将旧 patch 应用到暂存区
226
- if (oldPatch.length > 0) {
227
- try {
228
- gitApplyCachedFromStdin(oldPatch, mainWorktreePath);
229
- } catch (error) {
230
- // patch 无法应用(可能文件结构变化太大),降级为全量模式
231
- logger.warn(`增量 apply 失败: ${error}`);
232
- printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
233
- // 降级后暂存区保持为空,工作目录为最新全量变更,与首次 validate 一致
234
- printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
235
- return;
236
- }
218
+ // 步骤 4:保存最新快照为 git tree 对象
219
+ saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
220
+
221
+ // 步骤 5:将旧 tree 对象载入暂存区(恢复上次快照状态)
222
+ try {
223
+ gitReadTree(oldTreeHash, mainWorktreePath);
224
+ } catch (error) {
225
+ // tree 对象无法读取(可能被 git gc 回收),降级为全量模式
226
+ logger.warn(`增量 read-tree 失败: ${error}`);
227
+ printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
228
+ // 降级后暂存区保持为空,工作目录为最新全量变更,与首次 validate 一致
229
+ printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
230
+ return;
237
231
  }
238
232
 
239
233
  // 结果:暂存区=上次快照,工作目录=最新全量变更
@@ -274,19 +268,8 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
274
268
  return;
275
269
  }
276
270
 
277
- // 判断是否为增量 validate
278
- let isIncremental = hasSnapshot(projectName, options.branch);
279
-
280
- // 主分支 HEAD 发生变化或旧快照无 .head 记录时,清除后走首次全量模式
281
- if (isIncremental) {
282
- const savedHead = readSnapshotHead(projectName, options.branch);
283
- const currentHead = getHeadCommitHash(mainWorktreePath);
284
- if (!savedHead || savedHead !== currentHead) {
285
- logger.info(`主分支 HEAD 不匹配 (${savedHead ?? 'null'} → ${currentHead}),清除旧快照`);
286
- removeSnapshot(projectName, options.branch);
287
- isIncremental = false;
288
- }
289
- }
271
+ // 判断是否为增量 validate(tree 对象不依赖主分支 HEAD,无需一致性校验)
272
+ const isIncremental = hasSnapshot(projectName, options.branch);
290
273
 
291
274
  if (isIncremental) {
292
275
  // 增量模式:主 worktree 有残留状态时让用户选择处理方式
@@ -95,4 +95,8 @@ export const MESSAGES = {
95
95
  /** squash 完成但未提供 -m,提示用户自行提交 */
96
96
  MERGE_SQUASH_PENDING: (worktreePath: string, branch: string) =>
97
97
  `✓ 已将所有提交压缩到暂存区\n 请在目标 worktree 中提交后重新执行 merge:\n cd ${worktreePath}\n 提交完成后执行:clawt merge -b ${branch}`,
98
+ /** reset 成功 */
99
+ RESET_SUCCESS: '✓ 主 worktree 工作区和暂存区已重置',
100
+ /** reset 时工作区和暂存区已干净 */
101
+ RESET_ALREADY_CLEAN: '主 worktree 工作区和暂存区已是干净状态,无需重置',
98
102
  } as const;
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ import { registerValidateCommand } from './commands/validate.js';
13
13
  import { registerMergeCommand } from './commands/merge.js';
14
14
  import { registerConfigCommand } from './commands/config.js';
15
15
  import { registerSyncCommand } from './commands/sync.js';
16
+ import { registerResetCommand } from './commands/reset.js';
16
17
 
17
18
  // 从 package.json 读取版本号,避免硬编码
18
19
  const require = createRequire(import.meta.url);
@@ -38,6 +39,7 @@ registerValidateCommand(program);
38
39
  registerMergeCommand(program);
39
40
  registerConfigCommand(program);
40
41
  registerSyncCommand(program);
42
+ registerResetCommand(program);
41
43
 
42
44
  // 全局未捕获异常处理
43
45
  process.on('uncaughtException', (error) => {
package/src/utils/git.ts CHANGED
@@ -407,3 +407,21 @@ export function hasCommitWithMessage(branchName: string, messagePrefix: string,
407
407
  export function gitResetSoftTo(commitHash: string, cwd?: string): void {
408
408
  execCommand(`git reset --soft ${commitHash}`, { cwd });
409
409
  }
410
+
411
+ /**
412
+ * 将当前暂存区内容写入 git tree 对象并返回其 hash
413
+ * @param {string} [cwd] - 工作目录
414
+ * @returns {string} tree 对象的 hash
415
+ */
416
+ export function gitWriteTree(cwd?: string): string {
417
+ return execCommand('git write-tree', { cwd });
418
+ }
419
+
420
+ /**
421
+ * 将指定 tree 对象的内容载入暂存区(不影响工作目录)
422
+ * @param {string} treeHash - tree 对象的 hash
423
+ * @param {string} [cwd] - 工作目录
424
+ */
425
+ export function gitReadTree(treeHash: string, cwd?: string): void {
426
+ execCommand(`git read-tree ${treeHash}`, { cwd });
427
+ }
@@ -38,6 +38,8 @@ export {
38
38
  gitMergeBase,
39
39
  hasCommitWithMessage,
40
40
  gitResetSoftTo,
41
+ gitWriteTree,
42
+ gitReadTree,
41
43
  } from './git.js';
42
44
  export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
43
45
  export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
@@ -47,4 +49,4 @@ export { printSuccess, printError, printWarning, printInfo, printSeparator, prin
47
49
  export { ensureDir, removeEmptyDir } from './fs.js';
48
50
  export { multilineInput } from './prompt.js';
49
51
  export { launchInteractiveClaude } from './claude.js';
50
- export { getSnapshotPath, hasSnapshot, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots, readSnapshotHead } from './validate-snapshot.js';
52
+ export { getSnapshotPath, hasSnapshot, readSnapshotTreeHash, writeSnapshot, removeSnapshot, removeProjectSnapshots } from './validate-snapshot.js';
@@ -8,20 +8,10 @@ import { logger } from '../logger/index.js';
8
8
  * 获取指定项目和分支的 validate 快照文件路径
9
9
  * @param {string} projectName - 项目名
10
10
  * @param {string} branchName - 分支名
11
- * @returns {string} patch 文件的绝对路径
11
+ * @returns {string} tree hash 文件的绝对路径
12
12
  */
13
13
  export function getSnapshotPath(projectName: string, branchName: string): string {
14
- return join(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.patch`);
15
- }
16
-
17
- /**
18
- * 获取指定项目和分支的快照 HEAD hash 文件路径
19
- * @param {string} projectName - 项目名
20
- * @param {string} branchName - 分支名
21
- * @returns {string} head 文件的绝对路径
22
- */
23
- function getSnapshotHeadPath(projectName: string, branchName: string): string {
24
- return join(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
14
+ return join(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
25
15
  }
26
16
 
27
17
  /**
@@ -35,33 +25,28 @@ export function hasSnapshot(projectName: string, branchName: string): boolean {
35
25
  }
36
26
 
37
27
  /**
38
- * 读取指定项目和分支的 validate 快照内容
28
+ * 读取指定项目和分支的 validate 快照中存储的 tree hash
39
29
  * @param {string} projectName - 项目名
40
30
  * @param {string} branchName - 分支名
41
- * @returns {Buffer} patch 文件内容(Buffer 格式,保留二进制完整性)
31
+ * @returns {string} tree 对象的 hash
42
32
  */
43
- export function readSnapshot(projectName: string, branchName: string): Buffer {
33
+ export function readSnapshotTreeHash(projectName: string, branchName: string): string {
44
34
  const snapshotPath = getSnapshotPath(projectName, branchName);
45
35
  logger.debug(`读取 validate 快照: ${snapshotPath}`);
46
- return readFileSync(snapshotPath);
36
+ return readFileSync(snapshotPath, 'utf-8').trim();
47
37
  }
48
38
 
49
39
  /**
50
40
  * 写入 validate 快照内容(自动创建目录)
51
41
  * @param {string} projectName - 项目名
52
42
  * @param {string} branchName - 分支名
53
- * @param {Buffer} patch - patch 内容(Buffer 格式)
54
- * @param {string} [headHash] - 主分支 HEAD commit hash(用于增量 validate 一致性校验)
43
+ * @param {string} treeHash - git tree 对象的 hash
55
44
  */
56
- export function writeSnapshot(projectName: string, branchName: string, patch: Buffer, headHash?: string): void {
45
+ export function writeSnapshot(projectName: string, branchName: string, treeHash: string): void {
57
46
  const snapshotPath = getSnapshotPath(projectName, branchName);
58
47
  const snapshotDir = join(VALIDATE_SNAPSHOTS_DIR, projectName);
59
48
  ensureDir(snapshotDir);
60
- writeFileSync(snapshotPath, patch);
61
- // 保存主分支 HEAD hash,用于下次增量 validate 时校验一致性
62
- if (headHash) {
63
- writeFileSync(getSnapshotHeadPath(projectName, branchName), headHash, 'utf-8');
64
- }
49
+ writeFileSync(snapshotPath, treeHash, 'utf-8');
65
50
  logger.info(`已保存 validate 快照: ${snapshotPath}`);
66
51
  }
67
52
 
@@ -76,25 +61,6 @@ export function removeSnapshot(projectName: string, branchName: string): void {
76
61
  unlinkSync(snapshotPath);
77
62
  logger.info(`已删除 validate 快照: ${snapshotPath}`);
78
63
  }
79
- // 同时删除对应的 .head 文件
80
- const headPath = getSnapshotHeadPath(projectName, branchName);
81
- if (existsSync(headPath)) {
82
- unlinkSync(headPath);
83
- }
84
- }
85
-
86
- /**
87
- * 读取快照保存时的主分支 HEAD commit hash
88
- * @param {string} projectName - 项目名
89
- * @param {string} branchName - 分支名
90
- * @returns {string | null} HEAD hash,不存在则返回 null
91
- */
92
- export function readSnapshotHead(projectName: string, branchName: string): string | null {
93
- const headPath = getSnapshotHeadPath(projectName, branchName);
94
- if (!existsSync(headPath)) {
95
- return null;
96
- }
97
- return readFileSync(headPath, 'utf-8').trim();
98
64
  }
99
65
 
100
66
  /**