@thincoder/core 0.10.9 → 0.10.11

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 (92) hide show
  1. package/CHANGELOG.md +28 -2
  2. package/advisor/messages.mjs +4 -5
  3. package/advisor/project-context.mjs +69 -44
  4. package/agent/completion.mjs +4 -1
  5. package/agent/family-tools.mjs +12 -10
  6. package/agent/helpers.mjs +19 -11
  7. package/agent/light-round.mjs +1 -4
  8. package/agent/record-results.mjs +6 -1
  9. package/agent/run-start.mjs +1 -0
  10. package/agent/setup-reminders.mjs +1 -1
  11. package/agent/turn-loop.mjs +17 -13
  12. package/agent/unbooked-pen.mjs +62 -0
  13. package/agent-tools/advisor.mjs +15 -31
  14. package/agent-tools/audit-block.mjs +16 -62
  15. package/agent-tools/batch-gate.mjs +12 -21
  16. package/agent-tools/checkpoint.mjs +11 -6
  17. package/agent-tools/context.mjs +1 -1
  18. package/agent-tools/design-token.mjs +9 -2
  19. package/agent-tools/digest-budget.mjs +2 -0
  20. package/agent-tools/doc-check.mjs +17 -23
  21. package/agent-tools/manifest.mjs +45 -14
  22. package/agent-tools/review-facts.mjs +35 -81
  23. package/agent-tools/settings.mjs +65 -5
  24. package/agent-tools/spawn-gates.mjs +68 -31
  25. package/agent-tools/subagent-async.mjs +6 -3
  26. package/agent-tools/subagent-spawn.mjs +76 -80
  27. package/agent-tools/subagent.mjs +16 -16
  28. package/agent-tools.mjs +1 -1
  29. package/browser/launch.mjs +14 -3
  30. package/browser/queue.mjs +31 -3
  31. package/browser/session.mjs +4 -0
  32. package/config-defaults.mjs +8 -0
  33. package/config-presets.mjs +1 -1
  34. package/config.mjs +60 -11
  35. package/declaration.mjs +5 -12
  36. package/doc-check/anchors.mjs +53 -60
  37. package/doc-check/index.mjs +106 -140
  38. package/doc-check/paragraph.mjs +119 -0
  39. package/fts-text.mjs +4 -1
  40. package/i18n.mjs +5 -0
  41. package/ledger-cmd.mjs +17 -4
  42. package/ledger-db.mjs +14 -8
  43. package/ledger.mjs +1 -1
  44. package/manifest-discovery.mjs +19 -10
  45. package/manifest-schema.mjs +7 -35
  46. package/manifest.mjs +21 -9
  47. package/memory/code-search.mjs +5 -14
  48. package/memory/docs.mjs +5 -14
  49. package/memory/read-gate.mjs +26 -0
  50. package/model-context.mjs +98 -0
  51. package/model-specs.mjs +110 -37
  52. package/package.json +2 -2
  53. package/permission.mjs +3 -1
  54. package/prompts/advisor-design.md +104 -38
  55. package/prompts/advisor-round1.md +72 -24
  56. package/prompts/advisor-round2.md +62 -25
  57. package/prompts/advisor-round3.md +68 -24
  58. package/prompts/common.md +214 -58
  59. package/prompts/consult-base.md +10 -3
  60. package/prompts/discipline-engineering.md +202 -54
  61. package/prompts/discipline-normal.md +95 -44
  62. package/prompts/persona-coder.md +3 -1
  63. package/prompts/persona-eng-coder.md +29 -9
  64. package/prompts/persona-eng-designer.md +101 -35
  65. package/prompts/persona-engineering.md +320 -98
  66. package/prompts/persona-explore.md +1 -1
  67. package/prompts/persona-normal.md +16 -8
  68. package/provider/anthropic.mjs +127 -4
  69. package/provider/core.mjs +16 -6
  70. package/provider/google.mjs +8 -8
  71. package/provider/list-models.mjs +37 -5
  72. package/provider/normalize.mjs +2 -2
  73. package/provider/responses.mjs +7 -7
  74. package/provider/sse.mjs +6 -5
  75. package/proxy-transport.mjs +5 -3
  76. package/proxy.mjs +2 -1
  77. package/session-guard.mjs +7 -1
  78. package/session-lifecycle.mjs +23 -8
  79. package/session-segments.mjs +1 -0
  80. package/session-slot-claims.mjs +4 -0
  81. package/session-slot-write.mjs +6 -0
  82. package/session-slots-manifest.mjs +4 -0
  83. package/token-ttl.mjs +10 -0
  84. package/tool-docs/advisor.md +24 -5
  85. package/tool-docs/doc_check.md +44 -25
  86. package/tool-docs/ledger.md +2 -2
  87. package/tool-docs/manifest-read.md +8 -0
  88. package/tool-docs/manifest.md +9 -8
  89. package/tool-docs/subagent.md +41 -16
  90. package/tools/web-deepseek.mjs +26 -11
  91. package/doc-check/version.mjs +0 -84
  92. package/doc-check/width.mjs +0 -132
