thincoder 0.12.59 → 0.12.61

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 (192) hide show
  1. package/CHANGELOG.md +62 -4
  2. package/README.md +10 -8
  3. package/bin/thincoder.mjs +99 -133
  4. package/package.json +6 -4
  5. package/src/abort-provenance.mjs +116 -0
  6. package/src/acp/bridge.mjs +45 -21
  7. package/src/acp.mjs +6 -1
  8. package/src/advisor/citations.mjs +83 -21
  9. package/src/advisor/compaction.mjs +174 -0
  10. package/src/advisor/loop.mjs +293 -0
  11. package/src/advisor/messages.mjs +59 -137
  12. package/src/advisor/project-context.mjs +194 -0
  13. package/src/advisor/repos.mjs +17 -40
  14. package/src/advisor/run.mjs +156 -359
  15. package/src/advisor/truncate.mjs +57 -0
  16. package/src/advisor.mjs +27 -7
  17. package/src/agent/completion.mjs +17 -11
  18. package/src/agent/dispatch.mjs +145 -27
  19. package/src/agent/helpers.mjs +107 -13
  20. package/src/agent/record-results.mjs +55 -11
  21. package/src/agent/relay-prefix.mjs +39 -0
  22. package/src/agent/run-stages.mjs +242 -0
  23. package/src/agent/setup-reminders.mjs +69 -0
  24. package/src/agent/setup.mjs +107 -127
  25. package/src/agent/spawn-child.mjs +55 -13
  26. package/src/agent-tools/advisor-async.mjs +346 -0
  27. package/src/agent-tools/advisor-settle.mjs +231 -0
  28. package/src/agent-tools/advisor.mjs +167 -116
  29. package/src/agent-tools/async-settle.mjs +191 -0
  30. package/src/agent-tools/batch-segment.mjs +195 -0
  31. package/src/agent-tools/consult.mjs +139 -107
  32. package/src/agent-tools/design-token.mjs +117 -0
  33. package/src/agent-tools/digest-budget.mjs +76 -0
  34. package/src/agent-tools/eng.mjs +24 -29
  35. package/src/agent-tools/escalate-async.mjs +289 -0
  36. package/src/agent-tools/read-history.mjs +166 -32
  37. package/src/agent-tools/recent-changes.mjs +2 -1
  38. package/src/agent-tools/review-streak.mjs +93 -0
  39. package/src/agent-tools/settings.mjs +137 -34
  40. package/src/agent-tools/subagent-actions.mjs +180 -133
  41. package/src/agent-tools/subagent-async.mjs +184 -177
  42. package/src/agent-tools/subagent-panel.mjs +160 -0
  43. package/src/agent-tools/subagent-run.mjs +205 -0
  44. package/src/agent-tools/subagent-scheduler.mjs +100 -27
  45. package/src/agent-tools/subagent-spawn.mjs +453 -0
  46. package/src/agent-tools/subagent.mjs +256 -578
  47. package/src/agent-tools/verify.mjs +119 -292
  48. package/src/agent-tools.mjs +1 -0
  49. package/src/agent.mjs +89 -205
  50. package/src/cli/distill-command.mjs +12 -6
  51. package/src/cli/make-agent.mjs +26 -8
  52. package/src/cli/memory-command.mjs +4 -3
  53. package/src/cli/permission.mjs +2 -2
  54. package/src/cli/setup-wizard.mjs +42 -17
  55. package/src/completions.mjs +114 -0
  56. package/src/config-migrate.mjs +70 -0
  57. package/src/config.mjs +180 -63
  58. package/src/context.mjs +5 -147
  59. package/src/conventions.mjs +223 -0
  60. package/src/crash-reports.mjs +128 -0
  61. package/src/distill.mjs +11 -11
  62. package/src/expand-home.mjs +16 -0
  63. package/src/explore-distill.mjs +155 -0
  64. package/src/generate-title.mjs +1 -1
  65. package/src/hooks.mjs +7 -3
  66. package/src/memory/code-index.mjs +9 -3
  67. package/src/memory/code-sync.mjs +72 -32
  68. package/src/memory/core.mjs +6 -193
  69. package/src/memory/delete.mjs +236 -0
  70. package/src/memory/docs.mjs +68 -54
  71. package/src/memory/file-walk.mjs +109 -0
  72. package/src/memory/schema.mjs +15 -3
  73. package/src/memory.mjs +3 -1
  74. package/src/model-ref.mjs +66 -0
  75. package/src/model-specs.mjs +42 -8
  76. package/src/peer-domains.mjs +265 -0
  77. package/src/peer-instances.mjs +231 -0
  78. package/src/prompt-overlays.mjs +82 -0
  79. package/src/prompts/advisor-design.md +18 -75
  80. package/src/prompts/advisor-round1.md +14 -67
  81. package/src/prompts/advisor-round2.md +15 -51
  82. package/src/prompts/advisor-round3.md +15 -51
  83. package/src/prompts/common.md +115 -0
  84. package/src/prompts/consult-base.md +5 -23
  85. package/src/prompts/discipline-engineering.md +217 -0
  86. package/src/prompts/discipline-normal.md +179 -0
  87. package/src/prompts/persona-coder.md +21 -0
  88. package/src/prompts/persona-eng-coder.md +37 -0
  89. package/src/prompts/persona-eng-designer.md +55 -0
  90. package/src/prompts/persona-engineering.md +54 -0
  91. package/src/prompts/persona-explore.md +15 -0
  92. package/src/prompts/persona-normal.md +27 -0
  93. package/src/prompts/persona-plan.md +26 -0
  94. package/src/provider/anthropic.mjs +4 -4
  95. package/src/provider/core.mjs +18 -98
  96. package/src/provider/errors.mjs +101 -0
  97. package/src/provider/google.mjs +5 -6
  98. package/src/provider/index.mjs +2 -1
  99. package/src/provider/list-models.mjs +93 -0
  100. package/src/provider/rate.mjs +2 -1
  101. package/src/provider/responses.mjs +5 -3
  102. package/src/provider/retry.mjs +8 -45
  103. package/src/provider/sse.mjs +3 -4
  104. package/src/proxy.mjs +9 -14
  105. package/src/session-gc.mjs +214 -0
  106. package/src/session-guard.mjs +47 -0
  107. package/src/session-rename.mjs +38 -0
  108. package/src/session-slots.mjs +188 -60
  109. package/src/session.mjs +104 -124
  110. package/src/token-ttl.mjs +274 -0
  111. package/src/tools/{system.mjs → bash.mjs} +19 -221
  112. package/src/tools/checklist-sync.mjs +181 -0
  113. package/src/tools/checklist.mjs +52 -39
  114. package/src/tools/edit-batch.mjs +109 -10
  115. package/src/tools/edit-diff.mjs +110 -27
  116. package/src/tools/edit.md +17 -12
  117. package/src/tools/execute.mjs +31 -4
  118. package/src/tools/file.mjs +41 -16
  119. package/src/tools/git.md +1 -1
  120. package/src/tools/git.mjs +23 -34
  121. package/src/tools/glob-dialect.mjs +130 -0
  122. package/src/tools/glob.md +3 -3
  123. package/src/tools/grep.md +1 -1
  124. package/src/tools/index.mjs +9 -8
  125. package/src/tools/ops.mjs +188 -3
  126. package/src/tools/patch.mjs +3 -3
  127. package/src/tools/question.md +4 -0
  128. package/src/tools/question.mjs +26 -0
  129. package/src/tools/read.md +1 -2
  130. package/src/tools/read_image.md +1 -1
  131. package/src/tools/search.mjs +236 -0
  132. package/src/tools/shared.mjs +14 -13
  133. package/src/tools/wait_for.md +22 -0
  134. package/src/tui/agent-turn.mjs +36 -228
  135. package/src/tui/ansi.mjs +2 -0
  136. package/src/tui/clipboard.mjs +7 -1
  137. package/src/tui/cmd-advisor.mjs +3 -2
  138. package/src/tui/cmd-config.mjs +142 -30
  139. package/src/tui/cmd-eng.mjs +28 -40
  140. package/src/tui/cmd-exit.mjs +6 -8
  141. package/src/tui/cmd-mcp.mjs +8 -2
  142. package/src/tui/cmd-model.mjs +14 -12
  143. package/src/tui/cmd-new.mjs +3 -2
  144. package/src/tui/cmd-reindex.mjs +7 -0
  145. package/src/tui/cmd-session.mjs +19 -4
  146. package/src/tui/cmd-submodel.mjs +8 -5
  147. package/src/tui/cmd-think.mjs +10 -10
  148. package/src/tui/cmd-undo.mjs +4 -3
  149. package/src/tui/cmd-upgrade.mjs +19 -4
  150. package/src/tui/config-helpers.mjs +28 -16
  151. package/src/tui/distill-cmd.mjs +1 -1
  152. package/src/tui/index.mjs +40 -38
  153. package/src/tui/interaction.mjs +3 -3
  154. package/src/tui/key-handler.mjs +61 -17
  155. package/src/tui/key-modes.mjs +86 -8
  156. package/src/tui/layout.mjs +18 -10
  157. package/src/tui/model-catalog.mjs +89 -0
  158. package/src/tui/model-picker.mjs +498 -0
  159. package/src/tui/mouse.mjs +52 -9
  160. package/src/tui/pickers.mjs +28 -392
  161. package/src/tui/render-frame.mjs +32 -16
  162. package/src/tui/render-loop.mjs +2 -0
  163. package/src/tui/render-segments.mjs +12 -9
  164. package/src/tui/render.mjs +37 -5
  165. package/src/tui/slash-commands.mjs +2 -2
  166. package/src/tui/startup.mjs +4 -0
  167. package/src/tui/subagent-blocks.mjs +106 -295
  168. package/src/tui/subagent-children.mjs +162 -0
  169. package/src/tui/subagent-freeze.mjs +169 -0
  170. package/src/tui/subagent-panel.mjs +24 -31
  171. package/src/tui/suspension-drive.mjs +297 -0
  172. package/src/tui/tool-args.mjs +7 -5
  173. package/src/tui/tool-display.mjs +143 -0
  174. package/src/tui/tool-events.mjs +56 -185
  175. package/src/tui/tui-lifecycle.mjs +46 -4
  176. package/src/tui/update-notice.mjs +4 -0
  177. package/src/tui/wizard.mjs +61 -21
  178. package/src/tui/wrapped-spawn.mjs +38 -0
  179. package/src/prompts/coder.md +0 -56
  180. package/src/prompts/discipline.md +0 -102
  181. package/src/prompts/eng-coder.md +0 -44
  182. package/src/prompts/engineering-sub.md +0 -34
  183. package/src/prompts/engineering.md +0 -340
  184. package/src/prompts/explore.md +0 -21
  185. package/src/prompts/main.md +0 -56
  186. package/src/prompts/methodology-template.md +0 -58
  187. package/src/prompts/plan.md +0 -16
  188. package/src/prompts/system.md +0 -63
  189. package/src/tools/pdf-parse-text.mjs +0 -497
  190. package/src/tools/pdf-parse-xref.mjs +0 -499
  191. package/src/tools/pdf.mjs +0 -155
  192. package/src/tools/read_pdf.md +0 -21
