@xiaobuyu/nodesign 0.0.1 → 0.0.2

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 (282) hide show
  1. package/README.md +156 -106
  2. package/bin/nodesign.js +0 -0
  3. package/package.json +1 -1
  4. package/server/api/README.md +45 -45
  5. package/server/api/_guard.js +75 -75
  6. package/server/api/admin.js +187 -187
  7. package/server/api/assets/docx-page.js +103 -103
  8. package/server/api/assets/helpers.js +22 -22
  9. package/server/api/assets/notes.js +144 -144
  10. package/server/api/assets.js +896 -896
  11. package/server/api/board.js +57 -57
  12. package/server/api/browse.js +138 -138
  13. package/server/api/canvas.js +444 -444
  14. package/server/api/chatai.js +324 -324
  15. package/server/api/exports/build-standalone.js +980 -980
  16. package/server/api/exports/cards.js +118 -118
  17. package/server/api/exports/docx-pdf.js +46 -46
  18. package/server/api/exports/export-page.js +142 -142
  19. package/server/api/exports/handoff.js +147 -147
  20. package/server/api/exports.js +690 -690
  21. package/server/api/instruction.js +75 -75
  22. package/server/api/me.js +116 -116
  23. package/server/api/memory.js +185 -185
  24. package/server/api/pending-changes.js +366 -366
  25. package/server/api/plugins.js +164 -164
  26. package/server/api/projects.js +220 -220
  27. package/server/api/publish.js +64 -64
  28. package/server/api/recent.js +73 -73
  29. package/server/api/sessions.js +589 -589
  30. package/server/api/skills.js +77 -77
  31. package/server/api/standalone-fit.js +168 -168
  32. package/server/api/turn-compose.js +180 -180
  33. package/server/api/turn-inflight.js +44 -44
  34. package/server/auth/README.md +17 -17
  35. package/server/auth/middleware.js +138 -138
  36. package/server/auth/origin-guard.js +71 -71
  37. package/server/auth/session.js +108 -108
  38. package/server/auth/tier.js +113 -113
  39. package/server/auth/users-store.js +348 -348
  40. package/server/edit/README.md +29 -29
  41. package/server/engine/README.md +113 -113
  42. package/server/engine/agent/agent-shared.js +550 -550
  43. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
  44. package/server/engine/agent/auto-mode-rules.js +78 -78
  45. package/server/engine/agent/context.js +418 -418
  46. package/server/engine/agent/events.js +464 -464
  47. package/server/engine/agent/hooks/canvas-validate.js +199 -199
  48. package/server/engine/agent/hooks/failure.js +120 -120
  49. package/server/engine/agent/hooks/file-events.js +75 -75
  50. package/server/engine/agent/hooks/lifecycle.js +233 -233
  51. package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
  52. package/server/engine/agent/hooks/post-guidance.js +206 -206
  53. package/server/engine/agent/hooks/post-subagent-report.js +104 -104
  54. package/server/engine/agent/hooks/post-trim.js +58 -58
  55. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
  56. package/server/engine/agent/hooks/pre-defaults.js +84 -84
  57. package/server/engine/agent/hooks/pre-injectors.js +277 -277
  58. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
  59. package/server/engine/agent/hooks/pre-starter-files.js +61 -61
  60. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
  61. package/server/engine/agent/hooks/site-validate.js +94 -94
  62. package/server/engine/agent/hooks/tool-prompts.js +31 -31
  63. package/server/engine/agent/hooks/turn-state-memory.js +56 -56
  64. package/server/engine/agent/hooks/user-prompt-submit.js +215 -215
  65. package/server/engine/agent/hooks.js +354 -354
  66. package/server/engine/agent/init-contract.js +69 -69
  67. package/server/engine/agent/isolation.js +159 -159
  68. package/server/engine/agent/model-table.js +292 -292
  69. package/server/engine/agent/plugin-loader.js +266 -266
  70. package/server/engine/agent/prompts/nodesign-prelude.md +336 -336
  71. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
  72. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
  73. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
  74. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
  75. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
  76. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
  77. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
  78. package/server/engine/agent/prompts/tools/site-reference.md +106 -106
  79. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
  80. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
  81. package/server/engine/agent/session-loop.js +1024 -1024
  82. package/server/engine/agent/session-model.js +164 -164
  83. package/server/engine/agent/skill.js +204 -204
  84. package/server/engine/agent/task-events.js +78 -78
  85. package/server/engine/agents/ds-extractor.md +132 -132
  86. package/server/engine/agents/explorer.md +190 -190
  87. package/server/engine/agents/index.js +230 -230
  88. package/server/engine/agents/schemas/design-system.json +156 -156
  89. package/server/engine/agents/schemas/tweak-schema.json +113 -113
  90. package/server/engine/agents/tweak-proposer.md +127 -127
  91. package/server/engine/agents/vision-checker.md +254 -254
  92. package/server/engine/browse/capture.js +422 -422
  93. package/server/engine/browse/card.js +137 -137
  94. package/server/engine/browse/handover.js +59 -59
  95. package/server/engine/browse/page-digest.js +119 -119
  96. package/server/engine/browse/refs.js +196 -196
  97. package/server/engine/browse/registry.js +307 -307
  98. package/server/engine/browse/screencast.js +170 -170
  99. package/server/engine/browse/state.js +121 -121
  100. package/server/engine/chatai/chat-log.js +79 -79
  101. package/server/engine/chatai/index.js +193 -193
  102. package/server/engine/chatai/openai-compat.js +152 -152
  103. package/server/engine/chatai/orchestrate.js +309 -309
  104. package/server/engine/chatai/perform.js +40 -40
  105. package/server/engine/chatai/summarize.js +79 -79
  106. package/server/engine/mcp/capability-gate.js +52 -52
  107. package/server/engine/mcp/index.js +339 -339
  108. package/server/engine/mcp/param-sanitizer.js +142 -142
  109. package/server/engine/mcp/tools/arrange-on-board.js +124 -124
  110. package/server/engine/mcp/tools/artifact-session.js +278 -278
  111. package/server/engine/mcp/tools/browse-computer.js +421 -421
  112. package/server/engine/mcp/tools/browse-find-batch.js +183 -183
  113. package/server/engine/mcp/tools/browse-screenshot.js +157 -157
  114. package/server/engine/mcp/tools/browse.js +480 -480
  115. package/server/engine/mcp/tools/build-docx.js +101 -101
  116. package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
  117. package/server/engine/mcp/tools/create-on-board.js +155 -155
  118. package/server/engine/mcp/tools/crystallize-skill.js +168 -168
  119. package/server/engine/mcp/tools/explain-style.js +237 -237
  120. package/server/engine/mcp/tools/export-handoff.js +133 -133
  121. package/server/engine/mcp/tools/expose-tweaks.js +149 -149
  122. package/server/engine/mcp/tools/generate-image.js +842 -842
  123. package/server/engine/mcp/tools/get-computed-styles.js +164 -164
  124. package/server/engine/mcp/tools/get-pending-changes.js +205 -205
  125. package/server/engine/mcp/tools/h3box-ssh.js +84 -84
  126. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
  127. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
  128. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
  129. package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
  130. package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
  131. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
  132. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
  133. package/server/engine/mcp/tools/highlight.js +68 -68
  134. package/server/engine/mcp/tools/list-pages.js +222 -222
  135. package/server/engine/mcp/tools/lookup-tags.js +59 -59
  136. package/server/engine/mcp/tools/navigate-to-page.js +60 -60
  137. package/server/engine/mcp/tools/organize-board.js +64 -64
  138. package/server/engine/mcp/tools/paint-still.js +434 -434
  139. package/server/engine/mcp/tools/pin-to-board.js +130 -130
  140. package/server/engine/mcp/tools/preview-deck.js +130 -130
  141. package/server/engine/mcp/tools/profile-scroll.js +305 -305
  142. package/server/engine/mcp/tools/publish-site.js +102 -102
  143. package/server/engine/mcp/tools/query-elements.js +195 -195
  144. package/server/engine/mcp/tools/read-board.js +103 -103
  145. package/server/engine/mcp/tools/read-document.js +93 -93
  146. package/server/engine/mcp/tools/read-page.js +264 -264
  147. package/server/engine/mcp/tools/read-tavern-json.js +143 -143
  148. package/server/engine/mcp/tools/record-decision.js +140 -140
  149. package/server/engine/mcp/tools/relate-on-board.js +110 -110
  150. package/server/engine/mcp/tools/report-issue.js +115 -115
  151. package/server/engine/mcp/tools/roll-film.js +266 -266
  152. package/server/engine/mcp/tools/screenshot-docx.js +138 -138
  153. package/server/engine/mcp/tools/screenshot-url.js +195 -195
  154. package/server/engine/mcp/tools/screenshot.js +583 -583
  155. package/server/engine/mcp/tools/tier-gate.js +96 -96
  156. package/server/engine/mcp/tools/trace-motion.js +215 -215
  157. package/server/engine/mcp/tools/web-search.js +548 -548
  158. package/server/engine/motion/inventory.js +349 -349
  159. package/server/engine/perception/session.js +235 -235
  160. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
  161. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -192
  162. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
  163. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
  164. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
  165. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
  166. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
  167. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
  168. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
  169. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -162
  170. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
  171. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
  172. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -228
  173. package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -78
  174. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
  175. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -323
  176. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
  177. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
  178. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
  179. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
  180. package/server/engine/runs/active-runs.js +698 -698
  181. package/server/engine/runs/live-turn.js +252 -252
  182. package/server/engine/runs/store.js +364 -364
  183. package/server/engine/runs/turn-relay.js +217 -217
  184. package/server/engine/runtime/workspace.js +164 -164
  185. package/server/lib/artifact-file-path.js +80 -80
  186. package/server/lib/artifact-target.js +284 -284
  187. package/server/lib/asset-refs.js +239 -239
  188. package/server/lib/async-queue.js +104 -104
  189. package/server/lib/auto-relations.js +130 -130
  190. package/server/lib/binding-types.js +60 -60
  191. package/server/lib/board-kind-sizes.js +62 -62
  192. package/server/lib/board-relations.js +133 -133
  193. package/server/lib/browse-proxy.js +180 -180
  194. package/server/lib/canvas-id.js +36 -36
  195. package/server/lib/cover.js +227 -227
  196. package/server/lib/danbooru-tags.js +291 -291
  197. package/server/lib/doc-extract.js +156 -156
  198. package/server/lib/docx/build-from-source.js +260 -260
  199. package/server/lib/docx/build.js +490 -490
  200. package/server/lib/docx/dump-styles.js +287 -287
  201. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
  202. package/server/lib/docx/merge-runs.js +129 -129
  203. package/server/lib/docx/numbering.js +218 -218
  204. package/server/lib/docx/order.js +152 -152
  205. package/server/lib/docx/rawzip.js +172 -172
  206. package/server/lib/docx/render.js +92 -92
  207. package/server/lib/docx/text-lint.js +225 -225
  208. package/server/lib/docx/tokens.js +335 -335
  209. package/server/lib/docx/units.js +29 -29
  210. package/server/lib/docx/xml.js +291 -291
  211. package/server/lib/docx-pages.js +187 -187
  212. package/server/lib/export-collect.js +289 -289
  213. package/server/lib/export-package.js +210 -210
  214. package/server/lib/html-srcset.js +145 -145
  215. package/server/lib/image-variant.js +460 -460
  216. package/server/lib/ingress/forward-openai-chat.js +305 -305
  217. package/server/lib/ingress/openai-chat.js +491 -491
  218. package/server/lib/ingress/session-notice.js +66 -66
  219. package/server/lib/ingress/session-routes.js +68 -68
  220. package/server/lib/ingress/slot-probe.js +130 -130
  221. package/server/lib/ingress/upstream-billing.js +52 -52
  222. package/server/lib/ingress/upstream-fail-streak.js +72 -72
  223. package/server/lib/ingress/upstream-truncation.js +48 -48
  224. package/server/lib/issues-store.js +182 -182
  225. package/server/lib/kinds/deck.js +72 -72
  226. package/server/lib/kinds/docx.js +283 -283
  227. package/server/lib/kinds/file-kinds.js +113 -113
  228. package/server/lib/kinds/index.js +202 -202
  229. package/server/lib/kinds/site.js +161 -161
  230. package/server/lib/model-ingress.js +543 -543
  231. package/server/lib/moderation.js +255 -255
  232. package/server/lib/notice-store.js +103 -103
  233. package/server/lib/plugin-install.js +104 -104
  234. package/server/lib/plugin-validator.js +721 -721
  235. package/server/lib/publish-store.js +115 -115
  236. package/server/lib/quick-summary.js +52 -52
  237. package/server/lib/quota.js +270 -270
  238. package/server/lib/rate-window.js +29 -29
  239. package/server/lib/reference-assets.js +92 -92
  240. package/server/lib/region-shot.js +135 -135
  241. package/server/lib/safe-path.js +73 -73
  242. package/server/lib/sdk-session.js +33 -33
  243. package/server/lib/showcase-store.js +92 -92
  244. package/server/lib/site-publish.js +465 -465
  245. package/server/lib/ssrf-guard.js +432 -432
  246. package/server/lib/task-scan.js +158 -158
  247. package/server/lib/tavern-json.js +154 -154
  248. package/server/lib/video-variant.js +237 -237
  249. package/server/lib/workspace-path.js +33 -33
  250. package/server/ops/fix-sdk-musl.mjs +40 -40
  251. package/server/ops/install-macos-fonts.sh +114 -114
  252. package/server/ops/macos-fonts.conf +336 -336
  253. package/server/ops/sandbox-shim/bwrap +57 -57
  254. package/server/projects/assets-summary.js +119 -119
  255. package/server/projects/auto-name.js +53 -53
  256. package/server/projects/board-store.js +640 -640
  257. package/server/projects/move-entry.js +103 -103
  258. package/server/projects/store.js +270 -270
  259. package/server/projects/ui-config.js +54 -54
  260. package/server/projects/workspace-templates.js +53 -53
  261. package/server/projects/workspace.js +1025 -1025
  262. package/server/runtime/local-config.js +177 -177
  263. package/server/runtime/profile.js +72 -72
  264. package/server/services/rembg-service.py +334 -334
  265. package/server/shared/README.md +25 -25
  266. package/server/shared/deck.js +54 -54
  267. package/server/shared/time.js +49 -49
  268. package/server/style-pipeline/README.md +53 -53
  269. package/server/ws/broker.js +36 -36
  270. package/server/ws/browse-channel.js +177 -177
  271. package/server/ws/index.js +386 -386
  272. package/web/README.md +89 -89
  273. package/web/dist/assets/BrowserWindow-BYCgizHw.js +11 -0
  274. package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-pNydEM4L.js} +1 -1
  275. package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-PPgzh3Gq.js} +1 -1
  276. package/web/dist/assets/{SiteWindow-D6eS-2gM.js → SiteWindow-DM0LQPCO.js} +1 -1
  277. package/web/dist/assets/{index-Cbl3ATvp.js → index-BHTBJlpY.js} +32 -32
  278. package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-C0I13CbD.js} +1 -1
  279. package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-B1UQ8Hld.js} +1 -1
  280. package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-8lrgqmfv.js} +1 -1
  281. package/web/dist/index.html +17 -17
  282. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
