@thincoder/core 0.9.5 → 0.10.1

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 (215) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/abort-provenance.mjs +10 -10
  4. package/advisor/citations.mjs +5 -5
  5. package/advisor/compaction.mjs +16 -15
  6. package/advisor/loop.mjs +12 -12
  7. package/advisor/messages.mjs +6 -6
  8. package/advisor/project-context.mjs +5 -5
  9. package/advisor/repos.mjs +3 -3
  10. package/advisor/run.mjs +22 -12
  11. package/advisor.mjs +4 -4
  12. package/agent/assemble.mjs +111 -0
  13. package/agent/chat-call.mjs +68 -0
  14. package/agent/child-marks.mjs +2 -2
  15. package/agent/completion.mjs +9 -2
  16. package/agent/continue-decision.mjs +25 -0
  17. package/agent/dispatch-gates.mjs +140 -0
  18. package/agent/dispatch-run.mjs +167 -0
  19. package/agent/dispatch.mjs +54 -296
  20. package/agent/family-tools.mjs +5 -5
  21. package/agent/helpers.mjs +53 -3
  22. package/agent/live-beat.mjs +49 -0
  23. package/agent/post-turn.mjs +4 -8
  24. package/agent/record-results.mjs +23 -20
  25. package/agent/relay-prefix.mjs +2 -2
  26. package/agent/run-stages.mjs +15 -11
  27. package/agent/run-start.mjs +111 -0
  28. package/agent/setup-reminders.mjs +9 -9
  29. package/agent/setup.mjs +32 -4
  30. package/agent/spawn-child.mjs +9 -9
  31. package/agent/suspension.mjs +89 -29
  32. package/agent/timers.mjs +113 -0
  33. package/agent/turn-loop.mjs +244 -0
  34. package/agent/write-gate.mjs +103 -6
  35. package/agent-tools/advisor-async.mjs +9 -9
  36. package/agent-tools/advisor-settle.mjs +12 -12
  37. package/agent-tools/advisor.mjs +36 -41
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +21 -18
  40. package/agent-tools/audit-block.mjs +7 -7
  41. package/agent-tools/bash-async.mjs +281 -0
  42. package/agent-tools/batch-lifecycle.mjs +57 -18
  43. package/agent-tools/batch-paths.mjs +52 -8
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +12 -65
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +56 -56
  49. package/agent-tools/context.mjs +4 -20
  50. package/agent-tools/design-token.mjs +3 -3
  51. package/agent-tools/digest-budget.mjs +1 -1
  52. package/agent-tools/eng.mjs +5 -5
  53. package/agent-tools/escalate-async.mjs +16 -11
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +2 -2
  56. package/agent-tools/parent-channel.mjs +3 -13
  57. package/agent-tools/plan.mjs +3 -4
  58. package/agent-tools/read-history.mjs +12 -28
  59. package/agent-tools/recent-changes.mjs +4 -5
  60. package/agent-tools/review-facts.mjs +1 -1
  61. package/agent-tools/settings.mjs +47 -20
  62. package/agent-tools/skill.mjs +3 -5
  63. package/agent-tools/spawn-gates.mjs +3 -3
  64. package/agent-tools/subagent-actions-query.mjs +270 -0
  65. package/agent-tools/subagent-actions.mjs +21 -265
  66. package/agent-tools/subagent-async.mjs +25 -20
  67. package/agent-tools/subagent-panel.mjs +33 -4
  68. package/agent-tools/subagent-run.mjs +36 -21
  69. package/agent-tools/subagent-scheduler.mjs +28 -27
  70. package/agent-tools/subagent-spawn.mjs +45 -14
  71. package/agent-tools/subagent.mjs +33 -53
  72. package/agent-tools/task.mjs +3 -9
  73. package/agent-tools/timer.mjs +12 -12
  74. package/agent-tools/verify.mjs +14 -13
  75. package/agent-tools.mjs +0 -1
  76. package/agent.mjs +18 -355
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +11 -6
  80. package/config-watch.mjs +83 -0
  81. package/config.mjs +17 -4
  82. package/context-degrade.mjs +81 -0
  83. package/context-echo.mjs +88 -0
  84. package/context-push.mjs +51 -0
  85. package/context.mjs +8 -187
  86. package/conventions.mjs +110 -161
  87. package/declaration.mjs +194 -0
  88. package/embedding.mjs +3 -1
  89. package/env-ref.mjs +81 -0
  90. package/explore-distill.mjs +2 -3
  91. package/file-links.mjs +53 -0
  92. package/file-refs.mjs +103 -0
  93. package/generate-title.mjs +28 -4
  94. package/history-window.mjs +17 -2
  95. package/i18n.mjs +2 -0
  96. package/index-discover.mjs +7 -7
  97. package/ledger-cmd.mjs +34 -126
  98. package/ledger-db.mjs +10 -2
  99. package/ledger-tools.mjs +188 -0
  100. package/ledger.mjs +9 -4
  101. package/log.mjs +12 -7
  102. package/manifest-discovery.mjs +131 -0
  103. package/manifest-schema.mjs +166 -0
  104. package/manifest.mjs +18 -217
  105. package/mcp.mjs +4 -1
  106. package/memory/code-search.mjs +149 -0
  107. package/memory/code-sync.mjs +89 -230
  108. package/memory/core.mjs +47 -32
  109. package/memory/docs.mjs +63 -262
  110. package/memory/file-list.mjs +89 -0
  111. package/memory/file-walk.mjs +7 -4
  112. package/memory/memory-tool.mjs +229 -0
  113. package/memory/schema.mjs +18 -3
  114. package/memory/sweep.mjs +289 -0
  115. package/memory/sync-tail.mjs +81 -0
  116. package/memory-status.mjs +69 -0
  117. package/memory.mjs +3 -0
  118. package/model-specs.mjs +16 -7
  119. package/notify-policy.mjs +51 -0
  120. package/package.json +1 -1
  121. package/peer-claims.mjs +2 -2
  122. package/peer-domains.mjs +7 -11
  123. package/peer-instances.mjs +9 -10
  124. package/process-probe-exec.mjs +187 -0
  125. package/process-probe.mjs +27 -175
  126. package/prompt-overlays.mjs +11 -10
  127. package/prompts/advisor-design.md +2 -2
  128. package/prompts/advisor-round2.md +1 -1
  129. package/prompts/advisor-round3.md +1 -1
  130. package/prompts/common.md +16 -8
  131. package/prompts/discipline-engineering.md +57 -16
  132. package/prompts/discipline-normal.md +6 -3
  133. package/prompts/persona-coder.md +0 -1
  134. package/prompts/persona-eng-coder.md +1 -5
  135. package/prompts/persona-eng-designer.md +2 -3
  136. package/prompts/persona-engineering.md +32 -11
  137. package/prompts/persona-explore.md +0 -1
  138. package/prompts/persona-normal.md +1 -1
  139. package/prompts/persona-plan.md +3 -4
  140. package/prompts/subagent-base.md +14 -0
  141. package/provider/core.mjs +11 -48
  142. package/provider/list-models.mjs +3 -0
  143. package/provider/responses-request.mjs +237 -0
  144. package/provider/responses.mjs +6 -228
  145. package/provider-flows.mjs +248 -0
  146. package/queued.mjs +90 -0
  147. package/rules.mjs +209 -2
  148. package/session-guard.mjs +27 -7
  149. package/session-lifecycle.mjs +90 -18
  150. package/session-rename.mjs +5 -1
  151. package/session-slot-claims.mjs +230 -0
  152. package/session-slot-scan.mjs +299 -0
  153. package/session-slot-verify.mjs +304 -0
  154. package/session-slot-write.mjs +74 -8
  155. package/session-slots-manifest.mjs +163 -241
  156. package/session-slots.mjs +99 -71
  157. package/session.mjs +30 -10
  158. package/shell-candidates.mjs +78 -0
  159. package/text-budget.mjs +2 -2
  160. package/think-off.mjs +49 -0
  161. package/token-ttl.mjs +15 -4
  162. package/tool-args.mjs +73 -0
  163. package/tool-docs/advisor.md +9 -0
  164. package/tool-docs/batch.md +1 -0
  165. package/tool-docs/code_search.md +1 -0
  166. package/tool-docs/consult_start.md +5 -0
  167. package/tool-docs/consult_stop.md +3 -0
  168. package/tool-docs/context.md +7 -0
  169. package/tool-docs/doc_search.md +1 -0
  170. package/tool-docs/edit.md +2 -13
  171. package/tool-docs/eng.md +1 -0
  172. package/tool-docs/execute.md +1 -9
  173. package/tool-docs/git.md +25 -51
  174. package/tool-docs/goal.md +6 -0
  175. package/tool-docs/ledger_add.md +1 -0
  176. package/tool-docs/ledger_close.md +1 -0
  177. package/tool-docs/ledger_count.md +1 -0
  178. package/tool-docs/ledger_query.md +1 -0
  179. package/tool-docs/ledger_update.md +1 -0
  180. package/tool-docs/main_history.md +1 -0
  181. package/tool-docs/memory.md +10 -0
  182. package/tool-docs/notify_parent.md +9 -0
  183. package/tool-docs/peer_instances.md +1 -0
  184. package/tool-docs/plan.md +5 -0
  185. package/tool-docs/process.md +7 -3
  186. package/tool-docs/read_history.md +7 -0
  187. package/tool-docs/recent_changes.md +1 -0
  188. package/tool-docs/repo_outline.md +1 -0
  189. package/tool-docs/settings.md +6 -0
  190. package/tool-docs/skill.md +5 -0
  191. package/tool-docs/subagent.md +31 -0
  192. package/tool-docs/task.md +7 -0
  193. package/tool-docs/timer.md +6 -0
  194. package/tool-docs/verify.md +1 -0
  195. package/tool-docs/wait_for.md +2 -1
  196. package/tools/bash.mjs +28 -4
  197. package/tools/edit-batch.mjs +4 -4
  198. package/tools/edit-diff.mjs +4 -23
  199. package/tools/exec-run.mjs +102 -4
  200. package/tools/execute.mjs +5 -2
  201. package/tools/file.mjs +4 -9
  202. package/tools/git-run.mjs +1 -1
  203. package/tools/git.mjs +0 -24
  204. package/tools/index.mjs +2 -1
  205. package/tools/linter.mjs +2 -1
  206. package/tools/node-child.mjs +21 -0
  207. package/tools/ops.mjs +50 -13
  208. package/tools/patch.mjs +4 -2
  209. package/tools/repomap.mjs +20 -9
  210. package/tools/shared.mjs +8 -6
  211. package/tools/web.mjs +5 -2
  212. package/tools/write-path.mjs +1 -1
  213. package/traces/trace-store.mjs +17 -14
  214. package/vision-reader.mjs +76 -0
  215. package/agent-tools/batch-segment.mjs +0 -18
