minimal-agent 0.11.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 (181) hide show
  1. package/package.json +5 -1
  2. package/plugins/workflow-runner/plugin.js +27 -15
  3. package/plugins/workflow-runner/plugin.ts +34 -20
  4. package/plugins/workflow-runner/src/agentCommon.js +13 -12
  5. package/plugins/workflow-runner/src/agentCommon.ts +22 -13
  6. package/plugins/workflow-runner/src/expressions.js +29 -8
  7. package/plugins/workflow-runner/src/expressions.ts +28 -10
  8. package/plugins/workflow-runner/src/index.js +50 -20
  9. package/plugins/workflow-runner/src/index.ts +75 -31
  10. package/plugins/workflow-runner/src/multimodal.js +2 -1
  11. package/plugins/workflow-runner/src/multimodal.ts +2 -1
  12. package/plugins/workflow-runner/src/recovery.js +9 -2
  13. package/plugins/workflow-runner/src/recovery.ts +9 -2
  14. package/plugins/workflow-runner/src/runner.js +31 -5
  15. package/plugins/workflow-runner/src/runner.ts +33 -5
  16. package/plugins/workflow-runner/src/stepExecutors/assert.js +2 -1
  17. package/plugins/workflow-runner/src/stepExecutors/assert.ts +3 -1
  18. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  19. package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
  20. package/plugins/workflow-runner/src/stepExecutors/tool.js +91 -61
  21. package/plugins/workflow-runner/src/stepExecutors/tool.ts +111 -71
  22. package/plugins/workflow-runner/src/stepExecutors/verify.js +6 -3
  23. package/plugins/workflow-runner/src/stepExecutors/verify.ts +7 -3
  24. package/plugins/workflow-runner/src/taskIdentity.js +197 -21
  25. package/plugins/workflow-runner/src/taskIdentity.ts +201 -20
  26. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  27. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  28. package/plugins/workflow-runner/src/types.js +113 -7
  29. package/plugins/workflow-runner/src/types.ts +137 -14
  30. package/plugins/workflow-runner/src/workflowState.js +455 -54
  31. package/plugins/workflow-runner/src/workflowState.ts +508 -63
  32. package/plugins/workflow-runner/test/cli.e2e.test.ts +20 -0
  33. package/plugins/workflow-runner/test/context-files.test.ts +47 -16
  34. package/plugins/workflow-runner/test/e2eNewLayout.test.ts +314 -0
  35. package/plugins/workflow-runner/test/engineRobustness.test.ts +9 -1
  36. package/plugins/workflow-runner/test/expressions.test.ts +42 -0
  37. package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
  38. package/plugins/workflow-runner/test/illustrate.load.test.ts +2 -2
  39. package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
  40. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +19 -0
  41. package/plugins/workflow-runner/test/ppt.load.test.ts +15 -15
  42. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +3 -2
  43. package/plugins/workflow-runner/test/research.load.test.ts +328 -0
  44. package/plugins/workflow-runner/test/runnerPreflight.test.ts +68 -11
  45. package/plugins/workflow-runner/test/stateLifecycle.test.ts +60 -12
  46. package/plugins/workflow-runner/test/taskArbitration.test.ts +19 -3
  47. package/plugins/workflow-runner/test/taskLifecycle.test.ts +61 -7
  48. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  49. package/plugins/workflow-runner/test/webppt.load.test.ts +4 -4
  50. package/plugins/workflow-runner/test/word.load.test.ts +9 -9
  51. package/skills/skill-creator/scripts/run_eval.py +3 -2
  52. package/src/cli/print.js +36 -4
  53. package/src/cli/print.ts +42 -4
  54. package/src/cli/streamJson.js +19 -4
  55. package/src/cli/streamJson.ts +17 -4
  56. package/src/config/configDefaults.js +25 -0
  57. package/src/config/configDefaults.ts +33 -0
  58. package/src/config/configFile.js +2 -21
  59. package/src/config/configFile.ts +11 -24
  60. package/src/config/configManagement.js +1 -1
  61. package/src/config/configManagement.ts +2 -2
  62. package/src/config/configService.js +24 -0
  63. package/src/config/configService.ts +23 -0
  64. package/src/context/compact.js +4 -0
  65. package/src/context/compact.ts +4 -0
  66. package/src/context/contextBundle.js +38 -13
  67. package/src/context/contextBundle.ts +47 -13
  68. package/src/harness/stallGuard.js +19 -4
  69. package/src/harness/stallGuard.ts +30 -7
  70. package/src/infra/verification.js +25 -18
  71. package/src/infra/verification.ts +50 -18
  72. package/src/loop.js +95 -11
  73. package/src/loop.ts +125 -16
  74. package/src/main.js +8 -1
  75. package/src/main.tsx +7 -1
  76. package/src/memory/constants.js +10 -0
  77. package/src/memory/constants.ts +13 -0
  78. package/src/memory/episodicMemory.js +274 -2
  79. package/src/memory/episodicMemory.ts +306 -1
  80. package/src/memory/fileStore.js +24 -4
  81. package/src/memory/fileStore.ts +22 -2
  82. package/src/memory/memoryService.js +79 -12
  83. package/src/memory/memoryService.ts +108 -9
  84. package/src/memory/repository.js +415 -85
  85. package/src/memory/repository.ts +437 -86
  86. package/src/memory/turnJournal.js +95 -0
  87. package/src/memory/turnJournal.ts +120 -0
  88. package/src/memory/types.ts +16 -0
  89. package/src/plugins/pluginApi.ts +30 -6
  90. package/src/plugins/pluginRunner.js +23 -8
  91. package/src/plugins/pluginRunner.ts +32 -13
  92. package/src/prompts/policyRegistry.js +55 -1
  93. package/src/prompts/policyRegistry.ts +55 -1
  94. package/src/prompts/promptAssembly.js +39 -7
  95. package/src/prompts/promptAssembly.ts +145 -59
  96. package/src/session/runAgentTurn.js +43 -21
  97. package/src/session/runAgentTurn.ts +68 -26
  98. package/src/session/runTurn.js +11 -46
  99. package/src/session/runTurn.ts +12 -46
  100. package/src/session/runtimeStore.js +149 -0
  101. package/src/session/runtimeStore.ts +197 -0
  102. package/src/session/sessionState.js +5 -0
  103. package/src/session/sessionState.ts +14 -0
  104. package/src/tools/advisor/advisor.js +76 -9
  105. package/src/tools/advisor/advisor.ts +79 -9
  106. package/src/tools/agent/agent.js +143 -14
  107. package/src/tools/agent/agent.ts +149 -14
  108. package/src/tools/agent/agentTaskService.js +60 -0
  109. package/src/tools/agent/agentTaskService.ts +113 -0
  110. package/src/tools/agent/agentTypes.js +5 -0
  111. package/src/tools/agent/agentTypes.ts +5 -0
  112. package/src/tools/bash/bash.js +80 -15
  113. package/src/tools/bash/bash.ts +83 -15
  114. package/src/tools/capabilities.js +5 -0
  115. package/src/tools/capabilities.ts +11 -1
  116. package/src/tools/edit/edit.js +7 -4
  117. package/src/tools/edit/edit.ts +7 -4
  118. package/src/tools/edit/multi-edit.js +4 -2
  119. package/src/tools/edit/multi-edit.ts +4 -2
  120. package/src/tools/glob/glob.js +156 -40
  121. package/src/tools/glob/glob.ts +185 -46
  122. package/src/tools/index.js +35 -17
  123. package/src/tools/index.ts +81 -20
  124. package/src/tools/lsp/lsp.js +165 -22
  125. package/src/tools/lsp/lsp.ts +206 -36
  126. package/src/tools/powershell/powershell.js +134 -27
  127. package/src/tools/powershell/powershell.ts +171 -30
  128. package/src/tools/presentresult/presentresult.js +10 -0
  129. package/src/tools/presentresult/presentresult.ts +10 -0
  130. package/src/tools/read/read.js +249 -29
  131. package/src/tools/read/read.ts +396 -59
  132. package/src/tools/shared/diffSummary.js +44 -0
  133. package/src/tools/shared/diffSummary.ts +63 -0
  134. package/src/tools/shared/fileActivity.js +2 -1
  135. package/src/tools/shared/fileActivity.ts +3 -0
  136. package/src/tools/shared/permanentDeleteGuard.js +57 -0
  137. package/src/tools/shared/permanentDeleteGuard.ts +71 -0
  138. package/src/tools/shared/toolOutputStore.js +97 -0
  139. package/src/tools/shared/toolOutputStore.ts +110 -0
  140. package/src/tools/skill/skill.js +86 -0
  141. package/src/tools/skill/skill.ts +94 -0
  142. package/src/tools/snip/snip.js +67 -0
  143. package/src/tools/snip/snip.ts +80 -0
  144. package/src/tools/tasks/backgroundTaskService.js +79 -28
  145. package/src/tools/tasks/backgroundTaskService.ts +165 -49
  146. package/src/tools/tasks/taskManagement.js +163 -0
  147. package/src/tools/tasks/taskManagement.ts +180 -0
  148. package/src/tools/tasks/taskOutput.js +64 -14
  149. package/src/tools/tasks/taskOutput.ts +102 -23
  150. package/src/tools/tasks/taskStop.js +48 -8
  151. package/src/tools/tasks/taskStop.ts +69 -9
  152. package/src/tools/tasks/taskStore.js +46 -0
  153. package/src/tools/tasks/taskStore.ts +77 -0
  154. package/src/tools/terminal/terminalCapture.js +167 -0
  155. package/src/tools/terminal/terminalCapture.ts +179 -0
  156. package/src/tools/trash/trash.js +233 -0
  157. package/src/tools/trash/trash.ts +283 -0
  158. package/src/tools/webbrowser/browserControl.js +5 -1
  159. package/src/tools/webbrowser/browserControl.ts +7 -1
  160. package/src/tools/webbrowser/webbrowser.js +8 -4
  161. package/src/tools/webbrowser/webbrowser.ts +8 -4
  162. package/src/tools/worktree/sessionWorktree.js +128 -0
  163. package/src/tools/worktree/sessionWorktree.ts +158 -0
  164. package/src/tools/write/write.js +6 -2
  165. package/src/tools/write/write.ts +6 -2
  166. package/src/types.ts +22 -4
  167. package/src/ui/hooks/useChat.js +31 -11
  168. package/src/ui/hooks/useChat.ts +31 -13
  169. package/src/web/protocol.ts +78 -3
  170. package/workflows/adaptive-research.yaml +5 -5
  171. package/workflows/book-review-short.yaml +5 -5
  172. package/workflows/e2e-write-greet.yaml +2 -2
  173. package/workflows/illustrate.yaml +12 -12
  174. package/workflows/plan.yaml +18 -18
  175. package/workflows/ppt.yaml +201 -201
  176. package/workflows/ralph-loop.yaml +3 -1
  177. package/workflows/research.md +449 -0
  178. package/workflows/research.yaml +873 -0
  179. package/workflows/webppt.yaml +47 -47
  180. package/workflows/word.yaml +80 -80
  181. package/workflows/youtube-shorts.yaml +25 -25