@@ -6,10 +6,12 @@
6
6
  * - 2026-10-04(工具路径基面根治批 · #921):import 面扩为「node:* + manifest 发现/声明族
7
7
  * (叶向——manifest 族零依赖 advisor 面,零新模块边;KD-2)」——`REVIEW_ROOT_KEYS` +
8
8
  * `resolveReviewRootsFor` 判定本体自 `agent/write-gate.mjs` 迁入(write-gate 同名再出口保
9
- * 既有 import 面),新增 `resolveReviewDocPaths` 文档解析四分支单源。
9
+ * 既有 import 面);`resolveReviewDocPaths` = 评审文档路径解析单源;
10
+ * - 2026-10-25(评审文档路径收正批):该解析器收为**单规则**——相对形按会话项目根 / 绝对形
11
+ * 照用 / 受理 = 项目内现存 `.md`;候选试探与歧义归一退场(判据正文 = `ADVISOR-GUARDS.md` §12)。
10
12
  */
11
13
  import { existsSync, statSync } from "node:fs"
12
- import { dirname, join, resolve } from "node:path"
14
+ import { isAbsolute, join, relative, resolve, sep } from "node:path"
13
15
  import { DEFAULT_MANIFEST, docRootPaths, readManifest } from "../manifest.mjs"
14
16
  import { owningProject, projectRootView } from "../manifest-discovery.mjs"
15
17
 
@@ -27,12 +29,13 @@ export function docSetKey(documents, cwd) {
27
29
  return JSON.stringify(list.sort())
28
30
  }
29
31
 
30
- /** 评审目标解析读的 docRoot 键集(评审对象/被审文件可住的所有文档层——M6 分类据此判定)。
32
+ /** **写门面**评审根的 docRoot 键集(`agent/write-gate.mjs` 的 `resolveReviewTargetPaths` 消费)——
33
+ * 设计评审**受理面不再消费**本键集(受理 = 项目内现存 `.md`,判据单源 = `ADVISOR-GUARDS.md` §12)。
31
34
  * 2026-10-04 自 `agent/write-gate.mjs` 迁入(键集本体零变;write-gate 同名再出口)。 */
32
35
  export const REVIEW_ROOT_KEYS = ["requirements", "specs", "design", "modules", "batches"]
33
36
 
34
37
  /**
35
- * 评审目标解析单点(按用点形——2026-09-21 收口 · 台账 #828;2026-10-04 自 `agent/write-gate.mjs`
38
+ * 写门评审根解析单点(按用点形——2026-09-21 收口 · 台账 #828;2026-10-04 自 `agent/write-gate.mjs`
36
39
  * 迁入,逐字零变;write-gate 同名再出口保既有 import 面)。
37
40
  */
38
41
  export function resolveReviewRootsFor(dir) {
@@ -50,78 +53,45 @@ function readableFile(abs) {
50
53
  try { return existsSync(abs) && statSync(abs).isFile() } catch { return false }
51
54
  }
52
55
 
53
- /** 会话根集(分支①判据集——既有 advisor 面 `resolveReviewTargetPaths(agent)` 目录形态的函数式封装;档内私有——零外部消费面)。 */
54
- function sessionReviewRoots(cwd) {
55
- return resolveReviewRootsFor(cwd ?? process.cwd())
56
+ /** 受理判据 = 现存普通文件 ∧ 扩展名 `.md`(大小写不敏感)——`scope-not-doc` 的指义 = 评审对象必须是文档。 */
57
+ function isDocFile(abs) {
58
+ return /\.md$/i.test(abs) && readableFile(abs)
59
+ }
60
+
61
+ /** `p` 是否等于 `root` 或在其子树内(段边界判——`..` 逃逸 / 跨盘 ⇒ 否)。 */
62
+ function isInside(p, root) {
63
+ const rel = relative(root, p)
64
+ return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))
56
65
  }
57
66
 
