mocode-ai 1.6.9 → 1.7.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.
@@ -42,94 +42,135 @@ CLI dump 一般是 \`path/to/file.ts:line:col symbol\` 形式,与 grep 一样
42
42
  `;
43
43
  const BRAINSTORM_BODY = `# Brainstorming(需求澄清)
44
44
 
45
- 开工前的需求澄清流程:当需求可能被理解错、或信息不足以决定交付物时,先用对话把需求问清楚,再动手。
45
+ 开工前的需求澄清:当需求可能被理解错、信息不足或只给了粗略想法时,先用对话把需求收敛成用户认可的设计,再动手。
46
46
 
47
- ## 何时进入
47
+ ## 铁律
48
48
 
49
- - 用户的需求存在两种以上实质不同的理解,会导致不同交付物;
50
- - 缺少关键信息(目标、范围、约束、输入输出、验收标准),且靠仓库证据无法补全;
51
- - 改动涉及不可逆操作 / 公共 API / 多个会实质改变产品行为的方案;
52
- - 用户只给了粗略想法(如「帮我做个 X」),期望先讨论。
49
+ 开始写产品代码之前,必须完成所选路径对应的确认门。**批准是分阶段的**:用户认可想法 ≠ 批准写代码;对方案的口头认可只推进到下一步,不越级。
53
50
 
54
- ## 何时不要进入
51
+ ## 第一步:分级(说出来,让用户可纠正)
55
52
 
56
- - 任务清晰且存在合理默认做法 → 直接干,在最终回复中披露假设;
57
- - 只是命名或实现细节 → 遵循仓库惯例自主决定;
58
- - 用户已明确表示「直接做 / 别问我」。
53
+ 对需求先分类并明说——「这个需求看起来是有界改动,我会先在聊天里给短设计」:
59
54
 
60
- ## 流程
55
+ - **探针(Spike)**:可行性问题(「能不能…」「先快速试试」),产出是结论不是保留的代码。2-3 句说清要验证什么,口头确认即可动手;产出物明确标注一次性。
56
+ - **有界(Bounded)**:改动仓库里**已存在**的流程(新 flag、小端点、单文件修复)。问关键问题后,在聊天里给出几段短设计,停下等确认;不写 spec 文件。
57
+ - **架构级(Architectural)**:新项目、新子系统、改变组件间接口。走完整流程:澄清问题 → 分节设计 → 落盘 spec 文档 → 用户审阅 spec → 再谈实施计划。
58
+
59
+ 判断不了时取更重的级别。**棘轮单向**:任务中途发现隐藏复杂度 → 停下明说,升级路径;永不中途降级。
60
+
61
+ ## 澄清阶段怎么问
62
+
63
+ 1. **最小探查**:只读提出好问题所必需的代码(有 .codegraph/ 时优先 codegraph skill),不做大范围调研。
64
+ 2. **复述理解**:一两句复述你对需求的理解,区分「用户说的」和「我假设的」,邀请纠正。
65
+ 3. **一次一个问题**:每轮 ask_human 只问当前最关键的一个,自由文本优先(options: []);仅当答案能收敛为少量明确选项时才给 2-4 项。回答引出新歧义时先澄清新歧义,不重复问已回答的。
66
+
67
+ ## 收敛确认
61
68
 
62
- 1. **最小探查**:只读取提出好问题所必需的代码和上下文(有 .codegraph/ 时优先 codegraph skill),不要在澄清阶段做大范围调研。
63
- 2. **复述理解**:用一两句向用户复述你对需求的理解,并点出关键分歧或未知点。
64
- 3. **一次只问一个问题**:每轮 \`ask_human\` 只问当前最关键的一个问题,自由文本优先(\`options: []\`);仅当答案能收敛为少量明确选项时才给 2-4 个选项。不要一次甩多个问题。
65
- 4. **据回答继续**:必要时追问下一个最关键的问题;回答引出新歧义时先澄清新歧义。不要重复问已回答的内容。
66
- 5. **收敛确认**:需求清晰后,用一次 \`ask_human\` 分节呈现方案——目标 / 范围 / 做法与受影响文件 / 不做什么 / 验证方式,选项固定为「确认,开始做」「再调整」「取消」。
67
- 6. **转入执行**:用户确认后再开始改动。
69
+ 需求清晰后,用一次 ask_human 分节呈现方案——目标 / 范围 / 做法与受影响文件 / 不做什么 / 验证方式,选项固定为「确认,开始做」「再调整」「取消」。
70
+
71
+ **呈现前自查一遍(静默)**:有没有 TBD 或模糊措辞?各节是否互相矛盾?有没有可双解的需求点(有则当场选定并写明)?修完再呈现,自查过程不必展示。
68
72
 
69
73
  ## 边界
70
74
 
71
- - 澄清阶段的 \`ask_human\` 不计入「执行阶段每轮最多 2 次」的预算,但仍要克制:每个问题都必须实质推进理解。用户表现出不耐烦或说「你决定」时,采用最安全的可逆默认做法并说明。
75
+ - 澄清期 ask_human 不计入「执行阶段每轮最多 2 次」的预算,但每个问题必须实质推进理解;用户不耐烦或说「你决定」时,取最安全的可逆默认并说明。
72
76
  - 用户取消:停止,不要按自己的猜测继续。
73
- - 问题与方案用用户的语言表述。
77
+ - 问题与方案用用户的语言。
74
78
  - 澄清期间保持只读:不改文件、不跑有副作用的命令。
75
79
  `;
