clawt 3.10.3 → 3.10.5
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/README.md +6 -6
- package/README.zh-CN.md +5 -5
- package/dist/index.js +91 -23
- package/dist/postinstall.js +27 -0
- package/docs/superpowers/findings/2026-06-01-sync-validate-diverged-findings.md +203 -0
- package/docs/superpowers/plans/2026-06-01-validate-ignored-files-conflict.md +412 -0
- package/docs/superpowers/specs/2026-06-01-validate-ignored-files-conflict-design.md +76 -0
- package/docs/validate.md +42 -5
- package/package.json +1 -1
- package/src/constants/messages/validate.ts +17 -0
- package/src/utils/git-core.ts +23 -0
- package/src/utils/index.ts +2 -1
- package/src/utils/validate-core.ts +52 -0
- package/tests/unit/utils/git-core.test.ts +43 -0
- package/tests/unit/utils/validate-core.test.ts +60 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/clawt) [](https://github.com/afk101/clawt)
|
|
4
4
|
|
|
5
|
-
**[English](./README.md)** | [中文](
|
|
5
|
+
**[English](./README.md)** | [中文](https://github.com/afk101/clawt/blob/main/README.zh-CN.md)
|
|
6
6
|
|
|
7
7
|
Run multiple Claude Code Agent tasks in parallel based on Git Worktree — all agents' code changes are fully isolated from each other.
|
|
8
8
|
|
|
@@ -19,7 +19,7 @@ Claude Code can independently complete feature development, bug fixes, and refac
|
|
|
19
19
|
|
|
20
20
|
This is how most developers use Claude Code today — **serial execution**:
|
|
21
21
|
|
|
22
|
-

|
|
23
23
|
|
|
24
24
|
**3 tasks total ≈ 49.5 min.** The biggest problem: **while Claude is working on task A, B, C — you're just waiting.**
|
|
25
25
|
|
|
@@ -33,7 +33,7 @@ The core idea is simple:
|
|
|
33
33
|
|
|
34
34
|
Same 3 tasks, with Clawt:
|
|
35
35
|
|
|
36
|
-

|
|
37
37
|
|
|
38
38
|
**3 tasks total ≈ 26.5 min (parallel execution, sequential review)**
|
|
39
39
|
|
|
@@ -60,11 +60,11 @@ Git Worktree is a native Git mechanism (`git worktree add`) that creates multipl
|
|
|
60
60
|
|
|
61
61
|
> **Human's job: think about requirements, write prompts, review code(Optional). AI's job: write code. Git Worktree is the isolation layer between them.**
|
|
62
62
|
|
|
63
|
-

|
|
64
64
|
|
|
65
65
|
> **Clawt does not modify, replace, or wrap Claude Code itself. It only manages "where" and "how many" Claude Code instances run — at a higher level.**
|
|
66
66
|
|
|
67
|
-

|
|
68
68
|
|
|
69
69
|
Everything you use inside each worktree is vanilla Claude Code — same interaction, same commands, same `CLAUDE.md`, same MCP config. Any Claude Code update automatically benefits Clawt with zero adaptation needed.
|
|
70
70
|
|
|
@@ -118,7 +118,7 @@ clawt status -i
|
|
|
118
118
|
| `q` | Quit panel | — |
|
|
119
119
|
|
|
120
120
|
Example:
|
|
121
|
-

|
|
122
122
|
> All operations can also be executed via standalone commands — see "Command Reference" below.
|
|
123
123
|
|
|
124
124
|
## Command Reference
|
package/README.zh-CN.md
CHANGED
|
@@ -19,7 +19,7 @@ Claude Code 可以独立完成需求开发、Bug 修复、代码重构等任务
|
|
|
19
19
|
|
|
20
20
|
这是大多数开发者使用 Claude Code 的现状——**串行执行**:
|
|
21
21
|
|
|
22
|
-

|
|
23
23
|
|
|
24
24
|
**3 个任务总耗时 ≈ 49.5 分钟**。最大的问题是:**Claude 在执行任务的时候,你完全在干等。**
|
|
25
25
|
|
|
@@ -33,7 +33,7 @@ Claude Code 可以独立完成需求开发、Bug 修复、代码重构等任务
|
|
|
33
33
|
|
|
34
34
|
同样的 3 个任务,使用 Clawt 后:
|
|
35
35
|
|
|
36
|
-

|
|
37
37
|
|
|
38
38
|
**3 个任务总耗时 ≈ 26.5 分钟(并行执行,串行验证)**
|
|
39
39
|
|
|
@@ -62,11 +62,11 @@ Git Worktree 是 Git 原生支持的机制(`git worktree add`),它允许
|
|
|
62
62
|
|
|
63
63
|
> **人做的事情:思考需求、写 Prompt、审查代码(可选)。AI 做的事情:写代码。用 Git Worktree 作为两者之间的隔离层。**
|
|
64
64
|
|
|
65
|
-

|
|
66
66
|
|
|
67
67
|
> **Clawt 不修改、不替换、不包装 Claude Code 本身。它只是在更高一层管理"在哪里"以及"同时启动多少个" Claude Code。**
|
|
68
68
|
|
|
69
|
-

|
|
70
70
|
|
|
71
71
|
你在每个 Worktree 里使用的,就是原汁原味的 Claude Code——同样的交互方式、同样的命令、同样的 CLAUDE.md、同样的 MCP 配置。Claude Code 的任何更新,Clawt 自动受益,无需适配。
|
|
72
72
|
|
|
@@ -120,7 +120,7 @@ clawt status -i
|
|
|
120
120
|
| `q` | 退出面板 | — |
|
|
121
121
|
|
|
122
122
|
示例:
|
|
123
|
-

|
|
124
124
|
> 所有操作也可通过独立命令执行,详见下方「命令一览」。
|
|
125
125
|
|
|
126
126
|
## 命令一览
|
package/dist/index.js
CHANGED
|
@@ -819,6 +819,33 @@ var VALIDATE_MESSAGES_I18N = {
|
|
|
819
819
|
"zh-CN": (branch) => `\u53D8\u66F4\u8FC1\u79FB\u5931\u8D25\uFF1A\u76EE\u6807\u5206\u652F\u4E0E\u4E3B\u5206\u652F\u5DEE\u5F02\u8FC7\u5927
|
|
820
820
|
\u8BF7\u5148\u6267\u884C clawt sync -b ${branch} \u540C\u6B65\u4E3B\u5206\u652F\u540E\u91CD\u8BD5`
|
|
821
821
|
},
|
|
822
|
+
/** validate 检测到被 .gitignore 忽略的残留文件冲突 */
|
|
823
|
+
VALIDATE_IGNORED_FILES_CONFLICT: {
|
|
824
|
+
en: (files, cleanCommands) => {
|
|
825
|
+
const maxDisplay = 10;
|
|
826
|
+
const displayed = files.slice(0, maxDisplay).map((f) => ` - ${f}`).join("\n");
|
|
827
|
+
const more = files.length > maxDisplay ? `
|
|
828
|
+
...(${files.length} files total)` : "";
|
|
829
|
+
const cmds = cleanCommands.map((c) => ` ${c}`).join("\n");
|
|
830
|
+
return `Ignored files left in main worktree are blocking patch apply:
|
|
831
|
+
${displayed}${more}
|
|
832
|
+
|
|
833
|
+
Please clean up manually and retry:
|
|
834
|
+
${cmds}`;
|
|
835
|
+
},
|
|
836
|
+
"zh-CN": (files, cleanCommands) => {
|
|
837
|
+
const maxDisplay = 10;
|
|
838
|
+
const displayed = files.slice(0, maxDisplay).map((f) => ` - ${f}`).join("\n");
|
|
839
|
+
const more = files.length > maxDisplay ? `
|
|
840
|
+
...\uFF08\u5171 ${files.length} \u4E2A\u6587\u4EF6\uFF09` : "";
|
|
841
|
+
const cmds = cleanCommands.map((c) => ` ${c}`).join("\n");
|
|
842
|
+
return `\u68C0\u6D4B\u5230\u88AB .gitignore \u5FFD\u7565\u7684\u6587\u4EF6\u6B8B\u7559\u5728\u4E3B worktree \u4E2D\uFF0C\u5BFC\u81F4\u53D8\u66F4\u65E0\u6CD5\u5E94\u7528\uFF1A
|
|
843
|
+
${displayed}${more}
|
|
844
|
+
|
|
845
|
+
\u8BF7\u624B\u52A8\u6E05\u7406\u540E\u91CD\u8BD5\uFF1A
|
|
846
|
+
${cmds}`;
|
|
847
|
+
}
|
|
848
|
+
},
|
|
822
849
|
/** validate 无可用 worktree */
|
|
823
850
|
VALIDATE_NO_WORKTREES: {
|
|
824
851
|
en: "No worktrees available, please create one with clawt run or clawt create first",
|
|
@@ -2817,6 +2844,19 @@ function gitMergeContinue(cwd) {
|
|
|
2817
2844
|
function buildAutoSaveCommitMessage(mainBranch, branch) {
|
|
2818
2845
|
return `${AUTO_SAVE_COMMIT_MESSAGE_PREFIX} ${mainBranch} into ${branch}`;
|
|
2819
2846
|
}
|
|
2847
|
+
function gitCheckIgnored(paths, cwd) {
|
|
2848
|
+
if (paths.length === 0) return [];
|
|
2849
|
+
try {
|
|
2850
|
+
const output = execFileSync2("git", ["check-ignore", "--", ...paths], {
|
|
2851
|
+
cwd,
|
|
2852
|
+
encoding: "utf-8",
|
|
2853
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2854
|
+
});
|
|
2855
|
+
return output.trim().split("\n").filter(Boolean);
|
|
2856
|
+
} catch {
|
|
2857
|
+
return [];
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2820
2860
|
|
|
2821
2861
|
// src/utils/git-branch.ts
|
|
2822
2862
|
function checkBranchExists(branchName, cwd) {
|
|
@@ -5093,6 +5133,17 @@ async function executeRunCommand(command, mainWorktreePath) {
|
|
|
5093
5133
|
}
|
|
5094
5134
|
|
|
5095
5135
|
// src/utils/validate-core.ts
|
|
5136
|
+
import { existsSync as existsSync10 } from "fs";
|
|
5137
|
+
import { join as join10 } from "path";
|
|
5138
|
+
function buildCleanCommands(files) {
|
|
5139
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
5140
|
+
for (const file of files) {
|
|
5141
|
+
const lastSlash = file.lastIndexOf("/");
|
|
5142
|
+
const dir = lastSlash > 0 ? file.substring(0, lastSlash) : ".";
|
|
5143
|
+
dirs.add(dir);
|
|
5144
|
+
}
|
|
5145
|
+
return Array.from(dirs).map((dir) => `git clean -fdx ${dir}/`);
|
|
5146
|
+
}
|
|
5096
5147
|
function migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted) {
|
|
5097
5148
|
let didTempCommit = false;
|
|
5098
5149
|
try {
|
|
@@ -5101,6 +5152,13 @@ function migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName
|
|
|
5101
5152
|
gitCommit("clawt:temp-commit-for-validate", targetWorktreePath);
|
|
5102
5153
|
didTempCommit = true;
|
|
5103
5154
|
}
|
|
5155
|
+
const ignoredFiles = detectIgnoredFilesInPatch(branchName, mainWorktreePath);
|
|
5156
|
+
if (ignoredFiles.length > 0) {
|
|
5157
|
+
const cleanCommands = buildCleanCommands(ignoredFiles);
|
|
5158
|
+
logger.warn(`\u68C0\u6D4B\u5230 ${ignoredFiles.length} \u4E2A\u88AB\u5FFD\u7565\u7684\u6B8B\u7559\u6587\u4EF6\u51B2\u7A81`);
|
|
5159
|
+
printWarning(MESSAGES.VALIDATE_IGNORED_FILES_CONFLICT(ignoredFiles, cleanCommands));
|
|
5160
|
+
return { success: false };
|
|
5161
|
+
}
|
|
5104
5162
|
const patch = gitDiffBinaryAgainstBranch(branchName, mainWorktreePath);
|
|
5105
5163
|
if (patch.length > 0) {
|
|
5106
5164
|
try {
|
|
@@ -5175,6 +5233,16 @@ function switchToValidateBranch(branchName, mainWorktreePath) {
|
|
|
5175
5233
|
}
|
|
5176
5234
|
return validateBranchName;
|
|
5177
5235
|
}
|
|
5236
|
+
function detectIgnoredFilesInPatch(branchName, mainWorktreePath) {
|
|
5237
|
+
try {
|
|
5238
|
+
const output = execCommand(`git diff --name-only HEAD...${branchName}`, { cwd: mainWorktreePath });
|
|
5239
|
+
const patchFiles = output.split("\n").filter(Boolean);
|
|
5240
|
+
if (patchFiles.length === 0) return [];
|
|
5241
|
+
return gitCheckIgnored(patchFiles, mainWorktreePath).filter((file) => existsSync10(join10(mainWorktreePath, file)));
|
|
5242
|
+
} catch {
|
|
5243
|
+
return [];
|
|
5244
|
+
}
|
|
5245
|
+
}
|
|
5178
5246
|
|
|
5179
5247
|
// src/utils/interactive-panel.ts
|
|
5180
5248
|
import { createInterface as createInterface2 } from "readline";
|
|
@@ -5967,9 +6035,9 @@ async function handleMergeConflict(currentBranch, incomingBranch, cwd, autoFlag)
|
|
|
5967
6035
|
}
|
|
5968
6036
|
|
|
5969
6037
|
// src/hooks/post-create.ts
|
|
5970
|
-
import { existsSync as
|
|
6038
|
+
import { existsSync as existsSync11, accessSync, chmodSync, constants as fsConstants } from "fs";
|
|
5971
6039
|
import { spawn as spawn2 } from "child_process";
|
|
5972
|
-
import { join as
|
|
6040
|
+
import { join as join11 } from "path";
|
|
5973
6041
|
var POST_CREATE_SCRIPT_RELATIVE_PATH = ".clawt/postCreate.sh";
|
|
5974
6042
|
function isExecutable(filePath) {
|
|
5975
6043
|
try {
|
|
@@ -6001,8 +6069,8 @@ function resolvePostCreateHook() {
|
|
|
6001
6069
|
}
|
|
6002
6070
|
}
|
|
6003
6071
|
const mainWorktreePath = getMainWorktreePath();
|
|
6004
|
-
const scriptPath =
|
|
6005
|
-
if (
|
|
6072
|
+
const scriptPath = join11(mainWorktreePath, POST_CREATE_SCRIPT_RELATIVE_PATH);
|
|
6073
|
+
if (existsSync11(scriptPath)) {
|
|
6006
6074
|
if (!isExecutable(scriptPath)) {
|
|
6007
6075
|
autoFixExecutablePermission(scriptPath);
|
|
6008
6076
|
}
|
|
@@ -7263,8 +7331,8 @@ function registerAliasCommand(program2) {
|
|
|
7263
7331
|
}
|
|
7264
7332
|
|
|
7265
7333
|
// src/commands/projects.ts
|
|
7266
|
-
import { existsSync as
|
|
7267
|
-
import { join as
|
|
7334
|
+
import { existsSync as existsSync12, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
|
|
7335
|
+
import { join as join12 } from "path";
|
|
7268
7336
|
import chalk13 from "chalk";
|
|
7269
7337
|
function registerProjectsCommand(program2) {
|
|
7270
7338
|
program2.command("projects [name]").description(getCurrentLanguage() === "en" ? "Show worktree overview across projects, or view details for a specific project" : "\u5C55\u793A\u6240\u6709\u9879\u76EE\u7684 worktree \u6982\u89C8\uFF0C\u6216\u67E5\u770B\u6307\u5B9A\u9879\u76EE\u7684 worktree \u8BE6\u60C5").option("--json", getCurrentLanguage() === "en" ? "Output in JSON format" : "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA").action((name, options) => {
|
|
@@ -7288,8 +7356,8 @@ function handleProjectsOverview(json) {
|
|
|
7288
7356
|
printProjectsOverviewAsText(result);
|
|
7289
7357
|
}
|
|
7290
7358
|
function handleProjectDetail(name, json) {
|
|
7291
|
-
const projectDir =
|
|
7292
|
-
if (!
|
|
7359
|
+
const projectDir = join12(WORKTREES_DIR, name);
|
|
7360
|
+
if (!existsSync12(projectDir)) {
|
|
7293
7361
|
printError(MESSAGES.PROJECTS_NOT_FOUND(name));
|
|
7294
7362
|
process.exit(1);
|
|
7295
7363
|
}
|
|
@@ -7302,7 +7370,7 @@ function handleProjectDetail(name, json) {
|
|
|
7302
7370
|
printProjectDetailAsText(result);
|
|
7303
7371
|
}
|
|
7304
7372
|
function collectProjectsOverview() {
|
|
7305
|
-
if (!
|
|
7373
|
+
if (!existsSync12(WORKTREES_DIR)) {
|
|
7306
7374
|
return { projects: [], totalProjects: 0, totalDiskUsage: 0 };
|
|
7307
7375
|
}
|
|
7308
7376
|
const entries = readdirSync6(WORKTREES_DIR, { withFileTypes: true });
|
|
@@ -7311,7 +7379,7 @@ function collectProjectsOverview() {
|
|
|
7311
7379
|
if (!entry.isDirectory()) {
|
|
7312
7380
|
continue;
|
|
7313
7381
|
}
|
|
7314
|
-
const projectDir =
|
|
7382
|
+
const projectDir = join12(WORKTREES_DIR, entry.name);
|
|
7315
7383
|
const overview = collectSingleProjectOverview(entry.name, projectDir);
|
|
7316
7384
|
projects.push(overview);
|
|
7317
7385
|
}
|
|
@@ -7328,7 +7396,7 @@ function collectSingleProjectOverview(name, projectDir) {
|
|
|
7328
7396
|
const worktreeDirs = subEntries.filter((e) => e.isDirectory());
|
|
7329
7397
|
const worktreeCount = worktreeDirs.length;
|
|
7330
7398
|
const diskUsage = calculateDirSize(projectDir);
|
|
7331
|
-
const lastActiveTime = resolveProjectLastActiveTime(projectDir, worktreeDirs.map((e) =>
|
|
7399
|
+
const lastActiveTime = resolveProjectLastActiveTime(projectDir, worktreeDirs.map((e) => join12(projectDir, e.name)));
|
|
7332
7400
|
return {
|
|
7333
7401
|
name,
|
|
7334
7402
|
worktreeCount,
|
|
@@ -7343,7 +7411,7 @@ function collectProjectDetail(name, projectDir) {
|
|
|
7343
7411
|
if (!entry.isDirectory()) {
|
|
7344
7412
|
continue;
|
|
7345
7413
|
}
|
|
7346
|
-
const wtPath =
|
|
7414
|
+
const wtPath = join12(projectDir, entry.name);
|
|
7347
7415
|
const detail = collectSingleWorktreeDetail(entry.name, wtPath);
|
|
7348
7416
|
worktrees.push(detail);
|
|
7349
7417
|
}
|
|
@@ -7443,7 +7511,7 @@ function printWorktreeDetailItem(wt) {
|
|
|
7443
7511
|
}
|
|
7444
7512
|
|
|
7445
7513
|
// src/commands/completion.ts
|
|
7446
|
-
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as
|
|
7514
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync14 } from "fs";
|
|
7447
7515
|
import { resolve as resolve3 } from "path";
|
|
7448
7516
|
import { homedir as homedir2 } from "os";
|
|
7449
7517
|
|
|
@@ -7494,14 +7562,14 @@ compdef _clawt_completion clawt
|
|
|
7494
7562
|
}
|
|
7495
7563
|
|
|
7496
7564
|
// src/utils/completion-engine.ts
|
|
7497
|
-
import { existsSync as
|
|
7498
|
-
import { join as
|
|
7565
|
+
import { existsSync as existsSync13, readdirSync as readdirSync7, statSync as statSync5 } from "fs";
|
|
7566
|
+
import { join as join13, dirname, basename as basename2 } from "path";
|
|
7499
7567
|
function completeFilePath(partial) {
|
|
7500
7568
|
const cwd = process.cwd();
|
|
7501
7569
|
const hasDir = partial.includes("/");
|
|
7502
|
-
const searchDir = hasDir ?
|
|
7570
|
+
const searchDir = hasDir ? join13(cwd, dirname(partial)) : cwd;
|
|
7503
7571
|
const prefix = hasDir ? basename2(partial) : partial;
|
|
7504
|
-
if (!
|
|
7572
|
+
if (!existsSync13(searchDir)) {
|
|
7505
7573
|
return [];
|
|
7506
7574
|
}
|
|
7507
7575
|
const entries = readdirSync7(searchDir);
|
|
@@ -7510,7 +7578,7 @@ function completeFilePath(partial) {
|
|
|
7510
7578
|
for (const entry of entries) {
|
|
7511
7579
|
if (!entry.startsWith(prefix)) continue;
|
|
7512
7580
|
if (entry.startsWith(".")) continue;
|
|
7513
|
-
const fullPath =
|
|
7581
|
+
const fullPath = join13(searchDir, entry);
|
|
7514
7582
|
try {
|
|
7515
7583
|
const stat = statSync5(fullPath);
|
|
7516
7584
|
if (stat.isDirectory()) {
|
|
@@ -7599,7 +7667,7 @@ function generateCompletions(program2, args) {
|
|
|
7599
7667
|
|
|
7600
7668
|
// src/commands/completion.ts
|
|
7601
7669
|
function appendToFile(filePath, content) {
|
|
7602
|
-
if (
|
|
7670
|
+
if (existsSync14(filePath)) {
|
|
7603
7671
|
const current = readFileSync6(filePath, "utf-8");
|
|
7604
7672
|
if (current.includes("clawt completion")) {
|
|
7605
7673
|
printInfo(MESSAGES.COMPLETION_INSTALL_EXISTS + ": " + filePath);
|
|
@@ -7739,19 +7807,19 @@ async function handleHome() {
|
|
|
7739
7807
|
}
|
|
7740
7808
|
|
|
7741
7809
|
// src/commands/tasks.ts
|
|
7742
|
-
import { resolve as resolve4, dirname as dirname2, join as
|
|
7743
|
-
import { existsSync as
|
|
7810
|
+
import { resolve as resolve4, dirname as dirname2, join as join14 } from "path";
|
|
7811
|
+
import { existsSync as existsSync15, writeFileSync as writeFileSync6 } from "fs";
|
|
7744
7812
|
function registerTasksCommand(program2) {
|
|
7745
7813
|
const taskCmd = program2.command("tasks").description(getCurrentLanguage() === "en" ? "Task file management" : "\u4EFB\u52A1\u6587\u4EF6\u7BA1\u7406");
|
|
7746
7814
|
taskCmd.command("init").description(getCurrentLanguage() === "en" ? "Generate task template file" : "\u751F\u6210\u4EFB\u52A1\u6A21\u677F\u6587\u4EF6").argument("[path]", getCurrentLanguage() === "en" ? "Output file path" : "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84").action(async (path2) => {
|
|
7747
|
-
const filePath = path2 ??
|
|
7815
|
+
const filePath = path2 ?? join14(TASK_TEMPLATE_OUTPUT_DIR, generateTaskFilename(TASK_TEMPLATE_FILENAME_PREFIX));
|
|
7748
7816
|
await handleTasksInit(filePath);
|
|
7749
7817
|
});
|
|
7750
7818
|
}
|
|
7751
7819
|
async function handleTasksInit(filePath) {
|
|
7752
7820
|
const absolutePath = resolve4(filePath);
|
|
7753
7821
|
logger.info(`tasks init \u547D\u4EE4\u6267\u884C\uFF0C\u76EE\u6807\u6587\u4EF6: ${absolutePath}`);
|
|
7754
|
-
if (
|
|
7822
|
+
if (existsSync15(absolutePath)) {
|
|
7755
7823
|
throw new ClawtError(MESSAGES.TASK_INIT_FILE_EXISTS(filePath));
|
|
7756
7824
|
}
|
|
7757
7825
|
ensureDir(dirname2(absolutePath));
|
package/dist/postinstall.js
CHANGED
|
@@ -726,6 +726,33 @@ var VALIDATE_MESSAGES_I18N = {
|
|
|
726
726
|
"zh-CN": (branch) => `\u53D8\u66F4\u8FC1\u79FB\u5931\u8D25\uFF1A\u76EE\u6807\u5206\u652F\u4E0E\u4E3B\u5206\u652F\u5DEE\u5F02\u8FC7\u5927
|
|
727
727
|
\u8BF7\u5148\u6267\u884C clawt sync -b ${branch} \u540C\u6B65\u4E3B\u5206\u652F\u540E\u91CD\u8BD5`
|
|
728
728
|
},
|
|
729
|
+
/** validate 检测到被 .gitignore 忽略的残留文件冲突 */
|
|
730
|
+
VALIDATE_IGNORED_FILES_CONFLICT: {
|
|
731
|
+
en: (files, cleanCommands) => {
|
|
732
|
+
const maxDisplay = 10;
|
|
733
|
+
const displayed = files.slice(0, maxDisplay).map((f) => ` - ${f}`).join("\n");
|
|
734
|
+
const more = files.length > maxDisplay ? `
|
|
735
|
+
...(${files.length} files total)` : "";
|
|
736
|
+
const cmds = cleanCommands.map((c) => ` ${c}`).join("\n");
|
|
737
|
+
return `Ignored files left in main worktree are blocking patch apply:
|
|
738
|
+
${displayed}${more}
|
|
739
|
+
|
|
740
|
+
Please clean up manually and retry:
|
|
741
|
+
${cmds}`;
|
|
742
|
+
},
|
|
743
|
+
"zh-CN": (files, cleanCommands) => {
|
|
744
|
+
const maxDisplay = 10;
|
|
745
|
+
const displayed = files.slice(0, maxDisplay).map((f) => ` - ${f}`).join("\n");
|
|
746
|
+
const more = files.length > maxDisplay ? `
|
|
747
|
+
...\uFF08\u5171 ${files.length} \u4E2A\u6587\u4EF6\uFF09` : "";
|
|
748
|
+
const cmds = cleanCommands.map((c) => ` ${c}`).join("\n");
|
|
749
|
+
return `\u68C0\u6D4B\u5230\u88AB .gitignore \u5FFD\u7565\u7684\u6587\u4EF6\u6B8B\u7559\u5728\u4E3B worktree \u4E2D\uFF0C\u5BFC\u81F4\u53D8\u66F4\u65E0\u6CD5\u5E94\u7528\uFF1A
|
|
750
|
+
${displayed}${more}
|
|
751
|
+
|
|
752
|
+
\u8BF7\u624B\u52A8\u6E05\u7406\u540E\u91CD\u8BD5\uFF1A
|
|
753
|
+
${cmds}`;
|
|
754
|
+
}
|
|
755
|
+
},
|
|
729
756
|
/** validate 无可用 worktree */
|
|
730
757
|
VALIDATE_NO_WORKTREES: {
|
|
731
758
|
en: "No worktrees available, please create one with clawt run or clawt create first",
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# sync 后 validate 仍然失败 — 调查记录
|
|
2
|
+
|
|
3
|
+
**日期:** 2026-06-01
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 现象描述
|
|
8
|
+
|
|
9
|
+
用户在交互式面板(`clawt status`)中对目标分支执行 validate,patch apply 失败,提示:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
⚠ Change migration failed: target branch has diverged too far from main
|
|
13
|
+
Please run clawt sync -b <target-branch> first, then retry
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
用户选择自动 sync(或手动执行 `clawt sync -b <target-branch>`),sync 成功:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
✓ Synced latest code from <main-branch> to <target-branch>
|
|
20
|
+
Validation branch clawt-validate-<target-branch> has been rebuilt
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
但再次执行 validate 时,仍然报同样的错误。
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 调查过程
|
|
28
|
+
|
|
29
|
+
### 第一步:确认分支状态
|
|
30
|
+
|
|
31
|
+
- 主工作分支:HEAD 指向最新 commit
|
|
32
|
+
- 验证分支:与主分支 HEAD 一致(sync 后已重建)
|
|
33
|
+
- 目标分支:包含多次 merge commit,已合并主分支最新代码
|
|
34
|
+
|
|
35
|
+
**结论:** sync 已成功,验证分支已正确重建,分支状态本身没有问题。
|
|
36
|
+
|
|
37
|
+
### 第二步:模拟 patch apply
|
|
38
|
+
|
|
39
|
+
执行三点 diff 并尝试 apply:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
git diff clawt-validate-<target-branch>...<target-branch> --binary | git apply --check -
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
输出:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
错误:<ignored-dir>/findings/some-findings.md:已经存在于工作区中
|
|
49
|
+
错误:<ignored-dir>/plans/some-plan.md:已经存在于工作区中
|
|
50
|
+
...(共 N 个文件)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**关键发现:** patch 失败的原因不是分支差异过大,而是目标分支中跟踪的某些文件在主 worktree 工作目录中已经物理存在。
|
|
54
|
+
|
|
55
|
+
### 第三步:追溯文件来源
|
|
56
|
+
|
|
57
|
+
检查主 worktree 工作目录:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
ls <ignored-dir>/findings/
|
|
61
|
+
# some-findings.md ← 物理存在
|
|
62
|
+
# ...(共多个文件)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
检查 `git status`:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
无文件要提交,干净的工作区
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**为什么这些文件物理存在但 git status 不显示?**
|
|
72
|
+
|
|
73
|
+
检查 `.gitignore`:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
cat .gitignore | grep <ignored-dir>
|
|
77
|
+
# <ignored-dir>/ ← 被忽略!
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**结论:** 该目录在主分支的 `.gitignore` 中,这些文件是**被忽略的未跟踪文件**,`git status` 不会显示它们。
|
|
81
|
+
|
|
82
|
+
### 第四步:追溯文件如何进入主 worktree
|
|
83
|
+
|
|
84
|
+
这些文件是由**之前的 validate 操作**创建的:
|
|
85
|
+
|
|
86
|
+
1. validate 通过 `git diff HEAD...branch --binary | git apply -` 将目标分支的变更(包括被忽略目录下的文件)应用到主 worktree 工作目录
|
|
87
|
+
2. 目标分支跟踪了这些文件(尽管 `.gitignore` 中有该目录,但 AI Agent 在 worktree 中用 `git add -f` 强制添加了它们)
|
|
88
|
+
3. validate 完成后,这些文件作为**未跟踪文件**留在主 worktree 工作目录中
|
|
89
|
+
|
|
90
|
+
### 第五步:追溯为什么清理无效
|
|
91
|
+
|
|
92
|
+
validate 的清理逻辑(`handleIncrementalValidate` 步骤 2 和 `handleDirtyWorkingDir` 的 reset 选项)使用:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
git reset --hard # 只影响已跟踪文件
|
|
96
|
+
git clean -fd # 删除未跟踪文件,但**不删除被忽略的文件**
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`git clean -fd` 的行为:
|
|
100
|
+
- 删除未跟踪且未被忽略的文件 ✓
|
|
101
|
+
- **跳过**在 `.gitignore` 中的文件 ✗
|
|
102
|
+
|
|
103
|
+
要删除被忽略的文件,需要 `git clean -fdx`(`-x` 表示包括被忽略的文件)。
|
|
104
|
+
|
|
105
|
+
### 第六步:确认 sync 为什么不解决问题
|
|
106
|
+
|
|
107
|
+
`executeSyncForBranch` 的操作:
|
|
108
|
+
1. 检查目标 worktree 未提交变更 → 自动 commit
|
|
109
|
+
2. 在目标 worktree 中 `git merge <main-branch>`
|
|
110
|
+
3. 重建验证分支
|
|
111
|
+
|
|
112
|
+
**sync 完全不清理主 worktree 的工作目录**,因此那些被忽略的残留文件仍然存在。
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 根本原因
|
|
117
|
+
|
|
118
|
+
**完整因果链:**
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
上游 commit 将某目录加入 .gitignore 并从 git 跟踪中移除
|
|
122
|
+
↓
|
|
123
|
+
目标分支仍然跟踪着自己新增的文件(AI Agent 用 git add -f 强制提交)
|
|
124
|
+
↓
|
|
125
|
+
validate 通过 patch apply 将这些文件创建到主 worktree 工作目录
|
|
126
|
+
↓
|
|
127
|
+
validate 结束后的清理(git clean -fd)无法删除这些文件(因为在 .gitignore 中)
|
|
128
|
+
↓
|
|
129
|
+
文件作为"被忽略的未跟踪文件"永久残留在主 worktree
|
|
130
|
+
↓
|
|
131
|
+
下次 validate 的 patch apply 试图创建同名文件 → "已经存在于工作区中" → 失败
|
|
132
|
+
↓
|
|
133
|
+
auto-sync 不清理主 worktree 工作目录 → 问题持续存在(死循环)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**核心矛盾:**
|
|
137
|
+
|
|
138
|
+
- 被忽略目录在主分支 `.gitignore` 中 → `git clean -fd` 不删除
|
|
139
|
+
- 被忽略目录在目标分支被跟踪 → patch 包含这些文件 → apply 时与物理存在的文件冲突
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 影响范围
|
|
144
|
+
|
|
145
|
+
此 bug 在以下条件同时满足时触发:
|
|
146
|
+
|
|
147
|
+
1. 目标分支跟踪了某些在 `.gitignore` 中的文件(通常是 AI Agent 用 `git add -f` 强制添加)
|
|
148
|
+
2. 主分支的 `.gitignore` 包含对应路径
|
|
149
|
+
3. 至少执行过一次 validate(使文件通过 patch 进入主 worktree)
|
|
150
|
+
4. 后续再次执行 validate(patch apply 因文件已存在而失败)
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 技术决策记录
|
|
155
|
+
|
|
156
|
+
| 假设 | 验证结果 |
|
|
157
|
+
|------|----------|
|
|
158
|
+
| 分支确实 diverged(差异过大) | ❌ 排除:三点 diff 正常,merge-base 正确 |
|
|
159
|
+
| 验证分支未正确重建 | ❌ 排除:验证分支与主分支 HEAD 一致 |
|
|
160
|
+
| sync 未成功执行 | ❌ 排除:sync 成功,目标分支已合并主分支 |
|
|
161
|
+
| patch 内容与主分支冲突 | ❌ 排除:不是内容冲突,是文件已存在 |
|
|
162
|
+
| **被忽略文件残留导致 patch apply 失败** | ✅ 确认:`git apply --check` 明确报 "已经存在于工作区中" |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Brainstorming 决策(2026-06-01)
|
|
167
|
+
|
|
168
|
+
### 方案评估
|
|
169
|
+
|
|
170
|
+
| 方案 | 安全性 | 便利性 | 误删风险 | 代码复杂度 |
|
|
171
|
+
|------|--------|--------|----------|------------|
|
|
172
|
+
| A:检测 + 提示用户手动清理 | 高 | 低 | 无 | 低 |
|
|
173
|
+
| B:检测 + 自动清理后继续 | 中 | 高 | 有 | 略高 |
|
|
174
|
+
|
|
175
|
+
### 最终决策
|
|
176
|
+
|
|
177
|
+
**选择方案 A(检测 + 提示用户手动清理)**
|
|
178
|
+
|
|
179
|
+
理由:
|
|
180
|
+
1. 这类冲突本质上是项目 `.gitignore` 配置与分支跟踪状态不一致导致的,属于需要用户知情的项目级决策
|
|
181
|
+
2. 不应该由工具静默处理,用户需要知道哪些文件被清理了
|
|
182
|
+
3. 实现简单,风险为零
|
|
183
|
+
|
|
184
|
+
### 实现方案
|
|
185
|
+
|
|
186
|
+
1. 新增 `gitCheckIgnored(paths, cwd)` — 批量检测文件是否被 `.gitignore` 忽略
|
|
187
|
+
2. 新增 `detectIgnoredFilesInPatch(branchName, mainWorktreePath)` — 检测 patch 中的幽灵文件
|
|
188
|
+
3. 修改 `migrateChangesViaPatch` — apply 前调用检测函数,有冲突则输出提示并返回失败
|
|
189
|
+
4. 新增消息常量 `VALIDATE_IGNORED_FILES_CONFLICT`(双语)
|
|
190
|
+
|
|
191
|
+
### 提示格式
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
⚠ 检测到被 .gitignore 忽略的文件残留在主 worktree 中,导致变更无法应用:
|
|
195
|
+
- <ignored-dir>/findings/some-findings.md
|
|
196
|
+
- <ignored-dir>/plans/some-plan.md
|
|
197
|
+
...(共 N 个文件)
|
|
198
|
+
|
|
199
|
+
请手动清理后重试:
|
|
200
|
+
git clean -fdx <ignored-dir>/
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
清理命令按冲突文件的直接父目录去重生成。
|