58
67
  /**
59
- * **文档解析四分支单源**(2026-10-04 · 批档 §2.2 A 本体):逐文档
60
- * 分支① 零变:normAbs(doc, cwd) 落会话根集 ⇒ 受理;分支② 零变:所属项目根集命中 ⇒ 受理;
61
- * 分支③ 根治面:相对形 ∧ 前两分支未中 ⇒ 候选底座逐个试探(底座 = projectRootView(cwd) 现行集 +
62
- * cwd 祖先链全量目录——自父目录起至盘根、最近→远、去重、现行集在前)——resolve(c, doc)
63
- * 所属项目根集命中 = 结构命中;绝对形不进本分支(未中 = 真越界,无候选可试);
64
- * 分支④ 歧义归一(KD-3):结构命中 ≥2 ⇒ 可读文件存在性唯一化(batch-paths 读面「首个可读」
65
- * 同款判据):恰一可读 ⇒ 胜;≥2 可读 ⇒ fail-closed 拒(scope-doc-ambiguous 列全部
66
- * 可读命中);0 可读 ⇒ not-doc 拒(诊断列全部结构命中)。
67
- * @returns {{ resolved: Map<string,string>, invalid: Array<{doc, attempted:string[]}>,
68
- * ambiguous: {doc, hits:string[]}|null, candidates: string[] }} ambiguous = 首个歧义文档
69
- * (fail-closed 首拒——消费面拒发即止);candidates = 分支③候选集(B 文案候选提示同源)。
70
- * 非字符串 / 空白串项**静默跳过**(同 docSetKey 既有过滤语义——不进 resolved/invalid)。
68
+ * **评审文档路径解析单规则**(判据正文 = `docs/core/design/ADVISOR-GUARDS.md` §12)——逐文档:
69
+ * ① 相对形:基面 = **会话项目根** `projectRootView(cwd).root`——`p = resolve(根, doc)`;越根 ⇒ `outside-project`;
70
+ * ② 相对形 ∧ 项目根解析不出(锚 = `workspace` / `none`)⇒ `no-project-root`;
71
+ * ③ 绝对形:照用(零根试探、零拼接);不住在任何项目里(`owningProject` 空)⇒ `outside-project`;
72
+ * ④ 受理 = 现存普通文件 ∧ `.md`(大小写不敏感);否则 `not-doc-file`。
73
+ * **零试探**:无候选项目根、无祖先链、无歧义归一——路径要求写对(用户 2026-10-25 11:41
74
+ * 「为啥要试探?不能要求写对吗?」)。
75
+ * 非字符串 / 空白串项**静默跳过**(同 docSetKey 既有过滤语义——不进 resolved / invalid)。
76
+ * @returns {{ resolved: Map<string,string>, invalid: Array<{doc: string, resolved: string|null, reason: string}> }}
77
+ * `reason` ∈ `no-project-root` / `outside-project` / `not-doc-file`(标识行恒 `criterion=scope-not-doc`,
78
+ * 三值只作 `reason=`);`resolved` 项 = 解析后绝对路径(`no-project-root` 无基面 ⇒ `null`)。
71
79
  */
72
80
  export function resolveReviewDocPaths(documents, cwd) {
73
- const fwd = (p) => p.replace(/[\\/]/g, "/")
74
- const sessionRoots = sessionReviewRoots(cwd).map(fwd)
75
81
  const view = projectRootView(cwd ?? process.cwd())
76
- const candidates = candidateRoots(view, cwd ?? process.cwd())
77
- const ownRootsCache = new Map()
78
- const ownRoots = (owner) => {
79
- let r = ownRootsCache.get(owner)
80
- if (!r) {
81
- r = resolveReviewRootsFor(owner).map(fwd)
82
- ownRootsCache.set(owner, r)
83
- }
84
- return r
85
- }
86
- // under = 段边界判(比较面用 `/` 归一形;**产出面保原生形态**——docAbs / resolved 与 normAbs
87
- // 既有形态同族,冻结窗/陈旧谓词零改照比对——win32 反斜杠面不破)。
88
- const under = (pNative, rootsFwd) => {
89
- const p = fwd(pNative)
90
- return rootsFwd.some((r) => p === r || p.startsWith(r + "/"))
91
- }
92
-
93
82
  const resolved = new Map()
94
83
  const invalid = []
95
- let ambiguous = null
96
84
  for (const doc of (documents ?? [])) {
97
85
  if (typeof doc !== "string" || !doc.trim()) continue
98
- const isAbs = /^[a-zA-Z]:[\\/]/.test(doc) || doc.startsWith("/") || doc.startsWith("\\\\")
99
- const n = normAbs(doc, cwd)
100
- // 分支① 会话根集 / 分支② 所属项目根集(既有行为零变——判定序同 advisor.mjs 旧面)。
101
- if (under(n, sessionRoots)) { resolved.set(doc, n); continue }
102
- const owner = owningProject(n)
103
- if (owner && under(n, ownRoots(owner))) { resolved.set(doc, n); continue }
104
- // 分支③ 候选项目根试探(相对形 only——绝对形未中 = 真越界,无候选可试)。
105
- let hits = []
106
- if (!isAbs && candidates.length > 0) {
107
- for (const c of candidates) {
108
- const t = resolve(c, doc)
109
- const cOwner = owningProject(t)
110
- if (cOwner && under(t, ownRoots(cOwner))) hits.push(t)
111
- }
112
- }
113
- // 分支④ 歧义归一:结构命中 ≥2 ⇒ 可读文件存在性唯一化;恰 1 命中 ⇒ 受理(存在性仅作
114
- // 歧义唯一化判据——单命中无判别力,同分支①声明根集受理面一致不查存在)。
115
- if (hits.length >= 2) {
116
- const readable = hits.filter((h) => readableFile(h))
117
- if (readable.length === 1) { resolved.set(doc, readable[0]); continue }
118
- if (readable.length >= 2) { ambiguous = { doc, hits: readable }; break }
119
- } else if (hits.length === 1) {
120
- resolved.set(doc, hits[0]); continue
121
- }
122
- invalid.push({ doc, attempted: hits })
86
+ const abs = /^[a-zA-Z]:[\\/]/.test(doc) || doc.startsWith("/") || doc.startsWith("\\\\")
87
+ if (!abs && view.state !== "ok") { invalid.push({ doc, resolved: null, reason: "no-project-root" }); continue }
88
+ const p = abs ? doc : resolve(view.root, doc)
89
+ if (!abs && !isInside(p, view.root)) { invalid.push({ doc, resolved: p, reason: "outside-project" }); continue }
90
+ if (abs && !owningProject(p)) { invalid.push({ doc, resolved: p, reason: "outside-project" }); continue }
91
+ if (!isDocFile(p)) { invalid.push({ doc, resolved: p, reason: "not-doc-file" }); continue }
92
+ resolved.set(doc, p)
123
93
  }
124
- return { resolved, invalid, ambiguous, candidates }
94
+ return { resolved, invalid }
125
95
  }
