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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/clawt.svg)](https://www.npmjs.com/package/clawt) [![GitHub](https://img.shields.io/badge/GHub-afk101%2Fclawt-blue)](https://github.com/afk101/clawt)
4
4
 
5
- **[English](./README.md)** | [中文](./README.zh-CN.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
- ![Serial timeline](./docs/images/serial-timeline.png)
22
+ ![Serial timeline](https://raw.githubusercontent.com/afk101/clawt/main/docs/images/serial-timeline.png)
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
- ![Parallel timeline](./docs/images/parallel-timeline.png)
36
+ ![Parallel timeline](https://raw.githubusercontent.com/afk101/clawt/main/docs/images/parallel-timeline.png)
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
- ![Architecture](./docs/images/architecture.png)
63
+ ![Architecture](https://raw.githubusercontent.com/afk101/clawt/main/docs/images/architecture.png)
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
- ![Layer architecture](./docs/images/layer-architecture.png)
67
+ ![Layer architecture](https://raw.githubusercontent.com/afk101/clawt/main/docs/images/layer-architecture.png)
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
- ![Status panel](./docs/images/status-panel-en.png)
121
+ ![Status panel](https://raw.githubusercontent.com/afk101/clawt/main/docs/images/status-panel-en.png)
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
- ![串行模式时间轴](./docs/images/serial-timeline.png)
22
+ ![串行模式时间轴](https://raw.githubusercontent.com/afk101/clawt/main/docs/images/serial-timeline.png)
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
- ![并行模式时间轴](./docs/images/parallel-timeline.png)
36
+ ![并行模式时间轴](https://raw.githubusercontent.com/afk101/clawt/main/docs/images/parallel-timeline.png)
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
- ![架构图](./docs/images/architecture.png)
65
+ ![架构图](https://raw.githubusercontent.com/afk101/clawt/main/docs/images/architecture.png)
66
66
 
67
67
  > **Clawt 不修改、不替换、不包装 Claude Code 本身。它只是在更高一层管理"在哪里"以及"同时启动多少个" Claude Code。**
68
68
 
69
- ![分层架构](./docs/images/layer-architecture.png)
69
+ ![分层架构](https://raw.githubusercontent.com/afk101/clawt/main/docs/images/layer-architecture.png)
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
- ![状态面板](./docs/images/status-panel-zh.png)
123
+ ![状态面板](https://raw.githubusercontent.com/afk101/clawt/main/docs/images/status-panel-zh.png)
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 existsSync10, accessSync, chmodSync, constants as fsConstants } from "fs";
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 join10 } from "path";
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 = join10(mainWorktreePath, POST_CREATE_SCRIPT_RELATIVE_PATH);
6005
- if (existsSync10(scriptPath)) {
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 existsSync11, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
7267
- import { join as join11 } from "path";
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 = join11(WORKTREES_DIR, name);
7292
- if (!existsSync11(projectDir)) {
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 (!existsSync11(WORKTREES_DIR)) {
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 = join11(WORKTREES_DIR, entry.name);
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) => join11(projectDir, e.name)));
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 = join11(projectDir, entry.name);
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 existsSync13 } from "fs";
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 existsSync12, readdirSync as readdirSync7, statSync as statSync5 } from "fs";
7498
- import { join as join12, dirname, basename as basename2 } from "path";
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 ? join12(cwd, dirname(partial)) : cwd;
7570
+ const searchDir = hasDir ? join13(cwd, dirname(partial)) : cwd;
7503
7571
  const prefix = hasDir ? basename2(partial) : partial;
7504
- if (!existsSync12(searchDir)) {
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 = join12(searchDir, entry);
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 (existsSync13(filePath)) {
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 join13 } from "path";
7743
- import { existsSync as existsSync14, writeFileSync as writeFileSync6 } from "fs";
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 ?? join13(TASK_TEMPLATE_OUTPUT_DIR, generateTaskFilename(TASK_TEMPLATE_FILENAME_PREFIX));
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 (existsSync14(absolutePath)) {
7822
+ if (existsSync15(absolutePath)) {
7755
7823
  throw new ClawtError(MESSAGES.TASK_INIT_FILE_EXISTS(filePath));
7756
7824
  }
7757
7825
  ensureDir(dirname2(absolutePath));
@@ -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
+ 清理命令按冲突文件的直接父目录去重生成。