thincoder 0.12.60 → 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 (152) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/README.md +8 -6
  3. package/bin/thincoder.mjs +27 -122
  4. package/package.json +3 -2
  5. package/src/abort-provenance.mjs +116 -0
  6. package/src/acp/bridge.mjs +38 -17
  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 +36 -134
  12. package/src/advisor/project-context.mjs +194 -0
  13. package/src/advisor/repos.mjs +17 -40
  14. package/src/advisor/run.mjs +124 -329
  15. package/src/advisor/truncate.mjs +57 -0
  16. package/src/advisor.mjs +3 -2
  17. package/src/agent/completion.mjs +1 -1
  18. package/src/agent/dispatch.mjs +47 -12
  19. package/src/agent/helpers.mjs +71 -13
  20. package/src/agent/record-results.mjs +13 -5
  21. package/src/agent/relay-prefix.mjs +39 -0
  22. package/src/agent/run-stages.mjs +21 -6
  23. package/src/agent/setup-reminders.mjs +16 -9
  24. package/src/agent/setup.mjs +92 -128
  25. package/src/agent/spawn-child.mjs +26 -9
  26. package/src/agent-tools/advisor-async.mjs +70 -180
  27. package/src/agent-tools/advisor-settle.mjs +231 -0
  28. package/src/agent-tools/advisor.mjs +69 -20
  29. package/src/agent-tools/batch-segment.mjs +195 -0
  30. package/src/agent-tools/consult.mjs +23 -10
  31. package/src/agent-tools/design-token.mjs +14 -1
  32. package/src/agent-tools/digest-budget.mjs +76 -0
  33. package/src/agent-tools/eng.mjs +3 -3
  34. package/src/agent-tools/escalate-async.mjs +16 -13
  35. package/src/agent-tools/read-history.mjs +13 -3
  36. package/src/agent-tools/review-streak.mjs +93 -0
  37. package/src/agent-tools/settings.mjs +130 -17
  38. package/src/agent-tools/subagent-actions.mjs +15 -6
  39. package/src/agent-tools/subagent-async.mjs +66 -14
  40. package/src/agent-tools/subagent-panel.mjs +22 -15
  41. package/src/agent-tools/subagent-run.mjs +9 -6
  42. package/src/agent-tools/subagent-scheduler.mjs +57 -8
  43. package/src/agent-tools/subagent-spawn.mjs +63 -16
  44. package/src/agent-tools/subagent.mjs +175 -49
  45. package/src/agent-tools/verify.mjs +13 -34
  46. package/src/agent-tools.mjs +1 -0
  47. package/src/agent.mjs +38 -21
  48. package/src/cli/distill-command.mjs +2 -2
  49. package/src/cli/make-agent.mjs +23 -7
  50. package/src/cli/memory-command.mjs +2 -2
  51. package/src/cli/setup-wizard.mjs +29 -9
  52. package/src/completions.mjs +114 -0
  53. package/src/config-migrate.mjs +70 -0
  54. package/src/config.mjs +132 -63
  55. package/src/conventions.mjs +223 -0
  56. package/src/crash-reports.mjs +7 -2
  57. package/src/expand-home.mjs +16 -0
  58. package/src/generate-title.mjs +1 -1
  59. package/src/hooks.mjs +7 -3
  60. package/src/memory/code-index.mjs +9 -3
  61. package/src/memory/code-sync.mjs +70 -31
  62. package/src/memory/delete.mjs +2 -0
  63. package/src/memory/docs.mjs +10 -6
  64. package/src/memory/file-walk.mjs +109 -0
  65. package/src/memory/schema.mjs +15 -3
  66. package/src/model-ref.mjs +66 -0
  67. package/src/model-specs.mjs +42 -8
  68. package/src/prompt-overlays.mjs +73 -16
  69. package/src/prompts/advisor-design.md +18 -8
  70. package/src/prompts/advisor-round1.md +7 -1
  71. package/src/prompts/advisor-round2.md +13 -2
  72. package/src/prompts/advisor-round3.md +13 -2
  73. package/src/prompts/common.md +115 -0
  74. package/src/prompts/consult-base.md +2 -0
  75. package/src/prompts/discipline-engineering.md +217 -0
  76. package/src/prompts/discipline-normal.md +179 -0
  77. package/src/prompts/persona-coder.md +21 -0
  78. package/src/prompts/persona-eng-coder.md +37 -0
  79. package/src/prompts/persona-eng-designer.md +55 -0
  80. package/src/prompts/persona-engineering.md +54 -0
  81. package/src/prompts/persona-explore.md +15 -0
  82. package/src/prompts/persona-normal.md +27 -0
  83. package/src/prompts/persona-plan.md +26 -0
  84. package/src/provider/anthropic.mjs +4 -4
  85. package/src/provider/core.mjs +13 -32
  86. package/src/provider/errors.mjs +26 -1
  87. package/src/provider/google.mjs +5 -6
  88. package/src/provider/index.mjs +2 -1
  89. package/src/provider/list-models.mjs +93 -0
  90. package/src/provider/rate.mjs +2 -1
  91. package/src/provider/responses.mjs +5 -3
  92. package/src/provider/sse.mjs +3 -4
  93. package/src/proxy.mjs +9 -14
  94. package/src/session-slots.mjs +7 -2
  95. package/src/session.mjs +59 -38
  96. package/src/token-ttl.mjs +2 -1
  97. package/src/tools/{system.mjs → bash.mjs} +6 -243
  98. package/src/tools/file.mjs +30 -10
  99. package/src/tools/git.md +1 -1
  100. package/src/tools/git.mjs +15 -34
  101. package/src/tools/index.mjs +4 -2
  102. package/src/tools/ops.mjs +20 -7
  103. package/src/tools/question.md +1 -0
  104. package/src/tools/question.mjs +26 -0
  105. package/src/tools/read.md +1 -1
  106. package/src/tools/read_image.md +1 -1
  107. package/src/tools/search.mjs +236 -0
  108. package/src/tui/agent-turn.mjs +32 -13
  109. package/src/tui/ansi.mjs +2 -0
  110. package/src/tui/clipboard.mjs +7 -1
  111. package/src/tui/cmd-advisor.mjs +3 -2
  112. package/src/tui/cmd-config.mjs +108 -37
  113. package/src/tui/cmd-eng.mjs +11 -27
  114. package/src/tui/cmd-exit.mjs +6 -8
  115. package/src/tui/cmd-model.mjs +14 -12
  116. package/src/tui/cmd-reindex.mjs +7 -0
  117. package/src/tui/cmd-submodel.mjs +8 -5
  118. package/src/tui/cmd-undo.mjs +4 -3
  119. package/src/tui/index.mjs +38 -37
  120. package/src/tui/key-handler.mjs +61 -17
  121. package/src/tui/key-modes.mjs +86 -8
  122. package/src/tui/layout.mjs +18 -10
  123. package/src/tui/model-catalog.mjs +89 -0
  124. package/src/tui/model-picker.mjs +498 -0
  125. package/src/tui/mouse.mjs +47 -10
  126. package/src/tui/pickers.mjs +28 -410
  127. package/src/tui/render-frame.mjs +32 -16
  128. package/src/tui/render-loop.mjs +2 -0
  129. package/src/tui/render-segments.mjs +5 -19
  130. package/src/tui/render.mjs +37 -5
  131. package/src/tui/slash-commands.mjs +2 -2
  132. package/src/tui/subagent-blocks.mjs +69 -90
  133. package/src/tui/subagent-children.mjs +50 -64
  134. package/src/tui/subagent-freeze.mjs +40 -43
  135. package/src/tui/subagent-panel.mjs +21 -66
  136. package/src/tui/suspension-drive.mjs +28 -82
  137. package/src/tui/tool-args.mjs +4 -2
  138. package/src/tui/tool-display.mjs +4 -3
  139. package/src/tui/tool-events.mjs +23 -16
  140. package/src/tui/tui-lifecycle.mjs +18 -5
  141. package/src/tui/wizard.mjs +52 -18
  142. package/src/tui/wrapped-spawn.mjs +38 -0
  143. package/src/prompts/coder.md +0 -13
  144. package/src/prompts/discipline.md +0 -84
  145. package/src/prompts/eng-coder.md +0 -19
  146. package/src/prompts/engineering-sub.md +0 -14
  147. package/src/prompts/engineering.md +0 -87
  148. package/src/prompts/explore.md +0 -12
  149. package/src/prompts/main.md +0 -34
  150. package/src/prompts/methodology-template.md +0 -38
  151. package/src/prompts/plan.md +0 -9
  152. package/src/prompts/system.md +0 -44
