clawt 2.0.0 → 2.2.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,13 +60,14 @@ 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
 
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
 
@@ -62,7 +62,7 @@ run 命令有两种模式:
62
62
  - 快照存储路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.patch`(patch 文件)+ `<branchName>.head`(主分支 HEAD hash)
63
63
  - 变更检测:同时检测目标 worktree 的未提交修改和已提交 commit,两者均无则提示无需验证
64
64
  - 未提交修改处理:有未提交修改时先做临时 commit,diff 完成后通过 `git reset --soft` 撤销恢复原状
65
- - `merge`:检测目标 worktree 状态(有修改则需 `-m` 提交,已提交则跳过,无变更则报错)→ 合并到主 worktree → pull → push → 可选清理 worktree 和分支(受 `autoDeleteBranch` 配置或交互式确认控制)→ 清理对应的 validate 快照
65
+ - `merge`:检测目标 worktree 状态(有修改则需 `-m` 提交,已提交则跳过,无变更则报错)→ **squash 检测**(检查目标分支是否存在 `AUTO_SAVE_COMMIT_MESSAGE` 前缀的 auto-save commit,如有则提示用户是否压缩所有提交:用户确认后通过 `gitMergeBase` 计算分叉点、`gitResetSoftTo` 将所有 commit reset 到暂存区;有 `-m` 则直接提交继续流程,无 `-m` 则提示用户自行提交后退出)→ 合并到主 worktree → pull → push → 可选清理 worktree 和分支(受 `autoDeleteBranch` 配置或交互式确认控制)→ 清理对应的 validate 快照
66
66
  - `run` 中断清理:Ctrl+C 终止所有子进程后,根据 `autoDeleteBranch` 配置自动清理或交互式确认清理本次创建的 worktree 和分支
67
67
 
68
68
  ### sync 命令流程
@@ -75,11 +75,18 @@ run 命令有两种模式:
75
75
  6. 冲突处理:有冲突时提示用户手动解决,无冲突则输出成功
76
76
  7. 合并成功后清除该分支的 validate 快照(代码基础已变化,旧快照无效)
77
77
 
78
+ ### reset 命令流程
79
+
80
+ 1. `validateMainWorktree()` 确认在主 worktree 根目录
81
+ 2. 检测主 worktree 工作区和暂存区是否干净(`isWorkingDirClean()`)
82
+ 3. 不干净 → `gitResetHard()` + `gitCleanForce()` 重置工作区和暂存区(保留 validate 快照)
83
+ 4. 已干净 → 提示无需重置
84
+
78
85
  ### 目录层级
79
86
 
80
87
  - `src/commands/` — 各命令的注册与处理逻辑
81
- - `src/utils/` — 工具函数(git 操作(含三点 diff、分支合并、冲突检测等)、shell 执行与子进程管理、分支名处理、worktree 管理与批量清理、配置、格式化输出、交互式输入、Claude Code 交互式启动、validate 快照管理(含 HEAD hash 一致性校验))
82
- - `src/constants/` — 常量定义(路径、退出码、消息模板、分支规则、配置默认值、终端控制序列、validate 快照目录、sync 相关消息)
88
+ - `src/utils/` — 工具函数(git 操作(含三点 diff、分支合并、冲突检测、merge-base 计算、commit message 检测、soft reset 到指定 commit 等)、shell 执行与子进程管理、分支名处理、worktree 管理与批量清理、配置、格式化输出、交互式输入、Claude Code 交互式启动、validate 快照管理(含 HEAD hash 一致性校验))
89
+ - `src/constants/` — 常量定义(路径、退出码、消息模板、分支规则、配置默认值、终端控制序列、validate 快照目录、sync 相关消息、git 常量(如 `AUTO_SAVE_COMMIT_MESSAGE`)、squash 相关消息、reset 相关消息)
83
90
  - `src/types/` — TypeScript 类型定义
84
91
  - `src/errors/` — 自定义 `ClawtError` 错误类(携带退出码)
85
92
  - `src/logger/` — winston 日志(按日期滚动,写入 `~/.clawt/logs/`)
package/README.md CHANGED
@@ -148,6 +148,8 @@ clawt merge -b <branchName> [-m <commitMessage>]
148
148
 
149
149
  将目标 worktree 的变更合并到主 worktree 的当前分支,并推送到远程仓库。如果目标 worktree 工作区有未提交的修改,需要通过 `-m` 提供提交信息;如果目标 worktree 已经提交过(工作区干净但有本地提交),可以省略 `-m` 直接合并。merge 成功后会询问是否清理对应的 worktree 和分支(如果配置了 `autoDeleteBranch: true` 则自动清理)。
150
150
 
151
+ 如果检测到目标分支存在 `clawt sync` 产生的临时提交(auto-save commit),会自动提示是否将所有提交压缩(squash)为一个。用户选择压缩后,所有 commit 会被 reset 到暂存区:如果提供了 `-m` 则直接提交并继续合并流程;如果未提供 `-m` 则提示用户前往目标 worktree 自行提交后重新执行 merge。
152
+
151
153
  ```bash
