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
@@ -10,14 +10,13 @@ import { search as memorySearch, docSearch } from "../memory.mjs"
10
10
  import { pushReal } from "../context.mjs"
11
11
  import { toOpenAISchema } from "../tools/index.mjs"
12
12
  import { loadSkills, formatSkillListing } from "../skills.mjs"
13
+ import { assemblePrompt } from "../prompt-overlays.mjs"
13
14
  import {
14
15
  escapeXml, repairHistory, listWorkDir,
15
16
  collectGitContext, loadProjectInstructions, OUTLINE_INJECT_PREFIX,
16
17
  DEFAULT_MAX_TURNS, ensureAutoReminder,
17
18
  } from "./helpers.mjs"
18
- import { readFileSync, existsSync } from "node:fs"
19
- import { resolve, dirname } from "node:path"
20
- import { fileURLToPath } from "node:url"
19
+ import { pushEnvStateReminder, pushPeerReminder } from "./setup-reminders.mjs"
21
20
 
22
21
  const DEFAULT_COMPACT_THRESHOLD = 100_000
23
22
  const DOC_SEARCH_LIMIT = 5
@@ -35,45 +34,12 @@ function safeSliceUTF16(text, max) {
35
34
  }
36
35
  const MEMORY_SEARCH_LIMIT = 3
37
36
 
38
- /** Build engineering-mode system prompt by reading METHODOLOGY.md and wrapping it in the engineering template */
39
- export async function buildEngineeringPrompt(cwd, role) {
40
- const engFile = role === "eng-coder" ? "engineering-sub.md" : "engineering.md"
41
- const engTemplatePath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "prompts", engFile)
42
- let engTemplate = ""
43
- let templateMissing = false
44
- try { engTemplate = readFileSync(engTemplatePath, "utf8") } catch {
45
- templateMissing = true
46
- if (role === "eng-coder") console.warn(`[setup] engineering-sub.md missing — eng-coder will run with degraded engineering constraints. Path: ${engTemplatePath}`)
47
- else console.warn(`[setup] engineering.md missing — engineering mode will use template-only constraints. Path: ${engTemplatePath}`)
48
- }
49
- const methodologyPath = resolve(cwd, "METHODOLOGY.md")
50
- if (!existsSync(methodologyPath)) {
51
- // Template-only — engineering constraints stay active, minus project rules.
52
- // The caller injects a warning into the history. Resolve the built-in
53
- // methodology template to an absolute path (same-source join as the
54
- // engineering template above — the packaged path is unreachable from the
55
- // user's cwd) and carry its body so the warning can embed it verbatim
56
- // (2026-09-02 D-M1/D-M2: template reachability for the model).
57
- const methodologyTemplatePath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "prompts", "methodology-template.md")
58
- let methodologyTemplateBody = null
59
- try { methodologyTemplateBody = readFileSync(methodologyTemplatePath, "utf8") } catch {
60
- // Template unreadable (packaging) — degraded: base warning only (no path/body injected), same as VS Code.
61
- }
62
- return { prompt: engTemplate || null, templateMissing, methodologyMissing: true, methodologyTemplatePath, methodologyTemplateBody }
63
- }
64
- const methodology = readFileSync(methodologyPath, "utf8")
65
- const prompt = engTemplate
66
- ? `${engTemplate}\n\n---\n\n## Project METHODOLOGY.md\n\n${methodology}`
67
- : `[ENGINEERING MODE]\n\nFollow this methodology strictly:\n\n${methodology}`
68
- return { prompt, templateMissing, methodologyMissing: false }
69
- }
70
-
71
37
  /**
72
38
  * Prepare an agent run: inject context, build system prompt, inject tools.
73
39
  * Returns all state needed by the main loop, and writes initialization messages into agent.history.
74
40
  */