126
96
 
127
97
  /** #949(评审 history 串台面 ① 镜像键控):评审实例镜像三值单点定域(round ∥ prior ∥
@@ -140,19 +110,3 @@ export function noteReviewDelivered(agent, run) {
140
110
  if (!run || typeof run !== "object") return
141
111
  run.historyAnchorIdx = Array.isArray(agent?.history) ? agent.history.length : 0
142
112
  }
143
-
144
- /** 分支③候选底座(`MANIFEST.md` §2.5「路径归一增量」族第四笔 · 2026-10-05 · #945):现行
145
- * `projectRootView(cwd)` 集(ok → [root];workspace → candidates 按名序;none → 空集)+
146
- * cwd 祖先链全量目录(自父目录起至盘根、最近→远、去重、现行集在前)——容器相对形在子仓锚下
147
- * 可解析;受理判据(目标解析位最近 manifest 声明面)∥ 围栏 ∥ 可读唯一化 ∥ fail-closed 歧义零改。 */
148
- function candidateRoots(view, cwd) {
149
- const current = view.state === "ok" ? [view.root] : view.state === "workspace" ? view.candidates : []
150
- const ancestors = []
151
- for (let d = dirname(resolve(cwd)); ; ) {
152
- ancestors.push(d)
153
- const p = dirname(d)
154
- if (p === d) break
155
- d = p
156
- }
157
- return [...new Set([...current, ...ancestors])]
158
- }
@@ -12,7 +12,7 @@
12
12
  * 走显式形状表(_NULL_LEAF_SHAPES 相等面 / _SIBLING_SHAPES 存在性面——按各自真实消费形态
13
13
  * 校验,完备性机械锁 + 一次性警告;SETTINGS-TOOL.md §8);set 侧效走审批门(dispatch 动作级分类)。
14
14
  */
15
- import { DEFAULTS, configPath, writeConfigAtomic } from "../config.mjs"
15
+ import { DEFAULTS, configPath, promoteAgentViews, writeConfigAtomic } from "../config.mjs"
16
16
  import { DESC } from "../tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/settings.md
17
17
 
18
18
  /** 敏感键段判定(完整点分路径的段级匹配——两句取或:词表段 ∨ 开口键族段;SETTINGS-TOOL.md §2.4)。
@@ -136,14 +136,38 @@ function _checkShape(path, spec, value) {
136
136
  }
137
137
  }
138
138
 
139
+ /* ─── advisor 写面(本批——D-ST18 ∥ promoted 视图同步;SETTINGS-TOOL.md §2.3 / §2.6) ─── */
140
+
141
+ /** `agent.advisor.*` ⇄ promoted `advisor.*` 路径映射(写面三条腿的同步面——§2.3);其它键 ⇒ null。 */
142
+ function _promotedAdvisorPath(path) {
143
+ const key = String(path)
144
+ if (key === "agent.advisor") return "advisor"
145
+ if (key.startsWith("agent.advisor.")) return `advisor.${key.slice("agent.advisor.".length)}`
146
+ return null
147
+ }
148
+
149
+ /** D-ST18:advisor 超时的两个写面键只收 `null`(显式清除)∥ 有限正数(ms)。
150
+ * 读面的「非法一律回退默认」是手写 config.json 的兜底,不是写面的许可证——收下 `0` 再回显
151
+ * 「运行中已生效」= D-ST6 要防的静默面。拒绝句点名键与可消费形态。 */
152
+ function _checkAdvisorTimeout(value) {
153
+ if (value === null) return
154
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return
155
+ throw new Error("advisor.timeoutMs must be a finite positive number (ms) or null")
156
+ }
157
+
139
158
  /**
140
159
  * 已知键值校验(set 写盘前;违例抛错——磁盘/内存零变化):
160
+ * ⓪ advisor 写面两键先判(D-ST18:只收 null ∥ 有限正数;整段赋值腿对对象内 timeoutMs 同规则);
141
161
  * ① 形状表命中键(`_NULL_LEAF_SHAPES` / `_SIBLING_SHAPES` **两表都查**——表值 = 形状规格)→ 形状校验
142
162
  * (`null` = 显式清除,放行);② 非 null 叶子(派生表值 = `typeof` 串)→ 既有语义逐字保留
143
163
  * (`want === "array"` 跳过 / `null` + `want ∈ _NULL_CLEARABLE` 放行——#645 扩 number/boolean);
144
164
  * ③ 未知键 → 原样通过(全量域)。
145
165
  */
