@thincoder/core 0.9.4 → 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 (221) hide show
  1. package/CHANGELOG.md +63 -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 +14 -14
  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 +23 -13
  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 +55 -294
  20. package/agent/family-tools.mjs +11 -11
  21. package/agent/helpers.mjs +63 -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 +19 -15
  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 +90 -30
  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 +11 -11
  36. package/agent-tools/advisor-settle.mjs +17 -17
  37. package/agent-tools/advisor.mjs +38 -43
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +23 -20
  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 +65 -34
  43. package/agent-tools/batch-paths.mjs +186 -0
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +22 -119
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +58 -58
  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 +18 -13
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +3 -3
  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 +29 -273
  66. package/agent-tools/subagent-async.mjs +36 -31
  67. package/agent-tools/subagent-panel.mjs +38 -9
  68. package/agent-tools/subagent-run.mjs +44 -29
  69. package/agent-tools/subagent-scheduler.mjs +29 -28
  70. package/agent-tools/subagent-spawn.mjs +57 -25
  71. package/agent-tools/subagent.mjs +46 -66
  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 +20 -351
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +12 -7
  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/git/checkpoint.mjs +4 -8
  95. package/history-window.mjs +17 -2
  96. package/i18n.mjs +2 -0
  97. package/index-discover.mjs +7 -7
  98. package/ledger-cmd.mjs +34 -126
  99. package/ledger-db.mjs +35 -7
  100. package/ledger-migrate.mjs +299 -0
  101. package/ledger-tools.mjs +188 -0
  102. package/ledger.mjs +11 -5
  103. package/log.mjs +12 -7
  104. package/manifest-discovery.mjs +131 -0
  105. package/manifest-schema.mjs +166 -0
  106. package/manifest.mjs +18 -217
  107. package/mcp.mjs +4 -1
  108. package/memory/code-search.mjs +149 -0
  109. package/memory/code-sync.mjs +89 -230
  110. package/memory/core.mjs +47 -32
  111. package/memory/docs.mjs +63 -262
  112. package/memory/file-list.mjs +89 -0
  113. package/memory/file-walk.mjs +7 -4
  114. package/memory/memory-tool.mjs +229 -0
  115. package/memory/schema.mjs +18 -3
  116. package/memory/sweep.mjs +289 -0
  117. package/memory/sync-tail.mjs +81 -0
  118. package/memory-status.mjs +69 -0
  119. package/memory.mjs +3 -0
  120. package/model-specs.mjs +131 -66
  121. package/notify-policy.mjs +51 -0
  122. package/package.json +1 -1
  123. package/peer-claims.mjs +263 -0
  124. package/peer-domains.mjs +100 -71
  125. package/peer-instances.mjs +9 -10
  126. package/process-probe-exec.mjs +187 -0
  127. package/process-probe.mjs +27 -175
  128. package/prompt-overlays.mjs +11 -10
  129. package/prompts/advisor-design.md +2 -2
  130. package/prompts/advisor-round2.md +1 -1
  131. package/prompts/advisor-round3.md +1 -1
  132. package/prompts/common.md +19 -7
  133. package/prompts/discipline-engineering.md +57 -16
  134. package/prompts/discipline-normal.md +6 -3
  135. package/prompts/persona-coder.md +0 -1
  136. package/prompts/persona-eng-coder.md +1 -5
  137. package/prompts/persona-eng-designer.md +1 -2
  138. package/prompts/persona-engineering.md +41 -11
  139. package/prompts/persona-explore.md +0 -1
  140. package/prompts/persona-normal.md +10 -1
  141. package/prompts/persona-plan.md +3 -4
  142. package/prompts/subagent-base.md +14 -0
  143. package/provider/core.mjs +11 -48
  144. package/provider/list-models.mjs +3 -0
  145. package/provider/responses-request.mjs +237 -0
  146. package/provider/responses.mjs +6 -228
  147. package/provider-flows.mjs +248 -0
  148. package/queued.mjs +90 -0
  149. package/rules.mjs +209 -2
  150. package/session-gc.mjs +67 -6
  151. package/session-guard.mjs +27 -7
  152. package/session-lifecycle.mjs +90 -18
  153. package/session-rename.mjs +5 -1
  154. package/session-slot-claims.mjs +230 -0
  155. package/session-slot-scan.mjs +299 -0
  156. package/session-slot-verify.mjs +304 -0
  157. package/session-slot-write.mjs +74 -8
  158. package/session-slots-manifest.mjs +163 -236
  159. package/session-slots.mjs +99 -71
  160. package/session.mjs +30 -10
  161. package/shell-candidates.mjs +78 -0
  162. package/text-budget.mjs +2 -2
  163. package/think-off.mjs +49 -0
  164. package/token-ttl.mjs +15 -4
  165. package/tool-args.mjs +73 -0
  166. package/tool-docs/advisor.md +9 -0
  167. package/tool-docs/batch.md +1 -0
  168. package/tool-docs/code_search.md +1 -0
  169. package/tool-docs/consult_start.md +5 -0
  170. package/tool-docs/consult_stop.md +3 -0
  171. package/tool-docs/context.md +7 -0
  172. package/tool-docs/doc_search.md +1 -0
  173. package/tool-docs/edit.md +2 -13
  174. package/tool-docs/eng.md +1 -0
  175. package/tool-docs/execute.md +1 -9
  176. package/tool-docs/git.md +25 -51
  177. package/tool-docs/goal.md +6 -0
  178. package/tool-docs/ledger_add.md +1 -0
  179. package/tool-docs/ledger_close.md +1 -0
  180. package/tool-docs/ledger_count.md +1 -0
  181. package/tool-docs/ledger_query.md +1 -0
  182. package/tool-docs/ledger_update.md +1 -0
  183. package/tool-docs/main_history.md +1 -0
  184. package/tool-docs/memory.md +10 -0
  185. package/tool-docs/notify_parent.md +9 -0
  186. package/tool-docs/peer_instances.md +1 -0
  187. package/tool-docs/plan.md +5 -0
  188. package/tool-docs/process.md +7 -3
  189. package/tool-docs/question.md +1 -0
  190. package/tool-docs/read_history.md +7 -0
  191. package/tool-docs/recent_changes.md +1 -0
  192. package/tool-docs/repo_outline.md +1 -0
  193. package/tool-docs/settings.md +6 -0
  194. package/tool-docs/skill.md +5 -0
  195. package/tool-docs/subagent.md +31 -0
  196. package/tool-docs/task.md +7 -0
  197. package/tool-docs/timer.md +6 -0
  198. package/tool-docs/verify.md +1 -0
  199. package/tool-docs/wait_for.md +2 -1
  200. package/tools/bash.mjs +28 -4
  201. package/tools/edit-batch.mjs +10 -11
  202. package/tools/edit-diff.mjs +33 -25
  203. package/tools/exec-run.mjs +102 -4
  204. package/tools/execute.mjs +5 -2
  205. package/tools/file.mjs +11 -12
  206. package/tools/git-run.mjs +1 -1
  207. package/tools/git.mjs +0 -24
  208. package/tools/glob-dialect.mjs +3 -3
  209. package/tools/index.mjs +2 -1
  210. package/tools/linter.mjs +2 -1
  211. package/tools/node-child.mjs +21 -0
  212. package/tools/ops.mjs +50 -13
  213. package/tools/patch.mjs +4 -2
  214. package/tools/repomap.mjs +20 -9
  215. package/tools/search.mjs +2 -2
  216. package/tools/shared.mjs +9 -7
  217. package/tools/web.mjs +5 -2
  218. package/tools/write-path.mjs +1 -1
  219. package/traces/trace-store.mjs +17 -14
  220. package/vision-reader.mjs +76 -0
  221. package/agent-tools/batch-segment.mjs +0 -18