@@ -1,136 +1,27 @@
1
1
  /**
2
2
  * agent/dispatch.mjs — two-phase tool call execution
3
+ * 2026-09-28 拆分(Module Split Policy——498/499 贴 500 硬限跨档):门面谓词 ∕ 记账外提
4
+ * `./dispatch-gates.mjs`(四谓词 + logToolError + noteExecutedMutation)、单条执行体外提
5
+ * `./dispatch-run.mjs`(runPreparedItem——原 runOne 闭包,verbatim + 形参代入);
6
+ * `executeToolCalls` 签名与对外缝零改(本档导出面零变)。
3
7
  */
4
- import { logEvent, errText, headText } from "../log.mjs"
5
- import { offloadToolResult, FILE_MUTATORS } from "./helpers.mjs"
8
+ import { FILE_MUTATORS, toolTouchPaths } from "./helpers.mjs"
6
9
  import { runHooks } from "../hooks.mjs"
7
- import { snapshotForUndo } from "../undo-stack.mjs"
8
- import { isCodePath, loadConventions } from "../conventions.mjs"
9
- // R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3b / §4.4 D-MI17–D-MI21):写工具钩子——peerCollabNote
10
- // (执行前冲突与认领命中检测——软提示不阻止)+ recordPeerWrites(成功后足迹与认领登记)。
11
- import { PEER_WRITE_TOOLS, peerCollabNote, recordPeerWrites, markClaimNoted } from "../peer-domains.mjs"
12
- import { writeFileSync, mkdirSync, existsSync } from "node:fs"
13
- import { join, resolve, relative } from "node:path"
14
- import { homedir } from "node:os"
15
- // §29 fix A(AGENT-LOOP.md §29——2026-09-07):FILE_MUTATORS 的 mutation-seq 记账从
16
- // 批后提交(record-results noteMutations)移到执行成功即刻——唯一记账点(取代批后段
17
- // + agent.mjs 中断分支记账——不双计)——同消息 [写 + async advisor launch] 时 launch 前
18
- // 完成的写在 launchSeq 之前落地 → settle 不再误判 stale(§29 症状根因)。
19
- import { noteMutations } from "../agent-tools/advisor-async.mjs"
10
+ import { isCodePath, loadProjectDeclaration } from "../conventions.mjs"
11
+ import { resolve, relative } from "node:path"
20
12
  import { anyLiveDesignSlot } from "../token-ttl.mjs"
21
13
  // M4 写权门禁(模块设计 §2.1#2):冻结窗口判据组装(被审文件集 = 声明文档集 + 批次档
22
14
  // 的合流点)落 write-gate.mjs 单一权威源——本档只 import 消费(KD-M4-4 拆分点)。
