clawt 2.5.0 → 2.6.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.
@@ -12,14 +12,6 @@
12
12
  - 配置项说明在 `5.7 默认配置文件` 章节的表格中
13
13
  - 更新模式:新增步骤时追加编号,配置项影响范围变化时更新说明列
14
14
 
15
- ### CLAUDE.md
16
- - 面向 Claude Code 的项目架构指引,精简扼要
17
- - run 命令流程在 `核心流程(run 命令)` 章节,编号列表描述
18
- - resume 命令流程在独立的 `### resume 命令流程` 章节,编号列表 + 缩进列表描述匹配策略
19
- - merge 和 run 中断清理在 `validate + merge 工作流` 章节,一行式描述用箭头连接流程
20
- - utils 目录描述用括号内逗号分隔列举功能模块
21
- - 更新模式:编号列表追加步骤,箭头链追加阶段,括号内追加关键词
22
-
23
15
  ### README.md
24
16
  - 面向用户的使用文档
25
17
  - 每个命令一个 `###` 小节,含命令格式、参数表格、简要说明、示例
@@ -29,7 +21,7 @@
29
21
  ## 关键约定
30
22
  - `autoDeleteBranch` 配置项影响三处:remove 命令、merge 命令、run 中断清理
31
23
  - `confirmDestructiveOps` 配置项影响两处:reset 命令、validate --clean
32
- - merge 的清理确认在 merge 操作之前询问(避免交互中断),但清理在 merge 成功后执行
24
+ - merge 的清理确认和清理操作均在 merge 成功后执行(避免 merge 冲突时提前询问用户造成困惑)
33
25
  - merge 成功后自动清理对应的 validate 快照(hasSnapshot + removeSnapshot)
34
26
  - merge 成功消息根据 `autoPullPush` 配置动态显示推送状态
35
27
  - run 的中断清理在所有子进程退出后执行
@@ -46,12 +38,11 @@
46
38
 
47
39
  ## 配置项同步检查点
48
40
 
49
- 配置项变更时需在以下 5 处保持一致:
41
+ 配置项变更时需在以下 4 处保持一致:
50
42
  1. `src/constants/config.ts` — CONFIG_DEFINITIONS 对象(单一数据源,包含 defaultValue + description)
51
43
  2. `src/types/config.ts` — ClawtConfig 接口
52
44
  3. `docs/spec.md` — 5.7 默认配置文件章节(JSON 示例 + 配置项表格)
53
- 4. `CLAUDE.md` — 关键约定段落中的配置描述
54
- 5. `README.md` — 配置文件章节(JSON 示例 + 配置项表格)
45
+ 4. `README.md` — 配置文件章节(JSON 示例 + 配置项表格)
55
46
 
56
47
  ## 配置架构
57
48
 
@@ -65,7 +56,6 @@
65
56
  run 命令有两种模式(自 claudeCodeCommand 特性后):
66
57
  - 不传 `--tasks`:交互式界面模式(单 worktree + `launchInteractiveClaude` + spawnSync)
67
58
  - 传 `--tasks`:并行任务模式(多 worktree + `executeClaudeTask` + spawnProcess)
68
- - CLAUDE.md 中的核心流程按模式分段描述
69
59
 
70
60
  ## 命令清单(10 个)
71
61
 
@@ -81,14 +71,17 @@ Notes:
81
71
  ## validate 快照机制
82
72
 
83
73
  - validate 命令支持首次/增量两种模式,通过 `hasSnapshot()` 判断
84
- - 快照路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.tree`(存储 git tree 对象 hash)
74
+ - 快照由两个文件组成:`.tree`(git tree 对象 hash)和 `.head`(快照时主 worktree 的 HEAD commit hash
75
+ - 快照路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.tree` 和 `<branchName>.head`
85
76
  - 常量 `VALIDATE_SNAPSHOTS_DIR` 定义在 `src/constants/paths.ts`
86
77
  - validate 新增 `--clean` 选项(`ValidateOptions.clean?: boolean`)
87
- - 快照保存:`git add . → git write-tree → git restore --staged .`,将 tree hash 写入 `.tree` 文件
88
- - 增量模式核心:`git read-tree <旧 tree hash>` 将旧快照载入暂存区 + 新全量变更在工作目录 → `git diff` 可查看增量差异
89
- - tree 对象不依赖主分支 HEAD,无需一致性校验(旧方案需要 `.head` 文件校验 HEAD 一致性)
90
- - 增量 read-tree 失败时自动降级为全量模式(tree 对象可能被 git gc 回收)
91
- - git 层有 `gitWriteTree()`(返回 tree hash)和 `gitReadTree()`(载入暂存区)
78
+ - 快照保存:`git add . → git write-tree → git rev-parse HEAD → git restore --staged .`,tree hash 写入 `.tree`,HEAD commit hash 写入 `.head`
79
+ - 增量模式核心:检测 HEAD 是否变化决定策略
80
+ - HEAD 未变化:`git read-tree <旧 tree hash>` 直接载入暂存区
81
+ - HEAD 已变化:提取旧变更 patch(`git diff-tree` 旧 HEAD tree 旧快照 tree),`git apply --cached` 重放到当前 HEAD 暂存区;有冲突则降级全量
82
+ - 增量 read-tree / apply 失败时自动降级为全量模式
83
+ - git 层工具函数:`gitWriteTree()`、`gitReadTree()`、`getCommitTreeHash()`、`gitDiffTree()`、`gitApplyCachedCheck()`
84
+ - `readSnapshot()` 返回 `{ treeHash, headCommitHash }`,`writeSnapshot()` 接收 4 个参数(含 headCommitHash)
85
+ - `removeSnapshot()` 同时清理 `.tree` 和 `.head` 文件
92
86
  - merge 成功后自动清理对应快照;merge 时主 worktree 脏 + 存在快照会输出警告提示
93
87
  - docs/spec.md 中 validate 章节(5.4)按 `--clean 模式`、`首次 validate`、`增量 validate` 三段描述