@@ -0,0 +1,244 @@
1
+ /**
2
+ * agent/turn-loop.mjs — runAgent 回合环(2026-09-29 P2 三拆:agent.mjs runAgent 段 2 ∕ 3 ∕ 5 ∕ 6
3
+ * verbatim 迁入——循环局部 ∕ `_ctxBasis` ∕ drainChildUpstream ∕ for 循环本体 ∕ 零落盘结账 +
4
+ * ContinueError;模型调用段经 callModelTurn 外提 agent/chat-call.mjs)。
5
+ * 接口 = runTurnLoop(agent, ctx);控制流零转换(`continue` ∕ `return cr.content` ∕ `throw` 全留环内)。
6
+ */
7
+ import { resolve } from "node:path"
8
+ import { pushReal, summarizeRunExplorations } from "../context.mjs"
9
+ import { abortError, annotateAbort } from "../abort-provenance.mjs"
10
+ import { specForModel, assistantToolCallMessage } from "../config.mjs"
11
+ import { executeToolCalls } from "./dispatch.mjs"
12
+ import { recordToolResults } from "./record-results.mjs"
13
+ import { injectPostTurn } from "./post-turn.mjs"
14
+ import { handleCompletion } from "./completion.mjs"
15
+ import { runCompactionCheck, injectTurnReminders, injectResponseReminders } from "./run-stages.mjs"
16
+ import { callModelTurn } from "./chat-call.mjs"
17
+ import {
18
+ turnFrame, // 第 19 批(TURN-ACROSS-SEGMENTS):跨段累计编号帧(设计 TURN-CAP-CONTINUE.md §4)
19
+ ContinueError,
20
+ FILE_MUTATORS, toolTouchPaths,
21
+ } from "./helpers.mjs"
22
+
23
+ export async function runTurnLoop(agent, {
24
+ maxTurns, threshold, toolSchemas, toolByName, systemPrompt,
25
+ depth, signal, autoTurn, streamOutput,
26
+ consumeInjected, consumeQueuedInput, callbacks, distillSignal,
27
+ }) {
28
+ let guardPushbacks = 0
29
+ let advisorPushbacks = 0
30
+ let honestReminderInjected = false
31
+ const recentCallSigs = []
32
+ // "once" stream rules fire at most once per runAgent call; the set survives across
33
+ // chat() calls (rule abort-retry, tool loop) within the turn.
34
+ const streamRuleFired = new Set()
35
+
36
+ // Compaction overhead for the pure-estimation path: system prompt + tools schema are
37
+ // in every request but not in history — without them the first-turn/just-compacted
38
+ // estimate under-counts and may never trigger. Measured path already includes both.
39
+ const compactionOverhead = {
40
+ systemPrompt,
41
+ tools: toolSchemas,
42
+ // TRACES.md §6.1 D-TR4:compress 轨迹 depth 元数据(runAgent 的 depth 在此作用域——
43
+ // context.mjs compressIfNeeded 经 extras 透出到 logCtx)
44
+ traceDepth: depth,
45
+ }
46
+ // F-CC1(§6.16.4 阈值单源接线):本回合压缩判定所用的阈值 + 固定开销面暂存——
47
+ // `context` 工具的 stats 报**同一口径**(不自行重算第二口径;VSC checkAndCompact 同款暂存)。
48
+ agent._ctxBasis = { threshold, overhead: compactionOverhead }
49
+
50
+ // SUBAGENT-UPSTREAM-CHANNEL(AGENT-LOOP-UPSTREAM.md §6.27.4 消费点):子 → 父在飞消息的
51
+ // 回合边界注入单点取用一次(模块缓存 ⇒ 每 run 一次代价);动态 import = 零新增静态边
52
+ // (先例 = 上方 injectAsyncResult :113-117)。
53
+ const { drainChildUpstream } = await import("../agent-tools/parent-channel.mjs")
54
+
55
+ // #417(撞帽载荷「本段零落盘轮数」——只报数:零阈值 / 零自动动作):计数基线——回合环逐轮
56
+ // 比对 `_touchedFiles` 增量(轮顶对上一轮结账;撞帽收尾轮在环外单结)。
57
+ agent._turnFilesMark = agent._touchedFiles.length
58
+
59
+ for (let turn = 0; turn < maxTurns; turn++) {
60
+ // 第 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §4):编号帧——
61
+ // `_turnSeq` 每轮 +1(跨段累计,仅 `!resume` 链起点复位);面向消费面的两字段
62
+ // (状态行 + ⟦ev⟧turn / ⟦ev⟧approval 事件共用)在此同点赋值(编号唯一权威;帧
63
+ // 公式 = helpers.mjs turnFrame)。段内帽判定不读帧(下行循环条件只读段内
64
+ // turn / maxTurns——N6)。
65
+ const frame = turnFrame(++agent._turnSeq, turn, maxTurns)
66
+ agent._currentTurn = frame.turn
67
+ agent._maxTurns = frame.maxTurns
68
+ // #417(撞帽载荷「本段零落盘轮数」——采集点与 `_turnSeq` 同源面):上一轮至今 `_touchedFiles`
69
+ // 零增 ⇒ 上一轮零落盘,计数 +1;mark 前移(收尾轮在环外结账——见下方 ContinueError 前)。
70
+ if (turn > 0 && agent._touchedFiles.length === agent._turnFilesMark) agent._zeroWriteTurns += 1
71
+ agent._turnFilesMark = agent._touchedFiles.length
72
+ // D2 (AGENT-LOOP-SUBAGENT.md §6.7.2): depth>0 children emit a ⟦ev⟧turn progress token each turn —
73
+ // single emit point covering all three spawn tools; phase=llm (tool/done progress rides
74
+ // the onToolCall/onToolResult relay — no token for those). 第 19 批:载荷取上方帧值
75
+ // (agent._currentTurn / _maxTurns——字段形态 / 字段数 / phase 零变化,N5)。
76
+ if (depth > 0 && callbacks.onToken) {
77
+ callbacks.onToken(`⟦ev⟧turn\x1e${agent._currentTurn}\x1e${agent._maxTurns}\x1ellm\x1e`)
78
+ }
79
+ // §2.5 #78 并入(VSC 帧回调):每轮帧另经结构化回调发出(池条目 entry.turn 的
80
+ // VSC 等价通道——⟦ev⟧turn token 解析的宿主面随时可用;签名扩展向后兼容)。
81
+ callbacks.onAgentTurn?.(frame.turn, frame.maxTurns)
82
+
83
+ // SUBAGENT-OBSERVE-SEND D2: 子代理回合边界消费点——每轮开头把父侧经 subagent
84
+ // action:'send' 注入队列(entry._injected)的消息按普通 user 回合推入子历史
85
+ // (pushReal → 下一轮 chat 即含该指令)。由 executeAsyncSpawn 经 childRunOpts 贯通的
86
+ // consumeInjected 回调承载(异步子代理专属——缺省 null:主会话/阻塞子代理零开销)。
87
+ consumeInjected?.(agent)
88
+ // 子代理在飞消息(子 → 父;§6.27):空队列 no-op;非空 ⇒ 恰一条合并 user 消息注入。
89
+ drainChildUpstream(agent)
90
+ // ③ 用户 → 主会话投递(步边界 pickup——queue-visible 批 fix 轮 2026-09-24 · F16):循环头
91
+ // 回合边界缝(核只给缝、不给策略——取批 / pushReal / 呈现归端侧闭包);缺省 null,系统轮不传
92
+ // (`AGENT-LOOP-ASYNC-POOL.md` §6.8「步边界 pickup」· 适用面 = 用户回合)。
93
+ consumeQueuedInput?.(agent)
94
+
95
+ const lastRole = agent.history.at(-1)?.role
96
+ if (lastRole === "user" || lastRole === "tool") {
97
+ // 2026-09-05 实践轮:压缩检查/降级计数提为 runCompactionCheck(agent/run-stages.mjs——
98
+ // CLI 对位 VS run-stages)——循环骨架此处只剩检查调用(recentCallSigs 对象引用回流)。
99
+ await runCompactionCheck(agent, { threshold, callbacks, compactionOverhead, signal, recentCallSigs })
100
+ }
101
+
102
+ // 2026-09-05 实践轮:plan cadence + eng 状态注入提为 injectTurnReminders(run-stages)
103
+ await injectTurnReminders(agent, { depth })
104
+
105
+ const response = await callModelTurn(agent, {
106
+ systemPrompt, toolSchemas, streamOutput, callbacks, signal, streamRuleFired, autoTurn, depth, turn,
107
+ })
108
+
109
+ // 内置工具(Responses web_search)结果本地化:服务端已执行——入历史为 tool 消息;
110
+ // 服务端 item id 是 msg_xxx 非 web_search_call_ 前缀——必须合成前缀(toItems 识别锚点),
111
+ // 原始 id 存入 content(真机冒烟 2026-08-31 验证)。
112
+ for (const btr of response.builtinToolResults ?? []) {
113
+ if (!btr?.id) continue
114
+ pushReal(agent, {
115
+ role: "tool",
116
+ tool_call_id: `web_search_call_${btr.id}`,
117
+ content: JSON.stringify({ id: btr.id, query: btr.query ?? "", sources: btr.sources ?? [], status: btr.status ?? "completed" }),
118
+ })
119
+ }
120
+
121
+ // Stream rule triggered mid-generation (action: "abort"): halt, inject the rule's
122
+ // message as a reminder, retry from the same context.
123
+ if (response.ruleTriggered) {
124
+ if (response.content) {
125
+ pushReal(agent, { role: "assistant", content: response.content })
126
+ }
127
+ const label = response.ruleName ? ` — stream rule "${response.ruleName}"` : ""
128
+ agent.history.push({
129
+ role: "user",
130
+ content: `[System reminder${label}: ${response.ruleMessage}]`,
131
+ })
132
+ continue
133
+ }
134
+
135
+ // Stream rule warnings / finish-reason 警告(2026-09-05 实践轮——提为
136
+ // injectResponseReminders,agent/run-stages.mjs——verbatim,语义零变)
137
+ injectResponseReminders(agent, response)
138
+
139
+ // User interrupted mid-generation (Ctrl+I): commit partial output + inject the
140
+ // message, then signal the outer loop to recreate the controller and resume.
141
+ if (response.interrupted) {
142
+ if (response.content) {
143
+ pushReal(agent, { role: "assistant", content: response.content })
144
+ }
145
+ agent.history.push({
146
+ role: "user",
147
+ content: `[User interrupt: ${response.interruptMessage}]`,
148
+ })
149
+ // AGENT-LOOP-SUBAGENT.md §6.12 站点 #8(第 24 批):错误对象已自带 name/message——只补来源标注(缺 abortInfo 才补)
150
+ throw annotateAbort(Object.assign(new Error("User interrupted"), { name: "AbortError" }), signal, "agent", "interrupted-response")
151
+ }
152
+
153
+ if (response.usage) {
154
+ callbacks.onUsage?.(response.usage)
155
+ if (response.usage.prompt_tokens != null) {
156
+ agent._lastPromptTokens = response.usage.prompt_tokens
157
+ agent._usageAtLen = agent.history.length
158
+ }
159
+ }
160
+
161
+ // Warn on abnormal finish reasons — the response may be incomplete/truncated.
162
+ // 2026-09-05 实践轮:finish-reason 注入随流规则警告提为 injectResponseReminders。
163
+
164
+ if (response.toolCalls.length === 0) {
165
+ const cr = handleCompletion(agent, response, depth, turn, guardPushbacks, honestReminderInjected, advisorPushbacks, callbacks)
166
+ guardPushbacks = cr.guardPushbacks
167
+ honestReminderInjected = cr.honestReminderInjected
168
+ advisorPushbacks = cr.advisorPushbacks
169
+ if (cr.action === "continue") continue
170
+ if (depth === 0) {
171
+ // End-of-run exploration distillation (CONTEXT-COMPACTION §5 + SEND-STALL-DISTILL
172
+ // §2.1): async — the promise hangs on _pendingDistill, settling at the next run's
173
+ // start or the TUI exit flush. Silent (N3): failure never blocks return/history.
174
+ // TRACES.md §6.1 D-TR4:depth 透传(distill 轨迹元数据——与 compress 同通道);extras = 会话续写前缀面(§6.15)
175
+ // §2.5-B(P4-I):端壳经 `opts.distillSignal` 供蒸馏专用中止信号(与运行 signal 分离——
176
+ // 运行 controller 重建不杀蒸馏);缺省(null)⇒ `signal` 逐字零变。
177
+ const distill = summarizeRunExplorations(agent, callbacks, distillSignal ?? signal, depth,
178
+ { systemPrompt, tools: toolSchemas }).catch(() => {}) // 与回合请求同源(无第二构造点)
179
+ agent._pendingDistill = distill
180
+ }
181
+ return cr.content
182
+ }
183
+
184
+ // abort after chat completes, before committing history: don't commit a half-finished turn
185
+ if (signal?.aborted) throw abortError(signal, "agent", "post-chat")
186
+
187
+ pushReal(agent, assistantToolCallMessage(response, specForModel(agent.provider.model)))
188
+
189
+ const results = await executeToolCalls(agent, toolByName, response.toolCalls, callbacks, depth, signal)
190
+
191
+ // Ctrl+I interrupt during tool execution: skip committing partial results — inject
192
+ // the interrupt and retry (placeholder results keep strict providers pairable).
193
+ if (signal?.reason?.interrupt) {
194
+ // 中断变更记账(2026-08-31 评审 #4):此分支的工具已全部执行完成(磁盘已变,execute 已完成),
195
+ // 真实结果按语义不进历史(placeholder 替代)——但变更必须记账:否则 guard 看到
196
+ // "本轮未改代码" 放行,评审/verify 门禁被绕过(文件改了却没评审)。
197
+ // §29 fix A(2026-09-07):mutation-seq 记账已收敛到 dispatch runOne 执行成功即刻
198
+ // (唯一记账点——本分支不再 noteMutations——不双计——中断+同批 launch seq 单计
199
+ // 回归断言见 §29 T-A1i);此处仅剩 guard 标志 + touchedFiles 记账。
200
+ for (const { toolCall, ok } of results) {
201
+ const tool = toolByName.get(toolCall.name)
202
+ if (!ok || !tool || !FILE_MUTATORS.has(toolCall.name)) continue
203
+ agent._mutatedThisRun = true
204
+ agent._calledAdvisorThisRun = false
205
+ agent._verifiedThisRun = false
206
+ agent._verifyPassed = undefined
207
+ try {
208
+ const args = JSON.parse(toolCall.arguments)
209
+ const paths = toolTouchPaths(tool, args)
210
+ for (const p of paths) {
211
+ const abs = resolve(agent.cwd, p)
212
+ if (!agent._touchedFiles.includes(abs)) agent._touchedFiles.push(abs)
213
+ }
214
+ } catch { /* 畸形 args 不影响记账(touchedFiles 尽力而为) */ }
215
+ }
216
+ // The assistant tool_calls were committed above — synthesize placeholder tool
217
+ // results BEFORE the interrupt message (strict providers 400 on dangling
218
+ // tool_calls; consult P1, 2026-08-30).
219
+ for (const tc of response.toolCalls) {
220
+ agent.history.push({ role: "tool", tool_call_id: tc.id, content: "[Tool execution interrupted — results discarded]" })
221
+ }
222
+ agent.history.push({
223
+ role: "user",
224
+ content: `[User interrupt: ${signal.reason.message}]`,
225
+ })
226
+ callbacks.onTurnEnd?.(agent, turn)
227
+ continue
228
+ }
229
+
230
+ // Model is executing tools → real work: reset guard pushback counters
231
+ guardPushbacks = 0
232
+ advisorPushbacks = 0
233
+
234
+ // Commit tool results (pairing, multimodal deferral, mutation accounting, reindex)
235
+ await recordToolResults(agent, toolByName, results)
236
+
237
+ injectPostTurn(agent, results, recentCallSigs, callbacks, turn)
238
+ }
239
+
240
+ // #417(撞帽载荷「本段零落盘轮数」):收尾轮(撞帽轮)若零落盘同样计入——轮顶计数只
241
+ // 覆盖到倒数第二轮(maxTurns ≤ 0 时环未跑,不虚计)。
242
+ if (maxTurns > 0 && agent._touchedFiles.length === agent._turnFilesMark) agent._zeroWriteTurns += 1
243
+ throw new ContinueError(maxTurns)
244
+ }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * v2 唯一增量(模块设计 §2.1):评审对象 / 被审文件路径来源改读 manifest `docRoot`
5
5
  * (去硬编码 `docs/`),token 门(`anyLiveDesignSlot`)与冻结窗口谓词本体继承 v1 零改
