@thincoder/core 0.10.7 → 0.10.9

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 (149) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/advisor/compaction.mjs +1 -1
  3. package/advisor/run.mjs +1 -1
  4. package/agent/chat-call.mjs +40 -27
  5. package/agent/child-marks.mjs +1 -1
  6. package/agent/dispatch-gates.mjs +7 -4
  7. package/agent/dispatch-run.mjs +1 -0
  8. package/agent/family-tools.mjs +13 -8
  9. package/agent/helpers.mjs +12 -10
  10. package/agent/image-heal.mjs +65 -0
  11. package/agent/light-round.mjs +4 -4
  12. package/agent/live-beat.mjs +4 -4
  13. package/agent/post-turn.mjs +34 -0
  14. package/agent/record-results.mjs +9 -1
  15. package/agent/run-stages.mjs +45 -10
  16. package/agent/setup-reminders.mjs +14 -13
  17. package/agent/setup.mjs +72 -48
  18. package/agent/spawn-child.mjs +1 -1
  19. package/agent/subagent-event.mjs +1 -1
  20. package/agent/suspension.mjs +2 -1
  21. package/agent/timers.mjs +8 -8
  22. package/agent/turn-loop.mjs +7 -1
  23. package/agent-tools/advisor.mjs +1 -1
  24. package/agent-tools/async-discard.mjs +40 -5
  25. package/agent-tools/async-settle.mjs +3 -1
  26. package/agent-tools/audit-block.mjs +5 -3
  27. package/agent-tools/batch-gate.mjs +198 -0
  28. package/agent-tools/batch-lifecycle.mjs +216 -36
  29. package/agent-tools/batch-paths.mjs +20 -20
  30. package/agent-tools/batch-skeleton.mjs +87 -0
  31. package/agent-tools/batch.mjs +11 -3
  32. package/agent-tools/checkpoint.mjs +1 -1
  33. package/agent-tools/context.mjs +28 -5
  34. package/agent-tools/design-slots.mjs +20 -7
  35. package/agent-tools/doc-check.mjs +75 -0
  36. package/agent-tools/eng.mjs +5 -3
  37. package/agent-tools/manifest.mjs +18 -18
  38. package/agent-tools/parent-channel.mjs +3 -3
  39. package/agent-tools/review-facts.mjs +2 -2
  40. package/agent-tools/settings.mjs +1 -1
  41. package/agent-tools/spawn-gates.mjs +108 -47
  42. package/agent-tools/ssh-session.mjs +486 -0
  43. package/agent-tools/ssh-transport.mjs +324 -0
  44. package/agent-tools/ssh-vault.mjs +291 -0
  45. package/agent-tools/subagent-scheduler.mjs +6 -5
  46. package/agent-tools/subagent-spawn.mjs +18 -11
  47. package/agent-tools/subagent.mjs +17 -9
  48. package/agent-tools/task.mjs +1 -1
  49. package/agent-tools/timer.mjs +8 -2
  50. package/agent-tools/zero-write-watch.mjs +2 -2
  51. package/agent-tools.mjs +3 -2
  52. package/attachments.mjs +4 -4
  53. package/browser/cdp.mjs +2 -2
  54. package/browser/launch.mjs +50 -4
  55. package/browser/snapshot.mjs +1 -1
  56. package/config-defaults.mjs +92 -0
  57. package/config-io.mjs +7 -2
  58. package/config-normalize.mjs +109 -0
  59. package/config-watch.mjs +2 -2
  60. package/config.mjs +88 -190
  61. package/declaration.mjs +5 -1
  62. package/doc-check/anchors.mjs +142 -0
  63. package/doc-check/index.mjs +210 -0
  64. package/doc-check/version.mjs +84 -0
  65. package/doc-check/width.mjs +132 -0
  66. package/file-refs.mjs +6 -6
  67. package/generate-title.mjs +1 -1
  68. package/hooks.mjs +1 -1
  69. package/i18n.mjs +13 -1
  70. package/ledger-cmd.mjs +314 -62
  71. package/ledger-db.mjs +122 -31
  72. package/ledger-migrate.mjs +18 -12
  73. package/ledger-read.mjs +6 -7
  74. package/ledger-tools.mjs +138 -77
  75. package/ledger.mjs +34 -62
  76. package/log.mjs +17 -3
  77. package/manifest-discovery.mjs +21 -18
  78. package/manifest-schema.mjs +17 -14
  79. package/manifest.mjs +37 -22
  80. package/mcp/transport-stdio.mjs +1 -1
  81. package/mcp.mjs +2 -2
  82. package/memory/core.mjs +1 -1
  83. package/memory/scan.mjs +1 -1
  84. package/memory/sweep.mjs +1 -1
  85. package/model-specs.mjs +7 -7
  86. package/notify-policy.mjs +6 -6
  87. package/package.json +2 -1
  88. package/permission.mjs +21 -1
  89. package/prompts/advisor-design.md +53 -23
  90. package/prompts/advisor-round1.md +34 -14
  91. package/prompts/advisor-round2.md +32 -19
  92. package/prompts/advisor-round3.md +34 -21
  93. package/prompts/common.md +148 -79
  94. package/prompts/consult-base.md +14 -8
  95. package/prompts/discipline-engineering.md +70 -66
  96. package/prompts/discipline-normal.md +86 -77
  97. package/prompts/persona-coder.md +6 -6
  98. package/prompts/persona-eng-coder.md +19 -18
  99. package/prompts/persona-eng-designer.md +40 -31
  100. package/prompts/persona-engineering.md +124 -92
  101. package/prompts/persona-explore.md +2 -2
  102. package/prompts/persona-normal.md +20 -17
  103. package/prompts/persona-plan.md +7 -7
  104. package/prompts/subagent-base.md +4 -4
  105. package/provider/anthropic.mjs +4 -2
  106. package/provider/core.mjs +4 -2
  107. package/provider/errors.mjs +8 -0
  108. package/provider/google.mjs +8 -1
  109. package/provider/normalize.mjs +42 -12
  110. package/provider/responses-request.mjs +4 -3
  111. package/provider/responses.mjs +2 -2
  112. package/provider/wait-status.mjs +1 -1
  113. package/provider-flows.mjs +1 -1
  114. package/queued.mjs +2 -2
  115. package/rules.mjs +4 -4
  116. package/session-gc.mjs +1 -1
  117. package/session-lifecycle.mjs +7 -7
  118. package/session-slot-write.mjs +2 -2
  119. package/session-slots-manifest.mjs +1 -1
  120. package/session.mjs +1 -1
  121. package/shell-candidates.mjs +2 -2
  122. package/shell-identity.mjs +1 -1
  123. package/team.mjs +63 -5
  124. package/think-off.mjs +3 -3
  125. package/token-ttl.mjs +1 -1
  126. package/token-window.mjs +1 -1
  127. package/tool-docs/bash.md +1 -1
  128. package/tool-docs/batch.md +1 -1
  129. package/tool-docs/browser.md +1 -1
  130. package/tool-docs/doc_check.md +54 -0
  131. package/tool-docs/fetch.md +3 -2
  132. package/tool-docs/ledger.md +7 -5
  133. package/tool-docs/manifest.md +14 -2
  134. package/tool-docs/ssh.md +51 -0
  135. package/tool-docs/subagent.md +6 -9
  136. package/tool-docs/timer.md +2 -1
  137. package/tool-docs/websearch.md +3 -1
  138. package/tools/browser.mjs +1 -1
  139. package/tools/execute.mjs +1 -1
  140. package/tools/git-run.mjs +2 -2
  141. package/tools/git.mjs +8 -8
  142. package/tools/index.mjs +3 -0
  143. package/tools/ops.mjs +1 -1
  144. package/tools/process-tree.mjs +2 -2
  145. package/tools/ssh-askpass.mjs +35 -0
  146. package/tools/ssh.mjs +105 -0
  147. package/tools/web-deepseek.mjs +172 -0
  148. package/tools/web.mjs +29 -4
  149. package/vision-reader.mjs +4 -4