@@ -2,67 +2,29 @@
2
2
  * agent-tools/advisor.mjs — advisor tool wrapper.
3
3
  * The agent calls this explicitly to get an independent review.
4
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.
5
8
  */
6
- import { randomUUID, createHmac } from "node:crypto"
7
- import { runAdvisorReview } from "../advisor/run.mjs"
8
- import { isDocFile } from "../advisor/repos.mjs"
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"
9
23
 
10
- const TOKEN_TTL_DEFAULT_MS = 7 * 24 * 3600 * 1000 // 7-day ceiling (v2 2026-08-25): multi-batch delivery must not re-review an unchanged design within a week; agent.engTokenTtlMs overrides
11
- const TOKEN_SECRET = process.env.THINCODER_TOKEN_SECRET || "thincoder-default-secret"
12
-
13
- /** Effective token TTL: config override with runtime validation (advisor timeoutMs precedent —
14
- * invalid values fall back to the default, never silently disable the ceiling). */
15
- function effectiveTokenTtlMs(agent) {
16
- const cfg = agent?.config?.agent?.engTokenTtlMs
17
- return (Number.isFinite(cfg) && cfg > 0) ? cfg : TOKEN_TTL_DEFAULT_MS
18
- }
19
-
20
- /** Generate a signed design token with expiration */
21
- function generateDesignToken(agent) {
22
- const uuid = randomUUID()
23
- const expiresAt = Date.now() + effectiveTokenTtlMs(agent)
24
- const payload = `${uuid}:${expiresAt}`
25
- const signature = createHmac("sha256", TOKEN_SECRET).update(payload).digest("hex").slice(0, 16)
26
- return `${payload}:${signature}`
27
- }
28
-
29
- /** Validate design token: format, expiration, signature — ALL fail-closed (v2 2026-08-25).
30
- * The two legacy fail-open branches (parts!=3 → true, NaN expiry → true) were pass-through
31
- * backdoors: any malformed string bypassed validation. Only exact signed tokens pass now.
32
- * Format must be exactly uuid:expiresAt:hmacSig. */
33
- export function validateDesignToken(token) {
34
- if (!token || typeof token !== "string") return false
35
-
36
- // New format: uuid:expiresAt:signature (3 parts separated by ':')
37
- const parts = token.split(":")
38
- if (parts.length !== 3) return false // fail-closed (was: return true)
39
-
40
- const [uuid, expiresAt, signature] = parts
41
- const expTime = parseInt(expiresAt, 10)
42
- if (isNaN(expTime)) return false // fail-closed (was: return true)
43
-
44
- // Check expiration
45
- if (Date.now() > expTime) return false
46
-
47
- // Check signature
48
- const payload = `${uuid}:${expiresAt}`
49
- const expectedSig = createHmac("sha256", TOKEN_SECRET).update(payload).digest("hex").slice(0, 16)
50
- return signature === expectedSig
51
- }
52
-
53
- /** Build a [DESIGN-TOKEN:...] regex; escapes special chars as a safety net.
54
- * Matches the FULL token (uuid:expiresAt:signature) — prompt tells advisor
55
- * to echo the complete token verbatim, not just the UUID segment.
56
- * Flexible matching: allows token to be on its own line, in a code block,
57
- * or surrounded by whitespace. */
58
- const makeDesignTokenRegex = (token, flags = "") => {
59
- // Escape the entire token, not just UUID — advisor echoes [DESIGN-TOKEN:uuid:expiresAt:signature]
60
- const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
61
- return new RegExp(
62
- `(?:^|\\s|\`|\\*)\\[DESIGN-TOKEN:\\s*${escaped}\\s*\\](?:\\s|$|\`|\\*)`,
63
- flags + "ms"
64
- )
65
- }
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"
66
28
 