23
- import { freezeWindowConflict } from "./write-gate.mjs"
24
-
25
- const ERRORS_DIR = join(homedir(), ".thincoder", "tool-errors")
26
-
27
- /**
28
- * Persist a tool error to ~/.thincoder/tool-errors/YYYY-MM-DD/HHmmss-toolName.log
29
- * Only called for actual execution failures and malformed invocations.
30
- * Skipped for intentional denials (plan mode, user reject).
31
- */
32
- function logToolError(toolName, args, error) {
33
- try {
34
- const now = new Date()
35
- const ymd = now.toISOString().slice(0, 10)
36
- const ts = now.toISOString().replace(/:/g, "").replace(/\..+/, "").replace("T", "-")
37
- const dir = join(ERRORS_DIR, ymd)
38
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
39
- const file = join(dir, `${ts}-${toolName.replace(/[/\\]/g, "_")}.log`)
40
- const entry = [
41
- `time: ${now.toISOString()}`,
42
- `tool: ${toolName}`,
43
- `args: ${JSON.stringify(args, null, 2).slice(0, 2000)}${JSON.stringify(args, null, 2).length > 2000 ? "… (truncated)" : ""}`,
44
- `error: ${error?.message ?? String(error)}`,
45
- error?.stack ? `stack:\n${error.stack}` : "",
46
- ].filter(Boolean).join("\n") + "\n"
47
- writeFileSync(file, entry, "utf8")
48
- } catch {
49
- // Log failure itself must not crash the agent
50
- }
51
- }
52
-
53
- /**
54
- * action-level classification (AGENT-LOOP-SUBAGENT.md §6.7 D-M1): the merged subagent
55
- * tool expresses spawn (side effect) and status (read-only query) through
56
- * its `action` parameter — the tool-level readonly flag can no longer express both.
57
- * dispatch Phase-1/Phase-2 classifies per action: status behaves as readonly
58
- * (planMode pass / no permission ask / batchable), spawn keeps its non-readonly
59
- * gates, escalate runs non-readonly AND serially (the retired escalate tool had no
60
- * parallel flag — zero behavior change under the merged surface).
61
- * AGENT-LOOP-SUBAGENT.md §6.7.2 cancel (19.5.2b round2 #4): CONTROL-class exemption — cancel only
62
- * stops, never starts. isSubagentControlAction feeds the SAME two gate sites as
63
- * readonly (planMode pass / no permission ask — never joins a batch approval
64
- * group / no handler → not denied — digest 内 cancel 放行).
65
- * AGENT-LOOP-SUBAGENT.md §6.7.2 panel (round1 #5): view 面归只读类(同 status——planMode 放行、免
66
- * 审批、可批并行);freeze 面归控制类(同 cancel——planMode 放行、免权限审批、
67
- * 批审批不入组、digest 内放行)。freeze 存在(非空 key)即控制类——否则只读类。
68
- */
69
- function isSubagentReadonlyAction(toolName, args) {
70
- // §6 memory 工具面重构(MEMORY.md §6 D-M5):memory search/list 是只读动作——与
71
- // subagent status 同分类(planMode 放行/免审批——Phase-2 批并行只认工具级
72
- // readonly/parallel,memory 无 parallel → 按非只读串行,见 MEMORY.md §6.4 实现注)。
73
- // 动作级判定——不能按工具名(同一 memory 工具的 put/delete/clear 保持侧效门)。
74
- if (toolName === "memory") {
75
- const action = args?.action
76
- return action === "search" || action === "list"
77
- }
78
- // SETTINGS-TOOL.md(2026-09-05):settings list/get 是只读动作(memory search/list 同分类——
79
- // planMode 放行/免审批);set 保持侧效门。
80
- if (toolName === "settings") {
81
- const action = args?.action
82
- return action === "list" || action === "get"
83
- }
84
- if (toolName !== "subagent" || !args || typeof args !== "object") return false
85
- const action = args.action
86
- // AGENT-LOOP-SUBAGENT.md §6.7.5: check 动作已删除——只读面仅剩 status(planMode 放行/免权限审批/可批并行)
87
- if (action === "status") return true
88
- // SUBAGENT-OBSERVE-SEND:observe = readonly 查询(同 status——digest/planMode 放行)
89
- if (action === "observe") return true
90
- // AGENT-LOOP-SUBAGENT.md §6.7.2 panel view 面(freeze 缺省/空 = 视图请求——readonly;非空 freeze 归控制类)
91
- if (action === "panel" && (args.freeze === undefined || args.freeze === null || String(args.freeze) === "")) return true
92
- return false
93
- }
94
- function isSubagentControlAction(toolName, args) {
95
- if (toolName !== "subagent") return false
96
- if (args?.action === "cancel") return true
97
- // SUBAGENT-OBSERVE-SEND:send = 控制类豁免(同 cancel——父回合内显式调用即授权——
98
- // 写子输入队列属父对子轻量引导,非产品代码写——免审批、planMode 放行、digest 内放行)
99
- if (args?.action === "send") return true
100
- // AGENT-LOOP-SUBAGENT.md §6.7.2 panel freeze 面(D-P3 门控在 executor——只读/控制分类在此)
101
- if (args?.action === "panel" && args.freeze !== undefined && args.freeze !== null && String(args.freeze) !== "") return true
102
- return false
103
- }
104
- /**
105
- * §2.6 token 链终消费制(2026-09-07——评审 #7d dispatch 分类):consume-design =
106
- * 非只读控制动作——planMode 拒绝(不入 readonly/control 豁免——与其他非只读动作同门)、
107
- * 免权限审批、不入批审批分组(无文件写——控制类直行——只停既有状态不起新副作用)。
108
- * 与 cancel 的不同:cancel 是控制类豁免(planMode 放行),consume-design 按设计
109
- * planMode 拒绝——故不并入 isSubagentControlAction,单独谓词只接权限豁免位。
110
- */
111
- function isSubagentConsumeDesignAction(toolName, args) {
112
- return toolName === "subagent" && args?.action === "consume-design"
113
- }
114
- function isSubagentEscalateAction(toolName, args) {
115
- return toolName === "subagent" && args?.action === "escalate"
116
- }
117
-
118
- /**
119
- * §29 fix A — 唯一记账点:FILE_MUTATORS 工具执行成功即刻记 mutation seq(abs 路径)。
120
- * 取代 record-results 批后段 + agent.mjs 中断分支的 noteMutations(不双计——中断+同批
121
- * launch 场景 seq 单计,AGENT-LOOP.md §29 T-A1i)。调用时机 = 写执行成功(非 Error 前缀
122
- * 结果——recordPeerWrites 同款门);routed(M2 ACP 客户端执行)成功同样记账。
123
- */
124
- function noteExecutedMutation(agent, tool, args) {
125
- let paths
126
- try {
127
- paths = tool.touchedPaths ? tool.touchedPaths(args ?? {}) : [args?.path]
128
- } catch { return }
129
- const abs = (paths ?? [])
130
- .filter((p) => typeof p === "string" && p)
131
- .map((p) => resolve(agent.cwd, p))
132
- if (abs.length > 0) noteMutations(agent, abs)
133
- }
15
+ import { freezeWindowConflict, batchRecordWriteConflict } from "./write-gate.mjs"
16
+ // 门面谓词 ∕ 记账(2026-09-28 拆分外提——见头注):动作级四谓词 + logToolError 住 ./dispatch-gates.mjs。
17
+ import {
18
+ logToolError, isSubagentConsumeDesignAction, isSubagentEscalateAction,
19
+ readonlyActionOf, controlActionOf, // E1(§2.5):工具面动作谓词钩子优先 ∕ 核名面谓词回落
20
+ } from "./dispatch-gates.mjs"
21
+ // 单条执行体(2026-09-28 拆分外提——见头注):runPreparedItem(原 runOne 闭包;本档两处调用点改指)。
22
+ import { runPreparedItem } from "./dispatch-run.mjs"
23
+ // B7 3a:作用域规则 JIT 注入(核单源——`../rules.mjs`;相位一末打点见下方缝注释)。
24
+ import { injectScopedRules } from "../rules.mjs"
134
25
 