@@ -1,206 +1,206 @@
1
- /**
2
- * PostToolUse 行为引导族 —— 工具跑完后注 additionalContext 引导 agent 下一步
3
- * (截图后自检 / 交付后告知 / 重生看门狗 / 风格锚落盘提醒)。
4
- * (2026-08-14 可维护性行动:从 hooks.js 原样迁出,语义零改动)
5
- *
6
- * makePostToolUseRecordDecisionHandler — 已移除(git 历史可查)。
7
- * 之前注 "继续做用户的当前任务" 跟 SDK preset 'claude_code' 教的内容重复,
8
- * 让 agent 行为像被牵着走。删除后 agent 记完决策自己判断下一步,更接近
9
- * SDK 默认行为。如未来观察到 agent 反复 record_decision 信号稀释,再考虑
10
- * 加回(那时改成更精准的 anti-loop 检测,不是无脑注引导)。
11
- */
12
- import path from 'node:path';
13
-
14
- /**
15
- * PostToolUse(screenshot_canvas) handler — agent 截图后引导它做视觉自检。
16
- *
17
- * input: PostToolUseHookInput (sdk.d.ts:1926)
18
- * - tool_name / tool_input / tool_response / tool_use_id / duration_ms?
19
- *
20
- * output: PostToolUseHookSpecificOutput (sdk.d.ts:1938)
21
- * - additionalContext?: string 注入下一轮 prompt
22
- * - updatedToolOutput?: unknown 替换 tool 输出(不用)
23
- *
24
- * 注意:tool_response 里包含 image content block(base64)。agent 收到这条
25
- * additionalContext 时已经能"看到"图(multimodal)—— 我们只是用文字提示
26
- * 它接下来该做什么,不替换 image。
27
- *
28
- * 截图后的引导(2026-07-28;硬上限已撤销):
29
- * 实测一个真实会话 9-33 张截图,每张 0.6 倍光栅后 ≈1k vision tokens,且
30
- * **永久留在上下文里**(SDK 不能回改历史工具输出)。
31
- *
32
- * 曾经加过"整会话超 12 张就把图换成文字"的硬闸,撤掉了:那等于在 agent 检查
33
- * 自己作品的时候把它的眼睛蒙上,而且蒙得悄无声息 —— 它只会以为"看起来 OK"。
34
- * 省下来的几十 k 换不来这个代价。现在只报数、给建议,看不看由它自己判断;
35
- * 真正的省是 0.6 倍光栅(每张 1.85k→1.0k)和压缩阈值,不是拦着不让看。
36
- */
37
- const SCREENSHOT_BUSY_HINT_AT = 6; // 累到这个数开始提醒"大面积检查交给子代理"
38
-
39
- export function makePostToolUseScreenshotHandler({ ctx }) {
40
- let takenInSession = 0;
41
- let takenInTurn = 0;
42
- let lastTurn = -1;
43
- // 不 emit run.screenshot_taken —— mcp/tools/screenshot.js:114 已经 emit
44
- // 完整字段(sizeBytes / viewport / fullPage)。hook 只负责注 additionalContext
45
- // 引导 agent 行为,业务事件由 MCP 工具内部负责。
46
- return async (input, _toolUseId, _options) => {
47
- const args = input?.tool_input || {};
48
- const wasFullPage = args.fullPage === true;
49
- const wasPerPage = typeof args.pageIndex === 'number';
50
- const turn = ctx?.counters?.turns ?? 0;
51
- if (turn !== lastTurn) { lastTurn = turn; takenInTurn = 0; }
52
- takenInTurn += 1;
53
- takenInSession += 1;
54
-
55
- // fullPage 截图体积是 viewport 的 N×(N=页数),且会留在 context 多 turn
56
- // 直到 autoCompact。push agent 下次整 deck 自检走 vision-checker subagent,
57
- // subagent context 是隔离的,主线只收文字 critique,几 K vs 几百 K 的差距。
58
- // 08-21 起这里只注**状态**(张数计数),不再每张截图注一段"点 3 个问题 / 派 vision-checker"
59
- // 的说教 —— 那些住 prelude(做完之前先自己看 / 何时请外援),每张都说是 N 倍重复,而且
60
- // "整 deck 自检请派 vision-checker"跟 prelude 定的触发条件(自检两轮用户仍不满意)矛盾。
61
- void wasFullPage; void wasPerPage;
62
- if (takenInSession < SCREENSHOT_BUSY_HINT_AT) return {};
63
- return {
64
- hookSpecificOutput: {
65
- hookEventName: 'PostToolUse',
66
- additionalContext:
67
- `**上下文提示**:本轮 ${takenInTurn} 张、本会话累计 ${takenInSession} 张截图(每张约 1k tokens,进了上下文不会释放)。没有额度上限,该看就看。`,
68
- },
69
- };
70
- };
71
- }
72
-
73
- /**
74
- * PostToolUse(export_handoff) handler — agent 打交付包后引导它告知用户路径。
75
- */
76
- export function makePostToolUseExportHandler({ ctx: _ctx }) {
77
- // 不 emit run.export_built —— mcp/tools/export-handoff.js:83 已经 emit
78
- // 完整字段(format / path / sizeBytes / notes)。hook 从 tool_response 字符串
79
- // substring 拼出来的 path 反而不准。hook 只负责注 additionalContext。
80
- // 08-21:只在本 session 第一次打包时提醒一次;"收尾消息要自足"prelude 已讲,每次都注是重复
81
- let said = false;
82
- return async (_input, _toolUseId, _options) => {
83
- if (said) return {};
84
- said = true;
85
- return {
86
- hookSpecificOutput: {
87
- hookEventName: 'PostToolUse',
88
- additionalContext: '已生成交付包。把打包文件路径告诉用户(他从 UI 下载);同一个交付只打包一次。',
89
- },
90
- };
91
- };
92
- }
93
-
94
- // ── Phase Image-4:generate_image 重生看门狗 ──
95
- //
96
- // agent 同 outputName 调 generate_image 第 3 次起,注 systemMessage 建议:
97
- // - 直接 chat 邀请用户在最近 2-3 张候选里选最好的(generate_image 已返 image
98
- // content block,前端 chat 自动渲染,用户能直接看到)
99
- // - 或 accept 当前最好的一版继续后续工作
100
- // 防"agent 闷头改 5-10 次同 prompt 浪费 token + 用户也得不到更好版本"。
101
- //
102
- // 计数策略:
103
- // - in-memory Map(key: outputName 去 timestamp 的 base,value: count)
104
- // - 进程重启清;session 内累积;不区分 session(agent 进程同步 hook)
105
- // - 阈值固定 3,可后续 env 化
106
- //
107
- // outputName base 提取:
108
- // - "deck-cover-v1" → "deck-cover"(去掉 -v\d / -\d / -draft 等 suffix)
109
- // - 同 base 不同 suffix 仍计入同一组(避免 agent 改名绕过 watchdog)
110
- export function makePostToolUseGenerateImageRegenWatchdog() {
111
- let nudgedOnce = false; // 08-21:邀请反馈的判断准则每 session 说一次就够(以前每个 base 组第一张都说)
112
- const REGEN_THRESHOLD = 3;
113
- const counts = new Map();
114
-
115
- function extractBase(outputName) {
116
- if (!outputName || typeof outputName !== 'string') return null;
117
- return outputName
118
- .replace(/-(?:v\d+|draft\d*|final|new|old|alt|\d+)$/i, '')
119
- .replace(/[-_]+/g, '-')
120
- .toLowerCase();
121
- }
122
-
123
- return async (input, _toolUseId, _options) => {
124
- try {
125
- const outputName = input?.tool_input?.outputName;
126
- const base = extractBase(outputName);
127
- if (!base) return {};
128
-
129
- const next = (counts.get(base) || 0) + 1;
130
- counts.set(base, next);
131
-
132
- // 第 1 次:邀请反馈 nudge(按 SKILL.md 高代价 / 低代价节点判断)
133
- if (next === 1 && !nudgedOnce) {
134
- nudgedOnce = true;
135
- return {
136
- hookSpecificOutput: {
137
- hookEventName: 'PostToolUse',
138
- additionalContext:
139
- `<system-reminder>\n[image-feedback-nudge] 这是本组(base="${base}")第 1 张图。\n\n`
140
- + `如果是 cover / portrait / 跨页 anchor 这类高代价节点(会被当 referenceImages 种子用于全 deck),可以在 chat 里自然邀请用户确认一下方向("这个 cover 当全 deck 视觉锚 OK 吗?"),收到反馈再做后续;section-divider / decoration / icon 这类单张可直接继续,工具 caption 已自动在 chat 显示。\n\n`
141
- + `判断诀窍:错了会不会导致全 deck 重生?会 → 邀请反馈;不会 → 继续。\n`
142
- + `</system-reminder>`,
143
- },
144
- };
145
- }
146
-
147
- if (next < REGEN_THRESHOLD) return {};
148
-
149
- // ≥ 3 次同 base outputName → 注 systemMessage
150
- return {
151
- hookSpecificOutput: {
152
- hookEventName: 'PostToolUse',
153
- additionalContext:
154
- `<system-reminder>\n[regen-watchdog] 你已经对 outputName base "${base}" 调 generate_image ${next} 次。\n\n`
155
- + `如果是 conversational editing 微调("再暖一点 / 换日落色"),可以继续;\n`
156
- + `如果在反复尝试不同方向(每次 prompt 大改),**强烈建议**:\n`
157
- + ` 1. 直接在 chat 里邀请用户从最近 2-3 张候选选最好的(image content block 已自动在 chat 渲染)\n`
158
- + ` 2. 或 accept 当前最满意的那张,专心后续工作\n`
159
- + `理由:reroll 同 prompt 越多次 token 浪费越大,且用户也未必能在第 N 张里看出明显差别。\n`
160
- + `</system-reminder>`,
161
- },
162
- };
163
- } catch (err) {
164
- console.warn(`[hooks/regen-watchdog] threw:`, err.message);
165
- return {};
166
- }
167
- };
168
- }
169
-
170
- /**
171
- * PostToolUse(record_decision) —— 锚定风格那一笔之后提醒落两处长期资产
172
- * (2026-07-28,配合"记忆归 SDK / 品牌归我们 / 指引归用户"的分工)
173
- *
174
- * 只在这一笔上注、每 session 一次:
175
- * - 品牌档案 `agent-memory/brand/memory.md` —— 前端 BrandCard 会把色板 /
176
- * 字体渲染出来,是结构化资产,agent 不写就永远空着
177
- * - 项目指引 `.claude/CLAUDE.md` —— SDK 每次 session 自动读进 system prompt,
178
- * 但只有用户能决定要不要固化,所以是"问一句"不是"直接写"
179
- *
180
- * 通用偏好不在这儿管:那是 SDK 自动记忆的活(autoMemoryDirectory 已指到
181
- * .claude/agent-memory/auto,前端记忆卡直接显示)。
182
- */
183
- export function makePostToolUseStyleAnchorNudge({ sharedRoot }) {
184
- let nudged = false;
185
- const ANCHOR_RE = /(style-anchor|风格锚|锚定|视觉基调|palette|配色方案)/i;
186
- return async (input, _toolUseId, _options) => {
187
- if (nudged) return {};
188
- const t = input?.tool_input || {};
189
- const blob = `${t.topic || ''} ${t.title || ''} ${t.decision || ''} ${t.rationale || ''}`;
190
- if (!ANCHOR_RE.test(blob)) return {};
191
- nudged = true;
192
- const guidePath = sharedRoot ? path.join(sharedRoot, '.claude', 'CLAUDE.md') : '.claude/CLAUDE.md';
193
- return {
194
- hookSpecificOutput: {
195
- hookEventName: 'PostToolUse',
196
- additionalContext:
197
- '这一笔看起来是在锚定这个项目的视觉方向。顺手做两件事(都只做一次):\n'
198
- + '1. 把这版风格写进 `.claude/agent-memory/brand/memory.md`(色号 / 字体链 / 版式语言 / 动效预算;⚠️ 带 .claude/ 前缀,写错位置 BrandCard 读不到),'
199
- + '前端品牌档案卡会把色板和字体渲染出来给用户看,不写就一直空着。\n'
200
- + `2. 如果这次定下来的还包含**项目级约束**(不只这一个 deck 适用,比如"这个项目一律不用 emoji"),`
201
- + `在收尾时问用户一句要不要写进项目指引(${guidePath},SDK 每次 session 自动读它)。用户点头你再写。\n`
202
- + '用户的通用偏好不用你手动记,系统的自动记忆会管。',
203
- },
204
- };
205
- };
206
- }
1
+ /**
2
+ * PostToolUse 行为引导族 —— 工具跑完后注 additionalContext 引导 agent 下一步
3
+ * (截图后自检 / 交付后告知 / 重生看门狗 / 风格锚落盘提醒)。
4
+ * (2026-08-14 可维护性行动:从 hooks.js 原样迁出,语义零改动)
5
+ *
6
+ * makePostToolUseRecordDecisionHandler — 已移除(git 历史可查)。
7
+ * 之前注 "继续做用户的当前任务" 跟 SDK preset 'claude_code' 教的内容重复,
8
+ * 让 agent 行为像被牵着走。删除后 agent 记完决策自己判断下一步,更接近
9
+ * SDK 默认行为。如未来观察到 agent 反复 record_decision 信号稀释,再考虑
10
+ * 加回(那时改成更精准的 anti-loop 检测,不是无脑注引导)。
11
+ */
12
+ import path from 'node:path';
13
+
14
+ /**
15
+ * PostToolUse(screenshot_canvas) handler — agent 截图后引导它做视觉自检。
16
+ *
17
+ * input: PostToolUseHookInput (sdk.d.ts:1926)
18
+ * - tool_name / tool_input / tool_response / tool_use_id / duration_ms?
19
+ *
20
+ * output: PostToolUseHookSpecificOutput (sdk.d.ts:1938)
21
+ * - additionalContext?: string 注入下一轮 prompt
22
+ * - updatedToolOutput?: unknown 替换 tool 输出(不用)
23
+ *
24
+ * 注意:tool_response 里包含 image content block(base64)。agent 收到这条
25
+ * additionalContext 时已经能"看到"图(multimodal)—— 我们只是用文字提示
26
+ * 它接下来该做什么,不替换 image。
27
+ *
28
+ * 截图后的引导(2026-07-28;硬上限已撤销):
29
+ * 实测一个真实会话 9-33 张截图,每张 0.6 倍光栅后 ≈1k vision tokens,且
30
+ * **永久留在上下文里**(SDK 不能回改历史工具输出)。
31
+ *
32
+ * 曾经加过"整会话超 12 张就把图换成文字"的硬闸,撤掉了:那等于在 agent 检查
33
+ * 自己作品的时候把它的眼睛蒙上,而且蒙得悄无声息 —— 它只会以为"看起来 OK"。
34
+ * 省下来的几十 k 换不来这个代价。现在只报数、给建议,看不看由它自己判断;
35
+ * 真正的省是 0.6 倍光栅(每张 1.85k→1.0k)和压缩阈值,不是拦着不让看。
36
+ */
37
+ const SCREENSHOT_BUSY_HINT_AT = 6; // 累到这个数开始提醒"大面积检查交给子代理"
38
+
39
+ export function makePostToolUseScreenshotHandler({ ctx }) {
40
+ let takenInSession = 0;
41
+ let takenInTurn = 0;
42
+ let lastTurn = -1;
43
+ // 不 emit run.screenshot_taken —— mcp/tools/screenshot.js:114 已经 emit
44
+ // 完整字段(sizeBytes / viewport / fullPage)。hook 只负责注 additionalContext
45
+ // 引导 agent 行为,业务事件由 MCP 工具内部负责。
46
+ return async (input, _toolUseId, _options) => {
47
+ const args = input?.tool_input || {};
48
+ const wasFullPage = args.fullPage === true;
49
+ const wasPerPage = typeof args.pageIndex === 'number';
50
+ const turn = ctx?.counters?.turns ?? 0;
51
+ if (turn !== lastTurn) { lastTurn = turn; takenInTurn = 0; }
52
+ takenInTurn += 1;
53
+ takenInSession += 1;
54
+
55
+ // fullPage 截图体积是 viewport 的 N×(N=页数),且会留在 context 多 turn
56
+ // 直到 autoCompact。push agent 下次整 deck 自检走 vision-checker subagent,
57
+ // subagent context 是隔离的,主线只收文字 critique,几 K vs 几百 K 的差距。
58
+ // 08-21 起这里只注**状态**(张数计数),不再每张截图注一段"点 3 个问题 / 派 vision-checker"
59
+ // 的说教 —— 那些住 prelude(做完之前先自己看 / 何时请外援),每张都说是 N 倍重复,而且
60
+ // "整 deck 自检请派 vision-checker"跟 prelude 定的触发条件(自检两轮用户仍不满意)矛盾。
61
+ void wasFullPage; void wasPerPage;
62
+ if (takenInSession < SCREENSHOT_BUSY_HINT_AT) return {};
63
+ return {
64
+ hookSpecificOutput: {
65
+ hookEventName: 'PostToolUse',
66
+ additionalContext:
67
+ `**上下文提示**:本轮 ${takenInTurn} 张、本会话累计 ${takenInSession} 张截图(每张约 1k tokens,进了上下文不会释放)。没有额度上限,该看就看。`,
68
+ },
69
+ };
70
+ };
71
+ }
72
+
73
+ /**
74
+ * PostToolUse(export_handoff) handler — agent 打交付包后引导它告知用户路径。
75
+ */
76
+ export function makePostToolUseExportHandler({ ctx: _ctx }) {
77
+ // 不 emit run.export_built —— mcp/tools/export-handoff.js:83 已经 emit
78
+ // 完整字段(format / path / sizeBytes / notes)。hook 从 tool_response 字符串
79
+ // substring 拼出来的 path 反而不准。hook 只负责注 additionalContext。
80
+ // 08-21:只在本 session 第一次打包时提醒一次;"收尾消息要自足"prelude 已讲,每次都注是重复
81
+ let said = false;
82
+ return async (_input, _toolUseId, _options) => {
83
+ if (said) return {};
84
+ said = true;
85
+ return {
86
+ hookSpecificOutput: {
87
+ hookEventName: 'PostToolUse',
88
+ additionalContext: '已生成交付包。把打包文件路径告诉用户(他从 UI 下载);同一个交付只打包一次。',
89
+ },
90
+ };
91
+ };
92
+ }
93
+
94
+ // ── Phase Image-4:generate_image 重生看门狗 ──
95
+ //
96
+ // agent 同 outputName 调 generate_image 第 3 次起,注 systemMessage 建议:
97
+ // - 直接 chat 邀请用户在最近 2-3 张候选里选最好的(generate_image 已返 image
98
+ // content block,前端 chat 自动渲染,用户能直接看到)
99
+ // - 或 accept 当前最好的一版继续后续工作
100
+ // 防"agent 闷头改 5-10 次同 prompt 浪费 token + 用户也得不到更好版本"。
101
+ //
102
+ // 计数策略:
103
+ // - in-memory Map(key: outputName 去 timestamp 的 base,value: count)
104
+ // - 进程重启清;session 内累积;不区分 session(agent 进程同步 hook)
105
+ // - 阈值固定 3,可后续 env 化
106
+ //
107
+ // outputName base 提取:
108
+ // - "deck-cover-v1" → "deck-cover"(去掉 -v\d / -\d / -draft 等 suffix)
109
+ // - 同 base 不同 suffix 仍计入同一组(避免 agent 改名绕过 watchdog)
110
+ export function makePostToolUseGenerateImageRegenWatchdog() {
111
+ let nudgedOnce = false; // 08-21:邀请反馈的判断准则每 session 说一次就够(以前每个 base 组第一张都说)
112
+ const REGEN_THRESHOLD = 3;
113
+ const counts = new Map();
114
+
115
+ function extractBase(outputName) {
116
+ if (!outputName || typeof outputName !== 'string') return null;
117
+ return outputName
118
+ .replace(/-(?:v\d+|draft\d*|final|new|old|alt|\d+)$/i, '')
119
+ .replace(/[-_]+/g, '-')
120
+ .toLowerCase();
121
+ }
122
+
123
+ return async (input, _toolUseId, _options) => {
124
+ try {
125
+ const outputName = input?.tool_input?.outputName;
126
+ const base = extractBase(outputName);
127
+ if (!base) return {};
128
+
129
+ const next = (counts.get(base) || 0) + 1;
130
+ counts.set(base, next);
131
+
132
+ // 第 1 次:邀请反馈 nudge(按 SKILL.md 高代价 / 低代价节点判断)
133
+ if (next === 1 && !nudgedOnce) {
134
+ nudgedOnce = true;
135
+ return {
136
+ hookSpecificOutput: {
137
+ hookEventName: 'PostToolUse',
138
+ additionalContext:
139
+ `<system-reminder>\n[image-feedback-nudge] 这是本组(base="${base}")第 1 张图。\n\n`
140
+ + `如果是 cover / portrait / 跨页 anchor 这类高代价节点(会被当 referenceImages 种子用于全 deck),可以在 chat 里自然邀请用户确认一下方向("这个 cover 当全 deck 视觉锚 OK 吗?"),收到反馈再做后续;section-divider / decoration / icon 这类单张可直接继续,工具 caption 已自动在 chat 显示。\n\n`
141
+ + `判断诀窍:错了会不会导致全 deck 重生?会 → 邀请反馈;不会 → 继续。\n`
142
+ + `</system-reminder>`,
143
+ },
144
+ };
145
+ }
146
+
147
+ if (next < REGEN_THRESHOLD) return {};
148
+
149
+ // ≥ 3 次同 base outputName → 注 systemMessage
150
+ return {
151
+ hookSpecificOutput: {
152
+ hookEventName: 'PostToolUse',
153
+ additionalContext:
154
+ `<system-reminder>\n[regen-watchdog] 你已经对 outputName base "${base}" 调 generate_image ${next} 次。\n\n`
155
+ + `如果是 conversational editing 微调("再暖一点 / 换日落色"),可以继续;\n`
156
+ + `如果在反复尝试不同方向(每次 prompt 大改),**强烈建议**:\n`
157
+ + ` 1. 直接在 chat 里邀请用户从最近 2-3 张候选选最好的(image content block 已自动在 chat 渲染)\n`
158
+ + ` 2. 或 accept 当前最满意的那张,专心后续工作\n`
159
+ + `理由:reroll 同 prompt 越多次 token 浪费越大,且用户也未必能在第 N 张里看出明显差别。\n`
160
+ + `</system-reminder>`,
161
+ },
162
+ };
163
+ } catch (err) {
164
+ console.warn(`[hooks/regen-watchdog] threw:`, err.message);
165
+ return {};
166
+ }
167
+ };
168
+ }
169
+
170
+ /**
171
+ * PostToolUse(record_decision) —— 锚定风格那一笔之后提醒落两处长期资产
172
+ * (2026-07-28,配合"记忆归 SDK / 品牌归我们 / 指引归用户"的分工)
173
+ *
174
+ * 只在这一笔上注、每 session 一次:
175
+ * - 品牌档案 `agent-memory/brand/memory.md` —— 前端 BrandCard 会把色板 /
176
+ * 字体渲染出来,是结构化资产,agent 不写就永远空着
177
+ * - 项目指引 `.claude/CLAUDE.md` —— SDK 每次 session 自动读进 system prompt,
178
+ * 但只有用户能决定要不要固化,所以是"问一句"不是"直接写"
179
+ *
180
+ * 通用偏好不在这儿管:那是 SDK 自动记忆的活(autoMemoryDirectory 已指到
181
+ * .claude/agent-memory/auto,前端记忆卡直接显示)。
182
+ */
183
+ export function makePostToolUseStyleAnchorNudge({ sharedRoot }) {
184
+ let nudged = false;
185
+ const ANCHOR_RE = /(style-anchor|风格锚|锚定|视觉基调|palette|配色方案)/i;
186
+ return async (input, _toolUseId, _options) => {
187
+ if (nudged) return {};
188
+ const t = input?.tool_input || {};
189
+ const blob = `${t.topic || ''} ${t.title || ''} ${t.decision || ''} ${t.rationale || ''}`;
190
+ if (!ANCHOR_RE.test(blob)) return {};
191
+ nudged = true;
192
+ const guidePath = sharedRoot ? path.join(sharedRoot, '.claude', 'CLAUDE.md') : '.claude/CLAUDE.md';
193
+ return {
194
+ hookSpecificOutput: {
195
+ hookEventName: 'PostToolUse',
196
+ additionalContext:
197
+ '这一笔看起来是在锚定这个项目的视觉方向。顺手做两件事(都只做一次):\n'
198
+ + '1. 把这版风格写进 `.claude/agent-memory/brand/memory.md`(色号 / 字体链 / 版式语言 / 动效预算;⚠️ 带 .claude/ 前缀,写错位置 BrandCard 读不到),'
199
+ + '前端品牌档案卡会把色板和字体渲染出来给用户看,不写就一直空着。\n'
200
+ + `2. 如果这次定下来的还包含**项目级约束**(不只这一个 deck 适用,比如"这个项目一律不用 emoji"),`
201
+ + `在收尾时问用户一句要不要写进项目指引(${guidePath},SDK 每次 session 自动读它)。用户点头你再写。\n`
202
+ + '用户的通用偏好不用你手动记,系统的自动记忆会管。',
203
+ },
204
+ };
205
+ };
206
+ }
@@ -1,104 +1,104 @@
1
- /**
2
- * hooks/post-subagent-report.js — 子代理报告丢了的时候别整轮重跑(2026-08-18)
3
- *
4
- * ## 问题
5
- *
6
- * SDK 的子代理触到 `maxTurns` 时返回的是 SDKResultError,**那个类型没有 result
7
- * 字段** —— 最后一条 assistant 消息(也就是报告本身)直接消失,主 agent 拿到的
8
- * 是空的或者只有开场白。工作已经做完、token 已经烧掉,结果拿不到。
9
- *
10
- * 2026-08-05 真实记录:两次派 explorer,一次 59292 tokens / 16 轮工具调用 / 110s,
11
- * 回来只有一句开场白;一次 31228 tokens / 17 轮,回来 "(returned no output.)"。
12
- * 中间轮次少的那次正常。主 agent 的绕行只能是"拆小重派"或"放弃子代理" ——
13
- * 等于放弃了子代理隔离上下文的全部价值,长研究任务反而不敢派了。
14
- *
15
- * ## 做法
16
- *
17
- * 我们改不了 SDK 那个类型,但 `task_notification` 上带着 **`output_file`** ——
18
- * 子代理的完整转录 JSONL。所以:报告看起来是空的时候,把转录路径递给主 agent,
19
- * 让它 Read 一遍把结论捞出来。**从"整轮白烧"变成"多读一个文件"。**
20
- *
21
- * ⚠️ 判据故意保守(只在摘要短得不像报告时才提示):把这句话贴在每次正常的
22
- * 子代理返回后面是噪音,而噪音会训练 agent 忽略提示。
23
- */
24
-
25
- /** tool_use_id → 最近一次 task_notification(agent-shared 在收到时写进来) */
26
- const lastNotification = new Map();
27
- const MAX_TRACKED = 200;
28
-
29
- export function recordTaskNotification(msg) {
30
- if (!msg?.tool_use_id) return;
31
- lastNotification.set(msg.tool_use_id, {
32
- status: msg.status,
33
- summary: msg.summary || '',
34
- outputFile: msg.output_file || null,
35
- toolUses: msg.usage?.tool_uses ?? null,
36
- tokens: msg.usage?.total_tokens ?? null,
37
- });
38
- // 别无限长:一个会话里子代理数量有限,但进程是长命的
39
- if (lastNotification.size > MAX_TRACKED) {
40
- const first = lastNotification.keys().next().value;
41
- lastNotification.delete(first);
42
- }
43
- }
44
-
45
- /** 已经消费过的 id(用来区分"没记上"和"记过又用掉了",见下面那段) */
46
- const consumed = new Set();
47
- let missing = 0;
48
-
49
- /** 报告"看起来是空的"的判据 —— 短于这个就不像一份报告 */
50
- const SUSPICIOUS_LEN = 200;
51
-
52
- export function makePostToolUseSubagentReportRecovery() {
53
- return async (input) => {
54
- const id = input?.tool_use_id;
55
- if (!id) return {};
56
- const note = lastNotification.get(id);
57
- if (!note) {
58
- // ⚠️ 这条分支是「静默死」的经典形状:`recordTaskNotification` 必须**先于**
59
- // 这个 hook 跑,否则整个 handler 什么都不做而且一声不响。这个仓库为
60
- // 「hook 静默死」付过三次账(最贵一次 19 个会话 0 触发、38k tokens 白跑),
61
- // 所以宁可留一行日志:真出问题时它会成堆出现,而不是一片安静。
62
- // 正常也会走到这儿(同一个 id 第二次进来,note 已被一次性消费掉),
63
- // 所以只在**从来没见过这个 id** 时才记。
64
- if (!consumed.has(id)) {
65
- missing += 1;
66
- console.warn(`[subagent-report] tool_use_id ${id} 没有对应的 task_notification`
67
- + `(累计 ${missing} 次)—— 如果这个数字在涨,说明 recordTaskNotification`
68
- + ' 没有跑在这个 hook 之前,兜底其实是死的。');
69
- }
70
- return {};
71
- }
72
- consumed.add(id);
73
- if (consumed.size > MAX_TRACKED) consumed.delete(consumed.values().next().value);
74
- lastNotification.delete(id); // 一次性
75
-
76
- const looksEmpty = note.summary.trim().length < SUSPICIOUS_LEN;
77
- // 摘要有内容就什么都不说 —— 报告回来了,再贴一段提示是噪音,
78
- // 而噪音会训练 agent 忽略提示。
79
- if (!looksEmpty) return {};
80
- const didRealWork = (note.toolUses ?? 0) >= 4 || (note.tokens ?? 0) >= 8000;
81
-
82
- const lines = [
83
- `⚠️ 这个子代理回来的内容只有 ${note.summary.trim().length} 个字符`
84
- + `(status=${note.status}${note.toolUses != null ? `,跑了 ${note.toolUses} 次工具调用` : ''}`
85
- + `${note.tokens != null ? `,${note.tokens} tokens` : ''})。`,
86
- ];
87
- if (didRealWork) {
88
- lines.push('它**确实干了活**,但最终报告没回传 —— SDK 的子代理触到轮次上限时'
89
- + '返回的错误类型不带 result 字段,最后那条消息会整个丢掉。');
90
- }
91
- if (note.outputFile) {
92
- lines.push(`完整转录在 \`${note.outputFile}\`。**Read 它把结论捞出来,别整轮重派** ——`
93
- + 'token 已经烧掉了,重跑一遍只是再烧一次。转录是 JSONL,从后往前读最快。');
94
- } else {
95
- lines.push('没有转录路径可捞。要重派的话把任务拆小(工具调用轮次少的时候不会丢)。');
96
- }
97
- return {
98
- hookSpecificOutput: {
99
- hookEventName: 'PostToolUse',
100
- additionalContext: lines.join('\n'),
101
- },
102
- };
103
- };
104
- }
1
+ /**
2
+ * hooks/post-subagent-report.js — 子代理报告丢了的时候别整轮重跑(2026-08-18)
3
+ *
4
+ * ## 问题
5
+ *
6
+ * SDK 的子代理触到 `maxTurns` 时返回的是 SDKResultError,**那个类型没有 result
7
+ * 字段** —— 最后一条 assistant 消息(也就是报告本身)直接消失,主 agent 拿到的
8
+ * 是空的或者只有开场白。工作已经做完、token 已经烧掉,结果拿不到。
9
+ *
10
+ * 2026-08-05 真实记录:两次派 explorer,一次 59292 tokens / 16 轮工具调用 / 110s,
11
+ * 回来只有一句开场白;一次 31228 tokens / 17 轮,回来 "(returned no output.)"。
12
+ * 中间轮次少的那次正常。主 agent 的绕行只能是"拆小重派"或"放弃子代理" ——
13
+ * 等于放弃了子代理隔离上下文的全部价值,长研究任务反而不敢派了。
14
+ *
15
+ * ## 做法
16
+ *
17
+ * 我们改不了 SDK 那个类型,但 `task_notification` 上带着 **`output_file`** ——
18
+ * 子代理的完整转录 JSONL。所以:报告看起来是空的时候,把转录路径递给主 agent,
19
+ * 让它 Read 一遍把结论捞出来。**从"整轮白烧"变成"多读一个文件"。**
20
+ *
21
+ * ⚠️ 判据故意保守(只在摘要短得不像报告时才提示):把这句话贴在每次正常的
22
+ * 子代理返回后面是噪音,而噪音会训练 agent 忽略提示。
23
+ */
24
+
25
+ /** tool_use_id → 最近一次 task_notification(agent-shared 在收到时写进来) */
26
+ const lastNotification = new Map();
27
+ const MAX_TRACKED = 200;
28
+
29
+ export function recordTaskNotification(msg) {
30
+ if (!msg?.tool_use_id) return;
31
+ lastNotification.set(msg.tool_use_id, {
32
+ status: msg.status,
33
+ summary: msg.summary || '',
34
+ outputFile: msg.output_file || null,
35
+ toolUses: msg.usage?.tool_uses ?? null,
36
+ tokens: msg.usage?.total_tokens ?? null,
37
+ });
38
+ // 别无限长:一个会话里子代理数量有限,但进程是长命的
39
+ if (lastNotification.size > MAX_TRACKED) {
40
+ const first = lastNotification.keys().next().value;
41
+ lastNotification.delete(first);
42
+ }
43
+ }
44
+
45
+ /** 已经消费过的 id(用来区分"没记上"和"记过又用掉了",见下面那段) */
46
+ const consumed = new Set();
47
+ let missing = 0;
48
+
49
+ /** 报告"看起来是空的"的判据 —— 短于这个就不像一份报告 */
50
+ const SUSPICIOUS_LEN = 200;
51
+
52
+ export function makePostToolUseSubagentReportRecovery() {
53
+ return async (input) => {
54
+ const id = input?.tool_use_id;
55
+ if (!id) return {};
56
+ const note = lastNotification.get(id);
57
+ if (!note) {
58
+ // ⚠️ 这条分支是「静默死」的经典形状:`recordTaskNotification` 必须**先于**
59
+ // 这个 hook 跑,否则整个 handler 什么都不做而且一声不响。这个仓库为
60
+ // 「hook 静默死」付过三次账(最贵一次 19 个会话 0 触发、38k tokens 白跑),
61
+ // 所以宁可留一行日志:真出问题时它会成堆出现,而不是一片安静。
62
+ // 正常也会走到这儿(同一个 id 第二次进来,note 已被一次性消费掉),
63
+ // 所以只在**从来没见过这个 id** 时才记。
64
+ if (!consumed.has(id)) {
65
+ missing += 1;
66
+ console.warn(`[subagent-report] tool_use_id ${id} 没有对应的 task_notification`
67
+ + `(累计 ${missing} 次)—— 如果这个数字在涨,说明 recordTaskNotification`
68
+ + ' 没有跑在这个 hook 之前,兜底其实是死的。');
69
+ }
70
+ return {};
71
+ }
72
+ consumed.add(id);
73
+ if (consumed.size > MAX_TRACKED) consumed.delete(consumed.values().next().value);
74
+ lastNotification.delete(id); // 一次性
75
+
76
+ const looksEmpty = note.summary.trim().length < SUSPICIOUS_LEN;
77
+ // 摘要有内容就什么都不说 —— 报告回来了,再贴一段提示是噪音,
78
+ // 而噪音会训练 agent 忽略提示。
79
+ if (!looksEmpty) return {};
80
+ const didRealWork = (note.toolUses ?? 0) >= 4 || (note.tokens ?? 0) >= 8000;
81
+
82
+ const lines = [
83
+ `⚠️ 这个子代理回来的内容只有 ${note.summary.trim().length} 个字符`
84
+ + `(status=${note.status}${note.toolUses != null ? `,跑了 ${note.toolUses} 次工具调用` : ''}`
85
+ + `${note.tokens != null ? `,${note.tokens} tokens` : ''})。`,
86
+ ];
87
+ if (didRealWork) {
88
+ lines.push('它**确实干了活**,但最终报告没回传 —— SDK 的子代理触到轮次上限时'
89
+ + '返回的错误类型不带 result 字段,最后那条消息会整个丢掉。');
90
+ }
91
+ if (note.outputFile) {
92
+ lines.push(`完整转录在 \`${note.outputFile}\`。**Read 它把结论捞出来,别整轮重派** ——`
93
+ + 'token 已经烧掉了,重跑一遍只是再烧一次。转录是 JSONL,从后往前读最快。');
94
+ } else {
95
+ lines.push('没有转录路径可捞。要重派的话把任务拆小(工具调用轮次少的时候不会丢)。');
96
+ }
97
+ return {
98
+ hookSpecificOutput: {
99
+ hookEventName: 'PostToolUse',
100
+ additionalContext: lines.join('\n'),
101
+ },
102
+ };
103
+ };
104
+ }