146
166
  function _checkKnownKeyValue(path, value) {
167
+ if (path === "agent.advisor.timeoutMs" || path === "advisor.timeoutMs") return _checkAdvisorTimeout(value)
168
+ if ((path === "agent.advisor" || path === "advisor") && value !== null && typeof value === "object" && !Array.isArray(value) && "timeoutMs" in value) {
169
+ _checkAdvisorTimeout(value.timeoutMs) // 整段赋值腿(两个段名):对象内该键同规则校验(§2.6)
170
+ }
147
171
  const spec = _NULL_LEAF_SHAPES[path] ?? _SIBLING_SHAPES[path]
148
172
  if (spec) {
149
173
  if (value === null) return // 显式清除 = 有效动作(消费面均有「未设置」态——D-S2.3)
@@ -206,10 +230,28 @@ function flatten(obj, prefix = "", out = []) {
206
230
  return out.sort((a, b) => (a.path < b.path ? -1 : 1))
207
231
  }
208
232
 
209
- /** 行格式化:`key = value (type)`——敏感键值遮罩 */
233
+ /** 递归掩码:父对象不敏感、子键敏感时整棵子树替换为 MASKED(Gitee #21)。
234
+ * 只判查询路径本身会让 `get providers` 把 `providers[].apiKey` 明文吐出来——
235
+ * 而工具契约写着「敏感键永不明文回显」;父键不敏感不等于子树安全。 */
236
+ function maskDeep(value, prefix) {
237
+ if (value === null || typeof value !== "object") return value
238
+ if (Array.isArray(value)) return value.map((v, i) => maskDeep(v, `${prefix}.${i}`))
239
+ const out = {}
240
+ for (const [k, v] of Object.entries(value)) {
241
+ const sub = `${prefix}.${k}`
242
+ out[k] = isSensitiveKey(sub) ? MASKED : maskDeep(v, sub)
243
+ }
244
+ return out
245
+ }
246
+
247
+ /** 行格式化:`key = value (type)`——敏感键值遮罩(含子树递归) */
210
248
  function formatLine({ path, value }) {
211
249
  // #58(hygiene-sweep 批):对象值渲染收正(原 `[object Object]`——JSON 化;敏感键仍恒遮)
212
- const shown = isSensitiveKey(path) ? MASKED : value !== null && typeof value === "object" ? JSON.stringify(value) : value
250
+ const shown = isSensitiveKey(path)
251
+ ? MASKED
252
+ : value !== null && typeof value === "object"
253
+ ? JSON.stringify(maskDeep(value, path))
254
+ : value
213
255
  return `${path} = ${shown} (${Array.isArray(value) ? "array" : typeof value})`
214
256
  }
215
257
 
@@ -275,11 +317,29 @@ export function settingsTool(opts = {}) {
275
317
  else setKeyPath(disk, args.key, value)
276
318
  })
277
319
  if (!r.ok) throw new Error("config changed on disk concurrently — retry (settings set not applied)")
320
+ const promotedPath = _promotedAdvisorPath(args.key) // `agent.advisor.*` 的 promoted 副本路径(其它键 ⇒ null)
321
+ const fresh = (v) => (v !== null && typeof v === "object" ? structuredClone(v) : v) // DEFAULTS 是共享常量——回填不别名
278
322
  if (value === null) {
279
323
  deleteKeyPath(config, args.key)
324
+ // `null` 删除腿:副本同子键同删,**不再 re-derive**——promote 会把 config.advisor 旧值带回(§2.3)
325
+ if (promotedPath) deleteKeyPath(config, promotedPath)
280
326
  const d = resolvePath(DEFAULTS, String(args.key))
281
- if (d.ok) setKeyPath(config, args.key, d.value) // 回填默认(形状表键默认 null ⇒ 与今日同形)
282
- } else setKeyPath(config, args.key, value)
327
+ if (d.ok) {
328
+ setKeyPath(config, args.key, fresh(d.value)) // 回填默认(形状表键默认 null ⇒ 与今日同形)
329
+ if (promotedPath) setKeyPath(config, promotedPath, fresh(d.value)) // 副本与 reload 等价(loadConfig 以 nested 为准)
330
+ }
331
+ } else {
332
+ setKeyPath(config, args.key, value)
333
+ // promoted 视图同步(§2.3):整段赋值 = `promoteAgentViews` 合并语义;子键 set = **只同步被写的键**。
334
+ // 子键也整段 re-derive 会拿嵌套面上的同名默认值盖回运行面(`DEFAULTS.agent.advisor.guard = false`,
335
+ // 经 `loadConfig` 并进 `agent.advisor`)⇒ `/advisor` 打开的会话级 guard 会被静默关掉,
336
+ // 违反「运行面独有的会话级键在合并中保留」。
337
+ if (promotedPath === "advisor") {
338
+ promoteAgentViews(config) // 整段赋值 = 同一条 promote 规则(合并语义:只覆盖、不删除;非对象段不展开)
339
+ } else if (promotedPath !== null) {
340
+ setKeyPath(config, promotedPath, value) // 子键 set:键级同步(与 null 腿同一种「同一子键」口径)
341
+ }
342
+ }
283
343
  const sensitive = isSensitiveKey(String(args.key))
284
344
  const shown = sensitive ? MASKED : value