135
26
  /**
136
27
  * Two-phase execution:
@@ -143,7 +34,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
143
34
  // ---- Phase 1: serial preparation ----
144
35
  // Pre-gates run per tool (parse/planMode/engineering gates); non-readonly tools
145
36
  // that REACH the permission stage are collected into one batch — a single merged
146
- // ask covers the whole toolCalls array (§16 D-B1, "approve all / one by one /
37
+ // ask covers the whole toolCalls array (D-B1, "approve all / one by one /
147
38
  // deny"). Tools stopped by a pre-gate never join the batch (review #7).
148
39
  const prepared = []
149
40
  const permPending = [] // { toolCall, tool, args } — reached the permission stage
@@ -164,7 +55,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
164
55
  continue
165
56
  }
166
57
 
167
- if (agent.planMode && !tool.readonly && !isSubagentReadonlyAction(toolCall.name, args) && !isSubagentControlAction(toolCall.name, args)) {
58
+ if (agent.planMode && !tool.readonly && !readonlyActionOf(tool, toolCall.name, args) && !controlActionOf(tool, toolCall.name, args)) {
168
59
  prepared.push({ toolCall, tool, denied: true, reason: "plan mode" })
169
60
  continue
170
61
  }
@@ -186,11 +77,11 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
186
77
  // before the design review passed. Signaled by a live design slot (design-review
187
78
  // approval — persists in the session slot, survives across turns; _engDesignReviewed
188
79
  // is eng-coder-only and reset per run). Exemptions = the non-code classes of
189
- // src/conventions.mjs (documentation and temp scratch files) — writing a design
80
+ // thincoder-core/conventions.mjs (documentation and temp scratch files) — writing a design
190
81
  // document IS the design step. Anything inside a declared code segment (default:
191
82
  // src — incl. src/prompts/*.md) is product code, not documentation, and needs a
192
- // design token. The project can declare its own code paths (.thincoder/
193
- // conventions.json) so a non-src layout is not silently exempted. Mechanically
83
+ // design token. The project can declare its own code paths (the manifest's
84
+ // `codePaths`) so a non-src layout is not silently exempted. Mechanically
194
85
  // blocks "talk then code".
195
86
  // DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):资格判据 = 权威槽"任一活槽存在"
196
87
  // (anyLiveDesignSlot——查内存 Map,miss 回读槽文件——单值镜像 `_engDesignToken`
@@ -198,8 +89,8 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
198
89
  if (agent.config?.agent?.engineering && depth === 0
199
90
  && !anyLiveDesignSlot(agent)
200
91
  && FILE_MUTATORS.has(toolCall.name)) {
201
- const paths = tool.touchedPaths ? tool.touchedPaths(args) : [args.path]
202
- const conv = loadConventions(agent.cwd)
92
+ const paths = toolTouchPaths(tool, args)
93
+ const conv = loadProjectDeclaration(agent.cwd)
203
94
  // Unknown/missing paths (non-string, e.g. no path argument) are treated
204
95
  // as code — cannot tell what they touch, so block conservatively. Known
205
96
  // paths go through the single shared classifier: a declared code segment
@@ -209,7 +100,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
209
100
  // Undeclared project → point at the declaration file (§4.3 降级可见契约).
210
101
  const convNote = conv.declared
211
102
  ? ""
212
- : ` — this path was classified as product code by the default conventions (code paths: ${conv.codePaths.join(", ")}); declare project conventions in .thincoder/conventions.json to adjust.`
103
+ : ` — this path was classified as product code by the default conventions (code paths: ${conv.codePaths.join(", ")}); declare project conventions in PROJECT-MANIFEST.json to adjust.`
213
104
  prepared.push({
214
105
  toolCall, tool, denied: true,
215
106
  reason: "engineering design gate",
@@ -219,20 +110,28 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
219
110
  }
220
111
  }
221
112
 
222
- // E(第 11 批·F17/§14.14 E-3d):D5 冻结窗口写前拦截——设计评审在途(点火 → 结算)期间,
113
+ // E(第 11 批·F17/ADVISOR-GUARDS.md §5 E-3d):D5 冻结窗口写前拦截——设计评审在途(点火 → 结算)期间,
223
114
  // 父侧对被审文件集(声明文档集 + 批次档)的写入会被拒绝:在途写使本轮结算 stale——pass 轮
224
- // = token 直接丢失(实证:第 10 批 id=20 整轮作废)。工具面 = FILE_MUTATORS(与变更记账
225
- // 同集——不记入日志的写面既不判 stale 也不拦);判据与 reviewIsStale 同源、单一权威源 =
115
+ // = token 直接丢失(实证:第 10 批 id=20 整轮作废)。工具面 = FILE_MUTATORS ∪ `file_ops`
116
+ // (E2——冻结腿覆盖面;批次档腿仍 = FILE_MUTATORS:与变更记账同集——不记入日志的写面既不判 stale 也不拦);判据与 reviewIsStale 同源、单一权威源 =
226
117
  // agent/write-gate.mjs 的 freezeWindowConflict(声明文档集腿 = inflightDesignReviewConflict
227
118
  // 同 docAbs / 同 normAbs;批次档腿 = run.batchDoc——M4 合流点,仅扫 running 未取消的设计条目)。
228
119
  // 位置:只读 / autoApprove 短路之前——审批不得绕过冻结;拒绝 = 可见 denied + 逃生门
229
120
  // (先 cancel → 改 → 重发)。
230
- if (FILE_MUTATORS.has(toolCall.name)) {
231
- let touched = []
232
- try { touched = tool.touchedPaths ? tool.touchedPaths(args) : [args.path] } catch { touched = [] }
233
- const absPaths = (touched ?? [])
121
+ if (FILE_MUTATORS.has(toolCall.name) || toolCall.name === "file_ops") {
122
+ // #327:单源谓词(同集合内两处消费共用);#309 批次档写门与 D5 冻结窗同区、共用同一路径集。
123
+ // E2(parity-b1 §2.5——缺省取核形):外门扩 `file_ops`(冻结窗覆盖面与端壳对齐——`file_ops`
124
+ // 的 move ∕ copy ∕ rename 亦为写面);**批次档写门判据集不变**(= `FILE_MUTATORS`,file_ops 不入)。
125
+ const absPaths = toolTouchPaths(tool, args)
234
126
  .filter((p) => typeof p === "string" && p)
235
127
  .map((p) => resolve(agent.cwd, p))
128
+ if (FILE_MUTATORS.has(toolCall.name)) {
129
+ const crossBatch = batchRecordWriteConflict(agent, depth, absPaths)
130
+ if (crossBatch) {
131
+ prepared.push({ toolCall, tool, denied: true, reason: "cross-batch record write", hint: crossBatch.message })
132
+ continue
133
+ }
134
+ }
236
135
  const conflict = freezeWindowConflict(agent, absPaths)
237
136
  if (conflict) {
238
137
  prepared.push({
@@ -246,7 +145,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
246
145
 
247
146
  // Readonly tools (and autoApprove — the short-circuit, unchanged for the
248
147
  // whole batch too) skip the permission stage entirely.
249
- // §18 D-E3 task-domain authorization (spawn-time): an eng-coder child's
148
+ // AGENT-LOOP-SUBAGENT.md §6.7.6 D-E3 task-domain authorization (spawn-time): an eng-coder child's
250
149
  // tools skip the permission ASK stage exactly like autoApprove — granted by
251
150
  // the parent spawn (approved design + task = authorization; subagent.mjs
252
151
  // sets _engTaskAuthorized on the child). Everything EARLIER in Phase 1
@@ -254,7 +153,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
254
153
  // — the exemption never widens what reaches this stage (round4 #3, T-E14).
255
154
  // PreToolUse hooks still run below. Non-eng-coder children keep the manual
256
155
  // parent ask (human in the loop).
257
- if (tool.readonly || isSubagentReadonlyAction(toolCall.name, args) || isSubagentControlAction(toolCall.name, args) || isSubagentConsumeDesignAction(toolCall.name, args) || agent.autoApprove || agent._engTaskAuthorized) {
156
+ if (tool.readonly || readonlyActionOf(tool, toolCall.name, args) || controlActionOf(tool, toolCall.name, args) || isSubagentConsumeDesignAction(toolCall.name, args) || agent.autoApprove || agent._engTaskAuthorized) {
258
157
  if (!(await runHooks("PreToolUse", { agent, toolName: toolCall.name, toolArgs: args }))) {
259
158
  prepared.push({ toolCall, tool, denied: true, reason: "blocked by PreToolUse hook" })
260
159
  continue
@@ -267,7 +166,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
267
166
  permPending.push({ toolCall, tool, args })
268
167
  }
269
168
 
270
- // ---- Permission stage: one merged ask for the whole batch (§16 D-B1) ----
169
+ // ---- Permission stage: one merged ask for the whole batch (D-B1) ----
271
170
  // >1 non-readonly tools in the same toolCalls array → a single
272
171
  // onBatchPermissionRequest({ tools, count }) ask; verdicts:
273
172
  // "approveAll" → batch-scope allowance (autoApprove style, NOT persistent)
@@ -292,7 +191,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
292
191
  else if (batchAllowed === false) allowed = false
293
192
  else if (callbacks.onPermissionRequest) {
294
193
  allowed = await (async () => {
295
- // D2 (AGENT-LOOP.md §7.2): announce the wait BEFORE prompting — the TUI
194
+ // D2 (AGENT-LOOP-SUBAGENT.md §6.7.2): announce the wait BEFORE prompting — the TUI
296
195
  // subagent block header flips to "等待审批" so a waiting child is visibly
297
196
  // different from a stalled one. Depth>0 only (the parent TUI shows its own
298
197
  // permission panel). turn n/max = the child's live turn counters.
@@ -322,161 +221,20 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
322
221
  }
323
222
  }
324
223
 
325
- // ---- Phase 2: order-preserving execution ----
326
- const runOne = async (item) => {
327
- if (item.error) return { ...item, result: `Error: ${item.error}`, ok: false }
328
- if (item.denied) {
329
- const reason = item.reason === "plan mode"
330
- ? "Error: plan mode is active — only read-only tools are allowed. Exit plan mode first."
331
- : item.reason === "engineering design gate"
332
- ? `Error: design review required before any file modification. ${item.hint}`
333
- : item.reason === "d5 freeze window"
334
- ? `Error: ${item.hint}`
335
- : item.reason === "denied by user"
336
- ? "Error: permission denied by user"
337
- : item.reason === "blocked by PreToolUse hook"
338
- ? "Error: blocked by PreToolUse hook"
339
- : "Error: no permission handler configured — this tool requires user approval but the current context doesn't support interaction (e.g. subagent or non-TUI mode)"
340
- return { ...item, result: reason, ok: false }
341
- }
342
- // 2026-08-31 工具顺手度(用户批准"做吧"):dispatch 拦截工具执行期间的
343
- // console.log/console.error——工具的探查/调试输出(原本只到终端、模型看不到)
344
- // 收集后附在工具结果后回显给模型。bash 工具的输出走子进程回显(onOutput),
345
- // 不走 dispatch console——拦截安全。嵌套 dispatch(subagent)各自拦截/恢复,
346
- // 捕获分离(父恢复原始后子的拦截期间父捕获停止、子恢复后父继续)——正确。
347
- // 声明在 try 之外:catch 块(异常路径)也要访问(报错前的探查输出回显)。
348
- const capturedConsole = []
349
- // LOGGING(LOGGING.md):tool:* 事件——仅真实执行(pre-gate 拦截项在下方早退分支不入事件)。
350
- // 参数值永不落盘(NF-L3——工具事件不记 args);child=子代理 id(agent._logId,spawn 时 stamp)。
351
- const toolT0 = Date.now()
352
- const toolName = item.toolCall.name
353
- logEvent("tool:call", { tool: toolName, child: agent?._logId })
354
- try {
355
- // R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3b / §4.4.4):结构化写工具执行前查冲突与认领
356
- // 命中(软提示——决策⑥ A 不阻止;一次目录 stat——N3 度量);足迹累积(D-L3a——"检测+
357
- // 记录一次完成")与认领登记延后到执行成功(实际写过的文件)。
358
- const isPeerWriteTool = PEER_WRITE_TOOLS.has(toolName)
359
- const peerNote = isPeerWriteTool ? peerCollabNote(agent, item.tool, item.args) : null
360
- // Snapshot for undo before side-effect tools (setupOutputPanel already fired in Phase 1)
361
- if (!item.tool?.readonly && item.args) {
362
- snapshotForUndo(agent, item.toolCall.name, item.args, agent.cwd)
363
- }
364
- // M2 ACP: route fs tools through the client (IDE buffer / diff review).
365
- // toolRouter returns { handled: true, result } to short-circuit execution.
366
- if (callbacks.toolRouter) {
367
- const routed = await callbacks.toolRouter(item.toolCall.name, item.args)
368
- if (routed?.handled) {
369
- const routedOk = !String(routed.result).startsWith("Error:")
370
- const routedResult = peerNote && routedOk ? `${routed.result}\n${peerNote.text}` : routed.result
371
- if (peerNote && routedOk) markClaimNoted(agent, peerNote.keys) // 提示已附加 ⇒ 落去重标记
372
- if (isPeerWriteTool && routedOk) recordPeerWrites(agent, item.tool, item.args)
373
- // §29 fix A:routed 写成功(客户端执行)同样执行期即刻记账(唯一记账点)
374
- if (routedOk && FILE_MUTATORS.has(toolName)) noteExecutedMutation(agent, item.tool, item.args)
375
- callbacks.onToolResult?.(item.toolCall.name, routedResult, item.toolCall.id)
376
- logEvent("tool:done", { tool: toolName, ms: Date.now() - toolT0, head: headText(routedResult, 200), child: agent?._logId })
377
- return { ...item, result: routedResult, ok: true }
378
- }
379
- }
380
- const origConsoleLog = console.log
381
- const origConsoleErr = console.error
382
- console.log = (...a) => capturedConsole.push(a.map(String).join(" "))
383
- console.error = (...a) => capturedConsole.push("[err] " + a.map(String).join(" "))
384
- let rawResult
385
- // ctx 对象提升为变量(§7.2.3):subagent 阻塞 execute 返回前在 ctx 上留
386
- // _subagentKey(relayPrefix 去尾)——runOne 在 execute 返回后读它作 onToolResult
387
- // 第 4 参(普通工具/错误路径无此字段——undefined 兼容既有签名)。每次工具调用
388
- // 独立 ctx——并行同名工具(批并行 runOne)各自带自己的 key,互不串扰。
389
- const toolCtx = {
390
- cwd: agent.cwd,
391
- agent,
392
- depth,
393
- signal,
394
- callbacks,
395
- // §11.2 D-24b: per-call id — the advisor tool marker-keys its launch so
396
- // recordToolResults can split async-ack accounting from sync settles.
397
- _toolCallId: item.toolCall.id,
398
- onOutput: (chunk) => callbacks.onToolOutput?.(item.toolCall.name, chunk, item.toolCall.id),
399
- onQuestion: callbacks.onQuestion,
400
- onPermissionRequest: callbacks.onPermissionRequest,
401
- }
402
- try {
403
- // SUBAGENT-OBSERVE-SEND D1(评审 #1):in-flight 当前工具记账——工具执行期间在
404
- // agent 上留 _inflightTools Set(子代理 observe 从 dispatch 状态读——卡在长工具
405
- // 调用时 history 无新回合、恰需此信号);finally 清除。批并行工具同入 Set(observe
406
- // 如实返回多个在跑工具)。主会话同样记账——无害(无人读)。
407
- const inflight = agent._inflightTools ?? (agent._inflightTools = new Set())
408
- inflight.add(toolName)
409
- try {
410
- rawResult = await item.tool.execute(item.args, toolCtx)
411
- } finally {
412
- inflight.delete(toolName)
413
- }
414
- } finally {
415
- console.log = origConsoleLog
416
- console.error = origConsoleErr
417
- }
418
- if (rawResult === undefined) throw new Error(`Tool "${item.toolCall.name}" returned undefined — all tools must return a string value`)
419
- const raw = String(rawResult)
420
- // 写成功(非 "Error:" 字符串结果)→ 足迹计入本回合集合(flush 在 finalizeAgentTurn)
421
- if (isPeerWriteTool && !raw.startsWith("Error:")) {
422
- recordPeerWrites(agent, item.tool, item.args)
423
- }
424
- // §29 fix A:FILE_MUTATORS 执行成功即刻记账(唯一记账点——取代 record-results 批后
425
- // 段 + agent.mjs 中断分支——不双计)——同批 launch 前的写在 launchSeq 之前落地 →
426
- // async advisor settle 不误判 stale(同批 launch 后写仍保守 stale——T-A2/T-24b9)。
427
- if (FILE_MUTATORS.has(toolName) && !raw.startsWith("Error:")) {
428
- noteExecutedMutation(agent, item.tool, item.args)
429
- }
430
- // Multimodal tools keep the raw result (base64 images ride the multimodal
431
- // channel); everything else offloads oversized text to disk. Flag-driven, not
432
- // name-driven (consult P3, 2026-08-30).
433
- const result = item.tool?.multimodal ? raw : await offloadToolResult(raw, item.toolCall.id)
434
- // 2026-08-31:工具执行期间捕获的 console 输出附在结果后回显(模型视野)
435
- const resultWithConsole = capturedConsole.length > 0
436
- ? `${result}\n[console during ${item.toolCall.name}]\n${capturedConsole.join("\n")}`
437
- : result
438
- // R10 L3:冲突 / 认领软提示附在工具结果末尾(模型可见——不阻止写;提示附加成功才落去重标记)
439
- const notedOk = Boolean(peerNote) && !raw.startsWith("Error:")
440
- if (notedOk) markClaimNoted(agent, peerNote.keys)
441
- const resultForModel = notedOk
442
- ? `${resultWithConsole}\n${peerNote.text}`
443
- : resultWithConsole
444
- callbacks.onToolResult?.(item.toolCall.name, resultForModel, item.toolCall.id, toolCtx._subagentKey)
445
- // PostToolUse hooks: fire-and-forget (result not awaited on hook failure)
446
- runHooks("PostToolUse", { agent, toolName: item.toolCall.name, toolArgs: item.args, result: raw }).catch(() => {})
447
- logEvent("tool:done", { tool: toolName, ms: Date.now() - toolT0, head: headText(resultForModel, 200), child: agent?._logId })
448
- return { ...item, result: resultForModel, ok: true }
449
- } catch (error) {
450
- // Persist to ~/.thincoder/tool-errors/ for post-mortem; only pass message to the model (stack traces confuse LLMs and may leak paths)
451
- logToolError(item.toolCall.name, item.args, error)
452
- // User interrupt (Ctrl+C / Ctrl+I) must propagate, not become a tool error:
453
- // swallowing it here would make the parent keep looping while the user
454
- // asked to stop — worst case with subagents, where the child runs its
455
- // whole turn budget and the interrupt appears to do nothing.
456
- if (signal?.aborted) throw error
457
- // LOGGING(2026-09-03 code review #4):中止先于事件——用户停不落 tool:error
458
- //(vscode execute-tools parity;阻塞子代理 child:error 同款抑制)
459
- logEvent("tool:error", { tool: toolName, ms: Date.now() - toolT0, err: errText(error, 200), child: agent?._logId })
460
- runHooks("PostToolUseFailure", { agent, toolName: item.toolCall.name, toolArgs: item.args, error }).catch(() => {})
461
- // Build contextual error: tool name + key args so the model can reason about what went wrong
462
- const ctxParts = []
463
- if (item.args.path) ctxParts.push(`path=${item.args.path}`)
464
- if (item.args.pattern) ctxParts.push(`pattern=${item.args.pattern}`)
465
- if (item.args.command) ctxParts.push(`cmd=${item.args.command.slice(0, 80)}`)
466
- const ctx = ctxParts.length > 0 ? ` [${ctxParts.join(", ")}]` : ""
467
- // 2026-08-31:异常路径同样回显捕获的 console(工具报错前的探查输出最有价值)
468
- const consolePart = capturedConsole.length > 0
469
- ? `\n[console during ${item.toolCall.name}]\n${capturedConsole.join("\n")}`
470
- : ""
471
- return { ...item, result: `Error: ${error.message}${ctx}${consolePart}`, ok: false }
472
- }
224
+ // ---- B7 3a JIT 缝(相位一末 ∕ 相位二前——原 VSC「派发前」语义;`depth === 0` 门)--------
225
+ // 作用域规则注入:打点 = `prepared` 非拒项 `{tool, args}`(即将执行集);注入源 = 本 run 尾块
226
+ // 读点缓存 `agent._rules.scoped`(核 `prepareRun` 同点写入),去重域 = 会话。子代不注入。
227
+ if (depth === 0) {
228
+ injectScopedRules(agent, agent.history, prepared.filter((p) => p.tool && p.args).map(({ tool, args }) => ({ tool, args })))
473
229
  }
474
230
 
231
+ // ---- Phase 2: order-preserving execution ----
232
+
475
233
  const results = []
476
234
  let batch = []
477
235
  const flush = async () => {
478
236
  if (batch.length === 0) return
479
- results.push(...await Promise.all(batch.map(runOne)))
237
+ results.push(...await Promise.all(batch.map((item) => runPreparedItem(item, agent, depth, signal, callbacks))))
480
238
  batch = []
481
239
  }
482
240
  for (const item of prepared) {
@@ -486,11 +244,11 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
486
244
  if (item.tool && !item.tool.readonly
487
245
  && (!item.tool.parallel || isSubagentEscalateAction(item.tool.name, item.args))) {
488
246
  await flush()
489
- results.push(await runOne(item))
247
+ results.push(await runPreparedItem(item, agent, depth, signal, callbacks))
490
248
  } else {
491
249
  batch.push(item)
492
250
  }
493
251
  }
494
252
  await flush()
495
253
  return results
496
- }
254
+ }
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * 登记册**动态**载入:`agent-tools.mjs` 静态图经 consult/subagent 族可达核 agent 栈
15
15
  * (`../agent/setup.mjs` → `../memory.mjs` → `node:sqlite`)——静态引入会破端壳 W8 契约②
16
- * (`thincoder-vscode/test/engine-floor-guard.test.mjs`:端壳静态链不得到达 `node:sqlite`)。
16
+ * (W8 契约②判据(现载体 = 批件 `docs/batches/2026-09-29-residuals-round2.test.mjs`;单测树重建时回迁端侧单测档):端壳静态链不得到达 `node:sqlite`)。
17
17
  * 核内先例 = 登记册头注「Loaded from agent.mjs via dynamic import to avoid ESM circular
18
18
  * dependencies」(本档先于 `agent/setup.mjs` 的调用点载入,同一语义)。
19
19
  */