76
80
  const DEBUGGING_BODY = `# Systematic Debugging(系统化调试)
77
81
 
78
- 遇到 bug、测试失败或报错时,用「先理解再修」的流程,禁止靠猜测随机改动。
82
+ 遇到 bug、测试失败或报错时,用「先理解再修」的流程。**违反本流程的字面,就是违反调试的精神。**
83
+
84
+ ## 铁律
85
+
86
+ > 未定位根因,不得提出修复。
79
87
 
80
- ## 核心原则
88
+ 症状不是原因。「让报错消失」和「修好」是两回事——说不清为什么有效,就只是掩盖。
81
89
 
82
- - 没有找到根因之前,不要改代码。症状不是原因。
83
- - 一次只改一处;改完无法解释为什么有效,就不算修好。
84
- - 不要用「删掉重试 / 换个写法试试 / 加个 await 碰碰运气」这类无假设的尝试。
90
+ ## 最容易跳过的时刻(恰恰必须用本流程)
91
+
92
+ - 时间紧、线上着急、「就改一行」;
93
+ - 已经连试 2 次以上修复仍失败——这不是「再试一次」的信号,是前提或架构有问题的信号,回到第 1 步质疑假设;
94
+ - 感觉「我大概知道问题在哪」;
95
+ - 报错看起来很简单——简单 bug 也有根因。
85
96
 
86
97
  ## 流程
87
98
 
88
- 1. **复现**:先稳定复现问题。复现不了,先弄清楚触发条件(输入、环境、顺序),必要时向用户索取复现步骤或报错全文。
89
- 2. **收集证据**:读完整报错和堆栈,定位到具体文件/行;用日志或最小用例确认数据在**哪一步**开始出错。有 .codegraph/ 时用它追调用链。
90
- 3. **形成根因假设**:用一句话说明「为什么当前行为会必然产生这个症状」,并指出能证伪它的检查点。
91
- 4. **验证假设**:先做最小检查确认假设成立,再动手。如果证据与假设矛盾,回到第 2 步,不要硬修。
92
- 5. **最小修复**:只改根因,不动无关代码;避免为绕过症状而加补丁、吞异常、放宽校验。
93
- 6. **验证修复**:复现用例通过;补一个能覆盖该 bug 的回归测试(成本合理时);检查同类路径是否也受影响。
99
+ 1. **复现**:先稳定复现。复现不了就先弄清触发条件(输入、环境、顺序),必要时向用户索取复现步骤或报错全文。
100
+ 2. **收集证据**:读完整报错和堆栈,定位到具体文件/行;用日志或最小用例确认数据在**哪一步**开始出错。多组件系统(如 CI→构建→签名、API→DB)先在组件边界加诊断输出,跑一次看断在哪一层,再深入该层。有 .codegraph/ 时用它追调用链。
101
+ 3. **形成单一假设**:一句话说清「X 是根因,因为 Y」,并指出能证伪它的检查点。同时只验证这一个假设,不许一次改多处。
102
+ 4. **最小验证**:先做最小检查确认假设成立;证据与假设矛盾 → 回到第 2 步,不要硬修。承认「我没看懂这段」比假装看懂更好。
103
+ 5. **最小修复**:只改根因,不动无关代码;不加吞异常、放宽校验这类绕症状的补丁。
104
+ 6. **回归验证**:复现用例通过;按红绿法补回归测试(见 verification-before-completion skill);检查同类路径是否同样受影响。
94
105
  7. **复盘**:说明根因、改动、验证方式;仍有不确定就如实说。
95
106
 
96
- ## 反模式
107
+ ## 合理化借口对照
108
+
109
+ | 借口 | 现实 |
110
+ |------|------|
111
+ | 「问题很简单,不用走流程」 | 简单问题也有根因;流程对简单问题很快 |
112
+ | 「紧急,没时间走流程」 | 系统化比反复乱改快 |
113
+ | 「先随手修一下再查原因」 | 第一次随手修会定下错误方向 |
114
+ | 「多改几处一起试,省时间」 | 无法隔离哪个改动起效,还引入新变量 |
115
+ | 「我知道问题在哪,直接修」 | 看到症状 ≠ 理解根因 |
116
+ | 「再试一次就好了」 | 3+ 次失败说明该质疑前提,不是再试 |
97
117
 
98
- - 报错还没看完就开始改;
99
- - 同一处反复改了 3 次仍失败却不换思路;
100
- - 修复让错误「消失」但说不清机制(往往只是被掩盖);
101
- - 顺手重构与 bug 无关的代码。
118
+ 如果系统化调查后确认问题确实源于环境/时序/外部依赖、没有代码层根因:记录调查过程,实现恰当处理(重试、超时、报错信息)——这本身是完成流程,不是失败。
102
119
  `;
