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.
- package/.claude/agent-memory/docs-sync-updater/MEMORY.md +13 -20
- package/.claude/agents/docs-sync-updater.md +5 -7
- package/README.md +1 -1
- package/dist/index.js +61 -9
- package/docs/spec.md +40 -10
- package/package.json +1 -1
- package/src/commands/merge.ts +3 -5
- package/src/commands/validate.ts +37 -7
- package/src/utils/git.ts +40 -0
- package/src/utils/index.ts +4 -1
- package/src/utils/validate-snapshot.ts +40 -5
- package/CLAUDE.md +0 -105
|
@@ -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
|
|
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
|
-
配置项变更时需在以下
|
|
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. `
|
|
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
|
-
-
|
|
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
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
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,
|
|
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
|
|
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. 读取 `
|
|
37
|
-
3.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
484
|
+
将最新全量变更保存为新的 tree 对象(覆盖旧快照),同时记录当前 HEAD commit hash(流程同首次 validate 的步骤 4)。
|
|
483
485
|
|
|
484
|
-
##### 步骤 5
|
|
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
|
|
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
|
|
703
|
+
- merge 成功后,如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.tree` 和 `<branchName>.head`),自动删除这些快照文件(merge 成功后快照已无意义)
|
|
674
704
|
|
|
675
|
-
> **注意:**
|
|
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
package/src/commands/merge.ts
CHANGED
|
@@ -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(
|
|
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
|
|
206
|
+
// 步骤 9:merge 成功后确认并清理 worktree 和分支
|
|
207
|
+
const shouldCleanup = await shouldCleanupAfterMerge(options.branch);
|
|
210
208
|
if (shouldCleanup) {
|
|
211
209
|
cleanupWorktreeAndBranch(targetWorktreePath, options.branch);
|
|
212
210
|
}
|
package/src/commands/validate.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
229
|
-
const oldTreeHash =
|
|
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
|
|
251
|
+
// 步骤 5:将旧变更状态载入暂存区
|
|
245
252
|
try {
|
|
246
|
-
|
|
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
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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()` 自动清理
|