75
41
  export async function prepareRun(agent, input, callbacks, {
76
- depth = 0, signal, overrideTurns, resume, systemPrompt: corePrompt, disciplineRules, mainOverlay,
42
+ depth = 0, signal, overrideTurns, resume,
77
43
  } = {}) {
78
44
  const maxTurns = overrideTurns ?? agent.config?.agent?.maxTurns ?? DEFAULT_MAX_TURNS
79
45
  const threshold = agent.config?.agent?.compactThreshold ?? DEFAULT_COMPACT_THRESHOLD
@@ -83,21 +49,21 @@ export async function prepareRun(agent, input, callbacks, {
83
49
  agent.history = repairHistory(agent.history)
84
50
 
85
51
  if (!resume) {
86
- // Git context: branch, recent commits, uncommitted changes
87
- if (depth === 0) {
88
- const gitCtx = collectGitContext(agent.cwd)
89
- if (gitCtx) {
90
- agent.history.push({
91
- role: "user",
92
- content: `[System reminder: git context:\n${escapeXml(gitCtx)}]`,
93
- transient: true,
94
- })
95
- }
52
+ // Git context: branch, recent commits, uncommitted changes
53
+ // GIT-ASYNC L21:collectGitContext async(3×execFile 并行 + 失败冷却)——await
54
+ if (depth === 0) {
55
+ const gitCtx = await collectGitContext(agent.cwd)
56
+ if (gitCtx) {
57
+ agent.history.push({
58
+ role: "user",
59
+ content: `[System reminder: git context:\n${escapeXml(gitCtx)}]`,
60
+ transient: true,
61
+ })
96
62
  }
63
+ }
97
64
  if (depth === 0) {
98
65
  const tree = listWorkDir(agent.cwd)
99
66
  const platform = { win32: 'Windows', darwin: 'macOS', linux: 'Linux' }[process.platform] ?? process.platform
100
- const wasRestored = agent._sessionStart != null
101
67
  agent._sessionStart ??= new Date().toISOString()
102
68
  // Inject OS/cwd + cwd-tree only ONCE per process (restored sessionStart keeps the
103
69
  // content byte-identical across restarts — 2026-08-16 cache audit). Without the guard
@@ -111,8 +77,14 @@ export async function prepareRun(agent, input, callbacks, {
111
77
  }
112
78
 
113
79
  }
114
- if (wasRestored && !agent._restartReminderInjected) {
115
- agent._restartReminderInjected = true
80
+ // SESSION.md §11.2(2026-09-08——N6 评审 🔴 修复:注入句解耦——双信号独立消费):
81
+ // process restarted 句 = 进程级信号 _processRestartPending——仅 TUI 启动 resume 路径
82
+ // (bin/thincoder.mjs resumeSlot→applySession)设一次;/session 切换与 ACP 加载不设。
83
+ // 发句即清——进程内只发一次。resumed:yes 走 _envResumed(applySession 载入历史非空
84
+ // 时武装——每次恢复一次——pushEnvStateReminder 读即清)——互不绑门:切槽恢复只发
85
+ // resumed:yes、不误报进程重启(F3 伪触发消除——不再用 _sessionStart != null 推断)。
86
+ if (agent._processRestartPending) {
87
+ agent._processRestartPending = false
116
88
  agent.history.push({ role: "user", content: `[System reminder: process restarted at ${new Date().toISOString()}.]`, transient: true })
117
89
  }
118
90
  if (agent.memory && !agent.history.some((m) => typeof m.content === "string" && m.content.startsWith(OUTLINE_INJECT_PREFIX))) {
@@ -167,6 +139,16 @@ export async function prepareRun(agent, input, callbacks, {
167
139
  }
168
140
  pushReal(agent, { role: "user", content: input })
169
141
  }
142
+ // SESSION.md §11.1: unified per-turn env-state transient reminder (env/mode/
143
+ // model/resumed — R5/R8/R9/R11 one-shot coverage; changes surface next turn).
144
+ // R10 L1 (MULTI-INSTANCE-COLLAB §2a.4 D-L1a): peer-instance reminder right after
145
+ // env-state, BEFORE the time reminder — the time reminder stays LAST (prefix-cache
146
+ // contract). Both depth-0 only (describe the MAIN agent / its workspace peers).
147
+ if (depth === 0) {
148
+ pushEnvStateReminder(agent)
149
+ pushPeerReminder(agent)
150
+ }
151
+
170
152
  // Time grounding for EVERY agent depth AND every resume, pushed LAST (after the user
171
153
  // input): transient on the HUMAN line — dropped on persist; on the MACHINE line — kept
172
154
  // (byte-identical resume for the provider prefix cache, 2026-08-16), fresh at every run start
@@ -187,9 +169,8 @@ export async function prepareRun(agent, input, callbacks, {
187
169
 
188
170
  // task/plan tools are injected with the main loop; subagent/skill/goal/verify only at top level
189
171
  // eng-coder subagents get advisor for mandatory design review before coding
190
- const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool, engTool, readHistoryTool } = await import("../agent-tools.mjs")
191
- const { consultStartTool, consultCheckTool, consultStopTool } = await import("../agent-tools/consult.mjs")
192
- const { CONSULT_BASE } = await import("../agent.mjs")
172
+ const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool, engTool, readHistoryTool, batchSegmentTool } = await import("../agent-tools.mjs")
173
+ const { consultStartTool, consultStopTool } = await import("../agent-tools/consult.mjs")
193
174
  // withPool: decorate the consult_start description with the CURRENT candidate pool
194
175
  // so the model knows which models it can pick (CLI parity with the plugin). The
195
176
  // retired escalate tool surface is now the subagent action:"escalate" — its pool
@@ -200,12 +181,12 @@ export async function prepareRun(agent, input, callbacks, {
200
181
  if (!list) return tool
201
182
  return { ...tool, description: tool.description + `\nCurrently configured consultants (this tool's pool): ${list}` }
202
183
  }
203
- // Role enum is mutually exclusive: normal mode has "coder", engineering mode has "eng-coder"
184
+ // Role enum is mutually exclusive: normal mode has "coder", engineering mode has "eng-coder"/"eng-designer"
204
185
  const subagentRoles = (depth === 0 && agent.config?.agent?.engineering)
205
186
  ? {
206
- enum: ["explore", "plan", "eng-coder"],
187
+ enum: ["explore", "plan", "eng-designer", "eng-coder"],
207
188
  description: "The sub-agent role — see the tool description for the role capability matrix. Exact spelling required.",
208
- suffix: " In engineering mode, use role='eng-coder' for implementation (coder is disabled).",
189
+ suffix: " In engineering mode, use role='eng-coder' for implementation (coder is disabled) and role='eng-designer' for writing the requirements/design documents.",
209
190
  }
210
191
  : {
211
192
  enum: ["explore", "plan", "coder"],
@@ -234,39 +215,49 @@ export async function prepareRun(agent, input, callbacks, {
234
215
  },
235
216
  } : subagentTool
236
217
 
237
- // §18 D-E3: eng-coder children (depth>0) get an audit-only subagent channel —
238
- // role enum limited to explore, NO async parameter (sync only) and action pinned
239
- // to spawn (§19 D-M3 restricted-variant action gate escalate/check/status are
240
- // refused here at the schema level too; the mechanical re-check lives in
241
- // subagent.mjs execute the §19 action gate + gateEngCoderSpawn (spawn-child.mjs)
242
- // schema enums are advisory, providers don't enforce them).
243
- const engAuditSubagent = depth > 0 && agent._role === "eng-coder"
218
+ // §18 D-E3 + ENGINEERING-MODE.md §2.15 D(第 2 批——参数化复用,不并列第二个 IIFE):
219
+ // 工程子代理(depth>0;eng-coder = 偏差审计 / eng-designer = 自己勘察)get a restricted
220
+ // spawn channel — role enum limited to explore, NO async parameter (sync only) and action
221
+ // pinned to spawn(§19 D-M3 restricted-variant action gate——escalate/check/status are
222
+ // refused here at the schema level too;the mechanical re-check lives in subagent.mjs
223
+ // execute the §19 action gate + gateEngCoderSpawn (spawn-child.mjs) — schema enums are
224
+ // advisory, providers don't enforce them)。描述文案按父角色分流(审计 vs 勘察)。
225
+ const engChildRole = depth > 0 && (agent._role === "eng-coder" || agent._role === "eng-designer") ? agent._role : null
226
+ const engChildSubagent = engChildRole
244
227
  ? (() => {
245
228
  const props = { ...subagentTool.parameters.properties }
246
- // §19 review hygiene: the audit channel is spawn-only sync explore — drop
247
- // async, the check/status params (id/n) and the eng-coder token params
248
- // (designToken/designId are meaningless for a read-only audit spawn; the
249
- // parent spawn already carried the token). Schema noise would invite the
229
+ // §19 review hygiene: the child channel is spawn-only sync explore — drop
230
+ // async, the check/status params (id/n), the eng-coder token params
231
+ // (designToken/designId are meaningless for a read-only spawn; the parent
232
+ // spawn already carried the token) and batchDoc (an audit child derives no
233
+ // batch parameter — its task book rides the mechanical summary of the
234
+ // parent's _engTaskInput instead). Schema noise would invite the
250
235
  // model to pass irrelevant args.
251
- delete props.async // sync only — the eng-coder blocks on the audit report
236
+ delete props.async // sync only — the parent blocks on the child's report
252
237
  delete props.id
253
238
  delete props.n
254
239
  delete props.designToken
255
240
  delete props.designId
241
+ delete props.batchDoc
242
+ const designer = engChildRole === "eng-designer"
256
243
  props.role = {
257
244
  type: "string",
258
245
  enum: ["explore"],
259
- description: "explore only — the eng-coder's internal spawn channel is reserved for read-only divergence audits (AGENT-LOOP.md §18 D-E3).",
246
+ description: designer
247
+ ? "explore only — the eng-designer's internal spawn channel is reserved for read-only surveys of the current state (ENGINEERING-MODE.md §2.15 D; ≤6 spawns per batch)."
248
+ : "explore only — the eng-coder's internal spawn channel is reserved for read-only divergence audits (AGENT-LOOP.md §18 D-E3).",
260
249
  }
261
250
  props.action = {
262
251
  type: "string",
263
252
  enum: ["spawn"],
264
- description: "spawn only — the eng-coder's internal spawn channel is reserved for read-only divergence audits (AGENT-LOOP.md §19 D-M3); escalate/check/status are refused (escalate spawns a coder+WRITE child against explore-only intent; check/status have no async pool in a child context).",
253
+ description: `spawn only — the ${engChildRole}'s internal spawn channel is read-only (escalate/status/cancel/panel/consume-design/observe/send are refused: escalate spawns a coder+WRITE child, and the pool/panel actions have no async pool or panel mirror in a child context).`,
265
254
  }
266
255
  return {
267
256
  ...subagentTool,
268
257
  name: "subagent",
269
- description: "Spawn a read-only `explore` sub-agent to AUDIT your delivery against the design (AGENT-LOOP.md §18 D-E2 ③): it compares the delivered code with the design for divergence — partially implemented acceptance criteria, silent simplifications, doc drift, changes outside the approved file list. BLOCKING ONLY (no async — the audit report decides your next protocol step). action:'spawn' ONLY — the audit channel is a read-only spawn; escalate/check/status are not available (AGENT-LOOP.md §19). The audit task book is appended MECHANICALLY — your own spawn task (docs involved / acceptance criteria / file list) plus the files you actually touched; never hand the audit a self-written file list (a self-report could omit exactly the out-of-scope file it must catch).",
258
+ description: designer
259
+ ? "Spawn a read-only `explore` sub-agent to SURVEY the current state for the design (ENGINEERING-MODE.md §2.15 D): it reads code / docs / existing designs and reports evidence with file:line. BLOCKING ONLY (no async) and action:'spawn' ONLY — the survey channel is read-only; escalate/status/cancel/panel/consume-design/observe/send are not available (AGENT-LOOP.md §19). Survey budget: ≤6 explore spawns per batch — the main agent's survey result is reference only; do your own."
260
+ : "Spawn a read-only `explore` sub-agent to AUDIT your delivery against the design (AGENT-LOOP.md §18 D-E2 ③): it compares the delivered code with the design for divergence — partially implemented acceptance criteria, silent simplifications, doc drift, changes outside the approved file list. BLOCKING ONLY (no async — the audit report decides your next protocol step). action:'spawn' ONLY — the audit channel is a read-only spawn; escalate/status/cancel/panel/consume-design/observe/send are not available (AGENT-LOOP.md §19). The audit task book is appended MECHANICALLY — your own spawn task (docs involved / acceptance criteria / file list) plus the files you actually touched; never hand the audit a self-written file list (a self-report could omit exactly the out-of-scope file it must catch).",
270
261
  parameters: { ...subagentTool.parameters, properties: props },
271
262
  }
272
263
  })()
@@ -275,9 +266,11 @@ export async function prepareRun(agent, input, callbacks, {
275
266
  // consult 工具仅在配置时注册(consultModels 空池时注册会让模型调用后吃一个错误回合)——
276
267
  // §19: escalate 已并入常驻 subagent 的 action:"escalate"(无空池注册问题——动作在
277
268
  // 池空时返回既有错误语义,工程模式 fail-closed 在 execute 内拒绝)。
269
+ // §25 D-R17a: consult_check 已退役(digest 自动注入是唯一消费通道)——consult 家族
270
+ // 只剩 2 工具(consult_start/consult_stop——setup 注册点与描述面同步清零)。
278
271
  const consultModels = agent.config?.agent?.consultModels ?? []
279
272
  const consultTools = consultModels.length
280
- ? [withPool(consultStartTool), consultCheckTool, consultStopTool]
273
+ ? [withPool(consultStartTool), consultStopTool]
281
274
  : []
282
275
  const depthOnly = depth === 0 ? [filteredSubagent, skillTool, goalTool, engTool, verifyTool, recentChangesTool, readHistoryTool, advisorTool, ...consultTools]
283
276
  // SESSION.md §9 D-S2: read_history is depth-0 ONLY — a subagent querying "the session"
@@ -288,70 +281,57 @@ export async function prepareRun(agent, input, callbacks, {
288
281
  // escalate hit "unknown tool" and fell back to bash node --check / npm test to
289
282
  // self-verify (2026-08-16 deepseek escalate diagnosis; plugin parity).
290
283
  // eng-coder: advisor + verify + the §18 audit-only subagent channel (D-E3).
291
- : agent._role === "eng-coder" ? [advisorTool, verifyTool, ...(engAuditSubagent ? [engAuditSubagent] : [])]
284
+ // eng-designer (§2.15 D): the survey-only subagent channel alone no advisor
285
+ // (it does not fire reviews) and no verify (its deliverable is documents, not code).
286
+ // §2.20.3(第 4 批):两分支各追加 batch_segment——目标档 = spawn 时绑定的
287
+ // `agent._batchDoc`(§2.20.2);主 agent 不挂载(§1/§4/§6 走普通文档写)。
288
+ : engChildRole === "eng-coder" ? [advisorTool, verifyTool, batchSegmentTool(agent._batchDoc ?? null), ...(engChildSubagent ? [engChildSubagent] : [])]
289
+ : engChildRole === "eng-designer" ? [batchSegmentTool(agent._batchDoc ?? null), ...(engChildSubagent ? [engChildSubagent] : [])]
292
290
  : agent._role === "coder" ? [verifyTool, advisorTool]
293
291
  : agent._role === "consult" ? [recentChangesTool]
294
292
  : []
295
- // NOTE: every depth-0 tool schema is estimated into the compaction overhead per turn
296
- // (context.mjs extras.tools) — a tool-schema change shifts the compaction fixture
297
- // knife-edges (agent.test T3b: read_history's schema +~470 tokens once crossed its
298
- // 11000 threshold; fixture adjusted to 12500 — rationale in the test comment).
299
293
  const tools = [...agent.tools, taskTool, planTool, timerTool, ...depthOnly]
300
294
  const toolSchemas = tools.map(toOpenAISchema)
301
295
  const toolByName = new Map(tools.map((t) => [t.name, t]))
302
296
  agent._onTaskUpdate = callbacks.onTaskUpdate
303
297
 
304
- // system prompt
305
- const needsDiscipline = depth === 0 || agent._role === "coder" || agent._role === "eng-coder"
306
- let base
307
- if (agent._role === "consult") {
308
- // consult children: a lean, purpose-built base prompt (consult-base.md) — NOT the full
309
- // main-agent system.md (whose coding-agent persona, checklist/task/verify workflows and
310
- // tool references conflict with a read-only diagnosis and cost tokens every turn).
311
- base = CONSULT_BASE
312
- } else if ((depth === 0 || agent._role === "eng-coder") && agent.config?.agent?.engineering) {
313
- // Engineering mode: strict methodology, NO standard discipline injection.
314
- // Falling back to standard discipline on METHODOLOGY.md absence would leak
315
- // advisor enforcement into engineering mode — the two prompt sets stay separate.
316
- const engResult = await buildEngineeringPrompt(agent.cwd, agent._role)
317
- if (engResult.prompt) {
318
- base = `${corePrompt}\n\n${engResult.prompt}`
319
- } else {
320
- // Template unreadable last resort: core prompt only, no methodology.
321
- base = corePrompt
322
- }
323
- // Warn when templates or METHODOLOGY.md are missing (degraded engineering constraints).
324
- if (depth === 0) {
325
- const warnings = []
326
- if (engResult.templateMissing) {
327
- warnings.push(`Engineering template (${agent._role === "eng-coder" ? "engineering-sub.md" : "engineering.md"}) not found — using degraded constraints.`)
328
- }
329
- if (engResult.methodologyMissing) {
330
- let warning = "METHODOLOGY.md not found in the project root — no project methodology is loaded, so every 'per METHODOLOGY' reference in the engineering prompt is dangling and the three-document hard flow (requirements / design / test doc) is NOT enforced. Ask the user whether to create METHODOLOGY.md; if the user confirms, write cwd/METHODOLOGY.md before designing."
331
- // 2026-09-02 D-M1/D-M2 (template accessibility): absolute path + full body — the model
332
- // can read the template directly instead of hand-writing one from an unreachable source
333
- // path. Body read failure degraded warning above (path/body not injected). VS Code
334
- // setup-reminders.mjs parity (两端警告文本一致,本端以 CLI 为准).
335
- if (engResult.methodologyTemplateBody) {
336
- warning += `\n\nbuilt-in template(可 read ${engResult.methodologyTemplatePath} 或直接参考以下内容):\n\n${engResult.methodologyTemplateBody}`
337
- }
338
- warnings.push(warning)
339
- }
340
- if (warnings.length > 0) {
341
- agent.history.push({
342
- role: "user",
343
- content: `[System reminder: ENGINEERING MODE is active but ${warnings.join(" ")}]`,
344
- })
345
- }
346
- }
347
- } else {
348
- base = needsDiscipline ? `${corePrompt}\n\n${disciplineRules}` : corePrompt
298
+ // ── system prompt ── PROMPT-SYSTEM 施工② G2/G3(2026-09-10):四槽位装配函数
299
+ // assemblePrompt({scenario}) 表驱动(D1 场景表 = 蓝图 §3.2 装配矩阵)——取代旧
300
+ // consult/工程/普通三分支 + overlay 前缀。固定序 人格→common→纪律(§3.1);[4] 层
301
+ // (AGENTS + skills)由下方既有尾部逻辑承担。降级链(蓝图 §3.4):人格/纪律/common
302
+ // 槽文件缺失 该槽空缺跳过 + 醒目警告(不 fallback 其他槽——层间隔离);AGENTS.md
303
+ // 缺失 loadProjectInstructions 静默跳过(无警告——既有语义)。
304
+ // consult = 特殊模块(§3.3)——CONSULT_BASE 自含基底直接返回,不入主链、无四槽。
305
+ // eng-coder 场景即工程纪律(G6——subagent-spawn 的 childConfig engineering=true
306
+ // 强制语义由此表行承载:scenario=eng-coder discipline-engineering 槽)。
307
+ const { prompt: base, warnings: slotWarnings } = assemblePrompt(
308
+ agent._role === "consult"
309
+ ? "consult"
310
+ : (depth === 0 || engChildRole) && agent.config?.agent?.engineering
311
+ ? (engChildRole ?? "engineering")
312
+ : (depth === 0 ? "normal" : agent._role ?? "normal"),
313
+ )
314
+ // D2 警告通道 = 既有 setup 警告通道(history 注入)——不新增机制。深度 0 才注入
315
+ // (子代理警告不打扰主会话历史——旧工程警告块同款深度门)。
316
+ // ── Q1 审计收敛(蓝图 §3.4 第 4 款):特殊模块基底缺失 → 该模块不可用报错(不自降级
317
+ // ——空基底绝不可静默上岗)。consult 场景在外部消费点收口:入历史后抛错(Agent 循环
318
+ // 历史已含错误句可见——不静默、不降级)。四槽场景维持跳过+警告降级链(AC-2 三款)。
319
+ if (agent._role === "consult" && !base) {
320
+ const msg = "[Consult module unavailable: prompts/consult-base.md missing — the consultation module refuses to degrade (蓝图 §3.4 特殊模块不自降级). Check the installation's prompts directory.]"
321
+ agent.history.push({ role: "user", content: msg })
322
+ throw new Error(`consult-base.md missing — consultation module unavailable (no degraded fallback per PROMPT-SYSTEM §3.4)`)
323
+ }
324
+ if (depth === 0 && slotWarnings.length > 0) {
325
+ agent.history.push({
326
+ role: "user",
327
+ content: `[System reminder: prompt slots degraded ${slotWarnings.join(" ")}]`,
328
+ })
349
329
  }
350
- let systemPrompt = agent.overlay
351
- ? `${agent.overlay}\n\n${base}`
352
- : depth === 0 && !agent.config?.agent?.engineering
353
- ? `${base}\n\n${mainOverlay}`
354
- : base
330
+ // G3(施工②审计收敛):overlay(人格)前缀分支退役——CLI/VSC 同构(人格槽由场景表
331
+ // 承载,spawn 侧 overlay 恒空——createAgent 的 overlay 参数留空兼容位)。
332
+ // let——下方按 projectRules / skills listing 尾部追加(L326/L331 +=)再赋值;
333
+ // const 声明会 TypeError: Assignment to constant variable(每次 run 必炸)。
334
+ let systemPrompt = base
355
335
 
356
336
  // Time injection deliberately does NOT live here: system prompts must be byte-identical
357
337
  // across runs (provider prefix caches). The time rides a transient user reminder per turn
@@ -16,6 +16,10 @@
16
16
  */
17
17
  import { specForModel } from "../config.mjs"
18
18
  import { ContinueError } from "../agent.mjs"
19
+ import { relayPrefixOf } from "./relay-prefix.mjs"
20
+ // 第 27 批 §12.3①:relay 前缀文法单一权威模块(`src/agent/relay-prefix.mjs`)——
21
+ // 生成侧枢纽再导出(TUI/ACP 消费方可经此导入;消费方按 §12.3① 直连模块亦可)。
22
+ export { RELAY_PREFIX_RE, parseRelayPath, relayPrefixOf } from "./relay-prefix.mjs"
19
23
 
20
24
  /** 事件 token 哨兵串(D1)——LLM 正常内容混淆概率极低;字段分隔用 RS (\x1e)。 */
21
25
  export const EVENT_SENTINEL = "⟦ev⟧"
@@ -26,23 +30,36 @@ const RS = "\x1e"
26
30
  * 单源化(2026-08-30 评审):文案演进只改这里,消除文案与检测正则的漂移面。 */
27
31
  export const TURN_CAP_MARK = "stopped: turn cap reached"
28
32
 
33
+ /** SYNC-CANCEL(L52):sync spawn ⏹ 定向中止折叠报告的公共锚点——sync 折叠文案必含
34
+ * 此子串(buildSyncStoppedReport);消费端(TUI tool-events onToolResult)用
35
+ * includes() 检测"用户定向中止——工作可能不完整"语义(块冻结标 stopped 而非 done——
36
+ * R6)。与 TURN_CAP_MARK 同族单源纪律。 */
37
+ export const STOPPED_MARK = "stopped by user"
38
+
29
39
  /**
30
- * §18 D-E3 eng-coder 内部 spawn 机械门(AGENT-LOOP.md §18 D-E2 round5 #2 后备):
31
- * eng-coder 子代理(depth>0 且 parent._role==="eng-coder")的内部 spawn 通道只做
32
- * 偏差审计——role explore、async 强制同步;审计 spawn 预算 = 首审 1 + 修正轮
33
- * ≤5 的再审(第 7 次审计 spawn 机械拒绝——5 轮纪律失效时不静默,错误即 stalled
34
- * 信号)。返回 null = 非 eng-coder 上下文(不加限制);返回审计尝试序号 = 通过。
40
+ * §18 D-E3 工程子代理内部 spawn 机械门(AGENT-LOOP.md §18 D-E2 round5 #2 后备 +
41
+ * ENGINEERING-MODE.md §2.15 D):eng-coder(偏差审计)与 **eng-designer(自己勘察)**
42
+ * 同为受限通道——depth>0 且父角色 集合时,内部 spawn 只允许 role='explore'、
43
+ * async 强制同步。
44
+ * **审计预算(6)只计 eng-coder**(designer 勘察非审计——designer 父路径校验通过后
45
+ * 返回 null:不计数、且调用方以 `engAuditAttempt !== null` 为审计任务书注入开关——
46
+ * 非 null 会把审计范围误注进勘察任务书)。
47
+ * 返回 null = 非工程子代理上下文(不加限制)或 designer 勘察路径(无须计数);
48
+ * 返回审计尝试序号 = eng-coder 审计通过。
35
49
  * schema 层过滤(setup.mjs 受限变体)只是给模型的参数提示——本函数是机械强制。
36
50
  */
37
51
  export const ENG_AUDIT_SPAWN_LIMIT = 6 // 允许 6 次审计 spawn;第 7 次拒绝
38
52
  export function gateEngCoderSpawn(parent, depth, role, async) {
39
- if ((depth ?? 0) <= 0 || parent?._role !== "eng-coder") return null
53
+ const parentRole = parent?._role
54
+ if ((depth ?? 0) <= 0 || (parentRole !== "eng-coder" && parentRole !== "eng-designer")) return null
40
55
  if (role !== "explore") {
41
- throw new Error("eng-coder subagents may only spawn role='explore' — internal spawns exist solely for the read-only divergence audit (AGENT-LOOP.md §18 D-E3)")
56
+ throw new Error(`${parentRole} subagents may only spawn role='explore' — internal spawns exist solely for read-only work (the eng-coder divergence audit / the designer's own survey) (AGENT-LOOP.md §18 D-E3, ENGINEERING-MODE.md §2.15 D)`)
42
57
  }
43
58
  if (async === true) {
44
- throw new Error("eng-coder internal spawns are sync-only — the audit report must return before the next protocol step; async spawn is only available at the top level (AGENT-LOOP.md §18 D-E3)")
59
+ throw new Error(`${parentRole} internal spawns are sync-only — the child's report must return before the next protocol step; async spawn is only available at the top level (AGENT-LOOP.md §18 D-E3)`)
45
60
  }
61
+ // designer 勘察路径:校验通过即返回 null(非审计——不计数、不触发审计任务书注入)
62
+ if (parentRole === "eng-designer") return null
46
63
  const attempt = (parent._engAuditSpawns ?? 0) + 1
47
64
  if (attempt > ENG_AUDIT_SPAWN_LIMIT) {
48
65
  throw new Error("correction-round limit exceeded — deliver a stalled report (AGENT-LOOP.md §18: max 5 fix rounds; the 7th audit spawn is refused mechanically)")
@@ -58,7 +75,7 @@ export function gateEngCoderSpawn(parent, depth, role, async) {
58
75
  */
59
76
  export function makeRelay(parent, label, emit, model) {
60
77
  parent._subAgentCounter = (parent._subAgentCounter ?? 0) + 1
61
- const relayPrefix = `${label}#${parent._subAgentCounter}/`
78
+ const relayPrefix = relayPrefixOf(label, parent._subAgentCounter)
62
79
  emit?.(relayPrefix + "[model]" + (model ?? ""))
63
80
  return relayPrefix
64
81
  }
@@ -106,25 +123,45 @@ export function stripEventTokensForCapture(text) {
106
123
  * - onToolOutput 带**已加前缀**的 name 走父 onToolOutput(name 形如
107
124
  * "coder#1/bash",消费端剥前缀路由进对应区块;chunk 对象/裸串原样透传)。
108
125
  * 父回调缺省时不包装(headless 嵌入)。
126
+ * §27 R23 D-R23c1:包装产物在 onToken 上留 `_relayPrefix` 标记——同步收尾段据此
127
+ * 判定"ctx.callbacks 已是嵌套 wrapper"(本 spawn 处于更深一层——eng-coder 内
128
+ * explore)→ emitNestedChildEvent 补发射内层完成事件(T-R23c.2b 断言源)。
109
129
  */
110
130
  export function wrapChildCallbacks(relayPrefix, parentCallbacks = {}) {
131
+ const mark = (fn) => { fn._relayPrefix = relayPrefix; return fn }
111
132
  const wrapped = {
112
133
  onToken: parentCallbacks.onToken
113
- ? (t) => parentCallbacks.onToken(relayPrefix + stripEventToken(String(t)))
134
+ ? mark((t) => parentCallbacks.onToken(relayPrefix + stripEventToken(String(t))))
114
135
  : null,
115
136
  onReasoning: parentCallbacks.onReasoning
116
- ? (t) => parentCallbacks.onReasoning(relayPrefix + t)
137
+ ? mark((t) => parentCallbacks.onReasoning(relayPrefix + t))
117
138
  : null,
118
139
  onToolCall: parentCallbacks.onToolCall
119
- ? (name, args) => parentCallbacks.onToolCall(relayPrefix + name, args)
140
+ ? mark((name, args) => parentCallbacks.onToolCall(relayPrefix + name, args))
120
141
  : null,
121
142
  onToolOutput: parentCallbacks.onToolOutput
122
- ? (name, chunk) => parentCallbacks.onToolOutput(relayPrefix + name, chunk)
143
+ ? mark((name, chunk) => parentCallbacks.onToolOutput(relayPrefix + name, chunk))
123
144
  : null,
124
145
  }
125
146
  return wrapped
126
147
  }
127
148
 
149
+ /**
150
+ * §27 R23 D-R23c1(生成侧补发射——评审 #1 🅰):sync spawn 同步收尾时若父回调已是
151
+ * 嵌套 wrapper(onToken 带 `_relayPrefix` 标记——即本 spawn 的父本身是子代理,如
152
+ * eng-coder 内 explore 审计)→ 发内层 done/stopped 事件(带完整嵌套前缀——wrapper
153
+ * 链自动补外层前缀)→ 主 TUI routeSubToken 路由到子块定格。非嵌套(depth-0 直连主
154
+ * 回调——无标记)不发——完成冻结仍走既有 onToolResult/subKey 路径(零行为变化)。
155
+ * @returns {boolean} true = 已发射(嵌套上下文)
156
+ */
157
+ export function emitNestedChildEvent(ctx, relayPrefix, kind) {
158
+ const onToken = ctx?.callbacks?.onToken
159
+ if (typeof onToken?._relayPrefix !== "string" || !onToken._relayPrefix) return false
160
+ if (kind !== "done" && kind !== "stopped") return false
161
+ onToken(`${relayPrefix}⟦ev⟧${kind}\x1e0\x1e0\x1e${kind}\x1e`)
162
+ return true
163
+ }
164
+
128
165
  /**
129
166
  * 子 agent provider API key 检查:trim 后非空才保留;缺失返回 null(调用方
130
167
  * 按各自业务语汇报错——subagent 抛出 / escalate·consult 返回 Error 文本)。
@@ -173,6 +210,11 @@ export async function runWithContinue(runner, child, input, callbacks, runOpts,
173
210
  const capture = callbacks?.onToken
174
211
  ? (t) => { output += stripEventTokensForCapture(String(t)); child._capturedOutput = output; callbacks.onToken(t) }
175
212
  : (t) => { output += stripEventTokensForCapture(String(t)); child._capturedOutput = output }
213
+ // §27 R23 D-R23c1(评审 🔴 修复——2026-09-07):capture 是子代理 runAgent/dispatch ctx
214
+ // 实际收到的 onToken——嵌套 wrapper 标记(wrapChildCallbacks `_relayPrefix`)必须随
215
+ // capture 透传,否则 eng-coder 内 explore 同步收尾的 emitNestedChildEvent 判定恒 false
216
+ // (生成侧补发射结构性不可达——T-R23c.2b 真实路径回归)。
217
+ if (callbacks?.onToken?._relayPrefix) capture._relayPrefix = callbacks.onToken._relayPrefix
176
218
  for (let resume = false; ; resume = true) {
177
219
  try {
178
220
  return await runner(child, input, { ...callbacks, onToken: capture }, { ...runOpts, resume })