skyloom 1.13.5 → 1.13.7

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 (195) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +220 -159
  3. package/config/providers.yaml +39 -39
  4. package/config/skills/api_integrator/SKILL.md +15 -15
  5. package/config/skills/arch_designer/SKILL.md +13 -13
  6. package/config/skills/ci_cd_manager/SKILL.md +14 -14
  7. package/config/skills/code_analysis/SKILL.md +13 -13
  8. package/config/skills/code_generator/SKILL.md +12 -12
  9. package/config/skills/code_reviewer/SKILL.md +13 -13
  10. package/config/skills/content_writer/SKILL.md +14 -14
  11. package/config/skills/data_transformer/SKILL.md +15 -15
  12. package/config/skills/document_analysis/SKILL.md +13 -13
  13. package/config/skills/emotional_companion/SKILL.md +15 -15
  14. package/config/skills/performance_checker/SKILL.md +14 -14
  15. package/config/skills/security_auditor/SKILL.md +14 -14
  16. package/config/skills/self_evolve/SKILL.md +13 -13
  17. package/config/skills/sys_operator/SKILL.md +15 -15
  18. package/config/skills/task_planner/SKILL.md +14 -14
  19. package/config/skills/web_research/SKILL.md +14 -14
  20. package/config/skills/workflow_designer/SKILL.md +13 -13
  21. package/dist/agents/dew.js +52 -52
  22. package/dist/agents/fair.js +84 -84
  23. package/dist/agents/fog.js +30 -30
  24. package/dist/agents/frost.js +32 -32
  25. package/dist/agents/rain.js +32 -32
  26. package/dist/agents/snow.js +68 -68
  27. package/dist/cli/commands_md.d.ts +41 -0
  28. package/dist/cli/commands_md.d.ts.map +1 -0
  29. package/dist/cli/commands_md.js +140 -0
  30. package/dist/cli/commands_md.js.map +1 -0
  31. package/dist/cli/input_macros.d.ts +28 -0
  32. package/dist/cli/input_macros.d.ts.map +1 -0
  33. package/dist/cli/input_macros.js +120 -0
  34. package/dist/cli/input_macros.js.map +1 -0
  35. package/dist/cli/loom.d.ts +220 -0
  36. package/dist/cli/loom.d.ts.map +1 -0
  37. package/dist/cli/loom.js +1094 -0
  38. package/dist/cli/loom.js.map +1 -0
  39. package/dist/cli/loom_chat.d.ts +20 -0
  40. package/dist/cli/loom_chat.d.ts.map +1 -0
  41. package/dist/cli/loom_chat.js +685 -0
  42. package/dist/cli/loom_chat.js.map +1 -0
  43. package/dist/cli/main.js +310 -14
  44. package/dist/cli/main.js.map +1 -1
  45. package/dist/cli/tui.d.ts.map +1 -1
  46. package/dist/cli/tui.js +7 -1
  47. package/dist/cli/tui.js.map +1 -1
  48. package/dist/core/agent/guard.d.ts +45 -0
  49. package/dist/core/agent/guard.d.ts.map +1 -0
  50. package/dist/core/agent/guard.js +113 -0
  51. package/dist/core/agent/guard.js.map +1 -0
  52. package/dist/core/agent.d.ts +17 -0
  53. package/dist/core/agent.d.ts.map +1 -1
  54. package/dist/core/agent.js +182 -93
  55. package/dist/core/agent.js.map +1 -1
  56. package/dist/core/factory.d.ts.map +1 -1
  57. package/dist/core/factory.js +34 -2
  58. package/dist/core/factory.js.map +1 -1
  59. package/dist/core/file_checkpoint.d.ts +57 -0
  60. package/dist/core/file_checkpoint.d.ts.map +1 -0
  61. package/dist/core/file_checkpoint.js +162 -0
  62. package/dist/core/file_checkpoint.js.map +1 -0
  63. package/dist/core/hooks.d.ts +43 -0
  64. package/dist/core/hooks.d.ts.map +1 -0
  65. package/dist/core/hooks.js +110 -0
  66. package/dist/core/hooks.js.map +1 -0
  67. package/dist/core/llm.d.ts.map +1 -1
  68. package/dist/core/llm.js +15 -9
  69. package/dist/core/llm.js.map +1 -1
  70. package/dist/core/longdoc.js +5 -5
  71. package/dist/core/mcp.d.ts +16 -0
  72. package/dist/core/mcp.d.ts.map +1 -1
  73. package/dist/core/mcp.js +55 -0
  74. package/dist/core/mcp.js.map +1 -1
  75. package/dist/core/model_config.d.ts +40 -0
  76. package/dist/core/model_config.d.ts.map +1 -0
  77. package/dist/core/model_config.js +191 -0
  78. package/dist/core/model_config.js.map +1 -0
  79. package/dist/core/skill.d.ts +7 -0
  80. package/dist/core/skill.d.ts.map +1 -1
  81. package/dist/core/skill.js +47 -0
  82. package/dist/core/skill.js.map +1 -1
  83. package/dist/core/skymd.d.ts +39 -0
  84. package/dist/core/skymd.d.ts.map +1 -0
  85. package/dist/core/skymd.js +177 -0
  86. package/dist/core/skymd.js.map +1 -0
  87. package/dist/core/tool.d.ts +12 -0
  88. package/dist/core/tool.d.ts.map +1 -1
  89. package/dist/core/tool.js +30 -0
  90. package/dist/core/tool.js.map +1 -1
  91. package/dist/core/verify.d.ts +27 -0
  92. package/dist/core/verify.d.ts.map +1 -0
  93. package/dist/core/verify.js +62 -0
  94. package/dist/core/verify.js.map +1 -0
  95. package/dist/skills/loader.d.ts +22 -2
  96. package/dist/skills/loader.d.ts.map +1 -1
  97. package/dist/skills/loader.js +45 -15
  98. package/dist/skills/loader.js.map +1 -1
  99. package/dist/tools/builtin.d.ts.map +1 -1
  100. package/dist/tools/builtin.js +13 -3
  101. package/dist/tools/builtin.js.map +1 -1
  102. package/dist/tools/model_tool.d.ts +11 -0
  103. package/dist/tools/model_tool.d.ts.map +1 -0
  104. package/dist/tools/model_tool.js +71 -0
  105. package/dist/tools/model_tool.js.map +1 -0
  106. package/dist/tools/todo.d.ts +30 -0
  107. package/dist/tools/todo.d.ts.map +1 -0
  108. package/dist/tools/todo.js +78 -0
  109. package/dist/tools/todo.js.map +1 -0
  110. package/docs/AESTHETIC_DESIGN.md +152 -144
  111. package/docs/OPTIMIZATION_PLAN.md +178 -178
  112. package/package.json +1 -1
  113. package/scripts/install.js +48 -48
  114. package/scripts/link.js +10 -10
  115. package/setup.bat +79 -79
  116. package/skill-test-ty2fOA/test.md +10 -10
  117. package/src/agents/dew.ts +70 -70
  118. package/src/agents/fair.ts +102 -102
  119. package/src/agents/fog.ts +48 -48
  120. package/src/agents/frost.ts +50 -50
  121. package/src/agents/rain.ts +50 -50
  122. package/src/agents/snow.ts +239 -239
  123. package/src/cli/commands_md.ts +112 -0
  124. package/src/cli/input_macros.ts +83 -0
  125. package/src/cli/loom.ts +982 -0
  126. package/src/cli/loom_chat.ts +598 -0
  127. package/src/cli/main.ts +255 -9
  128. package/src/cli/mode.ts +58 -58
  129. package/src/cli/tui.ts +228 -222
  130. package/src/core/agent/guard.ts +134 -0
  131. package/src/core/agent/task.ts +100 -100
  132. package/src/core/agent.ts +177 -95
  133. package/src/core/arbitrate.ts +162 -162
  134. package/src/core/catalog.ts +178 -178
  135. package/src/core/checkpoint.ts +94 -94
  136. package/src/core/estimate.ts +104 -104
  137. package/src/core/evolve.ts +191 -191
  138. package/src/core/factory.ts +31 -2
  139. package/src/core/file_checkpoint.ts +136 -0
  140. package/src/core/filter.ts +103 -103
  141. package/src/core/graph.ts +156 -156
  142. package/src/core/hooks.ts +126 -0
  143. package/src/core/icons.ts +53 -53
  144. package/src/core/index.ts +37 -37
  145. package/src/core/learn.ts +146 -146
  146. package/src/core/llm.ts +15 -9
  147. package/src/core/longdoc.ts +155 -155
  148. package/src/core/mcp.ts +48 -0
  149. package/src/core/mcp_server.ts +176 -176
  150. package/src/core/model_config.ts +157 -0
  151. package/src/core/profile.ts +255 -255
  152. package/src/core/router.ts +124 -124
  153. package/src/core/sandbox.ts +142 -142
  154. package/src/core/security.ts +243 -243
  155. package/src/core/skill.ts +42 -0
  156. package/src/core/skymd.ts +143 -0
  157. package/src/core/theme.ts +65 -65
  158. package/src/core/tool.ts +30 -0
  159. package/src/core/tool_router.ts +193 -193
  160. package/src/core/vector.ts +152 -152
  161. package/src/core/verify.ts +71 -0
  162. package/src/core/workspace.ts +150 -150
  163. package/src/plugins/loader.ts +66 -66
  164. package/src/skills/loader.ts +45 -16
  165. package/src/sql.js.d.ts +29 -29
  166. package/src/tools/builtin.ts +13 -3
  167. package/src/tools/computer.ts +269 -269
  168. package/src/tools/delegate.ts +49 -49
  169. package/src/tools/model_tool.ts +74 -0
  170. package/src/tools/todo.ts +76 -0
  171. package/src/web/tts.ts +93 -93
  172. package/tests/agent.test.ts +159 -159
  173. package/tests/agent_helpers.test.ts +48 -48
  174. package/tests/bus.test.ts +121 -121
  175. package/tests/catalog.test.ts +86 -86
  176. package/tests/checkpoint_commands.test.ts +124 -0
  177. package/tests/claude_compat.test.ts +110 -0
  178. package/tests/config.test.ts +41 -41
  179. package/tests/guard.test.ts +75 -0
  180. package/tests/icons.test.ts +45 -45
  181. package/tests/loom.test.ts +248 -0
  182. package/tests/memory.test.ts +170 -170
  183. package/tests/model_config.test.ts +109 -0
  184. package/tests/router.test.ts +86 -86
  185. package/tests/schemas.test.ts +51 -51
  186. package/tests/semantic.test.ts +83 -83
  187. package/tests/setup.ts +10 -10
  188. package/tests/skill.test.ts +172 -172
  189. package/tests/skymd.test.ts +146 -0
  190. package/tests/task.test.ts +60 -60
  191. package/tests/todo_toolstats.test.ts +94 -0
  192. package/tests/tool.test.ts +108 -108
  193. package/tests/tool_router.test.ts +71 -71
  194. package/tests/tui.test.ts +67 -67
  195. package/vitest.config.ts +17 -17