285
345
  const suffix = value === null
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * spawn-gates.mjs — M5 委派与 spawn 门新门禁统一落点(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.3):
3
3
  * 全部新增门禁逻辑集中于此——装配点(buildSpawnChild / normalizeFileList)只加 import 调用。
4
- * 已实现:F2(round 枚举 + 任务书参数校验/拼装——四项核心 + 第五项按角色)· F6(files 声明面拦截谓词)。
4
+ * 已实现:F2(round 枚举 + 任务书参数门/拼装)· F6(files 声明面拦截谓词)。
5
+ * 2026-10-24(AGENT-LOOP-SUBAGENT.md §6.22):任务书单通道 = 批次档 §2——工程角色
6
+ * (eng-coder / eng-designer)零任务书文本参数,八个自由文本键(键在场即拒)命中即点名拒收;
7
+ * 非工程角色(explore / plan / coder)保持四项核心 + `scope`。
5
8
  * 裁撤(2026-09-17 主 agent 裁定——不落代码):
6
9
  * - F4 修复轮窄带:全量勘察散文 marker 不可机判(误拒风险不可接受)→ 由 F2 的 round 参数承载轮次语义;
7
10
  * - F5 advisor 角色:advisor 不入 spawn 通道(评审走 advisor 工具)——enum 收正为
@@ -14,18 +17,15 @@
14
17
  */
15
18
  export const ROUND_VALUES = ["initial", "fix"]
16
19
 
17
- /** 任务书样例内容(供人照抄;不是门的判据——门只判存在 / 类型 / 非空):核心四项一份,
18
- * 第五项按角色各一份(工程角色 = `designPoints` ∥ explore / plan / coder = `scope`)。 */
20
+ /** 任务书样例内容(供人照抄;不是门的判据——门只判存在 / 类型 / 非空):非工程角色
21
+ * = 四项核心 + `scope`(工程角色无任务书文本参数——任务书 = 批次档 §2)。 */
19
22
  const TASK_BOOK_CORE_MIN = {
20
- goal: "实现批准设计(样例任务书——四项核心 + 角色第五项齐备)。",
23
+ goal: "实现批准设计(样例任务书——四项核心 + `scope` 齐备)。",
21
24
  knownFacts: "设计已评审通过、token 已签发、批次档在册。",
22
25
  acceptance: "设计档的验收口径逐条可机验、项目验证链全绿。",
23
26
  reportFormat: "交付表 + 文件路径 + 验证命令与结果。",
24
27
  }
25
28
 
26
- /** 工程角色样例(核心四项 + `designPoints`)。 */
27
- export const ENG_TASK_BOOK_MIN = { ...TASK_BOOK_CORE_MIN, designPoints: "仅落设计档受影响文件清单;禁夹带新语义。" }
28
-
29
29
  /** 非工程角色样例(核心四项 + `scope`)——explore / plan / coder。 */
30
30
  export const PLAIN_TASK_BOOK_MIN = { ...TASK_BOOK_CORE_MIN, scope: "只读:相关源 / 设计档;不写任何文件、不碰 bench/。" }
31
31
 
@@ -46,61 +46,97 @@ export function validateRoundParam(args) {
46
46
  throw new Error(
47
47
  `Engineering spawn round gate: ${missing.join(" · ")}. ` +
48
48
  `The spawn contract requires the structured round parameter (initial|fix); ` +
49
- `the five task-book parameters are enforced by a separate gate (validateTaskBookParams) — ` +
49
+ `task-book text parameters are refused for engineering roles by a separate gate (rejectEngFreeTextParams) — ` +
50
50
  `a missing or invalid round is a dispatch defect and the spawn is refused.`)
51
51
  }
52
52
 
