thincoder 0.12.61 → 0.12.63

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 (230) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +63 -28
  4. package/package.json +6 -5
  5. package/src/acp/bridge.mjs +35 -15
  6. package/src/acp/client-caps.mjs +86 -0
  7. package/src/acp/ext.mjs +86 -0
  8. package/src/acp/handlers-session.mjs +240 -0
  9. package/src/acp/handlers-slots.mjs +196 -0
  10. package/src/acp/login.mjs +48 -0
  11. package/src/acp/session.mjs +6 -4
  12. package/src/acp.mjs +67 -371
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +59 -17
  15. package/src/cli/memory-command.mjs +3 -3
  16. package/src/cli/permission.mjs +4 -48
  17. package/src/cli/setup-wizard.mjs +1 -1
  18. package/src/completions.mjs +3 -1
  19. package/src/crash-reports.mjs +32 -10
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +88 -0
  22. package/src/prompt-injections.mjs +20 -0
  23. package/src/tui/agent-turn.mjs +40 -9
  24. package/src/tui/cmd-advisor.mjs +5 -5
  25. package/src/tui/cmd-clear.mjs +2 -0
  26. package/src/tui/cmd-config.mjs +8 -8
  27. package/src/tui/cmd-eng.mjs +25 -9
  28. package/src/tui/cmd-mcp.mjs +9 -8
  29. package/src/tui/cmd-model.mjs +1 -1
  30. package/src/tui/cmd-new.mjs +10 -5
  31. package/src/tui/cmd-reindex.mjs +1 -1
  32. package/src/tui/cmd-restore.mjs +2 -2
  33. package/src/tui/cmd-session.mjs +31 -4
  34. package/src/tui/cmd-skills.mjs +1 -1
  35. package/src/tui/cmd-think.mjs +22 -9
  36. package/src/tui/config-helpers.mjs +1 -1
  37. package/src/tui/display-budget.mjs +206 -0
  38. package/src/tui/index.mjs +40 -12
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-handler-search.mjs +9 -1
  41. package/src/tui/key-modes.mjs +9 -4
  42. package/src/tui/ledger-surface.mjs +85 -0
  43. package/src/tui/model-catalog.mjs +4 -4
  44. package/src/tui/model-picker.mjs +8 -7
  45. package/src/tui/mouse.mjs +11 -6
  46. package/src/tui/pickers.mjs +15 -2
  47. package/src/tui/render-conversation.mjs +1 -1
  48. package/src/tui/render-frame.mjs +17 -6
  49. package/src/tui/render-loop.mjs +1 -1
  50. package/src/tui/render-segments.mjs +3 -1
  51. package/src/tui/slash-commands.mjs +1 -1
  52. package/src/tui/startup.mjs +49 -17
  53. package/src/tui/subagent-blocks.mjs +21 -3
  54. package/src/tui/subagent-children.mjs +86 -14
  55. package/src/tui/subagent-freeze.mjs +80 -3
  56. package/src/tui/suspension-drive.mjs +48 -22
  57. package/src/tui/tool-args.mjs +5 -2
  58. package/src/tui/tool-display.mjs +16 -2
  59. package/src/tui/tool-events.mjs +64 -22
  60. package/src/tui/tui-lifecycle.mjs +9 -2
  61. package/src/tui/wizard.mjs +3 -3
  62. package/src/tui/wrapped-spawn.mjs +21 -5
  63. package/src/abort-provenance.mjs +0 -116
  64. package/src/advisor/citations.mjs +0 -139
  65. package/src/advisor/compaction.mjs +0 -174
  66. package/src/advisor/convergence.mjs +0 -80
  67. package/src/advisor/history.mjs +0 -77
  68. package/src/advisor/loop.mjs +0 -293
  69. package/src/advisor/messages.mjs +0 -299
  70. package/src/advisor/project-context.mjs +0 -194
  71. package/src/advisor/repos.mjs +0 -150
  72. package/src/advisor/run.mjs +0 -293
  73. package/src/advisor/truncate.mjs +0 -57
  74. package/src/advisor.mjs +0 -290
  75. package/src/agent/completion.mjs +0 -146
  76. package/src/agent/dispatch.mjs +0 -489
  77. package/src/agent/helpers.mjs +0 -384
  78. package/src/agent/post-turn.mjs +0 -70
  79. package/src/agent/record-results.mjs +0 -174
  80. package/src/agent/relay-prefix.mjs +0 -39
  81. package/src/agent/run-stages.mjs +0 -242
  82. package/src/agent/setup-reminders.mjs +0 -69
  83. package/src/agent/setup.mjs +0 -354
  84. package/src/agent/spawn-child.mjs +0 -228
  85. package/src/agent-tools/advisor-async.mjs +0 -346
  86. package/src/agent-tools/advisor-settle.mjs +0 -231
  87. package/src/agent-tools/advisor.mjs +0 -260
  88. package/src/agent-tools/async-settle.mjs +0 -191
  89. package/src/agent-tools/batch-segment.mjs +0 -195
  90. package/src/agent-tools/consult.mjs +0 -468
  91. package/src/agent-tools/design-token.mjs +0 -117
  92. package/src/agent-tools/digest-budget.mjs +0 -76
  93. package/src/agent-tools/eng.mjs +0 -67
  94. package/src/agent-tools/escalate-async.mjs +0 -289
  95. package/src/agent-tools/goal.mjs +0 -119
  96. package/src/agent-tools/plan.mjs +0 -81
  97. package/src/agent-tools/read-history.mjs +0 -294
  98. package/src/agent-tools/recent-changes.mjs +0 -24
  99. package/src/agent-tools/review-streak.mjs +0 -93
  100. package/src/agent-tools/settings.mjs +0 -265
  101. package/src/agent-tools/skill.mjs +0 -47
  102. package/src/agent-tools/subagent-actions.mjs +0 -479
  103. package/src/agent-tools/subagent-async.mjs +0 -434
  104. package/src/agent-tools/subagent-panel.mjs +0 -160
  105. package/src/agent-tools/subagent-run.mjs +0 -205
  106. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  107. package/src/agent-tools/subagent-spawn.mjs +0 -453
  108. package/src/agent-tools/subagent.mjs +0 -404
  109. package/src/agent-tools/task.mjs +0 -87
  110. package/src/agent-tools/timer.mjs +0 -46
  111. package/src/agent-tools/verify.mjs +0 -271
  112. package/src/agent-tools.mjs +0 -17
  113. package/src/agent.mjs +0 -413
  114. package/src/auto-think.mjs +0 -115
  115. package/src/config-migrate.mjs +0 -70
  116. package/src/config.mjs +0 -496
  117. package/src/context.mjs +0 -381
  118. package/src/conventions.mjs +0 -223
  119. package/src/embedding.mjs +0 -120
  120. package/src/escape.mjs +0 -152
  121. package/src/expand-home.mjs +0 -16
  122. package/src/explore-distill.mjs +0 -155
  123. package/src/generate-title.mjs +0 -83
  124. package/src/git/checkpoint.mjs +0 -448
  125. package/src/git/gitmem.mjs +0 -100
  126. package/src/hooks.mjs +0 -97
  127. package/src/log.mjs +0 -195
  128. package/src/markdown.mjs +0 -106
  129. package/src/mcp/helpers.mjs +0 -51
  130. package/src/mcp/transport-http.mjs +0 -248
  131. package/src/mcp/transport-stdio.mjs +0 -140
  132. package/src/mcp/transport-ws.mjs +0 -122
  133. package/src/mcp.mjs +0 -295
  134. package/src/memory/code-index.mjs +0 -219
  135. package/src/memory/code-sync.mjs +0 -413
  136. package/src/memory/core.mjs +0 -300
  137. package/src/memory/delete.mjs +0 -236
  138. package/src/memory/docs.mjs +0 -417
  139. package/src/memory/file-walk.mjs +0 -109
  140. package/src/memory/schema.mjs +0 -452
  141. package/src/memory.mjs +0 -21
  142. package/src/model-ref.mjs +0 -66
  143. package/src/model-specs.mjs +0 -179
  144. package/src/peer-domains.mjs +0 -265
  145. package/src/peer-instances.mjs +0 -231
  146. package/src/prompt-overlays.mjs +0 -82
  147. package/src/prompts/advisor-design.md +0 -41
  148. package/src/prompts/advisor-round1.md +0 -41
  149. package/src/prompts/advisor-round2.md +0 -46
  150. package/src/prompts/advisor-round3.md +0 -42
  151. package/src/prompts/common.md +0 -115
  152. package/src/prompts/consult-base.md +0 -19
  153. package/src/prompts/discipline-engineering.md +0 -217
  154. package/src/prompts/discipline-normal.md +0 -179
  155. package/src/prompts/persona-coder.md +0 -21
  156. package/src/prompts/persona-eng-coder.md +0 -37
  157. package/src/prompts/persona-eng-designer.md +0 -55
  158. package/src/prompts/persona-engineering.md +0 -54
  159. package/src/prompts/persona-explore.md +0 -15
  160. package/src/prompts/persona-normal.md +0 -27
  161. package/src/prompts/persona-plan.md +0 -26
  162. package/src/provider/anthropic.mjs +0 -225
  163. package/src/provider/core.mjs +0 -476
  164. package/src/provider/errors.mjs +0 -101
  165. package/src/provider/google.mjs +0 -257
  166. package/src/provider/index.mjs +0 -7
  167. package/src/provider/list-models.mjs +0 -93
  168. package/src/provider/normalize.mjs +0 -81
  169. package/src/provider/rate.mjs +0 -108
  170. package/src/provider/responses.mjs +0 -495
  171. package/src/provider/retry.mjs +0 -88
  172. package/src/provider/sse.mjs +0 -264
  173. package/src/proxy.mjs +0 -261
  174. package/src/rules.mjs +0 -53
  175. package/src/session-gc.mjs +0 -214
  176. package/src/session-guard.mjs +0 -47
  177. package/src/session-migrate.mjs +0 -48
  178. package/src/session-rename.mjs +0 -38
  179. package/src/session-slots.mjs +0 -489
  180. package/src/session.mjs +0 -475
  181. package/src/skills.mjs +0 -153
  182. package/src/token-ttl.mjs +0 -274
  183. package/src/tools/apply_patch.md +0 -15
  184. package/src/tools/bash.md +0 -37
  185. package/src/tools/bash.mjs +0 -268
  186. package/src/tools/checklist-sync.mjs +0 -181
  187. package/src/tools/checklist.md +0 -13
  188. package/src/tools/checklist.mjs +0 -299
  189. package/src/tools/delete.md +0 -13
  190. package/src/tools/edit-batch.mjs +0 -191
  191. package/src/tools/edit-diff.mjs +0 -348
  192. package/src/tools/edit.md +0 -30
  193. package/src/tools/execute.md +0 -21
  194. package/src/tools/execute.mjs +0 -228
  195. package/src/tools/fetch.md +0 -12
  196. package/src/tools/file.mjs +0 -469
  197. package/src/tools/file_ops.md +0 -17
  198. package/src/tools/get_current_time.md +0 -8
  199. package/src/tools/git-checkpoint.mjs +0 -143
  200. package/src/tools/git-ext.mjs +0 -173
  201. package/src/tools/git.md +0 -54
  202. package/src/tools/git.mjs +0 -356
  203. package/src/tools/glob-dialect.mjs +0 -130
  204. package/src/tools/glob.md +0 -11
  205. package/src/tools/grep.md +0 -19
  206. package/src/tools/hashline_edit.md +0 -14
  207. package/src/tools/index.mjs +0 -36
  208. package/src/tools/insert_after.md +0 -15
  209. package/src/tools/lint.md +0 -10
  210. package/src/tools/linter.mjs +0 -128
  211. package/src/tools/ls.md +0 -12
  212. package/src/tools/lsp.md +0 -10
  213. package/src/tools/lsp.mjs +0 -316
  214. package/src/tools/ops.mjs +0 -299
  215. package/src/tools/patch.mjs +0 -282
  216. package/src/tools/process.md +0 -10
  217. package/src/tools/question.md +0 -16
  218. package/src/tools/question.mjs +0 -26
  219. package/src/tools/read.md +0 -20
  220. package/src/tools/read_image.md +0 -8
  221. package/src/tools/repomap.mjs +0 -314
  222. package/src/tools/search.mjs +0 -236
  223. package/src/tools/shared.mjs +0 -446
  224. package/src/tools/tree.md +0 -14
  225. package/src/tools/tree.mjs +0 -66
  226. package/src/tools/wait_for.md +0 -22
  227. package/src/tools/web.mjs +0 -224
  228. package/src/tools/websearch.md +0 -16
  229. package/src/tools/write.md +0 -11
  230. package/src/traces/trace-store.mjs +0 -224