@@ -7,6 +7,7 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.BaseAgent = exports.TaskResult = exports.Task = exports.TaskState = exports.AgentState = void 0;
10
+ exports.clampToolResult = clampToolResult;
10
11
  const bus_1 = require("./bus");
11
12
  const constants_1 = require("./constants");
12
13
  const logger_1 = require("./logger");
@@ -15,7 +16,26 @@ const skill_1 = require("./skill");
15
16
  const agent_helpers_1 = require("./agent_helpers");
16
17
  const tool_router_1 = require("./tool_router");
17
18
  const catalog_1 = require("./catalog");
19
+ const guard_1 = require("./agent/guard");
18
20
  const log = (0, logger_1.getLogger)('agent');
21
+ /** Tools whose success means the filesystem changed (triggers the verify loop). */
22
+ const WRITE_TOOL_RE = /^(write_|edit_|delete_|create_)|^run_bash$|^git_commit$/;
23
+ /** Tools with side effects, hidden from the model while in plan mode. */
24
+ const SIDE_EFFECT_TOOL_RE = /^(write_|edit_|delete_|create_|kill_|launch_|service_|browser_)|^run_bash$|^git_commit$|^open_path$|^delegate_to$/;
25
+ /** Default context budget per recorded tool result (chars; ~3k tokens). */
26
+ const TOOL_RESULT_LIMIT = 12000;
27
+ /**
28
+ * Clamp an oversized tool result before it enters the context window:
29
+ * keep head + tail, tell the model what was cut and how to fetch precisely.
30
+ */
31
+ function clampToolResult(s, limit = TOOL_RESULT_LIMIT) {
32
+ if (s.length <= limit)
33
+ return s;
34
+ const head = s.slice(0, Math.floor(limit * 0.72));
35
+ const tail = s.slice(-Math.floor(limit * 0.18));
36
+ const cut = s.length - head.length - tail.length;
37
+ return `${head}\n…[工具结果过长,中间省略 ${cut} 字符 — 需要该部分时用更精确的参数重新调用(read_file 的 offset/limit、grep 定位、缩小查询范围)]\n${tail}`;
38
+ }
19
39
  // Domain model lives in ./agent/task — re-exported here so importers of