53
+ /** 工程角色名集(单一判据 = 是否有写权 ⇒ 只应有唯一任务来源——AGENT-LOOP-SUBAGENT.md §6.22)。 */
54
+ const ENGINEERING_ROLES = new Set(["eng-coder", "eng-designer"])
55
+
56
+ /** 角色 ∈ 工程角色(任务书 = 批次档 §2,零任务书文本参数)。 */
57
+ export function isEngineeringRole(role) {
58
+ return ENGINEERING_ROLES.has(role)
59
+ }
60
+
61
+ /** 工程角色拒收的自由文本参数(八名单源):全部进子代理 input 的自由文本 ⇒ 第二任务来源。
62
+ * `designPoints` 已从 schema 退役;门取工具入参原对象(未声明键仍可达),故仍在名单内
63
+ * ——拒收面不依赖 schema 声明。 */
64
+ export const ENG_REFUSED_PARAMS = ["goal", "knownFacts", "acceptance", "reportFormat", "scope", "task", "context", "designPoints"]
65
+
66
+ /** 工程角色必填集合(拒收文案的「还缺什么」面;判据本体 = batchDoc / round / token 各门)。 */
67
+ const ENG_REQUIRED_NAMES = {
68
+ "eng-coder": "role · batchDoc (readable) · round · designToken (plus designId when several slots are held)",
69
+ "eng-designer": "role · batchDoc (readable) · round",
70
+ }
71
+
53
72
  /**
54
- * F2 任务书名集(**所有 spawn 角色**):core = 四项核心(全员必带);fifth = 第五项两名
55
- * ——工程角色 = `designPoints` ∥ explore / plan / coder = `scope`。
73
+ * 工程角色任务书门(AGENT-LOOP-SUBAGENT.md §6.22.1):**键在场即拒**——值全空白也拒
74
+ * (键在场说明派单方仍以为自己在写第二本任务书)。命中即抛,逐键点名并给出必填集合;
75
+ * `designPoints` 另附退役句。非工程角色不经本门(零改)。
76
+ */
77
+ export function rejectEngFreeTextParams(args, role) {
78
+ const a = args ?? {}
79
+ const offending = ENG_REFUSED_PARAMS.filter((name) => a[name] !== undefined)
80
+ if (offending.length === 0) return
81
+ const names = offending
82
+ .map((name) => (name === "designPoints"
83
+ ? `${name} (designPoints is retired for every role — write §2, or scope for explore/plan/coder)`
84
+ : name))
85
+ .join(" / ")
86
+ const required = ENG_REQUIRED_NAMES[role] ?? "role · batchDoc (readable) · round"
87
+ throw new Error(
88
+ `Spawn task-channel gate (role='${role}'): ${names}. ` +
89
+ `An engineering spawn carries NO task-book text parameters — the task book is batch record §2 (batchDoc), read by the child itself. ` +
90
+ `Put everything the child needs into §2 before spawning; a thin §2 earns a bounce, not a supplement. ` +
91
+ `Required for role='${role}': ${required}.`)
92
+ }
93
+
94
+ /**
95
+ * F2 任务书名集(**非工程角色**:core = 四项核心 + fifth = `scope`)。工程角色 = 零任务书
96
+ * 文本参数(§6.22.1),不经本表。
56
97
  * 单源:本档为唯一名集处——校验 / 拼装 / 探针夹具校验一律经 `taskBookParamsFor(role)` 派生(D2)。
57
- * 样例对象 = `ENG_TASK_BOOK_MIN` / `PLAIN_TASK_BOOK_MIN`(供人照抄)。
98
+ * 样例对象 = `PLAIN_TASK_BOOK_MIN`(供人照抄)。
58
99
  */
59
100
  export const TASK_BOOK_PARAMS = {
60
101
  core: ["goal", "knownFacts", "acceptance", "reportFormat"],
61
- fifth: { engineering: "designPoints", plain: "scope" },
62
- }
63
-
64
- /** 角色 → 第五项类别(唯一判据;**与模式无关**——工程模式里的 explore 也取 plain)。
65
- * 未知角色取 plain:spawn 路径上角色 enum 门先拒收(`agent-tools/subagent.mjs` 的 `ROLES`),到不了本门。 */
66
- function fifthClassOf(role) {
67
- return role === "eng-coder" || role === "eng-designer" ? "engineering" : "plain"
102
+ fifth: "scope",
68
103
  }
69
104
 
70
- /** 角色 → 该角色的任务书名集(四项核心 + 第五项;序 = 拼装序:第五项插在 `knownFacts` 之后,
71
- * 与 C 之前 eng 角色的拼装序逐字相同——审计摘要 marker `/^Design points/i` 不受影响)。 */
105
+ /** 角色 → 该角色的任务书名集(四项核心 + `scope`;序 = 拼装序:第五项插在 `knownFacts` 之后)。
106
+ * 工程角色返回空集——它们没有任务书文本参数(任务书 = 批次档 §2)。 */
72
107
  export function taskBookParamsFor(role) {
108
+ if (isEngineeringRole(role)) return []
73
109
  const core = TASK_BOOK_PARAMS.core
74
- return [...core.slice(0, 2), TASK_BOOK_PARAMS.fifth[fifthClassOf(role)], ...core.slice(2)]
110
+ return [...core.slice(0, 2), TASK_BOOK_PARAMS.fifth, ...core.slice(2)]
75
111
  }
76
112
 
77
113
  /** 拼装标签(不导出;仅词面,不是判据——无任何匹配读它)。 */
78
114
  const TASK_BOOK_LABELS = {
79
115
  goal: "Goal & why",
80
116
  knownFacts: "Known facts",
81
- designPoints: "Design points & forbidden scope",
82
117
  scope: "Scope (what to read / what not to touch)",
83
118
  acceptance: "Acceptance criteria",
84
119
  reportFormat: "Delivery report format",
85
120
  }
86
121
 
87
122
  /**
88
- * F2 任务书参数校验(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.2)· **所有 spawn 角色**:
89
- * 名集按角色取(`taskBookParamsFor(role)`)。判据只有三条——参数存在 ∧ typeof string ∧
90
- * trim() 非空。不读内容:无正则、无标题匹配、无双语 marker。缺 / 空白 / 非字符串 → 合并抛,
91
- * 文案逐项点名该参数;第五项缺失而另一名在场时,文案附一句指路(只作文案,不加判据)。
123
+ * F2 任务书参数校验(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.2)· **非工程角色**:
124
+ * 名集按角色取(`taskBookParamsFor(role)` = 四项核心 + `scope`)。判据只有三条——参数存在 ∧
125
+ * typeof string ∧ trim() 非空。不读内容:无正则、无标题匹配、无双语 marker。缺 / 空白 /
126
+ * 非字符串 → 合并抛,文案逐项点名该参数;`scope` 缺失而 `designPoints` 在场时,文案附一句
127
+ * 指路(只作文案,不加判据;工程角色不经本门——它们拒收全部自由文本键)。
92
128
  */
