clawt 1.1.1 → 1.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,16 +4,18 @@
4
4
 
5
5
  ### docs/spec.md
6
6
  - 完整的软件规格说明,包含 7 大章节
7
- - 命令流程在 `5. 需求场景详细设计` 下,每个命令一个子章节(5.1-5.10
7
+ - 命令流程在 `5. 需求场景详细设计` 下,每个命令一个子章节(5.1-5.11
8
8
  - run 命令对应 `5.2 批量创建 Worktree + 执行 Claude Code 任务`,流程按步骤编号描述
9
9
  - merge 命令对应 `5.6 合并验证过的分支`,流程按步骤编号描述
10
10
  - config 命令对应 `5.10 查看全局配置`,只读展示配置
11
+ - resume 命令对应 `5.11 在已有 Worktree 中恢复会话`,查找已有 worktree 并启动交互式界面
11
12
  - 配置项说明在 `5.7 默认配置文件` 章节的表格中
12
13
  - 更新模式:新增步骤时追加编号,配置项影响范围变化时更新说明列
13
14
 
14
15
  ### CLAUDE.md
15
16
  - 面向 Claude Code 的项目架构指引,精简扼要
16
17
  - run 命令流程在 `核心流程(run 命令)` 章节,编号列表描述
18
+ - resume 命令流程在独立的 `### resume 命令流程` 章节,编号列表描述
17
19
  - merge 和 run 中断清理在 `validate + merge 工作流` 章节,一行式描述用箭头连接流程
18
20
  - utils 目录描述用括号内逗号分隔列举功能模块
19
21
  - 更新模式:编号列表追加步骤,箭头链追加阶段,括号内追加关键词
@@ -27,10 +29,13 @@
27
29
  ## 关键约定
28
30
  - `autoDeleteBranch` 配置项影响三处:remove 命令、merge 命令、run 中断清理
29
31
  - merge 的清理确认在 merge 操作之前询问(避免交互中断),但清理在 merge 成功后执行
32
+ - merge 成功后自动清理对应的 validate 快照(hasSnapshot + removeSnapshot)
30
33
  - run 的中断清理在所有子进程退出后执行
31
34
  - 文档中文风格,技术术语保留英文(worktree, merge, branch, SIGINT 等)
32
35
  - cleanupWorktrees 是 merge 和 run 共用的公共清理函数(在 src/utils/worktree.ts)
36
+ - `launchInteractiveClaude` 是 run(交互式模式)和 resume 共用的公共函数(在 src/utils/claude.ts)
33
37
  - killAllChildProcesses 是 run 专用的子进程终止函数(在 src/utils/shell.ts)
38
+ - validate 快照管理函数在 `src/utils/validate-snapshot.ts`,被 validate 和 merge 两个命令使用
34
39
 
35
40
  ## 配置项同步检查点
36
41
 
@@ -55,6 +60,24 @@ run 命令有两种模式(自 claudeCodeCommand 特性后):
55
60
  - 传 `--tasks`:并行任务模式(多 worktree + `executeClaudeTask` + spawnProcess)
56
61
  - CLAUDE.md 中的核心流程按模式分段描述
57
62
 
58
- ## 命令清单(7 个)
63
+ ## 命令清单(8 个)
59
64
 
60
- `create`、`run`、`list`、`remove`、`validate`、`merge`、`config`
65
+ `create`、`run`、`resume`、`list`、`remove`、`validate`、`merge`、`config`
66
+
67
+ Notes:
68
+ - resume 和 run(交互式模式)共用 `launchInteractiveClaude()`,该函数从 run.ts 提取到 src/utils/claude.ts
69
+ - `claudeCodeCommand` 配置项同时影响 run 交互式模式和 resume 命令
70
+
71
+ ## validate 快照机制
72
+
73
+ - validate 命令支持首次/增量两种模式,通过 `hasSnapshot()` 判断
74
+ - 快照路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.patch`
75
+ - 常量 `VALIDATE_SNAPSHOTS_DIR` 定义在 `src/constants/paths.ts`
76
+ - validate 新增 `--clean` 选项(`ValidateOptions.clean?: boolean`)
77
+ - 增量模式核心:旧 patch 应用到暂存区 + 新全量变更在工作目录 → `git diff` 可查看增量差异
78
+ - 增量 apply 失败时自动降级为全量模式
79
+ - shell 层新增 `execCommandWithInput()`(`execFileSync` + stdin),用于 `gitApplyCachedFromStdin()`
80
+ - git 层新增 `gitDiffCachedBinary()`(返回 Buffer)和 `gitApplyCachedFromStdin()`
81
+ - merge 成功后自动清理对应快照;merge 时主 worktree 脏 + 存在快照会输出警告提示
82
+ - docs/spec.md 中 validate 章节(5.4)按 `--clean 模式`、`首次 validate`、`增量 validate` 三段描述
83
+ - 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`、`list`、`remove`、`validate`、`merge`、`config`。
27
+ 八个命令:`create`、`run`、`resume`、`list`、`remove`、`validate`、`merge`、`config`。
28
28
 
29
29
  ### 核心流程(run 命令)
30
30
 
@@ -46,17 +46,28 @@ run 命令有两种模式:
46
46
  5. 每个任务完成时实时输出通知,全部完成后输出汇总
47
47
  6. SIGINT(Ctrl+C)中断处理:`killAllChildProcesses()` 终止所有子进程 → 等待退出 → `handleInterruptCleanup()` 根据 `autoDeleteBranch` 配置自动或交互式清理 worktree 和分支
48
48
 
49
+ ### resume 命令流程
50
+
51
+ 1. `validateMainWorktree()` 确认在主 worktree 根目录
52
+ 2. `validateClaudeCodeInstalled()` 确认 claude CLI 可用
53
+ 3. `findWorktreeByBranch()` 在当前项目的 worktree 列表中按分支名查找已有 worktree
54
+ 4. `launchInteractiveClaude()` 在目标 worktree 中启动 Claude Code 交互式界面
55
+
49
56
  ### validate + merge 工作流
50
57
 
51
- - `validate`:将目标 worktree 的变更通过 git stash 迁移到主 worktree,便于在主 worktree 中测试
52
- - `merge`:检测目标 worktree 状态(有修改则需 `-m` 提交,已提交则跳过,无变更则报错)→ 合并到主 worktree pull push 可选清理 worktree 和分支(受 `autoDeleteBranch` 配置或交互式确认控制)
58
+ - `validate`:将目标 worktree 的变更通过 git stash 迁移到主 worktree,便于在主 worktree 中测试。支持两种模式:
59
+ - **首次 validate**(无历史快照):stash 迁移保存纯净快照 patch结果:暂存区=空,工作目录=全量变更
60
+ - **增量 validate**(存在历史快照):读取旧 patch → 清空主 worktree → stash 迁移最新变更 → 保存新快照 → 旧 patch 应用到暂存区 → 结果:暂存区=上次快照,工作目录=最新变更(可通过 `git diff` 查看增量差异)
61
+ - `--clean` 选项:重置主 worktree + 删除对应快照文件
62
+ - 快照存储路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.patch`
63
+ - `merge`:检测目标 worktree 状态(有修改则需 `-m` 提交,已提交则跳过,无变更则报错)→ 合并到主 worktree → pull → push → 可选清理 worktree 和分支(受 `autoDeleteBranch` 配置或交互式确认控制)→ 清理对应的 validate 快照
53
64
  - `run` 中断清理:Ctrl+C 终止所有子进程后,根据 `autoDeleteBranch` 配置自动清理或交互式确认清理本次创建的 worktree 和分支
54
65
 
55
66
  ### 目录层级
56
67
 
57
68
  - `src/commands/` — 各命令的注册与处理逻辑
58
- - `src/utils/` — 工具函数(git 操作、shell 执行与子进程管理、分支名处理、worktree 管理与批量清理、配置、格式化输出、交互式输入)
59
- - `src/constants/` — 常量定义(路径、退出码、消息模板、分支规则、配置默认值、终端控制序列)
69
+ - `src/utils/` — 工具函数(git 操作、shell 执行与子进程管理、分支名处理、worktree 管理与批量清理、配置、格式化输出、交互式输入、Claude Code 交互式启动、validate 快照管理)
70
+ - `src/constants/` — 常量定义(路径、退出码、消息模板、分支规则、配置默认值、终端控制序列、validate 快照目录)
60
71
  - `src/types/` — TypeScript 类型定义
61
72
  - `src/errors/` — 自定义 `ClawtError` 错误类(携带退出码)
62
73
  - `src/logger/` — winston 日志(按日期滚动,写入 `~/.clawt/logs/`)
@@ -65,7 +76,7 @@ run 命令有两种模式:
65
76
 
66
77
  - 所有命令执行前都会调用 `validateMainWorktree()` 确保在主 worktree 根目录(`git rev-parse --git-common-dir === ".git"`)
67
78
  - Worktree 统一存放在 `~/.clawt/worktrees/<projectName>/` 下
68
- - 全局配置文件 `~/.clawt/config.json`,postinstall 时自动创建/合并,包含 `autoDeleteBranch`(是否自动删除分支)、`claudeCodeCommand`(Claude Code CLI 启动指令)、`autoPullPush`(merge 后是否自动 pull/push)三个配置项。配置项以 `CONFIG_DEFINITIONS` 为单一数据源,`DEFAULT_CONFIG` 和 `CONFIG_DESCRIPTIONS` 均从中派生
69
- - shell 命令执行有同步(`execCommand` → `execSync`)和异步(`spawnProcess` → `spawn`)两种方式
79
+ - 全局配置文件 `~/.clawt/config.json`,postinstall 时自动创建/合并,包含 `autoDeleteBranch`(是否自动删除分支)、`claudeCodeCommand`(Claude Code CLI 启动指令,用于 `run` 和 `resume` 的交互式界面)、`autoPullPush`(merge 后是否自动 pull/push)三个配置项。配置项以 `CONFIG_DEFINITIONS` 为单一数据源,`DEFAULT_CONFIG` 和 `CONFIG_DESCRIPTIONS` 均从中派生
80
+ - shell 命令执行有同步(`execCommand` → `execSync`)、异步(`spawnProcess` → `spawn`)和同步带 stdin(`execCommandWithInput` → `execFileSync`)三种方式
70
81
  - 项目为纯 ESM(`"type": "module"`),模块导入需带 `.js` 后缀
71
82
  - 分支名特殊字符会被 `sanitizeBranchName()` 自动清理
package/README.md CHANGED
@@ -12,7 +12,7 @@ npm i -g clawt
12
12
 
13
13
  - Node.js >= 18
14
14
  - Git >= 2.15
15
- - Claude Code CLI(仅 `clawt run` 需要)
15
+ - Claude Code CLI(`clawt run` 和 `clawt resume` 需要)
16
16
 
17
17
  ## 使用前提
18
18
 
@@ -73,20 +73,47 @@ clawt run -b feature-scheme \
73
73
  clawt run -b feature-login
74
74
  ```
75
75
 
76
+ ### `clawt resume` — 在已有 worktree 中恢复 Claude Code 会话
77
+
78
+ ```bash
79
+ clawt resume -b <branchName>
80
+ ```
81
+
82
+ | 参数 | 必填 | 说明 |
83
+ | ---- | ---- | ---- |
84
+ | `-b` | 是 | 要恢复的分支名 |
85
+
86
+ 在之前通过 `clawt run` 或 `clawt create` 创建的 worktree 中重新打开 Claude Code 交互式界面,继续之前的工作。启动命令由配置项 `claudeCodeCommand` 指定(默认 `claude`)。
87
+
88
+ ```bash
89
+ # 在已有 worktree 中恢复会话
90
+ clawt resume -b feature-login
91
+ ```
92
+
76
93
  ### `clawt validate` — 在主 worktree 验证分支变更
77
94
 
78
95
  ```bash
79
- clawt validate -b <branchName>
96
+ clawt validate -b <branchName> [--clean]
80
97
  ```
81
98
 
82
99
  | 参数 | 必填 | 说明 |
83
100
  | ---- | ---- | ---- |
84
101
  | `-b` | 是 | 要验证的分支名 |
102
+ | `--clean` | 否 | 清理 validate 状态(重置主 worktree 并删除快照) |
85
103
 
86
104
  将目标 worktree 的变更通过 `git stash` 迁移到主 worktree,方便在主 worktree 中直接测试,无需重新安装依赖。
87
105
 
106
+ 支持增量模式:首次 validate 后会自动保存快照,再次 validate 同一分支时会将上次快照应用到暂存区、最新变更保留在工作目录,用户可通过 `git diff` 查看两次 validate 之间的增量差异。使用 `--clean` 可清理 validate 状态(重置主 worktree 并删除快照文件)。
107
+
88
108
  ```bash
109
+ # 首次验证
89
110
  clawt validate -b feature-scheme-1
111
+
112
+ # 再次验证(增量模式,可通过 git diff 查看增量差异)
113
+ clawt validate -b feature-scheme-1
114
+
115
+ # 清理 validate 状态
116
+ clawt validate -b feature-scheme-1 --clean
90
117
  ```
91
118
 
92
119
  ### `clawt merge` — 合并分支到主 worktree
@@ -162,7 +189,7 @@ clawt config
162
189
  | 配置项 | 类型 | 默认值 | 说明 |
163
190
  | ------ | ---- | ------ | ---- |
164
191
  | `autoDeleteBranch` | `boolean` | `false` | 移除 worktree 时自动删除对应本地分支;merge 成功后自动清理 worktree 和分支;run 中断后自动清理本次创建的 worktree 和分支 |
165
- | `claudeCodeCommand` | `string` | `"claude"` | Claude Code CLI 启动指令,用于 `clawt run` 不传 `--tasks` 时在 worktree 中打开交互式界面 |
192
+ | `claudeCodeCommand` | `string` | `"claude"` | Claude Code CLI 启动指令,用于 `clawt run` 不传 `--tasks` 时和 `clawt resume` 在 worktree 中打开交互式界面 |
166
193
  | `autoPullPush` | `boolean` | `false` | merge 成功后是否自动执行 git pull 和 git push |
167
194
 
168
195
  ## 分支名规则
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ var CLAWT_HOME = join(homedir(), ".clawt");
13
13
  var CONFIG_PATH = join(CLAWT_HOME, "config.json");
14
14
  var LOGS_DIR = join(CLAWT_HOME, "logs");
15
15
  var WORKTREES_DIR = join(CLAWT_HOME, "worktrees");
16
+ var VALIDATE_SNAPSHOTS_DIR = join(CLAWT_HOME, "validate-snapshots");
16
17
 
17
18
  // src/constants/branch.ts
18
19
  var INVALID_BRANCH_CHARS = /[\/\\.\s~:*?[\]^]+/g;
@@ -81,7 +82,18 @@ var MESSAGES = {
81
82
  /** 创建数量参数无效 */
82
83
  INVALID_COUNT: (value) => `\u65E0\u6548\u7684\u521B\u5EFA\u6570\u91CF: "${value}"\uFF0C\u8BF7\u8F93\u5165\u6B63\u6574\u6570`,
83
84
  /** worktree 状态获取失败 */
84
- WORKTREE_STATUS_UNAVAILABLE: "(\u72B6\u6001\u4E0D\u53EF\u7528)"
85
+ WORKTREE_STATUS_UNAVAILABLE: "(\u72B6\u6001\u4E0D\u53EF\u7528)",
86
+ /** 增量 validate 成功提示 */
87
+ INCREMENTAL_VALIDATE_SUCCESS: (branch) => `\u2713 \u5DF2\u5C06\u5206\u652F ${branch} \u7684\u6700\u65B0\u53D8\u66F4\u5E94\u7528\u5230\u4E3B worktree\uFF08\u589E\u91CF\u6A21\u5F0F\uFF09
88
+ \u6682\u5B58\u533A = \u4E0A\u6B21\u5FEB\u7167\uFF0C\u5DE5\u4F5C\u76EE\u5F55 = \u6700\u65B0\u53D8\u66F4`,
89
+ /** 增量 validate 降级为全量模式提示 */
90
+ INCREMENTAL_VALIDATE_FALLBACK: "\u589E\u91CF\u5BF9\u6BD4\u5931\u8D25\uFF0C\u5DF2\u964D\u7EA7\u4E3A\u5168\u91CF\u6A21\u5F0F",
91
+ /** validate 状态已清理 */
92
+ VALIDATE_CLEANED: (branch) => `\u2713 \u5206\u652F ${branch} \u7684 validate \u72B6\u6001\u5DF2\u6E05\u7406`,
93
+ /** 增量 validate 检测到脏状态,即将清空 */
94
+ INCREMENTAL_VALIDATE_RESET: "\u68C0\u6D4B\u5230\u4E0A\u6B21 validate \u7684\u6B8B\u7559\u72B6\u6001\uFF0C\u5C06\u6E05\u7A7A\u4E3B worktree \u5E76\u91CD\u65B0\u5E94\u7528",
95
+ /** merge 命令检测到 validate 状态的提示 */
96
+ MERGE_VALIDATE_STATE_HINT: (branch) => `\u4E3B worktree \u53EF\u80FD\u5B58\u5728 validate \u6B8B\u7559\u72B6\u6001\uFF0C\u53EF\u5148\u6267\u884C clawt validate -b ${branch} --clean \u6E05\u7406`
85
97
  };
86
98
 
87
99
  // src/constants/exitCodes.ts
@@ -168,7 +180,7 @@ var logger = winston.createLogger({
168
180
  });
169
181
 
170
182
  // src/utils/shell.ts
171
- import { execSync, spawn } from "child_process";
183
+ import { execSync, execFileSync, spawn } from "child_process";
172
184
  function execCommand(command, options) {
173
185
  logger.debug(`\u6267\u884C\u547D\u4EE4: ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
174
186
  const result = execSync(command, {
@@ -192,9 +204,20 @@ function killAllChildProcesses(children) {
192
204
  }
193
205
  }
194
206
  }
207
+ function execCommandWithInput(command, args, options) {
208
+ logger.debug(`\u6267\u884C\u547D\u4EE4(stdin): ${command} ${args.join(" ")}${options.cwd ? ` (cwd: ${options.cwd})` : ""}`);
209
+ const result = execFileSync(command, args, {
210
+ cwd: options.cwd,
211
+ input: options.input,
212
+ encoding: "utf-8",
213
+ stdio: ["pipe", "pipe", "pipe"]
214
+ });
215
+ return result.trim();
216
+ }
195
217
 
196
218
  // src/utils/git.ts
197
219
  import { basename } from "path";
220
+ import { execSync as execSync2 } from "child_process";
198
221
  function getGitCommonDir(cwd) {
199
222
  return execCommand("git rev-parse --git-common-dir", { cwd });
200
223
  }
@@ -316,6 +339,16 @@ function getDiffStat(branchName, worktreePath, cwd) {
316
339
  deletions: committed.deletions + uncommitted.deletions
317
340
  };
318
341
  }
342
+ function gitDiffCachedBinary(cwd) {
343
+ logger.debug(`\u6267\u884C\u547D\u4EE4: git diff --cached --binary${cwd ? ` (cwd: ${cwd})` : ""}`);
344
+ return execSync2("git diff --cached --binary", {
345
+ cwd,
346
+ stdio: ["pipe", "pipe", "pipe"]
347
+ });
348
+ }
349
+ function gitApplyCachedFromStdin(patchContent, cwd) {
350
+ execCommandWithInput("git", ["apply", "--cached"], { input: patchContent, cwd });
351
+ }
319
352
 
320
353
  // src/utils/formatter.ts
321
354
  import chalk from "chalk";
@@ -537,6 +570,63 @@ function ensureClawtDirs() {
537
570
  // src/utils/prompt.ts
538
571
  import Enquirer from "enquirer";
539
572
 
573
+ // src/utils/claude.ts
574
+ import { spawnSync } from "child_process";
575
+ function launchInteractiveClaude(worktree) {
576
+ const commandStr = getConfigValue("claudeCodeCommand");
577
+ const parts = commandStr.split(/\s+/).filter(Boolean);
578
+ const cmd = parts[0];
579
+ const args = [
580
+ ...parts.slice(1),
581
+ "--append-system-prompt",
582
+ APPEND_SYSTEM_PROMPT
583
+ ];
584
+ printInfo(`\u6B63\u5728 worktree \u4E2D\u542F\u52A8 Claude Code \u4EA4\u4E92\u5F0F\u754C\u9762...`);
585
+ printInfo(` \u5206\u652F: ${worktree.branch}`);
586
+ printInfo(` \u8DEF\u5F84: ${worktree.path}`);
587
+ printInfo(` \u6307\u4EE4: ${commandStr}`);
588
+ printInfo("");
589
+ const result = spawnSync(cmd, args, {
590
+ cwd: worktree.path,
591
+ stdio: "inherit"
592
+ });
593
+ if (result.error) {
594
+ throw new ClawtError(`\u542F\u52A8 Claude Code \u5931\u8D25: ${result.error.message}`);
595
+ }
596
+ if (result.status !== null && result.status !== 0) {
597
+ printWarning(`Claude Code \u9000\u51FA\u7801: ${result.status}`);
598
+ }
599
+ }
600
+
601
+ // src/utils/validate-snapshot.ts
602
+ import { join as join3 } from "path";
603
+ import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync3, rmdirSync as rmdirSync2 } from "fs";
604
+ function getSnapshotPath(projectName, branchName) {
605
+ return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.patch`);
606
+ }
607
+ function hasSnapshot(projectName, branchName) {
608
+ return existsSync5(getSnapshotPath(projectName, branchName));
609
+ }
610
+ function readSnapshot(projectName, branchName) {
611
+ const snapshotPath = getSnapshotPath(projectName, branchName);
612
+ logger.debug(`\u8BFB\u53D6 validate \u5FEB\u7167: ${snapshotPath}`);
613
+ return readFileSync2(snapshotPath);
614
+ }
615
+ function writeSnapshot(projectName, branchName, patch) {
616
+ const snapshotPath = getSnapshotPath(projectName, branchName);
617
+ const snapshotDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
618
+ ensureDir(snapshotDir);
619
+ writeFileSync2(snapshotPath, patch);
620
+ logger.info(`\u5DF2\u4FDD\u5B58 validate \u5FEB\u7167: ${snapshotPath}`);
621
+ }
622
+ function removeSnapshot(projectName, branchName) {
623
+ const snapshotPath = getSnapshotPath(projectName, branchName);
624
+ if (existsSync5(snapshotPath)) {
625
+ unlinkSync(snapshotPath);
626
+ logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${snapshotPath}`);
627
+ }
628
+ }
629
+
540
630
  // src/commands/list.ts
541
631
  import chalk2 from "chalk";
542
632
  function registerListCommand(program2) {
@@ -596,7 +686,7 @@ function handleCreate(options) {
596
686
  }
597
687
 
598
688
  // src/commands/remove.ts
599
- import { join as join3 } from "path";
689
+ import { join as join4 } from "path";
600
690
  function registerRemoveCommand(program2) {
601
691
  program2.command("remove").description("\u79FB\u9664 worktree\uFF08\u652F\u6301\u5355\u4E2A/\u6279\u91CF/\u5168\u90E8\uFF09").option("--all", "\u79FB\u9664\u5F53\u524D\u9879\u76EE\u4E0B\u6240\u6709 worktree").option("-b, --branch <branchName>", "\u6307\u5B9A\u5206\u652F\u540D").option("-i, --index <index>", "\u6307\u5B9A\u7D22\u5F15\uFF08\u914D\u5408 -b \u4F7F\u7528\uFF09").action(async (options) => {
602
692
  await handleRemove(options);
@@ -613,7 +703,7 @@ function resolveWorktreesToRemove(options) {
613
703
  }
614
704
  if (options.index !== void 0) {
615
705
  const targetName = `${options.branch}-${options.index}`;
616
- const targetPath = join3(projectDir, targetName);
706
+ const targetPath = join4(projectDir, targetName);
617
707
  const found = allWorktrees.find((wt) => wt.path === targetPath);
618
708
  if (!found) {
619
709
  throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(targetName));
@@ -665,37 +755,11 @@ async function handleRemove(options) {
665
755
  }
666
756
 
667
757
  // src/commands/run.ts
668
- import { spawnSync } from "child_process";
669
758
  function registerRunCommand(program2) {
670
759
  program2.command("run").description("\u6279\u91CF\u521B\u5EFA worktree \u5E76\u542F\u52A8 Claude Code \u6267\u884C\u4EFB\u52A1").requiredOption("-b, --branch <branchName>", "\u5206\u652F\u540D").option("--tasks <task...>", "\u4EFB\u52A1\u5217\u8868\uFF08\u53EF\u591A\u6B21\u6307\u5B9A\uFF09\uFF0C\u4E0D\u4F20\u5219\u5728 worktree \u4E2D\u6253\u5F00 Claude Code \u4EA4\u4E92\u5F0F\u754C\u9762").action(async (options) => {
671
760
  await handleRun(options);
672
761
  });
673
762
  }
674
- function launchInteractiveClaude(worktree) {
675
- const commandStr = getConfigValue("claudeCodeCommand");
676
- const parts = commandStr.split(/\s+/).filter(Boolean);
677
- const cmd = parts[0];
678
- const args = [
679
- ...parts.slice(1),
680
- "--append-system-prompt",
681
- APPEND_SYSTEM_PROMPT
682
- ];
683
- printInfo(`\u6B63\u5728 worktree \u4E2D\u542F\u52A8 Claude Code \u4EA4\u4E92\u5F0F\u754C\u9762...`);
684
- printInfo(` \u5206\u652F: ${worktree.branch}`);
685
- printInfo(` \u8DEF\u5F84: ${worktree.path}`);
686
- printInfo(` \u6307\u4EE4: ${commandStr}`);
687
- printInfo("");
688
- const result = spawnSync(cmd, args, {
689
- cwd: worktree.path,
690
- stdio: "inherit"
691
- });
692
- if (result.error) {
693
- throw new ClawtError(`\u542F\u52A8 Claude Code \u5931\u8D25: ${result.error.message}`);
694
- }
695
- if (result.status !== null && result.status !== 0) {
696
- printWarning(`Claude Code \u9000\u51FA\u7801: ${result.status}`);
697
- }
698
- }
699
763
  function executeClaudeTask(worktree, task) {
700
764
  const child = spawnProcess(
701
765
  "claude",
@@ -856,12 +920,34 @@ async function handleRun(options) {
856
920
  printTaskSummary(summary);
857
921
  }
858
922
 
923
+ // src/commands/resume.ts
924
+ function registerResumeCommand(program2) {
925
+ program2.command("resume").description("\u5728\u5DF2\u6709 worktree \u4E2D\u6062\u590D Claude Code \u4EA4\u4E92\u5F0F\u4F1A\u8BDD").requiredOption("-b, --branch <branchName>", "\u8981\u6062\u590D\u7684\u5206\u652F\u540D").action(async (options) => {
926
+ await handleResume(options);
927
+ });
928
+ }
929
+ function findWorktreeByBranch(branchName) {
930
+ const worktrees = getProjectWorktrees();
931
+ const matched = worktrees.find((wt) => wt.branch === branchName);
932
+ if (!matched) {
933
+ throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(branchName));
934
+ }
935
+ return matched;
936
+ }
937
+ async function handleResume(options) {
938
+ validateMainWorktree();
939
+ validateClaudeCodeInstalled();
940
+ logger.info(`resume \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}`);
941
+ const worktree = findWorktreeByBranch(options.branch);
942
+ launchInteractiveClaude(worktree);
943
+ }
944
+
859
945
  // src/commands/validate.ts
860
- import { join as join4 } from "path";
861
- import { existsSync as existsSync5 } from "fs";
946
+ import { join as join5 } from "path";
947
+ import { existsSync as existsSync6 } from "fs";
862
948
  import Enquirer2 from "enquirer";
863
949
  function registerValidateCommand(program2) {
864
- program2.command("validate").description("\u5728\u4E3B worktree \u9A8C\u8BC1\u67D0\u4E2A worktree \u5206\u652F\u7684\u53D8\u66F4").requiredOption("-b, --branch <branchName>", "\u8981\u9A8C\u8BC1\u7684\u5206\u652F\u540D").action(async (options) => {
950
+ program2.command("validate").description("\u5728\u4E3B worktree \u9A8C\u8BC1\u67D0\u4E2A worktree \u5206\u652F\u7684\u53D8\u66F4").requiredOption("-b, --branch <branchName>", "\u8981\u9A8C\u8BC1\u7684\u5206\u652F\u540D").option("--clean", "\u6E05\u7406 validate \u72B6\u6001\uFF08\u91CD\u7F6E\u4E3B worktree \u5E76\u5220\u9664\u5FEB\u7167\uFF09").action(async (options) => {
865
951
  await handleValidate(options);
866
952
  });
867
953
  }
@@ -899,24 +985,8 @@ async function handleDirtyMainWorktree(mainWorktreePath) {
899
985
  throw new ClawtError("\u5DE5\u4F5C\u533A\u4ECD\u7136\u4E0D\u5E72\u51C0\uFF0C\u8BF7\u624B\u52A8\u5904\u7406");
900
986
  }
901
987
  }
902
- async function handleValidate(options) {
903
- validateMainWorktree();
904
- const projectName = getProjectName();
905
- const mainWorktreePath = getGitTopLevel();
906
- const projectDir = getProjectWorktreeDir();
907
- const targetWorktreePath = join4(projectDir, options.branch);
908
- logger.info(`validate \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}`);
909
- if (!existsSync5(targetWorktreePath)) {
910
- throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
911
- }
912
- if (!isWorkingDirClean(mainWorktreePath)) {
913
- await handleDirtyMainWorktree(mainWorktreePath);
914
- }
915
- if (isWorkingDirClean(targetWorktreePath)) {
916
- printInfo(MESSAGES.TARGET_WORKTREE_CLEAN);
917
- return;
918
- }
919
- const stashMessage = `clawt:validate:${options.branch}`;
988
+ function migrateChangesViaStash(targetWorktreePath, mainWorktreePath, branchName) {
989
+ const stashMessage = `clawt:validate:${branchName}`;
920
990
  gitAddAll(targetWorktreePath);
921
991
  gitStashPush(stashMessage, targetWorktreePath);
922
992
  gitStashApply(targetWorktreePath);
@@ -927,12 +997,86 @@ async function handleValidate(options) {
927
997
  throw new ClawtError(MESSAGES.STASH_CHANGED);
928
998
  }
929
999
  gitStashPop(0, mainWorktreePath);
930
- printSuccess(MESSAGES.VALIDATE_SUCCESS(options.branch));
1000
+ }
1001
+ function saveCurrentSnapshotPatch(mainWorktreePath, projectName, branchName) {
1002
+ gitAddAll(mainWorktreePath);
1003
+ const patch = gitDiffCachedBinary(mainWorktreePath);
1004
+ gitRestoreStaged(mainWorktreePath);
1005
+ writeSnapshot(projectName, branchName, patch);
1006
+ return patch;
1007
+ }
1008
+ function handleValidateClean(options) {
1009
+ validateMainWorktree();
1010
+ const projectName = getProjectName();
1011
+ const mainWorktreePath = getGitTopLevel();
1012
+ logger.info(`validate --clean \u6267\u884C\uFF0C\u5206\u652F: ${options.branch}`);
1013
+ if (!isWorkingDirClean(mainWorktreePath)) {
1014
+ gitResetHard(mainWorktreePath);
1015
+ gitCleanForce(mainWorktreePath);
1016
+ }
1017
+ removeSnapshot(projectName, options.branch);
1018
+ printSuccess(MESSAGES.VALIDATE_CLEANED(options.branch));
1019
+ }
1020
+ function handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName) {
1021
+ migrateChangesViaStash(targetWorktreePath, mainWorktreePath, branchName);
1022
+ saveCurrentSnapshotPatch(mainWorktreePath, projectName, branchName);
1023
+ printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
1024
+ }
1025
+ function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName) {
1026
+ const oldPatch = readSnapshot(projectName, branchName);
1027
+ printInfo(MESSAGES.INCREMENTAL_VALIDATE_RESET);
1028
+ gitResetHard(mainWorktreePath);
1029
+ gitCleanForce(mainWorktreePath);
1030
+ migrateChangesViaStash(targetWorktreePath, mainWorktreePath, branchName);
1031
+ const newPatch = saveCurrentSnapshotPatch(mainWorktreePath, projectName, branchName);
1032
+ if (oldPatch.length > 0) {
1033
+ try {
1034
+ gitApplyCachedFromStdin(oldPatch, mainWorktreePath);
1035
+ } catch (error) {
1036
+ logger.warn(`\u589E\u91CF apply \u5931\u8D25: ${error}`);
1037
+ printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
1038
+ printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
1039
+ return;
1040
+ }
1041
+ }
1042
+ printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
1043
+ }
1044
+ async function handleValidate(options) {
1045
+ if (options.clean) {
1046
+ handleValidateClean(options);
1047
+ return;
1048
+ }
1049
+ validateMainWorktree();
1050
+ const projectName = getProjectName();
1051
+ const mainWorktreePath = getGitTopLevel();
1052
+ const projectDir = getProjectWorktreeDir();
1053
+ const targetWorktreePath = join5(projectDir, options.branch);
1054
+ logger.info(`validate \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}`);
1055
+ if (!existsSync6(targetWorktreePath)) {
1056
+ throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
1057
+ }
1058
+ const isIncremental = hasSnapshot(projectName, options.branch);
1059
+ if (isIncremental) {
1060
+ if (isWorkingDirClean(targetWorktreePath)) {
1061
+ printInfo(MESSAGES.TARGET_WORKTREE_CLEAN);
1062
+ return;
1063
+ }
1064
+ handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, options.branch);
1065
+ } else {
1066
+ if (!isWorkingDirClean(mainWorktreePath)) {
1067
+ await handleDirtyMainWorktree(mainWorktreePath);
1068
+ }
1069
+ if (isWorkingDirClean(targetWorktreePath)) {
1070
+ printInfo(MESSAGES.TARGET_WORKTREE_CLEAN);
1071
+ return;
1072
+ }
1073
+ handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, options.branch);
1074
+ }
931
1075
  }
932
1076
 
933
1077
  // src/commands/merge.ts
934
- import { join as join5 } from "path";
935
- import { existsSync as existsSync6 } from "fs";
1078
+ import { join as join6 } from "path";
1079
+ import { existsSync as existsSync7 } from "fs";
936
1080
  function registerMergeCommand(program2) {
937
1081
  program2.command("merge").description("\u5408\u5E76\u67D0\u4E2A\u5DF2\u9A8C\u8BC1\u7684 worktree \u5206\u652F\u5230\u4E3B worktree").requiredOption("-b, --branch <branchName>", "\u8981\u5408\u5E76\u7684\u5206\u652F\u540D").option("-m, --message <message>", "\u63D0\u4EA4\u4FE1\u606F\uFF08\u5DE5\u4F5C\u533A\u6709\u4FEE\u6539\u65F6\u5FC5\u586B\uFF09").action(async (options) => {
938
1082
  await handleMerge(options);
@@ -954,12 +1098,16 @@ async function handleMerge(options) {
954
1098
  validateMainWorktree();
955
1099
  const mainWorktreePath = getGitTopLevel();
956
1100
  const projectDir = getProjectWorktreeDir();
957
- const targetWorktreePath = join5(projectDir, options.branch);
1101
+ const targetWorktreePath = join6(projectDir, options.branch);
958
1102
  logger.info(`merge \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}\uFF0C\u63D0\u4EA4\u4FE1\u606F: ${options.message ?? "(\u672A\u63D0\u4F9B)"}`);
959
- if (!existsSync6(targetWorktreePath)) {
1103
+ if (!existsSync7(targetWorktreePath)) {
960
1104
  throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
961
1105
  }
1106
+ const projectName = getProjectName();
962
1107
  if (!isWorkingDirClean(mainWorktreePath)) {
1108
+ if (hasSnapshot(projectName, options.branch)) {
1109
+ printWarning(MESSAGES.MERGE_VALIDATE_STATE_HINT(options.branch));
1110
+ }
963
1111
  throw new ClawtError(MESSAGES.MAIN_WORKTREE_DIRTY);
964
1112
  }
965
1113
  const shouldCleanup = await shouldCleanupAfterMerge(options.branch);
@@ -1000,6 +1148,9 @@ async function handleMerge(options) {
1000
1148
  if (shouldCleanup) {
1001
1149
  cleanupWorktreeAndBranch(targetWorktreePath, options.branch);
1002
1150
  }
1151
+ if (hasSnapshot(projectName, options.branch)) {
1152
+ removeSnapshot(projectName, options.branch);
1153
+ }
1003
1154
  }
1004
1155
 
1005
1156
  // src/commands/config.ts
@@ -1046,6 +1197,7 @@ registerListCommand(program);
1046
1197
  registerCreateCommand(program);
1047
1198
  registerRemoveCommand(program);
1048
1199
  registerRunCommand(program);
1200
+ registerResumeCommand(program);
1049
1201
  registerValidateCommand(program);
1050
1202
  registerMergeCommand(program);
1051
1203
  registerConfigCommand(program);
@@ -10,6 +10,7 @@ var CLAWT_HOME = join(homedir(), ".clawt");
10
10
  var CONFIG_PATH = join(CLAWT_HOME, "config.json");
11
11
  var LOGS_DIR = join(CLAWT_HOME, "logs");
12
12
  var WORKTREES_DIR = join(CLAWT_HOME, "worktrees");
13
+ var VALIDATE_SNAPSHOTS_DIR = join(CLAWT_HOME, "validate-snapshots");
13
14
 
14
15
  // src/constants/config.ts
15
16
  var CONFIG_DEFINITIONS = {