6
- * (KD-M4-3)——本档只承载两个单点:
6
+ * (KD-M4-3)——本档承载三个单点:
7
7
  *
8
8
  * 1. `resolveReviewTargetPaths(agent)` —— 评审目标解析单点:读 manifest `docRoot`
9
9
  * 产出评审对象 / 被审文件的绝对路径集合(目录级;子键值形态 = 串 | 多根数组,
@@ -15,19 +15,28 @@
15
15
  * KD-M1-12):CLI make-agent.mjs / VSC setup.mjs 同判);读错(权限等)→ 上抛不伪装成缺失(manifest.mjs 契约)。
16
16
  * M6 的 `advisor.mjs` design-review 分类分支消费本导出
17
17
  * (KD-M6-1——同源不重复实现;落 dispatch.mjs 会让 advisor 反向 import 门禁簇成环)。
18
+ * **按用点形(2026-10-02 收口 · 台账 #828)**:`resolveReviewRootsFor(dir)`——同单点
19
+ * 非第二实现(按目标目录 ∥ 所属项目根解析;多档锚下各文档按各自项目判定)。
18
20
  * 2. `freezeWindowConflict(agent, absPaths)` —— D5 冻结窗口判据组装:被审文件集 =
19
21
  * 声明文档集(`inflightDesignReviewConflict` 同源 docAbs 腿)+ 批次档(`run.batchDoc`
20
- * 腿——评审绑定批档在途时,写批档同样致 stale)。dispatch.mjs / VSC tool-gates.mjs
22
+ * 腿——评审绑定批档在途时,写批档同样致 stale)。dispatch.mjs
21
23
  * 只 import 消费,不再各自直连 `advisor-settle.mjs`。