20
40
  // '../core/agent' are unaffected by the Phase 3 split.
21
41
  const task_1 = require("./agent/task");
@@ -46,6 +66,11 @@ class BaseAgent {
46
66
  this._pendingRequests = new Map();
47
67
  this._bgTasks = new Set();
48
68
  this.approvalCallback = null;
69
+ /** Plan mode: read-only tool set + plan-first instructions on each turn. */
70
+ this.planMode = false;
71
+ /** Set when this turn executed a tool that mutates the filesystem (verify trigger). */
72
+ this._turnWroteFiles = false;
73
+ this._hooks = null;
49
74
  this._turnLock = Promise.resolve();
50
75
  this._turnLockCounter = 0;
51
76
  this._turnLockResolve = null;
@@ -128,10 +153,10 @@ class BaseAgent {
128
153
  const lang = this.config.llm?.language || 'zh';
129
154
  if (lang === 'en') {
130
155
  return prompt +
131
- `\n\n## Thinking Protocol\nBefore acting, briefly weigh: (1) **What** is the actual need? (2) **How** sure am I? If <80%, flag with [uncertain] and ask.\nIf stuck, admit it — propose a partial answer or ask the user. Never fabricate.\n\n## Behavior\n- Act, don't narrate. No "I will..." before tool calls.\n- Stay in scope. Do what's asked, then stop.\n- Batch independent tool calls in one response.\n- Verify writes: read back, report verified state.\n- Call list_skills when the task needs specialized capabilities.`;
156
+ `\n\n## Thinking Protocol\nBefore acting, briefly weigh: (1) **What** is the actual need? (2) **How** sure am I? If <80%, flag with [uncertain] and ask.\nIf stuck, admit it — propose a partial answer or ask the user. Never fabricate.\n\n## Behavior\n- Act, don't narrate. No "I will..." before tool calls.\n- Stay in scope. Do what's asked, then stop.\n- Batch independent tool calls in one response.\n- For tasks with 3+ steps, plan with todo_write first and update item status as you go.\n- Verify writes: read back, report verified state.\n- Call list_skills when the task needs specialized capabilities.`;
132
157
  }
133
158
  return prompt +
134
- `\n\n## 思考协议\n行动前快速判断:(1) 用户真实需求是什么?(2) 我有多大把握?低于80%标注 [不确定] 并主动询问。\n卡住时承认,给出部分答案或请求用户指导。绝不编造。\n\n## 行为守则\n- 直接行动,不预告。不说「我将要...」,直接调用工具\n- 不擅自扩大范围。用户要什么做什么,核心完成即止\n- 独立的工具调用一次发出,并行执行\n- 写入后回读验证,汇报已验证状态而非仅尝试\n- 任务涉及专业能力时(PPT/Excel/PDF/网页设计/代码审查等),先调 list_skills 查看可用技能,再用 use_skill 激活`;
159
+ `\n\n## 思考协议\n行动前快速判断:(1) 用户真实需求是什么?(2) 我有多大把握?低于80%标注 [不确定] 并主动询问。\n卡住时承认,给出部分答案或请求用户指导。绝不编造。\n\n## 行为守则\n- 直接行动,不预告。不说「我将要...」,直接调用工具\n- 不擅自扩大范围。用户要什么做什么,核心完成即止\n- 独立的工具调用一次发出,并行执行\n- 3 步以上的任务先用 todo_write 列任务清单,开工/完成时逐项更新状态\n- 写入后回读验证,汇报已验证状态而非仅尝试\n- 任务涉及专业能力时(PPT/Excel/PDF/网页设计/代码审查等),先调 list_skills 查看可用技能,再用 use_skill 激活`;
135
160
  }
136
161
  injectProgrammingWisdom(prompt) {
137
162
  const lang = this.config.llm?.language || 'zh';
@@ -140,15 +165,33 @@ class BaseAgent {
140
165
  }
141
166
  return prompt + `\n\n## 工程能力\n顶级工程师:类型安全、真实的错误处理、按根因调试、按安全与性能审查。你可以阅读和修改 Skyloom 自身源码。`;
142
167
  }
168
+ /** Layered SKY.md / CLAUDE.md / AGENTS.md project memory (see core/skymd). */
169
+ injectProjectMemory(prompt) {
170
+ try {
171
+ const { loadProjectMemory } = require('./skymd');
172
+ const mem = loadProjectMemory();
173
+ if (!mem.text)
174
+ return prompt;
175
+ return prompt + `\n\n## 项目记忆 (SKY.md)\n用户与项目维护的约定,优先级高于你的通用习惯:\n\n${mem.text}`;
176
+ }
177
+ catch {
178
+ return prompt;
179
+ }
180
+ }
143
181
  reinitLanguage() {
144
182
  this._baseSystemPrompt = '';
145
183
  this._baseSystemPrompt = this.resolveSystemPrompt();
146
184
  this._baseSystemPrompt = this.injectWorkspaceInfo(this._baseSystemPrompt);
147
185
  this._baseSystemPrompt = this.injectBehaviorRules(this._baseSystemPrompt);
148
186
  this._baseSystemPrompt = this.injectProgrammingWisdom(this._baseSystemPrompt);
187
+ this._baseSystemPrompt = this.injectProjectMemory(this._baseSystemPrompt);
149
188
  this._baseSystemPrompt += '\n\n' + this.currentTimeTag();
150
189
  this.rebuildSystemPrompt();
151
190
  }
191
+ /** Re-read SKY.md layers into the system prompt (after `#` quick memory / edits). */
192
+ reloadProjectMemory() {
193
+ this.reinitLanguage();
194
+ }
152
195
  async init() {
153
196
  if (this._baseSystemPrompt)
154
197
  return;
@@ -165,6 +208,7 @@ class BaseAgent {
165
208
  this._baseSystemPrompt = this.injectWorkspaceInfo(this._baseSystemPrompt);
166
209
  this._baseSystemPrompt = this.injectBehaviorRules(this._baseSystemPrompt);
167
210
  this._baseSystemPrompt = this.injectProgrammingWisdom(this._baseSystemPrompt);
211
+ this._baseSystemPrompt = this.injectProjectMemory(this._baseSystemPrompt);
168
212
  this._baseSystemPrompt += '\n\n' + this.currentTimeTag();
169
213
  this.rebuildSystemPrompt();
170
214
  this._tools = this.toolRegistry.getTools();
@@ -187,6 +231,14 @@ class BaseAgent {
187
231
  description: 'List all available skills with their names and descriptions. Use this first to discover what skills you can activate.',
188
232
  parameters: [],
189
233
  handler: async () => {
234
+ // live change detection: re-scan user/project skill folders so a
235
+ // SKILL.md edit or drop-in applies without restarting the session
236
+ try {
237
+ const { registerDynamicSkills } = require('../skills/loader');
238
+ registerDynamicSkills(self.skillRegistry);
239
+ self.loadSkills();
240
+ }
241
+ catch { /* live reload is best-effort */ }
190
242
  const skills = self.getAvailableSkills();
191
243
  if (!skills.length)
192
244
  return 'No skills available.';
@@ -509,9 +561,40 @@ class BaseAgent {
509
561
  if (onStatus)
510
562
  onStatus(p.label);
511
563
  await this.setState(task_1.AgentState.ACTING);
564
+ // File checkpoint: snapshot the target before any mutating file tool
565
+ // runs, so /rewind can restore the pre-turn state.
566
+ try {
567
+ const { getFileCheckpoints } = require('./file_checkpoint');
568
+ const cp = getFileCheckpoints();
569
+ const snapPath = cp.pathToSnapshot(p.toolName, p.toolArgs || {});
570
+ if (snapPath)
571
+ cp.snapshot(snapPath);
572
+ }
573
+ catch { /* checkpointing must never block execution */ }
574
+ // pre_tool hooks are enforced policy: a non-zero exit blocks the call.
575
+ const hooks = this.getHooks();
576
+ if (hooks.preTool.length > 0) {
577
+ try {
578
+ const { runPreToolHooks } = require('./hooks');
579
+ const pre = runPreToolHooks(hooks, p.toolName, p.toolArgs || {}, this.name);
580
+ if (!pre.allowed) {
581
+ return { idx, result: { tc: p.tc, result: `[blocked by pre_tool hook] ${pre.reason}`, success: false, toolName: p.toolName } };
582
+ }
583
+ }
584
+ catch { /* hook machinery must never break tool execution */ }
585
+ }
512
586
  try {
513
587
  const toolResult = await this.toolRegistry.execute(p.toolName, p.toolArgs || {});
514
588
  const resultStr = toolResult.result || toolResult.error || '(no output)';
589
+ if (toolResult.success && WRITE_TOOL_RE.test(p.toolName))
590
+ this._turnWroteFiles = true;
591
+ if (hooks.postTool.length > 0) {
592
+ try {
593
+ const { runPostToolHooks } = require('./hooks');
594
+ runPostToolHooks(hooks, p.toolName, p.toolArgs || {}, this.name);
595
+ }
596
+ catch { /* best-effort */ }
597
+ }
515
598
  return { idx, result: { tc: p.tc, result: resultStr, success: toolResult.success, toolName: p.toolName } };
516
599
  }
517
600
  catch (e) {
@@ -532,7 +615,9 @@ class BaseAgent {
532
615
  }
533
616
  }
534
617
  }
535
- // Phase D: Record results to memory
618
+ // Phase D: Record results to memory (clamped — one runaway read_file or
619
+ // http_get must not flood the context window)
620
+ const resultLimit = Number(this.config?.llm?.tool_result_limit) || undefined;
536
621
  for (const r of results) {
537
622
  if (!r)
538
623
  continue;
@@ -540,7 +625,7 @@ class BaseAgent {
540
625
  if (suppressed)
541
626
  suppressed.add(r.toolName);
542
627
  }
543
- this.memory.addMessage('tool', r.result, {
628
+ this.memory.addMessage('tool', clampToolResult(r.result, resultLimit), {
544
629
  name: r.toolName,
545
630
  toolCallId: r.tc.id,
546
631
  ephemeral,
@@ -658,7 +743,16 @@ class BaseAgent {
658
743
  }
659
744
  async *chatStreamImpl(message, autoActivated, signal) {
660
745
  await this.setState(task_1.AgentState.THINKING);
661
- this.memory.addMessage('user', message);
746
+ // Plan mode: the tag travels with the message so the model plans instead
747
+ // of acting, and the read-only tool filter below removes the temptation.
748
+ const userMessage = this.planMode
749
+ ? `[计划模式] 只读调研,不要执行任何修改。请输出一份编号的执行计划(涉及哪些文件、每步做什么、风险点),等待用户批准后再实施。\n\n${message}`
750
+ : message;
751
+ this.memory.addMessage('user', userMessage);
752
+ try {
753
+ require('./file_checkpoint').getFileCheckpoints().beginTurn(message);
754
+ }
755
+ catch { /* optional */ }
662
756
  let assistantStored = false;
663
757
  if (this.shouldAutoCompact()) {
664
758
  try {
@@ -675,19 +769,22 @@ class BaseAgent {
675
769
  this.memory.addMessage('system', '[Auto-activated skills: ' + autoActivated.join(', ') +
676
770
  '] These were chosen from your message\'s keywords. Do NOT call list_skills.');
677
771
  }
678
- const recentToolOutcomes = [];
679
- let stuckHintInjected = false;
680
- const recentResponseTexts = [];
681
- let repetitionHintInjected = false;
682
- const recentToolSigs = [];
683
- let toolLoopHintInjected = false;
772
+ const guard = new guard_1.LoopGuard();
684
773
  let toolNamesCache = null;
685
774
  let cacheKey = null;
686
775
  const resolveToolNames = () => {
687
- const key = JSON.stringify([[...suppressedTools].sort(), [...this._activeSkills].sort()]);
776
+ const key = JSON.stringify([[...suppressedTools].sort(), [...this._activeSkills].sort(), this.planMode]);
688
777
  if (toolNamesCache !== null && cacheKey === key)
689
778
  return toolNamesCache;
690
779
  let candidates = this.activeToolNames().filter(t => !suppressedTools.has(t));
780
+ if (this.planMode) {
781
+ candidates = candidates.filter(n => {
782
+ if (SIDE_EFFECT_TOOL_RE.test(n))
783
+ return false;
784
+ const t = this.toolRegistry.get(n);
785
+ return !t?.dangerous;
786
+ });
787
+ }
691
788
  const must = new Set();
692
789
  for (const s of this._skills) {
693
790
  if (this._activeSkills.has(s.name)) {
@@ -829,86 +926,13 @@ class BaseAgent {
829
926
  yield { type: 'done' };
830
927
  return;
831
928
  }
832
- // ── Narration-loop detection ──
833
- const normalizedRound = roundContent.trim();
834
- if (normalizedRound && recentResponseTexts.length > 0) {
835
- const highSim = recentResponseTexts.slice(-2).some(prev => (0, agent_helpers_1.textSimilarity)(normalizedRound, prev) >= 0.7);
836
- if (highSim && !repetitionHintInjected) {
837
- this.memory.addMessage('system', '[Stop narrating] Your last response is highly similar to your previous one. Stop writing prose. Either: (1) emit ONLY the next tool call, or (2) output the final deliverable.');
838
- repetitionHintInjected = true;
839
- }
840
- }
841
- recentResponseTexts.push(normalizedRound);
842
- if (recentResponseTexts.length > 3)
843
- recentResponseTexts.shift();
844
- // ── Tool-signature loop detection ──
845
- for (const tc of toolCallsReceived) {
846
- const tName = tc.function.name;
847
- if (['task_done', 'list_skills', 'use_skill'].includes(tName))
848
- continue;
849
- const rawArgs = tc.function.arguments;
850
- const tArgs = typeof rawArgs === 'string' ? (0, agent_helpers_1.parseToolArgs)(rawArgs) : rawArgs;
851
- const sig = (0, agent_helpers_1.toolCallSignature)(tName, tArgs);
852
- if (sig)
853
- recentToolSigs.push(sig);
854
- }
855
- if (recentToolSigs.length > agent_helpers_1.SIG_WINDOW) {
856
- recentToolSigs.splice(0, recentToolSigs.length - agent_helpers_1.SIG_WINDOW);
857
- }
858
- if (recentToolSigs.length > 0) {
859
- const counts = new Map();
860
- for (const s of recentToolSigs)
861
- counts.set(s, (counts.get(s) || 0) + 1);
862
- let topSig = '';
863
- let topCount = 0;
864
- for (const [s, c] of counts) {
865
- if (c > topCount) {
866
- topSig = s;
867
- topCount = c;
868
- }
869
- }
870
- if (topCount >= agent_helpers_1.SIG_LOOP_HINT && !toolLoopHintInjected) {
871
- this.memory.addMessage('system', `[Tool loop] You have called \`${topSig}\` ${topCount}x in the last ${recentToolSigs.length} tool calls — you are iterating without converging. STOP repeating it.`);
872
- toolLoopHintInjected = true;
873
- }
874
- if (topCount >= agent_helpers_1.SIG_LOOP_HARDSTOP) {
875
- this.memory.addMessage('assistant', `I have repeated \`${topSig}\` ${topCount} times without converging. Stopping.`);
876
- yield { type: 'content', text: `\n\n[stuck] tool \`${topSig}\` repeated ${topCount}x — stopping.` };
877
- await this.setState(task_1.AgentState.IDLE);
878
- yield { type: 'done' };
879
- return;
880
- }
881
- }
882
- // ── Stuck-loop detection ──
883
- for (const r of execResults) {
884
- if (!r || r.toolName === 'task_done')
885
- continue;
886
- const failed = !r.success || (typeof r.result === 'string' && (0, agent_helpers_1.looksLikeFailedToolResult)(r.result));
887
- recentToolOutcomes.push(!failed);
888
- if (recentToolOutcomes.length > 6)
889
- recentToolOutcomes.shift();
890
- }
891
- if (!stuckHintInjected && recentToolOutcomes.length >= 5 &&
892
- recentToolOutcomes.filter(Boolean).length <= 1) {
893
- this.memory.addMessage('system', '[Recovery hint] Your last several tool calls have mostly failed. Synthesize a partial answer from what worked or ask the user for guidance.');
894
- stuckHintInjected = true;
895
- }
896
- if (recentToolOutcomes.length >= 8 && recentToolOutcomes.every(x => !x)) {
897
- this.memory.addMessage('assistant', 'Every recent tool call failed. Please give me more context.');
898
- yield { type: 'content', text: '\n\n[stuck] every recent tool call failed — stopping.\n' };
899
- await this.setState(task_1.AgentState.IDLE);
900
- yield { type: 'done' };
901
- return;
902
- }
903
- // ── Search-storm detection ──
904
- const searchStormCount = recentToolSigs.filter(s => s.startsWith('web_search:') || ['fetch_page', 'http_get'].includes(s)).length;
905
- if (searchStormCount >= 8 && !toolLoopHintInjected) {
906
- this.memory.addMessage('system', `[Search storm] ${searchStormCount} search calls. STOP searching and synthesize.`);
907
- toolLoopHintInjected = true;
908
- }
909
- if (searchStormCount >= 12) {
910
- this.memory.addMessage('assistant', 'Too many search requests. Synthesizing best answer.');
911
- yield { type: 'content', text: `\n\n[stuck] excessive web searching (${searchStormCount} calls) — stopping.\n` };
929
+ // ── Anti-loop guard (narration / tool-signature / stuck / search-storm) ──
930
+ const decision = guard.observe(roundContent, toolCallsReceived, execResults);
931
+ for (const hint of decision.hints)
932
+ this.memory.addMessage('system', hint);
933
+ if (decision.stop) {
934
+ this.memory.addMessage('assistant', decision.stop.note);
935
+ yield { type: 'content', text: decision.stop.contentLine };
912
936
  await this.setState(task_1.AgentState.IDLE);
913
937
  yield { type: 'done' };
914
938
  return;
@@ -1012,6 +1036,24 @@ class BaseAgent {
1012
1036
  model: this.resolveModelId(),
1013
1037
  };
1014
1038
  }
1039
+ /** Per-role token breakdown for the /context command. */
1040
+ contextDetail() {
1041
+ const byRole = {};
1042
+ for (const m of this.memory.shortTerm) {
1043
+ const extra = m.toolCalls ? JSON.stringify(m.toolCalls).length : 0;
1044
+ const tokens = Math.ceil(((m.content || '').length + extra) / 4);
1045
+ const slot = byRole[m.role] || (byRole[m.role] = { tokens: 0, count: 0 });
1046
+ slot.tokens += tokens;
1047
+ slot.count += 1;
1048
+ }
1049
+ return {
1050
+ ...this.contextUsage(),
1051
+ byRole,
1052
+ systemPromptTokens: Math.ceil(this._baseSystemPrompt.length / 4),
1053
+ toolCount: this.activeToolNames().length,
1054
+ activeSkills: [...this._activeSkills],
1055
+ };
1056
+ }
1015
1057
  shouldAutoCompact() {
1016
1058
  const usage = this.memory.getContextWindowUsage();
1017
1059
  // Compact before hitting the real window — leave ~20% headroom for the reply.
@@ -1205,8 +1247,43 @@ class BaseAgent {
1205
1247
  }
1206
1248
  this.memory.addMessage('user', prompt);
1207
1249
  const preLen = this.memory.shortTerm.length;
1250
+ this._turnWroteFiles = false;
1251
+ try {
1252
+ require('./file_checkpoint').getFileCheckpoints().beginTurn(`[task] ${task.description}`);
1253
+ }
1254
+ catch { /* optional */ }
1208
1255
  try {
1209
- const response = await this.llmLoop({ onStatus, ephemeral: true });
1256
+ let response = await this.llmLoop({ onStatus, ephemeral: true });
1257
+ // ── 验证闭环: if this task touched the filesystem and verify commands
1258
+ // are configured (config.verify or SKY.md "## Verify"), run them and
1259
+ // feed failures back for a bounded number of fix rounds. ──
1260
+ try {
1261
+ const { resolveVerifyConfig, runVerify } = require('./verify');
1262
+ const vc = resolveVerifyConfig(this.config);
1263
+ if (vc.commands.length > 0 && this._turnWroteFiles) {
1264
+ for (let round = 0; round <= vc.maxFixRounds; round++) {
1265
+ if (onStatus)
1266
+ onStatus(`verify: ${vc.commands.length} 条命令`);
1267
+ const vr = runVerify(vc);
1268
+ if (vr.ok) {
1269
+ response.content += `\n\n[verify ✓ 全部通过]\n${vr.report}`;
1270
+ break;
1271
+ }
1272
+ if (round === vc.maxFixRounds) {
1273
+ response.content += `\n\n[verify ✗ 经 ${vc.maxFixRounds} 轮修复仍未通过]\n${vr.report.slice(0, 1500)}`;
1274
+ break;
1275
+ }
1276
+ if (onStatus)
1277
+ onStatus(`verify 失败 — 修复第 ${round + 1}/${vc.maxFixRounds} 轮`);
1278
+ log.warn('verify_failed_fixing', { agent: this.name, round: round + 1 });
1279
+ this.memory.addMessage('user', `[自动验证失败] 以下验证命令未通过。请定位根因并修复,确保它们全部通过:\n\n${vr.report}`);
1280
+ response = await this.llmLoop({ onStatus, ephemeral: true });
1281
+ }
1282
+ }
1283
+ }
1284
+ catch (e) {
1285
+ log.warn('verify_loop_error', { error: String(e) });
1286
+ }
1210
1287
  const filePaths = (0, agent_helpers_1.extractFilePathsFromMessages)(this.memory.shortTerm.slice(preLen));
1211
1288
  const enriched = (0, agent_helpers_1.enrichResponseWithArtifacts)(response.content, filePaths);
1212
1289
  this.memory.addMessage('assistant', enriched, { toolCalls: response.toolCalls, reasoningContent: response.reasoningContent });
@@ -1236,6 +1313,18 @@ class BaseAgent {
1236
1313
  this._security = {};
1237
1314
  }
1238
1315
  } return this._security; }
1316
+ getHooks() {
1317
+ if (!this._hooks) {
1318
+ try {
1319
+ const { loadHooks } = require('./hooks');
1320
+ this._hooks = loadHooks(this.config);
1321
+ }
1322
+ catch {
1323
+ this._hooks = { sessionStart: [], preTool: [], postTool: [] };
1324
+ }
1325
+ }
1326
+ return this._hooks;
1327
+ }
1239
1328
  async checkToolApproval(toolName, toolArgs) {
1240
1329
  try {
1241
1330
  const sec = this.security;