67
29
  export const advisorTool = {
68
30
  name: "advisor",
@@ -81,11 +43,22 @@ export const advisorTool = {
81
43
  "Optionally pass object={type,target,status,reason,exclude} to anchor the review target " +
82
44
  "(AGENT-LOOP.md §18.8 — the review-object declaration is mechanically injected into the review message); " +
83
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. " +
84
53
  "Returns the review report — the advisor's findings verdict: all-clear (call verify) or a findings list to fix.",
85
54
  parameters: {
86
55
  type: "object",
87
56
  properties: {
88
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
+ },
89
62
  object: {
90
63
  type: "object",
91
64
  properties: {
@@ -107,6 +80,10 @@ export const advisorTool = {
107
80
  items: { type: "string" },
108
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.",
109
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
+ },
110
87
  },
111
88
  },
112
89
  readonly: true,
@@ -129,80 +106,154 @@ export const advisorTool = {
129
106
 
130
107
  // Code review must have a scope — no implicit fallback.
131
108
  if (reviewType !== "design" && !paths && !documents) {
109
+ if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
132
110
  return "Advisor: no review scope specified. Provide paths (files/directories to review) or documents (acceptance criteria context)."
133
111
  }
134
112
 
135
- // Design review: validate that documents are in docs/ or are recognized doc files
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).
136
117
  if (reviewType === "design" && documents) {
137
- const invalidDocs = documents.filter((doc) => {
138
- // Allow docs/ directory and recognized doc files (METHODOLOGY.md, README.md, etc.)
139
- if (doc.startsWith("docs/") || doc.startsWith("docs\\")) return false
140
- return !isDocFile(doc)
141
- })
118
+ const conv = loadConventions(agent.cwd)
119
+ const invalidDocs = documents.filter((doc) => !isDocPath(doc, conv))
142
120
  if (invalidDocs.length > 0) {
143
- return `Advisor: design review documents must be in docs/ directory or be recognized doc files. Invalid: ${invalidDocs.join(", ")}`
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(", ")}`
144
123
  }
145
124
  }
146
125
 
147
- // Design review: NO round reset design reviews share the 5-round convergence
148
- // budget with code reviews (round advances in agent.mjs; cap in run.mjs).
149
- // Session reset also removed: design rounds 2+ continue the advisor session
150
- // like code reviews (fix claims + round-aware prompts).
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.)
151
139
 
152
- // Generate the design token BEFORE the review and inject it into the advisor's prompt.
153
- // The advisor (LLM) decides pass/fail itself and echoes the token only on approval
154
- // the gate is a mechanical string match, not fragile semantics parsing.
155
- // A random designId is minted for EVERY design-review call (2026-09-01 multi-design
156
- // slots): on pass the token is stored in parent._engDesignTokens keyed by this id and
157
- // the id is echoed to the parent; on failure the id is dropped — never stored, so it
158
- // cannot clobber any other design's slot. Not a document anchor (rejected 2026-08-31).
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).
159
158
  const designToken = reviewType === "design" ? generateDesignToken(agent) : null
160
- const designId = reviewType === "design" ? randomUUID() : 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
+ }
161
217
  const result = await runAdvisorReview(agent, reviewType, {
162
218
  onOutput: ctx.onOutput,
163
219
  signal: ctx.signal,
164
- }, designToken, documents, paths, reviewObject)
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
+ }
165
231
 
166
232
  if (reviewType === "design") {
167
- // Whitespace-tolerant match (LLM may add spaces or wrap in fences).
168
- // The token IS the verdict — the advisor echoes it only on approval (prompt-enforced);
233
+ // Design pass/fail settlement token echo IS the verdict (prompt-enforced);
169
234
  // no findings-table heuristics: a design with issues never carries the token.
170
- const tokenPattern = makeDesignTokenRegex(designToken)
171
- if (designToken && result && tokenPattern.test(result)) {
172
- // Advisor echoed the token review passed. Issue it to the parent for eng-coder.
173
- // (session cleanup for design reviews is owned by runAdvisorReview)
174
- // Multi-design slots (2026-09-01): store under this review's designId; the single
175
- // `_engDesignToken` mirror stays for the legacy boolean gates (dispatch "has token",
176
- // session persistence) — key decision ② of ENGINEERING-MODE.md §7 2026-09-01.
177
- agent._engDesignTokens ??= new Map()
178
- agent._engDesignTokens.set(designId, designToken)
179
- agent._engDesignToken = designToken
180
- // Unlock the dispatch design gate (dispatch.mjs) for eng-coder SELF-review:
181
- // an eng-coder whose own design review passed may write files without the
182
- // parent spawn-time authorization. NOTE: unreachable today — eng-coder.md
183
- // tells the child not to re-run the design review, and spawn already sets
184
- // _engDesignReviewed (subagent.mjs). Kept as defense-in-depth for a future
185
- // eng-coder autonomous design-revision entry. Parent agents (role undefined)
186
- // don't use this flag — their runAgent resets it anyway; they are trusted
187
- // via the engineering prompt.
188
- if (agent._role === "eng-coder") agent._engDesignReviewed = true
189
- // Strip the bracketed token so only ONE unambiguous format (plain UUID) reaches the main agent
190
- const cleanResult = result.replace(makeDesignTokenRegex(designToken, "g"), "").trim()
191
- // designId rides the Approved block (review #1): the parent needs it to aim the FIRST
192
- // eng-coder spawn when several designs live in the same session.
193
- return `${cleanResult}\n\nApproved. Pass this exact token to eng-coder (designToken parameter): ${designToken}\ndesignId: ${designId} (pass as the designId parameter when spawning eng-coder; optional while this session holds a single design)`
194
- }
195
- // Review failed (or advisor chose not to pass) → do NOT touch ANY slot (方案 ②, review #2:
196
- // a failed RE-review leaves the previously approved token alive until TTL; the failed call's
197
- // own designId was never stored, so there is nothing to clear). Isolation (2026-08-30,
198
- // extended to the multi-slot Map 2026-09-01): a network glitch must not clear / other
199
- // designs' slots must not be affected — only a COMPLETED non-passing review lands here,
200
- // and it revokes nothing.
201
- // Strip every dead token occurrence from the raw output so the main agent can't grab an invalid one
202
- if (result) {
203
- const stripped = result.replace(makeDesignTokenRegex(designToken, "g"), "").trim()
204
- return stripped || "Advisor: design review did not pass."
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
205
255
  }
256
+ return settled.output
206
257
  }
207
258
  return result
208
259
  },
@@ -0,0 +1,191 @@
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
+ }