minimal-agent 0.12.0 → 0.13.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.
Files changed (74) hide show
  1. package/package.json +3 -2
  2. package/plugins/workflow-runner/src/expressions.js +29 -8
  3. package/plugins/workflow-runner/src/expressions.ts +28 -10
  4. package/plugins/workflow-runner/src/index.js +45 -17
  5. package/plugins/workflow-runner/src/index.ts +44 -18
  6. package/plugins/workflow-runner/src/recovery.js +9 -2
  7. package/plugins/workflow-runner/src/recovery.ts +9 -2
  8. package/plugins/workflow-runner/src/runner.js +31 -5
  9. package/plugins/workflow-runner/src/runner.ts +33 -5
  10. package/plugins/workflow-runner/src/stepExecutors/assert.js +2 -1
  11. package/plugins/workflow-runner/src/stepExecutors/assert.ts +3 -1
  12. package/plugins/workflow-runner/src/stepExecutors/tool.js +29 -10
  13. package/plugins/workflow-runner/src/stepExecutors/tool.ts +29 -11
  14. package/plugins/workflow-runner/src/stepExecutors/verify.js +6 -3
  15. package/plugins/workflow-runner/src/stepExecutors/verify.ts +7 -3
  16. package/plugins/workflow-runner/src/taskIdentity.js +197 -21
  17. package/plugins/workflow-runner/src/taskIdentity.ts +201 -20
  18. package/plugins/workflow-runner/src/types.js +113 -7
  19. package/plugins/workflow-runner/src/types.ts +111 -7
  20. package/plugins/workflow-runner/src/workflowState.js +455 -54
  21. package/plugins/workflow-runner/src/workflowState.ts +508 -63
  22. package/plugins/workflow-runner/test/cli.e2e.test.ts +20 -0
  23. package/plugins/workflow-runner/test/e2eNewLayout.test.ts +314 -0
  24. package/plugins/workflow-runner/test/engineRobustness.test.ts +9 -1
  25. package/plugins/workflow-runner/test/expressions.test.ts +42 -0
  26. package/plugins/workflow-runner/test/illustrate.load.test.ts +2 -2
  27. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +19 -0
  28. package/plugins/workflow-runner/test/ppt.load.test.ts +15 -15
  29. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +3 -2
  30. package/plugins/workflow-runner/test/research.load.test.ts +328 -0
  31. package/plugins/workflow-runner/test/runnerPreflight.test.ts +68 -11
  32. package/plugins/workflow-runner/test/stateLifecycle.test.ts +60 -12
  33. package/plugins/workflow-runner/test/taskArbitration.test.ts +19 -3
  34. package/plugins/workflow-runner/test/taskLifecycle.test.ts +61 -7
  35. package/plugins/workflow-runner/test/webppt.load.test.ts +4 -4
  36. package/plugins/workflow-runner/test/word.load.test.ts +9 -9
  37. package/skills/skill-creator/scripts/run_eval.py +3 -2
  38. package/src/cli/print.js +17 -0
  39. package/src/cli/print.ts +25 -0
  40. package/src/config/configService.js +24 -0
  41. package/src/config/configService.ts +23 -0
  42. package/src/infra/verification.js +25 -18
  43. package/src/infra/verification.ts +50 -18
  44. package/src/plugins/pluginRunner.js +18 -7
  45. package/src/plugins/pluginRunner.ts +20 -6
  46. package/src/prompts/policyRegistry.js +28 -1
  47. package/src/prompts/policyRegistry.ts +28 -1
  48. package/src/tools/advisor/advisor.js +76 -9
  49. package/src/tools/advisor/advisor.ts +79 -9
  50. package/src/tools/bash/bash.js +51 -6
  51. package/src/tools/bash/bash.ts +56 -7
  52. package/src/tools/powershell/powershell.js +18 -1
  53. package/src/tools/powershell/powershell.ts +18 -2
  54. package/src/tools/read/read.js +9 -4
  55. package/src/tools/read/read.ts +9 -4
  56. package/src/tools/shared/permanentDeleteGuard.js +34 -3
  57. package/src/tools/shared/permanentDeleteGuard.ts +43 -6
  58. package/src/tools/webbrowser/browserControl.js +5 -1
  59. package/src/tools/webbrowser/browserControl.ts +7 -1
  60. package/src/tools/webbrowser/webbrowser.js +8 -4
  61. package/src/tools/webbrowser/webbrowser.ts +8 -4
  62. package/src/types.ts +10 -4
  63. package/workflows/adaptive-research.yaml +5 -5
  64. package/workflows/book-review-short.yaml +5 -5
  65. package/workflows/e2e-write-greet.yaml +2 -2
  66. package/workflows/illustrate.yaml +12 -12
  67. package/workflows/plan.yaml +18 -18
  68. package/workflows/ppt.yaml +201 -201
  69. package/workflows/ralph-loop.yaml +3 -1
  70. package/workflows/research.md +449 -0
  71. package/workflows/research.yaml +873 -0
  72. package/workflows/webppt.yaml +47 -47
  73. package/workflows/word.yaml +80 -80
  74. package/workflows/youtube-shorts.yaml +25 -25
@@ -9,8 +9,8 @@
9
9
  * 2) / 输入 → discoverPlugins → resolveCommand
10
10
  * a) 命中插件且其 plugin.ts 暴露 runCommand → 让 plugin.ts 接管
11
11
  * b) 命中插件但只有声明式 commands/*.md → 把 prompt 拼好喂给 runQuery
12
- * c) 未命中 → 静默透传 runQuery(/ 只是激活前缀,不是严格命令系统;
13
- * 未知的 /xxx 当普通用户输入处理,不报错)
12
+ * c) 未命中 -> warning 提示未知命令后透传 runQuery(/ 只是激活前缀,
13
+ * 不是严格命令系统;未知的 /xxx 当普通用户输入处理,不报错)
14
14
  * ============================================================
15
15
  */
16
16
 
@@ -19,6 +19,7 @@ import type { LoopEvent, Message, Provider } from '../types.js';
19
19
  import {
20
20
  buildCommandInput,
21
21
  discoverPlugins,
22
+ listAvailableCommands,
22
23
  resolveCommand,
23
24
  } from './commandRouter.js';
24
25
  import type { PluginEvent } from './pluginApi.js';