103
120
  const CODE_REVIEW_BODY = `# Code Review(自审 diff)
104
121
 
105
122
  提交/收尾前,对**自己本次的改动**做一遍审查。改动越多越要做;几行的机械改动可快速过。
106
123
 
107
- ## 流程
124
+ ## 独立视角优先
125
+
126
+ 自审最大的敌人是实现思维惯性——评估和实现在同一个上下文里互相污染。**若工具面可用 sub-agent,优先派子代理审查**:给它精心构造的上下文(任务目标、验收要求、diff 范围),不要给它你的会话历史;只收回结论。sub-agent 不可用或改动很小时自查,按清单逐项过,不要凭整体印象。
127
+
128
+ ## 清单
108
129
 
109
- 1. **拿到完整 diff**:只审本次相关改动(\`git diff\` / 未提交改动),不要凭记忆。
110
- 2. **意图对齐**:每处改动是否都服务于任务?有没有夹带无关重构、调试残留(console.log、注释掉的代码、临时文件)?
111
- 3. **正确性**:逻辑分支是否完整;边界(空值、空数组、并发/重复调用、错误路径);条件取反、off-by-one;异步是否有未 await 或未处理的 rejection。
112
- 4. **错误处理**:失败时是否给出有用信息而非静默吞掉;资源(文件句柄、锁、子进程)是否正确释放。
113
- 5. **兼容与契约**:公共 API、类型、配置键、默认值是否保持兼容;是否违背仓库既有约定(如纯 ESM 的 .js 扩展名、工具契约)。
114
- 6. **安全与副作用**:是否引入不可逆操作、权限绕过、注入面;写入/执行是否走了既有护栏。
115
- 7. **完整性**:文档、调用方、测试是否需要同步;用户要求的场景是否都覆盖。
116
- 8. **修复后复审**:发现问题就改,改完重新看那部分 diff,确认没有引入新问题。
130
+ 1. **意图对齐**:每处改动是否都服务于任务?有没有夹带无关重构、调试残留(console.log、注释掉的代码、临时文件)?
131
+ 2. **正确性**:逻辑分支是否完整;边界(空值、空数组、并发/重复调用、错误路径);条件取反、off-by-one;异步是否有未 await 或未处理的 rejection。
132
+ 3. **错误处理**:失败时是否给出有用信息而非静默吞掉;资源(文件句柄、锁、子进程)是否正确释放。
133
+ 4. **兼容与契约**:公共 API、类型、配置键、默认值是否保持兼容;是否违背仓库既有约定(如纯 ESM 的 .js 扩展名、工具契约)。
134
+ 5. **安全与副作用**:是否引入不可逆操作、权限绕过、注入面;写入/执行是否走了既有护栏。
135
+ 6. **完整性**:文档、调用方、测试是否需要同步;用户要求的场景是否都覆盖。
117
136
 
118
- ## 输出
137
+ ## 处理与输出
119
138
 
120
- 如实质问题:直接修复,最终回复中列出发现并修复了什么。
121
- 如无问题:简要说明审过哪些点。不要为了凑数报无关紧要的风格问题——格式交给 Prettier。
139
+ - 重要问题:立即修复,修完重看那部分 diff,确认没有引入新问题;
140
+ - 次要问题:记录,不阻塞收尾;
141
+ - 无实质问题:简要说明审过哪些点。
142
+ - 不要为了凑数报风格问题——格式交给 Prettier。
122
143
  `;