94
- - CLAUDE.md 中在 validate + merge 工作流章节用缩进列表描述两种模式
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: docs-sync-updater
3
- description: "Use this agent when the user explicitly requests to synchronize documentation files (docs/spec.md, CLAUDE.md, README.md) based on recent code changes in the working area or staging area. This agent must NEVER be called proactively or automatically — it must only be invoked when the user explicitly asks for documentation synchronization.\\n\\nExamples:\\n\\n- Example 1:\\n user: \"请同步更新文档\"\\n assistant: \"好的,我来调用文档同步 agent 来根据当前代码变更更新相关文档。\"\\n <Use the Task tool to launch the docs-sync-updater agent>\\n\\n- Example 2:\\n user: \"代码改完了,帮我把文档也更新一下\"\\n assistant: \"收到,我现在使用文档同步 agent 来分析代码变更并更新 docs/spec.md、CLAUDE.md 和 README.md。\"\\n <Use the Task tool to launch the docs-sync-updater agent>\\n\\n- Example 3:\\n user: \"update docs based on my changes\"\\n assistant: \"好的,我来启动文档同步 agent,根据工作区和暂存区的变更同步更新文档。\"\\n <Use the Task tool to launch the docs-sync-updater agent>\\n\\n- Counter-example (DO NOT do this):\\n user: \"我刚加了一个新命令\"\\n assistant: (DO NOT proactively launch this agent. Wait for the user to explicitly request documentation updates.)"
3
+ description: "Use this agent when the user explicitly requests to synchronize documentation files (docs/spec.md, README.md) based on recent code changes in the working area or staging area. This agent must NEVER be called proactively or automatically — it must only be invoked when the user explicitly asks for documentation synchronization.\\n\\nExamples:\\n\\n- Example 1:\\n user: \"请同步更新文档\"\\n assistant: \"好的,我来调用文档同步 agent 来根据当前代码变更更新相关文档。\"\\n <Use the Task tool to launch the docs-sync-updater agent>\\n\\n- Example 2:\\n user: \"代码改完了,帮我把文档也更新一下\"\\n assistant: \"收到,我现在使用文档同步 agent 来分析代码变更并更新 docs/spec.md 和 README.md。\"\\n <Use the Task tool to launch the docs-sync-updater agent>\\n\\n- Example 3:\\n user: \"update docs based on my changes\"\\n assistant: \"好的,我来启动文档同步 agent,根据工作区和暂存区的变更同步更新文档。\"\\n <Use the Task tool to launch the docs-sync-updater agent>\\n\\n- Counter-example (DO NOT do this):\\n user: \"我刚加了一个新命令\"\\n assistant: (DO NOT proactively launch this agent. Wait for the user to explicitly request documentation updates.)"
4
4
  model: opus
5
5
  memory: project
6
6
  ---
7
7
 
8
- 你是一位资深的技术文档工程师,精通代码变更分析与文档同步维护。你的核心职责是根据当前工作区(working directory)和暂存区(staging area)的代码修改,精准地同步更新项目中的三个关键文档:`docs/spec.md`、`CLAUDE.md` 和 `README.md`。
8
+ 你是一位资深的技术文档工程师,精通代码变更分析与文档同步维护。你的核心职责是根据当前工作区(working directory)和暂存区(staging area)的代码修改,精准地同步更新项目中的两个关键文档:`docs/spec.md` 和 `README.md`。
9
9
 
10
10
  ## 重要约束
11
11
 
@@ -33,15 +33,13 @@ memory: project
33
33
  ### 第二步:阅读现有文档
34
34
 
35
35
  1. 读取 `docs/spec.md` 的当前内容(如果存在)。
36
- 2. 读取 `CLAUDE.md` 的当前内容。
37
- 3. 读取 `README.md` 的当前内容(如果存在)。
38
- 4. 理解每个文档的结构、风格和覆盖范围。
36
+ 2. 读取 `README.md` 的当前内容(如果存在)。
37
+ 3. 理解每个文档的结构、风格和覆盖范围。
39
38
 
40
39
  ### 第三步:确定需要更新的内容
41
40
 
42
41
  对每个文档,判断代码变更是否影响其内容:
43
42
 
44
- - **`CLAUDE.md`**:项目架构说明、命令列表、核心流程、目录层级、关键约定、构建命令等。当新增命令、修改架构、调整目录结构、修改构建流程时需要更新。
45
43
  - **`docs/spec.md`**:项目规格说明文档。当功能需求、技术规格、API 设计发生变化时需要更新。
46
44
  - **`README.md`**:用户面向的项目说明。当用户可见的功能、安装方式、使用方法、命令参数发生变化时需要更新。
47
45
 
@@ -83,7 +81,7 @@ memory: project
83
81
  - 如果工作区和暂存区都没有变更,检查最近的提交并告知用户当前没有未提交的变更,询问是否基于最近提交更新。
84
82
  - 如果某个文档文件不存在,告知用户并询问是否需要创建。
85
83
  - 如果变更内容过于复杂或不确定如何反映到文档中,列出你的理解并询问用户确认。
86
- - 如果变更只涉及代码重构而不改变功能,可能不需要更新面向用户的文档(README.md),但可能需要更新架构文档(CLAUDE.md)。
84
+ - 如果变更只涉及代码重构而不改变功能,可能不需要更新面向用户的文档(README.md),但可能需要更新规格文档(docs/spec.md)。
87
85
 
88
86
  **Update your agent memory** as you discover documentation patterns, document structure conventions, terminology usage, and relationships between code modules and their documentation sections. This builds up institutional knowledge across conversations. Write concise notes about what you found and where.
89
87
 
package/README.md CHANGED
@@ -117,7 +117,7 @@ clawt validate -b <branchName> [--clean]
117
117
 
118
118
  将目标 worktree 的变更通过 `git diff`(三点 diff)迁移到主 worktree,方便在主 worktree 中直接测试,无需重新安装依赖。同时检测未提交修改和已提交 commit,确保所有变更都能被捕获。
119
119
 
120
- 支持增量模式:首次 validate 后会自动保存快照(通过 `git write-tree` 将变更存储为 git tree 对象),再次 validate 同一分支时会通过 `git read-tree` 将上次快照载入暂存区、最新变更保留在工作目录,用户可通过 `git diff` 查看两次 validate 之间的增量差异。使用 `--clean` 可清理 validate 状态(重置主 worktree 并删除快照文件)。
120
+ 支持增量模式:首次 validate 后会自动保存快照(通过 `git write-tree` 将变更存储为 git tree 对象,并记录当前 HEAD commit hash),再次 validate 同一分支时会将上次快照载入暂存区、最新变更保留在工作目录,用户可通过 `git diff` 查看两次 validate 之间的增量差异。当主分支 HEAD 发生变化(如合并了其他分支)时,会自动将旧变更 patch 重放到当前 HEAD 暂存区上,避免 diff 混入 HEAD 变化的内容;若 patch 存在冲突则自动降级为全量模式。使用 `--clean` 可清理 validate 状态(重置主 worktree 并删除快照文件)。
121
121
 
122
122
  > **提示:** 如果 validate 时 patch apply 失败(目标分支与主分支差异过大),可先执行 `clawt sync -b <branchName>` 同步主分支后重试。
123
123
 
package/dist/index.js CHANGED
@@ -368,9 +368,15 @@ function getDiffStat(worktreePath) {
368
368
  const output = execCommand("git diff --shortstat HEAD", { cwd: worktreePath });
369
369
  return parseShortStat(output);
370
370
  }
371
+ function gitApplyCachedFromStdin(patchContent, cwd) {
372
+ execCommandWithInput("git", ["apply", "--cached"], { input: patchContent, cwd });
373
+ }
371
374
  function getCurrentBranch(cwd) {
372
375
  return execCommand("git rev-parse --abbrev-ref HEAD", { cwd });
373
376
  }