@@ -25,13 +25,14 @@
25
25
  * domain still follows the consuming turn's tier — no family exception).
26
26
  */
27
27
  import { relative, isAbsolute } from "node:path"
28
- import { runAgent, createAgent, CODER_OVERLAY, DEFAULT_SUBAGENT_TURNS } from "../agent.mjs"
28
+ import { runAgent, createAgent, DEFAULT_SUBAGENT_TURNS } from "../agent.mjs"
29
29
  import { runWithContinue, TURN_CAP_MARK, wrapChildCallbacks } from "../agent/spawn-child.mjs"
30
30
  import { logEvent } from "../log.mjs"
31
+ import { deathLine } from "../abort-provenance.mjs"
31
32
  import {
32
- mergeChildMutations, runningPoolCount, poolDomainOf, poolLimitsFor, ASYNC_POOL_LIMITS,
33
+ mergeChildMutations, runningPoolCount, poolDomainOf, poolLimitsFor, ASYNC_POOL_LIMITS, enqueueAsk,
33
34
  } from "./subagent-async.mjs"
34
- import { refreshQueuedTokens } from "./subagent-scheduler.mjs"
35
+ import { refreshQueuedTokens, nextSubagentId } from "./subagent-scheduler.mjs"
35
36
  import { mutationSeqOf } from "./advisor-async.mjs"
36
37
  // ASYNC-RESULT-CONTAINER.md D3/D6:settle 公共收尾单点 + child signal 构建单点
37
38
  import { buildChildSignal, settleAsyncEntry } from "./async-settle.mjs"