@@ -1,260 +0,0 @@
1
- /**
2
- * agent-tools/advisor.mjs — advisor tool wrapper.
3
- * The agent calls this explicitly to get an independent review.
4
- * type="design" for design doc review, type="code" for code review (default).
5
- * §11.2 (R13 — async advisor): at depth 0 the review launches into the
6
- * background pool by DEFAULT (async:true / omitted; async:false forces the
7
- * blocking review); depth>0 (eng-coder self-review) stays synchronous always.
8
- */
9
- import { runAdvisorReview, MAX_ADVISOR_ROUNDS, buildCapMessage, advisorIncompleteMarker, ADVISOR_LAUNCH_REFUSAL_PREFIX, buildDesignReviewGuardMessage } from "../advisor/run.mjs"
10
- import { resolveBatchDocPath } from "./batch-segment.mjs"
11
- import { isDocPath, loadConventions } from "../conventions.mjs"
12
- import {
13
- generateDesignToken,
14
- settleDesignReview,
15
- resolveAdvisorLaunch,
16
- launchAsyncAdvisor,
17
- stripApprovedSuffix,
18
- } from "./advisor-async.mjs"
19
- // 第 33 批(§17.5):护栏消费面——工具层预检(检查点 1)+ 同步面计数(计数点 2)。
20
- import {
21
- designReviewOutcome, designReviewStreakRecord, designReviewStreakStopped, noteDesignReviewOutcome,
22
- } from "./review-streak.mjs"
23
-
24
- // Design-token utilities moved to advisor-async.mjs (the async settle shares
25
- // them — no wrapper↔runner module cycle); validateDesignToken stays exported
26
- // here for the tests' import surface (implementation re-exported).
27
- export { validateDesignToken } from "./advisor-async.mjs"
28
-
29
- export const advisorTool = {
30
- name: "advisor",
31
- description:
32
- "Run an independent review on your work. " +
33
- "Use type='design' to review design documents before implementation — pass documents=[...] with the explicit list of doc paths to review; use documents in code review too (the task's Docs involved list). " +
34
- "Use type='code' (default) to review code changes after implementation — pass paths=[...] to specify which files or directories to review, or documents=[...] for acceptance criteria context. " +
35
- "The advisor is an independent read-only sub-agent that explores the codebase, " +
36
- "reads files, and traces callers via grep/lsp. " +
37
- "For code review: round 1 does a full review, round 2 verifies the agent's fix claims, " +
38
- "round 3+ strictly checks only the fix claims — convergence, not divergence. " +
39
- "For design review: single-pass review against methodology and requirements. " +
40
- "Review criteria come from .thincoder/advisor.md (if present) or sensible defaults. " +
41
- "After the review, you MUST produce a response table (see discipline rules for format). " +
42
- "If advisor says all clear, call verify. " +
43
- "Optionally pass object={type,target,status,reason,exclude} to anchor the review target " +
44
- "(AGENT-LOOP.md §18.8 — the review-object declaration is mechanically injected into the review message); " +
45
- "absent → legacy behavior (no injection). " +
46
- "ASYNC (AGENT-LOOP.md §11.2): at depth 0 the review runs in the BACKGROUND by default " +
47
- "(async:true or omitted) — the call returns an ack immediately, the turn ends, and the report " +
48
- "arrives automatically in a digest turn when the review finishes; background reviews share one pool — " +
49
- "at most agent.poolLimits.advisor concurrent reviews (default 4 — configurable via /config 并发池 or " +
50
- "config.json; pool-full and same-scope refusals state the current limit) — launch reviews one at a time. " +
51
- "Inside a child (depth>0 — eng-coder self-review) " +
52
- "reviews are always synchronous; async:true is rejected there. " +
53
- "Returns the review report — the advisor's findings verdict: all-clear (call verify) or a findings list to fix.",
54
- parameters: {
55
- type: "object",
56
- properties: {
57
- type: { type: "string", enum: ["code", "design"], description: "Review type: 'design' for design doc review, 'code' for code review (default)" },
58
- async: {
59
- type: "boolean",
60
- 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).",
61
- },
62
- object: {
63
- type: "object",
64
- properties: {
65
- type: { type: "string", description: "Review type as declared by the caller (design/code)" },
66
- target: { type: "string", description: "Review target — document + section, or file(s)" },
67
- status: { type: "string", description: "Object state: 待评审 / 已批准 / 已实现 (pending-review / approved / implemented)" },
68
- reason: { type: "string", description: "Why this review runs: user-initiated / delivery verification" },
69
- exclude: { type: "string", description: "Explicit exclusion list — approved/implemented items NOT in this review" },
70
- },
71
- description: "Review-object declaration (§18.8): 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).",
72
- },
73
- paths: {
74
- type: "array",
75
- items: { type: "string" },
76
- 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.",
77
- },
78
- documents: {
79
- type: "array",
80
- items: { type: "string" },
81
- 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.",
82
- },
83
- batchDoc: {
84
- type: "string",
85
- 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 (ENGINEERING-MODE.md §2.20). Omit when no batch record is in flight — the review then runs unchanged with no write channel (zero regression).",
86
- },
87
- },
88
- },
89
- readonly: true,
90
- sideEffectExempt: true,
91
- outputPanel: true,
92
- async execute(args, ctx) {
93
- const agent = ctx.agent
94
- const reviewType = args.type || "code"
95
- const documents = args.documents || null
96
- // Review-object declaration (§18.8 D-OA3): the PARENT constructs it and the
97
- // advisor tool passes it through — mechanical anchoring, not model inference.
98
- // Any non-object value (string/array/primitive, possibly from a malformed
99
- // tool call) degrades to null = no injection (legacy calls unchanged).
100
- const reviewObject = args.object && typeof args.object === "object" && !Array.isArray(args.object)
101
- ? args.object
102
- : null
103
- // Scope fallback: the runtime mutation record (zero git) covers guard-triggered
104
- // reviews where the model did not pass explicit paths.
105
- const paths = args.paths || (agent._touchedFiles?.length ? [...agent._touchedFiles] : null)
106
-
107
- // Code review must have a scope — no implicit fallback.
108
- if (reviewType !== "design" && !paths && !documents) {
109
- if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
110
- return "Advisor: no review scope specified. Provide paths (files/directories to review) or documents (acceptance criteria context)."
111
- }
112
-
113
- // Design review: the review scope must be documentation files. Classification
114
- // comes from the single authority (src/conventions.mjs) — the old `docs/`
115
- // prefix test is retired with it (FR12: no directory-name hardcoding in the
116
- // gate; a project whose docs live elsewhere just declares its code paths).
117
- if (reviewType === "design" && documents) {
118
- const conv = loadConventions(agent.cwd)
119
- const invalidDocs = documents.filter((doc) => !isDocPath(doc, conv))
120
- if (invalidDocs.length > 0) {
121
- if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
122
- return `Advisor: design review documents must be documentation files (per the project's conventions). Invalid: ${invalidDocs.join(", ")}`
123
- }
124
- }
125
-
126
- // §11.2 (R13 — ruling ②-3 A): async gate. Depth-0 defaults to the
127
- // background pool; depth>0 (eng-coder internal self-review) is ALWAYS sync —
128
- // an explicit async:true there is rejected, the default never flips.
129
- const depth = ctx?.depth
130
- if (args.async === true && depth !== 0) {
131
- // 拒发登记(与 cap/池满拒同款):评审未跑——不置 called/不耗轮次(record-results
132
- // 的 REFUSED 契约——advisor 评审发现 #1:拒发不得静默满足 guard)。
133
- if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
134
- return "Advisor: async reviews are only available at depth 0 — the top-level session owns the background pool (AGENT-LOOP.md §11.2); inside a child (eng-coder self-review) reviews run synchronously. Call advisor again without async:true (or with async:false)."
135
- }
136
- const isAsync = args.async === true || (depth === 0 && args.async !== false)
137
- // (ctx.depth undefined = direct callers/tests without a dispatch context —
138
- // legacy sync semantics.)
139
-
140
- // Per-review instance resolution (§11.2 ③ — ruling ②-5 A): fix rounds
141
- // continue the same reviewId (design = the doc-set instance's designId —
142
- // slot/spawn continuity; code = the newest OPEN instance). The resolution
143
- // scopes agent._advisorRound/_lastAdvisorOutput so the message builder and
144
- // the run.mjs cap read THIS instance's round/prior (multi-review isolation).
145
- const resolved = resolveAdvisorLaunch(agent, reviewType, { documents })
146
- // §2.20.2 评审侧批次档门禁 + 实例键绑定(batch_segment 的唯一路径来源):
147
- // 口径 = **「若传则须可读」**(空/不可读 → throw;不强制必传——无批次档的在途设计评审
148
- // 零回归,N5);绑定落在 resolved.run(评审实例键,与 reviewType/round/designId 同族)——
149
- // 并发设计评审各绑各档,不用单值会话态(§2.20.8 #6)。
150
- if (args.batchDoc !== undefined && args.batchDoc !== null) {
151
- resolved.run.batchDoc = resolveBatchDocPath(agent.cwd, args.batchDoc)
152
- }
153
- // Design token minted for EVERY design round — the reviewer echoes it only on
154
- // a clean pass; on pass it is slotted under the instance's designId at settle
155
- // (sync: right here; async: the settle callback — fix #2). A NEW instance
156
- // gets a fresh designId; a continued fix round keeps the original one — and a
157
- // same-scope re-review after a pass REUSES the session's id (F2h §29.1).
158
- const designToken = reviewType === "design" ? generateDesignToken(agent) : null
159
- const designId = reviewType === "design" ? resolved.designId : null
160
-
161
- // Cap pre-check (T-24b11 — per-review ≤5 rounds, CODE REVIEWS ONLY): a 6th
162
- // launch of a capped CODE instance is refused synchronously — the review
163
- // never starts (sync and async alike; runAdvisorReview's own cap check stays
164
- // for legacy direct callers). DESIGN reviews are EXEMPT (2026-09-07 §8
165
- // ruling): their rounds keep advancing (ROUND2/3 convergence prompts + TUI
166
- // round display) but the cap never refuses them. The refusal marks no
167
- // called/round state (guard keeps pushing only while a review can still run
168
- // — at the cap the round check stops it).
169
- if (resolved.run.reviewType !== "design" && resolved.run.round >= MAX_ADVISOR_ROUNDS) {
170
- if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
171
- return buildCapMessage(agent)
172
- }
173
-
174
- // 第 33 批(§17.5 检查点 1——工具层预检;cap 预检邻位 / sync / async 分叉前):同一 doc-set
175
- // 连续 `MAX_DESIGN_REVIEW_STREAK` 次未产出可用结算 ⇒ 拒发(登记 `_advisorRefusals` 同 cap /
176
- // 池满款——不置 called / 不耗轮次 / **零 LLM**),返回结论串。
177
- if (reviewType === "design" && designReviewStreakStopped(agent, resolved.run.docSetKey)) {
178
- if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
179
- return buildDesignReviewGuardMessage(designReviewStreakRecord(agent, resolved.run.docSetKey), documents ?? [])
180
- }
181
-
182
- if (isAsync) {
183
- const ack = launchAsyncAdvisor(agent, ctx, {
184
- reviewType, documents, paths, object: reviewObject,
185
- designToken, designId, run: resolved.run,
186
- })
187
- if (ack.error) {
188
- // Pool-full refusal (②-6a — no queueing): the review did NOT launch — the
189
- // model must not count it as "advisor called" (the guard keeps pushing).
190
- if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
191
- return ack.error
192
- }
193
- // Async-ack marker: recordToolResults must NOT do the launch-time
194
- // accounting (called/round) for this call — the settle owns it.
195
- if (ctx._toolCallId !== undefined) {
196
- (agent._advisorAsyncAcks ??= new Set()).add(ctx._toolCallId)
197
- }
198
- // E(F17/§14.14 E-3c):设计评审点火回执追加冻结句(代码评审 ack 零改)——窗口下界以
199
- // 可观察信号表达:报告送达 / 取消前,被审文档(含批次档)零写入。
200
- const freezeNote = reviewType === "design"
201
- ? ";D5 冻结窗口:被审文档(含批次档)在报告送达前零写入——在途写入会被拒绝,写入将使本轮结算为陈旧 (pass 不发 token)"
202
- : ""
203
- return JSON.stringify({
204
- id: ack.id, kind: "advisor", status: "running",
205
- reviewId: resolved.reviewId,
206
- note: "评审已后台启动——完成自动回来 (review started in the background — the report arrives in a digest turn automatically; pass this id to cancel if needed)" + freezeNote,
207
- })
208
- }
209
-
210
- // Sync path (depth>0 / explicit async:false / direct callers without depth):
211
- // legacy blocking review. The design token/echo handling runs below; the
212
- // instance accounting (round++ per completed attempt) lands in
213
- // recordToolResults, marker-keyed by this tool call's id.
214
- if (ctx._toolCallId !== undefined) {
215
- (agent._advisorSyncCalls ??= new Map()).set(ctx._toolCallId, resolved.reviewId)
216
- }
217
- const result = await runAdvisorReview(agent, reviewType, {
218
- onOutput: ctx.onOutput,
219
- signal: ctx.signal,
220
- // 同步路径的实例绑定传递(异步路径由池条目 run.batchDoc 取——run.mjs 自行解析)。
221
- batchDoc: resolved.run.batchDoc ?? null,
222
- }, designToken, documents, paths, reviewObject, designId)
223
-
224
- // B 启动拒绝(§14.4 #2——稳定前缀):拒发登记(同池满 / cap 款——不置 called、不耗轮次),
225
- // 可见报错照常返回(record-results 的 REFUSED 契约)。第 33 批:判定单点——同时供设计
226
- // 失败分类复用(launchRefused ⇒ neutral——无尝试发生,§17.3 #1)。
227
- const launchRefused = String(result).startsWith(ADVISOR_LAUNCH_REFUSAL_PREFIX)
228
- if (launchRefused && ctx._toolCallId !== undefined) {
229
- (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
230
- }
231
-
232
- if (reviewType === "design") {
233
- // Design pass/fail settlement — token echo IS the verdict (prompt-enforced);
234
- // no findings-table heuristics: a design with issues never carries the token.
235
- // (session cleanup for design reviews is owned by runAdvisorReview)
236
- // Multi-design slots (2026-09-01): store under this review's designId.
237
- // DESIGN-TOKEN-SETTLEMENT D3 (2026-09-08): the single `_engDesignToken` mirror is
238
- // retired — no mirror write here; the dispatch/spawn gates read the authoritative
239
- // multi-slot Map (+ slot-file re-read). Slotting moved into settleDesignReview
240
- // (shared with the async settle — fix #2).
241
- const incomplete = advisorIncompleteMarker(result)
242
- const settled = settleDesignReview(agent, resolved.run, designToken, result, { incomplete })
243
- // 第 33 批(§17.5 计数点 2——同步面):同分类单源落账(同步面无 stale / 无落盘步骤
244
- // ⇒ persistFailed 恒 false)。
245
- noteDesignReviewOutcome(agent, resolved.run.docSetKey, designReviewOutcome({
246
- launchRefused, stale: false, hasResult: result != null, incomplete, persistFailed: false,
247
- }))
248
- // F2e (§29.1): the sync prior mirror must not carry the raw echo the runner
249
- // stored — overwrite with the clean settled form (exact-suffix truncation).
250
- if (settled.passed) {
251
- agent._lastAdvisorOutput = stripApprovedSuffix(settled.output, resolved.run.approvedSuffix)
252
- } else if (incomplete) {
253
- // 未完成 ⇒ 同步 prior 镜像覆写为清洗后输出(防未注册 token 进 prior——§14.3)。
254
- agent._lastAdvisorOutput = settled.output
255
- }
256
- return settled.output
257
- }
258
- return result
259
- },
260
- }
@@ -1,191 +0,0 @@
1
- /**
2
- * async-settle.mjs — async 结果容器统一共享 helper(ASYNC-RESULT-CONTAINER.md D1-D6,
3
- * 2026-09-08——CLI 端落地;VS Code 同名机制各自独立实现)。根治四族 settle 记账逐字
4
- * 重复(subagent/advisor/escalate/consult)+ pending 三族分叉(_pendingEscalateResults/
5
- * _pendingConsultResults 独立族废弃)+ done-in-pool 三表示 + _sessionSignal 兜底抄 3 处
6
- * (consult 无兜底):
7
- * - D1 池 accessor:`getAsyncPool(parent, role)`——role "advisor" 返 `_asyncAdvisors`,
8
- * 其余返 `_asyncSubagents`——底层保留双池(advisor 无队列独立调度),accessor 吸收差异。
9
- * - D2 pending 单容器:全族统一 `_pendingAsyncResults`,条目带 role 字段(consult 升格
10
- * 完整 entry);`parkAsyncPending` 统一停靠(`_inPending` 标记保留防重复移交——settle
11
- * 回调与 sweepSettledToPending 同一表示,done-in-pool 统一:留池 done:true + pending
12
- * 单容器)。
13
- * - D3 settle 共享 helper:`settleAsyncEntry(parent, entry, {pool, ctx, onAccounting})`——
14
- * 公共收尾单点(日志三连(ev:cancelled / child:done|error + ev:settled)→ cancelled /
15
- * parentAborted / 挂起分流 → settleSeq/`_settle` 唤醒 waiter)。族特有段作 onAccounting
16
- * hook 注入(advisor 陈旧判定/token D1 落盘记账;escalate 三分类 merge 决策 + 腾槽补位)。
17
- * - D4 守卫统一:`parentAborted`(= `ctx.signal?.aborted || entry.controller?.signal?.
18
- * aborted`——escalate 严格版)——日志三连与挂起分流共用同一守卫。
19
- * - D6 buildChildSignal:`parent._sessionSignal ?? ctx.signal ?? null` 单点(原 3 处
20
- * 逐字抄;consult 补 _sessionSignal 兜底——D5)。
21
- * 模块图:单向 import subagent-scheduler.mjs(dependentLabels——cancelled 提醒依赖者
22
- * 列表)+ log/context/helpers/spawn-child——四族 settle 回调(subagent-run/advisor-async/
23
- * escalate-async/consult)单向 import 本模块;scheduler 反向 import 本模块(getAsyncPool)
24
- * 与既有 scheduler ↔ subagent-async 同款惰性环——无求值期依赖。
25
- */
26
- import { logEvent, errText } from "../log.mjs"
27
- import { pushReal } from "../context.mjs"
28
- import { escapeXml } from "../agent/helpers.mjs"
29
- import { TURN_CAP_MARK } from "../agent/spawn-child.mjs"
30
- import { dependentLabels, maybeRefillAsync, refreshQueuedTokens } from "./subagent-scheduler.mjs"
31
-
32
- // ─── D1 池 accessor(吸收双池)────────────────────────────────────────────────
33
-
34
- /** 池 accessor(D1):role "advisor" → `_asyncAdvisors`(独立评审池——无队列独立调度);
35
- * 其余角色 → `_asyncSubagents`(子代理/飞刀共享槽位队列池)。未初始化返 null——调用方
36
- * 以 `?? new Map()` / 可选链处置。 */
37
- export function getAsyncPool(parent, role) {
38
- if (role === "advisor") return parent?._asyncAdvisors ?? null
39
- return parent?._asyncSubagents ?? null
40
- }
41
-
42
- // ─── D2 pending 单容器(+role)───────────────────────────────────────────────
43
-
44
- /** pending 单容器停靠(settle 挂起分流 / sweep 补扫共用——统一表示:条目置 `_inPending`
45
- * 防重复移交(sweep 幂等判据),includes 去重兜底)。 */
46
- export function parkAsyncPending(parent, entry) {
47
- const pend = (parent._pendingAsyncResults ??= [])
48
- if (pend.includes(entry)) return
49
- entry._inPending = true
50
- pend.push(entry)
51
- }
52
-
53
- // ─── D4 守卫统一(严格版)+ D6 信号兜底单点 ──────────────────────────────────
54
-
55
- /** D4 settle 守卫统一(escalate 严格版):父侧中止 = 回合 signal aborted 或条目
56
- * controller aborted(取消先行——cancelled 分支在守卫之前分流,定向 cancel 不算父中止)。
57
- * 日志三连与挂起分流共用本守卫。 */
58
- export function parentAborted(ctx, entry) {
59
- return Boolean(ctx?.signal?.aborted || entry?.controller?.signal?.aborted)
60
- }
61
-
62
- /** D6 child signal 构建单点(D5——consult 补 _sessionSignal 兜底):挂起会话内的回合
63
- * (digest/用户回合)子代理持会话 signal(`parent._sessionSignal`——会话 Stop 逐链中止,
64
- * 消化轮自身 Ctrl+I/Ctrl+C 不误伤);回合级 `ctx.signal` 兜底。原 subagent/advisor/
65
- * escalate 3 处逐字抄 + consult 补上统一入此。 */
66
- export function buildChildSignal(parent, ctx) {
67
- return parent?._sessionSignal ?? ctx?.signal ?? null
68
- }
69
-
70
- // ─── D3 settle 共享 helper ───────────────────────────────────────────────────
71
-
72
- /**
73
- * D3 settle 共享 helper——四族 settle 回调公共收尾单点(ASYNC-RESULT-CONTAINER.md
74
- * AC1:settle 记账单点,无逐字重复)。流程:
75
- * ① 落 done/status="done"(settle 即翻——不再占槽/持文件域;report/error 由各族
76
- * 在调用前落好——subagent 的未投递注记先于本调用附加);
77
- * ② 日志三连(统一守卫 !parentAborted——cancelled → ev:cancelled;非中止 →
78
- * child:done|:error(族形态:advisor 合并单事件;escalate 撞 cap 归 partial)+
79
- * 挂起期 ev:settled;consult 族跳过——per-model 日志已在子代理 settle 时记录);
80
- * ③ 分流:cancelled(出池 + cancelled 墓碑 + ⟦ev⟧stopped + 族提醒——不入 pending)→
81
- * parentAborted(守卫抑制——不落事件不入流——中止清池在回合尾/挂起中止统一做)→
82
- * settled(族 onAccounting hook 记账 → 挂起期(或 consult 族恒停靠)移交 pending
83
- * 单容器 + 出池 + ⟦ev⟧settled;回合内 ⟦ev⟧done 留池 done:true);
84
- * ④ 公共尾部:settleSeq 递增 + `_settle` 唤醒 waiter + 腾槽补位(subagent/escalate 族
85
- * 恒补——settle/cancel 释放槽 → maybeRefillAsync + refreshQueuedTokens——AGENT-LOOP.md
86
- * §10 "settle/cancel 释放槽后…启动到槽满";advisor(独立评审池——无队列)/consult
87
- * (会话池)豁免)。
88
- * opts(设计签名 `{pool, onAccounting}` + 实现参数 ctx):
89
- * - pool:条目所在池 Map(出池 delete 目标;consult 传 null——会话池无条目)。
90
- * - ctx:回合上下文(守卫读 ctx.signal;⟦ev⟧ 事件经 ctx.callbacks.onToken;腾槽补位
91
- * 的 refreshQueuedTokens 也经 ctx.callbacks.onToken;consult 传 null——无 TUI 事件
92
- * 无守卫)。
93
- * - onAccounting(parent, entry):族特有 hook——advisor 调 settleAdvisorRun 记账(D1
94
- * 落盘保留);escalate 调 classifyEscalateSettle(三分类 merge 决策)。
95
- * 设计偏差注(advisor code review 发现——交付报告同步):设计 D3 把 maybeRefillAsync
96
- * 归入 onAccounting hook(仅 settled 分支执行)——running 取消的 cancelled 分支将不再
97
- * 补位(槽释放但 queued 头停滞——挂起会话无回合尾补位覆盖),违背既有不变式
98
- * (AGENT-LOOP.md §10 + subagent-scheduler maybeRefillAsync 文档 "called from every
99
- * settle")与 VSC 镜像(无条件 refill)。修正:腾槽补位移入公共尾部(subagent/escalate
100
- * 族恒补——旧行为零回归;advisor/consult 豁免——同 VSC refill!==false 语义)。
101
- */
102
- export function settleAsyncEntry(parent, entry, opts = {}) {
103
- const { pool = null, ctx = null, onAccounting = null } = opts
104
- const role = entry.role ?? "subagent"
105
- entry.done = true
106
- entry.status = "done"
107
- const childLogId = `${role}#${entry.id}`
108
- const childMs = entry.startedAt ? Date.now() - entry.startedAt : 0
109
- const aborted = parentAborted(ctx, entry)
110
- // ② 日志三连(D4 统一守卫——中止不落错误事件——ev:stopped 已在中止清池点表达;
111
- // 定向 cancel 走 ev:cancelled)。族形态差异(LOGGING.md):
112
- // - advisor:合并单 child:done(kind error/ok);
113
- // - escalate:撞 cap 归 child:done partial(error 且非 capPartial 才 child:error);
114
- // - 其余(subagent 族):error → child:error;done kind 按报告撞 cap 判 partial。
115
- if (role !== "consult") {
116
- if (entry.cancelled) {
117
- logEvent("ev:cancelled", { id: childLogId })
118
- } else if (!aborted) {
119
- if (role === "advisor") {
120
- const err = entry.error != null ? errText(entry.error, 200) : null
121
- logEvent("child:done", { role, id: childLogId, ms: childMs, kind: err ? "error" : "ok" })
122
- } else if (role === "escalate") {
123
- const capPartial = String(entry.error ?? entry.report ?? "").includes(TURN_CAP_MARK)
124
- if (entry.error != null && !capPartial) logEvent("child:error", { role, id: childLogId, ms: childMs, err: errText(entry.error, 200) })
125
- else logEvent("child:done", { role, id: childLogId, ms: childMs, kind: capPartial ? "partial" : "ok" })
126
- } else {
127
- if (entry.error != null) logEvent("child:error", { role, id: childLogId, ms: childMs, err: errText(entry.error, 200) })
128
- else logEvent("child:done", { role, id: childLogId, ms: childMs, kind: String(entry.report ?? "").includes(TURN_CAP_MARK) ? "partial" : "ok" })
129
- }
130
- if (parent._suspended) logEvent("ev:settled", { id: childLogId, kind: "suspended" })
131
- }
132
- }
133
- // ③ 分流
134
- if (entry.cancelled) {
135
- // §19.5 cancelled settle 分支(D-M6):不入 pending、不参与回合尾直注入——清池 +
136
- // 终态墓碑(dependsOn 取消语义)+ ⟦ev⟧stopped 冻结 + 族提醒(半成品警示对模型可见)。
137
- pool?.delete(String(entry.id))
138
- const tombstones = (parent._asyncTombstones ??= new Map())
139
- tombstones.set(String(entry.id), { status: "cancelled", role })
140
- ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧stopped\x1e0\x1e0\x1estopped\x1e`)
141
- if (role === "advisor") {
142
- // ②-6b:评审取消——token 未签发提醒(settleAdvisorRun 不消费预算)。
143
- pushReal(parent, {
144
- role: "user",
145
- content: `[System reminder: async advisor review #${escapeXml(String(entry.id))} cancelled — the review did not settle; token not issued (评审已取消——token 未签发)]`,
146
- })
147
- } else if (role === "escalate") {
148
- pushReal(parent, {
149
- role: "user",
150
- content: `[System reminder: async escalate #${entry.id} (${entry.tag}) cancelled by user — partial changes not merged/audited]`,
151
- })
152
- } else {
153
- // §20 D-SD5:running 依赖取消——提醒列出依赖者(供模型决策)。
154
- const dependents = dependentLabels(parent, String(entry.id))
155
- const autoNote = parent.autoApprove
156
- ? " — AUTO session: they auto-start on slot availability (round2 #3)"
157
- : " — they stay queued until you cancel them or an AUTO session starts them"
158
- pushReal(parent, {
159
- role: "user",
160
- content: `[System reminder: subagent ${escapeXml(role)}#${entry.id} cancelled by user — partial changes not merged/audited${dependents.length > 0 ? `; queued dependents ${dependents.join(", ")} marked "dependency cancelled"${autoNote}` : ""}]`,
161
- })
162
- }
163
- } else if (!aborted) {
164
- // 族记账 hook(advisor 陈旧判定/token D1 落盘;escalate 三分类 merge 决策;subagent/
165
- // escalate 腾槽补位)——可改写 entry.report(settle 分支输出——digest 原样进)。
166
- onAccounting?.(parent, entry)
167
- // 挂起分流(§17 D-S8 + D-S3 记账——以读取时刻为准):挂起期 settle → 移交 pending
168
- // 单容器(digest 注入)+ 出池 + ⟦ev⟧settled 驻留;回合内 → ⟦ev⟧done 立即冻结
169
- // (条目留池——done-in-pool 统一表示——回合尾 collectSettledAsync 注入)。
170
- // consult 族恒停靠(settle 即出会话池入 pending——无 TUI 冻结事件——子块各自冻结)。
171
- if (parent._suspended || role === "consult") {
172
- parkAsyncPending(parent, entry)
173
- pool?.delete(String(entry.id))
174
- ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧settled\x1e0\x1e0\x1esettled\x1e`)
175
- } else {
176
- ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
177
- }
178
- }
179
- // ④ 公共尾部:settleSeq 递增 + _settle 唤醒(never rejects)+ 唤醒挂起驱动 waiter
180
- entry._settleSeq = (parent._asyncSettleSeq = (parent._asyncSettleSeq ?? 0) + 1)
181
- entry._settle?.()
182
- for (const w of parent._asyncWaiters?.splice(0) ?? []) { try { w() } catch { /* noop */ } }
183
- // §20 D-SD4 释放点:settle/cancel 释放槽 + 依赖终态转移 → 补位(依赖满足者/域冲突
184
- // 解除者自动启动——槽 ≤4)→ 排队态面板刷新(等待块头标注随终态更新——dependency
185
- // cancelled / 位置前移)。subagent/escalate 族恒补(旧行为零回归——cancelled 分支
186
- // 同样补位);advisor(独立评审池——无队列)/consult(会话池)豁免。
187
- if (role !== "advisor" && role !== "consult") {
188
- maybeRefillAsync(parent)
189
- refreshQueuedTokens(parent, ctx?.callbacks?.onToken)
190
- }
191
- }
@@ -1,195 +0,0 @@
1
- /**
2
- * agent-tools/batch-segment.mjs — 批次档段写入工具(ENGINEERING-MODE.md §2.20 · FR22 F1-F7)。
3
- *
4
- * 规则、守卫与 fail-closed 清单的**权威正文在 §2.20.1**(本文件不重述——D2 单一权威源);
5
- * 实现要点(正文未定死、由代码承载):
6
- * - 身份判据 = 评审实例绑定(`review` 形态)/ `agent._role`;
7
- * - append-only:在段尾(下一条 `## §N` 或 EOF)插入,既有行字节不变;
8
- * - 来源戳 `### 轮次 N(评审子代理)`(仅 §3)——N = §3 内该形态行计数 + 1,**调用方自带**
9
- * 的同名标题行被丢弃(否则伪造戳会污染 N 计数——§2.20.8 #5);
10
- * - 凭证剥除:含凭证形态的行剥掉该子串,剥后为空则整行丢弃(「零命中」+「其余逐字保留」);
11
- * - 路径门禁:`resolveBatchDocPath`(评审侧「若传则须可读」);工具内再查一次可读性。
12
- *
13
- * 导出面:`batchSegmentTool` · `resolveBatchDocPath` · `batchDocForReview`(异步评审实例键取绑定)。
14
- */
15
- import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
16
- import { resolve } from "node:path"
17
-
18
- /** text 单次上限(§2.20.1——超出引导分段追加,不承诺"不新盖戳")。 */
19
- export const MAX_TEXT_CHARS = 20000
20
- /** 身份 → 可写段号(§2.20.1 段白名单)。 */
21
- export const SEGMENT_BY_ROLE = { "eng-designer": 2, "eng-coder": 5 }
22
- /** 工具写入的轮次节标题形态(N 计数口径 = 该形态行;`### 轮次与发现(…)` 骨架行不匹配)。 */
23
- const ROUND_HEADING_RE = /^### 轮次 \d+(评审子代理)/
24
- /** 段标题定位(`## §N` 独立标题——`## §20` 不误命中 §2)。 */
25
- const sectionHeaderRe = (seg) => new RegExp(`^## §${seg}(?=\\s|$)`, "m")
26
- /** 凭证形态(§2.7 冒号态):`[DESIGN-TOKEN:…]` 与 `designId: …`——本节自有正则。 */
27
- const CRED_RE = /\[DESIGN-TOKEN:[^\]]*\]|designId\s*:\s*\S+/g
28
- const CRED_TEST_RE = /\[DESIGN-TOKEN:[^\]]*\]|designId\s*:\s*\S+/
29
-
30
- /** 可读文件判据(存在且为文件——目录/缺失同判不可读)。 */
31
- function readableFile(abs) {
32
- try { return existsSync(abs) && statSync(abs).isFile() } catch { return false }
33
- }
34
-
35
- /**
36
- * 批次档路径门禁(评审侧 §2.20.2 口径 = **「若传则须可读」**):空/非字符串/不可读 → throw。
37
- * 非空且可读 → 返回绝对路径(`\` 归一——照 `files`/`batchDoc` spawn 门先例)。
38
- */
39
- export function resolveBatchDocPath(cwd, given) {
40
- const raw = typeof given === "string" ? given.trim() : ""
41
- if (!raw) {
42
- throw new Error("batchDoc must be a non-empty path to the batch record (ENGINEERING-MODE.md §2.20.2) — pass the batch record currently in flight, or omit the parameter entirely when no batch record is in flight.")
43
- }
44
- const abs = resolve(cwd ?? process.cwd(), raw.replace(/\\/g, "/"))
45
- if (!readableFile(abs)) {
46
- throw new Error(`batchDoc is not a readable file: ${raw} — pass the path of the batch record currently in flight (a path that resolves to an existing file), or omit the parameter when no batch record is in flight.`)
47
- }
48
- return abs
49
- }
50
-
51
- /**
52
- * 设计评审的实例绑定解析(§2.20.2/§2.20.3——batch_segment 的唯一路径来源):
53
- * - 同步路径:调用方(advisor 工具)把实例绑定显式放进 callbacks(**带 batchDoc 键**,
54
- * 未绑定即 null)——以此为准,不再回看池条目(防同步/异步混跑时串档);
55
- * - 异步路径:本评审所在**池条目**的 `run.batchDoc`(文档集 = 实例键,各评审各取各条)。
56
- */
57
- export function batchDocForReview(agent, documents, callbacks = null) {
58
- if (callbacks && "batchDoc" in callbacks) return callbacks.batchDoc ?? null
59
- const key = JSON.stringify([...(documents ?? [])].sort())
60
- for (const e of agent?._asyncAdvisors?.values?.() ?? []) {
61
- if (e.status === "running" && e.reviewType === "design" && JSON.stringify([...(e.documents ?? [])].sort()) === key) {
62
- return e.run?.batchDoc ?? null
63
- }
64
- }
65
- return null
66
- }
67
-
68
- /** 段号解析:`§2` / `2` / `§2 批次任务` 均可;无法解析 → null。 */
69
- function segmentNumber(raw) {
70
- const m = /^§?\s*(\d+)/.exec(String(raw ?? "").trim())
71
- return m ? Number(m[1]) : null
72
- }
73
-
74
- /** 身份 → 可写段号;无写权身份 → null。评审实例绑定优先(评审者的身份即"设计评审"。) */
75
- function allowedSegment(agent, review) {
76
- if (review) return 3
77
- return SEGMENT_BY_ROLE[agent?._role] ?? null
78
- }
79
-
80
- /**
81
- * 凭证剥除 + 伪造轮次标题丢弃(§2.20.1 F6/AC30/AC34)。
82
- * 逐行处理:含凭证形态的行 → 剥除该子串,剥后为空则整行丢弃(其余内容逐字保留)。
83
- * `dropStamp`(仅 §3 目标段):调用方自带的 `### 轮次 N(评审子代理)` 行被丢弃——
84
- * 工具生成才是唯一来源戳(否则伪造戳会污染 N 计数)。
85
- */
86
- function sanitizeText(text, dropStamp) {
87
- const kept = []
88
- for (const line of text.replace(/\r\n?/g, "\n").split("\n")) {
89
- if (dropStamp && ROUND_HEADING_RE.test(line)) continue
90
- if (!CRED_TEST_RE.test(line)) { kept.push(line); continue }
91
- const cleaned = line.replace(CRED_RE, "").replace(/[ \t]+$/, "")
92
- if (cleaned.trim()) kept.push(cleaned)
93
- }
94
- return kept.join("\n")
95
- }
96
-
97
- /** §3 内工具写入的轮次行计数(N = 计数 + 1;收窄口径——只数工具戳形态)。 */
98
- function roundCount(src, headerMatch, endIdx) {
99
- const body = src.slice(headerMatch.index + headerMatch[0].length, endIdx)
100
- return body.split("\n").filter((l) => ROUND_HEADING_RE.test(l)).length
101
- }
102
-
103
- /** 段尾定位 + 插入(append-only:只在段尾插入,既有字节不变;返回新全文 + 本轮 N)。 */
104
- function insertIntoSection(src, seg, text) {
105
- const hdr = sectionHeaderRe(seg).exec(src)
106
- if (!hdr) {
107
- throw new Error(`batch_segment: the bound batch record has no "## §${seg}" section header — the six-section skeleton is written by the record's creator before any segment write (§1.12). Fix: ask the parent/creator to add the "## §${seg} …" heading (with the template's sub-headings) first, then call batch_segment again. Nothing was written.`)
108
- }
109
- const nextRe = /^## §\d/gm
110
- nextRe.lastIndex = hdr.index + hdr[0].length
111
- const next = nextRe.exec(src)
112
- const endIdx = next ? next.index : src.length
113
- // 来源戳(仅 §3):工具生成标题 + N——调用方写不进自己的标题(sanitize 已丢弃同名行)。
114
- const roundN = seg === 3 ? roundCount(src, hdr, endIdx) + 1 : 0
115
- const payload = (seg === 3 ? `### 轮次 ${roundN}(评审子代理)\n\n` : "") + text
116
- const before = src.slice(0, endIdx)
117
- const after = src.slice(endIdx)
118
- const eol = src.includes("\r\n") ? "\r\n" : "\n"
119
- const lead = before === "" ? "" : before.endsWith(eol + eol) ? "" : before.endsWith(eol) ? eol : eol + eol
120
- // 尾随 eol 恒加(块自成行);后面还有段标题时再补一个空行(保持模板的空行分隔)。
121
- const written = before + lead + payload.replaceAll("\n", eol) + eol + (after === "" ? "" : eol) + after
122
- return { written, roundN }
123
- }
124
-
125
- /**
126
- * 批次档段写入工具工厂。
127
- * @param {string|null} batchDoc — 绑定的目标档(spawn:`child._batchDoc`;评审:实例键)
128
- * @param {{review?: boolean}} [opts] — review=true 表示"设计评审实例"形态(写 §3 + 工具盖戳)
129
- */
130
- export function batchSegmentTool(batchDoc = null, { review = false } = {}) {
131
- const own = review ? "§3" : null
132
- return {
133
- name: "batch_segment",
134
- description:
135
- "Append your own section of the batch record (ENGINEERING-MODE.md §2.20 — 一段一作者). " +
136
- "There is NO path parameter: the target record is bound to you (at spawn for eng-designer/eng-coder, per review instance for a design review) and your identity fixes the section you may write " +
137
- "(eng-designer → §2, design review → §3, eng-coder → §5) — a write outside your own section is refused. " +
138
- "Append-only: the text lands at the end of your section; existing lines are never rewritten or deleted. " +
139
- "Credential values are stripped mechanically before writing (never write a token or designId value — §2.7). " +
140
- "A design review's append is stamped by the tool with a `### 轮次 N(评审子代理)` heading — N is tool-counted; do not write your own heading (it would be dropped). " +
141
- "Failures are hard and visible (no silent fallback): if the write is refused or fails, say so in your report — “§× 未写入”.",
142
- parameters: {
143
- type: "object",
144
- properties: {
145
- segment: {
146
- type: "string",
147
- description: `The batch-record section you are writing${own ? ` — yours is ${own}` : ""}. Declares the section number only; your identity decides what is actually writable (§2.20.1).`,
148
- },
149
- text: {
150
- type: "string",
151
- description: "The markdown to append (verbatim — findings table + VERDICT + counts for a design review §3). Limit 20000 characters per call; longer content is refused — split it into multiple calls (each call becomes its own section, N continues).",
152
- },
153
- },
154
- required: ["segment", "text"],
155
- },
156
- async execute(args, ctx) {
157
- const agent = ctx?.agent ?? {}
158
- const seg = allowedSegment(agent, review)
159
- if (seg === null) {
160
- throw new Error("batch_segment: no segment is writable by this caller — the channel exists for eng-designer (§2), eng-coder (§5) and design reviews bound to a batch record (§3); the parent agent writes §1/§4/§6 through ordinary document writes (ENGINEERING-MODE.md §2.20.1/§2.20.3).")
161
- }
162
- const n = segmentNumber(args?.segment)
163
- if (n === null) {
164
- throw new Error(`batch_segment: unknown segment ${JSON.stringify(args?.segment ?? null)} — pass the section number you write (e.g. "§${seg}").`)
165
- }
166
- if (n !== seg) {
167
- throw new Error(`batch_segment: §${n} is not yours to write — this caller writes §${seg} only (一段一作者: eng-designer → §2, design review → §3, eng-coder → §5; ENGINEERING-MODE.md §2.20.1).`)
168
- }
169
- if (!batchDoc) {
170
- throw new Error("batch_segment: no batch record is bound to this caller — there is no path parameter by design (the target arrives via the spawn binding / the review instance key, ENGINEERING-MODE.md §2.20.2). Report the section as not written.")
171
- }
172
- const abs = resolve(agent.cwd ?? process.cwd(), String(batchDoc).replace(/\\/g, "/"))
173
- if (!readableFile(abs)) {
174
- throw new Error(`batch_segment: the bound batch record is not a readable file: ${abs} — nothing was written (§2.20.1 fail-closed). Check the record still exists, then report the section as not written.`)
175
- }
176
- if (typeof args?.text !== "string") {
177
- throw new Error("batch_segment: text must be a string (the markdown to append).")
178
- }
179
- if (args.text.length > MAX_TEXT_CHARS) {
180
- throw new Error(`batch_segment: text is ${args.text.length} characters — the limit is ${MAX_TEXT_CHARS} per call. Split it into multiple calls: each call becomes its own section and the round number N continues (分段追加——每次调用各成节、N 顺延; ENGINEERING-MODE.md §2.20.1).`)
181
- }
182
- if (/^## §\d/m.test(args.text)) {
183
- throw new Error("batch_segment: the text contains a section header line matching `^## §N` — that would break section location and the append-only guarantee. Rewrite it (escape the heading, or drop the leading `## §N`), then call again (§2.20.1 骨架保护). Nothing was written.")
184
- }
185
- const body = sanitizeText(args.text, seg === 3).replace(/^\n+/, "").replace(/\s+$/, "")
186
- if (!body.trim()) {
187
- throw new Error("batch_segment: nothing to append — the text is empty after credential stripping (credential values never reach the record; ENGINEERING-MODE.md §2.7/§2.20.1).")
188
- }
189
- const src = readFileSync(abs, "utf8")
190
- const { written, roundN } = insertIntoSection(src, seg, body)
191
- writeFileSync(abs, written)
192
- return `batch_segment: appended ${body.length} characters to §${seg} of the batch record${roundN ? ` (### 轮次 ${roundN}(评审子代理))` : ""}.`
193
- },
194
- }
195
- }