@@ -77,7 +77,7 @@ export async function assembleFamilyTools({
77
77
  },
78
78
  } : subagentTool
79
79
 
80
- // §18 D-E3 + ENGINEERING-MODE.md §2.15 D(第 2 批——参数化复用,不并列第二个 IIFE):
80
+ // AGENT-LOOP-SUBAGENT.md §6.7.6 D-E3 + ENGINEERING-MODE.md §2.15 D(第 2 批——参数化复用,不并列第二个 IIFE):
81
81
  // 工程子代理(depth>0;eng-coder = 偏差审计 / eng-designer = 自己勘察)get a restricted
82
82
  // spawn channel — role enum limited to explore, NO async parameter (sync only) and action
83
83
  // pinned to spawn(AGENT-LOOP-SUBAGENT.md §6.7 D-M3 restricted-variant action gate——escalate/check/status are
@@ -132,7 +132,7 @@ export async function assembleFamilyTools({
132
132
  // consult 工具仅在配置时注册(consultModels 空池时注册会让模型调用后吃一个错误回合)——
133
133
  // AGENT-LOOP-SUBAGENT.md §6.7: escalate 已并入常驻 subagent 的 action:"escalate"(无空池注册问题——动作在
134
134
  // 池空时返回既有错误语义,工程模式 fail-closed 在 execute 内拒绝)。
135
- // §25 D-R17a: consult_check 已退役(digest 自动注入是唯一消费通道)——consult 家族
135
+ // CONSULTATION.md §6.2 D-R17a: consult_check 已退役(digest 自动注入是唯一消费通道)——consult 家族
136
136
  // 只剩 2 工具(consult_start/consult_stop——setup 注册点与描述面同步清零)。
137
137
  const consultTools = consultModels.length
138
138
  ? [decorate?.consultStart ?? withPool(consultStartTool), decorate?.consultStop ?? consultStopTool]
@@ -155,12 +155,12 @@ export async function assembleFamilyTools({
155
155
  // the system prompt names verify (system.md) and advisor (discipline.md) — without them an
156
156
  // escalate hit "unknown tool" and fell back to bash node --check / npm test to
157
157
  // self-verify (2026-08-16 deepseek escalate diagnosis; plugin parity).
158
- // eng-coder: advisor + verify + the §18 audit-only subagent channel (D-E3).
158
+ // eng-coder: advisor + verify + the AGENT-LOOP-SUBAGENT.md §6.7.6 audit-only subagent channel (D-E3).
159
159
  // eng-designer (§2.15 D): the survey-only subagent channel alone — no advisor
160
160
  // (it does not fire reviews) and no verify (its deliverable is documents, not code).
161
161
  // BATCH-RECORD §4.3 挂载表(批次档生命周期工具化批):eng 两分支各挂主名 `batch`
162
162
  // (绑定段 append/status;目标 = spawn 绑定 `child._batchDoc`)——BATCH-RECORD.md §4.2 的 batch_segment
163
- // 挂载形态已随单名化收口退役(过渡别名 §4.14 不入生产挂载面);depth-0 主 agent 同表挂载
163
+ // 挂载形态已随单名化收口退役;depth-0 主 agent 同表挂载
164
164
  // `batch`(D-BR18 扩权——create/close + append §1/§4/§6 + status §1(轮 2 裁定②:§4/§6
165
165
  // 状态面走普通文档写),目标走可选 path / 在飞扫描)。
166
166
  // SUBAGENT-UPSTREAM-CHANNEL(AGENT-LOOP-UPSTREAM.md §6.27.4 装配接线):子代理上行通道
package/agent/helpers.mjs CHANGED
@@ -85,6 +85,38 @@ const MAX_GIT_CHANGES_DISPLAY = 20
85
85
  export const OUTLINE_INJECT_PREFIX = "[System reminder: project dependency outline:"
86
86
  export const FILE_MUTATORS = new Set(["write", "edit", "insert_after", "apply_patch", "delete", "hashline_edit"])
87
87
 
88
+ /**
89
+ * 工具自报触达面提取**单源谓词**(`docs/core/design/TOOLS.md` §6.17 / D-TO12——批 GUARD-SCHEDULER ·
90
+ * 台账 #327):与 `FILE_MUTATORS` 同址(门禁 / 变更记账 / L3 足迹 / 作用域规则四条面的公共输入)。
91
+ * 契约(恒数组 · 恒零抛):
92
+ * - `args` 为 null **或非对象** ⇒ 一律规范化为 `{}` 再交钩子;
93
+ * - 有钩子 ⇒ **钩子裁决**;无钩子 ⇒ `file_ops` **动作感知**(D-TO13 · 台账 #333)——`action === "copy"`
94
+ * ⇒ `[args.dest]`(源仅读取,不属写域);`move` / `rename` / 未知或缺失 `action` ⇒ `[args.source,
95
+ * args.dest]`(保守双算);其余工具单参兜底(形态零变——缺 path 同样返含 `undefined` 的单元素
96
+ * 数组,由消费方按「未知路径 ⇒ 保守」自行判定);
97
+ * - 钩子 throw / 返回非数组 ⇒ `[]`;
98
+ * - **不过滤非字符串项**(未知路径的保守判据归门禁自身——`thincoder-cli/test/portability-classification.test.mjs`
99
+ * T-22 语义零变)。
100
+ * @param {Object|undefined} tool — 工具对象(可选钩子 `touchedPaths(args)`)
101
+ * @param {unknown} args — 工具入参(畸形入参不抛)
102
+ * @returns {unknown[]} 触达路径候选(可能含非字符串项——消费方过滤)
103
+ */
104
+ export function toolTouchPaths(tool, args) {
105
+ const a = args && typeof args === "object" ? args : {}
106
+ const hook = tool?.touchedPaths
107
+ if (typeof hook !== "function") {
108
+ if (tool?.name === "file_ops") return a.action === "copy" ? [a.dest] : [a.source, a.dest]
109
+ return [a.path]
110
+ }
111
+ try {
112
+ const out = hook(a)
113
+ return Array.isArray(out) ? out : []
114
+ } catch {
115
+ return []
116
+ }
117
+ }
118
+
119
+
88
120
  /** Escape XML special characters in a string for safe embedding in XML/HTML */
89
121
  export function escapeXml(s) {
90
122
  return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;")
@@ -342,9 +374,18 @@ export function readonlyToolNames(tools) {
342
374
  * the repo: the shell-side depth>0 filter consumes this set (design = TOOLS.md §6.16 / D-TO11). */
343
375
  export const SUBAGENT_TOOL_EXCLUSIONS = new Set(["question"])
344
376
 
345
- /** Apply the depth>0 exclusion set — always returns a NEW array (parent table untouched). */
377
+ /** 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`
379
+ * 先例;返回值仍恒新数组、未命中条目原对象直传)。运行期第二道在 `tools/bash.mjs` execute
380
+ * (照 `read_image`「schema 门 + 运行期第二道」双线先例)。 */
346
381
  export function excludeSubagentTools(tools) {
347
- return tools.filter((t) => !SUBAGENT_TOOL_EXCLUSIONS.has(t.name))
382
+ return tools
383
+ .filter((t) => !SUBAGENT_TOOL_EXCLUSIONS.has(t.name))
384
+ .map((t) => {
385
+ if (t.name !== "bash" || !t.parameters?.properties || !("async" in t.parameters.properties)) return t
386
+ const { async: _asyncDrop, ...restProps } = t.parameters.properties
387
+ return { ...t, parameters: { ...t.parameters, properties: restProps } }
388
+ })
348
389
  }
349
390
 
350
391
  const MAX_INSTRUCTION_CHARS = 32_000
@@ -384,7 +425,7 @@ export async function loadProjectInstructions(cwd) {
384
425
 
385
426
  /** Engineering mode reminder — shared with eng.mjs tool. */
386
427
  export const ENG_ON_REMINDER =
387
- "[System reminder: engineering mode is ON — design-before-code enforced. " +
428
+ "[System reminder: engineering mode is ON (session-scoped) — design-before-code enforced. " +
388
429
  "Workflow: Requirements doc → Design doc → advisor(type='design') → " +
389
430
  "user approval → eng-coder implementation. Code changes go through eng-coder " +
390
431
  "subagents only. Advisor calls are NOT per-turn-mandatory — call only at " +
@@ -418,6 +459,15 @@ export const AUTO_TURN_DIGEST_DOMAIN_ENG =
418
459
  export const UPSTREAM_TURN_DOMAIN =
419
460
  "[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.]"
420
461
 
462
+ /** Timer wake-turn domain (AGENT-LOOP-ASYNC-POOL.md §6.30.3 D-TW4): a pending timer
463
+ * expired while the session was idle and the end's latch opened this turn automatically —
464
+ * neither the digest domain ("no one is waiting", organize-only) nor the up-stream domain
465
+ * (a subagent waiting for a reply) fits: the timer's own semantics is "act on what the
466
+ * timer was for". Verbatim from the parent-side final text (single line — no newlines).
467
+ * Content authority = parent side. */
468
+ export const TIMER_TURN_DOMAIN =
469
+ "[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.]"
470
+
421
471
  /** Engineering-mode status injection — one reminder on EVERY transition (2026-08-25:
422
472
  * OFF is announced too — the model must know the gates lifted; silence after /eng-off
423
473
  * left it guessing. Covers TUI /eng, resume, and any path bypassing the eng tool.) */