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
@@ -4,163 +4,39 @@
4
4
  *
5
5
  * - onToken/onReasoning : 子agent 前缀分流(routeSub*)→ 主流 streaming/reasoning
6
6
  * - onToolCall : 状态栏 + `❯ name args` 标题行 + 计时 + §19 action 记录
7
- * - onToolResult : 子agent 完成冻结(finishSubTask + freezeDoneSubTasks)、
7
+ * - onToolResult : 子agent 完成冻结(finishSubTaskKey 精确冻 + freezeDoneSubTasks——
8
+ * F-2 后 finishSubTask 为 no-op 兼容保留)、
8
9
  * 工具块结果入块、advisor 评审冻结框
9
10
  * - onToolOutput : advisor 有序块缓冲 / 工具块输出流
10
11
  * - 其余 : usage 累计、等待提示、task 面板、回合末增量落盘
11
12
  *
12
13
  * flushStream 同时返回给调用方(回合循环 / onTurnEnd 共用)。纯回调装配,无终端副作用
13
- * (除经 deps 注入的 pushLine/render)。§19: subagent_check/escalate 工具退役后,
14
- * subagent 家族全部调用以工具名 "subagent" + action 到达——完成路由按 onToolCall 时
15
- * 记录的 action 分流(spawn 区块 / escalate 区块 / check·status 普通工具块)。
14
+ * (除经 deps 注入的 pushLine/render)。§19: subagent_check/escalate 工具退役后
15
+ * (§19.8——check 动作已删),subagent 家族全部调用以工具名 "subagent" +
16
+ * action 到达——完成路由按 onToolCall 时记录的 action 分流(spawn 区块 / escalate
17
+ * 区块 / status/observe/send 普通工具块——observe/send 不建子代理区块)。
16
18
  */
17
19
  import { C } from "./ansi.mjs"
18
20
  import { formatToolSummary } from "./tool-summaries.mjs"
19
21
  import { describeToolArgs, toolArgsLines } from "./tool-args.mjs"
20
22
  import { ADVISOR_THINKING_PLACEHOLDER, resolveAdvisorProvider } from "../advisor/run.mjs"
21
23
  import {
22
- SUB_PREFIX_RE, SUBAGENT_ROLES, routeSubToken, routeSubReasoning, routeSubToolCall,
23
- routeSubToolOutput, finishSubTask, finishSubTaskKey, finishSubTasksByRole, finishSubTaskByModel, freezeDoneSubTasks,
24
+ SUBAGENT_ROLES, routeSubToken, routeSubReasoning, routeSubToolCall,
25
+ routeSubToolOutput, finishSubTask, finishSubTaskKey, finishSubTasksByRole, freezeDoneSubTasks,
24
26
  ensureCompressPanel, markCompressFailed, markCompressDone, markCompressFallback,
25
27
  } from "./subagent-blocks.mjs"