152
154
  # 目标 worktree 有未提交修改,需提供 -m
153
155
  clawt merge -b feature-scheme-1 -m "feat: 实现用户登录功能"
@@ -185,6 +187,19 @@ clawt list
185
187
 
186
188
  列出当前项目在 `~/.clawt/worktrees/` 下的所有 worktree 及对应分支。
187
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
+
188
203
  ### `clawt config` — 查看全局配置
189
204
 
190
205
  ```bash
package/dist/index.js CHANGED
@@ -102,7 +102,20 @@ var MESSAGES = {
102
102
  \u89E3\u51B3\u51B2\u7A81\u540E\u6267\u884C git add . && git merge --continue`,
103
103
  /** validate patch apply 失败,提示用户同步主分支 */
104
104
  VALIDATE_PATCH_APPLY_FAILED: (branch) => `\u53D8\u66F4\u8FC1\u79FB\u5931\u8D25\uFF1A\u76EE\u6807\u5206\u652F\u4E0E\u4E3B\u5206\u652F\u5DEE\u5F02\u8FC7\u5927
105
- \u8BF7\u5148\u6267\u884C clawt sync -b ${branch} \u540C\u6B65\u4E3B\u5206\u652F\u540E\u91CD\u8BD5`
105
+ \u8BF7\u5148\u6267\u884C clawt sync -b ${branch} \u540C\u6B65\u4E3B\u5206\u652F\u540E\u91CD\u8BD5`,
106
+ /** merge 检测到 auto-save 提交,提示用户是否压缩 */
107
+ MERGE_SQUASH_PROMPT: "\u68C0\u6D4B\u5230 sync \u4EA7\u751F\u7684\u4E34\u65F6\u63D0\u4EA4\uFF0C\u662F\u5426\u5C06\u6240\u6709\u63D0\u4EA4\u538B\u7F29\u4E3A\u4E00\u4E2A\uFF1F\n \u538B\u7F29\u540E\u53D8\u66F4\u5C06\u4FDD\u7559\u5728\u76EE\u6807worktree\u7684\u6682\u5B58\u533A\uFF0C\u9700\u8981\u91CD\u65B0\u63D0\u4EA4\uFF08\u53EF\u4F7F\u7528 Claude Code Cli\u6216\u5176\u4ED6\u5DE5\u5177\u751F\u6210\u63D0\u4EA4\u4FE1\u606F\uFF09",
108
+ /** squash 完成且通过 -m 直接提交后的提示 */
109
+ MERGE_SQUASH_COMMITTED: (branch) => `\u2713 \u5DF2\u5C06\u5206\u652F ${branch} \u7684\u6240\u6709\u63D0\u4EA4\u538B\u7F29\u4E3A\u4E00\u4E2A`,
110
+ /** squash 完成但未提供 -m,提示用户自行提交 */
111
+ MERGE_SQUASH_PENDING: (worktreePath, branch) => `\u2713 \u5DF2\u5C06\u6240\u6709\u63D0\u4EA4\u538B\u7F29\u5230\u6682\u5B58\u533A
112
+ \u8BF7\u5728\u76EE\u6807 worktree \u4E2D\u63D0\u4EA4\u540E\u91CD\u65B0\u6267\u884C merge\uFF1A
113
+ cd ${worktreePath}
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"
106
119
  };