93
129
  export function validateTaskBookParams(args, role) {
94
130
  const a = args ?? {}
95
131
  const names = taskBookParamsFor(role)
96
- const fifth = TASK_BOOK_PARAMS.fifth[fifthClassOf(role)]
97
- const otherFifth = fifth === "designPoints" ? "scope" : "designPoints"
132
+ const fifth = TASK_BOOK_PARAMS.fifth
133
+ const otherFifth = "designPoints"
98
134
  const failed = []
99
135
  for (const name of names) {
100
136
  const v = a[name]
101
137
  if (v === undefined || v === null) {
102
138
  const hint = name === fifth && a[otherFifth] !== undefined && a[otherFifth] !== null
103
- ? ` (got ${otherFifth} instead — that name belongs to the ${fifthClassOf(role) === "engineering" ? "explore/plan/coder" : "eng-coder/eng-designer"} spawns)`
139
+ ? ` (got ${otherFifth} instead — ${otherFifth} is retired for every role; the fifth parameter here is ${fifth})`
104
140
  : ""
105
141
  failed.push(`${name} — missing (the task book is four core parameters + the role's fifth)${hint}`)
106
142
  }
@@ -114,8 +150,9 @@ export function validateTaskBookParams(args, role) {
114
150
  `(the fifth parameter for role='${role}'); a missing, blank or non-string value is a dispatch defect and the spawn is refused.`)
115
151
  }
116
152
 
117
- /** 五参 → 子代理任务书文本(**非门禁**,纯拼装):按 `taskBookParamsFor(role)` 固定序 + 标签,
118
- * 空值跳过,`\n\n` 分隔。拼装单源 = 本函数——子代理 input 与探针判官的「任务书全文」同源。 */
153
+ /** 非工程角色任务书 → 子代理任务书文本(**非门禁**,纯拼装):按 `taskBookParamsFor(role)` 固定序
154
+ * + 标签,空值跳过,`\n\n` 分隔。拼装单源 = 本函数——子代理 input、权限预览与探针判官同源。
155
+ * 工程角色名集为空 ⇒ 返回空串(任务书 = 批次档 §2,运行时不拼装——KD-SC5)。 */
119
156
  export function composeTaskBookBrief(args, role) {
120
157
  const a = args ?? {}
121
158
  const parts = []
@@ -21,7 +21,7 @@ import { runWithContinue, TURN_CAP_MARK } from "../agent/spawn-child.mjs"
21
21
  import { pushReal } from "../context.mjs"
22
22
  import { offloadToolResult } from "../agent/helpers.mjs"
23
23
  import { logEvent } from "../log.mjs"
24
- import { digestBudgetOver, persistOverflowReport } from "./digest-budget.mjs"
24
+ import { digestBudgetOver, persistOverflowReport, _digestOffloadDirForTest } from "./digest-budget.mjs"
25
25
  // 群 B 批 B5(AGENT-LOOP.md §6.14 D-DG4):预算单源迁出(digest-budget.mjs)——原处 re-export 保测试
26
26
  // 导入面零改(DIGEST_INJECT_BUDGET / _setDigestOffloadDirForTest)。
27
27
  export { DIGEST_INJECT_BUDGET, _setDigestOffloadDirForTest } from "./digest-budget.mjs"
@@ -380,10 +380,13 @@ export async function injectAsyncResult(agent, entry) {
380
380
  const body = entry.error ?? entry.report ?? "(no report)"
381
381
  // F-2(BATCH-3-STRUCTURE)+ AGENT-LOOP.md §6.14 D-DG2(群 B 批 B5):注入前查轮累计(单源)——超限条目不
382
382
  // inline 全文,改清单行(全文经 persistOverflowReport 落盘——path 随行)。首条豁免
383
- // (used===0 不判超):单条大报告 >64K offload 预览照旧——轮预算只约束累计。
383
+ // (used===0 不判超)。
384
384
  const raw = String(body)
385
385
  const over = digestBudgetOver(agent, raw.length)
386
- const preview = (over && await persistOverflowReport(raw, { tag: `async-subagent-${entry.id}` })) || await offloadToolResult(raw, `async-subagent-${entry.id}`)
386
+ // TIMEOUT-DIGEST.md §2.2:轮累计超预算 ⇒ 清单行(正文不进请求体);否则无条件落盘——`force` 跳过
387
+ // 长度门(≤64K 的报告也上盘,父回合丢了还能按路径回读);渲染二分全在 offloadToolResult。
388
+ const preview = (over && await persistOverflowReport(raw, { tag: `async-subagent-${entry.id}` }))
389
+ || await offloadToolResult(raw, `async-subagent-${entry.id}`, _digestOffloadDirForTest(), { force: true })
387
390
  // §6.10: advisor entries label themselves (role "advisor") — the digest
388
391
  // reminder says "async advisor review #N finished" (T-24b2 shape); subagent
389
392
  // entries keep the legacy wording verbatim.