377
+ function getHeadCommitHash(cwd) {
378
+ return execCommand("git rev-parse HEAD", { cwd });
379
+ }
374
380
  function gitDiffBinaryAgainstBranch(branchName, cwd) {
375
381
  logger.debug(`\u6267\u884C\u547D\u4EE4: git diff HEAD...${branchName} --binary${cwd ? ` (cwd: ${cwd})` : ""}`);
376
382
  return execSync2(`git diff HEAD...${branchName} --binary`, {
@@ -405,6 +411,24 @@ function gitWriteTree(cwd) {
405
411
  function gitReadTree(treeHash, cwd) {
406
412
  execCommand(`git read-tree ${treeHash}`, { cwd });
407
413
  }
414
+ function getCommitTreeHash(commitHash, cwd) {
415
+ return execCommand(`git rev-parse ${commitHash}^{tree}`, { cwd });
416
+ }
417
+ function gitDiffTree(baseTreeHash, targetTreeHash, cwd) {
418
+ logger.debug(`\u6267\u884C\u547D\u4EE4: git diff-tree -p --binary ${baseTreeHash} ${targetTreeHash}${cwd ? ` (cwd: ${cwd})` : ""}`);
419
+ return execSync2(`git diff-tree -p --binary ${baseTreeHash} ${targetTreeHash}`, {
420
+ cwd,
421
+ stdio: ["pipe", "pipe", "pipe"]
422
+ });
423
+ }
424
+ function gitApplyCachedCheck(patchContent, cwd) {
425
+ try {
426
+ execCommandWithInput("git", ["apply", "--cached", "--check"], { input: patchContent, cwd });
427
+ return true;
428
+ } catch {
429
+ return false;
430
+ }
431
+ }
408
432
 
409
433
  // src/utils/formatter.ts
410
434
  import chalk from "chalk";
@@ -667,27 +691,40 @@ import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync
667
691
  function getSnapshotPath(projectName, branchName) {
668
692
  return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
669
693
  }
694
+ function getSnapshotHeadPath(projectName, branchName) {
695
+ return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
696
+ }
670
697
  function hasSnapshot(projectName, branchName) {
671
698
  return existsSync5(getSnapshotPath(projectName, branchName));
672
699
  }
673
- function readSnapshotTreeHash(projectName, branchName) {
700
+ function readSnapshot(projectName, branchName) {
674
701
  const snapshotPath = getSnapshotPath(projectName, branchName);
702
+ const headPath = getSnapshotHeadPath(projectName, branchName);
675
703
  logger.debug(`\u8BFB\u53D6 validate \u5FEB\u7167: ${snapshotPath}`);
676
- return readFileSync2(snapshotPath, "utf-8").trim();
704
+ const treeHash = existsSync5(snapshotPath) ? readFileSync2(snapshotPath, "utf-8").trim() : "";
705
+ const headCommitHash = existsSync5(headPath) ? readFileSync2(headPath, "utf-8").trim() : "";
706
+ return { treeHash, headCommitHash };
677
707
  }
678
- function writeSnapshot(projectName, branchName, treeHash) {
708
+ function writeSnapshot(projectName, branchName, treeHash, headCommitHash) {
679
709
  const snapshotPath = getSnapshotPath(projectName, branchName);
710
+ const headPath = getSnapshotHeadPath(projectName, branchName);
680
711
  const snapshotDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
681
712
  ensureDir(snapshotDir);
682
713
  writeFileSync2(snapshotPath, treeHash, "utf-8");
683
- logger.info(`\u5DF2\u4FDD\u5B58 validate \u5FEB\u7167: ${snapshotPath}`);
714
+ writeFileSync2(headPath, headCommitHash, "utf-8");
715
+ logger.info(`\u5DF2\u4FDD\u5B58 validate \u5FEB\u7167: ${snapshotPath}, ${headPath}`);
684
716
  }
685
717
  function removeSnapshot(projectName, branchName) {
686
718
  const snapshotPath = getSnapshotPath(projectName, branchName);
719
+ const headPath = getSnapshotHeadPath(projectName, branchName);
687
720
  if (existsSync5(snapshotPath)) {
688
721
  unlinkSync(snapshotPath);
689
722
  logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${snapshotPath}`);
690
723
  }
724
+ if (existsSync5(headPath)) {
725
+ unlinkSync(headPath);
726
+ logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${headPath}`);
727
+ }
691
728
  }
692
729
  function removeProjectSnapshots(projectName) {
693
730
  const projectDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
@@ -1158,7 +1195,8 @@ function saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName) {
1158
1195
  gitAddAll(mainWorktreePath);
1159
1196
  const treeHash = gitWriteTree(mainWorktreePath);
1160
1197
  gitRestoreStaged(mainWorktreePath);
1161
- writeSnapshot(projectName, branchName, treeHash);
1198
+ const headCommitHash = getHeadCommitHash(mainWorktreePath);
1199
+ writeSnapshot(projectName, branchName, treeHash, headCommitHash);
1162
1200
  return treeHash;
1163
1201
  }
1164
1202
  async function handleValidateClean(options) {
@@ -1189,7 +1227,7 @@ function handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName,
1189
1227
  printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
1190
1228
  }
1191
1229
  function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
1192
- const oldTreeHash = readSnapshotTreeHash(projectName, branchName);
1230
+ const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash } = readSnapshot(projectName, branchName);
1193
1231
  if (!isWorkingDirClean(mainWorktreePath)) {
1194
1232
  gitResetHard(mainWorktreePath);
1195
1233
  gitCleanForce(mainWorktreePath);
@@ -1197,7 +1235,21 @@ function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, project
1197
1235
  migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
1198
1236
  saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
1199
1237
  try {
1200
- gitReadTree(oldTreeHash, mainWorktreePath);
1238
+ const currentHeadCommitHash = getHeadCommitHash(mainWorktreePath);
1239
+ if (oldHeadCommitHash && oldHeadCommitHash !== currentHeadCommitHash) {
1240
+ const oldHeadTreeHash = getCommitTreeHash(oldHeadCommitHash, mainWorktreePath);
1241
+ const oldChangePatch = gitDiffTree(oldHeadTreeHash, oldTreeHash, mainWorktreePath);
1242
+ if (oldChangePatch.length > 0 && gitApplyCachedCheck(oldChangePatch, mainWorktreePath)) {
1243
+ gitApplyCachedFromStdin(oldChangePatch, mainWorktreePath);
1244
+ } else if (oldChangePatch.length > 0) {
1245
+ logger.warn("\u65E7\u53D8\u66F4 patch \u4E0E\u5F53\u524D HEAD \u51B2\u7A81\uFF0C\u964D\u7EA7\u4E3A\u5168\u91CF\u6A21\u5F0F");
1246
+ printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
1247
+ printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
1248
+ return;
1249
+ }
1250
+ } else {
1251
+ gitReadTree(oldTreeHash, mainWorktreePath);
1252
+ }
1201
1253
  } catch (error) {
1202
1254
  logger.warn(`\u589E\u91CF read-tree \u5931\u8D25: ${error}`);
1203
1255
  printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
@@ -1274,7 +1326,7 @@ async function shouldCleanupAfterMerge(branchName) {
1274
1326
  printInfo(`\u5DF2\u914D\u7F6E\u81EA\u52A8\u5220\u9664\uFF0Cmerge \u6210\u529F\u540E\u5C06\u81EA\u52A8\u6E05\u7406 worktree \u548C\u5206\u652F: ${branchName}`);
1275
1327
  return true;
1276
1328
  }
1277
- return confirmAction(`merge \u6210\u529F\u540E\u662F\u5426\u5220\u9664\u5BF9\u5E94\u7684 worktree \u548C\u5206\u652F (${branchName})\uFF1F`);
1329
+ return confirmAction(`\u662F\u5426\u5220\u9664\u5BF9\u5E94\u7684 worktree \u548C\u5206\u652F (${branchName})\uFF1F`);
1278
1330
  }
1279
1331
  function cleanupWorktreeAndBranch(worktreePath, branchName) {
1280
1332
  cleanupWorktrees([{ path: worktreePath, branch: branchName }]);
@@ -1300,7 +1352,6 @@ async function handleMerge(options) {
1300
1352
  if (shouldExit) {
1301
1353
  return;
1302
1354
  }
1303
- const shouldCleanup = await shouldCleanupAfterMerge(options.branch);
1304
1355
  const targetClean = isWorkingDirClean(targetWorktreePath);
1305
1356
  if (!targetClean) {
1306
1357
  if (!options.message) {
@@ -1336,6 +1387,7 @@ async function handleMerge(options) {
1336
1387
  } else {
1337
1388
  printSuccess(MESSAGES.MERGE_SUCCESS_NO_MESSAGE(options.branch, autoPullPush));
1338
1389
  }
1390
+ const shouldCleanup = await shouldCleanupAfterMerge(options.branch);
1339
1391
  if (shouldCleanup) {
1340
1392
  cleanupWorktreeAndBranch(targetWorktreePath, options.branch);
1341
1393
  }
package/docs/spec.md CHANGED
@@ -143,6 +143,7 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
143
143
  ├── validate-snapshots/ # validate 快照目录
144
144
  │ └── <project-name>/ # 以项目名分组
145
145
  │ ├── <branchName>.tree # 每个分支一个 tree hash 快照文件(存储 git tree 对象的 hash)
146
+ │ ├── <branchName>.head # 每个分支一个 HEAD commit hash 快照文件(存储快照时主 worktree 的 HEAD commit hash)
146
147
  │ └── ...
147
148
  └── worktrees/ # 所有 worktree 的统一存放目录
148
149
  └── <project-name>/ # 以项目名分组
@@ -363,7 +364,7 @@ Git worktree 不会包含 `node_modules`、`.venv` 等依赖文件,每次安
363
364
 
364
365
  **快照机制:**
365
366
 
366
- 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,无需一致性校验。
367
+ validate 命令引入了**快照(snapshot)机制**来支持增量对比。每次 validate 执行成功后,会将当前全量变更通过 `git write-tree` 保存为 git tree 对象,并将 tree hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`),同时将主 worktree 的 HEAD commit hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.head`),用于增量 validate 时对齐基准。当再次执行 validate 时,如果主分支 HEAD 未变化,通过 `git read-tree` 将上次快照的 tree 对象载入暂存区;如果主分支 HEAD 已变化(如合并了其他分支),则将旧变更 patch(旧 tree 相对于旧 HEAD 的差异)重放到当前 HEAD 暂存区上,避免新旧 tree 基准不同导致 diff 混入 HEAD 变化的内容。最终用户可通过 `git diff` 查看两次 validate 之间的增量差异。
367
368
 
368
369
  **运行流程:**
369
370
 
@@ -444,11 +445,12 @@ git restore --staged .
444
445
 
445
446
  ##### 步骤 4:保存快照为 git tree 对象
446
447
 
447
- 将主 worktree 工作目录的全量变更保存为 git tree 对象:
448
+ 将主 worktree 工作目录的全量变更保存为 git tree 对象,同时记录当前 HEAD commit hash:
448
449
 
449
450
  ```bash
450
451
  git add .
451
452
  git write-tree # → 返回 tree hash,写入 ~/.clawt/validate-snapshots/<project>/<branchName>.tree
453
+ git rev-parse HEAD # → 返回 HEAD commit hash,写入 ~/.clawt/validate-snapshots/<project>/<branchName>.head
452
454
  git restore --staged .
453
455
  ```
454
456
 
@@ -465,9 +467,9 @@ git restore --staged .
465
467
 
466
468
  当 `~/.clawt/validate-snapshots/<project>/<branchName>.tree` 存在时,自动进入增量模式:
467
469
 
468
- ##### 步骤 1:读取旧 tree hash
470
+ ##### 步骤 1:读取旧快照
469
471
 
470
- 在清空主 worktree 之前,读取上次保存的快照 tree hash。
472
+ 在清空主 worktree 之前,读取上次保存的快照 tree hash 及当时的 HEAD commit hash
471
473
 
472
474
  ##### 步骤 2:确保主 worktree 干净
473
475
 
@@ -479,9 +481,15 @@ git restore --staged .
479
481
 
480
482
  ##### 步骤 4:保存最新快照为 git tree 对象
481
483
 
482
- 将最新全量变更保存为新的 tree 对象(覆盖旧快照,流程同首次 validate 的步骤 4)。
484
+ 将最新全量变更保存为新的 tree 对象(覆盖旧快照),同时记录当前 HEAD commit hash(流程同首次 validate 的步骤 4)。
483
485
 
484
- ##### 步骤 5:将旧 tree 对象载入暂存区
486
+ ##### 步骤 5:将旧变更状态载入暂存区
487
+
488
+ 根据主分支 HEAD 是否发生变化,选择不同的策略将旧变更载入暂存区:
489
+
490
+ **情况 A:HEAD 未变化(或旧版快照无 HEAD 信息)**
491
+
492
+ 直接通过 `git read-tree` 将旧 tree 对象载入暂存区:
485
493
 
486
494
  ```bash
487
495
  git read-tree <旧 tree hash>
@@ -490,6 +498,28 @@ git read-tree <旧 tree hash>
490
498
  - **读取成功** → 结果:暂存区=上次快照,工作目录=最新全量变更(用户可通过 `git diff` 查看增量差异)
491
499
  - **读取失败**(tree 对象可能被 git gc 回收)→ 降级为全量模式,暂存区保持为空,等同于首次 validate 的结果
492
500
 
501
+ **情况 B:HEAD 发生了变化(如主分支合并了其他分支)**
502
+
503
+ 此时旧 tree 对象基于旧 HEAD,直接 read-tree 会导致 diff 混入 HEAD 变化的内容。需要将旧变更 patch(旧 tree 相对于旧 HEAD 的差异)重放到当前 HEAD 暂存区上:
504
+
505
+ ```bash
506
+ # 获取旧 HEAD 对应的 tree hash
507
+ git rev-parse <旧 HEAD commit hash>^{tree} # → 旧 HEAD tree hash
508
+
509
+ # 提取旧变更 patch(旧 HEAD tree → 旧快照 tree 的差异)
510
+ git diff-tree -p --binary <旧 HEAD tree hash> <旧快照 tree hash>
511
+
512
+ # 检测 patch 能否无冲突地应用到暂存区
513
+ git apply --cached --check < patch
514
+
515
+ # 无冲突:apply --cached 到当前 HEAD 暂存区
516
+ git apply --cached < patch
517
+ ```
518
+
519
+ - **patch 为空**(旧变更为空)→ 暂存区保持干净
520
+ - **无冲突** → apply --cached 到当前 HEAD 暂存区,结果与情况 A 一致
521
+ - **有冲突** → 降级为全量模式(暂存区保持为空),等同于首次 validate 的结果
522
+
493
523
  ##### 步骤 6:输出成功提示
494
524
 
495
525
  ```
@@ -654,7 +684,7 @@ clawt merge -b <branchName> [-m <commitMessage>]
654
684
  ✓ 分支 feature-scheme-1 已成功合并到当前分支
655
685
  ```
656
686
 
657
- 9. **merge 成功后清理 worktree 和分支(可选)**
687
+ 9. **merge 成功后确认并清理 worktree 和分支(可选)**
658
688
  - 如果配置文件中 `autoDeleteBranch` 为 `true`,自动执行清理
659
689
  - 否则交互式询问用户是否清理
660
690
  - 用户确认后,依次执行:
@@ -670,9 +700,9 @@ clawt merge -b <branchName> [-m <commitMessage>]
670
700
  - 输出清理成功提示:`✓ 已清理 worktree 和分支: <branchName>`
671
701
 
672
702
  10. **清理 validate 快照**
673
- - merge 成功后,如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`),自动删除该快照文件(merge 成功后快照已无意义)
703
+ - merge 成功后,如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.tree` 和 `<branchName>.head`),自动删除这些快照文件(merge 成功后快照已无意义)
674
704
 
675
- > **注意:** 清理确认在 merge 操作之前询问(避免 merge 成功后因交互中断而遗留未清理的 worktree),但清理操作在 merge 成功后才执行。
705
+ > **注意:** 清理确认和清理操作均在 merge 成功后执行。只有 merge 成功才会询问用户是否清理 worktree 和分支,避免 merge 冲突时用户被提前询问造成困惑。
676
706
 
677
707
  ---
678
708
 
@@ -936,7 +966,7 @@ clawt sync -b <branchName>
936
966
  clawt validate -b <branch> 验证变更
937
967
  ```
938
968
  - **无冲突** → 继续
939
- 7. **清除 validate 快照**:合并成功后,如果该分支存在 validate 快照(`.tree` 文件),自动删除(代码基础已变化,旧快照无效)
969
+ 7. **清除 validate 快照**:合并成功后,如果该分支存在 validate 快照(`.tree` 和 `.head` 文件),自动删除(代码基础已变化,旧快照无效)
940
970
  8. **输出成功提示**:
941
971
  ```
942
972
  ✓ 已将 <mainBranch> 的最新代码同步到 <branchName>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -105,7 +105,7 @@ async function shouldCleanupAfterMerge(branchName: string): Promise<boolean> {
105
105
  printInfo(`已配置自动删除,merge 成功后将自动清理 worktree 和分支: ${branchName}`);
106
106
  return true;
107
107
  }
108
- return confirmAction(`merge 成功后是否删除对应的 worktree 和分支 (${branchName})?`);
108
+ return confirmAction(`是否删除对应的 worktree 和分支 (${branchName})?`);
109
109
  }
110
110
 
111
111
  /**
@@ -153,9 +153,6 @@ async function handleMerge(options: MergeOptions): Promise<void> {
153
153
  return;
154
154
  }
155
155
 
156
- // merge 前确认是否清理 worktree 和分支
157
- const shouldCleanup = await shouldCleanupAfterMerge(options.branch);
158
-
159
156
  // 步骤 4:根据目标 worktree 状态决定是否需要提交
160
157
  const targetClean = isWorkingDirClean(targetWorktreePath);
161
158
 
@@ -206,7 +203,8 @@ async function handleMerge(options: MergeOptions): Promise<void> {
206
203
  printSuccess(MESSAGES.MERGE_SUCCESS_NO_MESSAGE(options.branch, autoPullPush));
207
204
  }
208
205
 
209
- // 步骤 9:merge 成功后清理 worktree 和分支
206
+ // 步骤 9:merge 成功后确认并清理 worktree 和分支
207
+ const shouldCleanup = await shouldCleanupAfterMerge(options.branch);
210
208
  if (shouldCleanup) {
211
209
  cleanupWorktreeAndBranch(targetWorktreePath, options.branch);
212
210
  }
@@ -21,12 +21,17 @@ import {
21
21
  gitCleanForce,
22
22
  gitDiffBinaryAgainstBranch,
23
23
  gitApplyFromStdin,
24
+ gitApplyCachedFromStdin,
24
25
  gitResetSoft,
25
26
  gitWriteTree,
26
27
  gitReadTree,
28
+ getHeadCommitHash,
29
+ getCommitTreeHash,
30
+ gitDiffTree,
31
+ gitApplyCachedCheck,
27
32
  hasLocalCommits,
28
33
  hasSnapshot,
29
- readSnapshotTreeHash,
34
+ readSnapshot,
30
35
  writeSnapshot,
31
36
  removeSnapshot,
32
37
  confirmDestructiveAction,
@@ -148,6 +153,7 @@ function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: st
148
153
  /**
149
154
  * 保存当前主 worktree 工作目录变更为 git tree 对象快照
150
155
  * 操作序列:git add . → git write-tree → git restore --staged .
156
+ * 同时保存当前 HEAD commit hash,用于增量 validate 时对齐基准
151
157
  * @param {string} mainWorktreePath - 主 worktree 路径
152
158
  * @param {string} projectName - 项目名
153
159
  * @param {string} branchName - 分支名
@@ -157,7 +163,8 @@ function saveCurrentSnapshotTree(mainWorktreePath: string, projectName: string,
157
163
  gitAddAll(mainWorktreePath);
158
164
  const treeHash = gitWriteTree(mainWorktreePath);
159
165
  gitRestoreStaged(mainWorktreePath);
160
- writeSnapshot(projectName, branchName, treeHash);
166
+ const headCommitHash = getHeadCommitHash(mainWorktreePath);
167
+ writeSnapshot(projectName, branchName, treeHash, headCommitHash);
161
168
  return treeHash;
162
169
  }
163
170
 
@@ -225,8 +232,8 @@ function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: strin
225
232
  * @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
226
233
  */
227
234
  function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): void {
228
- // 步骤 1:读取旧 tree hash(在清空前读取)
229
- const oldTreeHash = readSnapshotTreeHash(projectName, branchName);
235
+ // 步骤 1:读取旧快照(tree hash + 当时的 HEAD commit hash)
236
+ const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash } = readSnapshot(projectName, branchName);
230
237
 
231
238
  // 步骤 2:确保主 worktree 干净(调用方已通过 handleDirtyMainWorktree 处理)
232
239
  // 这里做兜底清理,防止 handleDirtyMainWorktree 之后仍有残留
@@ -238,12 +245,35 @@ function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath:
238
245
  // 步骤 3:通过 patch 从目标分支获取最新全量变更
239
246
  migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
240
247
 
241
- // 步骤 4:保存最新快照为 git tree 对象
248
+ // 步骤 4:保存最新快照为 git tree 对象(同时记录当前 HEAD)
242
249
  saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
243
250
 
244
- // 步骤 5:将旧 tree 对象载入暂存区(恢复上次快照状态)
251
+ // 步骤 5:将旧变更状态载入暂存区
245
252
  try {
246
- gitReadTree(oldTreeHash, mainWorktreePath);
253
+ const currentHeadCommitHash = getHeadCommitHash(mainWorktreePath);
254
+
255
+ if (oldHeadCommitHash && oldHeadCommitHash !== currentHeadCommitHash) {
256
+ // HEAD 发生了变化(如主分支合并了其他分支):
257
+ // 将旧变更 patch(旧 tree 相对于旧 HEAD 的差异)重放到当前 HEAD 暂存区上,
258
+ // 避免新旧 tree 基准不同导致 diff 混入 HEAD 变化的内容
259
+ const oldHeadTreeHash = getCommitTreeHash(oldHeadCommitHash, mainWorktreePath);
260
+ const oldChangePatch = gitDiffTree(oldHeadTreeHash, oldTreeHash, mainWorktreePath);
261
+
262
+ if (oldChangePatch.length > 0 && gitApplyCachedCheck(oldChangePatch, mainWorktreePath)) {
263
+ // 无冲突:apply --cached 到当前 HEAD 暂存区
264
+ gitApplyCachedFromStdin(oldChangePatch, mainWorktreePath);
265
+ } else if (oldChangePatch.length > 0) {
266
+ // 有冲突:降级为全量模式(暂存区保持为空)
267
+ logger.warn('旧变更 patch 与当前 HEAD 冲突,降级为全量模式');
268
+ printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
269
+ printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
270
+ return;
271
+ }
272
+ // oldChangePatch 为空表示旧变更为空,暂存区保持干净即可
273
+ } else {
274
+ // HEAD 未变化(或旧版快照无 HEAD 信息):直接 read-tree 旧快照
275
+ gitReadTree(oldTreeHash, mainWorktreePath);
276
+ }
247
277
  } catch (error) {
248
278
  // 旧 tree 对象无法读取(可能被 git gc 回收),降级为全量模式
249
279
  logger.warn(`增量 read-tree 失败: ${error}`);
package/src/utils/git.ts CHANGED
@@ -425,3 +425,43 @@ export function gitWriteTree(cwd?: string): string {
425
425
  export function gitReadTree(treeHash: string, cwd?: string): void {
426
426
  execCommand(`git read-tree ${treeHash}`, { cwd });
427
427
  }
428
+
429
+ /**
430
+ * 获取指定 commit 对应的 tree 对象 hash
431
+ * @param {string} commitHash - commit hash
432
+ * @param {string} [cwd] - 工作目录
433
+ * @returns {string} tree 对象的 hash
434
+ */
435
+ export function getCommitTreeHash(commitHash: string, cwd?: string): string {
436
+ return execCommand(`git rev-parse ${commitHash}^{tree}`, { cwd });
437
+ }
438
+
439
+ /**
440
+ * 获取两个 tree 对象之间的 diff(patch 格式,含二进制)
441
+ * @param {string} baseTreeHash - 基准 tree hash
442
+ * @param {string} targetTreeHash - 目标 tree hash
443
+ * @param {string} [cwd] - 工作目录
444
+ * @returns {Buffer} diff patch 内容
445
+ */
446
+ export function gitDiffTree(baseTreeHash: string, targetTreeHash: string, cwd?: string): Buffer {
447
+ logger.debug(`执行命令: git diff-tree -p --binary ${baseTreeHash} ${targetTreeHash}${cwd ? ` (cwd: ${cwd})` : ''}`);
448
+ return execSync(`git diff-tree -p --binary ${baseTreeHash} ${targetTreeHash}`, {
449
+ cwd,
450
+ stdio: ['pipe', 'pipe', 'pipe'],
451
+ });
452
+ }
453
+
454
+ /**
455
+ * 检测 patch 能否无冲突地应用到暂存区(干运行,不实际修改)
456
+ * @param {Buffer} patchContent - patch 内容(Buffer 格式)
457
+ * @param {string} [cwd] - 工作目录
458
+ * @returns {boolean} patch 能否成功应用
459
+ */
460
+ export function gitApplyCachedCheck(patchContent: Buffer, cwd?: string): boolean {
461
+ try {
462
+ execCommandWithInput('git', ['apply', '--cached', '--check'], { input: patchContent, cwd });
463
+ return true;
464
+ } catch {
465
+ return false;
466
+ }
467
+ }
@@ -40,6 +40,9 @@ export {
40
40
  gitResetSoftTo,
41
41
  gitWriteTree,
42
42
  gitReadTree,
43
+ getCommitTreeHash,
44
+ gitDiffTree,
45
+ gitApplyCachedCheck,
43
46
  } from './git.js';
44
47
  export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
45
48
  export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
@@ -49,4 +52,4 @@ export { printSuccess, printError, printWarning, printInfo, printSeparator, prin
49
52
  export { ensureDir, removeEmptyDir } from './fs.js';
50
53
  export { multilineInput } from './prompt.js';
51
54
  export { launchInteractiveClaude } from './claude.js';
52
- export { getSnapshotPath, hasSnapshot, readSnapshotTreeHash, writeSnapshot, removeSnapshot, removeProjectSnapshots } from './validate-snapshot.js';
55
+ export { getSnapshotPath, hasSnapshot, readSnapshotTreeHash, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots } from './validate-snapshot.js';
@@ -5,7 +5,7 @@ import { ensureDir } from './fs.js';
5
5
  import { logger } from '../logger/index.js';
6
6
 
7
7
  /**
8
- * 获取指定项目和分支的 validate 快照文件路径
8
+ * 获取指定项目和分支的 validate 快照 tree 文件路径
9
9
  * @param {string} projectName - 项目名
10
10
  * @param {string} branchName - 分支名
11
11
  * @returns {string} tree hash 文件的绝对路径
@@ -14,6 +14,16 @@ export function getSnapshotPath(projectName: string, branchName: string): string
14
14
  return join(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
15
15
  }
16
16
 
17
+ /**
18
+ * 获取指定项目和分支的 validate 快照 head 文件路径
19
+ * @param {string} projectName - 项目名
20
+ * @param {string} branchName - 分支名
21
+ * @returns {string} head commit hash 文件的绝对路径
22
+ */
23
+ function getSnapshotHeadPath(projectName: string, branchName: string): string {
24
+ return join(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
25
+ }
26
+
17
27
  /**
18
28
  * 判断指定项目和分支是否存在 validate 快照
19
29
  * @param {string} projectName - 项目名
@@ -31,36 +41,61 @@ export function hasSnapshot(projectName: string, branchName: string): boolean {
31
41
  * @returns {string} tree 对象的 hash
32
42
  */
33
43
  export function readSnapshotTreeHash(projectName: string, branchName: string): string {
44
+ return readSnapshot(projectName, branchName).treeHash;
45
+ }
46
+
47
+ /**
48
+ * 读取指定项目和分支的 validate 快照(tree hash + HEAD commit hash)
49
+ * tree hash 从 .tree 文件读取,HEAD commit hash 从 .head 文件读取
50
+ * @param {string} projectName - 项目名
51
+ * @param {string} branchName - 分支名
52
+ * @returns {{ treeHash: string; headCommitHash: string }} 快照数据
53
+ */
54
+ export function readSnapshot(projectName: string, branchName: string): { treeHash: string; headCommitHash: string } {
34
55
  const snapshotPath = getSnapshotPath(projectName, branchName);
56
+ const headPath = getSnapshotHeadPath(projectName, branchName);
35
57
  logger.debug(`读取 validate 快照: ${snapshotPath}`);
36
- return readFileSync(snapshotPath, 'utf-8').trim();
58
+
59
+ const treeHash = existsSync(snapshotPath) ? readFileSync(snapshotPath, 'utf-8').trim() : '';
60
+ const headCommitHash = existsSync(headPath) ? readFileSync(headPath, 'utf-8').trim() : '';
61
+
62
+ return { treeHash, headCommitHash };
37
63
  }
38
64
 
39
65
  /**
40
66
  * 写入 validate 快照内容(自动创建目录)
67
+ * tree hash 写入 .tree 文件,HEAD commit hash 写入 .head 文件
41
68
  * @param {string} projectName - 项目名
42
69
  * @param {string} branchName - 分支名
43
70
  * @param {string} treeHash - git tree 对象的 hash
71
+ * @param {string} headCommitHash - 快照时主 worktree 的 HEAD commit hash
44
72
  */
45
- export function writeSnapshot(projectName: string, branchName: string, treeHash: string): void {
73
+ export function writeSnapshot(projectName: string, branchName: string, treeHash: string, headCommitHash: string): void {
46
74
  const snapshotPath = getSnapshotPath(projectName, branchName);
75
+ const headPath = getSnapshotHeadPath(projectName, branchName);
47
76
  const snapshotDir = join(VALIDATE_SNAPSHOTS_DIR, projectName);
48
77
  ensureDir(snapshotDir);
49
78
  writeFileSync(snapshotPath, treeHash, 'utf-8');
50
- logger.info(`已保存 validate 快照: ${snapshotPath}`);
79
+ writeFileSync(headPath, headCommitHash, 'utf-8');
80
+ logger.info(`已保存 validate 快照: ${snapshotPath}, ${headPath}`);
51
81
  }
52
82
 
53
83
  /**
54
- * 删除指定项目和分支的 validate 快照
84
+ * 删除指定项目和分支的 validate 快照(.tree + .head)
55
85
  * @param {string} projectName - 项目名
56
86
  * @param {string} branchName - 分支名
57
87
  */
58
88
  export function removeSnapshot(projectName: string, branchName: string): void {
59
89
  const snapshotPath = getSnapshotPath(projectName, branchName);
90
+ const headPath = getSnapshotHeadPath(projectName, branchName);
60
91
  if (existsSync(snapshotPath)) {
61
92
  unlinkSync(snapshotPath);
62
93
  logger.info(`已删除 validate 快照: ${snapshotPath}`);
63
94
  }
95
+ if (existsSync(headPath)) {
96
+ unlinkSync(headPath);
97
+ logger.info(`已删除 validate 快照: ${headPath}`);
98
+ }
64
99
  }
65
100
 
66
101
  /**
package/CLAUDE.md DELETED
@@ -1,105 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## 项目概述
6
-
7
- Clawt 是一个 CLI 工具,融合 Git Worktree 与 Claude Code CLI,支持在本地并行执行多个 Claude Code Agent 任务。核心思路:为每个任务创建独立的 git worktree,在各自隔离的环境中并行调用 `claude -p` 执行任务,互不干扰。
8
-
9
- ## 构建与开发
10
-
11
- ```bash
12
- npm run build # 使用 tsup 构建到 dist/
13
- npm run dev # tsup --watch 模式
14
- npm i -g . # 本地全局安装进行测试
15
- ```
16
-
17
- 构建工具为 tsup,入口 `src/index.ts`,输出 ESM 格式,target node18。构建产物在 `dist/index.js` 带有 shebang 头。另有 `scripts/postinstall.ts` 作为独立入口构建(npm 安装后初始化 `~/.clawt/` 目录)。
18
-
19
- 本项目无测试框架和 lint 工具。
20
-
21
- ## 架构
22
-
23
- ### 命令注册模式
24
-
25
- 每个命令为独立文件 `src/commands/<name>.ts`,导出 `registerXxxCommand(program)` 函数,在 `src/index.ts` 中统一注册到 Commander。命令内部逻辑封装在对应的 `handleXxx` 函数中。
26
-
27
- 十个命令:`create`、`run`、`resume`、`list`、`remove`、`validate`、`merge`、`config`、`sync`、`reset`。
28
-
29
- ### 核心流程(run 命令)
30
-
31
- run 命令有两种模式:
32
-
33
- **模式一:不传 `--tasks`(交互式界面模式)**
34
-
35
- 1. `validateMainWorktree()` 确认在主 worktree 根目录
36
- 2. `validateClaudeCodeInstalled()` 确认 claude CLI 可用
37
- 3. 检测分支是否已存在(`checkBranchExists()`),已存在则提示使用 `clawt resume -b <branch>` 恢复会话
38
- 4. `createWorktrees()` 创建单个 worktree
39
- 5. `launchInteractiveClaude()` 通过 `spawnSync` + `inherit stdio` 在 worktree 中直接启动 Claude Code 交互式界面(启动命令由配置项 `claudeCodeCommand` 指定,默认 `claude`)
40
-
41
- **模式二:传 `--tasks`(并行任务模式)**
42
-
43
- 1. `validateMainWorktree()` 确认在主 worktree 根目录
44
- 2. `validateClaudeCodeInstalled()` 确认 claude CLI 可用
45
- 3. `createWorktrees()` 批量创建 git worktree(串行)
46
- 4. `executeClaudeTask()` 通过 `spawnProcess` 并行调用 `claude -p <task> --output-format json --permission-mode bypassPermissions`
47
- 5. 每个任务完成时实时输出通知,全部完成后输出汇总
48
- 6. SIGINT(Ctrl+C)中断处理:`killAllChildProcesses()` 终止所有子进程 → 等待退出 → `handleInterruptCleanup()` 根据 `autoDeleteBranch` 配置自动或交互式清理 worktree 和分支
49
-
50
- ### resume 命令流程
51
-
52
- 1. `validateMainWorktree()` 确认在主 worktree 根目录
53
- 2. `validateClaudeCodeInstalled()` 确认 claude CLI 可用
54
- 3. `resolveTargetWorktree()` 解析目标 worktree(`-b` 可选):
55
- - 未传 `-b`:仅 1 个 worktree 直接使用,多个通过 `promptSelectBranch()`(Enquirer.Select)交互选择
56
- - 传了 `-b`:`findExactMatch()` 精确匹配 → `findFuzzyMatches()` 子串模糊匹配(大小写不敏感,唯一直接使用,多个交互选择) → 无匹配报错并列出可用分支
57
- 4. `launchInteractiveClaude()` 在目标 worktree 中启动 Claude Code 交互式界面
58
-
59
- ### validate + merge 工作流
60
-
61
- - `validate`:将目标分支的全量变更(已提交 + 未提交)通过 `git diff HEAD...branch --binary` 的 patch 方式迁移到主 worktree,便于在主 worktree 中测试。支持两种模式:
62
- - **首次 validate**(无历史快照):patch 迁移全量变更 → 通过 `git write-tree` 保存快照为 git tree 对象 → 结果:暂存区=空,工作目录=全量变更
63
- - **增量 validate**(存在历史快照):读取旧 tree hash → 确保主 worktree 干净 → patch 迁移最新变更 → 保存新 tree 对象快照 → `git read-tree` 将旧 tree 载入暂存区 → 结果:暂存区=上次快照,工作目录=最新变更(可通过 `git diff` 查看增量差异)
64
- - `--clean` 选项:根据 `confirmDestructiveOps` 配置提示确认 → 重置主 worktree + 删除对应快照文件
65
- - 快照存储路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.tree`(存储 git tree 对象 hash)
66
- - tree 对象不依赖主分支 HEAD,无需一致性校验
67
- - 变更检测:同时检测目标 worktree 的未提交修改和已提交 commit,两者均无则提示无需验证
68
- - 未提交修改处理:有未提交修改时先做临时 commit,diff 完成后通过 `git reset --soft` 撤销恢复原状
69
- - `merge`:检测目标 worktree 状态(有修改则需 `-m` 提交,已提交则跳过,无变更则报错)→ **squash 检测**(检查目标分支是否存在 `AUTO_SAVE_COMMIT_MESSAGE` 前缀的 auto-save commit,如有则提示用户是否压缩所有提交:用户确认后通过 `gitMergeBase` 计算分叉点、`gitResetSoftTo` 将所有 commit reset 到暂存区;有 `-m` 则直接提交继续流程,无 `-m` 则提示用户自行提交后退出)→ 合并到主 worktree → 根据 `autoPullPush` 配置决定是否 pull + push(成功消息动态显示推送状态)→ 可选清理 worktree 和分支(受 `autoDeleteBranch` 配置或交互式确认控制)→ 清理对应的 validate 快照
70
- - `run` 中断清理:Ctrl+C 终止所有子进程后,根据 `autoDeleteBranch` 配置自动清理或交互式确认清理本次创建的 worktree 和分支
71
-
72
- ### sync 命令流程
73
-
74
- 1. `validateMainWorktree()` 确认在主 worktree 根目录
75
- 2. 检查目标 worktree 是否存在
76
- 3. 获取主分支名(`getCurrentBranch()`,不硬编码 main/master)
77
- 4. 如果目标 worktree 有未提交变更,自动 `git add . && git commit` 保存
78
- 5. 在目标 worktree 中执行 `git merge <mainBranch>` 合并主分支
79
- 6. 冲突处理:有冲突时提示用户手动解决,无冲突则输出成功
80
- 7. 合并成功后清除该分支的 validate 快照(代码基础已变化,旧快照无效)
81
-
82
- ### reset 命令流程
83
-
84
- 1. `validateMainWorktree()` 确认在主 worktree 根目录
85
- 2. 检测主 worktree 工作区和暂存区是否干净(`isWorkingDirClean()`)
86
- 3. 不干净 → 根据 `confirmDestructiveOps` 配置决定是否通过 `confirmDestructiveAction()` 提示确认 → `gitResetHard()` + `gitCleanForce()` 重置工作区和暂存区(保留 validate 快照)
87
- 4. 已干净 → 提示无需重置
88
-
89
- ### 目录层级
90
-
91
- - `src/commands/` — 各命令的注册与处理逻辑
92
- - `src/utils/` — 工具函数(git 操作(含三点 diff、分支合并、冲突检测、merge-base 计算、commit message 检测、soft reset 到指定 commit、write-tree/read-tree、分支存在性检测等)、shell 执行与子进程管理、分支名处理、worktree 管理与批量清理、配置、格式化输出与破坏性操作确认、交互式输入、Claude Code 交互式启动、validate 快照管理(基于 git tree 对象))
93
- - `src/constants/` — 常量定义(路径、退出码、消息模板、分支规则、配置默认值、终端控制序列、validate 快照目录、sync 相关消息、git 常量(如 `AUTO_SAVE_COMMIT_MESSAGE`)、squash 相关消息、reset 相关消息、remove 相关消息、破坏性操作确认消息)
94
- - `src/types/` — TypeScript 类型定义
95
- - `src/errors/` — 自定义 `ClawtError` 错误类(携带退出码)
96
- - `src/logger/` — winston 日志(按日期滚动,写入 `~/.clawt/logs/`)
97
-
98
- ### 关键约定
99
-
100
- - 所有命令执行前都会调用 `validateMainWorktree()` 确保在主 worktree 根目录(`git rev-parse --git-common-dir === ".git"`)
101
- - Worktree 统一存放在 `~/.clawt/worktrees/<projectName>/` 下
102
- - 全局配置文件 `~/.clawt/config.json`,postinstall 时自动创建/合并,包含 `autoDeleteBranch`(是否自动删除分支)、`claudeCodeCommand`(Claude Code CLI 启动指令,用于 `run` 和 `resume` 的交互式界面)、`autoPullPush`(merge 后是否自动 pull/push)、`confirmDestructiveOps`(破坏性操作前是否提示确认,影响 `reset` 和 `validate --clean`)四个配置项。配置项以 `CONFIG_DEFINITIONS` 为单一数据源,`DEFAULT_CONFIG` 和 `CONFIG_DESCRIPTIONS` 均从中派生
103
- - shell 命令执行有同步(`execCommand` → `execSync`)、异步(`spawnProcess` → `spawn`)和同步带 stdin(`execCommandWithInput` → `execFileSync`)三种方式
104
- - 项目为纯 ESM(`"type": "module"`),模块导入需带 `.js` 后缀
105
- - 分支名特殊字符会被 `sanitizeBranchName()` 自动清理