clawt 3.9.4 → 3.9.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/dist/index.js CHANGED
@@ -73,7 +73,9 @@ var COMMON_MESSAGES = {
73
73
  \u539F\u56E0\uFF1A\u9501\u6587\u4EF6\u5DF2\u5B58\u5728\uFF08\u53EF\u80FD\u662F\u4E0A\u6B21 git \u64CD\u4F5C\u5F02\u5E38\u4E2D\u65AD\u6B8B\u7559\uFF09
74
74
  \u9501\u6587\u4EF6\u8DEF\u5F84\uFF1A${lockFilePath}
75
75
  \u4FEE\u590D\u65B9\u6CD5\uFF1A\u786E\u8BA4\u6CA1\u6709\u5176\u4ED6 git \u64CD\u4F5C\u5728\u8FDB\u884C\u540E\uFF0C\u6267\u884C\u4EE5\u4E0B\u547D\u4EE4\u5220\u9664\u9501\u6587\u4EF6\uFF1A
76
- rm ${lockFilePath}`
76
+ rm ${lockFilePath}`,
77
+ /** Git index.lock 重试中(简短提示) */
78
+ GIT_INDEX_LOCK_RETRYING: "Git index \u88AB\u9501\u5B9A\uFF0C\u6B63\u5728\u91CD\u8BD5..."
77
79
  };
78
80
 
79
81
  // src/constants/messages/run.ts
@@ -798,6 +800,12 @@ var PROJECT_CONFIG_DESCRIPTIONS = deriveConfigDescriptions2(PROJECT_CONFIG_DEFIN
798
800
  // src/constants/git.ts
799
801
  var AUTO_SAVE_COMMIT_MESSAGE_PREFIX = "clawt: auto-save before merging";
800
802
  var EXEC_MAX_BUFFER = 200 * 1024 * 1024;
803
+ var GIT_INDEX_LOCK_RETRY = {
804
+ /** 重试次数(用户反馈"重试一下就可以了",单次重试足够) */
805
+ MAX_RETRIES: 1,
806
+ /** 重试延迟毫秒数(让锁文件有时间被释放) */
807
+ DELAY_MS: 150
808
+ };
801
809
 
802
810
  // src/constants/logger.ts
803
811
  var DEBUG_TIMESTAMP_FORMAT = "HH:mm:ss.SSS";
@@ -1043,6 +1051,20 @@ function throwIfGitIndexLockError(error, cwd) {
1043
1051
  throw new ClawtError(MESSAGES.GIT_INDEX_LOCKED(lockFilePath));
1044
1052
  }
1045
1053
  }
1054
+ function sleepSync(ms) {
1055
+ const sharedBuffer = new SharedArrayBuffer(4);
1056
+ const int32 = new Int32Array(sharedBuffer);
1057
+ Atomics.wait(int32, 0, 0, ms);
1058
+ }
1059
+ function shouldRetryGitIndexLockError(error, retryCount) {
1060
+ const errorMessage = extractFullErrorMessage(error);
1061
+ return isGitIndexLockError(errorMessage) && retryCount < GIT_INDEX_LOCK_RETRY.MAX_RETRIES;
1062
+ }
1063
+ function waitForGitIndexLockRetrySync() {
1064
+ process.stderr.write(`${MESSAGES.GIT_INDEX_LOCK_RETRYING}
1065
+ `);
1066
+ sleepSync(GIT_INDEX_LOCK_RETRY.DELAY_MS);
1067
+ }
1046
1068
 
1047
1069
  // src/utils/shell.ts