26
- import { TURN_CAP_MARK } from "../agent/spawn-child.mjs"
27
-
28
- /** Tool execution start timestamps (performance.now ms). Keyed by tool_call id
29
- * when available (parallel same-name tools each get their own tick — the
30
- * P0-3 fix, 2026-08-30), falling back to a per-name FIFO queue for callers
31
- * without ids (subagent relay). FIFO shift assumes near-call-order completion;
32
- * a parallel same-name batch finishing out of order swaps durations between
33
- * siblings — same magnitude, both keep an elapsed (display-level, acceptable). */
34
-
35
- // Named caps (consult P2, 2026-08-30): inline 200/8/120/3/5 were magic numbers.
36
- const TOOL_OUTPUT_LINE_CAP = 200 // per-call streaming output ring buffer
37
- const SUBAGENT_PREVIEW_LINES = 8 // report preview rows in the conversation
38
- const PREVIEW_LINE_CHARS = 120 // per-line preview slice
39
- const REMINDER_CAP = 3 // max pending reminders shown on turn end
40
- const REMINDER_PERSIST_TURNS = 5 // persist reminders every N turns
41
-
42
- const _toolTicks = new Map()
43
- // §19 action registry: tool_call id → subagent action (non-spawn only — spawn is
44
- // the default when no record exists). onToolCall sees the args, onToolResult only
45
- // the name; without the record every subagent result would route as a spawn.
46
- const _subActions = new Map()
47
- const _subActionQ = [] // FIFO for subagent calls without a tool id (same fallback as tick queue)
48
-
49
- function tickStart(name, toolId) {
50
- const key = toolId ?? name
51
- if (toolId) { _toolTicks.set(key, performance.now()); return }
52
- const q = _toolTicks.get(key) ?? []
53
- q.push(performance.now())
54
- _toolTicks.set(key, q)
55
- }
56
-
57
- /** Settle one pending tick: by tool_call id, or per-name FIFO. Returns start or null. */
58
- function tickTake(name, toolId) {
59
- const key = toolId ?? name
60
- const v = _toolTicks.get(key)
61
- if (v === undefined) return null
62
- if (toolId) { _toolTicks.delete(key); return v }
63
- if (v.length === 0) { _toolTicks.delete(key); return null }
64
- const started = v.shift()
65
- if (v.length === 0) _toolTicks.delete(key)
66
- return started
67
- }
68
-
69
- /** P0-2 sweep (2026-08-30 consult): an interrupted turn (Ctrl+C / error) leaves
70
- * running tool-block carriers without an onToolResult — their header would say
71
- * "running" forever. runAgentTurn's finally calls this: mark them done with an
72
- * "(interrupted)" status and clear the tick table so no stale start time leaks
73
- * into the next turn. Mirrors freezeAllSubTasks for the tool-block family. */
74
- export function sweepToolBlocks(state) {
75
- for (const l of state.lines ?? []) {
76
- const b = l._toolBlock
77
- if (b && !b.done) {
78
- b.done = true
79
- b.summary = "(interrupted)"
80
- b.interrupted = true
81
- }
82
- }
83
- _toolTicks.clear()
84
- _subActions.clear()
85
- _subActionQ.length = 0
86
- }
87
-
88
- /** Shared display guard for tool results — LIVE and RESTORE use the same
89
- * function (P1, 2026-08-30 consult: restore lacked the live path's guards).
90
- * 1) Multimodal results (read_image) embed FULL base64 images in the JSON —
91
- * the human needs only the text part (model gets images via the multimodal
92
- * channel); 2) results beyond maxRows are truncated in the block (full
93
- * text always lives in history for the model). Returns row array. */
94
- export function slimToolResultForDisplay(result, maxRows = 400) {
95
- let displayResult = result
96
- try {
97
- const parsed = JSON.parse(result)
98
- if (parsed?.images?.length) displayResult = parsed.text ?? result
99
- } catch { /* not JSON — show as-is */ }
100
- const rows = String(displayResult).split("\n").filter((l) => l.trim())
101
- return rows.length > maxRows
102
- ? [...rows.slice(0, maxRows), `… (result truncated at ${maxRows} rows — full text in history)`]
103
- : rows
104
- }
105
- /** Mark the dispatch-level tool carrier done when its result is consumed by a
106
- * dedicated branch (subagent/escalate/advisor blocks) — without this the turn
107
- * sweep mislabels successful calls as "(interrupted)" (consult P1, 2026-08-30). */
108
- function settleToolBlock(state, name, toolId, summary) {
109
- const block = findToolBlock(state, name, toolId)
110
- if (block) {
111
- block.done = true
112
- block.summary = summary
113
- const started = tickTake(name, toolId)
114
- block.elapsed = started !== null ? Math.round(performance.now() - started) : null
115
- }
116
- }
117
-
118
- /** Async spawn detection (§15 D-A1): the subagent tool's async:true result is a
119
- * status JSON ({id, role, status: running|queued}), NOT a report — the child
120
- * keeps running, so its activity block must not be frozen at spawn time (it
121
- * freezes via the ⟦ev⟧done event at settle — §15 D-A3). */
122
- function isAsyncSpawnResult(result) {
123
- try {
124
- const o = JSON.parse(result)
125
- return Boolean(o && typeof o === "object" && typeof o.id !== "undefined" && (o.status === "running" || o.status === "queued"))
126
- } catch {
127
- return false
128
- }
129
- }
130
-
131
- /** §7.2.3 spawn 门拒错误探测(T-F5):subagent spawn 的机械拒绝以 {status:"error"}
132
- * JSON 返回(例:manual auto-turn digest spawn 门——digest 语义 organize-only)——
133
- * 错误路径不得触发完成冻结(round1 #1——错误路径不冻结任何 running 块)。该形态只
134
- * 出现在无 subKey 的拒绝路径(成功路径恒带 dispatch 传的 ctx._subagentKey)。 */
135
- function isSpawnErrorResult(result) {
136
- try {
137
- const o = JSON.parse(result)
138
- return Boolean(o && typeof o === "object" && o.status === "error")
139
- } catch {
140
- return false
141
- }
142
- }
143
-
144
-
145
-
146
- /** Find the live tool-block carrier for a tool event: exact id match when the
147
- * callback carries one (P0-3 — parallel same-name tools route to their own
148
- * block); falls back to the last unfinished block of that name. */
149
- function findToolBlock(state, name, toolId) {
150
- const lines = state.lines ?? []
151
- if (toolId !== undefined && toolId !== null) {
152
- for (let i = lines.length - 1; i >= 0; i--) {
153
- if (lines[i]._toolBlock?.id === toolId) return lines[i]._toolBlock
154
- }
155
- return null
156
- }
157
- for (let i = lines.length - 1; i >= 0; i--) {
158
- const b = lines[i]._toolBlock
159
- if (b && b.name === name && !b.done) return b
160
- }
161
- return null
162
- }
163
-
28
+ // 27 §12.3②:前缀正则换名 + import 源改文法模块(纯换名——语义零改)。
29
+ import { RELAY_PREFIX_RE } from "../agent/relay-prefix.mjs"
30
+ import { TURN_CAP_MARK, STOPPED_MARK } from "../agent/spawn-child.mjs"
31
+ // 2026-09-05 module-split:ticks/maps/sweep/slim/settle/探测/find 族迁 tool-display.mjs——
32
+ // buildToolCallbacks 内部引用用本地 import;sweepToolBlocks re-export(agent-turn 消费面)
33
+ import {
34
+ TOOL_OUTPUT_LINE_CAP, REMINDER_CAP, REMINDER_PERSIST_TURNS,
35
+ _toolTicks, _subActions, _subActionQ,
36
+ tickStart, tickTake, settleToolBlock, isAsyncSpawnResult, isSpawnErrorResult,
37
+ findToolBlock, slimToolResultForDisplay, sweepToolBlocks,
38
+ } from "./tool-display.mjs"
39
+ export { sweepToolBlocks, slimToolResultForDisplay } from "./tool-display.mjs"
164
40
 
165
41
  /** Build the agent callbacks + the shared flushStream for one turn.
166
42
  * askPermission, askQuestion, saveSessionImpl } */
@@ -278,7 +154,7 @@ export function buildToolCallbacks(deps) {
278
154
  : _subActionQ.shift() ?? null
279
155
  if (toolId !== undefined && toolId !== null) _subActions.delete(toolId)
280
156
  }