@@ -146,12 +147,13 @@ export function launchEscalateAsync(parent, ctx, launch) {
146
147
  // counter at launch — the returned id stays stable while the entry sits queued.
147
148
  // The [model] token (TUI block creation) is DEFERRED to actual start so queued
148
149
  // flights don't paint an empty panel block (subagent-parity).
149
- parent._subAgentCounter = (parent._subAgentCounter ?? 0) + 1
150
- const relayPrefix = `escalate#${parent._subAgentCounter}/`
151
- const id = parent._subAgentCounter
150
+ // SUBAGENT-ID-COUNTER-AGENT(2026-09-09):取号统一走 nextSubagentId(池活续号
151
+ // 兜底——counter 载体= agent 本体 _subAgentCounter——跨 run/跨压缩存活)。
152
+ const id = nextSubagentId(parent)
153
+ const relayPrefix = `escalate#${id}/`
152
154
  const entry = {
153
155
  id, role: "escalate", relayPrefix,
154
- _pool: poolDomainOf("escalate"), // other — shares the domain with explore/plan/coder (§24 D-24a)
156
+ _pool: poolDomainOf("escalate"), // other — shares the domain with explore/plan/coder (§11.1 D-24a)
155
157
  status: "queued",
156
158
  position: undefined,
157
159
  report: null, error: null, done: false, cancelled: false,
@@ -173,8 +175,9 @@ export function launchEscalateAsync(parent, ctx, launch) {
173
175
  // D6 buildChildSignal 单点(ASYNC-RESULT-CONTAINER.md——D5 同款:_sessionSignal 兜底)。
174
176
  const baseSignal = buildChildSignal(parent, ctx)
175
177
  if (baseSignal) {
176
- if (baseSignal.aborted) ctrl.abort()
177
- else baseSignal.addEventListener("abort", () => ctrl.abort(), { once: true })
178
+ // §20.3 站点 #10(第 24 批):hop 逐跳保 reason
179
+ if (baseSignal.aborted) ctrl.abort(baseSignal.reason)
180
+ else baseSignal.addEventListener("abort", () => ctrl.abort(baseSignal.reason), { once: true })
178
181
  }
179
182
  // Turn mirror (⟦ev⟧turn from the child runAgent → entry.turn/maxTurns — status parity).
180
183
  const flight = async () => {
@@ -189,7 +192,7 @@ export function launchEscalateAsync(parent, ctx, launch) {
189
192
  config: parent.config,
190
193
  cwd: parent.cwd,
191
194
  memory: parent.memory,
192
- overlay: CODER_OVERLAY,
195
+ // G3(施工②):overlay 摘除——coder 人格槽由 assemblePrompt 场景表承载(G3 映射)。
193
196
  role: "coder",
194
197
  })
195
198
  entry.childAgent = child // settle 分类/status touched 摘要绑定(start 时刻)
@@ -220,8 +223,7 @@ export function launchEscalateAsync(parent, ctx, launch) {
220
223
  : async (name, toolArgs) => {
221
224
  if (!ctx.onPermissionRequest) return false
222
225
  const ask = () => ctx.onPermissionRequest(`escalate/${name}`, toolArgs)
223
- parent._permQueue = (parent._permQueue ?? Promise.resolve()).then(ask, ask)
224
- return parent._permQueue
226
+ return enqueueAsk(parent, "_permQueue", ask)
225
227
  }
226
228
  const report = await runWithContinue(
227
229
  (childAgent, input, cbs, opts) => runner(childAgent, input, cbs, opts),
@@ -252,7 +254,8 @@ export function launchEscalateAsync(parent, ctx, launch) {
252
254
  // 运行失败/中止:错误文本落 entry.error(子代理同款——cancel 分支忽略它;
253
255
  // Ctrl+I 中止的残条目由收尾消化带错误文本——不落空 "(no report)" digest)。
254
256
  const child = entry.childAgent
255
- entry.error = `escalate (${tag}) error: ${e?.message ?? String(e)}\nPartial output: ${(child?._capturedOutput ?? "").slice(0, 2000)}`
257
+ // §20.3 3 条合成器(第 24 批):原 message 前缀逐字保留 + 来源后缀
258
+ entry.error = `escalate (${tag}) error: ${deathLine(e, entry.controller?.signal)}\nPartial output: ${(child?._capturedOutput ?? "").slice(0, 2000)}`
256
259
  })
257
260
  .finally(() => {
258
261
  // settle 公共收尾单点(ASYNC-RESULT-CONTAINER.md D3——settleAsyncEntry):日志三连
@@ -26,12 +26,14 @@
26
26
  * filter surface; path = "cwd:<dir>" → list every slot stored for that directory
27
27
  * (slot number + full file path + title/message count/updatedAt — no dead-slot
28
28
  * filtering, v1 decision). Single-file retrieval is guarded by a line-scan cap
29
- * (READ_HISTORY_SCAN_MAX) — an oversized file is refused before it is read whole.
29
+ * (READ_HISTORY_SCAN_MAX — an oversized file is refused before it is read whole)
30
+ * plus a message-count cap (READ_HISTORY_MAX_MESSAGES = 50,000 — L24 双保险第二道).
30
31
  *
31
32
  * readonly: true — planMode pass / no permission ask. Registered depth-0 only:
32
33
  * subagents get their own throwaway history, so querying "the session" from a
33
34
  * child would be semantically confusing (SESSION.md §9.5 refinement 1 + §13 T-R19.4).
34
- * Mirrored 1:1 in thincoder-vscode/src/agent-tools/read-history.mjs.
35
+ * §13 R19 extension mirrored per SESSION.md §13 — double-end isomorphic, no
36
+ * cross-end byte test (thincoder-vscode/src/agent-tools/read-history.mjs).
35
37
  */
36
38
 
37
39
  import { openSync, readSync, closeSync, readFileSync, existsSync, statSync } from "node:fs"
@@ -46,6 +48,10 @@ const VALID_ROLES = new Set(["user", "assistant", "tool"])
46
48
  /** 单槽检索行扫护栏(SESSION.md §13 D-R19a——评审 #3 定稿:超限不再读全文,返回定稿错误文案)。 */
47
49
  export const READ_HISTORY_SCAN_MAX = 200_000
48
50
 
51
+ /** L24 消息数预算(评审 #2 钉死——双保险第二道):行扫按物理 \n 行计——JSON 单行槽
52
+ * 行扫不设防——parse 后 history 数组长度超限即拒(同款定稿文案——双端同常量同文案)。 */
53
+ export const READ_HISTORY_MAX_MESSAGES = 50_000
54
+
49
55
  /** 超限错误文案(SESSION.md §13——逐字定稿——T-R19.7 断言)。 */
50
56
  const TOO_LARGE_ERROR = JSON.stringify({ error: "session too large — refine keyword or since/until" })
51
57
 
@@ -181,6 +187,10 @@ function querySessionFile(pathArg, { role, kwRe, tool, since, until, direction,
181
187
  if (!data || typeof data !== "object" || !Array.isArray(data.history)) {
182
188
  return `Error: ${file} is not a valid session file (no history array)`
183
189
  }
190
+ if (data.history.length > READ_HISTORY_MAX_MESSAGES) {
191
+ // L24 消息数第二道(parse 后——行扫按物理行、单行 JSON 槽行扫不设防——超限同款拒绝)。
192
+ return TOO_LARGE_ERROR
193
+ }
184
194
  const matched = data.history.filter((m) => matches(m, { role, kwRe, tool, since, until }))
185
195
  return formatMatches(matched, direction, limit)
186
196
  }
@@ -222,7 +232,7 @@ export const readHistoryTool = {
222
232
  "Cross-session (path, optional): a session file path deep-queries THAT session's history with the same filters " +
223
233
  "(relative paths resolve against the project cwd); \"cwd:<dir>\" lists every session slot stored for that directory — " +
224
234
  "one line per slot: slot number + full session file path + title + message count + updatedAt; copy a listed file path into path= to deep-query it. " +
225
- "A session file over 200,000 lines is refused (\"session too large\") instead of being read whole.\n" +
235
+ "A session file over 50,000 messages or 200,000 lines is refused (\"session too large\") instead of being read whole.\n" +
226
236
  SEARCH_FAMILY_GUIDE,
227
237
  parameters: {
228
238
  type: "object",
@@ -0,0 +1,93 @@
1
+ /**
2
+ * review-streak.mjs — design-review failure streak guard(第 33 批——2026-09-11)。
3
+ *
4
+ * 同一 doc-set 的 design 评审**连续**未产出可用结算(宿主截断尾五 kind + 陈旧结算 +
5
+ * 凭证落盘失败 + 无报告结算)达 `MAX_DESIGN_REVIEW_STREAK` 次 ⇒ 后续发起被拒
6
+ * (工具层预检 + `runAdvisorReview` 内防线两级——零 LLM / 不耗轮次 / 不置「评审已覆盖」)。
7
+ * 设计权威 = `docs/design/ADVISOR-CONVERGENCE.md` §17(需求 = requirements 档 §12 F28/F29 + N20/N21)。
8
+ *
9
+ * 中立模块(**不 import 任何 src/ 模块**——打断潜在环,§17.5 模块图):
10
+ * - `normAbs` 自 advisor-settle.mjs 迁入(原处 re-export——既有 import 面零变);
11
+ * - `docSetKey` 自 advisor-async.mjs 迁入(原为私有——零 import 面)。
12
+ * 分类单源:`designReviewOutcome` 为异步结算 / 同步面两计数点共用(D-SK8——防两处分类漂移)。
13
+ * 载体 = 会话级内存 `agent._designReviewStreaks`(Map——不落盘、不进 session 文件;
14
+ * eng 模式切换不清护栏——与 `_advisorRuns` 刻意不同步,§17.9 #5 定案)。
15
+ */
16
+ import { join } from "node:path"
17
+
18
+ /** 连续未产出可用结算的阈值(三振——§17.2 表 1 选定 N=3;`MAX_ADVISOR_ROUNDS` 零改动)。 */
19
+ export const MAX_DESIGN_REVIEW_STREAK = 3
20
+
21
+ /** ABS 归一(cwd 相对 → cwd 拼接)——陈旧判定 / 冻结拦截 / doc-set 键同源(§14.14 E-4)。 */
22
+ export function normAbs(p, cwd) {
23
+ const s = String(p)
24
+ return /^[a-zA-Z]:[\\/]/.test(s) || s.startsWith("/") || s.startsWith("\\\\") ? s : join(cwd, s)
25
+ }
26
+
27
+ /** Canonical scope key for design reviews — the document multi-set
28
+ * (order-insensitive, ABS-path normalized — launch 与 continuation 的写法差异
29
+ * ("./docs/x.md" vs "docs/x.md"、反斜杠) 不误建新实例).
30
+ * 第 33 批自 advisor-async.mjs 逐字迁入(护栏与实例续跑同锚单源)。 */
31
+ export function docSetKey(documents, cwd) {
32
+ const list = [...new Set((documents ?? [])
33
+ .filter((d) => typeof d === "string" && d.trim())
34
+ .map((d) => normAbs(d, cwd)))]
35
+ return JSON.stringify(list.sort())
36
+ }
37
+
38
+ /** 护栏适用的键判据:空清单(`[]` / `null` 的 `docSetKey` 产出 = "[]")不适用——不计数、
39
+ * 不停止(§17.9 #3 登记;fail-open 于此面)。 */
40
+ export function designReviewStreakApplies(key) {
41
+ return typeof key === "string" && key.length > 0 && key !== "[]"
42
+ }
43
+
44
+ /**
45
+ * 结算分类(纯函数单源——§17.3 优先级表,自上而下首个命中;输入 = 宿主可见事实,
46
+ * **零 LLM 输出解析**,N20)。neutral = 无信息事件(取消 / 中断 / 拒发)——不打断连续计数。
47
+ * @param {{launchRefused?: boolean, stale?: boolean, hasResult?: boolean,
48
+ * incomplete?: string|null, persistFailed?: boolean}} input
49
+ * @returns {{reset: boolean, count: string|null}} reset=true 计数复位;count=需加一的类名;
50
+ * 两者皆空 = neutral(不动计数)。
51
+ */
52
+ export function designReviewOutcome(input) {
53
+ const { launchRefused = false, stale = false, hasResult = true, incomplete = null, persistFailed = false } = input ?? {}
54
+ if (launchRefused) return { reset: false, count: null } // 1. 未发起请求(无尝试发生——§14.4 既有语义)
55
+ if (stale) return { reset: false, count: "stale" } // 2. 陈旧结算(未产出可用凭证)
56
+ if (!hasResult) return { reset: false, count: "no_report" } // 3. 无报告(fail-closed)
57
+ if (incomplete && incomplete !== "interrupted") return { reset: false, count: String(incomplete) } // 4. 宿主截断尾五 kind
58
+ if (incomplete === "interrupted") return { reset: false, count: null } // 5. 用户 / 系统中断类(与 cancelled 同族)
59
+ if (persistFailed) return { reset: false, count: "no_credential" } // 6. pass 但槽落盘失败(无可用凭证)
60
+ return { reset: true, count: null } // 7. 可用判决(pass 且落盘成功 / changes-required)——连续链断点
61
+ }
62
+
63
+ /** 记录只读视图(停止判定与结论表的数据源);空清单键 / 无载体 ⇒ null。 */
64
+ export function designReviewStreakRecord(agent, key) {
65
+ if (!designReviewStreakApplies(key)) return null
66
+ const streaks = agent?._designReviewStreaks
67
+ return streaks instanceof Map ? (streaks.get(key) ?? null) : null
68
+ }
69
+
70
+ /** 计数落账(§17.4):reset ⇒ 删除该键记录;count ⇒ `{count: prev+1, log: […].slice(-N)}`;
71
+ * neutral ⇒ 不动。载体懒初始化。 */
72
+ export function noteDesignReviewOutcome(agent, key, outcome) {
73
+ if (!agent || !designReviewStreakApplies(key) || !outcome) return
74
+ if (outcome.reset) {
75
+ if (agent._designReviewStreaks instanceof Map) agent._designReviewStreaks.delete(key)
76
+ return
77
+ }
78
+ if (typeof outcome.count !== "string" || !outcome.count) return
79
+ const streaks = agent._designReviewStreaks instanceof Map
80
+ ? agent._designReviewStreaks
81
+ : (agent._designReviewStreaks = new Map())
82
+ const prev = streaks.get(key)
83
+ streaks.set(key, {
84
+ count: (prev?.count ?? 0) + 1,
85
+ log: [...(prev?.log ?? []), outcome.count].slice(-MAX_DESIGN_REVIEW_STREAK),
86
+ })
87
+ }
88
+
89
+ /** 停止判定:记录达阈值即停;空清单键恒 false。停止不可自解除(复位仅经可用判决——被拒后
90
+ * 无法发生;会话结束随载体清零)。 */
91
+ export function designReviewStreakStopped(agent, key) {
92
+ return (designReviewStreakRecord(agent, key)?.count ?? 0) >= MAX_DESIGN_REVIEW_STREAK
93
+ }
@@ -4,8 +4,10 @@
4
4
  * 语义:set = 写盘(config.json——磁盘真相最小化:只写被设的键——默认值不固化)+
5
5
  * 热应用(ctx.agent.config 内存对象立即更新——回合边界键下回合生效)。
6
6
  * 护栏:敏感键(路径段含 apiKey/key/token/secret/password)回显/错误文本永不出现明文
7
- * (••••(masked)——防密钥泄漏进会话历史/trace);已知键类型校验(类型表自动派生自
8
- * config.mjs DEFAULTS——不手写防漂移);set 侧效走审批门(dispatch 动作级分类)。
7
+ * (••••(masked)——防密钥泄漏进会话历史/trace);已知键类型校验——非 null 叶子类型表
8
+ * 自动派生自 config.mjs DEFAULTS(不手写防漂移——F-S1.5/N-S1.5);null 默认值键与同族键
9
+ * 走显式形状表(_NULL_LEAF_SHAPES 相等面 / _SIBLING_SHAPES 存在性面——按各自真实消费形态
10
+ * 校验,完备性机械锁 + 一次性警告;SETTINGS-TOOL.md §8);set 侧效走审批门(dispatch 动作级分类)。
9
11
  */
10
12
  import { DEFAULTS, configPath, writeConfigAtomic } from "../config.mjs"
11
13
 
@@ -17,19 +19,128 @@ function isSensitiveKey(path) {
17
19
  return SENSITIVE_SEGMENT.test(path)
18
20
  }
19
21
 
22
+ /* ─── 形状层(第 8 批 2026-09-11——SETTINGS-TOOL.md §8.3;两表结构:相等面 / 存在性面) ─── */
23
+
20
24
  /**
21
- * 类型表:递归遍历 DEFAULTS——叶子(非对象/数组值)记 `路径 → typeof 默认值`。
22
- * 数组(providersList 等)不递归(下标元素无类型约束——全量域);对象节递归到叶子。
25
+ * null 叶子形状表——唯一「逐键手写形状」面(完备性锁的**相等面**:键集 == _nullLeafPaths(DEFAULTS))。
26
+ * 表值 = 形状规格:接受集 = 该键真实消费形态 ∪ `null`(显式清除——消费面均有「未设置」态);
27
+ * 不可消费形态(应用侧读不出写入值)→ 拒绝(违例抛错,磁盘/内存零变化)。
23
28
  */
24
- function buildTypeMap(obj, prefix = "", out = {}) {
29
+ const _NULL_LEAF_SHAPES = {
30
+ defaultModel: { kind: "nonEmptyString", providerModel: true, expects: 'non-empty string ("provider:model" composite)' },
31
+ "agent.subagentModel": { kind: "nonEmptyString", expects: "non-empty string" },
32
+ shell: { kind: "nonEmptyString", expects: "non-empty string" },
33
+ "memory.team": { kind: "team", expects: "object { repo: string, name?, dir? }" },
34
+ }
35
+
36
+ /** 同族/跨端形状表(**存在性断言面**——不参与完备性锁的集合相等)。W3 同族:`agent.subagentModels`
37
+ * 默认 `{}` ⇒ 派生表零条目 ⇒ 零约束(字符串被静默忽略——回落 subagentModel)。 */
38
+ const _SIBLING_SHAPES = {
39
+ "agent.subagentModels": { kind: "roleMap", expects: "object of role→non-empty string" },
40
+ }
41
+
42
+ /** 派生:递归遍历默认值对象——叶子(非对象/数组值)记 `路径 → typeof 默认值`(旧语义逐字保留:
43
+ * null 叶子记 "object",其约束由形状表接管);数组不递归(下标元素无类型约束——全量域)。 */
44
+ function _deriveTypes(obj, prefix = "", out = {}) {
25
45
  for (const [k, v] of Object.entries(obj)) {
26
46
  const p = prefix ? `${prefix}.${k}` : k
27
- if (v !== null && typeof v === "object" && !Array.isArray(v)) buildTypeMap(v, p, out)
47
+ if (v !== null && typeof v === "object" && !Array.isArray(v)) _deriveTypes(v, p, out)
28
48
  else out[p] = Array.isArray(v) ? "array" : typeof v
29
49
  }
30
50
  return out
31
51
  }
32
- const TYPE_MAP = buildTypeMap(DEFAULTS)
52
+
53
+ /** 类型表 = 派生 + 可选 `patch` 覆盖(测试缝——非 null 叶子逐键与派生结果相等;T-S2.15)。 */
54
+ function _buildShapeTable(obj, patch = null) {
55
+ const out = _deriveTypes(obj)
56
+ return patch ? Object.assign(out, patch) : out
57
+ }
58
+ const TYPE_MAP = _buildShapeTable(DEFAULTS)
59
+
60
+ /** null 叶子路径枚举(数组不递归——与派生表同域)。 */
61
+ function _nullLeafPaths(obj, prefix = "", out = []) {
62
+ for (const [k, v] of Object.entries(obj)) {
63
+ const p = prefix ? `${prefix}.${k}` : k
64
+ if (v === null) out.push(p)
65
+ else if (typeof v === "object" && !Array.isArray(v)) _nullLeafPaths(v, p, out)
66
+ }
67
+ return out
68
+ }
69
+
70
+ /** 完备性锁(N-S1.5):未声明的 null 叶子 = 形状表漂移——返回键名 + 一次性警告列出键名。 */
71
+ let _shapeWarned = false
72
+ function _checkShapeCompleteness(defaults, warn = console.warn) {
73
+ const declared = new Set(Object.keys(_NULL_LEAF_SHAPES))
74
+ const missing = _nullLeafPaths(defaults).filter((p) => !declared.has(p))
75
+ if (missing.length && !_shapeWarned) {
76
+ _shapeWarned = true
77
+ warn(`[settings] null-default keys missing from _NULL_LEAF_SHAPES: ${missing.join(", ")} — declare their real consumption shape (SETTINGS-TOOL.md §8.3; N-S1.5 completeness lock)`)
78
+ }
79
+ return missing
80
+ }
81
+ _checkShapeCompleteness(DEFAULTS) // 装载自检——形状表必须覆盖全部 null 叶子(漂移即警告)
82
+
83
+ /** 值位渲染(错误文本——敏感键命中时遮罩:N-S1.2 / D-S2.4——明文零进错误句)。 */
84
+ function _shownValue(path, value) {
85
+ return isSensitiveKey(path) ? MASKED : JSON.stringify(value)
86
+ }
87
+
88
+ function _kindOf(value) {
89
+ return value === null ? "null" : Array.isArray(value) ? "array" : typeof value
90
+ }
91
+
92
+ function _expectsError(path, expects, value) {
93
+ return new Error(`settings set: "${path}" expects ${expects} — got ${_kindOf(value)} (${_shownValue(path, value)})`)
94
+ }
95
+
96
+ /** 形状校验(形状表命中键——`null` 在 _checkKnownKeyValue 已放行)。 */
97
+ function _checkShape(path, spec, value) {
98
+ if (spec.kind === "team") {
99
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw _expectsError(path, spec.expects, value)
100
+ if (typeof value.repo !== "string" || !value.repo.trim()) {
101
+ throw new Error(`settings set: "${path}" requires a non-empty "repo" (team layer stays off without it) — got ${_shownValue(path, value)}`)
102
+ }
103
+ for (const k of ["name", "dir"]) {
104
+ if (value[k] !== undefined && (typeof value[k] !== "string" || !value[k].trim())) throw _expectsError(path, spec.expects, value)
105
+ }
106
+ return
107
+ }
108
+ if (spec.kind === "roleMap") {
109
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw _expectsError(path, spec.expects, value)
110
+ for (const v of Object.values(value)) {
111
+ if (typeof v !== "string" || !v.trim()) throw _expectsError(path, spec.expects, value)
112
+ }
113
+ return
114
+ }
115
+ // nonEmptyString —— defaultModel 另加 provider:model 形态面(与 model-ref.mjs:25-36 同判据;
116
+ // 存在性面不查——D-S2.5)
117
+ if (typeof value !== "string" || !value.trim()) throw _expectsError(path, spec.expects, value)
118
+ if (spec.providerModel) {
119
+ const sep = value.indexOf(":")
120
+ if (sep <= 0 || !value.slice(sep + 1)) throw _expectsError(path, spec.expects, value)
121
+ }
122
+ }
123
+
124
+ /**
125
+ * 已知键值校验(set 写盘前;违例抛错——磁盘/内存零变化):
126
+ * ① 形状表命中键(`_NULL_LEAF_SHAPES` / `_SIBLING_SHAPES` **两表都查**——表值 = 形状规格)→ 形状校验
127
+ * (`null` = 显式清除,放行);② 非 null 叶子(派生表值 = `typeof` 串)→ 既有语义逐字保留
128
+ * (`want === "array"` 跳过 / `null` + `want === "object"` 放行);③ 未知键 → 原样通过(全量域)。
129
+ */
130
+ function _checkKnownKeyValue(path, value) {
131
+ const spec = _NULL_LEAF_SHAPES[path] ?? _SIBLING_SHAPES[path]
132
+ if (spec) {
133
+ if (value === null) return // 显式清除 = 有效动作(消费面均有「未设置」态——D-S2.3)
134
+ return _checkShape(path, spec, value)
135
+ }
136
+ const want = TYPE_MAP[path]
137
+ if (want && want !== "array") {
138
+ const got = value === null ? "null" : typeof value
139
+ if (got !== want && !(value === null && want === "object")) throw _expectsError(path, want, value)
140
+ }
141
+ }
142
+
143
+ /* ─── 既有工具面 helpers(第 8 批零改动) ─── */
33
144
 
34
145
  /** 点分路径解析(含数组数字段)——逐段下钻;返回 { ok, value } 或缺失段信息 */
35
146
  function resolvePath(obj, path) {
@@ -80,7 +191,9 @@ function parseValue(raw) {
80
191
  const v = JSON.parse(s)
81
192
  // JSON 顶层标量都接受;解析失败(裸字符串)→ 字符串字面
82
193
  if (typeof v === "number" || typeof v === "boolean" || v === null || Array.isArray(v) || typeof v === "object") return v
83
- return s // "abc"(带引号传的字符串)——JSON.parse 成功但返回字符串——保持解析结果
194
+ // "abc"(带引号传的字符串)——JSON.parse 成功但返回字符串:返回**解析值**(去引号)
195
+ // ——两端统一 ①(2026-09-11 用户裁定;SETTINGS-TOOL.md §9.3)。
196
+ return v
84
197
  } catch {
85
198
  return s // 裸 abc → 字符串字面
86
199
  }
@@ -95,7 +208,7 @@ export function settingsTool(opts = {}) {
95
208
  "Adjust ThinCoder runtime configuration — persisted to config.json AND hot-applied to the live agent config.\n" +
96
209
  "Actions: list (all keys + values, flattened) | get <key> | set <key> <value> — dot paths into config.json (agent.maxTurns, traces.enabled, providers.0.model, any nesting).\n" +
97
210
  "set persists to disk (only the set key is written — defaults are never baked in) and takes effect in the running session immediately (turn-boundary keys apply next turn); the value survives restarts.\n" +
98
- "Known keys are type-checked against the built-in defaults (agent.maxTurns must be a number, traces.enabled a boolean); unknown keys under a known section are stored as given. Values parse as JSON first (true/false/numbers/objects/arrays), else stay strings.\n" +
211
+ "Known keys are type-checked: scalar keys against the built-in defaults (agent.maxTurns a number, traces.enabled a boolean); keys whose default is null against their real consumption shape — defaultModel \"provider:model\", agent.subagentModel / shell non-empty string, memory.team object with a repo — so a value the app would silently drop is refused (null clears the key). Unknown keys under a known section are stored as given. Values parse as JSON first (true/false/numbers/objects/arrays), else stay strings.\n" +
99
212
  "SENSITIVE keys (path segment contains apiKey/key/token/secret/password) are NEVER echoed in plaintext — list/get/set replies show ••••(masked); setting a sensitive key is allowed and stored, but never echoed back.\n" +
100
213
  "list/get are read-only (planMode ok, no approval); set is a side effect (approval gate). The /config TUI command is the human equivalent.",
101
214
  parameters: {
@@ -130,14 +243,8 @@ export function settingsTool(opts = {}) {
130
243
  // set
131
244
  if (!args.key || args.value === undefined) throw new Error("settings set: key and value are required")
132
245
  const value = parseValue(args.value)
133
- // 已知键类型校验(类型表派生自 DEFAULTS——未知键 JSON 原样)
134
- const want = TYPE_MAP[String(args.key)]
135
- if (want && want !== "array") {
136
- const got = value === null ? "null" : typeof value
137
- if (got !== want && !(value === null && want === "object")) {
138
- throw new Error(`settings set: "${args.key}" expects ${want} — got ${got} (${JSON.stringify(args.value)})`)
139
- }
140
- }
246
+ // 已知键校验(形状表两表都查 + 派生类型表——SETTINGS-TOOL.md §8.3;未知键 JSON 原样)
247
+ _checkKnownKeyValue(String(args.key), value)
141
248
  // 写盘(D-F5b:磁盘真相最小化——writeConfigAtomic 磁盘新鲜读 + 只改被设键 + mtime
142
249
  // 门控——默认不固化;冲突/畸形抛错,内存不热应用(零虚假成功))+ 热应用(内存对象)
143
250
  const r = await writeConfigAtomic(cfgPath, (disk) => {
@@ -150,3 +257,9 @@ export function settingsTool(opts = {}) {
150
257
  },
151
258
  }
152
259
  }
260
+
261
+ /**
262
+ * 测试缝导出(`_` 前缀——config.mjs `_setConfigPathForTest` 先例;SETTINGS-TOOL.md §8.3 第 6 条):
263
+ * `_checkShapeCompleteness` = 完备性锁的运行时面(T-S2.14「一次性警告列出键名」的机械断言缝)。
264
+ */
265
+ export { _buildShapeTable, _nullLeafPaths, _NULL_LEAF_SHAPES, _SIBLING_SHAPES, _checkKnownKeyValue, _checkShapeCompleteness }
@@ -12,7 +12,7 @@
12
12
  * CONTAINER.md D1 落地(池访问点改 getAsyncPool accessor)。
13
13
  */
14
14
  import { isAbsolute, relative } from "node:path"
15
- import { runAgent, createAgent, CODER_OVERLAY, DEFAULT_SUBAGENT_TURNS } from "../agent.mjs"
15
+ import { runAgent, createAgent, DEFAULT_SUBAGENT_TURNS } from "../agent.mjs"
16
16
  import {
17
17
  runWithContinue, TURN_CAP_MARK, makeRelay, wrapChildCallbacks,
18
18
  ensureChildApiKey, clampEffort,
@@ -77,6 +77,15 @@ function shortTouchedPath(f, cwd) {
77
77
  }
78
78
 
79
79
  function statusFields(entry, cwd) {
80
+ // §18.3 #1(第 10 批——双池并表):评审池条目专属字段面——role 区分两池;子代理条目
81
+ // 字段面零变化(NFR-B2)。reviewType(design|code) / round / elapsedSec——与子代理
82
+ // 的 turn/maxTurns 而非(评审无子回合预算语义)。
83
+ if (entry.role === "advisor") {
84
+ const base = { id: String(entry.id), role: entry.role, reviewType: entry.reviewType ?? null, round: entry.round ?? entry.run?.round ?? null }
85
+ if (entry.model != null) base.model = entry.model
86
+ if (entry.startedAt) base.elapsedSec = Math.max(0, Math.round((Date.now() - entry.startedAt) / 1000))
87
+ return base
88
+ }
80
89
  const base = { id: String(entry.id), role: entry.role }
81
90
  if (entry.status === "running") {
82
91
  base.model = entry.model ?? null
@@ -104,7 +113,7 @@ export function executeStatusAction(args, ctx) {
104
113
  const { id } = args ?? {}
105
114
  if (id !== undefined && id !== null && String(id) !== "") {
106
115
  const key = String(id)
107
- // §24 D-24b: by-id queries fall through to the advisor pool (shared counter —
116
+ // §11.2 D-24b: by-id queries fall through to the advisor pool (shared counter —
108
117
  // ids are unique across both pools; role identifies the kind).
109
118
  const entry = map.get(key) ?? advisors.get(key)
110
119
  if (!entry) {
@@ -172,7 +181,7 @@ export function executeStatusAction(args, ctx) {
172
181
  }
173
182
 
174
183
  // ═══════════════════════════════════════════════════════════════════════════
175
- // SUBAGENT-OBSERVE-SEND(docs/design/SUBAGENT-OBSERVE-SEND.md——CLI 端)
184
+ // SUBAGENT-OBSERVE-SEND(SUBAGENT-OBSERVE-SEND.md——CLI 端)
176
185
  // observe(D1——readonly 查询)+ send(D2——控制类豁免注入引导)——动作执行器。
177
186
  // 目标 = 父自身 spawn 的异步子代理池条目(_asyncSubagents——非 advisor/escalate——
178
187
  // 后者共享 id 计数但非"读写子代理",错误路径明示)。
@@ -289,7 +298,7 @@ export function executeSendAction(args, ctx) {
289
298
  const entry = getAsyncPool(agent, "subagent")?.get(key)
290
299
  if (!entry) {
291
300
  if (getAsyncPool(agent, "advisor")?.has(key)) {
292
- return JSON.stringify({ status: "error", error: `id ${key} is an async ADVISOR review — send is for async subagents; you cannot inject direction into a running review (AGENT-LOOP.md §7.2)` })
301
+ return JSON.stringify({ status: "error", error: `id ${key} is an async ADVISOR review — send is for async subagents; you cannot inject direction into a running review (AGENT-LOOP.md §7.2) — track it with action:'status' (role:"advisor") or wait for its report to arrive automatically` })
293
302
  }
294
303
  return JSON.stringify({ status: "error", error: `unknown async subagent id: ${key}` })
295
304
  }
@@ -388,13 +397,13 @@ export async function executeEscalateAction(args, ctx) {
388
397
  let escT0 = Date.now()
389
398
  try {
390
399
  // 全写路径(role "coder"):权限经父 onPermissionRequest,mutations merge 回父
400
+ // G3(施工②):overlay 摘除——coder 人格槽由 assemblePrompt 场景表承载(G3 映射)。
391
401
  child = createAgent({
392
402
  provider,
393
403
  tools: parent.tools,
394
404
  config: parent.config,
395
405
  cwd: parent.cwd,
396
406
  memory: parent.memory,
397
- overlay: CODER_OVERLAY,
398
407
  role: "coder",
399
408
  })
400
409
  child._logId = escId // LOGGING:子内事件归属(escalate#N)
@@ -423,7 +432,7 @@ export async function executeEscalateAction(args, ctx) {
423
432
  child, task, { ...childCallbacks, onPermissionRequest: parent.autoApprove ? async () => true : (ctx.onPermissionRequest ?? null) },
424
433
  runOpts,
425
434
  {
426
- // escalate has NO permQueue: prompts go straight to the user (T-L spec).
435
+ // sync escalate has NO permQueue——async 飞行权限走 _permQueue(escalate-async.mjs): prompts go straight to the user (T-L spec).
427
436
  askContinue: (e) => (ctx.onPermissionRequest
428
437
  ? ctx.onPermissionRequest("continue", { turns: e.turn, agent: tag })
429
438
  : Promise.resolve(false)),