@@ -0,0 +1,95 @@
1
+ /**
2
+ * ============================================================
3
+ * src/memory/turnJournal.ts —— 「一个用户回合」的会话原文记账
4
+ * ------------------------------------------------------------
5
+ * 把 user 输入与本回合最终 assistant 回答写进 `episodes/*.jsonl`,
6
+ * 给 `SearchConversations` 提供可检索的历史原文。
7
+ *
8
+ * ★ 为什么是独立模块、而不是塞进 loop.ts:
9
+ * `runQuery` 不等于「一个用户回合」——子 Agent(tools/agent/agent.ts 直接调
10
+ * runQuery,且**携带同一份 workspaceId/sessionId**)和 workflow 的每个 llm/agent 步
11
+ * 都会各调一次 runQuery。记在那里会把子任务提示词当成「用户说的话」灌进历史检索,
12
+ * 污染的正是这个功能要保护的东西。
13
+ *
14
+ * 也不能只留在 `session/runTurn.ts`:TUI 走的是 `ui/hooks/useChat.ts` → runWithPlugins,
15
+ * **根本不经过 runTurn**。此前的后果是 TUI 对话永远不入库,而一次性的
16
+ * `.backfill-<ws>.json` 标记又让 SearchConversations 在首次搜索后永久停更 ——
17
+ * 工具宣称"搜索本机历史会话原文",实际对 TUI 用户静默失效。
18
+ *
19
+ * 所以:实现只有这一份,调用点恰好落在两个真正的「用户回合」边界上
20
+ * (runTurn 与 useChat.submit),子 Agent 与 workflow 步一概不记。
21
+ * ============================================================
22
+ */
23
+ import { applyEpisodeRetention, episodicMemoryService, finishRetentionSweep, shouldSweepRetention, } from './episodicMemory.js';
24
+ import { memoryService } from './memoryService.js';
25
+ function messageText(message) {
26
+ if (typeof message.content === 'string')
27
+ return message.content;
28
+ if (!Array.isArray(message.content))
29
+ return '';
30
+ return message.content
31
+ .filter((part) => part.type === 'text')
32
+ .map((part) => part.text)
33
+ .join('\n');
34
+ }
35
+ function lastAssistantMessage(messages, start) {
36
+ for (let index = messages.length - 1; index >= start; index -= 1) {
37
+ if (messages[index]?.role === 'assistant')
38
+ return messages[index];
39
+ }
40
+ return undefined;
41
+ }
42
+ /**
43
+ * 会话原文的保留清理:节流后台执行,绝不阻塞本回合。
44
+ * 与 `clipboardImage::pruneOldTempImages` 同款 fire-and-forget,不需要常驻定时器。
45
+ */
46
+ function sweepRetentionInBackground(workspaceId) {
47
+ if (!shouldSweepRetention(workspaceId))
48
+ return;
49
+ void (async () => {
50
+ try {
51
+ const settings = await memoryService.settings();
52
+ if (!settings.retention)
53
+ return;
54
+ await applyEpisodeRetention(workspaceId, settings.retention);
55
+ }
56
+ finally {
57
+ finishRetentionSweep(workspaceId);
58
+ }
59
+ })().catch(() => undefined);
60
+ }
61
+ /**
62
+ * 开一次回合记账。缺权限 / 缺 workspaceId / 缺 sessionId 都返回 null(不记,也不报错)——
63
+ * 记账是附加能力,绝不能反过来阻断对话。
64
+ */
65
+ export async function startTurnJournal(input) {
66
+ const context = input.memoryContext;
67
+ // 判据是 `!== 'denied'`:只要宿主没有明确拒绝记忆访问就记账,`read-only` 也照记。
68
+ // 这是**有意放松**的(2026-08-13 明确决定:优先保证检索效果与性能)——收紧成"只有可写
69
+ // 权限才记"会让 `-p`(默认 read-only)和部分 Desktop 场景的历史检索大面积失效。
70
+ // 真正的隐私边界由 `denied` 与保留窗口(MemorySettings.retention)负责,不靠这里收窄。
71
+ if (!context || context.access === 'denied' || !context.workspaceId || !context.sessionId) {
72
+ return null;
73
+ }
74
+ const target = { workspaceId: context.workspaceId, sessionId: context.sessionId };
75
+ await episodicMemoryService.append({
76
+ ...target,
77
+ role: 'user',
78
+ content: input.userInput,
79
+ ...(input.userMessageId ? { messageId: input.userMessageId } : {}),
80
+ }).catch(() => undefined);
81
+ sweepRetentionInBackground(target.workspaceId);
82
+ return {
83
+ async finish(history, startIndex) {
84
+ const assistant = lastAssistantMessage(history, startIndex);
85
+ const content = assistant ? messageText(assistant) : '';
86
+ if (!content)
87
+ return;
88
+ await episodicMemoryService.append({
89
+ ...target,
90
+ role: 'assistant',
91
+ content,
92
+ }).catch(() => undefined);
93
+ },
94
+ };
95
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * ============================================================
3
+ * src/memory/turnJournal.ts —— 「一个用户回合」的会话原文记账
4
+ * ------------------------------------------------------------
5
+ * 把 user 输入与本回合最终 assistant 回答写进 `episodes/*.jsonl`,
6
+ * 给 `SearchConversations` 提供可检索的历史原文。
7
+ *
8
+ * ★ 为什么是独立模块、而不是塞进 loop.ts:
9
+ * `runQuery` 不等于「一个用户回合」——子 Agent(tools/agent/agent.ts 直接调
10
+ * runQuery,且**携带同一份 workspaceId/sessionId**)和 workflow 的每个 llm/agent 步
11
+ * 都会各调一次 runQuery。记在那里会把子任务提示词当成「用户说的话」灌进历史检索,
12
+ * 污染的正是这个功能要保护的东西。
13
+ *
14
+ * 也不能只留在 `session/runTurn.ts`:TUI 走的是 `ui/hooks/useChat.ts` → runWithPlugins,
15
+ * **根本不经过 runTurn**。此前的后果是 TUI 对话永远不入库,而一次性的
16
+ * `.backfill-<ws>.json` 标记又让 SearchConversations 在首次搜索后永久停更 ——
17
+ * 工具宣称"搜索本机历史会话原文",实际对 TUI 用户静默失效。
18
+ *
19
+ * 所以:实现只有这一份,调用点恰好落在两个真正的「用户回合」边界上
20
+ * (runTurn 与 useChat.submit),子 Agent 与 workflow 步一概不记。
21
+ * ============================================================
22
+ */
23
+
24
+ import {
25
+ applyEpisodeRetention,
26
+ episodicMemoryService,
27
+ finishRetentionSweep,
28
+ shouldSweepRetention,
29
+ } from './episodicMemory.js';
30
+ import { memoryService } from './memoryService.js';
31
+ import type { Message } from '../types.js';
32
+
33
+ export interface TurnJournalInput {
34
+ memoryContext?: {
35
+ access?: string;
36
+ workspaceId?: string;
37
+ sessionId?: string;
38
+ };
39
+ userInput: string;
40
+ userMessageId?: string;
41
+ }
42
+
43
+ export interface TurnJournal {
44
+ /** 回合收尾时调用:从 history 里取本回合最后一条 assistant 消息入库。 */
45
+ finish(history: readonly Message[], startIndex: number): Promise<void>;
46
+ }
47
+
48
+ function messageText(message: Message): string {
49
+ if (typeof message.content === 'string') return message.content;
50
+ if (!Array.isArray(message.content)) return '';
51
+ return message.content
52
+ .filter((part): part is Extract<typeof part, { type: 'text' }> => part.type === 'text')
53
+ .map((part) => part.text)
54
+ .join('\n');
55
+ }
56
+
57
+ function lastAssistantMessage(
58
+ messages: readonly Message[],
59
+ start: number,
60
+ ): Message | undefined {
61
+ for (let index = messages.length - 1; index >= start; index -= 1) {
62
+ if (messages[index]?.role === 'assistant') return messages[index];
63
+ }
64
+ return undefined;
65
+ }
66
+
67
+ /**
68
+ * 会话原文的保留清理:节流后台执行,绝不阻塞本回合。
69
+ * 与 `clipboardImage::pruneOldTempImages` 同款 fire-and-forget,不需要常驻定时器。
70
+ */
71
+ function sweepRetentionInBackground(workspaceId: string): void {
72
+ if (!shouldSweepRetention(workspaceId)) return;
73
+ void (async () => {
74
+ try {
75
+ const settings = await memoryService.settings();
76
+ if (!settings.retention) return;
77
+ await applyEpisodeRetention(workspaceId, settings.retention);
78
+ } finally {
79
+ finishRetentionSweep(workspaceId);
80
+ }
81
+ })().catch(() => undefined);
82
+ }
83
+
84
+ /**
85
+ * 开一次回合记账。缺权限 / 缺 workspaceId / 缺 sessionId 都返回 null(不记,也不报错)——
86
+ * 记账是附加能力,绝不能反过来阻断对话。
87
+ */
88
+ export async function startTurnJournal(
89
+ input: TurnJournalInput,
90
+ ): Promise<TurnJournal | null> {
91
+ const context = input.memoryContext;
92
+ // 判据是 `!== 'denied'`:只要宿主没有明确拒绝记忆访问就记账,`read-only` 也照记。
93
+ // 这是**有意放松**的(2026-08-13 明确决定:优先保证检索效果与性能)——收紧成"只有可写
94
+ // 权限才记"会让 `-p`(默认 read-only)和部分 Desktop 场景的历史检索大面积失效。
95
+ // 真正的隐私边界由 `denied` 与保留窗口(MemorySettings.retention)负责,不靠这里收窄。
96
+ if (!context || context.access === 'denied' || !context.workspaceId || !context.sessionId) {
97
+ return null;
98
+ }
99
+ const target = { workspaceId: context.workspaceId, sessionId: context.sessionId };
100
+ await episodicMemoryService.append({
101
+ ...target,
102
+ role: 'user',
103
+ content: input.userInput,
104
+ ...(input.userMessageId ? { messageId: input.userMessageId } : {}),
105
+ }).catch(() => undefined);
106
+ sweepRetentionInBackground(target.workspaceId);
107
+
108
+ return {
109
+ async finish(history, startIndex) {
110
+ const assistant = lastAssistantMessage(history, startIndex);
111
+ const content = assistant ? messageText(assistant) : '';
112
+ if (!content) return;
113
+ await episodicMemoryService.append({
114
+ ...target,
115
+ role: 'assistant',
116
+ content,
117
+ }).catch(() => undefined);
118
+ },
119
+ };
120
+ }
@@ -71,10 +71,26 @@ export interface MemoryReviewWindows {
71
71
  decision: number;
72
72
  }
73
73
 
74
+ /**
75
+ * 会话原文日志(`episodes/*.jsonl`)的保留窗口。
76
+ *
77
+ * 记忆条目(records)是用户明确授权保存的稳定事实,只有用户能删;会话原文日志是为了
78
+ * `SearchConversations` 而留的**全量对话副本**,性质完全不同——它必须有保留上限,否则
79
+ * 明文对话会无限累积(仓库里其它 append-only 存储都有清理,只有它没有)。
80
+ */
81
+ export interface MemoryRetention {
82
+ /** 超过这个天数的会话原文自动清理。 */
83
+ episodeDays: number;
84
+ /** 单个工作区的会话原文总体积上限(MB),先到者先触发。 */
85
+ episodeMaxMegabytes: number;
86
+ }
87
+
74
88
  export interface MemorySettings {
75
89
  autoRemember: boolean;
76
90
  /** Optional on the public wire type for pre-v2 desktop clients. */
77
91
  reviewAfterDays?: MemoryReviewWindows;
92
+ /** Optional on the wire so pre-retention clients keep deserializing. */
93
+ retention?: MemoryRetention;
78
94
  }
79
95
 
80
96
  export interface MemoryDiagnostic {
@@ -21,9 +21,21 @@
21
21
  * ============================================================
22
22
  */
23
23
 
24
- import type { LoopEvent, Message, Provider, ToolCallContext } from '../types.js';
24
+ import type { ContextBundle } from '../context/contextBundle.js';
25
+ import type { AgentRuntimeContext } from '../runtime/agentRuntimeContext.js';
26
+ import type {
27
+ RunAgentTurnOptions,
28
+ RunAgentTurnResult,
29
+ } from '../session/runAgentTurn.js';
25
30
  import type { SessionState } from '../session/sessionState.js';
26
- import type { RunAgentTurnOptions, RunAgentTurnResult } from '../session/runAgentTurn.js';
31
+ import type {
32
+ LoopEvent,
33
+ Message,
34
+ PromptCacheContext,
35
+ Provider,
36
+ ToolCallContext,
37
+ ToolResult,
38
+ } from '../types.js';
27
39
 
28
40
  /** 框架传给插件的运行时上下文(仅暴露执行 LLM/工具 所需的最小集合)。 */
29
41
  export interface PluginContext {
@@ -38,12 +50,23 @@ export interface PluginContext {
38
50
  maxTurns?: number;
39
51
  /** 当前回合的瞬时外部上下文;插件不得把它持久化到 history。 */
40
52
  transientContext?: string;
41
- transientBundle?: import('../context/contextBundle.js').ContextBundle;
42
- promptCache?: import('../types.js').PromptCacheContext;
53
+ transientBundle?: ContextBundle;
54
+ promptCache?: PromptCacheContext;
43
55
  /** Canonical host-issued trust context; plugins may narrow it but never elevate it. */
44
- runtimeContext?: import('../runtime/agentRuntimeContext.js').AgentRuntimeContext;
56
+ runtimeContext?: AgentRuntimeContext;
45
57
  /** Host tool names are an upper bound. Plugins may narrow, never expand. */
46
58
  allowedTools?: readonly string[];
59
+ /**
60
+ * Host tool dispatcher. Rich plugins must use the same injected executor as
61
+ * the parent run so tests, embedders and host-specific dispatch remain on one
62
+ * execution path.
63
+ */
64
+ executeTool?: (
65
+ name: string,
66
+ rawArgsJson: string,
67
+ signal?: AbortSignal,
68
+ context?: ToolCallContext,
69
+ ) => Promise<ToolResult>;
47
70
  /**
48
71
  * 宿主的**授权与可见性上下文**,原样转交给工具层(`executeTool` 第四参)。
49
72
  *
@@ -100,7 +123,8 @@ export interface PluginEvent {
100
123
  export interface PluginApi {
101
124
  activate?(
102
125
  ctx: PluginRuntimeContext,
103
- ): Promise<void | (() => void)> | void | (() => void);
126
+ ): // biome-ignore lint/suspicious/noConfusingVoidType: lifecycle hooks intentionally accept synchronous void or an async disposer.
127
+ Promise<void | (() => void)> | void | (() => void);
104
128
  runCommand?(
105
129
  commandName: string,
106
130
  args: string,
@@ -9,13 +9,13 @@
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
- import { discoverPlugins, resolveCommand, buildCommandInput, } from './commandRouter.js';
17
- import { loadPluginApi } from './pluginLoader.js';
18
16
  import { runQuery } from '../loop.js';
17
+ import { buildCommandInput, discoverPlugins, listAvailableCommands, resolveCommand, } from './commandRouter.js';
18
+ import { loadPluginApi } from './pluginLoader.js';
19
19
  export async function* runWithPlugins(userInput, options) {
20
20
  const { provider, history, signal } = options;
21
21
  const isSlashCommand = userInput.trimStart().startsWith('/');
@@ -30,6 +30,7 @@ export async function* runWithPlugins(userInput, options) {
30
30
  sessionState: options.sessionState,
31
31
  chat: options.chat,
32
32
  executeTool: options.executeTool,
33
+ commitFormalDelivery: options.commitFormalDelivery,
33
34
  toolGate: options.toolGate,
34
35
  transientContext: options.transientContext,
35
36
  transientBundle: options.transientBundle,
@@ -55,10 +56,21 @@ export async function* runWithPlugins(userInput, options) {
55
56
  await discoverPlugins();
56
57
  const matched = resolveCommand(userInput);
57
58
  if (!matched) {
58
- // 未命中任何插件命令 → 静默透传 runQuery。
59
- // / 只是激活前缀(类似 Claude Code 的 / 模式),不是严格命令系统。
60
- // 用户可能输入 /init、/help、/任何东西 —— 不匹配就当普通对话处理,
61
- // 让 LLM 自行理解意图。不报错、不提示、不打断用户体验。
59
+ // 未命中任何插件命令 -> 提示后透传 runQuery。
60
+ // / 只是激活前缀(类似 Claude Code 的 / 模式),不是严格命令系统:
61
+ // 不匹配仍当普通对话处理,让 LLM 自行理解意图。但 -p 等无人值守场景里
62
+ // 拼错的 /reseach 会被静默当文本烧一轮 LLM(实战:用户以为在跑工作流,
63
+ // 实际什么都没发生)--先 warning 列出已注册命令,零成本纠正拼写。
64
+ const trimmedUnknown = userInput.trimStart();
65
+ const spaceUnknown = trimmedUnknown.indexOf(' ', 1);
66
+ const unknownCmd = (spaceUnknown >= 0 ? trimmedUnknown.slice(1, spaceUnknown) : trimmedUnknown.slice(1)).trim();
67
+ if (unknownCmd) {
68
+ const available = listAvailableCommands().map((c) => c.command);
69
+ yield {
70
+ type: 'warning',
71
+ message: `未知命令 /${unknownCmd}(已注册:${available.length ? available.join('、') : '无'}),已按普通对话处理。`,
72
+ };
73
+ }
62
74
  yield* runQuery(userInput, {
63
75
  provider,
64
76
  history,
@@ -69,6 +81,7 @@ export async function* runWithPlugins(userInput, options) {
69
81
  sessionState: options.sessionState,
70
82
  chat: options.chat,
71
83
  executeTool: options.executeTool,
84
+ commitFormalDelivery: options.commitFormalDelivery,
72
85
  toolGate: options.toolGate,
73
86
  transientContext: options.transientContext,
74
87
  transientBundle: options.transientBundle,
@@ -104,6 +117,7 @@ export async function* runWithPlugins(userInput, options) {
104
117
  promptCache: options.promptCache,
105
118
  runtimeContext: options.runtimeContext,
106
119
  allowedTools: options.allowedTools,
120
+ executeTool: options.executeTool,
107
121
  // ⚠️ 宿主授权上下文**必须**跟着走。这一处曾经漏掉,而下面两条 runQuery 路径
108
122
  // 都带着它 —— 于是富插件成了三条路里唯一拿不到 browserRun/computerRun 的那条。
109
123
  // 后果不是"少个字段":workflow 里调 ComputerUse 恒返回「当前宿主未启用
@@ -129,6 +143,7 @@ export async function* runWithPlugins(userInput, options) {
129
143
  sessionState: options.sessionState,
130
144
  chat: options.chat,
131
145
  executeTool: options.executeTool,
146
+ commitFormalDelivery: options.commitFormalDelivery,
132
147
  toolGate: options.toolGate,
133
148
  transientContext: options.transientContext,
134
149
  transientBundle: options.transientBundle,
@@ -9,20 +9,21 @@
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
 
17
+ import { type RunQueryOptions, runQuery } from '../loop.js';
18
+ import type { LoopEvent, Message, Provider } from '../types.js';
17
19
  import {
20
+ buildCommandInput,
18
21
  discoverPlugins,
22
+ listAvailableCommands,
19
23
  resolveCommand,
20
- buildCommandInput,
21
24
  } from './commandRouter.js';
22
- import { loadPluginApi } from './pluginLoader.js';
23
- import { runQuery, type RunQueryOptions } from '../loop.js';
24
- import type { LoopEvent, Message, Provider } from '../types.js';
25
25
  import type { PluginEvent } from './pluginApi.js';
26
+ import { loadPluginApi } from './pluginLoader.js';
26
27
 
27
28
  export interface PluginRunnerOptions {
28
29
  provider: Provider;
@@ -38,6 +39,7 @@ export interface PluginRunnerOptions {
38
39
  */
39
40
  chat?: RunQueryOptions['chat'];
40
41
  executeTool?: RunQueryOptions['executeTool'];
42
+ commitFormalDelivery?: RunQueryOptions['commitFormalDelivery'];
41
43
  /** 工具执行闸门(生产用):透传给 runQuery。见 src/toolGate.ts。 */
42
44
  toolGate?: RunQueryOptions['toolGate'];
43
45
  transientContext?: RunQueryOptions['transientContext'];
@@ -77,6 +79,7 @@ export async function* runWithPlugins(
77
79
  sessionState: options.sessionState,
78
80
  chat: options.chat,
79
81
  executeTool: options.executeTool,
82
+ commitFormalDelivery: options.commitFormalDelivery,
80
83
  toolGate: options.toolGate,
81
84
  transientContext: options.transientContext,
82
85
  transientBundle: options.transientBundle,
@@ -104,10 +107,23 @@ export async function* runWithPlugins(
104
107
  const matched = resolveCommand(userInput);
105
108
 
106
109
  if (!matched) {
107
- // 未命中任何插件命令 → 静默透传 runQuery。
108
- // / 只是激活前缀(类似 Claude Code 的 / 模式),不是严格命令系统。
109
- // 用户可能输入 /init、/help、/任何东西 —— 不匹配就当普通对话处理,
110
- // 让 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
+ }
111
127
  yield* runQuery(userInput, {
112
128
  provider,
113
129
  history,
@@ -118,6 +134,7 @@ export async function* runWithPlugins(
118
134
  sessionState: options.sessionState,
119
135
  chat: options.chat,
120
136
  executeTool: options.executeTool,
137
+ commitFormalDelivery: options.commitFormalDelivery,
121
138
  toolGate: options.toolGate,
122
139
  transientContext: options.transientContext,
123
140
  transientBundle: options.transientBundle,
@@ -154,6 +171,7 @@ export async function* runWithPlugins(
154
171
  promptCache: options.promptCache,
155
172
  runtimeContext: options.runtimeContext,
156
173
  allowedTools: options.allowedTools,
174
+ executeTool: options.executeTool,
157
175
  // ⚠️ 宿主授权上下文**必须**跟着走。这一处曾经漏掉,而下面两条 runQuery 路径
158
176
  // 都带着它 —— 于是富插件成了三条路里唯一拿不到 browserRun/computerRun 的那条。
159
177
  // 后果不是"少个字段":workflow 里调 ComputerUse 恒返回「当前宿主未启用
@@ -180,10 +198,11 @@ export async function* runWithPlugins(
180
198
  sessionState: options.sessionState,
181
199
  chat: options.chat,
182
200
  executeTool: options.executeTool,
201
+ commitFormalDelivery: options.commitFormalDelivery,
183
202
  toolGate: options.toolGate,
184
- transientContext: options.transientContext,
185
- transientBundle: options.transientBundle,
186
- promptCache: options.promptCache,
203
+ transientContext: options.transientContext,
204
+ transientBundle: options.transientBundle,
205
+ promptCache: options.promptCache,
187
206
  userAttachments: options.userAttachments,
188
207
  userMessageId: options.userMessageId,
189
208
  userBrowserReferences: options.userBrowserReferences,
@@ -84,6 +84,8 @@ const ROUTABLE_TOOLS = [
84
84
  'ComputerUse', 'WebBrowser', 'WebFetch', 'WebSearch',
85
85
  'Download', 'Read', 'Glob', 'Grep', 'Edit', 'MultiEdit', 'Write', 'Bash',
86
86
  'NotebookEdit', 'LSP', 'PowerShell', 'TaskOutput', 'TaskStop',
87
+ 'Trash', 'TaskCreate', 'TaskGet', 'TaskUpdate', 'TaskList', 'Skill',
88
+ 'EnterWorktree', 'ExitWorktree', 'TerminalCapture', 'Snip',
87
89
  ];
88
90
  const PARAMETERIZED_TOOLS = [
89
91
  ...ROUTABLE_TOOLS, ...MEMORY_TOOLS, 'AskUserQuestion', 'Advisor', 'Agent',
@@ -173,6 +175,31 @@ const CAPABILITY_POLICIES = [
173
175
  requires: ['background.process'],
174
176
  render: () => '- 后台任务按当前会话隔离。启动后保存 task_id;TaskOutput 可阻塞等待新输出,避免高频轮询;任务不再需要或需要中止时使用 TaskStop,不遗留服务进程。',
175
177
  },
178
+ {
179
+ id: 'capability.trash', layer: 'capability', profiles: ['main', 'subagent-general'], source: '06',
180
+ requiresTools: ['Trash'],
181
+ render: () => '- 删除项目文件或目录使用 Trash 移入系统回收站;清理 Git 未跟踪文件使用 Trash(action="git-clean")。Bash/PowerShell 不执行绕过回收站的永久删除命令。',
182
+ },
183
+ {
184
+ id: 'capability.task-management', layer: 'capability', profiles: ALL_PROFILES, source: '06',
185
+ requires: ['task.management'],
186
+ render: () => '- 多阶段、跨回合或存在依赖关系的工作用 TaskCreate/TaskUpdate 保持任务图;开始前检查 blockedBy,完成验证后才标记 completed。单步工作不必创建任务。',
187
+ },
188
+ {
189
+ id: 'capability.skill-invoke', layer: 'capability', profiles: ALL_PROFILES, source: '07',
190
+ requiresTools: ['Skill'],
191
+ render: () => '- 需求匹配已安装 Skill 或用户明确点名 Skill 时,用 Skill 按精确名称加载完整 SKILL.md;其中引用的资源再按需 Read,不预加载整个目录。',
192
+ },
193
+ {
194
+ id: 'capability.session-worktree', layer: 'capability', profiles: MAIN_PROFILE, source: '06',
195
+ requires: ['worktree.session'],
196
+ render: () => '- 主任务需要隔离试验时可 EnterWorktree;完成后用 ExitWorktree apply/keep/discard 收口。apply 必须通过补丁检查且不自动提交;discard 不永久丢弃已有成果。',
197
+ },
198
+ {
199
+ id: 'capability.terminal-capture', layer: 'capability', profiles: MAIN_PROFILE, source: '06',
200
+ requiresTools: ['TerminalCapture'],
201
+ render: () => '- 只有确需查看当前 TUI 屏幕状态时使用 TerminalCapture;项目命令输出仍通过 Bash/TaskOutput 获取。',
202
+ },
176
203
  {
177
204
  id: 'capability.web-research', layer: 'capability', profiles: ALL_PROFILES, source: '04',
178
205
  requiresAnyTools: ['WebSearch', 'WebFetch'],
@@ -205,7 +232,34 @@ const CAPABILITY_POLICIES = [
205
232
  {
206
233
  id: 'capability.advisor', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
207
234
  requiresTools: ['Advisor'],
208
- 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 的方法论自我审查(拆原子主张、反向提问、去现场核对),再决定是否需要顾问。`,
209
263
  },
210
264
  {
211
265
  id: 'capability.subagent', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
@@ -133,6 +133,8 @@ const ROUTABLE_TOOLS = [
133
133
  'ComputerUse', 'WebBrowser', 'WebFetch', 'WebSearch',
134
134
  'Download', 'Read', 'Glob', 'Grep', 'Edit', 'MultiEdit', 'Write', 'Bash',
135
135
  'NotebookEdit', 'LSP', 'PowerShell', 'TaskOutput', 'TaskStop',
136
+ 'Trash', 'TaskCreate', 'TaskGet', 'TaskUpdate', 'TaskList', 'Skill',
137
+ 'EnterWorktree', 'ExitWorktree', 'TerminalCapture', 'Snip',
136
138
  ];
137
139
 
138
140
  const PARAMETERIZED_TOOLS = [
@@ -215,6 +217,31 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
215
217
  requires: ['background.process'],
216
218
  render: () => '- 后台任务按当前会话隔离。启动后保存 task_id;TaskOutput 可阻塞等待新输出,避免高频轮询;任务不再需要或需要中止时使用 TaskStop,不遗留服务进程。',
217
219
  },
220
+ {
221
+ id: 'capability.trash', layer: 'capability', profiles: ['main','subagent-general'], source: '06',
222
+ requiresTools: ['Trash'],
223
+ render: () => '- 删除项目文件或目录使用 Trash 移入系统回收站;清理 Git 未跟踪文件使用 Trash(action="git-clean")。Bash/PowerShell 不执行绕过回收站的永久删除命令。',
224
+ },
225
+ {
226
+ id: 'capability.task-management', layer: 'capability', profiles: ALL_PROFILES, source: '06',
227
+ requires: ['task.management'],
228
+ render: () => '- 多阶段、跨回合或存在依赖关系的工作用 TaskCreate/TaskUpdate 保持任务图;开始前检查 blockedBy,完成验证后才标记 completed。单步工作不必创建任务。',
229
+ },
230
+ {
231
+ id: 'capability.skill-invoke', layer: 'capability', profiles: ALL_PROFILES, source: '07',
232
+ requiresTools: ['Skill'],
233
+ render: () => '- 需求匹配已安装 Skill 或用户明确点名 Skill 时,用 Skill 按精确名称加载完整 SKILL.md;其中引用的资源再按需 Read,不预加载整个目录。',
234
+ },
235
+ {
236
+ id: 'capability.session-worktree', layer: 'capability', profiles: MAIN_PROFILE, source: '06',
237
+ requires: ['worktree.session'],
238
+ render: () => '- 主任务需要隔离试验时可 EnterWorktree;完成后用 ExitWorktree apply/keep/discard 收口。apply 必须通过补丁检查且不自动提交;discard 不永久丢弃已有成果。',
239
+ },
240
+ {
241
+ id: 'capability.terminal-capture', layer: 'capability', profiles: MAIN_PROFILE, source: '06',
242
+ requiresTools: ['TerminalCapture'],
243
+ render: () => '- 只有确需查看当前 TUI 屏幕状态时使用 TerminalCapture;项目命令输出仍通过 Bash/TaskOutput 获取。',
244
+ },
218
245
  {
219
246
  id: 'capability.web-research', layer: 'capability', profiles: ALL_PROFILES, source: '04',
220
247
  requiresAnyTools: ['WebSearch', 'WebFetch'],
@@ -247,7 +274,34 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
247
274
  {
248
275
  id: 'capability.advisor', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
249
276
  requiresTools: ['Advisor'],
250
- 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 的方法论自我审查(拆原子主张、反向提问、去现场核对),再决定是否需要顾问。`,
251
305
  },
252
306
  {
253
307
  id: 'capability.subagent', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',