123
144
  const VERIFICATION_BODY = `# Verification Before Completion(完成前证据验证)
124
145
 
125
146
  宣布任务「完成」之前,必须用实际运行的证据确认改动真的生效。禁止把「代码看起来对 / 应该能跑」当作完成。
126
147
 
127
- ## 硬性规则
148
+ ## 铁律
149
+
150
+ **没有本次修改之后产生的运行证据,不得做任何完成/成功/通过类声称。**上一次运行的旧结果、部分检查、别的检查碰巧通过,都不算证据。
151
+
152
+ ## 声称 → 必须的证据 → 不算数
153
+
154
+ | 声称 | 必须有 | 不算数 |
155
+ |------|--------|--------|
156
+ | 测试通过 | 测试命令输出 0 failures | 上次运行、「应该通过」 |
157
+ | 构建成功 | build 命令 exit 0 | lint 通过、日志看着没报错 |
158
+ | bug 已修 | 用能复现该 bug 的输入跑过且通过 | 改完代码就当修好 |
159
+ | 回归测试有效 | 红绿循环验证过(见下) | 测试只绿过一次 |
160
+ | 子 agent 完成 | 核查其落盘的改动 | 子 agent 自报 success |
161
+ | 需求全部满足 | 逐条对照用户的验收要求 | 相关测试都绿 |
162
+
163
+ ## 红绿回归验证
164
+
165
+ 补回归测试时,只绿过一次不算数:写测试 → 确认它在修复前的代码上会红(临时回滚修复,或注入原触发输入) → 恢复修复 → 确认转绿。这样才能证明测试测的就是这个 bug,而不是碰巧通过。
166
+
167
+ ## 红旗(出现即停)
128
168
 
129
- 1. **先验证,再宣称完成**:任何「已修复 / 已完成 / 能用了」的结论,前面必须有一次实际执行及其真实输出。
130
- 2. **验证必须对得上声称**:改了行为就跑能体现该行为的场景;修了 bug 就用能复现该 bug 的输入确认;不能用「编译通过」代替「功能正确」。
131
- 3. **看完整结果**:确认退出码、断言/测试通过数、关键输出行;有 warning 或失败项要逐条判断,不能只看最后一行没报错。
132
- 4. **验证不了就如实说**:环境缺失、需要用户凭据/设备、或验证成本明显不合理时,说明已验证到哪一步、什么没验证、用户可以怎么验证。不要假称已验证。
169
+ - 「应该没问题」「大概可以」「看起来对」;
170
+ - 还没跑验证就表达满意(「搞定」「完美」);
171
+ - 引用改动前的旧运行结果;
172
+ - 拿与改动无关的检查冒充验证;
173
+ - 测试失败或被跳过,却报告成功。
133
174
 
134
175
  ## 怎么选验证(按成本,够小即可)
135
176
 
@@ -138,12 +179,9 @@ const VERIFICATION_BODY = `# Verification Before Completion(完成前证据验
138
179
  - UI/交互类改动:能自动化就自动化(浏览器工具/webapp 测试),否则明确告知未做可视化验证;
