@thincoder/core 0.9.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 (203) hide show
  1. package/abort-provenance.mjs +116 -0
  2. package/advisor/citations.mjs +139 -0
  3. package/advisor/compaction.mjs +174 -0
  4. package/advisor/convergence.mjs +80 -0
  5. package/advisor/history.mjs +77 -0
  6. package/advisor/loop.mjs +288 -0
  7. package/advisor/messages.mjs +299 -0
  8. package/advisor/notice.mjs +141 -0
  9. package/advisor/project-context.mjs +197 -0
  10. package/advisor/repos.mjs +150 -0
  11. package/advisor/run.mjs +190 -0
  12. package/advisor/truncate.mjs +57 -0
  13. package/advisor.mjs +281 -0
  14. package/agent/child-marks.mjs +24 -0
  15. package/agent/completion.mjs +145 -0
  16. package/agent/dispatch.mjs +493 -0
  17. package/agent/family-tools.mjs +174 -0
  18. package/agent/helpers.mjs +412 -0
  19. package/agent/post-turn.mjs +70 -0
  20. package/agent/record-results.mjs +174 -0
  21. package/agent/relay-prefix.mjs +39 -0
  22. package/agent/run-stages.mjs +244 -0
  23. package/agent/setup-reminders.mjs +199 -0
  24. package/agent/setup.mjs +234 -0
  25. package/agent/spawn-child.mjs +258 -0
  26. package/agent/suspension.mjs +240 -0
  27. package/agent/write-gate.mjs +87 -0
  28. package/agent-tools/advisor-async.mjs +481 -0
  29. package/agent-tools/advisor-settle.mjs +240 -0
  30. package/agent-tools/advisor.mjs +280 -0
  31. package/agent-tools/async-discard.mjs +143 -0
  32. package/agent-tools/async-settle.mjs +299 -0
  33. package/agent-tools/batch-segment.mjs +265 -0
  34. package/agent-tools/child-permission.mjs +45 -0
  35. package/agent-tools/consult.mjs +471 -0
  36. package/agent-tools/design-token.mjs +117 -0
  37. package/agent-tools/digest-budget.mjs +76 -0
  38. package/agent-tools/eng.mjs +102 -0
  39. package/agent-tools/escalate-async.mjs +302 -0
  40. package/agent-tools/goal.mjs +119 -0
  41. package/agent-tools/panel-blocks.mjs +24 -0
  42. package/agent-tools/parent-channel.mjs +231 -0
  43. package/agent-tools/plan.mjs +86 -0
  44. package/agent-tools/read-history.mjs +309 -0
  45. package/agent-tools/recent-changes.mjs +24 -0
  46. package/agent-tools/review-facts.mjs +31 -0
  47. package/agent-tools/settings.mjs +268 -0
  48. package/agent-tools/skill.mjs +63 -0
  49. package/agent-tools/spawn-gates.mjs +109 -0
  50. package/agent-tools/subagent-actions.mjs +496 -0
  51. package/agent-tools/subagent-async.mjs +456 -0
  52. package/agent-tools/subagent-panel.mjs +160 -0
  53. package/agent-tools/subagent-run.mjs +208 -0
  54. package/agent-tools/subagent-scheduler.mjs +446 -0
  55. package/agent-tools/subagent-spawn.mjs +478 -0
  56. package/agent-tools/subagent.mjs +419 -0
  57. package/agent-tools/task.mjs +87 -0
  58. package/agent-tools/timer.mjs +46 -0
  59. package/agent-tools/verify.mjs +295 -0
  60. package/agent-tools.mjs +23 -0
  61. package/agent.mjs +430 -0
  62. package/auto-think.mjs +115 -0
  63. package/compress-form.mjs +24 -0
  64. package/config-io.mjs +277 -0
  65. package/config-migrate.mjs +178 -0
  66. package/config-presets.mjs +49 -0
  67. package/config.mjs +419 -0
  68. package/context.mjs +495 -0
  69. package/conventions.mjs +223 -0
  70. package/embedding.mjs +120 -0
  71. package/escape.mjs +152 -0
  72. package/expand-home.mjs +16 -0
  73. package/explore-distill.mjs +152 -0
  74. package/generate-title.mjs +123 -0
  75. package/git/checkpoint.mjs +448 -0
  76. package/git/gitmem.mjs +100 -0
  77. package/history-window.mjs +179 -0
  78. package/hooks.mjs +108 -0
  79. package/i18n.mjs +106 -0
  80. package/index-bin.mjs +48 -0
  81. package/index-discover.mjs +176 -0
  82. package/ledger-cmd.mjs +209 -0
  83. package/ledger-db.mjs +86 -0
  84. package/ledger-surface.mjs +76 -0
  85. package/ledger.mjs +202 -0
  86. package/log.mjs +195 -0
  87. package/manifest.mjs +338 -0
  88. package/markdown.mjs +106 -0
  89. package/mcp/helpers.mjs +51 -0
  90. package/mcp/transport-http.mjs +248 -0
  91. package/mcp/transport-stdio.mjs +140 -0
  92. package/mcp/transport-ws.mjs +122 -0
  93. package/mcp.mjs +295 -0
  94. package/memory/code-index.mjs +219 -0
  95. package/memory/code-sync.mjs +427 -0
  96. package/memory/core.mjs +318 -0
  97. package/memory/delete.mjs +242 -0
  98. package/memory/docs.mjs +431 -0
  99. package/memory/file-walk.mjs +109 -0
  100. package/memory/origin.mjs +24 -0
  101. package/memory/scan.mjs +177 -0
  102. package/memory/schema.mjs +460 -0
  103. package/memory.mjs +21 -0
  104. package/model-ref.mjs +66 -0
  105. package/model-specs.mjs +277 -0
  106. package/package.json +32 -0
  107. package/peer-domains.mjs +265 -0
  108. package/peer-instances.mjs +178 -0
  109. package/permission.mjs +79 -0
  110. package/process-probe.mjs +315 -0
  111. package/prompt-files.mjs +113 -0
  112. package/prompt-overlays.mjs +78 -0
  113. package/prompts/advisor-design.md +43 -0
  114. package/prompts/advisor-round1.md +41 -0
  115. package/prompts/advisor-round2.md +46 -0
  116. package/prompts/advisor-round3.md +42 -0
  117. package/prompts/common.md +158 -0
  118. package/prompts/consult-base.md +19 -0
  119. package/prompts/discipline-engineering.md +123 -0
  120. package/prompts/discipline-normal.md +206 -0
  121. package/prompts/persona-coder.md +21 -0
  122. package/prompts/persona-eng-coder.md +41 -0
  123. package/prompts/persona-eng-designer.md +80 -0
  124. package/prompts/persona-engineering.md +160 -0
  125. package/prompts/persona-explore.md +15 -0
  126. package/prompts/persona-normal.md +35 -0
  127. package/prompts/persona-plan.md +27 -0
  128. package/provider/anthropic.mjs +225 -0
  129. package/provider/core.mjs +491 -0
  130. package/provider/errors.mjs +101 -0
  131. package/provider/google.mjs +257 -0
  132. package/provider/index.mjs +7 -0
  133. package/provider/list-models.mjs +163 -0
  134. package/provider/normalize.mjs +81 -0
  135. package/provider/rate.mjs +168 -0
  136. package/provider/responses.mjs +495 -0
  137. package/provider/retry.mjs +88 -0
  138. package/provider/sse.mjs +264 -0
  139. package/provider/wait-status.mjs +59 -0
  140. package/proxy.mjs +274 -0
  141. package/rules.mjs +53 -0
  142. package/session-gc.mjs +248 -0
  143. package/session-guard.mjs +59 -0
  144. package/session-lifecycle.mjs +305 -0
  145. package/session-migrate.mjs +48 -0
  146. package/session-rename.mjs +38 -0
  147. package/session-segments.mjs +99 -0
  148. package/session-slot-write.mjs +168 -0
  149. package/session-slots-manifest.mjs +264 -0
  150. package/session-slots.mjs +298 -0
  151. package/session-store.mjs +441 -0
  152. package/session.mjs +244 -0
  153. package/skills.mjs +234 -0
  154. package/text-budget.mjs +79 -0
  155. package/token-ttl.mjs +285 -0
  156. package/tool-docs/apply_patch.md +15 -0
  157. package/tool-docs/bash.md +38 -0
  158. package/tool-docs/delete.md +13 -0
  159. package/tool-docs/edit.md +30 -0
  160. package/tool-docs/execute.md +21 -0
  161. package/tool-docs/fetch.md +12 -0
  162. package/tool-docs/file_ops.md +17 -0
  163. package/tool-docs/get_current_time.md +8 -0
  164. package/tool-docs/git.md +54 -0
  165. package/tool-docs/glob.md +11 -0
  166. package/tool-docs/grep.md +19 -0
  167. package/tool-docs/hashline_edit.md +14 -0
  168. package/tool-docs/insert_after.md +15 -0
  169. package/tool-docs/lint.md +10 -0
  170. package/tool-docs/ls.md +12 -0
  171. package/tool-docs/lsp.md +10 -0
  172. package/tool-docs/process.md +10 -0
  173. package/tool-docs/question.md +16 -0
  174. package/tool-docs/read.md +20 -0
  175. package/tool-docs/read_image.md +8 -0
  176. package/tool-docs/tree.md +14 -0
  177. package/tool-docs/wait_for.md +22 -0
  178. package/tool-docs/websearch.md +16 -0
  179. package/tool-docs/write.md +11 -0
  180. package/tools/bash.mjs +276 -0
  181. package/tools/edit-batch.mjs +204 -0
  182. package/tools/edit-diff.mjs +388 -0
  183. package/tools/exec-run.mjs +43 -0
  184. package/tools/execute.mjs +243 -0
  185. package/tools/file.mjs +464 -0
  186. package/tools/git-checkpoint.mjs +143 -0
  187. package/tools/git-ext.mjs +173 -0
  188. package/tools/git.mjs +415 -0
  189. package/tools/glob-dialect.mjs +130 -0
  190. package/tools/index.mjs +76 -0
  191. package/tools/linter.mjs +120 -0
  192. package/tools/lsp.mjs +335 -0
  193. package/tools/ops.mjs +293 -0
  194. package/tools/patch.mjs +290 -0
  195. package/tools/question.mjs +26 -0
  196. package/tools/repomap.mjs +314 -0
  197. package/tools/search.mjs +248 -0
  198. package/tools/shared.mjs +467 -0
  199. package/tools/tree.mjs +81 -0
  200. package/tools/web.mjs +224 -0
  201. package/tools/write-path.mjs +191 -0
  202. package/traces/trace-store.mjs +303 -0
  203. package/undo-stack.mjs +47 -0