package/CHANGELOG.md CHANGED
@@ -3,6 +3,27 @@
3
3
  All notable changes to the core package are documented here.
4
4
  Format: Keep a Changelog · 中文 · 号在发布时定(CalVer——见 `docs/RELEASE.md` §4)。
5
5
 
6
+ ## [Unreleased]
7
+
8
+ ## [0.10.9] — 2026-10-19
9
+
10
+ ### Removed
11
+
12
+ - **`design-slots` 的 `reveal` 开关与掩蔽(token-visibility-simplify 批 2026-10-17)**:清点面改**直读**——逐槽行恒携 `designToken`(过期照列、`status` 自标态;记录畸形 ⇒ `—`);删净 `reveal` 开关 / 掩蔽 / 一次一槽限制(用户裁定「没必要把 token 藏起来,agent 要就告诉他」);签发链(mint / settle / TTL / persist)零改。
13
+
14
+ ## [0.10.8] — 2026-10-16
15
+
16
+ > 0.10.7 → 0.10.8(月内 +1——发布时定号)
17
+
18
+ ### Added
19
+
20
+ - **MCP 探活具名读数(#1431——2026-10-14 走查轮)**:`probeMcpServer` 返回值增 `tools`(工具名数组——原 `mcpTools` 到手即弃,控制台探活只见计数);服务端页面随正(具名读数形,缺名回落计数形——零回归)。
21
+ - **注入开关(#1420——injection-switches 批 2026-10-14)**:`agent.injectDocs` ∥ `agent.injectMemories` 两枚全局开关(DEFAULTS `true`;判据 `!== false`);关 = 阻断三块自动注入(项目指令文件块 ∥ 文档检索块 ∥ 记忆检索块——检索调用不执行),不删数据;`settings` 工具 + CLI `/config` 热应用(写后下一次组装生效);VSC/桌面设置界面各落两开关。
22
+ - **SSH 会话工具(#1436——ssh-session 批 2026-10-14)**:`ssh` 工具(`connect`/`send`/`read`/`secret_input`/`close`/`list`——多往返会话、状态保持、设备提示符与分页驱动);密钥 / agent 优先,需要密码走凭据弹窗(**值零入上下文**——`onSecretInput` 注入缝 + askpass 链);**零第三方依赖**(系统 openssh + `ssh -tt` 远端 PTY + `SSH_ASKPASS`);三端(CLI / VSC / 桌面)界面在位。
23
+ - **`design-slots` 回收读口(#1384——design-token-recovery 批 2026-10-14)**:`designId` 过滤单槽(trim 精确匹配;未知 ⇒ 拒 + 列在场 id)∥ `reveal:true` 揭值(必携 `designId`;只揭活槽——过期/畸形槽掩蔽注因;`null` 视同缺省——零揭值不 fail-open);`log.mjs` `SECRET_FORM` 增 token 值形(`uuid:epoch`)。
24
+ - **产品内建 `doc_check` 工具(#1586——builtin-doc-check 批 2026-10-16)**:文档纪律机检内建为产品工具(只读 · 报告态 · **非门禁**)——起步两条判据:锚(档 token ± `§N` ⇒ 档缺 ∥ 节缺;行号不作节指)∥ 行宽(>300 字符;四类谓词 + 区带豁免);声明面 = `PROJECT-MANIFEST.json` `checkConfig`(零硬编码路径);项目自建脚本退出判据链。
25
+ - **台账工具项目根读写同一推导(#1588——ledger-root-derivation 批 2026-10-16)**:`resolveLedgerRoot` 判据单源(自目标路径沿祖先链取最近「已注册」= 带档 ∨ 自身路径键控库在盘;工作区态不立为根);族发现取祖先支;无根契约 = 读面空账 ∥ 写面抛(零落盘);写面「抛错列候选」退场。
26
+
6
27
  ## [0.10.7] — 2026-10-14
7
28
 
8
29
  > 0.10.6 → 0.10.7(月内 +1——发布时定号)
@@ -10,7 +10,7 @@
10
10
  * advisor-loop-split 批:loop 的压缩检查点归位本档(`compactContextIfNeeded`——`context_limit` 尾串生成点同迁;时间线面 ⇒ `advisor/timeline.mjs`)。
11
11
  */
12
12
 
13
- import { providerSpec, outputReserve, usableWindow } from "../config.mjs" // 第 25 批:预算派生(与 loop.mjs:12 同源导入);`outputReserve`/`usableWindow` = 2026-10-06 预留裁定(#975)
13
+ import { providerSpec, outputReserve, usableWindow } from "../config.mjs" // 第 25 批:预算派生(与 loop.mjs 的 providerSpec 同源导入);`outputReserve`/`usableWindow` = 2026-10-06 预留裁定(#975)
14
14
  // B4(群 B 批,ADVISOR-GUARDS.md §9——F32):CJK 加权单源(provider/rate.mjs 叶子向无环)
15
15
  import { estimateText } from "../provider/rate.mjs"
16
16
 
package/advisor/run.mjs CHANGED
@@ -37,7 +37,7 @@ export function resolveAdvisorProvider(agent) {
37
37
  // chat 前 `assertProviderModel` fail-fast(不静默)。
38
38
  const result = cfg.model ? { ...provider, model: cfg.model } : { ...provider, model: agent.provider?.model }
39
39
  // 台账 #329 / `MODEL-SPECS.md §15.4-3`:off 形须**活着到达载荷层**——载荷层 off 门首款 = `provider.thinking === null`
40
- // (`thincoder-core/provider/core.mjs:213-219`);归一为 `undefined` 会让该门永不开(effort 族 advisor
40
+ // (`thincoder-core/provider/core.mjs` 的 `chatImpl`);归一为 `undefined` 会让该门永不开(effort 族 advisor
41
41
  // 关思考静默失效)。显式 off 同清**继承档**(渠条目自带 / 主 provider 的 `reasoningEffort` 不得随行——
42
42
  // 随行则显式档支先命中、off 意图作废)。`false`(非法原值)仍归一 `undefined`(零变)。
43
43
  if (cfg.thinking === null) {
@@ -5,6 +5,7 @@
5
5
  * 由 runAgent 的 catch/finally 收束。
6
6
  */
7
7
  import { chat } from "../provider/index.mjs"
8
+ import { healImageRejection, clearImageFailStreak } from "./image-heal.mjs"
8
9
 
9
10
  /** 流式输出门(§2.5 #78 并入——VSC 三态门,纯函数可直驱测试):depth 0 恒通;
10
11
  * consult 子代理豁免(其输出进会诊面板);其余经 `streamOutput` 显式选择进入。 */
@@ -24,32 +25,36 @@ export async function callModelTurn(agent, {
24
25
  await classifyAndApply(agent, turn).catch(() => {})
25
26
  }
26
27
 
27
- try { response = await chat(agent.provider, {
28
- messages, tools: toolSchemas,
29
- // §2.5 #78 并入(VSC onToken 三态门):depth 0 恒通;consult 子代理豁免(其输出进
30
- // 会诊面板);escalate 等经 opts.streamOutput 显式选择进入;其余子代理不流式。
31
- onToken: streamOutputAllowed(depth, agent._role, streamOutput) ? callbacks.onToken : null,
32
- onReasoning: callbacks.onReasoning,
33
- onWait: callbacks.onWait,
34
- signal,
35
- streamRules: agent.config.agent?.streamRules ?? [],
36
- firedPatterns: streamRuleFired,
37
- // LOGGING(LOGGING.md):llm:* 事件的语义上下文(stage=turn 主循环回合——含
38
- // digest 消化轮 auto=true;child=子代理 id(spawn 时 stamp 于 child._logId))
39
- // TRACES.md §6.1 D-TR4:轨迹元数据增补(role/depth/kind/session/cwd——trace-store 只读
40
- // logCtx,签名不变);kind:depth>0 = subagent(consult 孩子 = consult)——子代理
41
- // 对回靠 role+depth+child id(children 无 _sessionStart——不经 depth-0 设置——
42
- // session 字段对子代理轨迹为 null——见 trace-store/agent.mjs 注释)。
43
- logCtx: {
44
- stage: "turn", turn: turn + 1, auto: autoTurn, child: agent._logId,
45
- role: agent._role ?? null,
46
- depth,
47
- kind: depth > 0 ? (agent._role === "consult" ? "consult" : "subagent") : "turn",
48
- session: agent._sessionStart ?? null,
49
- cwd: agent.cwd,
50
- traces: agent.config?.traces?.enabled !== false,
51
- },
52
- })
28
+ // 发送选项(首送 ∥ §6.26 自愈重发共用——两送除 messages 外逐字同参)
29
+ const sendOpts = {
30
+ tools: toolSchemas,
31
+ // §2.5 #78 并入(VSC onToken 三态门):depth 0 恒通;consult 子代理豁免(其输出进
32
+ // 会诊面板);escalate 等经 opts.streamOutput 显式选择进入;其余子代理不流式。
33
+ onToken: streamOutputAllowed(depth, agent._role, streamOutput) ? callbacks.onToken : null,
34
+ onReasoning: callbacks.onReasoning,
35
+ onWait: callbacks.onWait,
36
+ signal,
37
+ streamRules: agent.config.agent?.streamRules ?? [],
38
+ firedPatterns: streamRuleFired,
39
+ // LOGGING(LOGGING.md):llm:* 事件的语义上下文(stage=turn 主循环回合——含
40
+ // digest 消化轮 auto=true;child=子代理 id(spawn 时 stamp 于 child._logId))
41
+ // TRACES.md §6.1 D-TR4:轨迹元数据增补(role/depth/kind/session/cwd——trace-store 只读
42
+ // logCtx,签名不变);kind:depth>0 = subagent(consult 孩子 = consult)——子代理
43
+ // 对回靠 role+depth+child id(children 无 _sessionStart——不经 depth-0 设置——
44
+ // session 字段对子代理轨迹为 null——见 trace-store/agent.mjs 注释)。
45
+ logCtx: {
46
+ stage: "turn", turn: turn + 1, auto: autoTurn, child: agent._logId,
47
+ role: agent._role ?? null,
48
+ depth,
49
+ kind: depth > 0 ? (agent._role === "consult" ? "consult" : "subagent") : "turn",
50
+ session: agent._sessionStart ?? null,
51
+ cwd: agent.cwd,
52
+ traces: agent.config?.traces?.enabled !== false,
53
+ },
54
+ }
55
+
56
+ try {
57
+ response = await chat(agent.provider, { messages, ...sendOpts })
53
58
  } catch (e) {
54
59
  // User interrupt (Ctrl+I): controller.abort({ interrupt: true, message }).
55
60
  // Inject into history; the outer loop recreates the controller and resumes.
@@ -61,8 +66,16 @@ export async function callModelTurn(agent, {
61
66
  agent.history.push({ role: "user", content: msg })
62
67
  }
63
68
  }
64
- throw e
69
+ // §6.26 图片拒绝自愈(判定 ∥ 剥除 ∥ 注记 ∥ 记账 = agent/image-heal.mjs 单源):
70
+ // 未愈 ⇒ 原错照抛(非图像错误零误伤);已愈 ⇒ 载荷已修(历史无图 + 机读注记)单次重发。
71
+ const { healed } = healImageRejection(agent, e)
72
+ if (!healed) throw e
73
+ // 重建消息(§6.26 步 4——剥除后的 agent.history 含注记):再发一次(非重试——同请求
74
+ // 重发对图像拒绝恒无效);再败 ⇒ 本行抛新错(不循环、不二次剥除,历史保持已剥除态)。
75
+ response = await chat(agent.provider, { messages: [{ role: "system", content: systemPrompt }, ...agent.history], ...sendOpts })
65
76
  }
77
+ // §6.26 清零调用点(记账单源 = image-heal.mjs):任一次成功调用清零(含自愈重发成功)
78
+ clearImageFailStreak(agent)
66
79
 
67
80
  return response
68
81
  }
@@ -8,7 +8,7 @@
8
8
  * 原样再导出 ⇒ 既有 import 面(核 agent-tools 族 / CLI `tool-events.mjs`)零改。
9
9
  *
10
10
  * 语义:消费端用 `includes()` 检出锚点 ⇒ 合成块头注记("work may be partial")——两端各一处:
11
- * CLI = `thincoder-cli/src/tui/tool-events.mjs:217`;端壳 = `thincoder-vscode/src/extension/
11
+ * CLI = `thincoder-cli/src/tui/tool-events.mjs` 的 `onToolResult`;端壳 = `thincoder-vscode/src/extension/
12
12
  * panel-callbacks.mjs`(onToolResult)。文案演进只改这里(与检测判据单源——消除文案/判据漂移面)。
13
13
  *
14
14
  * 零 import——任意层可引、无环。
@@ -11,6 +11,7 @@ import { writeFileSync, mkdirSync, existsSync } from "node:fs"
11
11
  import { join, resolve } from "node:path"
12
12
  import { homedir } from "node:os"
13
13
  import { toolTouchPaths } from "./helpers.mjs"
14
+ import { redactArgs } from "../log.mjs" // #1432:args 落痕前过遮蔽链(单源 redactSecret 族;log.mjs 为叶——零环)
14
15
  // fix A(2026-09-07):FILE_MUTATORS 的 mutation-seq 记账从
15
16
  // 批后提交(record-results noteMutations)移到执行成功即刻——唯一记账点(取代批后段
16
17
  // + agent.mjs 中断分支记账——不双计)——同消息 [写 + async advisor launch] 时 launch 前
@@ -32,10 +33,11 @@ export function logToolError(toolName, args, error) {
32
33
  const dir = join(ERRORS_DIR, ymd)
33
34
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
34
35
  const file = join(dir, `${ts}-${toolName.replace(/[/\\]/g, "_")}.log`)
36
+ const argsText = JSON.stringify(redactArgs(args), null, 2) // #1432:遮蔽先于截断(≤2000 截断零改)
35
37
  const entry = [
36
38
  `time: ${now.toISOString()}`,
37
39
  `tool: ${toolName}`,
38
- `args: ${JSON.stringify(args, null, 2).slice(0, 2000)}${JSON.stringify(args, null, 2).length > 2000 ? "… (truncated)" : ""}`,
40
+ `args: ${argsText.slice(0, 2000)}${argsText.length > 2000 ? "… (truncated)" : ""}`,
39
41
  `error: ${error?.message ?? String(error)}`,
40
42
  error?.stack ? `stack:\n${error.stack}` : "",
41
43
  ].filter(Boolean).join("\n") + "\n"
@@ -60,8 +62,9 @@ export function logToolError(toolName, args, error) {
60
62
  * AGENT-LOOP-SUBAGENT.md §6.7.2 panel (round1 #5): view 面归只读类(同 status——planMode 放行、免
61
63
  * 审批、可批并行);freeze 面归控制类(同 cancel——planMode 放行、免权限审批、
62
64
  * 批审批不入组、digest 内放行)。freeze 存在(非空 key)即控制类——否则只读类。
65
+ * #1178:泛化改名(原「子代理专用」谓词 ⇒ 通用动作级只读谓词——语义零改,旧标识符零余)——第二消费面 = agent/record-results.mjs 的 verify 失效链(动作级只读 ⇒ 不清 `_verifiedThisRun`)。
63
66
  */
64
- export function isSubagentReadonlyAction(toolName, args) {
67
+ export function isReadonlyAction(toolName, args) {
65
68
  // §6 memory 工具面重构(MEMORY.md §6 D-M5):memory search/list 是只读动作——与
66
69
  // subagent status 同分类(planMode 放行/免审批——Phase-2 批并行只认工具级
67
70
  // readonly/parallel,memory 无 parallel → 按非只读串行,见 MEMORY.md §6.4 实现注)。
@@ -92,7 +95,7 @@ export function isSubagentReadonlyAction(toolName, args) {
92
95
  if (action === "observe") return true
93
96
  // AGENT-LOOP-SUBAGENT.md §6.7.2 panel view 面(freeze 缺省/空 = 视图请求——readonly;非空 freeze 归控制类)
94
97
  if (action === "panel" && (args.freeze === undefined || args.freeze === null || String(args.freeze) === "")) return true
95
- // DESIGN-TOKEN-SETTLEMENT.md §10 F-SL1(2026-10-07 批 ledger-tool):design-slots = 只读清点
98
+ // DESIGN-TOKEN-SETTLEMENT.md §9 F-SL1(2026-10-07 批 ledger-tool):design-slots = 只读清点
96
99
  // (同 status / observe——planMode 放行 / 免审批 / digest 放行;工程模式父侧门在执行器内)。
97
100
  if (action === "design-slots") return true
98
101
  return false
@@ -127,7 +130,7 @@ export function isSubagentEscalateAction(toolName, args) {
127
130
  * 由此保形;核内工具零钩子 ⇒ CLI ∕ desktop 逐字零变(`undefined ?? 核谓词`)。
128
131
  * 两谓词 = 上列两门禁位(planMode ∕ 权限短路)的公共输入(同址同用,防两处漂移)。 */
129
132
  export function readonlyActionOf(tool, toolName, args) {
130
- return tool?.isReadonlyAction?.(args) ?? isSubagentReadonlyAction(toolName, args)
133
+ return tool?.isReadonlyAction?.(args) ?? isReadonlyAction(toolName, args)
131
134
  }
132
135
  export function controlActionOf(tool, toolName, args) {
133
136
  return tool?.isControlAction?.(args) ?? isSubagentControlAction(toolName, args)
@@ -105,6 +105,7 @@ export async function runPreparedItem(item, agent, depth, signal, callbacks) {
105
105
  _toolCallId: item.toolCall.id,
106
106
  onOutput: (chunk) => callbacks.onToolOutput?.(item.toolCall.name, chunk, item.toolCall.id),
107
107
  onQuestion: callbacks.onQuestion,
108
+ onSecretInput: callbacks.onSecretInput,
108
109
  onPermissionRequest: callbacks.onPermissionRequest,
109
110
  }
110
111
  try {
@@ -2,7 +2,7 @@
2
2
  * agent/family-tools.mjs — 家族矩阵单源(`assembleFamilyTools`)。
3
3
  *
4
4
  * 批次 `docs/batches/2026-09-15-vsc-tool-table-dup.md` §2.3A:家族矩阵(哪个运行面得哪些
5
- * 家族工具)自 CLI 侧 `agent/setup.mjs:173-293`(as-of 迁出前坐标)整体迁出——CLI 与 VSC 同调同一份实现
5
+ * 家族工具)自 CLI 侧 `agent/setup.mjs` 的 `assembleFamilyTools`(as-of 迁出前坐标)整体迁出——CLI 与 VSC 同调同一份实现
6
6
  * (消灭「同一角色矩阵两份实现」= 本批缺陷的类根因);端差(VSC 装饰链 / settings 追加 /
7
7
  * consult 池来源)经 `decorate` 注入——**不传 = 核默认形态**(CLI = 迁出前逐字)。
8
8
  *
@@ -26,7 +26,7 @@ export async function assembleFamilyTools({
26
26
  decorate = null, // object 端差面:{ subagent?, consultStart?, consultStop?, settings? }
27
27
  } = {}) {
28
28
  // CORE-UNIFICATION TOOLS #83:consult 家族随统一登记册自 `../agent-tools.mjs` 取用(单一来源)
29
- const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool, engTool, readHistoryTool, contextTool, batchTool, manifestTool, consultStartTool, consultStopTool, parentChannelTool } = await import("../agent-tools.mjs")
29
+ const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool, engTool, readHistoryTool, contextTool, batchTool, manifestTool, consultStartTool, consultStopTool, parentChannelTool, docCheckTool } = await import("../agent-tools.mjs")
30
30
  // 台账统一入口(2026-10-05 统一入口批——台账 #923):全量变体 = depth-0 段 ∥ 只读变体 = 全部
31
31
  // depth>0 段。动态 import——ledger 链静态达 node:sqlite(W8 契约②);两变体同名单对象、schema 窄化。
32
32
  const { ledgerTool, ledgerReadTool } = await import("../ledger.mjs")
@@ -125,8 +125,8 @@ export async function assembleFamilyTools({
125
125
  ...subagentTool,
126
126
  name: "subagent",
127
127
  description: designer
128
- ? "Spawn a read-only `explore` sub-agent to SURVEY the current state for the design: it reads code / docs / existing designs and reports evidence with file:line. BLOCKING ONLY (no async) and action:'spawn' ONLY — the survey channel is read-only; escalate/status/cancel/panel/consume-design/observe/send are not available. Survey budget: ≤6 explore spawns per batch — the main agent's survey result is reference only; do your own."
129
- : "Spawn a read-only `explore` sub-agent to AUDIT your delivery against the design: it compares the delivered code with the design for divergence — partially implemented acceptance criteria, silent simplifications, doc drift, changes outside the approved file list. BLOCKING ONLY (no async — the audit report decides your next protocol step). action:'spawn' ONLY — the audit channel is a read-only spawn; escalate/status/cancel/panel/consume-design/observe/send are not available. The audit task book is appended MECHANICALLY — your own spawn task (docs involved / acceptance criteria / file list) plus the files you actually touched; never hand the audit a self-written file list (a self-report could omit exactly the out-of-scope file it must catch).",
128
+ ? "Spawn a read-only `explore` sub-agent to SURVEY the current state for the design: it reads code / docs / existing designs and reports evidence with file:line. BLOCKING ONLY (no async) and action:'spawn' ONLY — the survey channel is read-only; escalate/status/cancel/panel/consume-design/observe/send are not available. Survey budget: ≤6 explore spawns per batch — the main agent's survey result is reference only; do your own. The spawn carries the five task-book parameters (four core: goal / knownFacts / acceptance / reportFormat + `scope`); a task-only spawn is refused."
129
+ : "Spawn a read-only `explore` sub-agent to AUDIT your delivery against the design: it compares the delivered code with the design for divergence — partially implemented acceptance criteria, silent simplifications, doc drift, changes outside the approved file list. BLOCKING ONLY (no async — the audit report decides your next protocol step). action:'spawn' ONLY — the audit channel is a read-only spawn; escalate/status/cancel/panel/consume-design/observe/send are not available. The audit task book is appended MECHANICALLY — your own spawn task (docs involved / acceptance criteria / file list) plus the files you actually touched; never hand the audit a self-written file list (a self-report could omit exactly the out-of-scope file it must catch). The spawn carries the five task-book parameters (four core: goal / knownFacts / acceptance / reportFormat + `scope`); a task-only spawn is refused.",
130
130
  parameters: { ...subagentTool.parameters, properties: props },
131
131
  }
132
132
  })()
@@ -151,6 +151,9 @@ export async function assembleFamilyTools({
151
151
  // manifest 工具(`MANIFEST.md` §2.10 · 2026-10-08 · 台账 #1098):主 agent 读写出口——
152
152
  // **两模式同挂**(需求未设模式门);子代理各段零挂(未装配 = 权限面第一道)。
153
153
  manifestTool,
154
+ // 文档纪律机检(`DOC-DISCIPLINE.md` §3.20 条目 S):只读报告态——depth-0 主 agent 段;
155
+ // 写文档 / 设计档的角色段(eng-coder / eng-designer)另挂(见下两分支)。
156
+ docCheckTool,
154
157
  // 端差(decorate.settings——VSC depth-0 主 agent 面;缺省不追加):核默认形态里
155
158
  // settings 住**基础集**(`tools/index.mjs` `assembleBuiltinTools`),端侧自持清单
156
159
  // 无该面 ⇒ 端以 decorate 补位(收敛通道 = 将来去 decorate 项即归核位)。
@@ -175,12 +178,14 @@ export async function assembleFamilyTools({
175
178
  // SUBAGENT-UPSTREAM-CHANNEL(AGENT-LOOP-UPSTREAM.md §6.27.4 装配接线):子代理上行通道
176
179
  // (`notify_parent`)随 depth>0 段**前置**——4 处携带 = eng-coder / eng-designer / coder / 兜底段
177
180
  // (未列名 depth>0 role 落同一兜底段 ⇒ 亦装配;语义 =「depth>0 且非 consult 皆装配」);
178
- // 计数口径:`consult` 分支不入 ⇒ 「5 个插入点」读法已作废(实读 `thincoder-core/agent/family-tools.mjs:165-169`)。
181
+ // 计数口径:`consult` 分支不入 ⇒ 「5 个插入点」读法已作废(实读本档 eng 两分支的 `batch` 挂载段)。
179
182
  // consult 段不入(其角色语义 = 父发起的一次性会诊,父在其 settle 前不期望中途对话)。
180
183
  // 台账只读变体(query / count)——全部 depth>0 角色段(含 consult ∕ 兜底;写三 schema 级不可达);
181
184
  // 团队台账只读变体(team-ledger 批)同律。
182
- : engChildRole === "eng-coder" ? [parentChannelTool, ledgerReadTool, teamLedgerReadTool, advisorTool, verifyTool, batchTool(batchDoc), ...(engChildSubagent ? [engChildSubagent] : [])]
183
- : engChildRole === "eng-designer" ? [parentChannelTool, ledgerReadTool, teamLedgerReadTool, batchTool(batchDoc), ...(engChildSubagent ? [engChildSubagent] : [])]
185
+ : engChildRole === "eng-coder" ? [parentChannelTool, ledgerReadTool, teamLedgerReadTool, advisorTool, verifyTool, batchTool(batchDoc), docCheckTool,
186
+ ...(engChildSubagent ? [engChildSubagent] : [])]
187
+ : engChildRole === "eng-designer" ? [parentChannelTool, ledgerReadTool, teamLedgerReadTool, batchTool(batchDoc), docCheckTool,
188
+ ...(engChildSubagent ? [engChildSubagent] : [])]
184
189
  : role === "coder" ? [parentChannelTool, ledgerReadTool, teamLedgerReadTool, verifyTool, advisorTool, ...(batchDoc ? [batchTool(batchDoc)] : [])]
185
190
  : role === "consult" ? [recentChangesTool, ledgerReadTool, teamLedgerReadTool]
186
191
  : [parentChannelTool, ledgerReadTool, teamLedgerReadTool]
@@ -191,7 +196,7 @@ export async function assembleFamilyTools({
191
196
  // execute 层另有机械拒兜底,见 `agent-tools/task.mjs`)。
192
197
  // 工程模式固定段 = plan / task 皆**不入表**;普通模式固定段逐字不变(FR31 边界 / F10 边界)。
193
198
  // 判据 = 单一模式位 `engineering`(不带深度分支——子代理面随同排除,role enum
194
- // `:44-49` + spawn 门 / task execute 门已同向)。
199
+ // `subagentRoles` + spawn 门 / task execute 门已同向)。
195
200
  return engineering
196
201
  ? [timerTool, ...depthOnly]
197
202
  : [taskTool, planTool, timerTool, ...depthOnly]
package/agent/helpers.mjs CHANGED
@@ -370,12 +370,14 @@ export function readonlyToolNames(tools) {
370
370
  }
371
371
 
372
372
  /** Depth>0 tool exclusions — subagents never get the interactive main-session tools (`question`:
373
- * no user to ask; the routing guidance already lives in every persona file). Single source for
374
- * the repo: the shell-side depth>0 filter consumes this set (design = TOOLS.md §6.16 / D-TO11). */
375
- export const SUBAGENT_TOOL_EXCLUSIONS = new Set(["question"])
373
+ * no user to ask; `ssh`: the credential window is a user-facing event AND the session pool
374
+ * belongs to the main session — no UI at depth>0 and no session to hand back; the routing
375
+ * guidance already lives in every persona file). Single source for the repo: the shell-side
376
+ * depth>0 filter consumes this set (design = TOOLS.md §6.16 / D-TO11 + §6.22). */
377
+ export const SUBAGENT_TOOL_EXCLUSIONS = new Set(["question", "ssh"])
376
378
 
377
379
  /** Apply the depth>0 exclusion set — always returns a NEW array (parent table untouched).
378
- * #9(TOOLS.md §6.19):`bash` 的 `async` 参仅 depth-0——子代副本删参(照 `family-tools.mjs:98`
380
+ * #9(TOOLS.md §6.19):`bash` 的 `async` 参仅 depth-0——子代副本删参(照 `family-tools.mjs` 的 `engChildSubagent`
379
381
  * 先例;返回值仍恒新数组、未命中条目原对象直传)。运行期第二道在 `tools/bash.mjs` execute
380
382
  * (照 `read_image`「schema 门 + 运行期第二道」双线先例);批 browser-async-fix(#1045)同线
381
383
  * 扩至 `browser` 的 `async` 参(运行期第二道在 `tools/browser.mjs` execute)。 */
@@ -449,14 +451,14 @@ export const AUTO_TURN_DIGEST_DOMAIN =
449
451
  /** Engineering-mode auto-turn digest domain variant (mode = `agent.config.agent.engineering`;
450
452
  * clause 2 drops the task-tool pointer — F10: the task tool is disabled in engineering mode,
451
453
  * the batch record + ledger are the tracking authority). Single line, verbatim from the
452
- * design doc (TOOLS.md §6.15.3); mechanism = AGENT-LOOP-ASYNC-POOL.md §6.8. Content authority = parent side. */
454
+ * design doc (TOOLS.md §6.15.3); mechanism = AGENT-LOOP-ASYNC-POOL.md §6.8. Content basis = the mechanism doc; landing = eng-coder. */
453
455
  export const AUTO_TURN_DIGEST_DOMAIN_ENG =
454
456
  "[System reminder: auto-turn — background async subagents finished while there was no user message, and this turn runs automatically to digest their reports (the finished-report reminders above). No one is waiting for this reply, so organize only: 1) summarize each finished report's key points into this conversation for the user to read later; 2) (engineering mode: the task tool is disabled — no task-list update is expected this turn; the batch record + ledger are the tracking authority and are updated in real user turns); 3) write decision points with a suggested next step as text — do not execute it. FORBIDDEN this turn (mechanically enforced): modifying files, bash/execute/verify, spawning subagents, asking questions — those need a real user message. End the turn once the summaries are written.]"
455
457
 
456
458
  /** Up-stream wake-turn domain (AGENT-LOOP-UPSTREAM.md §6.27.12.8): a running subagent sent
457
459
  * an in-flight message and is waiting for the reply — the digest domain's "No one is waiting
458
460
  * for this reply" is the opposite of the truth, so the wake turn gets its own text.
459
- * Verbatim from the design doc (single line — no newlines). Content authority = parent side. */
461
+ * Verbatim from the design doc (single line — no newlines). Content basis = the mechanism doc; landing = eng-coder. */
460
462
  export const UPSTREAM_TURN_DOMAIN =
461
463
  "[System reminder: auto-turn — a running subagent sent you an in-flight message (shown below). No user message is waiting. Decide it now and reply with subagent action:'send' (id + message) — the child consumes the reply at its next turn boundary and keeps working on the unaffected parts; if the message needs no answer, say so in one line and move on. If finished subagent reports are also present above, summarize them as usual in the same turn. Do not start new work: FORBIDDEN this turn (mechanically enforced): modifying files, bash/execute/verify, spawning subagents, asking questions — those need a real user message. End the turn once the reply is sent.]"
462
464
 
@@ -464,16 +466,16 @@ export const UPSTREAM_TURN_DOMAIN =
464
466
  * expired while the session was idle and the end's latch opened this turn automatically —
465
467
  * neither the digest domain ("no one is waiting", organize-only) nor the up-stream domain
466
468
  * (a subagent waiting for a reply) fits: the timer's own semantics is "act on what the
467
- * timer was for". Verbatim from the parent-side final text (single line — no newlines).
468
- * Content authority = parent side. */
469
+ * timer was for". Verbatim from the mechanism doc (single line — no newlines).
470
+ * Content basis = the mechanism doc; landing = eng-coder. */
469
471
  export const TIMER_TURN_DOMAIN =
470
472
  "[System reminder: auto-turn — a timer you set earlier has expired (reminder above). No user message is waiting. Act on what the timer was for now: pull the data or run the check rather than continuing to reason in the abstract. Stay within the work that was in flight when the timer was set; do not open unrelated new work. Wrap up as soon as the action is done or blocked.]"
471
473
 
472
474
  /** Digest-accounting requirement line (AGENT-LOOP-ASYNC-POOL.md §6.31.3 · batch digest-accounting ·
473
475
  * 2026-10-05):消化轮账目要求——两档同发(手动 / AUTO);manual 域文本只覆盖手动档 ⇒ 要求行
474
476
  * 独立常量承载,避免 AUTO 轮无要求。`<ids>` = 未销账 id 清单占位(动态拼 = digest-account.mjs
475
- * `digestAccountRequirement`);注入面 = transient(机器线独有)。Verbatim from the parent-side
476
- * final text (single line — no newlines); content authority = parent side. */
477
+ * `digestAccountRequirement`);注入面 = transient(机器线独有)。Verbatim from the mechanism doc
478
+ * (single line — no newlines). Content basis = the mechanism doc; landing = eng-coder. */
477
479
  export const DIGEST_ACCOUNT_DOMAIN =
478
480
  "[System reminder: digest accounting — for every report listed below as not yet accounted, emit exactly one line: \"[digest-ack #<id>] digested — <key points>\" (handled) or \"[digest-ack #<id>] deferred — <reason>\" (not handled). Not yet accounted: <ids>. Do not omit any.]"
479
481
 
@@ -0,0 +1,65 @@
1
+ /**
2
+ * agent/image-heal.mjs — 图片拒绝自愈(PROVIDER.md §6.26;批 image-history-400 · 台账 #1450)。
3
+ *
4
+ * 判定(T1 图像语义 400 ∥ T2 同态重复)→ 全剥历史图(copy-on-write,人读线 `_fullHistory`
5
+ * 零损)→ 机读注记 → `_imageFailStreak` 记账(单源本档)。返回 `{ healed, dropped }`;零抛
6
+ * (失败面不阻断回合驱动——落账为尽力面)。重发(一次,非重试)与「再败照抛」归
7
+ * agent/chat-call.mjs catch 支;清零接口 = clearImageFailStreak(调用点 = chat-call 成功路径)。
8
+ */
9
+ import { looksLikeImageRejection } from "../provider/errors.mjs"
10
+ import { stripRejectedImages } from "../provider/normalize.mjs"
11
+ import { logEvent } from "../log.mjs"
12
+
13
+ /** 机读注记(§6.26 逐字;N = 剥除数)。 */
14
+ const healNote = (n) =>
15
+ `[System reminder: the provider rejected the image payload (HTTP 4xx) — ${n} image(s) were dropped from the context so the session can continue; the request was re-sent. Re-read a downscaled copy with read_image if still needed, and tell the user which image(s) were dropped.]`
16
+
17
+ /** 状态码提取(§6.26——技法同 T2 族:`LLM API error 4xx:` 构造位文本取码;非该族 ⇒ 0)。 */
18
+ function statusCodeOf(text) {
19
+ const m = String(text ?? "").match(/LLM API error (\d{3}):/)
20
+ return m ? Number(m[1]) : 0
21
+ }
22
+
23
+ /** 历史 image part 计数(T1/T2 的「史含图」判据 + dropped 取数)。 */
24
+ function countImageParts(messages) {
25
+ let n = 0
26
+ if (!Array.isArray(messages)) return 0
27
+ for (const m of messages) {
28
+ if (!Array.isArray(m?.content)) continue
29
+ for (const p of m.content) if (p?.type === "image_url") n++
30
+ }
31
+ return n
32
+ }
33
+
34
+ /** 成功路径清零(§6.26——清零调用点 = chat-call.mjs;Abort 豁免 ∥ 失败不清零)。 */
35
+ export function clearImageFailStreak(agent) {
36
+ if (agent) agent._imageFailStreak = 0
37
+ }
38
+
39
+ /**
40
+ * 判定 ∥ 剥除 ∥ 注记 ∥ streak 记账(§6.26 机制 1–3。判定先后:T2 读本回合自增前值)。
41
+ * @returns {{ healed: boolean, dropped: number }}
42
+ */
43
+ export function healImageRejection(agent, error) {
44
+ try {
45
+ // Abort 类失败豁免:既不自增也不清零(中断不是「被拒」——重启后原样重发)
46
+ if (!agent || error?.name === "AbortError") return { healed: false, dropped: 0 }
47
+ const message = String(error?.message ?? "")
48
+ const code = statusCodeOf(message)
49
+ const is4xxNon429 = code >= 400 && code < 500 && code !== 429 // 429 归 provider 重试族
50
+ const prev = Number(agent._imageFailStreak) || 0
51
+ if (is4xxNon429) agent._imageFailStreak = prev + 1 // 自增(T2 判据读上值——见上方调用序)
52
+ const dropped = countImageParts(agent.history)
53
+ if (dropped === 0) return { healed: false, dropped: 0 } // 剥除后自熄面:两判据皆需「史含图」
54
+ const t1 = code === 400 && looksLikeImageRejection(message) // 图像语义 400(HTTP 400 合取)
55
+ const t2 = is4xxNon429 && prev >= 1 // 结构兜底:上一回合同类失败(防网关文案不在词表内)
56
+ if (!t1 && !t2) return { healed: false, dropped: 0 }
57
+ // 全剥(历史本体持久修正——后续回合不再重发毒图)+ 机读注记(模型知晓 + 复合指令)
58
+ agent.history = stripRejectedImages(agent.history)
59
+ agent.history.push({ role: "user", content: healNote(dropped) })
60
+ logEvent("llm:image-heal", { dropped, streak: agent._imageFailStreak, model: agent.provider?.model })
61
+ return { healed: true, dropped }
62
+ } catch {
63
+ return { healed: false, dropped: 0 } // §6.26 零抛:失败面不阻断回合驱动
64
+ }
65
+ }
@@ -6,7 +6,7 @@
6
6
  * ② 该指针指向的轮次批档:`§1` 状态行 = 「进行中」(判定器单源 = `readBatchStatusLine`)。
7
7
  * 查找面(纯向下)= 会话锚 + 其直接子目录一层(候选枚举单源 = `ledgerChildren`——与族发现同级判据
8
8
  * 同源;不递归 ∥ 不取同胞):逐候选查行 ∥ 以各候选为基解析指针(台账写门同式)。歧义候选
9
- * (`projectRootView` 判 `ambiguous`——如工作区锚自身)⇒ 跳过该候选、照查其余(既定观察态,非读错);
9
+ * (`projectRootView` 判 `workspace`——如工作区锚自身)⇒ 跳过该候选、照查其余(既定观察态,非读错);
10
10
  * 候选读错 ∥ 不可判 ⇒ 停本候选(总判不因他候选读错而失——开路凭据仍是「两件校验全过」)。
11
11
  * 语义:开门动作 ≡ 开轮两件落盘(「启动即挂账」本体——无独立于账的开门面);单件不放行
12
12
  * (轮档独在 ∥ 行独在 ∥ 无标记 ∥ 行非在途 ∥ 指针失据 ∥ 档已收口 ⇒ 皆 false);收口即自闭
@@ -38,9 +38,9 @@ export async function lightRoundOpen(agent) {
38
38
  // 候选面 = 锚自身 + 直接子目录一层(纯向下;子项枚举单源 = `ledgerChildren`)。
39
39
  const candidates = [anchor, ...ledgerChildren(anchor).map((p) => p.root)]
40
40
  for (const cand of candidates) {
41
- // 歧义候选(≥2 候选项目——`projectRootView`)⇒ 跳过、照查其余(歧义锚不充当项目 = AC-M2-19 同判;
42
- // `openLedger` 歧义拒 = AC-M2-17——既定观察态,非读错)。
43
- if (projectRootView(cand).state === "ambiguous") continue
41
+ // 工作区候选(下辖 ≥2 项目——`projectRootView`)⇒ 跳过、照查其余(工作区锚不充当项目 = AC-M2-19 同判;
42
+ // 该态判据 = 根推导单源「已注册」的库臂排除(键控库在盘 ≠ 可作项目)——既定观察态,非读错)。
43
+ if (projectRootView(cand).state === "workspace") continue
44
44
  let rows
45
45
  try { rows = ledgerQuery({ cwd: cand, status: "在途" }) } catch { continue } // 候选读错 ⇒ 停本候选
46
46
  if (!Array.isArray(rows) || rows.length === 0) continue
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * agent/live-beat.mjs — 存活投影心跳(拍间隔 ∕ 单拍 ∕ 起 ∕ 停 —— 幂等 · 清点)——「桌面处理流 · VSC 对齐」批 R6 上提产物。
3
3
  *
4
- * 上提源(迁移前坐标)= VSC `thincoder-vscode/src/extension/panel-messages.mjs:45`(`LIVE_HEARTBEAT_MS = 2000`)
5
- * `:50`(单拍 `liveHeartbeatBeat`)`:62-75`(起 ∕ 停 `startLiveHeartbeat` ∕ `stopLiveHeartbeat`);
4
+ * 上提源(迁移前坐标)= VSC `thincoder-vscode/src/extension/panel-messages.mjs`(`LIVE_HEARTBEAT_MS = 2000`)
5
+ * (单拍 `liveHeartbeatBeat`)·(起 ∕ 停 `startLiveHeartbeat` ∕ `stopLiveHeartbeat`);
6
6
  * 桌面先例 = `thincoder-desktop/src/main/subagent-face.mjs`(拍体逐键清点)。**纯搬 + 转口,零语义改**:
7
7
  * 拍间隔 ∕ 单拍 ∕ 起停 ∕ 幂等 ∕ 清点入核。两处 VSC 端面判据**随拍体留端**(非本档语义):
8
8
  * 「未就绪不拍」前置(`panel._wvReady`)与拍日志(`ev:subreassert`)。
9
9
  *
10
10
  * 拍体(`beat` 注入)= 各端存活再断言本体:VSC = `reassertLiveChildren(panel)`(`thincoder-vscode/src/
11
- * extension/suspension.mjs:148` 投影面);桌面 = 逐键 `bridge(key).reassertLive(agent)`(只发在飞实例)。
11
+ * extension/suspension.mjs` 的 `reassertLiveChildren` 投影面);桌面 = 逐键 `bridge(key).reassertLive(agent)`(只发在飞实例)。
12
12
  * 回值 = 本拍投递条数(**清点**读数 —— 拍体自清点,本档原样透传)。
13
13
  * **`setInterval` ∕ `unref` 转注入缝**:`timer(fn, ms) ⇒ handle` ∕ `clear(handle)` 由调用方注入
14
14
  * (缺省 = 平台 `setInterval` ∕ `clearInterval`;句柄 `unref` = 可选面 —— 在场即调,不阻进程退出)。
@@ -16,7 +16,7 @@
16
16
  * 迁移轮改指本档(2026-09-29,双写窗口收口;两处 VSC 端面判据随拍体留端)。
17
17
  */
18
18
 
19
- /** 拍间隔(毫秒)——单源(VSC 同值 `panel-messages.mjs:45`):起拍即按此周期。 */
19
+ /** 拍间隔(毫秒)——单源(VSC 同值 `panel-messages.mjs` 的 `LIVE_HEARTBEAT_MS`):起拍即按此周期。 */
20
20
  export const LIVE_HEARTBEAT_MS = 2000
21
21
 
22
22
  /**
@@ -10,6 +10,31 @@ export const STALL_WINDOW_SIZE = 5
10
10
  export const STALL_THRESHOLD = 3
11
11
  export const GOAL_BUDGET_WARN_RATIO = 0.75
12
12
 
13
+ // 设计轮轮数注入阈值(AGENT-LOOP.md §6.6「设计轮轮数注入」· 台账 #1661):
14
+ // 5 = 在 persona 判定点(过 7 轮未落盘 ⇒ 降级)之前 3 轮给出计数;8 = 与 persona 同一判定点;
15
+ // 15 = persona 的首版预算(注入串里的 15 一律由它插值——改常量即改注入文本,不另留字面)。
16
+ export const DESIGN_TURN_NOTICE_AT = 5
17
+ export const DESIGN_TURN_SKELETON_AT = 8
18
+ export const DESIGN_FIRST_VERSION_TURNS = 15
19
+
20
+ /**
21
+ * 设计轮轮数提醒(纯函数——注入串 | `null`;判据源 = 链内累计轮号 + 本设计轮是否落过盘)。
22
+ * 逐字注入形 = `AGENT-LOOP.md` §6.6「注入形逐字」;角色表当前一行(`eng-designer`),
23
+ * `eng-coder` 零注入(其零落笔面另由零落笔看门狗兜)。
24
+ * @param {{role?:string|null, turn?:number, wrote?:boolean}} o `turn` = 链内累计轮号(1 起)
25
+ * @returns {string|null}
26
+ */
27
+ export function designTurnReminder({ role, turn, wrote } = {}) {
28
+ if (role !== "eng-designer") return null
29
+ const used = Number(turn ?? 0)
30
+ if (!Number.isFinite(used) || used < DESIGN_TURN_NOTICE_AT) return null
31
+ const budget = DESIGN_FIRST_VERSION_TURNS
32
+ if (used >= DESIGN_TURN_SKELETON_AT && !wrote) {
33
+ return `[System reminder: design round turn ${used}/${budget} — nothing has been written to disk yet. The 7-turn mark has passed with nothing landed: degrade the delivery NOW — write the skeleton (design doc + batch record §2) plus an unresolved list, then refine in place.]`
34
+ }
35
+ return `[System reminder: design round turn ${used}/${budget} — ${wrote ? "something has been written to disk" : "nothing has been written to disk yet"}. First version budget is ${budget} turns; past 7 turns with nothing landed the rule is a skeleton + unresolved list.]`
36
+ }
37
+
13
38
  /**
14
39
  * Inject all post-turn events into agent history.
15
40
  * Must be called after every tool-execution turn within runAgent's loop.
@@ -66,5 +91,14 @@ export function injectPostTurn(agent, results, recentCallSigs, callbacks, turn,
66
91
  })
67
92
  }
68
93
 
94
+ // 设计轮轮数注入(AGENT-LOOP.md §6.6 · 台账 #1661):轮号取 `_currentTurn`(链内累计、续跑不重置),
95
+ // 写入痕迹取 `_touchedFiles`(在 resume 保留集内 ⇒ 续跑段沿用前段痕迹);段内 `turn` 形参不参与判据。
96
+ const designReminder = designTurnReminder({
97
+ role: agent._role,
98
+ turn: agent._currentTurn ?? 0,
99
+ wrote: (agent._touchedFiles ?? []).length > 0,
100
+ })
101
+ if (designReminder) agent.history.push({ role: "user", content: designReminder })
102
+
69
103
  callbacks.onTurnEnd?.(agent, turn)
70
104
  }
@@ -32,9 +32,16 @@ import { advisorRuns, stripApprovedSuffix } from "../agent-tools/advisor-async.m
32
32
  import { looksLikeReviewOutput, advisorIncompleteMarker } from "../advisor/run.mjs"
33
33
  // #949(评审 history 串台面):同步面投递水印单点(叶档——零环)。
34
34
  import { noteReviewDelivered } from "../agent-tools/review-facts.mjs"
35
+ // #1178:动作级只读分类(settings get ∥ manifest read 类)不失效 verify——谓词单源 = dispatch-gates。
36
+ import { isReadonlyAction } from "./dispatch-gates.mjs"
35
37
 
36
38
  let _reindexFile = null
37
39
 
40
+ /** 动作级只读分类的入参(畸形 ⇒ null——谓词自守;解析口径同 dispatch 相位一)。 */
41
+ function actionArgsOf(toolCall) {
42
+ try { return JSON.parse(toolCall.arguments || "{}") } catch { return null }
43
+ }
44
+
38
45
  export async function recordToolResults(agent, toolByName, results) {
39
46
  // Multimodal user messages (injected images / not-injected reminders) must NOT be pushed
40
47
  // between tool results of parallel calls — strict providers (DeepSeek) 400 when a tool
@@ -88,7 +95,7 @@ export async function recordToolResults(agent, toolByName, results) {
88
95
  agent._calledAdvisorThisRun = false
89
96
  agent._verifiedThisRun = false
90
97
  agent._verifyPassed = undefined
91
- } else if (!tool.readonly && !tool.sideEffectExempt) {
98
+ } else if (!tool.readonly && !isReadonlyAction(toolCall.name, actionArgsOf(toolCall)) && !tool.sideEffectExempt) {
92
99
  // Non-mutating side-effect tools (bash, git): do NOT invalidate the
93
100
  // advisor review — a review is triggered by CODE MUTATIONS only
94
101
  // (user decision 2026-08-08: the guard rule is "review after code
@@ -96,6 +103,7 @@ export async function recordToolResults(agent, toolByName, results) {
96
103
  // barred from writing files, so it cannot change the reviewed code).
97
104
  // Verify IS invalidated: its state snapshot (git diff, file list)
98
105
  // may be stale after git/shell operations.
106
+ // #1178:动作级只读(settings get ∥ manifest read 一类)非侧效——verify 保持有效。
99
107
  if (agent._verifiedThisRun) {
100
108
  agent._verifiedThisRun = false
101
109
  agent._verifyPassed = undefined