107
120
 
108
121
  // src/constants/exitCodes.ts
@@ -146,6 +159,9 @@ function deriveConfigDescriptions(definitions) {
146
159
  var DEFAULT_CONFIG = deriveDefaultConfig(CONFIG_DEFINITIONS);
147
160
  var CONFIG_DESCRIPTIONS = deriveConfigDescriptions(CONFIG_DEFINITIONS);
148
161
 
162
+ // src/constants/git.ts
163
+ var AUTO_SAVE_COMMIT_MESSAGE = "chore: auto-save before sync";
164
+
149
165
  // src/errors/index.ts
150
166
  var ClawtError = class extends Error {
151
167
  /** 退出码 */
@@ -358,6 +374,21 @@ function gitApplyFromStdin(patchContent, cwd) {
358
374
  function gitResetSoft(count = 1, cwd) {
359
375
  execCommand(`git reset --soft HEAD~${count}`, { cwd });
360
376
  }
377
+ function gitMergeBase(branchA, branchB, cwd) {
378
+ return execCommand(`git merge-base ${branchA} ${branchB}`, { cwd });
379
+ }
380
+ function hasCommitWithMessage(branchName, messagePrefix, cwd) {
381
+ try {
382
+ const output = execCommand(`git log HEAD..${branchName} --format=%s`, { cwd });
383
+ if (!output.trim()) return false;
384
+ return output.trim().split("\n").some((msg) => msg.startsWith(messagePrefix));
385
+ } catch {
386
+ return false;
387
+ }
388
+ }
389
+ function gitResetSoftTo(commitHash, cwd) {
390
+ execCommand(`git reset --soft ${commitHash}`, { cwd });
391
+ }
361
392
 
362
393
  // src/utils/formatter.ts
363
394
  import chalk from "chalk";
@@ -1121,6 +1152,26 @@ function registerMergeCommand(program2) {
1121
1152
  await handleMerge(options);
1122
1153
  });
1123
1154
  }
1155
+ async function handleSquashIfNeeded(targetWorktreePath, mainWorktreePath, branchName, commitMessage) {
1156
+ if (!hasCommitWithMessage(branchName, AUTO_SAVE_COMMIT_MESSAGE, mainWorktreePath)) {
1157
+ return false;
1158
+ }
1159
+ const shouldSquash = await confirmAction(MESSAGES.MERGE_SQUASH_PROMPT);
1160
+ if (!shouldSquash) {
1161
+ return false;
1162
+ }
1163
+ const mainBranch = getCurrentBranch(mainWorktreePath);
1164
+ const mergeBase = gitMergeBase(mainBranch, branchName, mainWorktreePath);
1165
+ logger.info(`squash: merge-base = ${mergeBase}, \u5206\u652F = ${branchName}`);
1166
+ gitResetSoftTo(mergeBase, targetWorktreePath);
1167
+ if (commitMessage) {
1168
+ gitCommit(commitMessage, targetWorktreePath);
1169
+ printSuccess(MESSAGES.MERGE_SQUASH_COMMITTED(branchName));
1170
+ return false;
1171
+ }
1172
+ printInfo(MESSAGES.MERGE_SQUASH_PENDING(targetWorktreePath, branchName));
1173
+ return true;
1174
+ }
1124
1175
  async function shouldCleanupAfterMerge(branchName) {
1125
1176
  const autoDelete = getConfigValue("autoDeleteBranch");
1126
1177
  if (autoDelete) {
@@ -1149,6 +1200,10 @@ async function handleMerge(options) {
1149
1200
  }
1150
1201
  throw new ClawtError(MESSAGES.MAIN_WORKTREE_DIRTY);
1151
1202
  }
1203
+ const shouldExit = await handleSquashIfNeeded(targetWorktreePath, mainWorktreePath, options.branch, options.message);
1204
+ if (shouldExit) {
1205
+ return;
1206
+ }
1152
1207
  const shouldCleanup = await shouldCleanupAfterMerge(options.branch);
1153
1208
  const targetClean = isWorkingDirClean(targetWorktreePath);
1154
1209
  if (!targetClean) {
@@ -1235,7 +1290,7 @@ function registerSyncCommand(program2) {
1235
1290
  }
1236
1291
  function autoSaveChanges(worktreePath, branch) {
1237
1292
  gitAddAll(worktreePath);
1238
- gitCommit("chore: auto-save before sync", worktreePath);
1293
+ gitCommit(AUTO_SAVE_COMMIT_MESSAGE, worktreePath);
1239
1294
  printInfo(MESSAGES.SYNC_AUTO_COMMITTED(branch));
1240
1295
  logger.info(`\u5DF2\u81EA\u52A8\u4FDD\u5B58 ${branch} \u5206\u652F\u7684\u672A\u63D0\u4EA4\u53D8\u66F4`);
1241
1296
  }
@@ -1278,6 +1333,25 @@ async function handleSync(options) {
1278
1333
  printSuccess(MESSAGES.SYNC_SUCCESS(branch, mainBranch));
1279
1334
  }
1280
1335
 
1336
+ // src/commands/reset.ts
1337
+ function registerResetCommand(program2) {
1338
+ program2.command("reset").description("\u91CD\u7F6E\u4E3B worktree \u5DE5\u4F5C\u533A\u548C\u6682\u5B58\u533A\uFF08\u4FDD\u7559 validate \u5FEB\u7167\uFF09").action(() => {
1339
+ handleReset();
1340
+ });
1341
+ }
1342
+ function handleReset() {
1343
+ validateMainWorktree();
1344
+ const mainWorktreePath = getGitTopLevel();
1345
+ logger.info("reset \u547D\u4EE4\u6267\u884C");
1346
+ if (!isWorkingDirClean(mainWorktreePath)) {
1347
+ gitResetHard(mainWorktreePath);
1348
+ gitCleanForce(mainWorktreePath);
1349
+ printSuccess(MESSAGES.RESET_SUCCESS);
1350
+ } else {
1351
+ printInfo(MESSAGES.RESET_ALREADY_CLEAN);
1352
+ }
1353
+ }
1354
+
1281
1355
  // src/index.ts
1282
1356
  var require2 = createRequire(import.meta.url);
1283
1357
  var { version } = require2("../package.json");
@@ -1293,6 +1367,7 @@ registerValidateCommand(program);
1293
1367
  registerMergeCommand(program);
1294
1368
  registerConfigCommand(program);
1295
1369
  registerSyncCommand(program);
1370
+ registerResetCommand(program);
1296
1371
  process.on("uncaughtException", (error) => {
1297
1372
  if (error instanceof ClawtError) {
1298
1373
  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
 
@@ -168,6 +169,7 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
168
169
  | `clawt config` | 查看全局配置 | 5.10 |
169
170
  | `clawt resume` | 在已有 worktree 中恢复 Claude Code 交互式会话 | 5.11 |
170
171
  | `clawt sync` | 将主分支最新代码同步到目标 worktree | 5.12 |
172
+ | `clawt reset` | 重置主 worktree 工作区和暂存区 | 5.13 |
171
173
 
172
174
  所有命令执行前,都必须先执行**主 worktree 校验**(见 [2.1](#21-主-worktree-的定义与定位规则))。
173
175
 
@@ -589,7 +591,22 @@ clawt merge -b <branchName> [-m <commitMessage>]
589
591
  - 如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.patch`),额外输出警告提示用户可先执行 `clawt validate -b <branchName> --clean` 清理
590
592
  - 提示 `主 worktree 有未提交的更改,请先处理`,退出
591
593
  - 无更改 → 继续
592
- 3. **根据目标 worktree 状态决定是否需要提交**
594
+ 3. **Squash 检测与执行(auto-save 临时提交压缩)**
595
+ - 通过 `git log HEAD..<branchName> --format=%s` 检查目标分支是否存在以 `AUTO_SAVE_COMMIT_MESSAGE`(`chore: auto-save before sync`)为前缀的 commit
596
+ - **不存在** → 跳过,进入步骤 4
597
+ - **存在** → 提示用户是否将所有提交压缩为一个:
598
+ ```
599
+ 检测到 sync 产生的临时提交,是否将所有提交压缩为一个?
600
+ 压缩后变更将保留在目标worktree的暂存区,需要重新提交
601
+ ```
602
+ - **用户选择不压缩** → 跳过,进入步骤 4
603
+ - **用户选择压缩** →
604
+ 1. 获取主分支名(`git rev-parse --abbrev-ref HEAD`)
605
+ 2. 计算分叉点:`git merge-base <mainBranch> <branchName>`
606
+ 3. 在目标 worktree 中执行 `git reset --soft <merge-base>`,将所有 commit 撤销到暂存区
607
+ 4. 如果用户提供了 `-m` → 直接在目标 worktree 执行 `git commit -m '<commitMessage>'`,输出成功提示,继续步骤 4
608
+ 5. 如果用户未提供 `-m` → 提示用户前往目标 worktree 自行提交后重新执行 `clawt merge`,**退出流程**
609
+ 4. **根据目标 worktree 状态决定是否需要提交**
593
610
  - 检测目标 worktree 工作区是否干净(`git status --porcelain`)
594
611
  - **工作区有未提交修改**:
595
612
  - 如果用户未提供 `-m`,提示 `目标 worktree 有未提交的修改,请通过 -m 参数提供提交信息`,退出
@@ -603,21 +620,21 @@ clawt merge -b <branchName> [-m <commitMessage>]
603
620
  - 检查目标分支相对于主分支是否有本地提交(`git log HEAD..<branchName> --oneline`)
604
621
  - 有本地提交 → 跳过提交步骤,直接进入合并
605
622
  - 无本地提交 → 提示 `目标 worktree 没有任何可合并的变更(工作区干净且无本地提交)`,退出
606
- 4. **回到主 worktree 进行合并**
623
+ 5. **回到主 worktree 进行合并**
607
624
  ```bash
608
625
  cd <主 worktree 路径>
609
626
  git merge <branchName>
610
627
  ```
611
- 5. **冲突检测**
628
+ 6. **冲突检测**
612
629
  - 检查 merge 退出码及 `git status` 是否存在冲突
613
630
  - **有冲突** → 提示 `合并存在冲突,请手动处理`,退出
614
631
  - **无冲突** → 继续
615
- 6. **推送**
632
+ 7. **推送**
616
633
  ```bash
617
634
  git pull
618
635
  git push
619
636
  ```
620
- 7. **输出成功提示**
637
+ 8. **输出成功提示**
621
638
 
622
639
  ```
623
640
  # 提供了 -m 时
@@ -630,7 +647,7 @@ clawt merge -b <branchName> [-m <commitMessage>]
630
647
  已推送到远程仓库
631
648
  ```
632
649
 
633
- 8. **merge 成功后清理 worktree 和分支(可选)**
650
+ 9. **merge 成功后清理 worktree 和分支(可选)**
634
651
  - 如果配置文件中 `autoDeleteBranch` 为 `true`,自动执行清理
635
652
  - 否则交互式询问用户是否清理
636
653
  - 用户确认后,依次执行:
@@ -645,8 +662,8 @@ clawt merge -b <branchName> [-m <commitMessage>]
645
662
  ```
646
663
  - 输出清理成功提示:`✓ 已清理 worktree 和分支: <branchName>`
647
664
 
648
- 9. **清理 validate 快照**
649
- - merge 成功后,如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.patch`),自动删除该快照文件(merge 成功后快照已无意义)
665
+ 10. **清理 validate 快照**
666
+ - merge 成功后,如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.patch`),自动删除该快照文件(merge 成功后快照已无意义)
650
667
 
651
668
  > **注意:** 清理确认在 merge 操作之前询问(避免 merge 成功后因交互中断而遗留未清理的 worktree),但清理操作在 merge 成功后才执行。
652
669
 
@@ -851,7 +868,7 @@ clawt sync -b <branchName>
851
868
  - 不存在 → 报错退出
852
869
  3. **获取主分支名**:通过 `git rev-parse --abbrev-ref HEAD` 获取主 worktree 当前分支名(不硬编码 main/master)
853
870
  4. **自动保存未提交变更**:检查目标 worktree 是否有未提交修改
854
- - 有修改 → 自动执行 `git add . && git commit -m "chore: auto-save before sync"` 保存变更
871
+ - 有修改 → 自动执行 `git add . && git commit -m "<AUTO_SAVE_COMMIT_MESSAGE>"` 保存变更(commit message 由常量 `AUTO_SAVE_COMMIT_MESSAGE` 定义,值为 `chore: auto-save before sync`,同时用于 merge 命令的 squash 检测)
855
872
  - 无修改 → 跳过
856
873
  5. **在目标 worktree 中合并主分支**:
857
874
  ```bash
@@ -874,6 +891,38 @@ clawt sync -b <branchName>
874
891
 
875
892
  ---
876
893
 
894
+ ### 5.13 重置主 Worktree 工作区和暂存区
895
+
896
+ **命令:**
897
+
898
+ ```bash
899
+ clawt reset
900
+ ```
901
+
902
+ **无参数。**
903
+
904
+ **使用场景:**
905
+
906
+ 当用户通过 `clawt validate` 将分支变更迁移到主 worktree 后,希望快速清除工作区和暂存区的所有修改,恢复到干净状态。与 `clawt validate --clean` 的区别在于:`reset` 仅重置工作区和暂存区,**不删除** validate 快照文件,适用于只想清空变更而保留快照以便后续增量 validate 的场景。
907
+
908
+ **运行流程:**
909
+
910
+ 1. **主 worktree 校验** (2.1)
911
+ 2. **检测工作区状态**:通过 `git status --porcelain` 检测主 worktree 是否有未提交的更改
912
+ - **工作区干净** → 输出提示 `主 worktree 工作区和暂存区已是干净状态,无需重置`,退出
913
+ - **工作区不干净** → 继续
914
+ 3. **重置工作区和暂存区**:
915
+ ```bash
916
+ git reset --hard
917
+ git clean -f
918
+ ```
919
+ 4. **输出成功提示**:
920
+ ```
921
+ ✓ 主 worktree 工作区和暂存区已重置
922
+ ```
923
+
924
+ ---
925
+
877
926
  ## 6. 错误处理规范
878
927
 
879
928
  ### 6.1 通用错误处理
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3,7 +3,7 @@ import { join } from 'node:path';
3
3
  import { existsSync } from 'node:fs';
4
4
  import { logger } from '../logger/index.js';
5
5
  import { ClawtError } from '../errors/index.js';
6
- import { MESSAGES } from '../constants/index.js';
6
+ import { MESSAGES, AUTO_SAVE_COMMIT_MESSAGE } from '../constants/index.js';
7
7
  import type { MergeOptions } from '../types/index.js';
8
8
  import {
9
9
  validateMainWorktree,
@@ -26,6 +26,10 @@ import {
26
26
  getConfigValue,
27
27
  confirmAction,
28
28
  cleanupWorktrees,
29
+ hasCommitWithMessage,
30
+ gitMergeBase,
31
+ gitResetSoftTo,
32
+ getCurrentBranch,
29
33
  } from '../utils/index.js';
30
34
 
31
35
  /**
@@ -43,6 +47,52 @@ export function registerMergeCommand(program: Command): void {
43
47
  });
44
48
  }
45
49
 
50
+ /**
51
+ * 检测并处理目标分支的 auto-save 提交压缩
52
+ * 如果检测到 sync 产生的临时提交,提示用户是否将所有提交压缩为一个
53
+ * @param {string} targetWorktreePath - 目标 worktree 路径
54
+ * @param {string} mainWorktreePath - 主 worktree 路径
55
+ * @param {string} branchName - 分支名
56
+ * @param {string} [commitMessage] - 用户提供的提交信息
57
+ * @returns {Promise<boolean>} 是否需要退出 merge 流程(用户选择 squash 但未提供 -m 时返回 true)
58
+ */
59
+ async function handleSquashIfNeeded(
60
+ targetWorktreePath: string,
61
+ mainWorktreePath: string,
62
+ branchName: string,
63
+ commitMessage?: string,
64
+ ): Promise<boolean> {
65
+ // 检查目标分支是否存在 auto-save commit
66
+ if (!hasCommitWithMessage(branchName, AUTO_SAVE_COMMIT_MESSAGE, mainWorktreePath)) {
67
+ return false;
68
+ }
69
+
70
+ // 提示用户是否压缩
71
+ const shouldSquash = await confirmAction(MESSAGES.MERGE_SQUASH_PROMPT);
72
+ if (!shouldSquash) {
73
+ return false;
74
+ }
75
+
76
+ // 获取当前主分支名并计算分叉点
77
+ const mainBranch = getCurrentBranch(mainWorktreePath);
78
+ const mergeBase = gitMergeBase(mainBranch, branchName, mainWorktreePath);
79
+ logger.info(`squash: merge-base = ${mergeBase}, 分支 = ${branchName}`);
80
+
81
+ // 在目标 worktree 中执行 reset --soft 到分叉点
82
+ gitResetSoftTo(mergeBase, targetWorktreePath);
83
+
84
+ if (commitMessage) {
85
+ // 有 -m 参数,直接提交
86
+ gitCommit(commitMessage, targetWorktreePath);
87
+ printSuccess(MESSAGES.MERGE_SQUASH_COMMITTED(branchName));
88
+ return false;
89
+ }
90
+
91
+ // 没有 -m 参数,提示用户自行提交
92
+ printInfo(MESSAGES.MERGE_SQUASH_PENDING(targetWorktreePath, branchName));
93
+ return true;
94
+ }
95
+
46
96
  /**
47
97
  * 判断 merge 成功后是否需要清理 worktree 和分支
48
98
  * 如果全局配置了 autoDeleteBranch 则直接返回 true,否则询问用户
@@ -97,6 +147,12 @@ async function handleMerge(options: MergeOptions): Promise<void> {
97
147
  throw new ClawtError(MESSAGES.MAIN_WORKTREE_DIRTY);
98
148
  }
99
149
 
150
+ // 步骤 3.5:检测是否需要 squash(sync 临时提交压缩)
151
+ const shouldExit = await handleSquashIfNeeded(targetWorktreePath, mainWorktreePath, options.branch, options.message);
152
+ if (shouldExit) {
153
+ return;
154
+ }
155
+
100
156
  // merge 前确认是否清理 worktree 和分支
101
157
  const shouldCleanup = await shouldCleanupAfterMerge(options.branch);
102
158
 
@@ -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
+ }
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import type { Command } from 'commander';
3
3
  import { logger } from '../logger/index.js';
4
4
  import { ClawtError } from '../errors/index.js';
5
- import { MESSAGES } from '../constants/index.js';
5
+ import { MESSAGES, AUTO_SAVE_COMMIT_MESSAGE } from '../constants/index.js';
6
6
  import type { SyncOptions } from '../types/index.js';
7
7
  import {
8
8
  validateMainWorktree,
@@ -43,7 +43,7 @@ export function registerSyncCommand(program: Command): void {
43
43
  */
44
44
  function autoSaveChanges(worktreePath: string, branch: string): void {
45
45
  gitAddAll(worktreePath);
46
- gitCommit('chore: auto-save before sync', worktreePath);
46
+ gitCommit(AUTO_SAVE_COMMIT_MESSAGE, worktreePath);
47
47
  printInfo(MESSAGES.SYNC_AUTO_COMMITTED(branch));
48
48
  logger.info(`已自动保存 ${branch} 分支的未提交变更`);
49
49
  }
@@ -0,0 +1,2 @@
1
+ /** sync 自动保存的 commit message 前缀,用于检测 auto-save 提交 */
2
+ export const AUTO_SAVE_COMMIT_MESSAGE = 'chore: auto-save before sync';
@@ -4,3 +4,4 @@ export { MESSAGES } from './messages.js';
4
4
  export { EXIT_CODES } from './exitCodes.js';
5
5
  export { ENABLE_BRACKETED_PASTE, DISABLE_BRACKETED_PASTE, PASTE_THRESHOLD_MS } from './terminal.js';
6
6
  export { DEFAULT_CONFIG, CONFIG_DESCRIPTIONS, APPEND_SYSTEM_PROMPT } from './config.js';
7
+ export { AUTO_SAVE_COMMIT_MESSAGE } from './git.js';
@@ -87,4 +87,16 @@ export const MESSAGES = {
87
87
  /** validate patch apply 失败,提示用户同步主分支 */
88
88
  VALIDATE_PATCH_APPLY_FAILED: (branch: string) =>
89
89
  `变更迁移失败:目标分支与主分支差异过大\n 请先执行 clawt sync -b ${branch} 同步主分支后重试`,
90
+ /** merge 检测到 auto-save 提交,提示用户是否压缩 */
91
+ MERGE_SQUASH_PROMPT: '检测到 sync 产生的临时提交,是否将所有提交压缩为一个?\n 压缩后变更将保留在目标worktree的暂存区,需要重新提交(可使用 Claude Code Cli或其他工具生成提交信息)',
92
+ /** squash 完成且通过 -m 直接提交后的提示 */
93
+ MERGE_SQUASH_COMMITTED: (branch: string) =>
94
+ `✓ 已将分支 ${branch} 的所有提交压缩为一个`,
95
+ /** squash 完成但未提供 -m,提示用户自行提交 */
96
+ MERGE_SQUASH_PENDING: (worktreePath: string, branch: string) =>
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 工作区和暂存区已是干净状态,无需重置',
90
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
@@ -369,3 +369,41 @@ export function gitApplyFromStdin(patchContent: Buffer, cwd?: string): void {
369
369
  export function gitResetSoft(count: number = 1, cwd?: string): void {
370
370
  execCommand(`git reset --soft HEAD~${count}`, { cwd });
371
371
  }
372
+
373
+ /**
374
+ * 获取两个分支的分叉点(merge-base)
375
+ * @param {string} branchA - 分支 A
376
+ * @param {string} branchB - 分支 B
377
+ * @param {string} [cwd] - 工作目录
378
+ * @returns {string} merge-base 的 commit hash
379
+ */
380
+ export function gitMergeBase(branchA: string, branchB: string, cwd?: string): string {
381
+ return execCommand(`git merge-base ${branchA} ${branchB}`, { cwd });
382
+ }
383
+
384
+ /**
385
+ * 检查目标分支相对于当前分支是否存在指定前缀的 commit message
386
+ * 通过 git log HEAD..<branch> 遍历所有新增 commit 的 message
387
+ * @param {string} branchName - 目标分支名
388
+ * @param {string} messagePrefix - 要匹配的 commit message 前缀
389
+ * @param {string} [cwd] - 工作目录
390
+ * @returns {boolean} 是否存在匹配的 commit
391
+ */
392
+ export function hasCommitWithMessage(branchName: string, messagePrefix: string, cwd?: string): boolean {
393
+ try {
394
+ const output = execCommand(`git log HEAD..${branchName} --format=%s`, { cwd });
395
+ if (!output.trim()) return false;
396
+ return output.trim().split('\n').some((msg) => msg.startsWith(messagePrefix));
397
+ } catch {
398
+ return false;
399
+ }
400
+ }
401
+
402
+ /**
403
+ * git reset --soft <commitHash>,将指定 commit 之后的所有提交撤销到暂存区
404
+ * @param {string} commitHash - 目标 commit hash(reset 到此处)
405
+ * @param {string} [cwd] - 工作目录
406
+ */
407
+ export function gitResetSoftTo(commitHash: string, cwd?: string): void {
408
+ execCommand(`git reset --soft ${commitHash}`, { cwd });
409
+ }
@@ -35,6 +35,9 @@ export {
35
35
  gitDiffBinaryAgainstBranch,
36
36
  gitApplyFromStdin,
37
37
  gitResetSoft,
38
+ gitMergeBase,
39
+ hasCommitWithMessage,
40
+ gitResetSoftTo,
38
41
  } from './git.js';
39
42
  export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
40
43
  export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';