1048
1070
  function getEnvWithoutNestedSessionFlag() {
@@ -1051,17 +1073,26 @@ function getEnvWithoutNestedSessionFlag() {
1051
1073
  }
1052
1074
  function execCommand(command, options) {
1053
1075
  logger.debug(`\u6267\u884C\u547D\u4EE4: ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
1054
- try {
1055
- const result = execSync2(command, {
1056
- cwd: options?.cwd,
1057
- encoding: "utf-8",
1058
- stdio: ["pipe", "pipe", "pipe"],
1059
- maxBuffer: EXEC_MAX_BUFFER
1060
- });
1061
- return result.trim();
1062
- } catch (error) {
1063
- throwIfGitIndexLockError(error, options?.cwd);
1064
- throw error;
1076
+ let retryCount = 0;
1077
+ while (true) {
1078
+ try {
1079
+ const result = execSync2(command, {
1080
+ cwd: options?.cwd,
1081
+ encoding: "utf-8",
1082
+ stdio: ["pipe", "pipe", "pipe"],
1083
+ maxBuffer: EXEC_MAX_BUFFER
1084
+ });
1085
+ return result.trim();
1086
+ } catch (error) {
1087
+ if (shouldRetryGitIndexLockError(error, retryCount)) {
1088
+ retryCount++;
1089
+ logger.debug(`\u68C0\u6D4B\u5230 index.lock \u9519\u8BEF\uFF0C\u7B2C ${retryCount} \u6B21\u91CD\u8BD5`);
1090
+ waitForGitIndexLockRetrySync();
1091
+ continue;
1092
+ }
1093
+ throwIfGitIndexLockError(error, options?.cwd);
1094
+ throw error;
1095
+ }
1065
1096
  }
1066
1097
  }
1067
1098
  function spawnProcess(command, args, options) {
@@ -1081,18 +1112,27 @@ function killAllChildProcesses(children) {
1081
1112
  }
1082
1113
  function execCommandWithInput(command, args, options) {
1083
1114
  logger.debug(`\u6267\u884C\u547D\u4EE4(stdin): ${command} ${args.join(" ")}${options.cwd ? ` (cwd: ${options.cwd})` : ""}`);
1084
- try {
1085
- const result = execFileSync(command, args, {
1086
- cwd: options.cwd,
1087
- input: options.input,
1088
- encoding: "utf-8",
1089
- stdio: ["pipe", "pipe", "pipe"],
1090
- maxBuffer: EXEC_MAX_BUFFER
1091
- });
1092
- return result.trim();
1093
- } catch (error) {
1094
- throwIfGitIndexLockError(error, options.cwd);
1095
- throw error;
1115
+ let retryCount = 0;
1116
+ while (true) {
1117
+ try {
1118
+ const result = execFileSync(command, args, {
1119
+ cwd: options.cwd,
1120
+ input: options.input,
1121
+ encoding: "utf-8",
1122
+ stdio: ["pipe", "pipe", "pipe"],
1123
+ maxBuffer: EXEC_MAX_BUFFER
1124
+ });
1125
+ return result.trim();
1126
+ } catch (error) {
1127
+ if (shouldRetryGitIndexLockError(error, retryCount)) {
1128
+ retryCount++;
1129
+ logger.debug(`\u68C0\u6D4B\u5230 index.lock \u9519\u8BEF\uFF0C\u7B2C ${retryCount} \u6B21\u91CD\u8BD5`);
1130
+ waitForGitIndexLockRetrySync();
1131
+ continue;
1132
+ }
1133
+ throwIfGitIndexLockError(error, options.cwd);
1134
+ throw error;
1135
+ }
1096
1136
  }
1097
1137
  }
1098
1138
  function runCommandInherited(command, options) {
@@ -64,7 +64,9 @@ var COMMON_MESSAGES = {
64
64
  \u539F\u56E0\uFF1A\u9501\u6587\u4EF6\u5DF2\u5B58\u5728\uFF08\u53EF\u80FD\u662F\u4E0A\u6B21 git \u64CD\u4F5C\u5F02\u5E38\u4E2D\u65AD\u6B8B\u7559\uFF09
65
65
  \u9501\u6587\u4EF6\u8DEF\u5F84\uFF1A${lockFilePath}
66
66
  \u4FEE\u590D\u65B9\u6CD5\uFF1A\u786E\u8BA4\u6CA1\u6709\u5176\u4ED6 git \u64CD\u4F5C\u5728\u8FDB\u884C\u540E\uFF0C\u6267\u884C\u4EE5\u4E0B\u547D\u4EE4\u5220\u9664\u9501\u6587\u4EF6\uFF1A
67
- rm ${lockFilePath}`
67
+ rm ${lockFilePath}`,
68
+ /** Git index.lock 重试中(简短提示) */
69
+ GIT_INDEX_LOCK_RETRYING: "Git index \u88AB\u9501\u5B9A\uFF0C\u6B63\u5728\u91CD\u8BD5..."
68
70
  };
69
71
 
70
72
  // src/constants/messages/run.ts
package/docs/spec.md CHANGED
@@ -384,6 +384,7 @@ async function interactiveConfigEditor<T extends object>(
384
384
  | Worktree 路径已存在 | 输出错误提示,退出 (exit code 1) |
385
385
  | Git 命令执行失败 | 捕获 stderr,记录日志,输出错误提示,退出 (exit code 1) |
386
386
  | 目标 worktree 不存在 | 输出错误提示(列出可用 worktree),退出 (exit code 1) |
387
+ | Git index.lock 被锁定 | 自动重试 1 次(延迟 150ms),重试失败则输出错误提示和修复方法 |
387
388
 
388
389
  ### 7.2 退出码
389
390
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "3.9.4",
3
+ "version": "3.9.5",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3,3 +3,14 @@ export const AUTO_SAVE_COMMIT_MESSAGE_PREFIX = 'clawt: auto-save before merging'
3
3
 
4
4
  /** execSync 最大缓冲区大小(200MB),防止大分支 diff 时触发 ENOBUFS 错误 */
5
5
  export const EXEC_MAX_BUFFER = 200 * 1024 * 1024;
6
+
7
+ /**
8
+ * Git index.lock 错误重试配置
9
+ * 当检测到 index.lock 错误时自动重试,避免因短暂竞争导致用户操作失败
10
+ */
11
+ export const GIT_INDEX_LOCK_RETRY = {
12
+ /** 重试次数(用户反馈"重试一下就可以了",单次重试足够) */
13
+ MAX_RETRIES: 1,
14
+ /** 重试延迟毫秒数(让锁文件有时间被释放) */
15
+ DELAY_MS: 150,
16
+ } as const;
@@ -55,4 +55,6 @@ export const COMMON_MESSAGES = {
55
55
  ` 锁文件路径:${lockFilePath}\n` +
56
56
  ` 修复方法:确认没有其他 git 操作在进行后,执行以下命令删除锁文件:\n` +
57
57
  ` rm ${lockFilePath}`,
58
+ /** Git index.lock 重试中(简短提示) */
59
+ GIT_INDEX_LOCK_RETRYING: 'Git index 被锁定,正在重试...',
58
60
  } as const;
@@ -3,6 +3,7 @@ import { execSync } from 'node:child_process';
3
3
  import { logger } from '../logger/index.js';
4
4
  import { ClawtError } from '../errors/index.js';
5
5
  import { MESSAGES } from '../constants/index.js';
6
+ import { GIT_INDEX_LOCK_RETRY } from '../constants/git.js';
6
7
 
7
8
  /**
8
9
  * index.lock 错误的关键词匹配模式
@@ -38,7 +39,7 @@ export function isGitIndexLockError(errorMessage: string): boolean {
38
39
  * @param {unknown} error - 捕获的错误对象
39
40
  * @returns {string} 合并后的错误消息
40
41
  */
41
- function extractFullErrorMessage(error: unknown): string {
42
+ export function extractFullErrorMessage(error: unknown): string {
42
43
  if (!(error instanceof Error)) return String(error);
43
44
  const stderr = (error as { stderr?: string | Buffer }).stderr;
44
45
  const stderrStr = stderr ? String(stderr) : '';
@@ -94,3 +95,34 @@ export function throwIfGitIndexLockError(error: unknown, cwd?: string): void {
94
95
  throw new ClawtError(MESSAGES.GIT_INDEX_LOCKED(lockFilePath));
95
96
  }
96
97
  }
98
+
99
+ /**
100
+ * 同步延迟函数(使用 Atomics.wait 实现真正的阻塞等待)
101
+ * 相比 busy-wait,不消耗 CPU 资源
102
+ * @param {number} ms - 延迟毫秒数
103
+ */
104
+ function sleepSync(ms: number): void {
105
+ const sharedBuffer = new SharedArrayBuffer(4);
106
+ const int32 = new Int32Array(sharedBuffer);
107
+ Atomics.wait(int32, 0, 0, ms);
108
+ }
109
+
110
+ /**
111
+ * 检测是否应该重试 Git index.lock 错误
112
+ * @param {unknown} error - 捕获的错误对象
113
+ * @param {number} retryCount - 当前已重试次数
114
+ * @returns {boolean} 是否应重试(是 lock 错误且未超过重试上限)
115
+ */
116
+ export function shouldRetryGitIndexLockError(error: unknown, retryCount: number): boolean {
117
+ const errorMessage = extractFullErrorMessage(error);
118
+ return isGitIndexLockError(errorMessage) && retryCount < GIT_INDEX_LOCK_RETRY.MAX_RETRIES;
119
+ }
120
+
121
+ /**
122
+ * 等待 index.lock 重试延迟(同步版本)
123
+ * 打印提示信息并等待指定时间
124
+ */
125
+ export function waitForGitIndexLockRetrySync(): void {
126
+ process.stderr.write(`${MESSAGES.GIT_INDEX_LOCK_RETRYING}\n`);
127
+ sleepSync(GIT_INDEX_LOCK_RETRY.DELAY_MS);
128
+ }
@@ -2,7 +2,7 @@ import { execSync, execFileSync, spawn, spawnSync, type ChildProcess, type Spawn
2
2
  import { logger } from '../logger/index.js';
3
3
  import { EXEC_MAX_BUFFER } from '../constants/git.js';
4
4
  import { CLAUDE_CODE_ENTRYPOINT_VALUE } from '../constants/index.js';
5
- import { throwIfGitIndexLockError } from './git-lock.js';
5
+ import { throwIfGitIndexLockError, shouldRetryGitIndexLockError, waitForGitIndexLockRetrySync } from './git-lock.js';
6
6
 
7
7
  /**
8
8
  * 获取移除了 CLAUDECODE 嵌套会话标记的环境变量副本,并注入 CLAUDE_CODE_ENTRYPOINT 标识
@@ -45,26 +45,39 @@ export interface ParallelCommandResultWithStderr extends ParallelCommandResult {
45
45
 
46
46
  /**
47
47
  * 同步执行 shell 命令并返回 stdout
48
+ * 当检测到 Git index.lock 错误时,会自动重试一次
48
49
  * @param {string} command - 要执行的命令
49
50
  * @param {object} options - 可选配置
50
51
  * @param {string} options.cwd - 工作目录
51
52
  * @returns {string} 命令的标准输出(已 trim)
52
- * @throws {ClawtError} 检测到 index.lock 错误时抛出中文友好提示
53
+ * @throws {ClawtError} 检测到 index.lock 错误且重试失败时抛出中文友好提示
53
54
  * @throws {Error} 其他命令执行失败时抛出
54
55
  */
55
56
  export function execCommand(command: string, options?: { cwd?: string }): string {
56
57
  logger.debug(`执行命令: ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`);
57
- try {
58
- const result = execSync(command, {
59
- cwd: options?.cwd,
60
- encoding: 'utf-8',
61
- stdio: ['pipe', 'pipe', 'pipe'],
62
- maxBuffer: EXEC_MAX_BUFFER,
63
- });
64
- return result.trim();
65
- } catch (error) {
66
- throwIfGitIndexLockError(error, options?.cwd);
67
- throw error;
58
+
59
+ let retryCount = 0;
60
+
61
+ while (true) {
62
+ try {
63
+ const result = execSync(command, {
64
+ cwd: options?.cwd,
65
+ encoding: 'utf-8',
66
+ stdio: ['pipe', 'pipe', 'pipe'],
67
+ maxBuffer: EXEC_MAX_BUFFER,
68
+ });
69
+ return result.trim();
70
+ } catch (error) {
71
+ if (shouldRetryGitIndexLockError(error, retryCount)) {
72
+ retryCount++;
73
+ logger.debug(`检测到 index.lock 错误,第 ${retryCount} 次重试`);
74
+ waitForGitIndexLockRetrySync();
75
+ continue;
76
+ }
77
+
78
+ throwIfGitIndexLockError(error, options?.cwd);
79
+ throw error;
80
+ }
68
81
  }
69
82
  }
70
83
 
@@ -104,29 +117,42 @@ export function killAllChildProcesses(children: ChildProcess[]): void {
104
117
 
105
118
  /**
106
119
  * 同步执行命令,通过 stdin 传入数据
120
+ * 当检测到 Git index.lock 错误时,会自动重试一次
107
121
  * @param {string} command - 要执行的命令
108
122
  * @param {string[]} args - 命令参数
109
123
  * @param {object} options - 配置
110
124
  * @param {Buffer} options.input - 通过 stdin 传入的数据(Buffer 格式,保留二进制完整性)
111
125
  * @param {string} [options.cwd] - 工作目录
112
126
  * @returns {string} 命令的标准输出(已 trim)
113
- * @throws {ClawtError} 检测到 index.lock 错误时抛出中文友好提示
127
+ * @throws {ClawtError} 检测到 index.lock 错误且重试失败时抛出中文友好提示
114
128
  * @throws {Error} 其他命令执行失败时抛出
115
129
  */
116
130
  export function execCommandWithInput(command: string, args: string[], options: { input: Buffer; cwd?: string }): string {
117
131
  logger.debug(`执行命令(stdin): ${command} ${args.join(' ')}${options.cwd ? ` (cwd: ${options.cwd})` : ''}`);
118
- try {
119
- const result = execFileSync(command, args, {
120
- cwd: options.cwd,
121
- input: options.input,
122
- encoding: 'utf-8',
123
- stdio: ['pipe', 'pipe', 'pipe'],
124
- maxBuffer: EXEC_MAX_BUFFER,
125
- });
126
- return result.trim();
127
- } catch (error) {
128
- throwIfGitIndexLockError(error, options.cwd);
129
- throw error;
132
+
133
+ let retryCount = 0;
134
+
135
+ while (true) {
136
+ try {
137
+ const result = execFileSync(command, args, {
138
+ cwd: options.cwd,
139
+ input: options.input,
140
+ encoding: 'utf-8',
141
+ stdio: ['pipe', 'pipe', 'pipe'],
142
+ maxBuffer: EXEC_MAX_BUFFER,
143
+ });
144
+ return result.trim();
145
+ } catch (error) {
146
+ if (shouldRetryGitIndexLockError(error, retryCount)) {
147
+ retryCount++;
148
+ logger.debug(`检测到 index.lock 错误,第 ${retryCount} 次重试`);
149
+ waitForGitIndexLockRetrySync();
150
+ continue;
151
+ }
152
+
153
+ throwIfGitIndexLockError(error, options.cwd);
154
+ throw error;
155
+ }
130
156
  }
131
157
  }
132
158
 
@@ -24,11 +24,19 @@ vi.mock('../../../src/errors/index.js', () => ({
24
24
  vi.mock('../../../src/constants/index.js', () => ({
25
25
  MESSAGES: {
26
26
  GIT_INDEX_LOCKED: (lockFilePath: string) => `Git index 被锁定,锁文件路径:${lockFilePath}`,
27
+ GIT_INDEX_LOCK_RETRYING: 'Git index 被锁定,正在重试...',
27
28
  },
28
29
  }));
29
30
 
30
31
  import { execSync } from 'node:child_process';
31
- import { isGitIndexLockError, findGitIndexLockPath, throwIfGitIndexLockError } from '../../../src/utils/git-lock.js';
32
+ import {
33
+ isGitIndexLockError,
34
+ findGitIndexLockPath,
35
+ throwIfGitIndexLockError,
36
+ shouldRetryGitIndexLockError,
37
+ waitForGitIndexLockRetrySync,
38
+ extractFullErrorMessage,
39
+ } from '../../../src/utils/git-lock.js';
32
40
 
33
41
  const mockedExecSync = vi.mocked(execSync);
34
42
 
@@ -159,3 +167,79 @@ describe('throwIfGitIndexLockError', () => {
159
167
  expect(() => throwIfGitIndexLockError(error)).not.toThrow();
160
168
  });
161
169
  });
170
+
171
+ describe('extractFullErrorMessage', () => {
172
+ it('从 Error 对象提取 message', () => {
173
+ const error = new Error('test error');
174
+ expect(extractFullErrorMessage(error)).toBe('test error');
175
+ });
176
+
177
+ it('合并 message 和 stderr', () => {
178
+ const error = new Error('Command failed');
179
+ (error as any).stderr = 'fatal: Unable to write index.';
180
+ expect(extractFullErrorMessage(error)).toBe('Command failed\nfatal: Unable to write index.');
181
+ });
182
+
183
+ it('处理 Buffer 类型的 stderr', () => {
184
+ const error = new Error('Command failed');
185
+ (error as any).stderr = Buffer.from('fatal: Unable to write index.');
186
+ expect(extractFullErrorMessage(error)).toBe('Command failed\nfatal: Unable to write index.');
187
+ });
188
+
189
+ it('非 Error 对象返回 String 转换结果', () => {
190
+ expect(extractFullErrorMessage('string error')).toBe('string error');
191
+ expect(extractFullErrorMessage(123)).toBe('123');
192
+ });
193
+ });
194
+
195
+ describe('shouldRetryGitIndexLockError', () => {
196
+ it('index.lock 错误且未达到重试上限时返回 true', () => {
197
+ const error = new Error("fatal: Unable to create '/repo/.git/index.lock': File exists.");
198
+ expect(shouldRetryGitIndexLockError(error, 0)).toBe(true);
199
+ });
200
+
201
+ it('index.lock 错误但已达到重试上限时返回 false', () => {
202
+ const error = new Error("fatal: Unable to create '/repo/.git/index.lock': File exists.");
203
+ // MAX_RETRIES 默认为 1,所以 retryCount = 1 时不应再重试
204
+ expect(shouldRetryGitIndexLockError(error, 1)).toBe(false);
205
+ });
206
+
207
+ it('非 index.lock 错误返回 false', () => {
208
+ const error = new Error('Command failed: git merge feature');
209
+ expect(shouldRetryGitIndexLockError(error, 0)).toBe(false);
210
+ });
211
+
212
+ it('从 stderr 检测 index.lock 错误', () => {
213
+ const error = new Error('Command failed');
214
+ (error as any).stderr = "fatal: Unable to create '/repo/.git/index.lock': File exists.";
215
+ expect(shouldRetryGitIndexLockError(error, 0)).toBe(true);
216
+ });
217
+ });
218
+
219
+ describe('waitForGitIndexLockRetrySync', () => {
220
+ it('向 stderr 输出重试提示', () => {
221
+ const stderrWriteSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
222
+
223
+ // 执行函数(会有短暂延迟)
224
+ waitForGitIndexLockRetrySync();
225
+
226
+ // 验证输出了重试提示
227
+ expect(stderrWriteSpy).toHaveBeenCalledWith('Git index 被锁定,正在重试...\n');
228
+
229
+ stderrWriteSpy.mockRestore();
230
+ });
231
+
232
+ it('执行时间接近配置的延迟时间', () => {
233
+ const stderrWriteSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
234
+
235
+ const startTime = Date.now();
236
+ waitForGitIndexLockRetrySync();
237
+ const elapsed = Date.now() - startTime;
238
+
239
+ // 延迟时间应该接近 150ms(允许 50ms 误差)
240
+ expect(elapsed).toBeGreaterThanOrEqual(100);
241
+ expect(elapsed).toBeLessThanOrEqual(300);
242
+
243
+ stderrWriteSpy.mockRestore();
244
+ });
245
+ });