@@ -0,0 +1,240 @@
1
+ /**
2
+ * advisor-settle.mjs — advisor settle accounting + mutation log + stale / freeze
3
+ * determination(2026-09-11 第 11 批自 advisor-async.mjs 拆出——500 行 = 硬帽在册,
4
+ * 任何新增必越;既有 import 面经 advisor-async.mjs **re-export 保持不变**
5
+ * (noteMutations / mutationSeqOf / reviewIsStale / settleAdvisorRun)。
6
+ *
7
+ * 内容 = 迁入(逐字搬移含注释)+ 本批两处接线:
8
+ * - `settleAdvisorRun` —— 记账本体零改(轮次 / 陈旧 / token D1 落盘 / prior);A 族
9
+ * 未完成判定改用单谓词 `advisorIncompleteMarker`(§14.3 消费点 1/2):design 结算经
10
+ * `settleDesignReview(…, { incomplete })` 未完成即不签发;code 完成守卫同谓词
11
+ * (旧 `^` 锚首行失败正则退役——六形态语义零丢,含 review_failed 字符串
12
+ * resolve 形态);
13
+ * - `inflightDesignReviewConflict` —— E 族(F17/§14.14)D5 冻结窗口冲突检测:与
14
+ * `reviewIsStale` 同族(同 docAbs / 同 normAbs)、仅扫 running 且未取消的设计条目
15
+ * (dispatch Phase-1 预闸消费)。
16
+ *
17
+ * 第 33 批(2026-09-11——评审失败护栏 §17.5):`normAbs` 本体迁 `review-facts.mjs`
18
+ * (原处 re-export 保面);**2026-09-18 顾问面治理批**:计数落账(`designReviewOutcome` →
19
+ * `noteDesignReviewOutcome`)随会话级计数器整体退场——改治**失败结论**:结算出口按
20
+ * `settlementCriterion`(纯函数单源,`advisor/notice.mjs`)追加结论块(**两轨共用**——
21
+ * ADVISOR-GUARDS.md §7)。记账本体 / 陈旧判定 / 轮次 / token 落盘零改。
22
+ */
23
+ import { persistEngTokens } from "../token-ttl.mjs"
24
+ import { settleDesignReview, makeDesignTokenRegex, stripApprovedSuffix } from "./design-token.mjs"
25
+ import { looksLikeReviewOutput, advisorIncompleteMarker, ADVISOR_LAUNCH_REFUSAL_PREFIX } from "../advisor/run.mjs"
26
+ import { isCodePath, loadConventions } from "../conventions.mjs"
27
+ import { logEvent } from "../log.mjs"
28
+ // 第 33 批(§17.5):`normAbs` 迁 `review-facts.mjs`(中立模块——陈旧判定与事实面共用同一归一);
29
+ // 2026-09-18:结算分类 / 结论文案单源迁 `advisor/notice.mjs`(纯函数、零状态、零计数载体)。
30
+ import { normAbs } from "./review-facts.mjs"
31
+ import { settlementCriterion, buildSettlementConclusion, scopeSummary } from "../advisor/notice.mjs"
32
+
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+ // Mutation log (stale-review determination — fix #2: FILE_MUTATORS after the
35
+ // review launch make the settle stale: no called-mark, no token, guard re-pushes)
36
+ // ─────────────────────────────────────────────────────────────────────────────
37
+
38
+ export function mutationSeqOf(agent) {
39
+ return agent._mutationSeq ?? 0
40
+ }
41
+
42
+ /** Record a file-mutation commit — bounded ring feeding the stale scan. paths = ABSOLUTE.
43
+ * 唯一记账点(§29 fix A):dispatch runOne 写执行成功即刻调用(取代批后段 + 中断分支——
44
+ * 不双计);mergeChildMutations(子代理合入)是独立事件面。 */
45
+ export function noteMutations(agent, paths) {
46
+ const list = (paths ?? []).filter((p) => typeof p === "string" && p.length > 0)
47
+ if (list.length === 0) return
48
+ agent._mutationSeq = (agent._mutationSeq ?? 0) + 1
49
+ const log = agent._mutLog ??= []
50
+ log.push({ seq: agent._mutationSeq, paths: [...new Set(list)] })
51
+ if (log.length > 200) log.splice(0, log.length - 200)
52
+ }
53
+
54
+ // 第 33 批(§17.5):`normAbs` 本体迁 `review-facts.mjs`;原处 re-export 保既有 import 面
55
+ // (advisor-async / 测试)零变。
56
+ export { normAbs } from "./review-facts.mjs"
57
+
58
+ /** Stale = a mutation committed after the launch touched the review's face:
59
+ * code reviews judge the CODE face; design reviews judge their OWN documents. */
60
+ export function reviewIsStale(agent, entry) {
61
+ const log = agent?._mutLog
62
+ if (!Array.isArray(log) || log.length === 0) return false
63
+ const since = log.filter((m) => m.seq > (entry.launchSeq ?? -1))
64
+ if (since.length === 0) return false
65
+ if (entry.reviewType === "design") {
66
+ const scope = (entry.docAbs ?? []).map((p) => normAbs(p, agent?.cwd))
67
+ if (scope.length === 0) return false // no explicit doc scope — nothing to judge stale
68
+ const set = new Set(scope)
69
+ return since.some((m) => (m.paths ?? []).some((p) => set.has(normAbs(p, agent?.cwd))))
70
+ }
71
+ // Code face = the shared classifier (src/conventions.mjs) — one authority with
72
+ // the design gate and the mutation guard (PORTABILITY FR12 / PO-10).
73
+ const conv = loadConventions(agent?.cwd)
74
+ return since.some((m) => (m.paths ?? []).some((p) => isCodePath(normAbs(p, agent?.cwd), conv)))
75
+ }
76
+
77
+ /**
78
+ * E 族(F17/§14.14 E-3d)——D5 冻结窗口写前拦截的冲突检测:设计评审**在途**(点火 → 结算)
79
+ * 期间,父侧对被审文件集(`docAbs` = 声明文档集,含批次档)的写入会被 dispatch 预闸拒绝。
80
+ * 判据与 `reviewIsStale` **同源**(同一 docAbs / 同一 normAbs);仅扫 **running 且未取消**
81
+ * 的设计条目(已结算 / 已取消条目不拦——结算后写不再致 stale、取消的结算早退不判)。
82
+ * 保守残余(如实注):回合中止后池清前的窗口可能拒一笔不致 stale 的写(保守方向)。
83
+ * @param {Object} agent
84
+ * @param {string[]} absPaths — 本次写将触碰的路径(ABS——调用方 resolve(agent.cwd, p))
85
+ * @returns {{id: string, path: string}|null} 命中评审 id + 冲突路径(ABS),无冲突 null
86
+ */
87
+ export function inflightDesignReviewConflict(agent, absPaths) {
88
+ const pool = agent?._asyncAdvisors
89
+ if (!(pool instanceof Map) || pool.size === 0) return null
90
+ const wanted = (absPaths ?? [])
91
+ .filter((p) => typeof p === "string" && p.length > 0)
92
+ .map((p) => normAbs(p, agent?.cwd))
93
+ if (wanted.length === 0) return null
94
+ for (const entry of pool.values()) {
95
+ if (!entry || entry.reviewType !== "design") continue
96
+ if (entry.status !== "running" || entry.cancelled || entry.done) continue
97
+ const scope = (entry.docAbs ?? []).map((p) => normAbs(p, agent?.cwd))
98
+ if (scope.length === 0) continue
99
+ const set = new Set(scope)
100
+ for (const p of wanted) if (set.has(p)) return { id: String(entry.id), path: p }
101
+ }
102
+ return null
103
+ }
104
+
105
+ /**
106
+ * Settle accounting (fix #2/#4 — runs when a non-cancelled review settles,
107
+ * BEFORE the pending transfer / digest injection):
108
+ * 1. round++ (attempts count — parity with the legacy _advisorRound++ budget);
109
+ * 2. stale determination — mutated targets since launch → no called-mark, no token;
110
+ * 3. non-stale: code review → _calledAdvisorThisRun = true; design review →
111
+ * token echo check (settleDesignReview: slot + instance close; single-value
112
+ * mirror retired per DESIGN-TOKEN-SETTLEMENT D3) + D1 settle-time slot persist
113
+ * (persistEngTokens — write failure = settle failure: no registration, no
114
+ * Approved echo, re-review — 评审 #1);
115
+ * 4. §29 fix B — branch-shaped report output (the caller writes it back to
116
+ * entry.report; digest injects the CLEANED form): pass → stripped +
117
+ * Approved/designId suffix (sync 参照形态); persist-failed → stripped +
118
+ * "D1: …token could NOT be durably written…" re-review notice (no Approved
119
+ * suffix — never an unregistered token); stale → echo stripped +
120
+ * "评审目标已变更——token 未签发" prefix — never an unregistered token.
121
+ * 第 11 批增补(A/F11):`incomplete` = 宿主尾族判定(单谓词)——design 结算未完成即不
122
+ * 签发(`settleDesignReview` 的 `opts.incomplete`);code 完成守卫的失败判定改用同谓词
123
+ * (六形态语义零丢——旧 `^` 锚正则退役)。
124
+ * 第 33 批增补(§17.5):design 结算按分类单源落账到会话级护栏——**2026-09-18 撤**(零载体)。
125
+ * 现行:结算出口按判据名单源(`settlementCriterion`)追加失败结论块(两轨共用;F28/F29)——
126
+ * 判据名非空 ⇒ 块在位(块首行 = 对象标识行);可用判决 / 中断 / 拒发 ⇒ 零噪声(无块)。
127
+ * Cancelled / parent-aborted reviews consume nothing (the user dropped the
128
+ * attempt — the retry must not lose budget).
129
+ * @returns {{cancelled: boolean, stale: boolean, passed: boolean, report: string|null}}
130
+ */
131
+ export function settleAdvisorRun(agent, entry) {
132
+ if (entry.cancelled) return { cancelled: true, stale: false, passed: false, report: null }
133
+ const run = entry.run
134
+ if (!run) return { cancelled: false, stale: false, passed: false, report: entry.report ?? null }
135
+ const result = entry.report ?? null
136
+ run.round++
137
+ agent._advisorRound = run.round
138
+ const stale = reviewIsStale(agent, entry)
139
+ run.stale = stale
140
+ let report = result
141
+ let passed = false
142
+ let persistFailed = false
143
+ // 宿主尾族判定(单谓词——design 结算与 code 守卫共用;§14.3 消费点 1/2)。
144
+ const incomplete = result != null ? advisorIncompleteMarker(String(result)) : null
145
+ // 启动拒绝报告(未发起请求——无评审产出):第 33 批上移为本结算段**单点**——消费 = ① design
146
+ // 失败分类(neutral——无尝试发生,§17.3 #1)② code 守卫 failureVerdict(既有语义零变)。
147
+ const launchRefused = result != null && String(result).startsWith(ADVISOR_LAUNCH_REFUSAL_PREFIX)
148
+ if (!stale) {
149
+ if (run.reviewType === "design" && entry.designToken && result) {
150
+ // settle 前 Map 快照——落盘失败时回滚用(settle 失败 = 结算未发生,不留半结算态:
151
+ // 重评覆盖旧槽的边角(F2h 复用 designId)也原样恢复旧 token——内存与盘一致)。
152
+ const preMap = agent._engDesignTokens instanceof Map
153
+ ? new Map(agent._engDesignTokens)
154
+ : null
155
+ const settled = settleDesignReview(agent, run, entry.designToken, result, { incomplete })
156
+ if (settled.passed) {
157
+ // DESIGN-TOKEN-SETTLEMENT D1(2026-09-08):settle 是唯一结算点——settle 当场
158
+ // 同步落盘 token 字段到槽文件(persistEngTokens = engTokenSlotFields 序列化 +
159
+ // session 安全写/轮转——勿裸写文件),不等下个回合尾 saveSession(消除"settle→
160
+ // 下个 saveSession"间的重启丢 token 窗口)。
161
+ // 写失败即 settle 失败(评审 #1):token 不注册(Map 回滚到 settle 前快照)、无
162
+ // Approved 回显、可重评——不静默吞错、不产生"内存有盘上无"态(宁可结算失败
163
+ // 可重评,不留半结算态)。
164
+ let durable = false
165
+ try {
166
+ durable = persistEngTokens(agent)
167
+ } catch (e) {
168
+ logEvent("advisor:error", { id: `advisor#${entry.id}`, err: `engDesignTokens slot persist threw: ${e?.message ?? String(e)}` })
169
+ }
170
+ if (durable) {
171
+ passed = true
172
+ report = settled.output
173
+ } else {
174
+ if (preMap) agent._engDesignTokens = preMap
175
+ else delete agent._engDesignTokens
176
+ run.approvedSuffix = null
177
+ persistFailed = true
178
+ logEvent("advisor:error", { id: `advisor#${entry.id}`, err: "engDesignTokens slot persist failed — settle failed (re-review)" })
179
+ const stripped = String(result)
180
+ .replace(makeDesignTokenRegex(entry.designToken, "g"), "")
181
+ .trim()
182
+ report = `${stripped}\n\nD1: the design review passed but the token could NOT be durably written to the session ledger (slot persist failed) — re-run advisor(type='design') to re-issue; no eng-coder spawn is authorized for this review (评审通过但 token 未能持久化——需重评).`.trim()
183
+ }
184
+ } else {
185
+ report = settled.output
186
+ }
187
+ }
188
+ // A completed review covers the code face ONLY when it actually produced a
189
+ // verdict: a mechanical-failure settle ("Advisor: review failed/timeout/…" —
190
+ // run.mjs resolves with the failure text, it never throws) must not satisfy
191
+ // the guard silently — the digest shows the failure and the guard pushes
192
+ // back for a retry (fix #2 anti-silent-skip intent; attempts still consume
193
+ // the per-review round budget, so repeated failures stay bounded by the
194
+ // cap). An error settle (rejection path — report null with an error) has no
195
+ // verdict either — same exclusion (advisor 复评补边). Design reviews keep
196
+ // the mark on any completed verdict (parity with the sync recordToolResults
197
+ // mark — they have no code face to cover) — EXCEPT a launch-refusal report
198
+ // (nothing was sent, no verdict at all: see launchRefused below).
199
+ // 第 11 批(F16/A3):失败判定 = 单谓词六 kind(含 review_failed 字符串 resolve 形态)
200
+ // ——旧 `^` 锚正则只认首行形态,漏「时间线 + 尾」;语义零丢(六形态全覆盖)。
201
+ // 另:设计评审的**启动拒绝报告**(未发起请求——无评审产出)同样不得计为评审覆盖
202
+ // (§14.4 异步结算面判据;与同步工具面 _advisorRefusals 同源前缀;正常链不可达=防御纵深)
203
+ // ——判定已上移为本结算段单点(launchRefused,上方)。
204
+ const failureVerdict = (run.reviewType !== "design" && (
205
+ incomplete !== null ||
206
+ (result == null && entry.error != null)
207
+ )) || launchRefused
208
+ if (!failureVerdict) {
209
+ agent._calledAdvisorThisRun = true
210
+ }
211
+ } else if (run.reviewType === "design" && entry.designToken && result != null) {
212
+ // §29 fix B(stale 分支):陈旧评审不签发——digest 不得展示未注册 token——先剥
213
+ // 方括号回显 + 前置 "评审目标已变更——token 未签发"(不变式——两分支都清洗)。
214
+ const stripped = String(result)
215
+ .replace(makeDesignTokenRegex(entry.designToken, "g"), "")
216
+ .trim()
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
+ }
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
221
+ // carry the raw token / designId (exact truncation — zero collateral).
222
+ // 结论块(下方)刻意晚于 prior 归一:prior 只承载评审正文,不携带机制文案。
223
+ if (report && looksLikeReviewOutput(report)) {
224
+ run.priorOutput = stripApprovedSuffix(report, run.approvedSuffix)
225
+ }
226
+ // F28/F29 失败结论(2026-09-18 顾问面治理批——**两轨共用**,ADVISOR-GUARDS.md §7 契约二):
227
+ // 判据名非空 ⇒ 结算出口追加结论块(块首行 = 对象标识行 + 既有正文逐字 + 本次尝试单行 +
228
+ // 选项三值)。**零会话级计数载体 / 零封禁**——下一次发起照常受理(cap / 连败停止预检已撤除)。
229
+ const criterion = settlementCriterion({ launchRefused, stale, hasResult: result != null, incomplete, persistFailed })
230
+ if (criterion) {
231
+ report = buildSettlementConclusion({
232
+ type: run.reviewType,
233
+ scope: scopeSummary(entry.documents?.length ? entry.documents : entry.paths),
234
+ round: run.round,
235
+ criterion,
236
+ body: report,
237
+ })
238
+ }
239
+ return { cancelled: false, stale, passed, report }
240
+ }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * agent-tools/advisor.mjs — advisor tool wrapper.
3
+ * The agent calls this explicitly to get an independent review.
4
+ * The top-level `type` is REQUIRED and must be exactly "code" or "design"
5
+ * (F30 fail-closed — no silent default; ADVISOR-GUARDS.md §2.4).
6
+ * §11.2 (R13 — async advisor): at depth 0 the review launches into the
7
+ * background pool by DEFAULT (async:true / omitted; async:false forces the
8
+ * blocking review); depth>0 (eng-coder self-review) stays synchronous always.
9
+ */
10
+ import { runAdvisorReview, advisorIncompleteMarker, ADVISOR_LAUNCH_REFUSAL_PREFIX } from "../advisor/run.mjs"
11
+ import { resolveBatchDocPath } from "./batch-segment.mjs"
12
+ // M6(模块设计 §2.1 F3):评审对象来源读 manifest docRoot(声明面)——复用 M4 的
13
+ // write-gate.mjs 单一权威源(KD-M6-1),替代 v1 的 loadConventions/isDocPath 分类;
14
+ // normAbs 同源 re-export(指针非副本)。不 import dispatch.mjs(簇间回边,环风险)。
15
+ import { resolveReviewTargetPaths, normAbs } from "../agent/write-gate.mjs"
16
+ import { sep } from "node:path"
17
+ import {
18
+ generateDesignToken,
19
+ settleDesignReview,
20
+ resolveAdvisorLaunch,
21
+ launchAsyncAdvisor,
22
+ stripApprovedSuffix,
23
+ } from "./advisor-async.mjs"
24
+ // F30/F31(2026-09-18 顾问面治理批):类型门判定 / 拒发串 / 对象标识行——单源 `advisor/notice.mjs`
25
+ // (文案逐字 = ADVISOR-GUARDS.md §2.4 / §2.5);零计数载体(计数护栏随撤 cap 整体退场)。
26
+ import { typeGateCriterion, buildTypeGateRefusal, scopeSummary, withIdentityLine } from "../advisor/notice.mjs"
27
+
28
+ // Design-token utilities moved to advisor-async.mjs (the async settle shares
29
+ // them — no wrapper↔runner module cycle); validateDesignToken stays exported
30
+ // here for the tests' import surface (implementation re-exported).
31
+ export { validateDesignToken } from "./advisor-async.mjs"
32
+
33
+ export const advisorTool = {
34
+ name: "advisor",
35
+ description:
36
+ "Run an independent review on your work. " +
37
+ "type is REQUIRED — exactly one of the two legal values: type='design' reviews design / requirement documents before implementation (pass documents=[...] with the explicit list of doc paths; plus batchDoc when a batch record is in flight); type='code' reviews the code you changed after implementation (pass paths=[...] to scope files/directories — documents=[...] adds acceptance-criteria context). " +
38
+ "A call without a type (or with a conflicting object.type) is refused — there is no default and no silent fallback. " +
39
+ "The advisor is an independent read-only sub-agent that explores the codebase, " +
40
+ "reads files, and traces callers via grep/lsp. " +
41
+ "For code review: round 1 does a full review, round 2 verifies the agent's fix claims, " +
42
+ "round 3+ strictly checks only the fix claims — convergence, not divergence. " +
43
+ "For design review: single-pass review against methodology and requirements. " +
44
+ "Review criteria come from .thincoder/advisor.md (if present) or sensible defaults. " +
45
+ "After the review, you MUST produce a response table (see discipline rules for format). " +
46
+ "If advisor says all clear, call verify. " +
47
+ "Optionally pass object={type,target,status,reason,exclude} to anchor the review target " +
48
+ " (the review-object declaration is mechanically injected into the review message); " +
49
+ "absent → legacy behavior (no injection). " +
50
+ "ASYNC: at depth 0 the review runs in the BACKGROUND by default " +
51
+ "(async:true or omitted) — the call returns an ack immediately, the turn ends, and the report " +
52
+ "arrives automatically in a digest turn when the review finishes; background reviews share one pool — " +
53
+ "at most agent.poolLimits.advisor concurrent reviews (default 4 — configurable via /config 并发池 or " +
54
+ "config.json; pool-full and same-scope refusals state the current limit) — launch reviews one at a time. " +
55
+ "Inside a child (depth>0 — eng-coder self-review) " +
56
+ "reviews are always synchronous; async:true is rejected there. " +
57
+ "Returns the review report — the advisor's findings verdict: all-clear (call verify) or a findings list to fix.",
58
+ parameters: {
59
+ type: "object",
60
+ properties: {
61
+ type: { type: "string", enum: ["code", "design"], description: "Review type (required): 'design' for design doc review, 'code' for code review. Omitting it is refused — there is no default." },
62
+ async: {
63
+ type: "boolean",
64
+ description: "Background review: default at depth 0 = true (async — ack now, report via digest); async:false forces the blocking review (mechanism parameter — top-level launches are async by default). depth>0 → always sync (async:true rejected).",
65
+ },
66
+ object: {
67
+ type: "object",
68
+ properties: {
69
+ type: { type: "string", description: "Review type as declared by the caller (design/code)" },
70
+ target: { type: "string", description: "Review target — document + section, or file(s)" },
71
+ status: { type: "string", description: "Object state: 待评审 / 已批准 / 已实现 (pending-review / approved / implemented)" },
72
+ reason: { type: "string", description: "Why this review runs: user-initiated / delivery verification" },
73
+ exclude: { type: "string", description: "Explicit exclusion list — approved/implemented items NOT in this review" },
74
+ },
75
+ description: "Review-object declaration: mechanically injected at the start of the review user message so the advisor does not re-derive the review target. Absent → no injection (legacy behavior).",
76
+ },
77
+ paths: {
78
+ type: "array",
79
+ items: { type: "string" },
80
+ description: "Code files or directories to review (for code review). Required unless documents is provided. The advisor reads the files/directories listed here — it has no git tool and never inspects diffs.",
81
+ },
82
+ documents: {
83
+ type: "array",
84
+ items: { type: "string" },
85
+ description: "Explicit list of doc paths to review (design docs, requirements docs, referenced docs). The advisor reviews ONLY these — it does NOT scan git diff. Use for both design review and code review to pass the task's Docs involved list.",
86
+ },
87
+ batchDoc: {
88
+ type: "string",
89
+ description: "Design review only: path to the batch record currently in flight. Validated WHENEVER passed (any review type) — a value that is not a readable file is refused with an error rather than ignored; for design reviews the reviewer then ALSO gets the batch_segment write channel to record its findings table + VERDICT + counts into §3. Omit when no batch record is in flight — the review then runs unchanged with no write channel (zero regression).",
90
+ },
91
+ },
92
+ required: ["type"],
93
+ },
94
+ readonly: true,
95
+ sideEffectExempt: true,
96
+ outputPanel: true,
97
+ async execute(args, ctx) {
98
+ const agent = ctx.agent
99
+ // Review-object declaration (§18.8 D-OA3): the PARENT constructs it and the
100
+ // advisor tool passes it through — mechanical anchoring, not model inference.
101
+ // Any non-object value (string/array/primitive, possibly from a malformed
102
+ // tool call) degrades to null = no injection (legacy calls unchanged).
103
+ const reviewObject = args.object && typeof args.object === "object" && !Array.isArray(args.object)
104
+ ? args.object
105
+ : null
106
+ // F30 类型门(**最早判定**——先于范围判定 / 实例解析 / 一切拒发族):顶层 `type` 必须逐字
107
+ // ∈ {"code","design"}——缺失 / 空串 / 非法值 / 非字符串 / 与 `object.type` 声明冲突 ⇒ 拒发
108
+ // (前缀 `Advisor: launch refused` + Why + 两个合法值行 + 标识行),登记 `_advisorRefusals`;
109
+ // **零实例 / 零 token / 零 LLM**(不静默降级——用户 2026-09-18 裁定)。
110
+ const gateCriterion = typeGateCriterion(args.type, reviewObject?.type)
111
+ if (gateCriterion) {
112
+ if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
113
+ return buildTypeGateRefusal({
114
+ criterion: gateCriterion, received: args.type, declared: reviewObject?.type ?? null,
115
+ scope: scopeSummary(args.documents?.length ? args.documents : args.paths),
116
+ })
117
+ }
118
+ const reviewType = args.type
119
+ const documents = args.documents || null
120
+ // Scope fallback: the runtime mutation record (zero git) covers guard-triggered
121
+ // reviews where the model did not pass explicit paths.
122
+ const paths = args.paths || (agent._touchedFiles?.length ? [...agent._touchedFiles] : null)
123
+
124
+ // Code review must have a scope — no implicit fallback.
125
+ if (reviewType !== "design" && !paths && !documents) {
126
+ if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
127
+ // F31:既有稳定前缀逐字(行首)+ 标识块尾随(单源 notice.mjs)。
128
+ return withIdentityLine(
129
+ "Advisor: no review scope specified. Provide paths (files/directories to review) or documents (acceptance criteria context).",
130
+ { type: reviewType, scope: "none", round: "—", criterion: "scope-missing" },
131
+ )
132
+ }
133
+
134
+ // Design review: the review scope must be documentation files. Classification
135
+ // comes from the manifest-declared review-target roots (M4 write-gate.mjs single
136
+ // authority — FR12: no directory-name hardcoding; a project whose docs live
137
+ // elsewhere declares them in PROJECT-MANIFEST.json docRoot).
138
+ if (reviewType === "design" && documents) {
139
+ const roots = resolveReviewTargetPaths(agent).map((r) => r.replace(/[\\/]/g, sep))
140
+ const invalidDocs = documents.filter((doc) => {
141
+ const n = normAbs(doc, agent.cwd).replace(/[\\/]/g, sep)
142
+ return !roots.some((r) => n === r || n.startsWith(r + sep))
143
+ })
144
+ if (invalidDocs.length > 0) {
145
+ if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
146
+ return withIdentityLine(
147
+ `Advisor: design review documents must be documentation files (per the project's conventions). Invalid: ${invalidDocs.join(", ")}`,
148
+ { type: reviewType, scope: scopeSummary(documents), round: "—", criterion: "scope-not-doc" },
149
+ )
150
+ }
151
+ }
152
+
153
+ // §11.2 (R13 — ruling ②-3 A): async gate. Depth-0 defaults to the
154
+ // background pool; depth>0 (eng-coder internal self-review) is ALWAYS sync —
155
+ // an explicit async:true there is rejected, the default never flips.
156
+ // depth 显式校验(归一形态——判定单点:`depth` 取一次 + `asyncRequested` 显式布尔,
157
+ // 判定表与原逐值等价):
158
+ // depth 0 + 未传/true ⇒ 异步(本层默认)/ depth 0 + false ⇒ 同步;
159
+ // depth>0(或直调方无 depth) + 显式 true ⇒ 拒;其余 ⇒ 同步。
160
+ // 「无 depth」= 无 dispatch 上下文的直调方(测试/legacy)——按同步处理。
161
+ const depth = ctx?.depth ?? null
162
+ const asyncRequested = args.async === true
163
+ if (asyncRequested && depth !== 0) {
164
+ // 拒发登记(与 cap/池满拒同款):评审未跑——不置 called/不耗轮次(record-results
165
+ // 的 REFUSED 契约——advisor 评审发现 #1:拒发不得静默满足 guard)。
166
+ if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
167
+ return "Advisor: async reviews are only available at depth 0 — the top-level session owns the background pool; inside a child (eng-coder self-review) reviews run synchronously. Call advisor again without async:true (or with async:false)."
168
+ }
169
+ const isAsync = asyncRequested || (depth === 0 && args.async !== false)
170
+
171
+ // Per-review instance resolution (§11.2 ③ — ruling ②-5 A): fix rounds
172
+ // continue the same reviewId (design = the doc-set instance's designId —
173
+ // slot/spawn continuity; code = the newest OPEN instance). The resolution
174
+ // scopes agent._advisorRound/_lastAdvisorOutput so the message builder and
175
+ // the run.mjs cap read THIS instance's round/prior (multi-review isolation).
176
+ const resolved = resolveAdvisorLaunch(agent, reviewType, { documents })
177
+ // §2.20.2 评审侧批次档门禁 + 实例键绑定(batch_segment 的唯一路径来源):
178
+ // 口径 = **「若传则须可读」**(空/不可读 → throw;不强制必传——无批次档的在途设计评审
179
+ // 零回归,N5);绑定落在 resolved.run(评审实例键,与 reviewType/round/designId 同族)——
180
+ // 并发设计评审各绑各档,不用单值会话态(§2.20.8 #6)。
181
+ if (args.batchDoc !== undefined && args.batchDoc !== null) {
182
+ resolved.run.batchDoc = resolveBatchDocPath(agent.cwd, args.batchDoc)
183
+ }
184
+ // Design token minted for EVERY design round — the reviewer echoes it only on
185
+ // a clean pass; on pass it is slotted under the instance's designId at settle
186
+ // (sync: right here; async: the settle callback — fix #2). A NEW instance
187
+ // gets a fresh designId; a continued fix round keeps the original one — and a
188
+ // same-scope re-review after a pass REUSES the session's id (F2h §29.1).
189
+ const designToken = reviewType === "design" ? generateDesignToken(agent) : null
190
+ const designId = reviewType === "design" ? resolved.designId : null
191
+
192
+ // 撤 cap 预检 / 撤停止预检(2026-09-18 用户裁定——ADVISOR-CONVERGENCE.md §3.1):本工具层
193
+ // **无任何按计数拒发**——第 6 次及以后的发起照常受理(轮次仅作提示词衰减与显示);失败路径
194
+ // 的出口 = 结算出口的失败结论块(F28/F29——两轨共用,ADVISOR-GUARDS.md §7)。
195
+
196
+ if (isAsync) {
197
+ const ack = launchAsyncAdvisor(agent, ctx, {
198
+ reviewType, documents, paths, object: reviewObject,
199
+ designToken, designId, run: resolved.run,
200
+ })
201
+ if (ack.error) {
202
+ // Pool-full refusal (②-6a — no queueing): the review did NOT launch — the
203
+ // model must not count it as "advisor called" (the guard keeps pushing).
204
+ if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
205
+ return ack.error
206
+ }
207
+ // Async-ack marker: recordToolResults must NOT do the launch-time
208
+ // accounting (called/round) for this call — the settle owns it.
209
+ if (ctx._toolCallId !== undefined) {
210
+ (agent._advisorAsyncAcks ??= new Set()).add(ctx._toolCallId)
211
+ }
212
+ // E(F17/§14.14 E-3c):设计评审点火回执追加冻结句(代码评审 ack 零改)——窗口下界以
213
+ // 可观察信号表达:报告送达 / 取消前,被审文档(含批次档)零写入。
214
+ const freezeNote = reviewType === "design"
215
+ ? ";D5 冻结窗口:被审文档(含批次档)在报告送达前零写入——在途写入会被拒绝,写入将使本轮结算为陈旧 (pass 不发 token)"
216
+ : ""
217
+ if (ack.queued) {
218
+ // ED-4(2026-09-16 · AGENT-LOOP-SUBAGENT.md §6.10):排队 ack——模型可见状态如实 queued +
219
+ // position(评审槽空自动启动——不误导模型等待即刻 digest)。
220
+ return JSON.stringify({
221
+ id: ack.id, kind: "advisor", status: "queued", position: ack.position,
222
+ reviewId: resolved.reviewId,
223
+ note: `评审已排队(第 ${ack.position} 位)——评审槽空自动启动,完成自动回来 (review queued at position ${ack.position} — it starts automatically when a pool slot frees; the report arrives in a digest turn automatically; pass this id to cancel if needed)` + freezeNote,
224
+ })
225
+ }
226
+ return JSON.stringify({
227
+ id: ack.id, kind: "advisor", status: "running",
228
+ reviewId: resolved.reviewId,
229
+ note: "评审已后台启动——完成自动回来 (review started in the background — the report arrives in a digest turn automatically; pass this id to cancel if needed)" + freezeNote,
230
+ })
231
+ }
232
+
233
+ // Sync path (depth>0 / explicit async:false / direct callers without depth):
234
+ // legacy blocking review. The design token/echo handling runs below; the
235
+ // instance accounting (round++ per completed attempt) lands in
236
+ // recordToolResults, marker-keyed by this tool call's id.
237
+ if (ctx._toolCallId !== undefined) {
238
+ (agent._advisorSyncCalls ??= new Map()).set(ctx._toolCallId, resolved.reviewId)
239
+ }
240
+ const result = await runAdvisorReview(agent, reviewType, {
241
+ onOutput: ctx.onOutput,
242
+ signal: ctx.signal,
243
+ // 同步路径的实例绑定传递(异步路径由池条目 run.batchDoc 取——run.mjs 自行解析)。
244
+ batchDoc: resolved.run.batchDoc ?? null,
245
+ }, designToken, documents, paths, reviewObject, designId)
246
+
247
+ // B 启动拒绝(§14.4 #2——稳定前缀):拒发登记(同池满 / cap 款——不置 called、不耗轮次),
248
+ // 可见报错照常返回(record-results 的 REFUSED 契约)。第 33 批:判定单点——同时供设计
249
+ // 失败分类复用(launchRefused ⇒ neutral——无尝试发生,§17.3 #1)。
250
+ const launchRefused = String(result).startsWith(ADVISOR_LAUNCH_REFUSAL_PREFIX)
251
+ if (launchRefused && ctx._toolCallId !== undefined) {
252
+ (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
253
+ }
254
+
255
+ if (reviewType === "design") {
256
+ // Design pass/fail settlement — token echo IS the verdict (prompt-enforced);
257
+ // no findings-table heuristics: a design with issues never carries the token.
258
+ // (session cleanup for design reviews is owned by runAdvisorReview)
259
+ // Multi-design slots (2026-09-01): store under this review's designId.
260
+ // DESIGN-TOKEN-SETTLEMENT D3 (2026-09-08): the single `_engDesignToken` mirror is
261
+ // retired — no mirror write here; the dispatch/spawn gates read the authoritative
262
+ // multi-slot Map (+ slot-file re-read). Slotting moved into settleDesignReview
263
+ // (shared with the async settle — fix #2).
264
+ const incomplete = advisorIncompleteMarker(result)
265
+ const settled = settleDesignReview(agent, resolved.run, designToken, result, { incomplete })
266
+ // 撤计数(2026-09-18 用户裁定):原同步面“分类落账”随会话级计数器整体退场(零载体——F28③);
267
+ // 失败结论块由**结算出口**产出(异步结算 `settleAdvisorRun`——两轨共用,ADVISOR-GUARDS.md §7)。
268
+ // F2e (§29.1): the sync prior mirror must not carry the raw echo the runner
269
+ // stored — overwrite with the clean settled form (exact-suffix truncation).
270
+ if (settled.passed) {
271
+ agent._lastAdvisorOutput = stripApprovedSuffix(settled.output, resolved.run.approvedSuffix)
272
+ } else if (incomplete) {
273
+ // 未完成 ⇒ 同步 prior 镜像覆写为清洗后输出(防未注册 token 进 prior——§14.3)。
274
+ agent._lastAdvisorOutput = settled.output
275
+ }
276
+ return settled.output
277
+ }
278
+ return result
279
+ },
280
+ }