24
+ * 3. `batchRecordWriteConflict(agent, depth, absPaths)` —— 批次档写门判据组装(#309 ·
25
+ * `AGENT-LOOP-SUBAGENT.md` §6.29.1):判据与拒绝文案同址(文案字面只出本档)。
26
+ * 2026-09-29 收窄(台账 #545):自身批次伴随件(非 `.md` · 同目录 · 词干族 = 三合取)放行——
27
+ * 批次档本体 ∕ 他批伴随件 ∕ 异目录伴随件照拒;两端取用 = 核 `agent/dispatch.mjs` Phase 1(两端同径)。
22
28
  *
23
29
  * 继承零改面(本档不碰):token 门资格判据(anyLiveDesignSlot / validateDesignToken)、
24
30
  * 代码路径判定(conventions.mjs 单一权威分类——KD-M4-1 保留为代码路径判定用)。
25
31
  */
32
+ import { basename, dirname, resolve } from "node:path"
26
33
  import { DEFAULT_MANIFEST, docRootPaths, readManifest } from "../manifest.mjs"
27
34
  // advisor-settle 无上游依赖本簇(dispatch → write-gate → advisor-settle 单向)。normAbs
28
35
  // 权威本体 = review-facts.mjs(第 33 批迁入),advisor-settle re-export;本档再 re-export
29
36
  // (指针链非副本),M6 经本档一行 import 取齐。
30
37
  import { inflightDesignReviewConflict, normAbs } from "../agent-tools/advisor-settle.mjs"
38
+ // 批次档路径单源(叶档——`BATCH-RECORD.md` §4.15):批次档写门基底集取 `batchDocBases`。
39
+ import { batchDocBases } from "../agent-tools/batch-paths.mjs"
31
40
  export { normAbs }
32
41
 
33
42
  /** 评审目标解析读的 docRoot 键集(评审对象/被审文件可住的所有文档层——M6 分类据此判定)。 */
@@ -44,14 +53,21 @@ const REVIEW_ROOT_KEYS = ["requirements", "specs", "design", "modules", "batches
44
53
  * @returns {string[]} 评审目标目录的绝对路径(去重)
45
54
  */
46
55
  export function resolveReviewTargetPaths(agent) {
47
- const cwd = agent?.cwd ?? process.cwd()
48
- // 读错上抛(manifest.mjs:readManifest 契约——不伪装成缺失);整档缺失/非法 → {ok:false}。
49
- const man = readManifest(cwd)
56
+ return resolveReviewRootsFor(agent?.cwd ?? process.cwd())
57
+ }
58
+
59
+ /** 同单点之**按用点解析**增量(2026-09-21「参数按用点解析」之裁收口 · 台账 #828):按**目标目录**
60
+ * (其所属项目根——最近带档祖先)解析评审目标根集——多档锚下各文档按各自项目判定。
61
+ * @param {string} dir 目标目录(项目根——最近带档祖先)
62
+ * @returns {string[]} 评审目标目录绝对路径(去重)
63
+ */
64
+ export function resolveReviewRootsFor(dir) {
65
+ const man = readManifest(dir)
50
66
  const root = man.ok && man.manifest?.docRoot && typeof man.manifest.docRoot === "object"
51
67
  ? man.manifest.docRoot
52
68
  : DEFAULT_MANIFEST.docRoot
53
69
  const out = []
54
- for (const key of REVIEW_ROOT_KEYS) out.push(...docRootPaths(root?.[key], cwd))
70
+ for (const key of REVIEW_ROOT_KEYS) out.push(...docRootPaths(root?.[key], dir))
55
71
  return [...new Set(out)]
56
72
  }
57
73
 
@@ -85,3 +101,84 @@ export function freezeWindowConflict(agent, absPaths) {
85
101
  }
86
102
  return null
87
103
  }
104
+
105
+ /** 批次档写门基底集**按 run 记忆化**(每 run 一次 manifest 读——§6.29.1;键 = agent 对象)。
106
+ * 载体 = WeakMap(agent 回收即随 GC——无跨 run 残留);cwd 变更即失效重读。
107
+ * **歧义锚**(多项目并存 · #828)⇒ 基底 = 候选项目基底**并集**(`batchDocBases` 解析单点——覆盖随之对齐)。 */
108
+ const batchGateBasesMemo = new WeakMap()
109
+
110
+ /** 比较键(绝对形态归一:`.` 段归位 + 分隔符归一 `/` + win32 大小写归一——§6.29.1 判据)。 */
111
+ const gateKey = (p) => {
112
+ const s = resolve(String(p)).replace(/\\/g, "/")
113
+ return process.platform === "win32" ? s.toLowerCase() : s
114
+ }
115
+
116
+ function memoizedBatchBases(agent, cwd) {
117
+ const memo = agent && typeof agent === "object" ? batchGateBasesMemo.get(agent) : null
118
+ if (memo && memo.cwd === cwd) return memo.bases
119
+ const bases = batchDocBases(cwd)
120
+ if (agent && typeof agent === "object") batchGateBasesMemo.set(agent, { cwd, bases })
121
+ return bases
122
+ }
123
+
124
+ /** 目标是否落基底根内(按路径段——win32 大小写归一;同 `batch-paths.mjs` 越基底判据口径)。 */
125
+ function underBase(targetKey, baseAbs, cwd) {
126
+ const root = gateKey(normAbs(baseAbs, cwd))
127
+ return targetKey === root || targetKey.startsWith(root.endsWith("/") ? root : root + "/")
128
+ }
129
+
130
+ /** 自身批次伴随件判据(三合取——`AGENT-LOOP-SUBAGENT.md` §6.29.1 · 2026-09-29 收窄 · 台账 #545):
131
+ * ① 目标非 `.md`(批次档本体永在门内——含近词干 `<词干>-v2.md`;`.md` 判定大小写不敏感 = fail-closed 侧);
132
+ * ② 目标与绑定档同目录;③ 目标基名以 `<词干>.` 或 `<词干>-` 打头(词干 = 绑定档基名剥 `.md`)。
133
+ * 入参 = 比较键(gateKey 归一后——win32 大小写归一同覆盖 ②③ 的字面比较)。
134
+ * @returns {boolean} 三合取全中 = true(放行面);余者 false(照拒) */
135
+ function isOwnBatchCompanion(targetKey, boundKey) {
136
+ const targetBase = basename(targetKey)
137
+ if (targetBase.toLowerCase().endsWith(".md")) return false // ① 批次档本体(任何 `.md`)永在门内
138
+ if (dirname(targetKey) !== dirname(boundKey)) return false // ② 同目录(按比较键)
139
+ const boundBase = basename(boundKey)
140
+ const stem = boundBase.toLowerCase().endsWith(".md") ? boundBase.slice(0, -3) : boundBase
141
+ return targetBase.startsWith(stem + ".") || targetBase.startsWith(stem + "-") // ③ 词干族(`.` ∕ `-` 前缀)
142
+ }
143
+
144
+ /**
145
+ * 批次档写门判据(#309 · `docs/core/design/AGENT-LOOP-SUBAGENT.md` §6.29.1——containment;
146
+ * 2026-09-29 收窄 · 台账 #545):
147
+ * 工程角色子代理(`depth > 0` ∧ `agent._batchDoc` 在场)对**批次档文件**的写 ⇒ 目标 ≠ 绑定档
148
+ * **且非绑定批次的伴随件**(见 `isOwnBatchCompanion`)⇒ 冲突(fail-closed;写自己绑定的档 ∥
149
+ * 自身批次伴随件 = 正常面放行)。比较键 = 绝对路径 + win32 大小写归一。
150
+ * 界面:depth 0(主 agent——跨档处置是父侧职责)· 无 `_batchDoc`(非工程绑定族)· 主基底外的
151
+ * 普通文件写 —— 三种形态均不进本门(返回 null,行为零变)。判据集由调用方限定 = `FILE_MUTATORS`
152
+ * (`file_ops` / 读类不在门内——§6.29.1 边界)。
153
+ * 拒绝文案**单源 = 本档**(两端直取 `message`——零字面副本)。
154
+ * 恒零抛(形照 `freezeWindowConflict` 先例——全判据带形态守卫)。
155
+ * @param {Object} agent — 子代理 agent(读 `_batchDoc` / `cwd`)
156
+ * @param {number} depth — 执行深度(> 0 = 子代理)
157
+ * @param {string[]} absPaths — 本次写将触碰的绝对路径集(调用方已 resolve)
158
+ * @returns {{bound: string, target: string, message: string}|null} 命中 = 冲突信息 / 否则 null
159
+ */
160
+ export function batchRecordWriteConflict(agent, depth, absPaths) {
161
+ if (!(depth > 0)) return null
162
+ const bound = agent?._batchDoc
163
+ if (typeof bound !== "string" || bound.length === 0) return null
164
+ const wanted = (absPaths ?? []).filter((p) => typeof p === "string" && p.length > 0)
165
+ if (wanted.length === 0) return null
166
+ const cwd = typeof agent?.cwd === "string" && agent.cwd.length > 0 ? agent.cwd : process.cwd()
167
+ const bases = memoizedBatchBases(agent, cwd)
168
+ const boundAbs = normAbs(bound, cwd)
169
+ const boundKey = gateKey(boundAbs)
170
+ for (const p of wanted) {
171
+ const target = normAbs(p, cwd)
172
+ const targetKey = gateKey(target)
173
+ if (targetKey === boundKey) continue // 写自己绑定的档 = 正常面
174
+ if (!bases.some((b) => underBase(targetKey, b, cwd))) continue // 非批次档文件(含缺 manifest 的基底外盘面)
175
+ if (isOwnBatchCompanion(targetKey, boundKey)) continue // 自身批次伴随件 = 放行面(三合取——见判据)
176
+ return {
177
+ bound: boundAbs,
178
+ target,
179
+ message: `write refused — cross-batch batch-record write: this child is bound to ${basename(boundAbs)}; ${basename(target)} belongs to a different batch. Write only your own bound record and its companion files (the batch tool targets your bound record); the parent agent handles other batch records.`,
180
+ }
181
+ }
182
+ return null
183
+ }
184
+
@@ -4,7 +4,7 @@
4
4
  * The advisor tool runs reviews in a background pool at depth 0 (default async —
5
5
  * ruling ②-3 A): the launch returns an ack, the turn ends naturally, the session
6
6
  * suspends, the review settles into _pendingAsyncResults and a digest turn
7
- * delivers the report (§17 consumption machinery — role-agnostic; entries carry
7
+ * delivers the report (AGENT-LOOP-ASYNC-POOL.md §6.8 consumption machinery — role-agnostic; entries carry
8
8
  * role "advisor" and ride the subagent panel as a pseudo-role, ruling ②-4 A).
9
9
  *
10
10
  * This module owns:
@@ -51,7 +51,7 @@ import { normAbs, mutationSeqOf, settleAdvisorRun } from "./advisor-settle.mjs"
51
51
  export {
52
52
  mutationSeqOf, noteMutations, reviewIsStale, settleAdvisorRun, inflightDesignReviewConflict,
53
53
  } from "./advisor-settle.mjs"
54
- // 第 33 批(§17.5):doc-set 键迁事实面档(实例续跑与陈旧判定同锚单源;原为私有——零 import 面)。
54
+ // 第 33 批(评审失败护栏):doc-set 键迁事实面档(实例续跑与陈旧判定同锚单源;原为私有——零 import 面)。
55
55
  // 本文件继续在实例解析 / 池 entries 上消费它。
56
56
  import { docSetKey } from "./review-facts.mjs"
57
57
  // F31(2026-09-18 顾问面治理批):拒回文案对象标识行单源。
@@ -110,7 +110,7 @@ function openDesignRun(agent, key) {
110
110
  }
111
111
 
112
112
  /**
113
- * Resolve the review instance a launch continues (§11.2 ③ — per-review
113
+ * Resolve the review instance a launch continues (AGENT-LOOP-ASYNC-POOL.md §6.10 ③ — per-review
114
114
  * rounds/prior; reviewId = designId for design reviews, a random id for code).
115
115
  * - design: continue the OPEN instance of the same document set (fix rounds of
116
116
  * a review thread keep its designId and its round/prior); none → new instance.
@@ -172,7 +172,7 @@ export function effectiveAdvisorRound(agent) {
172
172
 
173
173
  export const ADVISOR_POOL_LIMIT = 4
174
174
 
175
- /** §11.2 advisor 池生效上限(纯——读 agent.poolLimits.advisor——合法 ≥1 整数生效——
175
+ /** AGENT-LOOP-ASYNC-POOL.md §6.10 advisor 池生效上限(纯——读 agent.poolLimits.advisor——合法 ≥1 整数生效——
176
176
  * 非法/缺省回退 ADVISOR_POOL_LIMIT——与 subagent 域 resolvePoolLimits 独立不共享
177
177
  * (subagent 两键表不含本键——键表语义不同——POOL-CONFIG-UNIFIED F-2))。 */
178
178
  export function resolveAdvisorPoolLimit(raw) {
@@ -185,7 +185,7 @@ export function resolveAdvisorPoolLimit(raw) {
185
185
  export function advisorPoolLimitFor(agent) {
186
186
  return resolveAdvisorPoolLimit(agent?.config?.agent?.poolLimits)
187
187
  }
188
- /** §11.2 同 scope 守卫(F-5——用户裁①):同 reviewType+scope 有 running 评审 → true
188
+ /** §6.10 同 scope 守卫(F-5——用户裁①):同 reviewType+scope 有 running 评审 → true
189
189
  * (拒——running 并行多实例歧义放大——settled 续跑语义不变)。design scope = 文档集键
190
190
  * (docSetKey);code = 单 code 线程(记录不记路径键——与 openCodeRun 语义一致)。
191
191
  * 与池容量守卫独立:容量 = 全局 ≤N——scope = 同 scope ≤1——两关都过才启动。 */
@@ -257,7 +257,7 @@ export function cancelAsyncAdvisor(agent, id, onToken) {
257
257
  return { id: key, status: "cancelled", was: "queued" }
258
258
  }
259
259
  entry.cancelled = true
260
- // §20.3 站点 #11(第 24 批):定向中止 = cancel(reason 载荷)
260
+ // AGENT-LOOP-SUBAGENT.md §6.12 站点 #11(第 24 批):定向中止 = cancel(reason 载荷)
261
261
  entry.controller?.abort?.({ abortTrigger: "cancel", abortDetail: "advisor-cancel" })
262
262
  return { id: key, status: "cancelled" }
263
263
  }
@@ -352,13 +352,13 @@ export function refillAdvisorQueue(parent, onToken) {
352
352
  * digest/panel/freeze consumers route it like any other settled block.
353
353
  * @returns {{ok: true, id: string}} — ack; or {{ok: true, id: string, queued: true,
354
354
  * position: number}} — ED-4 排队 ack(池满 + 异 scope——slot 释放自动起跑); or
355
- * {{error: string}} — scope guard (same reviewType+scope running — §11.2 F-5)
355
+ * {{error: string}} — scope guard (same reviewType+scope running — §6.10 F-5)
356
356
  * or invalid ctx.
357
357
  */
358
358
  export function launchAsyncAdvisor(parent, ctx, launch) {
359
359
  const { reviewType, documents, paths, object, designToken, designId, run } = launch
360
360
  const limit = advisorPoolLimitFor(parent)
361
- // §11.2 same-scope guard(F-5——用户裁①):同 type+scope 有 running → 拒——与池容量
361
+ // §6.10 same-scope guard(F-5——用户裁①):同 type+scope 有 running → 拒——与池容量
362
362
  // 守卫独立两关都过才启动——同 scope 不等不排(settled 续跑不变;排队仅异 scope 面)。
363
363
  if (runningAdvisorOfScope(parent, reviewType, run?.docSetKey ?? null)) {
364
364
  const scopeNote = reviewType === "design"
@@ -398,7 +398,7 @@ export function launchAsyncAdvisor(parent, ctx, launch) {
398
398
  // D6 buildChildSignal 单点(ASYNC-RESULT-CONTAINER.md)。
399
399
  const ctrl = new AbortController()
400
400
  entry.controller = ctrl
401
- // §20.3 站点 #10(第 24 批):hop 逐跳保 reason;#98 链结单点(interrupt 豁免面——
401
+ // §6.12 站点 #10(第 24 批):hop 逐跳保 reason;#98 链结单点(interrupt 豁免面——
402
402
  // Ctrl+I 不逐链中止飞行评审)。
403
403
  bindChildController(ctrl, buildChildSignal(parent, ctx))
404
404
  entry.promise = new Promise((res) => { entry._settle = res })
@@ -416,7 +416,7 @@ export function launchAsyncAdvisor(parent, ctx, launch) {
416
416
  signal: entry.controller.signal,
417
417
  }, designToken, documents, paths, object, designId)
418
418
  .then((report) => { entry.report = report })
419
- // §20.3 第 3 条合成器(第 24 批):原 message 前缀逐字保留 + 来源后缀
419
+ // §6.12 第 3 条合成器(第 24 批):原 message 前缀逐字保留 + 来源后缀
420
420
  .catch((err) => { entry.error = deathLine(err, entry.controller?.signal) })
421
421
  .finally(() => {
422
422
  // settle 公共收尾单点(ASYNC-RESULT-CONTAINER.md D3——settleAsyncEntry):日志三连
@@ -430,7 +430,7 @@ export function launchAsyncAdvisor(parent, ctx, launch) {
430
430
  ctx,
431
431
  onAccounting: () => {
432
432
  const settled = settleAdvisorRun(parent, entry)
433
- // §29 fix B:settle 分支输出(清洗/未签发提示)写回 entry.report——digest 原样进。
433
+ // fix B:settle 分支输出(清洗/未签发提示)写回 entry.report——digest 原样进。
434
434
  if (settled.report != null) entry.report = settled.report
435
435
  },
436
436
  })
@@ -6,15 +6,15 @@
6
6
  *
7
7
  * 内容 = 迁入(逐字搬移含注释)+ 本批两处接线:
8
8
  * - `settleAdvisorRun` —— 记账本体零改(轮次 / 陈旧 / token D1 落盘 / prior);A 族
9
- * 未完成判定改用单谓词 `advisorIncompleteMarker`(§14.3 消费点 1/2):design 结算经
9
+ * 未完成判定改用单谓词 `advisorIncompleteMarker`(ADVISOR-GUARDS.md §1 消费点 1/2):design 结算经
10
10
  * `settleDesignReview(…, { incomplete })` 未完成即不签发;code 完成守卫同谓词
11
11
  * (旧 `^` 锚首行失败正则退役——六形态语义零丢,含 review_failed 字符串
12
12
  * resolve 形态);
13
- * - `inflightDesignReviewConflict` —— E 族(F17/§14.14)D5 冻结窗口冲突检测:与
13
+ * - `inflightDesignReviewConflict` —— E 族(F17/ADVISOR-GUARDS.md §5)D5 冻结窗口冲突检测:与
14
14
  * `reviewIsStale` 同族(同 docAbs / 同 normAbs)、仅扫 running 且未取消的设计条目
15
15
  * (dispatch Phase-1 预闸消费)。
16
16
  *
17
- * 第 33 批(2026-09-11——评审失败护栏 §17.5):`normAbs` 本体迁 `review-facts.mjs`
17
+ * 第 33 批(2026-09-11——评审失败护栏):`normAbs` 本体迁 `review-facts.mjs`
18
18
  * (原处 re-export 保面);**2026-09-18 顾问面治理批**:计数落账(`designReviewOutcome` →
19
19
  * `noteDesignReviewOutcome`)随会话级计数器整体退场——改治**失败结论**:结算出口按
20
20
  * `settlementCriterion`(纯函数单源,`advisor/notice.mjs`)追加结论块(**两轨共用**——
@@ -23,9 +23,9 @@
23
23
  import { persistEngTokens } from "../token-ttl.mjs"
24
24
  import { settleDesignReview, makeDesignTokenRegex, stripApprovedSuffix } from "./design-token.mjs"
25
25
  import { looksLikeReviewOutput, advisorIncompleteMarker, ADVISOR_LAUNCH_REFUSAL_PREFIX } from "../advisor/run.mjs"
26
- import { isCodePath, loadConventions } from "../conventions.mjs"
26
+ import { isCodePath, loadProjectDeclaration } from "../conventions.mjs"
27
27
  import { logEvent } from "../log.mjs"
28
- // 第 33 批(§17.5):`normAbs` 迁 `review-facts.mjs`(中立模块——陈旧判定与事实面共用同一归一);
28
+ // 第 33 批(评审失败护栏):`normAbs` 迁 `review-facts.mjs`(中立模块——陈旧判定与事实面共用同一归一);
29
29
  // 2026-09-18:结算分类 / 结论文案单源迁 `advisor/notice.mjs`(纯函数、零状态、零计数载体)。
30
30
  import { normAbs } from "./review-facts.mjs"
31
31
  import { settlementCriterion, buildSettlementConclusion, scopeSummary } from "../advisor/notice.mjs"
@@ -40,7 +40,7 @@ export function mutationSeqOf(agent) {
40
40
  }
41
41
 
42
42
  /** Record a file-mutation commit — bounded ring feeding the stale scan. paths = ABSOLUTE.
43
- * 唯一记账点(§29 fix A):dispatch runOne 写执行成功即刻调用(取代批后段 + 中断分支——
43
+ * 唯一记账点(fix A):dispatch runOne 写执行成功即刻调用(取代批后段 + 中断分支——
44
44
  * 不双计);mergeChildMutations(子代理合入)是独立事件面。 */
45
45
  export function noteMutations(agent, paths) {
46
46
  const list = (paths ?? []).filter((p) => typeof p === "string" && p.length > 0)
@@ -51,7 +51,7 @@ export function noteMutations(agent, paths) {
51
51
  if (log.length > 200) log.splice(0, log.length - 200)
52
52
  }
53
53
 
54
- // 第 33 批(§17.5):`normAbs` 本体迁 `review-facts.mjs`;原处 re-export 保既有 import 面
54
+ // 第 33 批(评审失败护栏):`normAbs` 本体迁 `review-facts.mjs`;原处 re-export 保既有 import 面
55
55
  // (advisor-async / 测试)零变。
56
56
  export { normAbs } from "./review-facts.mjs"
57
57
 
@@ -68,14 +68,14 @@ export function reviewIsStale(agent, entry) {
68
68
  const set = new Set(scope)
69
69
  return since.some((m) => (m.paths ?? []).some((p) => set.has(normAbs(p, agent?.cwd))))
70
70
  }
71
- // Code face = the shared classifier (src/conventions.mjs) — one authority with
71
+ // Code face = the shared classifier (thincoder-core/conventions.mjs) — one authority with
72
72
  // the design gate and the mutation guard (PORTABILITY FR12 / PO-10).
73
- const conv = loadConventions(agent?.cwd)
73
+ const conv = loadProjectDeclaration(agent?.cwd)
74
74
  return since.some((m) => (m.paths ?? []).some((p) => isCodePath(normAbs(p, agent?.cwd), conv)))
75
75
  }
76
76
 
77
77
  /**
78
- * E 族(F17/§14.14 E-3d)——D5 冻结窗口写前拦截的冲突检测:设计评审**在途**(点火 → 结算)
78
+ * E 族(F17/ADVISOR-GUARDS.md §5 E-3d)——D5 冻结窗口写前拦截的冲突检测:设计评审**在途**(点火 → 结算)
79
79
  * 期间,父侧对被审文件集(`docAbs` = 声明文档集,含批次档)的写入会被 dispatch 预闸拒绝。
80
80
  * 判据与 `reviewIsStale` **同源**(同一 docAbs / 同一 normAbs);仅扫 **running 且未取消**
81
81
  * 的设计条目(已结算 / 已取消条目不拦——结算后写不再致 stale、取消的结算早退不判)。
@@ -112,7 +112,7 @@ export function inflightDesignReviewConflict(agent, absPaths) {
112
112
  * mirror retired per DESIGN-TOKEN-SETTLEMENT D3) + D1 settle-time slot persist
113
113
  * (persistEngTokens — write failure = settle failure: no registration, no
114
114
  * Approved echo, re-review — 评审 #1);
115
- * 4. §29 fix B — branch-shaped report output (the caller writes it back to
115
+ * 4. fix B — branch-shaped report output (the caller writes it back to
116
116
  * entry.report; digest injects the CLEANED form): pass → stripped +
117
117
  * Approved/designId suffix (sync 参照形态); persist-failed → stripped +
118
118
  * "D1: …token could NOT be durably written…" re-review notice (no Approved
@@ -121,7 +121,7 @@ export function inflightDesignReviewConflict(agent, absPaths) {
121
121
  * 第 11 批增补(A/F11):`incomplete` = 宿主尾族判定(单谓词)——design 结算未完成即不
122
122
  * 签发(`settleDesignReview` 的 `opts.incomplete`);code 完成守卫的失败判定改用同谓词
123
123
  * (六形态语义零丢——旧 `^` 锚正则退役)。
124
- * 第 33 批增补(§17.5):design 结算按分类单源落账到会话级护栏——**2026-09-18 撤**(零载体)。
124
+ * 第 33 批增补(评审失败护栏):design 结算按分类单源落账到会话级护栏——**2026-09-18 撤**(零载体)。
125
125
  * 现行:结算出口按判据名单源(`settlementCriterion`)追加失败结论块(两轨共用;F28/F29)——
126
126
  * 判据名非空 ⇒ 块在位(块首行 = 对象标识行);可用判决 / 中断 / 拒发 ⇒ 零噪声(无块)。
127
127
  * Cancelled / parent-aborted reviews consume nothing (the user dropped the
@@ -140,10 +140,10 @@ export function settleAdvisorRun(agent, entry) {
140
140
  let report = result
141
141
  let passed = false
142
142
  let persistFailed = false
143
- // 宿主尾族判定(单谓词——design 结算与 code 守卫共用;§14.3 消费点 1/2)。
143
+ // 宿主尾族判定(单谓词——design 结算与 code 守卫共用;ADVISOR-GUARDS.md §1 消费点 1/2)。
144
144
  const incomplete = result != null ? advisorIncompleteMarker(String(result)) : null
145
145
  // 启动拒绝报告(未发起请求——无评审产出):第 33 批上移为本结算段**单点**——消费 = ① design
146
- // 失败分类(neutral——无尝试发生,§17.3 #1)② code 守卫 failureVerdict(既有语义零变)。
146
+ // 失败分类(neutral——无尝试发生;`ADVISOR-GUARDS.md` §7 失败结算表行 1)② code 守卫 failureVerdict(既有语义零变)。
147
147
  const launchRefused = result != null && String(result).startsWith(ADVISOR_LAUNCH_REFUSAL_PREFIX)
148
148
  if (!stale) {
149
149
  if (run.reviewType === "design" && entry.designToken && result) {
@@ -199,7 +199,7 @@ export function settleAdvisorRun(agent, entry) {
199
199
  // 第 11 批(F16/A3):失败判定 = 单谓词六 kind(含 review_failed 字符串 resolve 形态)
200
200
  // ——旧 `^` 锚正则只认首行形态,漏「时间线 + 尾」;语义零丢(六形态全覆盖)。
201
201
  // 另:设计评审的**启动拒绝报告**(未发起请求——无评审产出)同样不得计为评审覆盖
202
- // (§14.4 异步结算面判据;与同步工具面 _advisorRefusals 同源前缀;正常链不可达=防御纵深)
202
+ // (异步结算面判据;与同步工具面 _advisorRefusals 同源前缀;正常链不可达=防御纵深)
203
203
  // ——判定已上移为本结算段单点(launchRefused,上方)。
204
204
  const failureVerdict = (run.reviewType !== "design" && (
205
205
  incomplete !== null ||
@@ -209,7 +209,7 @@ export function settleAdvisorRun(agent, entry) {
209
209
  agent._calledAdvisorThisRun = true
210
210
  }
211
211
  } else if (run.reviewType === "design" && entry.designToken && result != null) {
212
- // §29 fix B(stale 分支):陈旧评审不签发——digest 不得展示未注册 token——先剥
212
+ // fix B(stale 分支):陈旧评审不签发——digest 不得展示未注册 token——先剥
213
213
  // 方括号回显 + 前置 "评审目标已变更——token 未签发"(不变式——两分支都清洗)。
214
214
  const stripped = String(result)
215
215
  .replace(makeDesignTokenRegex(entry.designToken, "g"), "")
@@ -217,7 +217,7 @@ export function settleAdvisorRun(agent, entry) {
217
217
  report = `评审目标已变更——token 未签发 (review target changed after launch — this review judged a stale state; no design token was issued — re-run the review on the current state)\n\n${stripped}`.trim()
218
218
  }
219
219
  // Prior of round 2+ = the last REVIEW-LOOKING output (mirror of run.mjs's guard).
220
- // F2e (§29.1): strip the engine-approved suffix FIRST — the prior must never
220
+ // F2e: strip the engine-approved suffix FIRST — the prior must never
221
221
  // carry the raw token / designId (exact truncation — zero collateral).
222
222
  // 结论块(下方)刻意晚于 prior 归一:prior 只承载评审正文,不携带机制文案。
223
223
  if (report && looksLikeReviewOutput(report)) {