139
180
  - 纯文档、注释等零行为改动:可不跑验证。
140
181
 
141
- ## 反模式
182
+ ## 验证不了就如实说
142
183
 
143
- - 「理论上没问题」「逻辑上应该可以」——没有运行证据就不算完成;
144
- - 测试其实失败/被跳过,却报告成功;
145
- - 跑了一个与改动无关的检查,用它冒充验证;
146
- - 改完代码不重新运行,沿用改动前的旧结果。
184
+ 环境缺失、需要用户凭据/设备、或验证成本明显不合理时:说明已验证到哪一步、什么没验证、用户可以怎么验证。如实报告未验证项不是失败;假称验证才是。
147
185
  `;
148
186
  /** 单一事实源:加新内置 skill 在此数组加一条。 */
149
187
  export const builtinSkills = [
@@ -175,14 +213,16 @@ export const builtinSkills = [
175
213
  },
176
214
  {
177
215
  name: 'systematic-debugging',
178
- description: '系统化调试:遇到 bug、测试失败或报错时,先稳定复现并定位根因(读完堆栈、用证据验证假设),再做最小修复并回归验证。' +
179
- '禁止没找到根因就靠猜测反复改代码。',
216
+ description: '系统化调试:遇到 bug、测试失败或报错时,在提出任何修复之前先稳定复现并定位根因(读完堆栈、用证据验证假设),' +
217
+ '再做最小修复并回归验证。禁止没找到根因就靠猜测反复改代码。',
180
218
  body: DEBUGGING_BODY,
181
219
  dir: 'builtin',
182
220
  skillMdPath: 'builtin',
183
221
  context: 'inline',
184
222
  modelInvocable: true,
185
223
  origin: 'builtin',
224
+ // debug 需要深度推理:激活期把 effort 提到 high(仅未显式设置时生效)。
225
+ effort: 'high',
186
226
  warnings: [],
187
227
  },
188
228
  {
@@ -52,8 +52,16 @@ export const CAPABILITIES = {
52
52
  // 后台进程与浏览器会话跨调用存活;串行执行避免同一页面/服务被并发操作。
53
53
  dev_server: { effect: 'process', concurrency: 'serial', resources: workspaceResource, supportsAbort: true },
54
54
  browser: { effect: 'process', concurrency: 'serial', resources: workspaceResource },
55
- glob: { effect: 'read', concurrency: 'parallel', resources: workspaceResource, supportsAbort: true },
56
- grep: { effect: 'read', concurrency: 'parallel', resources: workspaceResource, supportsAbort: true },
55
+ // glob/grep 不取任何资源锁:读枚举容忍命令执行期间的瞬时不一致(一次目录扫描的结果
56
+ // 本来就无事务语义),换取「一条 run_command/dev_server 持 workspace 写锁期间,只读
57
+ // 枚举与并行子 agent 的读不被全面压制」。网络只读同理不锁。
58
+ //
59
+ // read_file 不在此列:它取 file:<path> 读锁(读单个**已知**文件,与目录枚举的
60
+ // 瞬时不一致不同层级)。2026-10-05 起 resource-lock 的 requestConflicts 放行了
61
+ // 「workspace 写锁 vs 资源读锁」这一格,所以 run_command 期间 read_file 不再排队;
62
+ // 但 read_file 仍与 write_file/edit_file 的 file 写锁互斥(见 resource-lock.ts 的冲突矩阵)。
63
+ glob: { effect: 'read', concurrency: 'parallel', resources: () => [], supportsAbort: true },
64
+ grep: { effect: 'read', concurrency: 'parallel', resources: () => [], supportsAbort: true },
57
65
  // 网络只读查询:无副作用,可安全重复执行 → idempotent 让 runtime 对瞬时失败(429/5xx/超时/
58
66
  // 网络抖动)自动退避重试,省掉模型「再发一轮 tool call 自救」的往返。写/进程类工具绝不置此位。
59
67
  web_search: { effect: 'network', concurrency: 'parallel', supportsAbort: true, idempotent: true },
@@ -1,4 +1,4 @@
1
- import { spawn, spawnSync } from 'node:child_process';
1
+ import { spawn } from 'node:child_process';
2
2
  import { MAX_OUTPUT } from '../constants.js';
3
3
  import { getSandboxRoot, filterEnv, isCommandDenied, jailResolve } from '../../sandbox/index.js';
4
4
  import { t } from '../../i18n/index.js';
@@ -77,12 +77,22 @@ export async function runCommandRaw(command, timeout = DEFAULT_COMMAND_TIMEOUT_M
77
77
  });
78
78
  const output = new BoundedCommandOutput();
79
79
  let finished = false;
80
- const killTree = () => {
80
+ // 异步树杀:spawnSync('taskkill') 会同步阻塞整个事件循环(扫进程树期间流式渲染、
81
+ // 其它并行工具全部停摆)。taskkill 自身是独立进程,发出去后等它退出即可;
82
+ // 只在 abort/timeout 路径调用,正常退出(close)不杀。
83
+ const killTree = async () => {
81
84
  try {
82
85
  if (isWin) {
83
- if (child.pid != null) {
84
- spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
85
- }
86
+ if (child.pid == null)
87
+ return;
88
+ await new Promise((resolve) => {
89
+ const killer = spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], {
90
+ stdio: 'ignore',
91
+ windowsHide: true,
92
+ });
93
+ killer.on('close', () => resolve());
94
+ killer.on('error', () => resolve());
95
+ });
86
96
  }
87
97
  else {
88
98
  child.kill('SIGTERM');
@@ -100,23 +110,35 @@ export async function runCommandRaw(command, timeout = DEFAULT_COMMAND_TIMEOUT_M
100
110
  signal?.removeEventListener('abort', onAbort);
101
111
  done({ ...result, durationMs: Date.now() - startedAt });
102
112
  };
113
+ // 树杀(异步)进行中:child 的 close 事件可能由 taskkill 触发并抢在 finish 前,
114
+ // 必须忽略之,否则 abort/timeout 会被误报成 failed(进程被杀的 exitCode ≠ 0)。
115
+ let pendingTermination = null;
116
+ const terminate = (status) => {
117
+ if (finished || pendingTermination)
118
+ return;
119
+ pendingTermination = status;
120
+ void killTree().then(() => {
121
+ finish({ status, exitCode: null, output: output.render().trim() });
122
+ });
123
+ };
103
124
  const onAbort = () => {
104
- killTree();
105
- finish({ status: 'aborted', exitCode: null, output: output.render().trim() });
125
+ terminate('aborted');
106
126
  };
107
127
  const onChunk = (chunk) => output.append(chunk.toString('utf8'));
108
128
  // 先起 timer 再挂事件:finish/onAbort 是闭包,里面要 clearTimeout(timer)。
109
129
  // 把 timer 的初始化提到所有引用它的注册点之前,避免依赖"事件回调必然异步"这一前提。
110
130
  const timer = setTimeout(() => {
111
- killTree();
112
- finish({ status: 'timed_out', exitCode: null, output: output.render().trim() });
131
+ terminate('timed_out');
113
132
  }, effectiveTimeout);
114
133
  child.stdout.on('data', onChunk);
115
134
  child.stderr.on('data', onChunk);
116
135
  child.on('error', (error) => {
117
- finish({ status: 'spawn_error', exitCode: null, output: error.message });
136
+ if (!pendingTermination)
137
+ finish({ status: 'spawn_error', exitCode: null, output: error.message });
118
138
  });
119
139
  child.on('close', (code) => {
140
+ if (pendingTermination)
141
+ return;
120
142
  finish({
121
143
  status: code === 0 ? 'passed' : 'failed',
122
144
  exitCode: code,
@@ -56,7 +56,7 @@ function addToolGroupsSchema(groups) {
56
56
  type: 'function',
57
57
  function: {
58
58
  name: ADD_TOOL_GROUPS_TOOL_NAME,
59
- description: 'Expand the tool surface for this turn when the current tools are insufficient. Call this tool ALONE, before any dependent tool call. Added groups become available on the next model step and cannot be removed during the turn.',
59
+ description: 'Expand the tool surface for this turn when the current tools are insufficient. Call it alone or batched with read-only tools (read_file/glob/grep/web_search/web_fetch): the read-only calls run in the same step and the new groups take effect on the next model step. Write/execute tools batched with it are skipped and must be retried on the next step, so keep calls that depend on the newly added groups for the next step. Groups cannot be removed during the turn.',
60
60
  parameters: {
61
61
  type: 'object',
62
62
  properties: {
@@ -237,7 +237,7 @@ export class ToolPolicyController {
237
237
  '## Tool route (current turn)',
238
238
  `Policy ${snapshot.id} v${snapshot.version}; active groups: ${active}.`,
239
239
  `Router reason: ${snapshot.reason}`,
240
- 'Use only the exposed tools. Missing capability? Call add_tool_groups — alone, or batched with read-only tools (read_file/glob/grep/web_search/web_fetch); the read-only calls run in that same step and the new groups take effect on the next step. Do not batch it with write/execute tools, and keep calls that depend on the newly added groups in the next step.',
240
+ 'Use only the exposed tools. Missing capability? Call add_tool_groups — alone, or batched with read-only tools (read_file/glob/grep/web_search/web_fetch); the read-only calls run in that same step and the new groups take effect on the next step. Write/execute tools batched with it are skipped (retry them on the next step), and calls that depend on the newly added groups belong in the next step.',
241
241
  ];
242
242
  if (remaining.length)
243
243
  lines.push(`Groups still available: ${remaining.join(', ')}.`);
@@ -5,11 +5,48 @@ function abortError() {
5
5
  error.name = 'AbortError';
6
6
  return error;
7
7
  }
8
+ /**
9
+ * 两个锁请求是否冲突。
10
+ *
11
+ * 冲突矩阵(workspace 锁 vs 资源锁):
12
+ *
13
+ * |左\右| workspace-write | resource-read | resource-write |
14
+ * |------|-----------------|--------------|----------------|
15
+ * | ws-write |冲突(命令互斥) | **不冲突** | 冲突(防命令与 Agent 写同文件) |
16
+ * | ws-read | 不冲突 | 不冲突 | 冲突 |
17
+ * | res-read | 不冲突 | 同 key 不冲突 | 冲突 |
18
+ *
19
+ * **唯一放行的是「workspace 写锁 vs 资源读锁」**(表中加粗那格)。这条正是
20
+ * 「一条 run_command 期间所有 read_file 排队」的根因:run_command 声明
21
+ * `resources: () => ['workspace']` + `effect: 'process'` → resolveResourceLockRequests
22
+ * 恒返回 workspace 写锁(resource-lock.ts 的 workspaceWrite()),而 read_file 持
23
+ * `file:<path>` 读锁,旧判据「任一 write 即冲突」把两者判成互斥 —— 一条 60s 的
24
+ * `npm test` 会让同 step 的所有 read_file 全部阻塞排队,并行探查退化为串行。
25
+ *
26
+ * 放行的依据与 glob/grep 的 `resources: () => []` 豁免同源(见 builtins/index.ts 的
27
+ * CAPABILITIES 注释「读枚举容忍命令执行期间的瞬时不一致」):run_command **不通过
28
+ * ChangeSet 写文件**,它的工作区写入对 read_file 不构成事务性冲突 —— read_file 读的是
29
+ * 单个已知文件,与命令的写入在绝大多数情况下无关。若命令恰好在改这个文件,读到不一致
30
+ * 内容的后果由既有机制兜住:read_file 的输出带 content hash 记入 artifact,后续
31
+ * refreshArtifactFreshness / invalidateArtifacts 会把它标 stale。
32
+ *
33
+ * **保持冲突的边界(不可放宽)**:
34
+ * - workspace-write vs resource-write:run_command 与 write_file/edit_file 改同一文件
35
+ * 必须互斥,否则 ChangeSet 的 read-modify-write 会读到命令的半成品。
36
+ * - workspace-write vs workspace-write:命令之间互斥(文件句柄 / 端口 / 环境变量共享)。
37
+ */
8
38
  function requestConflicts(a, b) {
9
39
  if (a.scope === 'workspace' || b.scope === 'workspace') {
10
- if (a.mode === 'write' || b.mode === 'write')
11
- return true;
12
- return false;
40
+ // 同为 workspace 锁(实践中只有 write):沿用「任一 write 即冲突」。
41
+ if (a.scope === 'workspace' && b.scope === 'workspace') {
42
+ return a.mode === 'write' || b.mode === 'write';
43
+ }
44
+ const workspaceReq = a.scope === 'workspace' ? a : b;
45
+ const resourceReq = a.scope === 'workspace' ? b : a;
46
+ // 放行格:workspace 写锁(命令)与资源读锁(读单个已知文件)不冲突。
47
+ if (workspaceReq.mode === 'write' && resourceReq.mode === 'read')
48
+ return false;
49
+ return workspaceReq.mode === 'write' || resourceReq.mode === 'write';
13
50
  }
14
51
  return a.key === b.key && (a.mode === 'write' || b.mode === 'write');
15
52
  }
@@ -122,7 +159,11 @@ export function resolveResourceLockRequests(capabilities, args) {
122
159
  return workspaceWrite();
123
160
  }
124
161
  if (keys.length === 0) {
125
- return capabilities.effect === 'network' ? [] : workspaceWrite();
162
+ // 显式声明「无资源」(glob/grep 等读枚举、网络只读)即不锁:
163
+ // 读枚举容忍命令执行期间的瞬时不一致,换取 workspace 写锁期间的读不被压制。
164
+ if (capabilities.effect === 'network' || capabilities.effect === 'read')
165
+ return [];
166
+ return workspaceWrite();
126
167
  }
127
168
  const mode = modeFor(capabilities.effect);
128
169
  const requests = [];
@@ -32,10 +32,18 @@ export function resolveRgPath() {
32
32
  const probe = spawnSync(isWin ? 'rg.exe' : 'rg', ['--version'], { stdio: 'ignore', windowsHide: true });
33
33
  return probe.status === 0 ? (isWin ? 'rg.exe' : 'rg') : null;
34
34
  }
35
+ // 异步树杀:spawnSync('taskkill') 会同步阻塞事件循环(扫树期间流式渲染/并行工具停摆)。
36
+ // 两处调用都是 fire-and-forget(结果由 close 事件收口,finish 有 settled 守卫)。
35
37
  function killTree(child) {
36
38
  try {
37
39
  if (isWin && child.pid != null) {
38
- spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
40
+ const killer = spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], {
41
+ stdio: 'ignore',
42
+ windowsHide: true,
43
+ });
44
+ killer.on('error', () => {
45
+ // 进程已退出或终止失败,尽力而为。
46
+ });
39
47
  }
40
48
  else {
41
49
  child.kill('SIGTERM');
package/dist/ui/batch.js CHANGED
@@ -519,10 +519,11 @@ const CARET_NONE = ' ';
519
519
  * 曾为 4 空格——摘要行(`◆ 探索 …`)与工具调用行之间留白过大,工具名被推得过右(用户实测反馈);
520
520
  * 子批再叠 8 空格后层级被拉散。收成 2 空格后每层固定递进 2 列,层级仍清晰但不空旷。 */
521
521
  const ENTRY_INDENT = ' ';
522
- /** 子 agent 批摘要行的缩进(= 两层 entry 缩进)。
523
- * 子批摘要挂在父批的 entry 行之下,是第二层;硬编码会与 ENTRY_INDENT 脱钩,
524
- * 故由 ENTRY_INDENT 推得。 */
525
- export const SUB_BATCH_INDENT = ENTRY_INDENT + ENTRY_INDENT;
522
+ /** 子 agent 批摘要行的缩进(= 三层 entry 缩进)。
523
+ * 子批摘要挂在父批的 entry 行之下,entry 行本身已带 2 空格;此前取两层(4)时子批
524
+ * 摘要与父 entry 行仅差 2 列,视觉上挤在同一层级(用户实测反馈)。取三层(6)后,
525
+ * 子批摘要比父 entry 行深 4 列,父子层级一眼可分;仍由 ENTRY_INDENT 推导,不脱钩。 */
526
+ export const SUB_BATCH_INDENT = ENTRY_INDENT + ENTRY_INDENT + ENTRY_INDENT;
526
527
  /** 详情行缩进:5 空格(对齐 entry 行的 " ▸ " 之后再右移一列,形成层次)。 */
527
528
  const DETAIL_INDENT = ' ';
528
529
  /**