@thincoder/core 0.10.7 → 0.10.8

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 (125) hide show
  1. package/CHANGELOG.md +15 -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 +11 -6
  9. package/agent/helpers.mjs +6 -4
  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/record-results.mjs +9 -1
  14. package/agent/run-stages.mjs +2 -1
  15. package/agent/setup-reminders.mjs +14 -13
  16. package/agent/setup.mjs +62 -46
  17. package/agent/spawn-child.mjs +1 -1
  18. package/agent/subagent-event.mjs +1 -1
  19. package/agent/suspension.mjs +2 -1
  20. package/agent/timers.mjs +8 -8
  21. package/agent/turn-loop.mjs +1 -1
  22. package/agent-tools/advisor.mjs +1 -1
  23. package/agent-tools/async-discard.mjs +40 -5
  24. package/agent-tools/async-settle.mjs +3 -1
  25. package/agent-tools/audit-block.mjs +1 -1
  26. package/agent-tools/batch-paths.mjs +20 -20
  27. package/agent-tools/checkpoint.mjs +1 -1
  28. package/agent-tools/design-slots.mjs +44 -5
  29. package/agent-tools/doc-check.mjs +37 -0
  30. package/agent-tools/eng.mjs +5 -3
  31. package/agent-tools/manifest.mjs +18 -18
  32. package/agent-tools/parent-channel.mjs +3 -3
  33. package/agent-tools/review-facts.mjs +2 -2
  34. package/agent-tools/settings.mjs +1 -1
  35. package/agent-tools/spawn-gates.mjs +8 -6
  36. package/agent-tools/ssh-session.mjs +439 -0
  37. package/agent-tools/ssh-transport.mjs +311 -0
  38. package/agent-tools/subagent-scheduler.mjs +6 -5
  39. package/agent-tools/subagent.mjs +7 -5
  40. package/agent-tools/task.mjs +1 -1
  41. package/agent-tools/timer.mjs +6 -0
  42. package/agent-tools/zero-write-watch.mjs +2 -2
  43. package/agent-tools.mjs +3 -2
  44. package/attachments.mjs +4 -4
  45. package/browser/cdp.mjs +2 -2
  46. package/browser/launch.mjs +50 -4
  47. package/browser/snapshot.mjs +1 -1
  48. package/config-io.mjs +7 -2
  49. package/config-watch.mjs +2 -2
  50. package/config.mjs +7 -0
  51. package/declaration.mjs +5 -1
  52. package/doc-check/anchors.mjs +100 -0
  53. package/doc-check/index.mjs +61 -0
  54. package/doc-check/width.mjs +131 -0
  55. package/file-refs.mjs +6 -6
  56. package/generate-title.mjs +1 -1
  57. package/hooks.mjs +1 -1
  58. package/i18n.mjs +11 -1
  59. package/ledger-cmd.mjs +4 -5
  60. package/ledger-db.mjs +100 -21
  61. package/ledger-migrate.mjs +3 -3
  62. package/ledger-read.mjs +6 -7
  63. package/ledger-tools.mjs +24 -9
  64. package/ledger.mjs +23 -54
  65. package/log.mjs +17 -3
  66. package/manifest-discovery.mjs +21 -18
  67. package/manifest-schema.mjs +2 -13
  68. package/manifest.mjs +37 -22
  69. package/mcp/transport-stdio.mjs +1 -1
  70. package/mcp.mjs +2 -2
  71. package/memory/core.mjs +1 -1
  72. package/memory/scan.mjs +1 -1
  73. package/memory/sweep.mjs +1 -1
  74. package/model-specs.mjs +7 -7
  75. package/notify-policy.mjs +6 -6
  76. package/package.json +2 -1
  77. package/permission.mjs +14 -0
  78. package/prompts/advisor-design.md +43 -13
  79. package/prompts/advisor-round1.md +25 -5
  80. package/prompts/advisor-round2.md +17 -4
  81. package/prompts/advisor-round3.md +17 -4
  82. package/prompts/common.md +60 -6
  83. package/prompts/consult-base.md +8 -2
  84. package/prompts/discipline-engineering.md +11 -10
  85. package/prompts/discipline-normal.md +22 -14
  86. package/prompts/persona-coder.md +1 -1
  87. package/prompts/persona-eng-designer.md +8 -3
  88. package/prompts/persona-engineering.md +21 -5
  89. package/prompts/persona-normal.md +4 -1
  90. package/provider/errors.mjs +8 -0
  91. package/provider/google.mjs +3 -0
  92. package/provider/normalize.mjs +42 -12
  93. package/provider/responses-request.mjs +1 -1
  94. package/provider/responses.mjs +2 -2
  95. package/provider/wait-status.mjs +1 -1
  96. package/provider-flows.mjs +1 -1
  97. package/queued.mjs +2 -2
  98. package/rules.mjs +4 -4
  99. package/session-gc.mjs +1 -1
  100. package/session-lifecycle.mjs +1 -1
  101. package/session-slot-write.mjs +2 -2
  102. package/session-slots-manifest.mjs +1 -1
  103. package/session.mjs +1 -1
  104. package/shell-candidates.mjs +2 -2
  105. package/shell-identity.mjs +1 -1
  106. package/team.mjs +61 -4
  107. package/think-off.mjs +3 -3
  108. package/token-ttl.mjs +1 -1
  109. package/tool-docs/bash.md +1 -1
  110. package/tool-docs/browser.md +1 -1
  111. package/tool-docs/doc_check.md +31 -0
  112. package/tool-docs/manifest.md +14 -2
  113. package/tool-docs/ssh.md +34 -0
  114. package/tool-docs/subagent.md +2 -0
  115. package/tool-docs/timer.md +2 -1
  116. package/tools/browser.mjs +1 -1
  117. package/tools/execute.mjs +1 -1
  118. package/tools/git-run.mjs +2 -2
  119. package/tools/git.mjs +8 -8
  120. package/tools/index.mjs +3 -0
  121. package/tools/ops.mjs +1 -1
  122. package/tools/process-tree.mjs +2 -2
  123. package/tools/ssh-askpass.mjs +35 -0
  124. package/tools/ssh.mjs +92 -0
  125. package/vision-reader.mjs +4 -4
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
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.8] — 2026-10-16
9
+
10
+ > 0.10.7 → 0.10.8(月内 +1——发布时定号)
11
+
12
+ ### Added
13
+
14
+ - **MCP 探活具名读数(#1431——2026-10-14 走查轮)**:`probeMcpServer` 返回值增 `tools`(工具名数组——原 `mcpTools` 到手即弃,控制台探活只见计数);服务端页面随正(具名读数形,缺名回落计数形——零回归)。
15
+ - **注入开关(#1420——injection-switches 批 2026-10-14)**:`agent.injectDocs` ∥ `agent.injectMemories` 两枚全局开关(DEFAULTS `true`;判据 `!== false`);关 = 阻断三块自动注入(项目指令文件块 ∥ 文档检索块 ∥ 记忆检索块——检索调用不执行),不删数据;`settings` 工具 + CLI `/config` 热应用(写后下一次组装生效);VSC/桌面设置界面各落两开关。
16
+ - **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 / 桌面)界面在位。
17
+ - **`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`)。
18
+ - **产品内建 `doc_check` 工具(#1586——builtin-doc-check 批 2026-10-16)**:文档纪律机检内建为产品工具(只读 · 报告态 · **非门禁**)——起步两条判据:锚(档 token ± `§N` ⇒ 档缺 ∥ 节缺;行号不作节指)∥ 行宽(>300 字符;四类谓词 + 区带豁免);声明面 = `PROJECT-MANIFEST.json` `checkConfig`(零硬编码路径);项目自建脚本退出判据链。
19
+ - **台账工具项目根读写同一推导(#1588——ledger-root-derivation 批 2026-10-16)**:`resolveLedgerRoot` 判据单源(自目标路径沿祖先链取最近「已注册」= 带档 ∨ 自身路径键控库在盘;工作区态不立为根);族发现取祖先支;无根契约 = 读面空账 ∥ 写面抛(零落盘);写面「抛错列候选」退场。
20
+
6
21
  ## [0.10.7] — 2026-10-14
7
22
 
8
23
  > 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")
@@ -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)。 */
@@ -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
  /**
@@ -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
@@ -18,7 +18,7 @@ import { carrierField, getAsyncPool, releaseSettledEntry } from "../agent-tools/
18
18
  import { accountDigestRound, isDigestRound } from "./digest-account.mjs"
19
19
  // 批 4 CLI-ASYNC-DISCARD(AGENT-LOOP-ASYNC-POOL.md §6.20):中止分支「只清已死」收尾单点
20
20
  // (#9:+ 后台 bash 任务族——逐条杀树 + 出池 + 墓碑)
21
- import { discardAbortedPool, discardAbortedAdvisors, discardAbortedBgTasks, discardAbortedBrowserTasks } from "../agent-tools/async-discard.mjs"
21
+ import { discardAbortedPool, discardAbortedAdvisors, discardAbortedBgTasks, discardAbortedBrowserTasks, discardAbortedSshSessions } from "../agent-tools/async-discard.mjs"
22
22
  // R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3a):回合末域登记 flush(写工具钩子累积 →
23
23
  // 整写一次本实例 peers 文件——无写入跳过;失败容忍不抛)
24
24
  import { flushPeerDomains } from "../peer-domains.mjs"
@@ -216,6 +216,7 @@ export async function finalizeAgentTurn(agent, ctx) {
216
216
  discardAbortedAdvisors(agent)
217
217
  discardAbortedBgTasks(agent)
218
218
  discardAbortedBrowserTasks(agent) // 批 browser-async-fix:后台浏览器动作同面(不杀浏览器)
219
+ discardAbortedSshSessions(agent) // 批 ssh-session:SSH 会话同面(逐条杀树——远端连接该断)
219
220
  // R17: the consult family dies with the user stop (marked stopped — no digest).
220
221
  cleanupConsultSessions(agent)
221
222
  // ASYNC-RESULT-CONTAINER.md D2:pending 单容器——中止丢弃 consult/escalate 族停靠
@@ -20,7 +20,7 @@
20
20
  * #34 adds the per-turn value refresh (mtime-gated re-read — §2.6 ③b / KD-M1-17..M1-19):
21
21
  * the line always carries the CURRENT on-disk phase, not the assembly-time snapshot.
22
22
  * 2026-09-21(#188):**状态选行**(③' `projectView(agent.cwd)` —— KD-M1-26)——`ok` ⇒ 相位行
23
- * (逐字零改);`no-project` / `ambiguous` ⇒ 报明行;`missing` / `invalid` ⇒ 有既往好值保守沿用、
23
+ * (逐字零改);`no-project` / `workspace` ⇒ 报明行;`missing` / `invalid` ⇒ 有既往好值保守沿用、
24
24
  * 无 ⇒ 报明行;无锚 ⇒ **零 I/O / 零报明**(KD-M1-27)。报明行构造 = `manifestReportLine` 四态。
25
25
  *
26
26
  * #113(S1 续轮第二批——VSC 侧并入 ④ 段):`pushInjections`(机器行专用注入——编辑器上下文
@@ -76,11 +76,12 @@ export function manifestStateLine({ phase }) {
76
76
  }
77
77
 
78
78
  /**
79
- * 报明行构造 — pure, unit-testable(**本批新增**——`docs/core/design/MANIFEST.md` §2.6 条 1b /
80
- * KD-M1-26):`projectView` 四非 ok 态各自的行文,**逐字**(同族前缀 `MANIFEST_LINE_PREFIX`——
81
- * 报明行与相位行共用单活体机制)。歧义行按 `matched` **两变体**(`manifest` = 带档级「marker
82
- * directories」/ `git` = 裸仓档「repositories … (none carries …)」——同口径 = `TOOLS.md` §6.13 A22);
83
- * `candidates` 按名排序(判据同 `discoverProjects`)、`<abs…>` 以「、」连接;`errors` 逐条以「;」连接。
79
+ * 报明行构造 — pure, unit-testable(`docs/core/design/MANIFEST.md` §2.6 条 1b / KD-M1-26):
80
+ * `projectView` 四非 ok 态各自的行文,**逐字**(同族前缀 `MANIFEST_LINE_PREFIX`——报明行与相位行
81
+ * 共用单活体机制)。工作区行按 `matched` **两变体**(`manifest` = 带档级「<N> projects」/ `git` =
82
+ * 裸仓档「<N> repositories (none carries …)」——同口径 = `TOOLS.md` §6.13 A22);`candidates` 按名
83
+ * 排序(判据同 `discoverProjects`)、`<abs…>` 以「、」连接;`errors` 逐条以「;」连接。行文口径 =
84
+ * **信息行,非错误行**(如实播报、不替它挑;措辞不含 `ambiguous` / 候选二义框定)。
84
85
  * `ok` / 未知态 ⇒ `null`(无报明行——相位行归 `manifestStateLine`)。
85
86
  * @param {{state?:string, cwd?:string, root?:string|null, path?:string|null, candidates?:string[], errors?:string[], matched?:string|null}} p 行素材(= `projectView` 返回面 + 锚)
86
87
  * @returns {string|null} 行文 / null(非报明态)
@@ -90,12 +91,12 @@ export function manifestReportLine({ state, cwd, root, path, candidates, errors,
90
91
  return `${MANIFEST_LINE_PREFIX}none — no project at ${cwd} (no manifest on the ancestor chain, none below). ` +
91
92
  `Parameters fall back to defaults. Create PROJECT-MANIFEST.json here to land a project (git optional).]`
92
93
  }
93
- if (state === "ambiguous") {
94
+ if (state === "workspace") {
94
95
  const list = candidates ?? []
95
- const head = matched === "git"
96
- ? `ambiguous — ${list.length} candidate repositories under ${cwd} (none carries PROJECT-MANIFEST.json):`
97
- : `ambiguous — ${list.length} candidate marker directories under ${cwd}:`
98
- return `${MANIFEST_LINE_PREFIX}${head} ${list.join("、")} — target the intended one explicitly (the mechanism never picks).]`
96
+ const holding = matched === "git"
97
+ ? `${list.length} repositories (none carries PROJECT-MANIFEST.json)`
98
+ : `${list.length} projects`
99
+ return `${MANIFEST_LINE_PREFIX}workspace — ${cwd} is a workspace holding ${holding}: ${list.join("、")} — pass an explicit target path to act on one of them.]`
99
100
  }
100
101
  if (state === "missing") {
101
102
  return `${MANIFEST_LINE_PREFIX}missing — the resolved project root ${root} has no PROJECT-MANIFEST.json; ` +
@@ -165,8 +166,8 @@ export function pushManifestStateReminder(agent, { depth = 0 } = {}) {
165
166
  if (view.state === "ok") {
166
167
  const phase = (agent.manifest ?? view.manifest)?.phase
167
168
  line = phase === undefined ? null : manifestStateLine({ phase })
168
- } else if (view.state === "no-project" || view.state === "ambiguous") {
169
- line = manifestReportLine({ ...view, cwd: resolve(agent.cwd) }) // 本批新报明格(无项目 / 歧义)
169
+ } else if (view.state === "no-project" || view.state === "workspace") {
170
+ line = manifestReportLine({ ...view, cwd: resolve(agent.cwd) }) // 本批新报明格(无项目 / 工作区)
170
171
  } else if (agent.manifest) {
171
172
  line = manifestStateLine({ phase: agent.manifest.phase }) // missing / invalid:有既往好值 ⇒ 保守沿用
172
173
  } else {