@@ -106,10 +107,23 @@ export async function* runWithPlugins(
106
107
  const matched = resolveCommand(userInput);
107
108
 
108
109
  if (!matched) {
109
- // 未命中任何插件命令 → 静默透传 runQuery。
110
- // / 只是激活前缀(类似 Claude Code 的 / 模式),不是严格命令系统。
111
- // 用户可能输入 /init、/help、/任何东西 —— 不匹配就当普通对话处理,
112
- // 让 LLM 自行理解意图。不报错、不提示、不打断用户体验。
110
+ // 未命中任何插件命令 -> 提示后透传 runQuery。
111
+ // / 只是激活前缀(类似 Claude Code 的 / 模式),不是严格命令系统:
112
+ // 不匹配仍当普通对话处理,让 LLM 自行理解意图。但 -p 等无人值守场景里
113
+ // 拼错的 /reseach 会被静默当文本烧一轮 LLM(实战:用户以为在跑工作流,
114
+ // 实际什么都没发生)--先 warning 列出已注册命令,零成本纠正拼写。
115
+ const trimmedUnknown = userInput.trimStart();
116
+ const spaceUnknown = trimmedUnknown.indexOf(' ', 1);
117
+ const unknownCmd = (
118
+ spaceUnknown >= 0 ? trimmedUnknown.slice(1, spaceUnknown) : trimmedUnknown.slice(1)
119
+ ).trim();
120
+ if (unknownCmd) {
121
+ const available = listAvailableCommands().map((c) => c.command);
122
+ yield {
123
+ type: 'warning',
124
+ message: `未知命令 /${unknownCmd}(已注册:${available.length ? available.join('、') : '无'}),已按普通对话处理。`,
125
+ };
126
+ }
113
127
  yield* runQuery(userInput, {
114
128
  provider,
115
129
  history,
@@ -232,7 +232,34 @@ const CAPABILITY_POLICIES = [
232
232
  {
233
233
  id: 'capability.advisor', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
234
234
  requiresTools: ['Advisor'],
235
- render: () => '- Advisor 是可选的第二意见:复杂架构决策、高风险修改、反复受阻、证据冲突或用户明确要求时再调用。普通实现和收尾不固定咨询;顾问不可用不得阻塞主流程。咨询前先落盘成果,顾问意见与实测冲突时以实测为准并明确核对冲突。',
235
+ // 注意:文本内不得硬编码其他工具名(如 PresentResult),否则该工具缺席时这里
236
+ // 成为唯一残留的「幽灵引用」——capability 规则交叉引用必须在 toolNames 条件下拼接。
237
+ render: ({ toolNames }) => `- Advisor(顾问模型)使用规则:
238
+ **在动手之前问,不是出事之后问**:顾问的价值在方案定型前最大——代码一旦写下去,换方向的成本就变了。
239
+ 「摸清现状」不算动手(找文件、读代码、跑命令看状态,这些先做完再问);
240
+ 「动手」指写文件、认定某个解释、在假设上继续搭、给出结论。
241
+ **当 Advisor 工具出现在当前可用工具列表中时**,以下 5 种场景必须主动调用,不要等到出错或用户问了才想起来:
242
+ 1. 方案/开发计划成型后、动手执行前(先让顾问挑刺,避免做一半才发现方向错了)
243
+ 2. 最终结果提交用户前${toolNames.has('PresentResult') ? '(PresentResult 之前)' : ''}:代码评审、评估报告、技术选型、重构方案等所有"给出结论"的场合
244
+ 3. 连续 2 次遇到同类错误或修复不收敛(死循环前兆,立刻换思路)
245
+ 4. 同一轮内用户第 2 次反馈不满意(不要在原地打转,先找顾问换角度)
246
+ 5. 涉及安全性、数据丢失、不可逆操作的决策(顾问双保险)
247
+ 调用时 note 里必须包含:你的完整结论/方案 + 关键代码原文或证据摘要,不要只写结论摘要。
248
+ 场景 2 有个前置动作:**先把产出落盘再调用**(写文件、保存结果)。这一次调用要花时间,
249
+ 回合若在此期间结束,写下的文件还在,没写的答案就没了。
250
+ 长任务至少咨询两次:方案定型前一次、宣布完成前一次;短的、下一步由刚读到的输出直接决定的任务,一次即可。
251
+ **顾问返回后必须做验证,不能盲信:**
252
+ - 顾问的意见只是第二视角的候选诊断,不是事实。它没有工具访问权,无法自行验证。
253
+ - 对顾问提出的每一条严重问题/反例,都必须回到代码、测试、原始资料或工具输出核实。
254
+ - 核实后分类:真问题(有证据支撑)、误报(被事实推翻)、待澄清(暂时无法验证)。
255
+ - 未经核实的顾问意见,不得写成确定结论;与实测冲突时,以实测为准。
256
+ - ⚠️ 但「自测通过」不等于顾问说错了:那往往只说明你的测试没测到它指的那一层。
257
+ - 与你已掌握的证据冲突时**不要默默改方向**:再调一次顾问,把冲突摆出来
258
+ (「我实测是 X,你建议 Y,哪个约束更硬?」)。它看过你的证据但可能低估了,
259
+ 一次对账远比顺着错误分支做下去便宜。
260
+ **如果 Advisor 不在工具列表中**(未配置、已关闭、仅手动模式或当前不可用):视为用户明确选择跳过,不得尝试调用,也不阻塞流程,无需在输出中专门说明。
261
+ 咨询前先落盘成果,顾问意见与实测冲突时以实测为准并明确核对冲突。
262
+ 补充:任何评审/评估/结论输出前,先用 adversarial-review 的方法论自我审查(拆原子主张、反向提问、去现场核对),再决定是否需要顾问。`,
236
263
  },
237
264
  {
238
265
  id: 'capability.subagent', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
@@ -274,7 +274,34 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
274
274
  {
275
275
  id: 'capability.advisor', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
276
276
  requiresTools: ['Advisor'],
277
- render: () => '- Advisor 是可选的第二意见:复杂架构决策、高风险修改、反复受阻、证据冲突或用户明确要求时再调用。普通实现和收尾不固定咨询;顾问不可用不得阻塞主流程。咨询前先落盘成果,顾问意见与实测冲突时以实测为准并明确核对冲突。',
277
+ // 注意:文本内不得硬编码其他工具名(如 PresentResult),否则该工具缺席时这里
278
+ // 成为唯一残留的「幽灵引用」——capability 规则交叉引用必须在 toolNames 条件下拼接。
279
+ render: ({ toolNames }) => `- Advisor(顾问模型)使用规则:
280
+ **在动手之前问,不是出事之后问**:顾问的价值在方案定型前最大——代码一旦写下去,换方向的成本就变了。
281
+ 「摸清现状」不算动手(找文件、读代码、跑命令看状态,这些先做完再问);
282
+ 「动手」指写文件、认定某个解释、在假设上继续搭、给出结论。
283
+ **当 Advisor 工具出现在当前可用工具列表中时**,以下 5 种场景必须主动调用,不要等到出错或用户问了才想起来:
284
+ 1. 方案/开发计划成型后、动手执行前(先让顾问挑刺,避免做一半才发现方向错了)
285
+ 2. 最终结果提交用户前${toolNames.has('PresentResult') ? '(PresentResult 之前)' : ''}:代码评审、评估报告、技术选型、重构方案等所有"给出结论"的场合
286
+ 3. 连续 2 次遇到同类错误或修复不收敛(死循环前兆,立刻换思路)
287
+ 4. 同一轮内用户第 2 次反馈不满意(不要在原地打转,先找顾问换角度)
288
+ 5. 涉及安全性、数据丢失、不可逆操作的决策(顾问双保险)
289
+ 调用时 note 里必须包含:你的完整结论/方案 + 关键代码原文或证据摘要,不要只写结论摘要。
290
+ 场景 2 有个前置动作:**先把产出落盘再调用**(写文件、保存结果)。这一次调用要花时间,
291
+ 回合若在此期间结束,写下的文件还在,没写的答案就没了。
292
+ 长任务至少咨询两次:方案定型前一次、宣布完成前一次;短的、下一步由刚读到的输出直接决定的任务,一次即可。
293
+ **顾问返回后必须做验证,不能盲信:**
294
+ - 顾问的意见只是第二视角的候选诊断,不是事实。它没有工具访问权,无法自行验证。
295
+ - 对顾问提出的每一条严重问题/反例,都必须回到代码、测试、原始资料或工具输出核实。
296
+ - 核实后分类:真问题(有证据支撑)、误报(被事实推翻)、待澄清(暂时无法验证)。
297
+ - 未经核实的顾问意见,不得写成确定结论;与实测冲突时,以实测为准。
298
+ - ⚠️ 但「自测通过」不等于顾问说错了:那往往只说明你的测试没测到它指的那一层。
299
+ - 与你已掌握的证据冲突时**不要默默改方向**:再调一次顾问,把冲突摆出来
300
+ (「我实测是 X,你建议 Y,哪个约束更硬?」)。它看过你的证据但可能低估了,
301
+ 一次对账远比顺着错误分支做下去便宜。
302
+ **如果 Advisor 不在工具列表中**(未配置、已关闭、仅手动模式或当前不可用):视为用户明确选择跳过,不得尝试调用,也不阻塞流程,无需在输出中专门说明。
303
+ 咨询前先落盘成果,顾问意见与实测冲突时以实测为准并明确核对冲突。
304
+ 补充:任何评审/评估/结论输出前,先用 adversarial-review 的方法论自我审查(拆原子主张、反向提问、去现场核对),再决定是否需要顾问。`,
278
305
  },
279
306
  {
280
307
  id: 'capability.subagent', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
@@ -42,11 +42,23 @@ const inputSchema = z.object({
42
42
  const parameters = toToolParameters(inputSchema);
43
43
  // ---------------- 2. Description(给 LLM 看的工具说明) ----------------
44
44
  const description = `- Consults an independent, separately-configured second LLM ("顾问模型" / advisor) for a second opinion on your current plan, diagnosis, or progress.
45
- - Call with no arguments in most cases — the advisor receives a bounded, relevant view of the current session (the original request, recent discussion, current plan/diagnosis, and key tool results). Only pass \`note\` if something important is not otherwise clear.
46
- - Exception: if your current plan/diagnosis relies on things you saw in images (screenshots, photos, diagrams), summarize the key visual facts in \`note\` in 1-2 sentences (what each image shows and what you concluded from it) — the advisor may not be able to see the images themselves, and any visual judgment you never wrote down as text does not exist for it.
47
- - Call this: (1) after a plan/diagnosis has taken shape but before you commit to it (write code, declare a conclusion); (2) when you're stuck — the same error keeps recurring, your fixes aren't converging, or the user has pushed back and you're reconsidering direction; (3) before declaring a multi-step task complete and reporting back to the user.
45
+ - Call with no arguments in most cases — the advisor receives a bounded, relevant view of the current session (the original request, recent discussion, current plan/diagnosis, and key tool results). Always pass key evidence (original code snippets, raw data, full plan text) in \`note\`, not just your summary — the advisor cannot verify things you never wrote down.
46
+ - Exception: if your current plan/diagnosis relies on things you saw in images (screenshots, photos, diagrams), summarize the key visual facts in \`note\` in 1-2 sentences (what each image shows and what you concluded from it) — the advisor may not be able to see the images themselves.
47
+ - **Call it BEFORE substantive work, not after things go wrong.** The advisor is worth the most before an approach hardens — once you have written the code, the plan is expensive to abandon.
48
+ - Orientation is NOT substantive work: finding files, reading code, running a command to see the current state. Do that first, then consult.
49
+ - Substantive work IS: writing or editing files, committing to an interpretation, building on an assumption, declaring an answer.
50
+ - **MUST call Advisor in all 5 of these situations — do not wait for errors or user prompts:**
51
+ 1. After a plan / development plan is finalized, **before** executing it (get critique first, avoid discovering wrong direction halfway)
52
+ 2. **Before** submitting final results to the user (before PresentResult): code reviews, assessment reports, technical selections, refactoring plans — any time you "deliver a conclusion"
53
+ 3. The same error occurs 2+ times in a row, or fixes are not converging (early sign of infinite loop — switch approach immediately)
54
+ 4. The user gives unsatisfied feedback a **second time** in the same turn (stop spinning in place, get a fresh perspective first)
55
+ 5. Decisions involving security, data loss, or irreversible operations (advisor double-check)
56
+ - **Make your work durable BEFORE calling in situation 2.** Write the file, save the result. This call takes time; if the turn ends during it, a written file survives and an unwritten answer does not.
57
+ - On a long task, consult at least twice: once before the approach is locked in, once before you declare it done. On a short reactive task whose next step is dictated by output you just read, one call is enough — the advisor adds most of its value early.
58
+ - **Take the advice seriously, but do not obey it blindly.** Adapt if you follow a step and it empirically fails, or you hold primary-source evidence contradicting a specific claim (the file says X, the test output says Y). A passing self-test is NOT evidence the advice is wrong — it is evidence your test does not check what the advice checks.
59
+ - **If the advisor contradicts evidence you already gathered, do not silently switch.** Call Advisor once more stating the conflict outright ("I measured X, you advise Y — which constraint breaks the tie?"). The advisor saw your evidence but may have underweighted it; one reconciling call is far cheaper than committing to the wrong branch.
48
60
  - The advisor has no tool access and does a single read-only pass over the transcript — it cannot verify anything itself, only reason about what it's shown.
49
- - If this tool errors (advisor not configured in shared settings, disabled, manual-only, or a call failure), that's not a signal to stop — treat it as "no second opinion available this time" and continue with your own judgment. Do not block or retry in a loop.`;
61
+ - If this tool errors (advisor not configured in shared settings, disabled, manual-only, or a call failure), that's not a signal to stop — treat it as "no second opinion available this time" and continue with your own judgment, but **explicitly note "not cross-verified by advisor" in your final output**. Do not block or retry in a loop.`;
50
62
  // ---------------- 3. 顾问 persona system prompt ----------------
51
63
  const ADVISOR_SYSTEM_PROMPT = `你是一位经验丰富的全栈开发专家技术顾问,为正在独立执行任务的另一个 AI 助手提供第二意见评审。
52
64
  你的职责:技术方案评估、代码评审、疑难问题的根因定位与解决方向判断。
@@ -54,6 +66,14 @@ const ADVISOR_SYSTEM_PROMPT = `你是一位经验丰富的全栈开发专家技
54
66
  你会看到该助手当前会话中受预算约束的相关记录——包括首要需求、近期交流、它的思考过程、当前方案
55
67
  和关键工具结果与执行结果,以及它现在想让你重点关注的内容(如果有)。
56
68
 
69
+ 重要:你**没有工具访问权,无法自行验证任何事实**。你只是提供一个不同视角的怀疑和推理,
70
+ 不能把自己的推测当成确定结论。每条指出的问题都要明确标注:
71
+ - 【有证据】:基于记录里明确出现的代码/测试结果/事实
72
+ - 【推断】:基于已有信息的合理推断,但未被证实
73
+ - 【存疑】:信息不足,需要执行助手回去核实
74
+ 不要过度自信,不要给出确定性语气的未验证判断——执行助手会对你的意见逐条做验证,
75
+ 夸大其词会被判定为误报。
76
+
57
77
  审视时请做到:
58
78
  - 像资深工程师做方案评审/代码评审一样直接、具体——发现问题就明说,不要无条件附和或说套话。
59
79
  - 优先检查:方案是否遗漏了风险或边界情况、诊断是否真的找到了根因还是只是让症状消失、
@@ -66,7 +86,16 @@ const ADVISOR_SYSTEM_PROMPT = `你是一位经验丰富的全栈开发专家技
66
86
  或换个方式绕开,都要点出来。
67
87
  - 结论必须基于你在历史记录里看到的实际证据,不要脱离上下文给泛泛的通用建议;
68
88
  被省略的记录(尤其工具结果原文)所涉及的判断,明说你无法核实。
69
- - 篇幅精炼:只讲最关键的 1-3 点,不要复述历史、不要客套开场白。`;
89
+ - 篇幅精炼:只讲最关键的 1-3 点,不要复述历史、不要客套开场白。
90
+
91
+ 关于「带着反证回来」的那一次咨询(助手被要求这样做,不是在质疑你):
92
+ 当它说「我实测是 X,你上次建议 Y」时,它手里的是一手证据,你手里的是推理——
93
+ 默认它的观测为真,然后只回答一个问题:**哪个约束更硬**。
94
+ - 你上次漏看了它给出的证据 → 直接认,并说清结论怎么变。
95
+ - 它的观测其实没测到你担心的那一层(测了被改的函数,而真实调用路径根本走不到)
96
+ → 指出这个错位,并说要测什么才算数。
97
+ - 两者都成立、只是适用范围不同 → 说清各自的边界,别硬选一个。
98
+ 不要为了显得一致而坚持原判,也不要因为被反驳就整个收回——两种都会让它失去判断依据。`;
70
99
  // ---------------- 4. 消息构造(纯函数,单独导出可测) ----------------
71
100
  /**
72
101
  * 把主循环的 history 转成喂给顾问模型的 messages。
@@ -203,6 +232,13 @@ function fitAdvisorBudget(messages, inputBudget) {
203
232
  selected.push(first);
204
233
  used += countMessagesTokens([first]);
205
234
  }
235
+ // 从最近往前收,**装不下就停**(break,不是 continue)。
236
+ //
237
+ // ⚠️ 曾经是 continue:一条大消息放不下就跳过它、继续往前捡更小的旧消息。
238
+ // 结果是给顾问一段**中间有洞**的对话 —— 而洞恰好开在信息量最大的地方(长工具
239
+ // 结果、完整方案文本),偏偏下面那句省略提示还写着「保留最初需求和最近相关记录」,
240
+ // 顾问于是把两条本不相邻的消息读成前后相接,在残缺的因果链上给出自信结论。
241
+ // 宁可少给,不可给错:连续的尾部 + 如实说明从哪里开始省略。
206
242
  const recent = [];
207
243
  for (let index = body.length - 1; index >= 0; index--) {
208
244
  if (index === firstUserIndex)
@@ -210,7 +246,7 @@ function fitAdvisorBudget(messages, inputBudget) {
210
246
  const message = body[index];
211
247
  const cost = countMessagesTokens([message]);
212
248
  if (used + cost > inputBudget)
213
- continue;
249
+ break;
214
250
  recent.unshift(message);
215
251
  used += cost;
216
252
  }
@@ -287,17 +323,22 @@ export async function runAdvisor(input, signal, ctx, chatFn = chat, runtime = {}
287
323
  return {
288
324
  ok: false,
289
325
  error: '顾问模型未配置。请在设置 → 模型中完成专家顾问配置。' +
290
- '可继续凭自己判断推进,不必等待顾问。',
326
+ '本回合后续不要再次尝试调用 Advisor;可继续凭自己判断推进,不必等待顾问。',
291
327
  };
292
328
  }
293
329
  if (!advisor.enabled) {
294
330
  return {
295
331
  ok: false,
296
- error: '顾问模型当前已关闭(/advisor on 开启)。可继续凭自己判断推进,不必等待顾问。',
332
+ error: '顾问模型当前已关闭(/advisor on 开启)。' +
333
+ '本回合后续不要再次尝试调用 Advisor;可继续凭自己判断推进,不必等待顾问。',
297
334
  };
298
335
  }
299
336
  if (advisor.mode === 'manual' && ctx?.advisorInvocation !== 'manual') {
300
- return { ok: false, error: '顾问模型当前为“仅手动”模式;请使用 /advisor ask 发起咨询。' };
337
+ return {
338
+ ok: false,
339
+ error: '顾问模型当前为"仅手动"模式;请使用 /advisor ask 发起咨询。' +
340
+ '本回合后续不要再次尝试自动调用 Advisor。',
341
+ };
301
342
  }
302
343
  if (ctx?.advisorCalls && ctx.advisorCalls.used >= ctx.advisorCalls.max) {
303
344
  // ⚠️ 必须说明「配额会增长」:配额随本回合工作量上调(loop.ts::advisorQuotaFor),
@@ -435,6 +476,32 @@ export const advisorTool = {
435
476
  isConcurrencySafe: false,
436
477
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
437
478
  interruptBehavior: () => 'cancel',
479
+ /**
480
+ * 动态可用性探测:未配置 / 已关闭 / 仅手动模式 → 工具不暴露给模型。
481
+ * 对齐 ComputerUse / WebBrowser 的模式:关闭时工具直接从 LLM 视野消失,
482
+ * 避免"看到工具但调不了→反复尝试"的流畅度问题。
483
+ *
484
+ * signature 随 enabled/mode/modelId 变化,确保配置改动后工具集签名更新、
485
+ * 模型下一轮拿到的是新工具列表。
486
+ */
487
+ resolveAvailability() {
488
+ // 走同步探测(读内存快照),不触发磁盘 IO;
489
+ // resolveAvailability 是工具集构造阶段的同步调用,不能 await。
490
+ const avail = configService.advisorAvailabilitySync();
491
+ // ⚠️ 安全兜底:如果 snapshot 尚未初始化(极端情况:工具集构造发生在任何
492
+ // configService 异步调用之前),宁可返回 available=true 让工具显示出来
493
+ // (调用时 runAdvisor 会做真正的 fresh reload 校验),也不能误判为
494
+ // "未配置" 导致 Advisor 从工具列表里消失——少一个工具比多一个工具严重得多。
495
+ if (avail.configured && !avail.enabled) {
496
+ return { available: false, signature: 'disabled' };
497
+ }
498
+ if (avail.configured && avail.mode === 'manual') {
499
+ return { available: false, signature: 'manual-only' };
500
+ }
501
+ // 未初始化 / 已配置且可用 → 都显示工具(未配置时调用会有明确错误提示)
502
+ // modelId 进签名:不同顾问模型 = 不同能力 = 工具语义不同。
503
+ return { available: true, signature: avail.modelId ?? 'initializing' };
504
+ },
438
505
  call,
439
506
  callWithContext: runAdvisor,
440
507
  };
@@ -51,11 +51,23 @@ const parameters = toToolParameters(inputSchema);
51
51
 
52
52
  // ---------------- 2. Description(给 LLM 看的工具说明) ----------------
53
53
  const description = `- Consults an independent, separately-configured second LLM ("顾问模型" / advisor) for a second opinion on your current plan, diagnosis, or progress.
54
- - Call with no arguments in most cases — the advisor receives a bounded, relevant view of the current session (the original request, recent discussion, current plan/diagnosis, and key tool results). Only pass \`note\` if something important is not otherwise clear.
55
- - Exception: if your current plan/diagnosis relies on things you saw in images (screenshots, photos, diagrams), summarize the key visual facts in \`note\` in 1-2 sentences (what each image shows and what you concluded from it) — the advisor may not be able to see the images themselves, and any visual judgment you never wrote down as text does not exist for it.
56
- - Call this: (1) after a plan/diagnosis has taken shape but before you commit to it (write code, declare a conclusion); (2) when you're stuck — the same error keeps recurring, your fixes aren't converging, or the user has pushed back and you're reconsidering direction; (3) before declaring a multi-step task complete and reporting back to the user.
54
+ - Call with no arguments in most cases — the advisor receives a bounded, relevant view of the current session (the original request, recent discussion, current plan/diagnosis, and key tool results). Always pass key evidence (original code snippets, raw data, full plan text) in \`note\`, not just your summary — the advisor cannot verify things you never wrote down.
55
+ - Exception: if your current plan/diagnosis relies on things you saw in images (screenshots, photos, diagrams), summarize the key visual facts in \`note\` in 1-2 sentences (what each image shows and what you concluded from it) — the advisor may not be able to see the images themselves.
56
+ - **Call it BEFORE substantive work, not after things go wrong.** The advisor is worth the most before an approach hardens — once you have written the code, the plan is expensive to abandon.
57
+ - Orientation is NOT substantive work: finding files, reading code, running a command to see the current state. Do that first, then consult.
58
+ - Substantive work IS: writing or editing files, committing to an interpretation, building on an assumption, declaring an answer.
59
+ - **MUST call Advisor in all 5 of these situations — do not wait for errors or user prompts:**
60
+ 1. After a plan / development plan is finalized, **before** executing it (get critique first, avoid discovering wrong direction halfway)
61
+ 2. **Before** submitting final results to the user (before PresentResult): code reviews, assessment reports, technical selections, refactoring plans — any time you "deliver a conclusion"
62
+ 3. The same error occurs 2+ times in a row, or fixes are not converging (early sign of infinite loop — switch approach immediately)
63
+ 4. The user gives unsatisfied feedback a **second time** in the same turn (stop spinning in place, get a fresh perspective first)
64
+ 5. Decisions involving security, data loss, or irreversible operations (advisor double-check)
65
+ - **Make your work durable BEFORE calling in situation 2.** Write the file, save the result. This call takes time; if the turn ends during it, a written file survives and an unwritten answer does not.
66
+ - On a long task, consult at least twice: once before the approach is locked in, once before you declare it done. On a short reactive task whose next step is dictated by output you just read, one call is enough — the advisor adds most of its value early.
67
+ - **Take the advice seriously, but do not obey it blindly.** Adapt if you follow a step and it empirically fails, or you hold primary-source evidence contradicting a specific claim (the file says X, the test output says Y). A passing self-test is NOT evidence the advice is wrong — it is evidence your test does not check what the advice checks.
68
+ - **If the advisor contradicts evidence you already gathered, do not silently switch.** Call Advisor once more stating the conflict outright ("I measured X, you advise Y — which constraint breaks the tie?"). The advisor saw your evidence but may have underweighted it; one reconciling call is far cheaper than committing to the wrong branch.
57
69
  - The advisor has no tool access and does a single read-only pass over the transcript — it cannot verify anything itself, only reason about what it's shown.
58
- - If this tool errors (advisor not configured in shared settings, disabled, manual-only, or a call failure), that's not a signal to stop — treat it as "no second opinion available this time" and continue with your own judgment. Do not block or retry in a loop.`;
70
+ - If this tool errors (advisor not configured in shared settings, disabled, manual-only, or a call failure), that's not a signal to stop — treat it as "no second opinion available this time" and continue with your own judgment, but **explicitly note "not cross-verified by advisor" in your final output**. Do not block or retry in a loop.`;
59
71
 
60
72
  // ---------------- 3. 顾问 persona system prompt ----------------
61
73
  const ADVISOR_SYSTEM_PROMPT = `你是一位经验丰富的全栈开发专家技术顾问,为正在独立执行任务的另一个 AI 助手提供第二意见评审。
@@ -64,6 +76,14 @@ const ADVISOR_SYSTEM_PROMPT = `你是一位经验丰富的全栈开发专家技
64
76
  你会看到该助手当前会话中受预算约束的相关记录——包括首要需求、近期交流、它的思考过程、当前方案
65
77
  和关键工具结果与执行结果,以及它现在想让你重点关注的内容(如果有)。
66
78
 
79
+ 重要:你**没有工具访问权,无法自行验证任何事实**。你只是提供一个不同视角的怀疑和推理,
80
+ 不能把自己的推测当成确定结论。每条指出的问题都要明确标注:
81
+ - 【有证据】:基于记录里明确出现的代码/测试结果/事实
82
+ - 【推断】:基于已有信息的合理推断,但未被证实
83
+ - 【存疑】:信息不足,需要执行助手回去核实
84
+ 不要过度自信,不要给出确定性语气的未验证判断——执行助手会对你的意见逐条做验证,
85
+ 夸大其词会被判定为误报。
86
+
67
87
  审视时请做到:
68
88
  - 像资深工程师做方案评审/代码评审一样直接、具体——发现问题就明说,不要无条件附和或说套话。
69
89
  - 优先检查:方案是否遗漏了风险或边界情况、诊断是否真的找到了根因还是只是让症状消失、
@@ -76,7 +96,16 @@ const ADVISOR_SYSTEM_PROMPT = `你是一位经验丰富的全栈开发专家技
76
96
  或换个方式绕开,都要点出来。
77
97
  - 结论必须基于你在历史记录里看到的实际证据,不要脱离上下文给泛泛的通用建议;
78
98
  被省略的记录(尤其工具结果原文)所涉及的判断,明说你无法核实。
79
- - 篇幅精炼:只讲最关键的 1-3 点,不要复述历史、不要客套开场白。`;
99
+ - 篇幅精炼:只讲最关键的 1-3 点,不要复述历史、不要客套开场白。
100
+
101
+ 关于「带着反证回来」的那一次咨询(助手被要求这样做,不是在质疑你):
102
+ 当它说「我实测是 X,你上次建议 Y」时,它手里的是一手证据,你手里的是推理——
103
+ 默认它的观测为真,然后只回答一个问题:**哪个约束更硬**。
104
+ - 你上次漏看了它给出的证据 → 直接认,并说清结论怎么变。
105
+ - 它的观测其实没测到你担心的那一层(测了被改的函数,而真实调用路径根本走不到)
106
+ → 指出这个错位,并说要测什么才算数。
107
+ - 两者都成立、只是适用范围不同 → 说清各自的边界,别硬选一个。
108
+ 不要为了显得一致而坚持原判,也不要因为被反驳就整个收回——两种都会让它失去判断依据。`;
80
109
 
81
110
  // ---------------- 4. 消息构造(纯函数,单独导出可测) ----------------
82
111
 
@@ -234,12 +263,19 @@ function fitAdvisorBudget(messages: Message[], inputBudget: number): Message[] {
234
263
  selected.push(first);
235
264
  used += countMessagesTokens([first]);
236
265
  }
266
+ // 从最近往前收,**装不下就停**(break,不是 continue)。
267
+ //
268
+ // ⚠️ 曾经是 continue:一条大消息放不下就跳过它、继续往前捡更小的旧消息。
269
+ // 结果是给顾问一段**中间有洞**的对话 —— 而洞恰好开在信息量最大的地方(长工具
270
+ // 结果、完整方案文本),偏偏下面那句省略提示还写着「保留最初需求和最近相关记录」,
271
+ // 顾问于是把两条本不相邻的消息读成前后相接,在残缺的因果链上给出自信结论。
272
+ // 宁可少给,不可给错:连续的尾部 + 如实说明从哪里开始省略。
237
273
  const recent: Message[] = [];
238
274
  for (let index = body.length - 1; index >= 0; index--) {
239
275
  if (index === firstUserIndex) continue;
240
276
  const message = body[index]!;
241
277
  const cost = countMessagesTokens([message]);
242
- if (used + cost > inputBudget) continue;
278
+ if (used + cost > inputBudget) break;
243
279
  recent.unshift(message);
244
280
  used += cost;
245
281
  }
@@ -348,19 +384,26 @@ export async function runAdvisor(
348
384
  ok: false,
349
385
  error:
350
386
  '顾问模型未配置。请在设置 → 模型中完成专家顾问配置。' +
351
- '可继续凭自己判断推进,不必等待顾问。',
387
+ '本回合后续不要再次尝试调用 Advisor;可继续凭自己判断推进,不必等待顾问。',
352
388
  };
353
389
  }
354
390
 
355
391
  if (!advisor.enabled) {
356
392
  return {
357
393
  ok: false,
358
- error: '顾问模型当前已关闭(/advisor on 开启)。可继续凭自己判断推进,不必等待顾问。',
394
+ error:
395
+ '顾问模型当前已关闭(/advisor on 开启)。' +
396
+ '本回合后续不要再次尝试调用 Advisor;可继续凭自己判断推进,不必等待顾问。',
359
397
  };
360
398
  }
361
399
 
362
400
  if (advisor.mode === 'manual' && ctx?.advisorInvocation !== 'manual') {
363
- return { ok: false, error: '顾问模型当前为“仅手动”模式;请使用 /advisor ask 发起咨询。' };
401
+ return {
402
+ ok: false,
403
+ error:
404
+ '顾问模型当前为"仅手动"模式;请使用 /advisor ask 发起咨询。' +
405
+ '本回合后续不要再次尝试自动调用 Advisor。',
406
+ };
364
407
  }
365
408
 
366
409
  if (ctx?.advisorCalls && ctx.advisorCalls.used >= ctx.advisorCalls.max) {
@@ -507,6 +550,33 @@ export const advisorTool: Tool<AdvisorInput> = {
507
550
  isConcurrencySafe: false,
508
551
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
509
552
  interruptBehavior: () => 'cancel',
553
+ /**
554
+ * 动态可用性探测:未配置 / 已关闭 / 仅手动模式 → 工具不暴露给模型。
555
+ * 对齐 ComputerUse / WebBrowser 的模式:关闭时工具直接从 LLM 视野消失,
556
+ * 避免"看到工具但调不了→反复尝试"的流畅度问题。
557
+ *
558
+ * signature 随 enabled/mode/modelId 变化,确保配置改动后工具集签名更新、
559
+ * 模型下一轮拿到的是新工具列表。
560
+ */
561
+ resolveAvailability() {
562
+ // 走同步探测(读内存快照),不触发磁盘 IO;
563
+ // resolveAvailability 是工具集构造阶段的同步调用,不能 await。
564
+ const avail = configService.advisorAvailabilitySync();
565
+
566
+ // ⚠️ 安全兜底:如果 snapshot 尚未初始化(极端情况:工具集构造发生在任何
567
+ // configService 异步调用之前),宁可返回 available=true 让工具显示出来
568
+ // (调用时 runAdvisor 会做真正的 fresh reload 校验),也不能误判为
569
+ // "未配置" 导致 Advisor 从工具列表里消失——少一个工具比多一个工具严重得多。
570
+ if (avail.configured && !avail.enabled) {
571
+ return { available: false, signature: 'disabled' };
572
+ }
573
+ if (avail.configured && avail.mode === 'manual') {
574
+ return { available: false, signature: 'manual-only' };
575
+ }
576
+ // 未初始化 / 已配置且可用 → 都显示工具(未配置时调用会有明确错误提示)
577
+ // modelId 进签名:不同顾问模型 = 不同能力 = 工具语义不同。
578
+ return { available: true, signature: avail.modelId ?? 'initializing' };
579
+ },
510
580
  call,
511
581
  callWithContext: runAdvisor,
512
582
  };
@@ -56,7 +56,7 @@ import { requestUserChoice } from '../../askBridge.js';
56
56
  import { classifyCommand } from '../../irreversible/rules.js';
57
57
  import { startBackgroundTask } from '../tasks/backgroundTaskService.js';
58
58
  import { processExecutionMetadata } from '../tasks/processEvidence.js';
59
- import { permanentDeleteReason } from '../shared/permanentDeleteGuard.js';
59
+ import { permanentDeleteReason, } from '../shared/permanentDeleteGuard.js';
60
60
  import { ToolOutputStore } from '../shared/toolOutputStore.js';
61
61
  // ---------------- 0. 常量 ----------------
62
62
  const DEFAULT_TIMEOUT_MS = 120_000; // 2 min
@@ -105,6 +105,16 @@ function detectWindowsBash() {
105
105
  return null;
106
106
  }
107
107
  const WIN_BASH = detectWindowsBash();
108
+ /**
109
+ * 本工具这一刻**真正**把命令交给谁执行。
110
+ *
111
+ * 单一出处:description(告诉模型该写哪种语法)与 permanentDeleteGuard(按哪套动词
112
+ * 匹配删除命令)必须看同一个判断,否则就会出现「description 说这是 cmd、守卫却按
113
+ * bash 匹配」这种两头对不上的裂缝 —— del / rd 会因此直接穿过全部三层防护。
114
+ */
115
+ function effectiveShellKind() {
116
+ return process.platform === 'win32' && !WIN_BASH ? 'cmd' : 'bash';
117
+ }
108
118
  /**
109
119
  * G1:stdout / stderr 即时缓冲上限(各自)。命令狂输出(如 cat 大文件 / yes)时,
110
120
  * 在最终统一截断之前先把内存封顶,防 OOM。取最终展示截断阈值的 2 倍 —— 正常输出不受影响,
@@ -311,8 +321,11 @@ Some commands return non-zero exit codes for informational (non-error) reasons.
311
321
  - \`test\` / \`[\` exit 1 → condition is false (the answer to a question, not a failure)
312
322
  Only exit codes ≥ 2 from these commands indicate a real failure. For all other commands, non-zero exit codes are treated as failures normally.
313
323
 
314
- # Safety
315
- The following command patterns are blocked at the tool level and will fail before execution (no need to try them):
324
+ # Safety - three levels, from strict to lenient
325
+
326
+ ## 1. Blocked outright (fails before execution - do NOT retry with variants)
327
+
328
+ Catastrophic commands (no legitimate use):
316
329
  - \`rm -rf /\` and variants targeting root, $HOME, ~, or system directories (/etc, /usr, /bin, /Windows, /Users, /home, ...)
317
330
  - Filesystem destruction: \`mkfs\`, \`format X:\`, \`dd of=/dev/sdX\`, redirects to raw block devices
318
331
  - Power management: \`shutdown\`, \`reboot\`, \`halt\`, \`poweroff\`, \`init 0/6\`
@@ -320,9 +333,37 @@ The following command patterns are blocked at the tool level and will fail befor
320
333
  - Pipe-to-shell from network: \`curl ... | sh\`, \`wget ... | bash\`, etc.
321
334
  - \`chmod 777 /\`, Windows full-disk \`del /s\` / \`rmdir /s\`, \`diskpart\`
322
335
 
323
- If you have a legitimate use case that requires one of the above patterns, ask the user to run the command themselves in their terminal — do not try to bypass the check.
336
+ Permanent deletion (bypasses the OS Trash / Recycle Bin - use the Trash tool instead):
337
+ - \`rm\`, \`unlink\`, \`rmdir\` (any shell-level file/dir deletion, any target)
338
+ - \`find ... -delete\`
339
+ - \`git clean -f\` / \`git clean --force\` (\`-n\` / \`--dry-run\` is fine)
340
+
341
+ To delete files or directories use the **Trash** tool (\`action: "move"\` - recoverable); to clean Git untracked files use Trash with \`action: "git-clean"\`.
342
+ Do NOT work around this by switching to \`find -delete\`, Python \`os.remove\`, Node \`fs.rm\`, or other shell variants - they are blocked the same way.
343
+ If a permanent deletion is truly required, ask the user to run it themselves in their terminal.
344
+
345
+ ## 2. Requires user confirmation (irreversible)
346
+
347
+ These have legitimate uses but destroy the last copy - execution pauses and asks the user to confirm. In non-interactive contexts (sub-agents, cron, batch mode) they are automatically cancelled.
348
+ - \`git push --force\` / \`-f\`, and \`git push origin +branch\` (+refspec = force push)
349
+ - \`git push --delete <branch>\` / \`git push :<branch>\` (deleting remote branches)
350
+ - \`git filter-branch\` / \`git filter-repo\` (rewriting history)
351
+ - \`git reflog expire --expire=now\` / \`git gc --prune=now\` (destroying reflog)
352
+ - \`shred -u\` / \`shred --remove\` (overwrite and delete - unrecoverable)
353
+ - SQL data destruction: \`DROP DATABASE\`, \`DROP TABLE\`, \`TRUNCATE TABLE\`
354
+
355
+ ## 3. Warning only (command still runs)
356
+
357
+ These execute normally; a \`⚠️ 警告:\` line is prepended to the output as a reminder. They are NOT blocked.
358
+ - \`git reset --hard\` (reflog keeps it recoverable)
359
+ - \`git push --force-with-lease\` (rejects if the remote has unseen commits - the safe force-push form)
360
+ - \`git checkout -- .\` / \`git restore .\` (discards uncommitted changes)
361
+ - \`git branch -D\`, \`git stash drop\` / \`git stash clear\` (local, recoverable via reflog in most cases)
362
+ - \`git commit --amend\`, \`git commit --no-verify\`
363
+ - \`DELETE FROM ...\` (SQL row deletion; DROP/TRUNCATE are in level 2)
364
+ - \`kubectl delete\`, \`terraform destroy\`
324
365
 
325
- Separately, Bash scans for common destructive-but-recoverable patterns (\`git reset --hard\`, \`git push -f\` / \`--force\` / \`--force-with-lease\`, \`git checkout .\`, \`git restore .\`, \`git clean -f\`, \`git stash drop/clear\`, \`git branch -D\`, \`git commit --amend\` / \`--no-verify\`, \`rm -rf <path>\`, \`DROP TABLE\`, \`TRUNCATE\`, \`DELETE FROM\`, \`kubectl delete\`, \`terraform destroy\`, etc.) and prepends a \`⚠️ 警告:\` line to the output. These commands are NOT blocked — the warning is informational. Treat it as a signal to double-check intent and surface the warning to the user when relevant.${process.platform !== 'win32'
366
+ Regular Git operations (commit, add, status, log, diff, pull, fetch, checkout <branch>, merge, rebase, etc.) run without extra restrictions.${process.platform !== 'win32'
326
367
  ? ''
327
368
  : WIN_BASH
328
369
  ? `
@@ -349,7 +390,11 @@ async function call(input, signal, context) {
349
390
  error: `安全检查拒绝执行:${forbiddenReason}\n命令:${command}`,
350
391
  };
351
392
  }
352
- const permanentDelete = permanentDeleteReason(command, 'bash');
393
+ // 传**实际执行命令的 shell**,不是工具名:win32 上找不到 Git Bash 时下面走的是
394
+ // cmd.exe(见 WIN_BASH 探测),description 也会告诉模型「这里是 cmd」,模型于是写
395
+ // del / rd —— 按 'bash' 匹配的话这两个词一个都拦不住(黑名单与 irreversible 规则表
396
+ // 同样只认 POSIX 形态),等于三层全空。
397
+ const permanentDelete = permanentDeleteReason(command, effectiveShellKind());
353
398
  if (permanentDelete)
354
399
  return { ok: false, error: `永久删除检查拒绝执行:${permanentDelete}\n命令:${command}` };
355
400
  // 4a-1. 不可逆动作闸门:毁掉「最后一份副本」的命令要**停下来问人**。
@@ -63,7 +63,10 @@ import { requestUserChoice } from '../../askBridge.js';
63
63
  import { classifyCommand } from '../../irreversible/rules.js';
64
64
  import { startBackgroundTask } from '../tasks/backgroundTaskService.js';
65
65
  import { processExecutionMetadata } from '../tasks/processEvidence.js';
66
- import { permanentDeleteReason } from '../shared/permanentDeleteGuard.js';
66
+ import {
67
+ permanentDeleteReason,
68
+ type ShellKind,
69
+ } from '../shared/permanentDeleteGuard.js';
67
70
  import { ToolOutputStore } from '../shared/toolOutputStore.js';
68
71
 
69
72
  // ---------------- 0. 常量 ----------------
@@ -112,6 +115,17 @@ function detectWindowsBash(): string | null {
112
115
 
113
116
  const WIN_BASH = detectWindowsBash();
114
117
 
118
+ /**
119
+ * 本工具这一刻**真正**把命令交给谁执行。
120
+ *
121
+ * 单一出处:description(告诉模型该写哪种语法)与 permanentDeleteGuard(按哪套动词
122
+ * 匹配删除命令)必须看同一个判断,否则就会出现「description 说这是 cmd、守卫却按
123
+ * bash 匹配」这种两头对不上的裂缝 —— del / rd 会因此直接穿过全部三层防护。
124
+ */
125
+ function effectiveShellKind(): ShellKind {
126
+ return process.platform === 'win32' && !WIN_BASH ? 'cmd' : 'bash';
127
+ }
128
+
115
129
  /**
116
130
  * G1:stdout / stderr 即时缓冲上限(各自)。命令狂输出(如 cat 大文件 / yes)时,
117
131
  * 在最终统一截断之前先把内存封顶,防 OOM。取最终展示截断阈值的 2 倍 —— 正常输出不受影响,
@@ -332,8 +346,11 @@ Some commands return non-zero exit codes for informational (non-error) reasons.
332
346
  - \`test\` / \`[\` exit 1 → condition is false (the answer to a question, not a failure)
333
347
  Only exit codes ≥ 2 from these commands indicate a real failure. For all other commands, non-zero exit codes are treated as failures normally.
334
348
 
335
- # Safety
336
- The following command patterns are blocked at the tool level and will fail before execution (no need to try them):
349
+ # Safety - three levels, from strict to lenient
350
+
351
+ ## 1. Blocked outright (fails before execution - do NOT retry with variants)
352
+
353
+ Catastrophic commands (no legitimate use):
337
354
  - \`rm -rf /\` and variants targeting root, $HOME, ~, or system directories (/etc, /usr, /bin, /Windows, /Users, /home, ...)
338
355
  - Filesystem destruction: \`mkfs\`, \`format X:\`, \`dd of=/dev/sdX\`, redirects to raw block devices
339
356
  - Power management: \`shutdown\`, \`reboot\`, \`halt\`, \`poweroff\`, \`init 0/6\`
@@ -341,9 +358,37 @@ The following command patterns are blocked at the tool level and will fail befor
341
358
  - Pipe-to-shell from network: \`curl ... | sh\`, \`wget ... | bash\`, etc.
342
359
  - \`chmod 777 /\`, Windows full-disk \`del /s\` / \`rmdir /s\`, \`diskpart\`
343
360
 
344
- If you have a legitimate use case that requires one of the above patterns, ask the user to run the command themselves in their terminal — do not try to bypass the check.
345
-
346
- Separately, Bash scans for common destructive-but-recoverable patterns (\`git reset --hard\`, \`git push -f\` / \`--force\` / \`--force-with-lease\`, \`git checkout .\`, \`git restore .\`, \`git clean -f\`, \`git stash drop/clear\`, \`git branch -D\`, \`git commit --amend\` / \`--no-verify\`, \`rm -rf <path>\`, \`DROP TABLE\`, \`TRUNCATE\`, \`DELETE FROM\`, \`kubectl delete\`, \`terraform destroy\`, etc.) and prepends a \`⚠️ 警告:\` line to the output. These commands are NOT blocked — the warning is informational. Treat it as a signal to double-check intent and surface the warning to the user when relevant.${
361
+ Permanent deletion (bypasses the OS Trash / Recycle Bin - use the Trash tool instead):
362
+ - \`rm\`, \`unlink\`, \`rmdir\` (any shell-level file/dir deletion, any target)
363
+ - \`find ... -delete\`
364
+ - \`git clean -f\` / \`git clean --force\` (\`-n\` / \`--dry-run\` is fine)
365
+
366
+ To delete files or directories use the **Trash** tool (\`action: "move"\` - recoverable); to clean Git untracked files use Trash with \`action: "git-clean"\`.
367
+ Do NOT work around this by switching to \`find -delete\`, Python \`os.remove\`, Node \`fs.rm\`, or other shell variants - they are blocked the same way.
368
+ If a permanent deletion is truly required, ask the user to run it themselves in their terminal.
369
+
370
+ ## 2. Requires user confirmation (irreversible)
371
+
372
+ These have legitimate uses but destroy the last copy - execution pauses and asks the user to confirm. In non-interactive contexts (sub-agents, cron, batch mode) they are automatically cancelled.
373
+ - \`git push --force\` / \`-f\`, and \`git push origin +branch\` (+refspec = force push)
374
+ - \`git push --delete <branch>\` / \`git push :<branch>\` (deleting remote branches)
375
+ - \`git filter-branch\` / \`git filter-repo\` (rewriting history)
376
+ - \`git reflog expire --expire=now\` / \`git gc --prune=now\` (destroying reflog)
377
+ - \`shred -u\` / \`shred --remove\` (overwrite and delete - unrecoverable)
378
+ - SQL data destruction: \`DROP DATABASE\`, \`DROP TABLE\`, \`TRUNCATE TABLE\`
379
+
380
+ ## 3. Warning only (command still runs)
381
+
382
+ These execute normally; a \`⚠️ 警告:\` line is prepended to the output as a reminder. They are NOT blocked.
383
+ - \`git reset --hard\` (reflog keeps it recoverable)
384
+ - \`git push --force-with-lease\` (rejects if the remote has unseen commits - the safe force-push form)
385
+ - \`git checkout -- .\` / \`git restore .\` (discards uncommitted changes)
386
+ - \`git branch -D\`, \`git stash drop\` / \`git stash clear\` (local, recoverable via reflog in most cases)
387
+ - \`git commit --amend\`, \`git commit --no-verify\`
388
+ - \`DELETE FROM ...\` (SQL row deletion; DROP/TRUNCATE are in level 2)
389
+ - \`kubectl delete\`, \`terraform destroy\`
390
+
391
+ Regular Git operations (commit, add, status, log, diff, pull, fetch, checkout <branch>, merge, rebase, etc.) run without extra restrictions.${
347
392
  process.platform !== 'win32'
348
393
  ? ''
349
394
  : WIN_BASH
@@ -374,7 +419,11 @@ async function call(input: BashInput, signal?: AbortSignal, context?: ToolCallCo
374
419
  error: `安全检查拒绝执行:${forbiddenReason}\n命令:${command}`,
375
420
  };
376
421
  }
377
- const permanentDelete = permanentDeleteReason(command, 'bash');
422
+ // 传**实际执行命令的 shell**,不是工具名:win32 上找不到 Git Bash 时下面走的是
423
+ // cmd.exe(见 WIN_BASH 探测),description 也会告诉模型「这里是 cmd」,模型于是写
424
+ // del / rd —— 按 'bash' 匹配的话这两个词一个都拦不住(黑名单与 irreversible 规则表
425
+ // 同样只认 POSIX 形态),等于三层全空。
426
+ const permanentDelete = permanentDeleteReason(command, effectiveShellKind());
378
427
  if (permanentDelete) return { ok:false, error:`永久删除检查拒绝执行:${permanentDelete}\n命令:${command}` };
379
428
 
380
429
  // 4a-1. 不可逆动作闸门:毁掉「最后一份副本」的命令要**停下来问人**。