281
- const isSubagent = name === "subagent" && subAction !== "check" && subAction !== "status" && subAction !== "escalate" && subAction !== "cancel" && subAction !== "panel"
157
+ const isSubagent = name === "subagent" && subAction !== "status" && subAction !== "escalate" && subAction !== "cancel" && subAction !== "panel" && subAction !== "observe" && subAction !== "send"
282
158
  const isEscalate = name === "subagent" && subAction === "escalate"
283
159
  // Subagent complete: mark the earliest running child as done — the block
284
160
  // persists (✓ frozen elapsed header, expandable) as the ONLY carrier of the
@@ -300,9 +176,12 @@ export function buildToolCallbacks(deps) {
300
176
  // 启发式(async eng-coder 先启动时 explore 完成会误冻其块——7.2.3.1/T-F2);
301
177
  // ② spawn 门拒错误({status:"error"} JSON——auto-turn digest spawn 拒绝):
302
178
  // 不冻结任何块(round1 #1——错误路径不冻结 running 块——T-F5);
303
- // ③ subKey undefined 非错误(老回调/测试直调——成功路径未知工具)→ 启发式
304
- // 兜底(既有行为不变——面板单块时与精确冻同效——T-F1)。
305
- const lastError = result.includes(TURN_CAP_MARK) ? "turn cap reached — work may be partial" : null
179
+ // ③ subKey undefined 非错误(老回调/测试直调——成功路径未知工具)→ 不冻结
180
+ // 任何块(CLI-ACTIVITY-DEBLOAT F-2 收窄:finishSubTask 恒 no-op——无 key
181
+ // 无从精确归属,宁可 no-op 不误冻;块由回合尾 freezeAllSubTasks 兜底清场)。
182
+ // SYNC-CANCEL(R6):⏹ 折叠报告带 STOPPED_MARK——块冻结标 stopped 而非 done
183
+ // (lastError 注记 + 事件定格——兜底竞态窗口的 dispatch 精确冻路径)
184
+ const lastError = result.includes(TURN_CAP_MARK) ? "turn cap reached — work may be partial" : result.includes(STOPPED_MARK) ? "stopped by user — work may be partial" : null
306
185
  const hasSubKey = subKey !== undefined && subKey !== null && subKey !== ""
307
186
  if (hasSubKey) {
308
187
  finishSubTaskKey(state, String(subKey), lastError)
@@ -311,48 +190,40 @@ export function buildToolCallbacks(deps) {
311
190
  finishSubTask(state, SUBAGENT_ROLES, lastError)
312
191
  freezeDoneSubTasks(state)
313
192
  }
314
- // Subagent report preview (max 8 lines) displayed directly in conversation
315
- const lines = result.split("\n")
316
- const preview = lines.slice(0, SUBAGENT_PREVIEW_LINES).map((l) => l.slice(0, PREVIEW_LINE_CHARS)).join("\n")
317
- if (preview) pushLine(preview, C.dim)
318
- if (lines.length > SUBAGENT_PREVIEW_LINES) pushLine(` ... (${lines.length - SUBAGENT_PREVIEW_LINES} more lines)`, C.dim)
193
+ // CLI-ACTIVITY-DEBLOAT F-1 (2026-09-10): the conversation-stream report
194
+ // preview (max 8 dim lines) is deleted — the frozen block is the ONLY
195
+ // carrier of the child's report (full text also lives in history for the
196
+ // model; escalate#N keeps its no-preview surface unchanged).
319
197
  }
320
198
  } else if (isEscalate) {
321
199
  // 飞刀 post-op report landed under the subagent tool name — freeze the
322
200
  // escalate#N activity block (no preview; legacy surface).
323
201
  settleToolBlock(state, name, toolId, "completed")
324
- // §7.2.3(round1 #2):escalate 成功返回带 subKey(escalate#N)→ 精确冻;
325
- // 失败/老回调无 subKey escalate 角色启发式兜底(escalate 串行 + 角色限定——
326
- // 既有行为)。
327
- const lastError = result.includes(TURN_CAP_MARK) ? "turn cap reached — work may be partial" : null
328
- if (subKey !== undefined && subKey !== null && subKey !== "") {
329
- finishSubTaskKey(state, String(subKey), lastError)
330
- } else {
331
- finishSubTask(state, ["escalate"], lastError)
332
- }
333
- freezeDoneSubTasks(state)
334
- } else if (name === "consult_check" || name === "consult_stop") {
335
- // Consult session-level settle (2026-08-30 consult review): a session spawns
336
- // N parallel children, so completion must settle ALL of them (single-shot
337
- // finishSubTask froze only the earliest — N-1 stayed "running" then got
338
- // mislabeled "interrupted").
339
- // - individual reply (done:false): settle precisely by r.model.
340
- // - done:true / stopped: settle every remaining consult block.
341
- try {
342
- const r = JSON.parse(result)
343
- if (r?.done || r?.stopped !== undefined) {
344
- finishSubTasksByRole(state, ["consult"], null)
345
- freezeDoneSubTasks(state)
346
- } else if (r?.model) {
347
- finishSubTaskByModel(state, "consult", r.model)
348
- freezeDoneSubTasks(state)
349
- }
350
- } catch {
351
- if (name === "consult_stop") {
352
- finishSubTasksByRole(state, ["consult"], null)
353
- freezeDoneSubTasks(state)
202
+ // §25 D-R17b (R17): async escalate ack ({id, role:"escalate", status:
203
+ // running|queued}) the child KEEPS running skip the freeze like the
204
+ // async spawn path (the block freezes on the ⟦ev⟧done/stopped settle
205
+ // event at flight end). Sync results (async:false) freeze below.
206
+ if (!isAsyncSpawnResult(result)) {
207
+ // §7.2.3(round1 #2):escalate 成功返回带 subKey(escalate#N)→ 精确冻;
208
+ // 失败/老回调无 subKey → 不冻结任何块(F-2 收窄——finishSubTask 恒 no-op,
209
+ // 块由回合尾 freezeAllSubTasks 兜底清场)。
210
+ // SYNC-CANCEL(R6):同上——escalate 路径同款扩展(sync escalate 无 registry——
211
+ // 恒不折叠——扩展仅口径一致——零行为变化)
212
+ const lastError = result.includes(TURN_CAP_MARK) ? "turn cap reached — work may be partial" : result.includes(STOPPED_MARK) ? "stopped by user — work may be partial" : null
213
+ if (subKey !== undefined && subKey !== null && subKey !== "") {
214
+ finishSubTaskKey(state, String(subKey), lastError)
215
+ } else {
216
+ finishSubTask(state, ["escalate"], lastError)
354
217
  }
355
- } /* non-JSON result — leave blocks as-is */
218
+ freezeDoneSubTasks(state)
219
+ }
220
+ } else if (name === "consult_stop") {
221
+ // R17(§25 D-R17a——check 已删):consult_stop = 取消指定会诊会话。被 abort 的
222
+ // children 各自 settle(settleChild)时发 ⟦ev⟧done 冻结自己的卡——此处**不做按
223
+ // 角色整组清扫**(会诊会话可并发——按角色会误冻其他仍在运行的会话的卡,冻结即
224
+ // 截断其活动流——tool-events advisor 复评 🟡1 修正);取消的即时可见性由 abort
225
+ // settle 事件承担(abort 解绕通常在同回合内完成——卡片在其 child settle 即冻结)。
226
+ // 仅结果本身落本工具调用自己的载体块(下方通用分支)。
356
227
  }
357
228
  if (!isSubagent && !isEscalate && name !== "advisor") {
358
229
  // Result lands INSIDE the block (restore parity — the restored carrier
@@ -407,7 +278,7 @@ export function buildToolCallbacks(deps) {
407
278
  // child-stdout/SSE fragments at arbitrary byte boundaries; trimEnd eats
408
279
  // real trailing newlines and routeSubToolOutput's verbatim concat would
409
280
  // glue lines (2026-09-03 修复轮; main path keeps the trimmed form below).
410
- const isSubRelay = SUB_PREFIX_RE.test(name)
281
+ const isSubRelay = RELAY_PREFIX_RE.test(name)
411
282
  const rawText = typeof chunk === "string" ? chunk : String(chunk?.text ?? "")
412
283
  const part = {
413
284
  kind: typeof chunk === "string" ? "text" : (chunk?.kind ?? "text"),
@@ -3,6 +3,7 @@
3
3
  * 超 500 行硬限拆分——启动序列、退出清理序列与退出闭包从 index.mjs 移入本模块)。
4
4
  */
5
5
 
6
+ import { appendFileSync } from "node:fs"
6
7
  import { ansi } from "./ansi.mjs"
7
8
 
8
9
  /** TUI 启动序列:alt buffer + 隐藏光标 + 鼠标/粘贴/键盘增强 + 禁环绕。
@@ -19,8 +20,42 @@ export function writeCleanupSequence(write = (s) => process.stdout.write(s)) {
19
20
  write(ansi.clearScreen + ansi.mouseOff + ansi.bracketedPasteOff + ansi.keyboardPop + ansi.modifyOtherKeysOff + ansi.mainBuffer + ansi.showCursor + ansi.reset + ansi.wrapOn)
20
21
  }
21
22
 
22
- /** 退出清理闭包:保存会话(同步)+ 关闭 MCP 子进程 + 恢复终端。幂等(cleanedUp 守卫)。 */
23
- export function createExitCleanup({ agent, saveSession, closeAllMcp }) {
23
+ // R25(ARCHITECTURE.md §R25 F-R25a——复审 #1):TUI 活动态标志——index.mjs 在终端接管
24
+ // 处(writeStartupSequence 之后)经 setTuiActive(true) 置位,createExitCleanup 清理后清
25
+ // false;bin/thincoder.mjs 崩溃钩子经 restoreTerminalAfterCrash 读同一标志——chat 模式
26
+ // stdout 管道不得收 ANSI(误判即污染输出)。默认 false = 未启动(生产零变化——测试缝同源:
27
+ // T-R25a.2 注入 mock 走同一 setter)。
28
+ let tuiActive = false
29
+
30
+ /** 置/清 TUI 活动态(TUI 启动接管处与退出清理处调用;测试 env 门注入同走此 setter)。 */
31
+ export function setTuiActive(active) {
32
+ tuiActive = active === true
33
+ }
34
+
35
+ /** 读 TUI 活动态(F-2 渲染抑制守卫——render-loop doRender 前查——cleanup 清 false 后不再重绘已恢复的主屏)。 */
36
+ export function isTuiActive() {
37
+ return tuiActive
38
+ }
39
+
40
+ /** R25 崩溃恢复:仅 TUI 活动态执行 writeCleanupSequence(复用本模块清理序列——符号锚)。
41
+ * 测试缝(T-R25a.2——env 门注入):THINCODER_TEST_CLEANUP_OUT 指向文件时序列写入该文件
42
+ * (观察恢复被调 + stdout 管道零 ANSI)——生产不设该 env → 恒 stdout(零变化)。
43
+ * @returns {boolean} 是否执行了恢复(false = TUI 未启动——调用方无需处理) */
44
+ export function restoreTerminalAfterCrash() {
45
+ if (!tuiActive) return false
46
+ const seamOut = process.env.THINCODER_TEST_CLEANUP_OUT
47
+ if (seamOut) {
48
+ writeCleanupSequence((s) => appendFileSync(seamOut, s, "utf8"))
49
+ } else {
50
+ writeCleanupSequence()
51
+ }
52
+ return true
53
+ }
54
+
55
+ /** 退出清理闭包:保存会话 + 关闭 MCP + 恢复终端。幂等(cleanedUp 守卫)。stdin/write 注入缝(测试锁序——缺省生产零变化)。
56
+ * F-1 唯一权威序(RESIZE-MOUSE-LEAK-FIX):① mouseOff DECRST → ② settle ~20ms(DECRST 往返——raw 仍开回显仍关)→
57
+ * ③ raw off → ④ stdin 排空 → ⑤ 恢复屏幕 → ⑥ 清 TUI 活动态。 */
58
+ export function createExitCleanup({ agent, saveSession, closeAllMcp, stdin = process.stdin, write = (s) => process.stdout.write(s) }) {
24
59
  let cleanedUp = false
25
60
  return () => {
26
61
  if (cleanedUp) return
@@ -39,7 +74,14 @@ export function createExitCleanup({ agent, saveSession, closeAllMcp }) {
39
74
  } catch {
40
75
  // Can't close? fine, process is exiting anyway
41
76
  }
42
- process.stdin.setRawMode(false)
43
- writeCleanupSequence()
77
+ // ① mouseOff 单独写(DECRST——不整包 writeCleanupSequence):先于 raw off 停鼠标——消除「回显开而鼠标未停」暴露窗口
78
+ write(ansi.mouseOff)
79
+ // ② settle 20ms:Atomics.wait 同步等(不拆注册点——cleanup 亦挂 process.on("exit") 兜底异常退出——exit 事件仅同步合法)
80
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20)
81
+ // ③ raw off(DECRST 已被终端处理——无新上报可回显)→ ④ 排空:摘 data 监听 + pause——不依赖限时读——在途已读字节丢弃不归 shell
82
+ stdin.setRawMode(false); stdin.removeAllListeners("data"); stdin.pause()
83
+ // ⑤ writeCleanupSequence 余部(mouseOff 已单写——clearScreen/bracketedPasteOff/…/wrapOn 恢复屏幕)
84
+ write(ansi.clearScreen + ansi.bracketedPasteOff + ansi.keyboardPop + ansi.modifyOtherKeysOff + ansi.mainBuffer + ansi.showCursor + ansi.reset + ansi.wrapOn)
85
+ setTuiActive(false) // ⑥ R25:清理完即清活动态(崩溃钩子不再误判——渲染抑制锚)
44
86
  }
45
87
  }
@@ -37,9 +37,13 @@ export function createUpdateNotice(ctx) {
37
37
  const { exec } = await import("node:child_process")
38
38
  const cp = exec("npm install -g thincoder@latest", { windowsHide: true })
39
39
  let stdout = ""
40
+ let done = false
40
41
  cp.stdout?.on("data", (d) => { stdout += d })
41
42
  cp.stderr?.on("data", (d) => { stdout += d })
43
+ cp.on("error", (e) => { if (done) return; done = true; pushLine(`✗ Upgrade failed (${e.message}). Run \`thincoder upgrade\` manually.`, C.error); render() })
42
44
  cp.on("close", (code) => {
45
+ if (done) return
46
+ done = true
43
47
  if (code === 0) {
44
48
  pushLine(`✓ Upgraded to ${result.latest}. Restart to apply.`, C.tool)
45
49
  } else {
@@ -8,6 +8,8 @@
8
8
 
9
9
  import { PROVIDER_PRESETS as PRESETS } from "../config.mjs"
10
10
  import { ansi, C } from "./ansi.mjs"
11
+ import { computeLayout } from "./layout.mjs"
12
+ import { probeChannelModels } from "./model-catalog.mjs"
11
13
 
12
14
  /**
13
15
  * Creates the wizard controller.
@@ -16,16 +18,17 @@ import { ansi, C } from "./ansi.mjs"
16
18
  export function createWizard(ctx) {
17
19
  const { agent, state, pushLine, pushLabel, render, persistRaw } = ctx
18
20
 
19
- /** Candidates for the menu step: existing providers (marked "no key" if missing), unadded presets, custom */
21
+ /** Candidates for the menu step: existing providers (marked "no key" if missing), unadded presets, custom
22
+ * MODEL-SELECTION v2:渠道默认模型 = 单值 `model`(preset 自带;候选清单运行期拉取) */
20
23
  function wizardProviderItems() {
21
24
  const items = []
22
25
  for (const p of agent.providers) {
23
- items.push({ kind: "existing", name: p.name, baseURL: p.baseURL, model: p.model, label: `${p.name} (added${p.apiKey ? "" : ", no key"})` })
26
+ items.push({ kind: "existing", name: p.name, baseURL: p.baseURL, model: p.model ?? "", label: `${p.name} (added${p.apiKey ? "" : ", no key"})` })
24
27
  }
25
28
  for (const [name, p] of Object.entries(PRESETS)) {
26
29
  if (!agent.providers.some((x) => x.name === name)) {
27
30
  items.push({
28
- kind: "preset", name, baseURL: p.baseURL, model: p.model, label: `${name} (${p.desc})`,
31
+ kind: "preset", name, baseURL: p.baseURL, model: p.model ?? "", label: `${name} (${p.desc})`,
29
32
  // 预设自身声明的扩展字段随 preset 直达落盘(code review 🟡——与 pickers preset 路径同构;
30
33
  // claude/gemini 缺 format、deepseek/glm 缺 thinking/maxTokens 会静默错配);不新增提问步。
31
34
  format: p.format, thinking: p.thinking, reasoningEffort: p.reasoningEffort,
@@ -100,6 +103,19 @@ export function createWizard(ctx) {
100
103
  }
101
104
  if (w.error) lines.push({ text: ` ${w.error}`, color: C.error })
102
105
  w.lines = lines
106
+ // A4(第 20 批 §12.5——D-SS6):provider 步选中行自动滚入可视窗——renderWizard 是索引变化的单一路径(产出即一致);
107
+ // winH 走 computeLayout(同 pickers.mjs 口径)+ try/catch 兜底 8(无 dims/测试环境不崩)。
108
+ if (w.step === "provider") {
109
+ let winH
110
+ try {
111
+ winH = Math.max(1, (computeLayout(state, { cols: (state.dims?.get() ?? {}).cols ?? (process.stdout.columns || 80), rows: (state.dims?.get() ?? {}).rows ?? (process.stdout.rows || 24) }).panels.picker?.h ?? lines.length + 1) - 1)
112
+ } catch {
113
+ winH = 8 // safe fallback for mocks without dims(同 pickers.mjs 兜底口径)
114
+ }
115
+ if (w.selectedLine < w.scroll) w.scroll = w.selectedLine
116
+ if (w.selectedLine >= w.scroll + winH) w.scroll = w.selectedLine - winH + 1
117
+ w.scroll = Math.max(0, Math.min(w.scroll, Math.max(0, lines.length - winH)))
118
+ }
103
119
  render()
104
120
  }
105
121
 
@@ -109,8 +125,7 @@ export function createWizard(ctx) {
109
125
  w.step = "name"
110
126
  } else {
111
127
  w.fields = { name: item.name, baseURL: item.baseURL, model: item.model }
112
- // preset 直达:预设声明的扩展字段(format/thinking/maxTokens/chatPath…)直接带进 fields——
113
- // 无 format 提问步(T-C4)但落盘不丢字段(code review 🟡——picker preset 路径同款复制)。
128
+ // preset 直达:其余扩展字段照旧(渠道默认模型 = 单值 model——随 fields 落盘)
114
129
  for (const k of ["format", "thinking", "reasoningEffort", "maxTokens", "chatPath"]) {
115
130
  if (item[k]) w.fields[k] = item[k]
116
131
  }
@@ -145,46 +160,71 @@ export function createWizard(ctx) {
145
160
 
146
161
  function cancelWizard() {
147
162
  state.wizard = null
148
- pushLine("Skipped initial setup. Use /model to add providers and configure API keys anytime.", C.dim)
163
+ // MODEL-MERGE-SESSION 引导 A(F-6):有 provider defaultModel 未设时指引 /config 入口
164
+ const hint = (agent.providers?.length ?? 0) > 0 && !agent.config?.defaultModel
165
+ ? "Skipped initial setup. 已配置渠道但 config.defaultModel 未设——新会话无起点:/config → 默认模型 设置一次(或 /model 仅改本会话)。"
166
+ : "Skipped initial setup. Use /model to add providers and configure API keys anytime."
167
+ pushLine(hint, C.dim)
149
168
  render()
150
169
  }
151
170
 
152
- /** Wizard complete: write provider (update if exists), set active, persist, then open model picker */
171
+ /** Wizard complete: write provider (update if exists) with its single default model (`model`),
172
+ * set config.defaultModel(裁定⑦——首配模型即写 defaultModel——新会话起点), then open the
173
+ * session model picker. 加渠道 = 配置写入面——落盘后探一次 `/models`(M9:探不通标「不可用」
174
+ * + 明示原因,不阻断保存)。 */
153
175
  async function finishWizard() {
154
176
  const f = state.wizard.fields
155
177
  state.wizard = null
156
178
  // D-C2:format 非默认(anthropic/google)时落盘;openai = 默认省略(与 D-C1 picker 路径同构)
179
+ // MODEL-SELECTION v2:渠道默认模型 = 单值 model
157
180
  const providerRec = { name: f.name, baseURL: f.baseURL, model: f.model, apiKey: f.key }
158
181
  if (f.format && f.format !== "openai") providerRec.format = f.format
159
- // code review 🟡:preset 直达带来的扩展字段一并落盘(truthy 语义与 pickers preset 分支一致——
160
- // thinking: null 不落;Custom 路径无这些字段不受影响)
161
182
  for (const k of ["thinking", "reasoningEffort", "maxTokens", "chatPath"]) {
162
183
  if (f[k]) providerRec[k] = f[k]
163
184
  }
185
+ // D-F5a(wizard finishWizard——清单外同型写回补正)先盘后存:磁盘 fresh raw 单操作
186
+ // (upsert 目标项 + defaultModel + 清 legacy 字段)——冲突放弃不留下内存 ghost(F5 约定)
187
+ await persistRaw((raw) => {
188
+ raw.providers ??= []
189
+ const existing = raw.providers.find((p) => p?.name === f.name)
190
+ if (existing) Object.assign(existing, providerRec)
191
+ else raw.providers.push(providerRec)
192
+ raw.defaultModel = `${f.name}:${providerRec.model}`
193
+ delete raw.activeProvider
194
+ delete raw.activeModel
195
+ // 渠道老字段(models 候选清单)由 config-migrate 在下次 load 统一清理(迁移唯一权威)
196
+ })
164
197
  const existing = agent.providers.find((p) => p.name === f.name)
165
198
  if (existing) Object.assign(existing, providerRec)
166
199
  else agent.providers.push(providerRec)
167
200
  agent.activeProvider = f.name
168
- agent.activeModel = null
201
+ agent.activeModel = providerRec.model
169
202
  agent.provider = { ...agent.providers.find((p) => p.name === f.name) }
203
+ agent.provider.model = agent.activeModel
170
204
  if (agent.config?.agent?.compactThresholdAuto) {
171
205
  const { resolveCompactThreshold } = await import("../config.mjs")
172
- agent.config.agent.compactThreshold = resolveCompactThreshold(null, f.model).value
206
+ agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
173
207
  }
174
- await persistRaw((raw) => {
175
- raw.providers = agent.providers
176
- raw.activeProvider = f.name
177
- raw.activeModel = undefined // reset to default model
178
- })
179
- agent.config.activeProvider = f.name
180
- agent.config.activeModel = null
208
+ // agent.config loadConfig merged——无 active* 键可写——defaultModel 随内存 merged 更新
209
+ agent.config.defaultModel = `${f.name}:${agent.activeModel}`
181
210
  pushLabel(`❯ Setup`, ansi.bold + C.tool)
182
- pushLine(`Setup complete: ${f.name} / ${f.model} (saved to config)`, C.tool)
211
+ pushLine(`Setup complete: ${f.name} / ${agent.activeModel} (defaultModel 已设——新会话起点)`, C.tool)
212
+ // M9 配置阶段准入:加渠道属配置写入面——保存已落,探一次 `/models`(探不通标「不可用」+ 明示原因;不阻断)
213
+ const channel = agent.providers.find((p) => p.name === f.name) ?? providerRec
214
+ const probe = await probeChannelModels(channel)
215
+ if (probe.ok) {
216
+ delete channel._unavailable
217
+ pushLine(`${f.name}: /models 可用(${probe.list.length} 个模型可候选)`, C.tool)
218
+ } else {
219
+ channel._unavailable = true
220
+ pushLine(`${f.name} 不可用 — ${probe.message}`, C.error)
221
+ }
183
222
  // embedding key: if provided, enable vector search; if not, show how to enable later
184
223
  if (f.embedkey) {
224
+ // D-F5b 语义先盘后存(embedding 单键补丁——冲突放弃不留 ghost)
225
+ await persistRaw((raw) => { raw.embedding = { ...(raw.embedding ?? {}), apiKey: f.embedkey } })
185
226
  agent.config.embedding ??= {}
186
227
  agent.config.embedding.apiKey = f.embedkey
187
- await persistRaw((raw) => { raw.embedding = { ...(raw.embedding ?? {}), apiKey: f.embedkey } })
188
228
  if (agent.memory && !agent.memory.embedder) {
189
229
  const { createEmbedder } = await import("../embedding.mjs")
190
230
  agent.memory.embedder = createEmbedder(agent.config.embedding)
@@ -193,7 +233,7 @@ export function createWizard(ctx) {
193
233
  } else {
194
234
  pushLine(`Vector search disabled (memory falls back to text-only search). Run /config embedkey <key> to enable.`, C.dim)
195
235
  }
196
- pushLine(`Select model (Esc to keep ${f.model})`, C.dim)
236
+ pushLine(`Select model (Esc to keep ${agent.activeModel})`, C.dim)
197
237
  ctx.openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error))
198
238
  }
199
239
 
@@ -0,0 +1,38 @@
1
+ /** wrapped-spawn.mjs — TUI-STDERR-CAPTURE F-1/F-3:包装父
2
+ * spawn 子(自身 bin)tee stderr → 终端 + crash-reports/tui-stderr-<ts>-<pid>.log(外部终止/
3
+ * native abort——fd 2 进程内不可改——诊断唯一默认捕获路)。子死 → 日志收尾 → 同码退(null 映射
4
+ * code??(signal?1:0)——评审 #1);spawn error → 注日志 + exit 1(评审 #5——不挂死)。 */
5
+ import { appendFileSync, mkdirSync, writeFileSync } from "node:fs"
6
+ import { spawn } from "node:child_process"
7
+ import { fileURLToPath } from "node:url"
8
+ import { crashReportsDir } from "../crash-reports.mjs"
9
+
10
+ // F-3 信号语义(raw mode 既有 key-handler 双按语义——Ctrl+C = stdin 字节不产生信号 → 子正常退 → 父收
11
+ // exit 同码退):父忽略 SIGINT/SIGTERM——tee 不被打断。2026-09-09 实测:Windows process.kill(SIGINT)
12
+ // = 硬杀 ≠ 控制台 Ctrl+C 事件(handler 不触发)——信号面测试走 mock(真控制台端到端留发布前手动 QA)。
13
+ export const ignoreSignal = () => {} // no-op 单一引用——父信号 + stderr-error 监听共用——测试可精确复原
14
+ export function spawnTuiWrapped({ dir = crashReportsDir(), script = fileURLToPath(new URL("../../bin/thincoder.mjs", import.meta.url)), spawnImpl = spawn, exitImpl = (code) => process.exit(code) } = {}) {
15
+ // ① mkdir 前置(评审 #2——prepareCrashReporting 只在子内跑——首启目录缺失会静默不包装)+ 开日志
16
+ //(文件头元信息行:时间/pid/argv——F-2——0600 append)——失败 → false:不包装直接跑现逻辑(尽力面)
17
+ let logPath = null
18
+ try {
19
+ mkdirSync(dir, { recursive: true })
20
+ logPath = `${dir}/tui-stderr-${Date.now()}-${process.pid}.log`
21
+ writeFileSync(logPath, `# tui-stderr ${new Date().toISOString()} pid=${process.pid} argv=${JSON.stringify(process.argv)}\n`, { flag: "a", mode: 0o600 })
22
+ } catch { return false }
23
+ process.on("SIGINT", ignoreSignal); process.on("SIGTERM", ignoreSignal) // 父忽略信号(F-3)——tee 不被打断
24
+ process.stderr.on("error", ignoreSignal) // 父 stderr 异步 EPIPE(终端/管道已关)→ 不炸父——tee 继续
25
+ const note = (s) => { try { appendFileSync(logPath, s) } catch { /* 落盘尽力面——不阻断 tee */ } }
26
+ let exitCode = null, exitSignal = null, settled = false
27
+ const finish = (code) => { if (!settled) { settled = true; exitImpl(code) } }
28
+ const child = spawnImpl(process.execPath, [script, ...process.argv.slice(2)], {
29
+ stdio: ["inherit", "inherit", "pipe"], // 子 stderr pipe → tee;stdin/stdout 继承(TTY 原样)
30
+ env: { ...process.env, THINCODER_TUI_WRAPPED: "1" }, // env 门——子内判定不包装——纯现逻辑(红线)
31
+ windowsHide: false,
32
+ })
33
+ child.stderr.on("data", (chunk) => { try { process.stderr.write(chunk) } catch { /* 终端已关——日志仍落 */ } note(chunk) }) // ② tee 双写:终端实时 + 日志
34
+ child.on("exit", (code, signal) => { exitCode = code; exitSignal = signal; setTimeout(() => finish(exitCode ?? (exitSignal ? 1 : 0)), 30_000).unref() }) // ③ exit 记码;兜底 30s 强退
35
+ child.on("close", () => finish(exitCode ?? (exitSignal ? 1 : 0))) // ④ close = stderr 尾数据全收(AC-2)→ 同码退
36
+ child.on("error", (err) => { note(`# spawn error: ${err.message}\n`); finish(1) }) // ⑤ spawn 失败不发 exit 只发 error
37
+ return true
38
+ }
@@ -1,56 +0,0 @@
1
- You are a coding subagent. The parent agent dispatched you to handle a self-contained coding task. The parent CANNOT see your context — it only sees your final report.
2
-
3
- ## Your role (identity — read before you code)
4
-
5
- You are an IMPLEMENTER with independent judgment — not a typewriter.
6
-
7
- 1. **Evidence discipline**: every factual/behavioral assertion you make MUST be
8
- verified from the code/docs in front of you (read them, cite file:line) —
9
- or explicitly marked `unverified`. NEVER assert "Known behavior…",
10
- "I'm confident…", or rely on remembered API semantics when the source is
11
- readable — a behavioral question is an EVIDENCE question, not a reasoning
12
- question.
13
- 2. **Neutrality**: you implement the design; you are not the designer. If the
14
- design conflicts with what you find in the code (an interface change broke a
15
- caller, a referenced symbol does not exist), STOP and report the conflict
16
- to the parent — do not silently adapt. The parent decides; you surface.
17
- 3. **Boundary**: your task = the parent's task brief (files, acceptance
18
- criteria). Do not expand it. Findings that touch things outside the brief
19
- (other modules, parent-side docs) go in a trailing "out-of-scope note" in
20
- your report — no action without the parent's word.
21
-
22
- - before you start coding, locate the owning design doc for this change (docs/design/ — via the doc map); if it exists, note the change in it (变更记录/设计注); if not, create it and register it in the map. Then code. No exemption — even one-line fixes.
23
-
24
- Guidelines:
25
- - Work independently: use doc_search to learn project conventions and design, repo_outline to understand structure, then code_search to find implementations.
26
- Don't write code until you know what the project intends.
27
- - COMPLETE delivery: solve the ENTIRE task the parent gave you — every requirement, every file, every acceptance criterion. Nothing less. Do what was asked, fully. No opportunistic cleanup, no speculative generality, no half-finished refactors. When you finish, include a delivery table (see Discipline rules) — every requirement either Done, Simplified, or Not done. The parent doesn't read your diff; it reads your report.
28
- - Write code one file at a time, verify each before moving on — don't write multiple files at once without checking each along the way:
29
- 1. After every write/edit of a file: run a syntax/lint check to catch parse errors immediately
30
- 2. After a logical group of changes: run the relevant tests to confirm behavior
31
- 3. Before finishing: run tests relevant to your changes; run the full test suite only if you changed core infrastructure (agent loop, provider protocol, config schema, tool execution, memory schema)
32
- - Be thorough: include what you did, which files you changed, why, and any caveats
33
- - If the task is ambiguous, note the ambiguity in your report; do not ask the user
34
- - It is always OK to say "this is too hard for me." Bad work is worse than no work — you will not be penalized for escalating
35
- - BEFORE finishing, do a final review of your work:
36
- 1. Run relevant tests — confirm all pass
37
- 2. If no existing test covers your change, add at least one test
38
- 3. Read every file you changed — catch leftover debug code, stale comments, or incomplete edits
39
- 4. Check that comments and docstrings match what the code actually does
40
- 5. Verify imports/dependencies are correct — no stale or missing references
41
- - Your last message IS the report the parent sees — it is the ONLY thing the parent receives. Make it complete and self-contained. A report that fails this checklist is sent back for expansion, costing an extra turn:
42
- 1. What you changed and why
43
- 2. The path of every file you touched
44
- 3. How you verified the change (tests run, commands executed, with results)
45
- 4. **Delivery transparency table** — mandatory. Format:
46
- | # | Status | Requirement |
47
- |---|--------|-------------|
48
- | 1 | ✅ Done | (fully covered) |
49
- | 2 | ⚠️ Simplified | (delivered but simpler — explain the gap) |
50
- | 3 | ❌ Not done | (NOT implemented — including anything you wanted to defer) |
51
- Every requirement point from the parent's task must appear in exactly one row. There is no "deferred" or "later" column — pushing to later means "not done now," so it goes under ❌.
52
- 5. consistency self-check: does the delivery match the task instruction and the board design doc (if any)? Report deviations explicitly. Fix implementation deviations (partial implementation / silent simplification) so the delivery matches the doc before reporting; report genuine doc drift or out-of-scope changes.
53
-
54
- IMPORTANT — Tool permissions: when you see "permission denied by user" for a tool, it means the parent has not granted that tool.
55
- This is expected: your job is to write a detailed report of what SHOULD be done, not to force tool execution.
56
- Describe the needed changes clearly in your report so the parent agent can apply them.