@thincoder/core 0.10.8 → 0.10.10

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 (85) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/advisor/messages.mjs +4 -5
  3. package/advisor/project-context.mjs +69 -44
  4. package/agent/family-tools.mjs +11 -9
  5. package/agent/helpers.mjs +12 -6
  6. package/agent/post-turn.mjs +34 -0
  7. package/agent/run-stages.mjs +43 -9
  8. package/agent/run-start.mjs +1 -0
  9. package/agent/setup.mjs +10 -2
  10. package/agent/turn-loop.mjs +23 -13
  11. package/agent-tools/audit-block.mjs +4 -2
  12. package/agent-tools/batch-gate.mjs +197 -0
  13. package/agent-tools/batch-lifecycle.mjs +216 -36
  14. package/agent-tools/batch-skeleton.mjs +87 -0
  15. package/agent-tools/batch.mjs +11 -3
  16. package/agent-tools/checkpoint.mjs +11 -6
  17. package/agent-tools/context.mjs +28 -5
  18. package/agent-tools/design-slots.mjs +7 -33
  19. package/agent-tools/doc-check.mjs +50 -7
  20. package/agent-tools/manifest.mjs +44 -12
  21. package/agent-tools/spawn-gates.mjs +100 -41
  22. package/agent-tools/ssh-session.mjs +108 -61
  23. package/agent-tools/ssh-transport.mjs +17 -4
  24. package/agent-tools/ssh-vault.mjs +291 -0
  25. package/agent-tools/subagent-spawn.mjs +52 -30
  26. package/agent-tools/subagent.mjs +14 -8
  27. package/agent-tools/timer.mjs +2 -2
  28. package/agent-tools.mjs +1 -1
  29. package/config-defaults.mjs +92 -0
  30. package/config-normalize.mjs +109 -0
  31. package/config.mjs +88 -197
  32. package/declaration.mjs +5 -12
  33. package/doc-check/anchors.mjs +68 -34
  34. package/doc-check/index.mjs +143 -32
  35. package/doc-check/version.mjs +84 -0
  36. package/doc-check/width.mjs +10 -54
  37. package/i18n.mjs +7 -0
  38. package/ledger-cmd.mjs +314 -61
  39. package/ledger-db.mjs +22 -10
  40. package/ledger-migrate.mjs +15 -9
  41. package/ledger-tools.mjs +115 -69
  42. package/ledger.mjs +13 -10
  43. package/log.mjs +2 -2
  44. package/manifest-schema.mjs +7 -21
  45. package/manifest.mjs +4 -4
  46. package/memory/code-search.mjs +5 -14
  47. package/memory/docs.mjs +5 -14
  48. package/memory/read-gate.mjs +26 -0
  49. package/package.json +1 -1
  50. package/permission.mjs +7 -1
  51. package/prompts/advisor-design.md +95 -44
  52. package/prompts/advisor-round1.md +75 -27
  53. package/prompts/advisor-round2.md +68 -31
  54. package/prompts/advisor-round3.md +77 -33
  55. package/prompts/common.md +283 -113
  56. package/prompts/consult-base.md +16 -9
  57. package/prompts/discipline-engineering.md +213 -83
  58. package/prompts/discipline-normal.md +137 -85
  59. package/prompts/persona-coder.md +8 -6
  60. package/prompts/persona-eng-coder.md +42 -21
  61. package/prompts/persona-eng-designer.md +101 -43
  62. package/prompts/persona-engineering.md +376 -127
  63. package/prompts/persona-explore.md +3 -3
  64. package/prompts/persona-normal.md +29 -21
  65. package/prompts/persona-plan.md +7 -7
  66. package/prompts/subagent-base.md +4 -4
  67. package/provider/anthropic.mjs +4 -2
  68. package/provider/core.mjs +4 -2
  69. package/provider/google.mjs +5 -1
  70. package/provider/responses-request.mjs +3 -2
  71. package/session-lifecycle.mjs +6 -6
  72. package/team.mjs +2 -1
  73. package/token-window.mjs +1 -1
  74. package/tool-docs/batch.md +1 -1
  75. package/tool-docs/doc_check.md +47 -12
  76. package/tool-docs/fetch.md +3 -2
  77. package/tool-docs/ledger.md +7 -5
  78. package/tool-docs/manifest-read.md +8 -0
  79. package/tool-docs/manifest.md +9 -8
  80. package/tool-docs/ssh.md +23 -6
  81. package/tool-docs/subagent.md +7 -12
  82. package/tool-docs/websearch.md +3 -1
  83. package/tools/ssh.mjs +24 -11
  84. package/tools/web-deepseek.mjs +172 -0
  85. package/tools/web.mjs +29 -4
@@ -26,9 +26,9 @@ import { nextSubagentId } from "./subagent-scheduler.mjs"
26
26
  import {
27
27
  normalizeFileList, describeBlockers, assertNoDepCycle, depInfo,
28
28
  } from "./subagent-scheduler.mjs"
29
- // M5 F2(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.3):任务书六强制字段校验本体落
29
+ // M5 F2(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.3):任务书 round 枚举 + 五项结构化参数校验/拼装本体落
30
30
  // spawn-gates.mjs(纯谓词零依赖)——本处只加 import 调用。
31
- import { validateTaskBookFields } from "./spawn-gates.mjs"
31
+ import { validateRoundParam, validateTaskBookParams, composeTaskBookBrief } from "./spawn-gates.mjs"
32
32
  // 台账 #23(AGENT-LOOP-SUBAGENT.md §6.26 · D23-3):审计块构造器外提(`summarizeEngTaskBook`
33
33
  // + `buildAuditBlock` 纯函数)——spawn 档只做「收集 + 绑定」;块文本逐字搬移(D23-4)。
34
34
  import { buildAuditBlock } from "./audit-block.mjs"
@@ -38,6 +38,14 @@ import { resolveBatchReadPath, batchDocBases } from "./batch-paths.mjs"
38
38
  // LOGGING(LOGGING.md):#309 形三观测留痕走同一事件通道。
39
39
  import { logEvent } from "../log.mjs"
40
40
 
41
+ /** 任务书播报文本:该角色名集拼装 + 可选 task 自由文本(拼装空 ⇒ 原 task 逐字——旧形零变)。 */
42
+ function taskBriefOf(args, role) {
43
+ const composed = composeTaskBookBrief(args, role)
44
+ if (!composed) return args.task
45
+ const extra = typeof args.task === "string" ? args.task.trim() : ""
46
+ return extra ? `${composed}\n\n${extra}` : composed
47
+ }
48
+
41
49
  /** #309 任务书 token 判据(`AGENT-LOOP-SUBAGENT.md` §6.29.3):`.md` 结尾的连续路径型字面
42
50
  * (含盘符形态——裸名 `AGENTS.md` 无分隔符且非绝对,见调用点过滤)。 */
43
51
  const MD_PATH_TOKEN = /[A-Za-z0-9_@.:\\/-]+\.md\b/g
@@ -120,7 +128,7 @@ export function resolveDesignSlot(parent, designIdArg) {
120
128
  throw new Error(`Multiple approved designs in this session (${size}) — pass the designId parameter (echoed with each token) to choose which design this eng-coder spawn belongs to. Held design ids: ${heldIds}`)
121
129
  }
122
130
  if (size === 1) return { token: [...slots.values()][0] }
123
- throw new Error("Invalid or missing design token — run advisor with type='design' first and pass the returned token as designToken.")
131
+ throw designTokenRefusal("missing")
124
132
  }
125
133
 
126
134
  /**
@@ -211,7 +219,7 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
211
219
  batchDocAbs = resolveBatchReadPath(parent.cwd ?? process.cwd(), given)
212
220
  if (!batchDocAbs) throw refusal(" (given path is not a readable file)")
213
221
  // #309 形三:任务书中的他批批次档引用观测留痕(不阻断——放行不受影响)。
214
- logBatchDocRefs(parent, role, args.task, batchDocAbs)
222
+ logBatchDocRefs(parent, role, taskBriefOf(args, role), batchDocAbs)
215
223
  } else if (role === "coder") {
216
224
  const given = typeof args.batchDoc === "string" ? args.batchDoc.trim() : ""
217
225
  if (given) {
@@ -222,33 +230,18 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
222
230
  }
223
231
  }
224
232
 
225
- // M5 F2(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.2):任务书六强制字段门——
226
- // 工程角色(eng-coder/eng-designer)必带 round(initial|fix)+ 五段任务书字段;
227
- // 落点 = batchDoc 门之后、token 门之前(§2.2 数据流契约同序)。判据只到「字段在场」——
228
- // 内容够不够由执行者拒收兜底(需求 §1.14 #9 行为面)。explore/plan/coder 豁免。
229
- if (role === "eng-coder" || role === "eng-designer") {
230
- validateTaskBookFields(args)
231
- }
233
+ // M5 F2(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.2):任务书参数门——**所有 spawn 角色**。
234
+ // 工程角色:round 枚举(initial|fix)+ 四项核心 + `designPoints`;explore / plan / coder:
235
+ // 四项核心 + `scope`。判据三条零改(存在 ∧ string ∧ trim 非空);落点 = batchDoc 门之后、
236
+ // token 门之前(门序 = AGENT-LOOP-SUBAGENT.md §6.22)。
237
+ if (role === "eng-coder" || role === "eng-designer") validateRoundParam(args)
238
+ validateTaskBookParams(args, role)
232
239
 
233
- // eng-coder token gate: the design review must have passed and the caller must
234
- // present the exact token advisor issued — otherwise the child is not authorized to code.
235
- // 2026-09-01: multi-design slots — the token is located by designId (exact slot,
236
- // single-slot fallthrough); token validation itself is unchanged (2026-09-06:
237
- // HMAC removed — validateDesignToken now checks format + TTL only).
240
+ // eng-coder token gate(TOOLS.md §6.25 ④):判定本体 = gateDesignToken(本档模块尾)——
241
+ // missing / mismatch / expired / invalid 四态各出互异拒句;拒绝集合与旧三条件合取全等。
242
+ // 返回值 = 槽内令牌,必须落回 issuedToken:下方 `child._engDesignId` 回写依赖它。
238
243
  let issuedToken
239
- if (role === "eng-coder") {
240
- issuedToken = resolveDesignSlot(parent, args.designId).token
241
- // R16 D-R16c ③ — spawn-gate cleanup: only an EXPIRY rejection removes the slot
242
- // (the caller passed the slot's own token and it is past TTL — 长跑不重启也清);
243
- // mismatch / format rejections never delete — the slot's own token may still be
244
- // valid and a caller error must not destroy it (T-R16d). 单值镜像已退役(D3)——
245
- // 无镜像同步清。
246
- const expiredReject = !!issuedToken && args.designToken === issuedToken && tokenExpired(issuedToken)
247
- if (!issuedToken || args.designToken !== issuedToken || !validateDesignToken(args.designToken)) {
248
- if (expiredReject) removeDesignTokenSlot(parent, args.designId, issuedToken)
249
- throw new Error("Invalid or missing design token — run advisor with type='design' first and pass the returned token as designToken.")
250
- }
251
- }
244
+ if (role === "eng-coder") issuedToken = gateDesignToken(parent, args)
252
245
 
253
246
  // Filter tool set by role: explore/plan are read-only (plan is a planning agent, its deliverable is the plan itself)
254
247
  let tools
@@ -345,7 +338,7 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
345
338
  // 审计/探索("status 里这个文件算不算超清单?")。注入分支整体删除(B 方案
346
339
  // git 只读变体亦随裁定废弃——D-AG5)。顶层主 agent 注入保留(§3 prepareRun——
347
340
  // setup.mjs depth===0——D-AG7 范围边界)。
348
- let input = args.context ? `Context:\n${args.context}\n\nTask:\n${args.task}` : args.task
341
+ let input = args.context ? `Context:\n${args.context}\n\nTask:\n${taskBriefOf(args, role)}` : taskBriefOf(args, role)
349
342
  // 台账 #23(AGENT-LOOP-SUBAGENT.md §6.26 · D23-1):**spawn 级固定机制性指令改走 system 固块**——
350
343
  // 两处(批次档路径行 + 审计模板块)原随 user 首条下发,压缩把首条并入摘要即丢(长任务中后期
351
344
  // 既看不到批次档路径、也看不到审计模板 ⇒ 过度勘察的真病根);固块值在 child 生命周期内恒定
@@ -410,3 +403,32 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
410
403
  const childRunOpts = buildChildRunOpts(ctx)
411
404
  return { child, input, childOpts, childRunOpts, relayPrefix, childProvider }
412
405
  }
406
+
407
+ // ── design-token 门(TOOLS.md §6.25 ③④——四态互异拒句;拒绝集合与旧三条件合取全等) ──
408
+
409
+ /** 四态拒句(逐字 = TOOLS.md §6.25 ③):missing / mismatch / expired / invalid。
410
+ * mismatch 可携 designId(非凭证);四句均零令牌值回显。 */
411
+ export function designTokenRefusal(kind, designId) {
412
+ if (kind === "missing") return new Error("designToken missing — the eng-coder spawn is refused: no usable token for an approved design slot came with this spawn. Pass the designToken parameter with the exact value from the subagent action:'design-slots' row (the text after designToken=), copied verbatim; if that list is empty, no design review has been approved in this session yet — run advisor with type='design' first. No token value is echoed here.")
413
+ if (kind === "mismatch") return new Error("designToken mismatch — the eng-coder spawn is refused: the value passed does not equal the token held for this design slot. Re-read the subagent action:'design-slots' row and pass the value after designToken= verbatim, unchanged. No token value is echoed here." + (designId ? ` (designId: ${designId})` : ""))
414
+ if (kind === "expired") return new Error("designToken expired — the eng-coder spawn is refused: the token matches this design slot but is past its TTL. The expired slot has been removed; run advisor with type='design' again on the current design and pass the newly issued token — an expired authorization cannot be retried. No token value is echoed here.")
415
+ return new Error("designToken invalid — the eng-coder spawn is refused: the value passed equals this design slot's stored token, but that stored value is not a well-formed token, so no expiry can be judged and the slot was left in place. Run advisor with type='design' to issue a fresh token for this design; a malformed record never passes the gate. No token value is echoed here.")
416
+ }
417
+
418
+ /** eng-coder spawn 的令牌门(TOOLS.md §6.25 ④):resolveDesignSlot 的既有两条 designId
419
+ * 诊断零改 → 参数在场 → 值相等 → 值有效。槽删除只在 tokenExpired(given) 成立时发生
420
+ * (D-R16c ③);mismatch / invalid 不删槽(T-R16d)。返回槽内令牌(调用方回写 designId
421
+ * 依赖它);sync / async 两路共经 buildSpawnChild——一处判定双路生效。 */
422
+ export function gateDesignToken(parent, args) {
423
+ const issuedToken = resolveDesignSlot(parent, args?.designId).token
424
+ // 非字符串 ⇒ 空串 ⇒ missing(本意——T9 锁定)
425
+ const given = typeof args?.designToken === "string" ? args.designToken : ""
426
+ if (!issuedToken || !given.trim()) throw designTokenRefusal("missing")
427
+ if (given !== issuedToken) throw designTokenRefusal("mismatch", args?.designId)
428
+ if (!validateDesignToken(given)) {
429
+ const expired = tokenExpired(given)
430
+ if (expired) removeDesignTokenSlot(parent, args?.designId, given)
431
+ throw designTokenRefusal(expired ? "expired" : "invalid")
432
+ }
433
+ return issuedToken
434
+ }
@@ -5,7 +5,6 @@
5
5
  * 2026-10-07 批 ledger-tool(#927 并入):+action: design-slots(只读清点面——
6
6
  * DESIGN-TOKEN-SETTLEMENT.md §9 F-SL1)+ consume-design 选择器扩展(designIds / expired /
7
7
  * olderThanDays)——实现在 `design-slots.mjs`(subagent-spawn.mjs re-export 保 import 面)。
8
- * 2026-10-14 批 design-token-recovery(#1384 并入):design-slots 回收读口——`designId` 过滤 + `reveal:true` 单槽揭值(必携 designId;掩蔽 = 只揭活槽)——实现随动在 `design-slots.mjs`。
9
8
  *
10
9
  * 2026-09-03 拆分轮: subagent.mjs 超 500 硬顶——async 常量、共享 post-spawn 管线
11
10
  *(runChildPipeline)与队列/注入/并账机械迁至 ./subagent-async.mjs。execute
@@ -37,7 +36,7 @@ import { executeStatusAction, executeEscalateAction, executePanelAction, execute
37
36
  import { prepareScheduling, buildSpawnChild, executeConsumeDesignAction } from "./subagent-spawn.mjs"
38
37
  import { executeDesignSlotsAction } from "./design-slots.mjs"
39
38
  import { executeAsyncSpawn } from "./subagent-run.mjs"
40
- import { ROUND_VALUES } from "./spawn-gates.mjs"
39
+ import { ROUND_VALUES, composeTaskBookBrief } from "./spawn-gates.mjs"
41
40
  // #15 描述外置:描述文本单点 = tool-docs/subagent.md(DESC 单解析面,缺档抛错语义不变)
42
41
  import { DESC } from "../tools/shared.mjs"
43
42
 
@@ -140,18 +139,23 @@ export const subagentTool = {
140
139
  action: { type: "string", enum: ["spawn", "status", "escalate", "cancel", "panel", "consume-design", "observe", "send", "design-slots"], description: "Which action — spawn (default) / status / observe / send / escalate / cancel / panel / consume-design / design-slots; semantics per action in the tool description." },
141
140
  view: { type: "boolean", description: "action:'panel' only: true (default) = the live panel blocks as the user sees them; false without freeze = error. Mutually exclusive with freeze (freeze wins)." },
142
141
  freeze: { type: "string", description: "action:'panel' only: block key of a digested-stuck block — reclaims it into the conversation; refused for running/done/unknown blocks or a still-pending report; works on the CLI TUI panel and on the desktop renderer panel (live readback of the blocks as the user sees them)." },
143
- task: { type: "string", description: "Required for action:'spawn' (the self-contained task brief) and action:'escalate' (goal, constraints, entry files, acceptance criteria)." },
142
+ task: { type: "string", description: "For action:'spawn' (any role): OPTIONAL free-text supplement — the brief itself is the structured task-book parameters (four core: goal / knownFacts / acceptance / reportFormat, plus the role's fifth: designPoints for eng-coder/eng-designer, scope for explore/plan/coder); a task-only spawn is refused. For action:'escalate': the brief (goal, constraints, entry files, acceptance criteria)." },
144
143
  context: { type: "string", description: "Optional background the sub-agent needs (it cannot see this conversation); action:'spawn' only." },
145
144
  role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder", "eng-designer"], description: "The sub-agent role — see the role matrix in the tool description. Exact spelling required. action:'spawn' only (escalate spawns its own expert internally)." },
146
145
  model: { type: "string", description: "action:'spawn': provider/model override ('provider:model', a provider, or a model name on the parent's provider) — defaults config.agent.subagentModels[role] → config.agent.subagentModel → the parent's provider; \"default\" inherits. action:'escalate': a consult candidate (default = the first)." },
147
- designToken: { type: "string", description: "Required when role='eng-coder': the token from advisor(type='design') after the review passed — without it, eng-coder cannot modify files." },
148
- designId: { type: "string", description: "role='eng-coder': the designId echoed with the approved token — required to pick between concurrent designs (optional for one). action:'consume-design': the single-slot selector (required to pick between several; the gate refuses to guess); see designIds / expired / olderThanDays for batch consumption. action:'design-slots': filter the inventory to one slot (trimmed exact match; an empty string means no filter; an unknown id is refused with the held id list) — REQUIRED with reveal:true." },
146
+ designToken: { type: "string", description: "REQUIRED for role='eng-coder': the exact token issued by advisor(type='design') after the review passed — copy it verbatim from the design-slots row (the text after designToken=). missing or mismatched = the spawn is refused; an expired token is refused with expired named. Pass it on every eng-coder spawn, fix rounds included (they reuse the same token)." },
147
+ designId: { type: "string", description: "role='eng-coder': the designId echoed with the approved token — required to pick between concurrent designs (optional for one). action:'consume-design': the single-slot selector (required to pick between several; the gate refuses to guess); see designIds / expired / olderThanDays for batch consumption. action:'design-slots': filter the inventory to one slot (trimmed exact match; an empty string means no filter; an unknown id is refused with the held id list)." },
149
148
  designIds: { type: "array", items: { type: "string" }, description: "action:'consume-design' (batch): an explicit non-empty list of design ids to close out — pass exactly ONE selector per call (designId / designIds / expired / olderThanDays)." },
150
149
  expired: { type: "boolean", description: "action:'consume-design' (batch): true = close out every expired slot (exactly ONE selector per call)." },
151
150
  olderThanDays: { type: "number", description: "action:'consume-design' (batch): close out every slot older than N days (age > N; exactly ONE selector per call)." },
152
- reveal: { type: "boolean", description: "action:'design-slots' only: true = reveal the selected slot's full designToken — REQUIRES designId (one slot at a time, never a bulk dump); an expired or malformed slot is masked (designToken=—); a non-boolean value is refused. After a token drops from your context (compaction, restart), read it back this way and pass designId + designToken verbatim on the eng-coder spawn — the gate checks exact equality." },
153
151
  batchDoc: { type: "string", description: "REQUIRED for role='eng-coder'/'eng-designer': the batch record path (e.g. <repo>/docs/batches/<batch>-<topic>.md; relative paths resolve against the session cwd first, then candidate project roots — or absolute) — the task book this spawn implements/writes. Refused when absent or when the path does not resolve to a readable file; the content is never validated. role='coder' (normal mode): optional — binds this spawn's §5 record-write channel (unbound ⇒ the main agent records §5 on its behalf); when provided the path must resolve to a readable file." },
154
152
  round: { type: "string", enum: ROUND_VALUES, description: "REQUIRED for role='eng-coder'/'eng-designer': initial (first round for this design) or fix (a correction round reusing the same designId+designToken; docs FIRST). No default — missing = refused." },
153
+ goal: { type: "string", description: "REQUIRED for every spawn role: the task book's goal & why — the problem this solves and the cost chain (content only, no label — the runtime composes the labels)." },
154
+ knownFacts: { type: "string", description: "REQUIRED for every spawn role: the facts and file paths the parent already verified — the child must not re-explore them." },
155
+ designPoints: { type: "string", description: "REQUIRED for role='eng-coder'/'eng-designer' (the fifth task-book parameter for engineering roles): design points & forbidden scope — what to land and what must not be touched. Non-engineering roles use `scope` instead — `scope` never substitutes for this one." },
156
+ scope: { type: "string", description: "REQUIRED for role='explore'/'plan'/'coder' (the fifth task-book parameter for non-engineering roles): scope & forbidden scope — what to read / search and what not to touch. Engineering roles use `designPoints` instead — `designPoints` never substitutes for this one." },
157
+ acceptance: { type: "string", description: "REQUIRED for every spawn role: acceptance criteria — machine-verifiable (commands / thresholds / assertion counts)." },
158
+ reportFormat: { type: "string", description: "REQUIRED for every spawn role: the delivery-report format the child must produce." },
155
159
  async: { type: "boolean", description: "action:'spawn'/'escalate': default at depth 0 = true (async; the report arrives automatically); depth>0 always sync. async:false forces the blocking flight." },
156
160
  files: { type: "array", items: { type: "string" }, description: "action:'spawn' only: the file write-domain this task declares — file-level paths only (cwd-relative or absolute; directories rejected — they bypass the conflict detector). Overlapping-file tasks serialize automatically. Omit = no conflict detection." },
157
161
  dependsOn: { type: "array", items: { type: "string" }, description: "action:'spawn' only: ids from prior async returns whose outcome this task needs — queues until every dependency settles, then starts. Consumed ids count as satisfied; a cancelled/failed dependency stays queued marked 'dependency cancelled' until you decide (AUTO sessions auto-start). Unknown ids error." },
@@ -218,8 +222,10 @@ export const subagentTool = {
218
222
  // Spawn requires a task brief — schema `required` is advisory (multi-action
219
223
  // schema), so the mechanical check lives here: an absent task would otherwise
220
224
  // flow downstream as `content: undefined` and surface as an obscure error.
221
- if (typeof args.task !== "string" || !args.task.trim()) {
222
- throw new Error("subagent action:'spawn' requires a task (the self-contained task brief).")
225
+ // 任务书载体:结构化参数(拼装后非空)——task 只作补充;两者皆缺 ⇒ 此处先拒。
226
+ // (五项必带门 = spawn-gates.validateTaskBookParams,覆盖所有 spawn 角色、落 buildSpawnChild——task-only 由该门点名拒。)
227
+ if (!composeTaskBookBrief(args, args.role) && (typeof args.task !== "string" || !args.task.trim())) {
228
+ throw new Error("subagent action:'spawn' requires the structured task-book parameters (four core + the role's fifth: designPoints for eng-coder/eng-designer, scope for explore/plan/coder) — a free-text task alone is not the brief.")
223
229
  }
224
230
  // §18 D-E1a depth-gated async default (2026-09-06 需求池 R12): depth-0 spawns
225
231
  // default to async for EVERY role (the old role-level default — eng-coder only —
@@ -50,14 +50,14 @@ function kindText(t) {
50
50
  if (typeof everyMs === "number" && Number.isFinite(everyMs) && everyMs > 0) return `every ${everyMs / 1000}s`
51
51
  const daily = t?.daily
52
52
  if (typeof daily === "string" && DAILY_RE.test(daily)) return `daily ${daily}`
53
- if (t?.at === true) return `at ${localText(t.expiresAt)}`
53
+ if (t?.at === true && Number.isFinite(t.expiresAt)) return `at ${localText(t.expiresAt)}`
54
54
  return "one-shot"
55
55
  }
56
56
 
57
57
  /** `list` 行第三槽:到期未投 ⇒ `due now`;否则 `next <dur>`(剩余现算)。 */
58
58
  function nextText(t, now) {
59
59
  const at = Number(t?.expiresAt)
60
- return at > now ? `next ${durText(at - now)}` : "due now"
60
+ return Number.isFinite(at) && at > now ? `next ${durText(at - now)}` : "due now"
61
61
  }
62
62
 
63
63
  /** `message` 首行(`list` 行第四槽——逐字原样,不截断)。 */
package/agent-tools.mjs CHANGED
@@ -13,7 +13,7 @@ export { recentChangesTool } from "./agent-tools/recent-changes.mjs"
13
13
  export { timerTool } from "./agent-tools/timer.mjs"
14
14
  export { advisorTool } from "./agent-tools/advisor.mjs"
15
15
  export { engTool } from "./agent-tools/eng.mjs"
16
- export { manifestTool } from "./agent-tools/manifest.mjs"
16
+ export { manifestTool, manifestReadTool } from "./agent-tools/manifest.mjs"
17
17
  export { readHistoryTool } from "./agent-tools/read-history.mjs"
18
18
  // 模型主动整理上下文(CONTEXT-COMPACTION.md §6.16 · F-CC5):单工具三操作 stats / prune / compact
19
19
  // ——depth-0 段挂载(子代理面裁见 §6.16.6 `family-tools.mjs` `depthOnly`)。
@@ -0,0 +1,92 @@
1
+ /**
2
+ * config-defaults.mjs — the configuration default table itself (pure data, one comment per
3
+ * non-obvious key). Split out of config.mjs so that changing a default value does not require
4
+ * reading the whole loader: defaults (data) ∥ load-time normalization rules (logic) ∥ request
5
+ * budget arithmetic were three unrelated things living in one file
6
+ * (CONTEXT-COMPACTION.md §6.16.9 — C1 split, judged by responsibility, not by line count).
7
+ *
8
+ * Re-exported unchanged by config.mjs (`export { DEFAULTS }`) — the 34 importer files keep
9
+ * their import path and this table keeps its name.
10
+ */
11
+
12
+ import { join } from "node:path"
13
+ import { configDir } from "./config-io.mjs"
14
+
15
+ export const DEFAULTS = {
16
+ defaultModel: null, // top-level "provider:model" composite — new-session starting point (F-1)
17
+ agent: {
18
+ maxTurns: 200,
19
+ subagentTurns: 100,
20
+ subagentModel: null, // default subagent model: "provider:model" | model name (parent provider); null = inherit parent provider
21
+ subagentModels: {}, // per-type override: { explore, plan, coder, "eng-coder" } — priority: subagent tool model arg > this[role] > subagentModel > parent provider
22
+ goalTurns: 200,
23
+ // compactThreshold: 键缺省 = auto(CONTEXT-COMPACTION.md §6.17 三档落值)——无默认值:留着默认值
24
+ // 会让每份配置看着都像显式。派生线只进 agent.compactThresholdResolved(内存面,不落盘)。
25
+ verifyGuard: false, // push model back to verify when files were mutated but verify not run (opt-in)
26
+ // Multi-model consultation ("会诊") + escalate ("飞刀") — CLI parity with the VS Code plugin.
27
+ // consultModels: candidate pool for BOTH consult and escalate ({ provider, model, effort? }, up to 5).
28
+ consultModels: [],
29
+ consultTurns: 40, // per-consultant tool-turn budget (diagnosis tasks)
30
+ consultTimeoutMs: 600000, // wall-clock ceiling per consultant (10min)
31
+ streamRules: [], // time-traveling stream rules: [{ pattern: "regex", message: "reminder", action: "abort"|"warn", repeat: "always"|"once" }]
32
+ advisor: { guard: false }, // code review is always available; guard: true pushes completion back until reviewed (opt-in). Also accepts provider/model/thinking/reasoningEffort/timeoutMs overrides. Deprecated: enabled (2026-08-21)
33
+ autoThink: false, // auto-classify task difficulty and set reasoning effort per-turn
34
+ engineering: false, // initial default only — the runtime value is the session authority (MANIFEST.md §2.2 / ENGINEERING-MODE-V2.md §2.3 E5.1). Strict methodology enforcement — design-before-code (design review + user approval before code)
35
+ // Async pool limits (AGENT-LOOP-ASYNC-POOL.md §6.10 D-24a/R14 + R13 — POOL-CONFIG-
36
+ // UNIFIED 2026-09-09): { engCoder, other, advisor } — eng-coder pool / other-role
37
+ // pool / advisor-review pool, defaults 4/4/4 (user ruling "eng-coder 四路,其他
38
+ // 4 路" + advisor 评审池并入同一可配体系——三池统一默认 4)。engCoder/other
39
+ // runtime-validated at every subagent pool admission (subagent-async
40
+ // resolvePoolLimits); advisor runtime-validated at every advisor launch
41
+ // (advisor-async advisorPoolLimitFor——F-2——两域读取器独立不共享)——非法/缺省
42
+ // 回退 4——settings tool 与 /config 并发池菜单写此键——变更下个 spawn/launch 生效。
43
+ // ⚠ 与 subagent-async.mjs ASYNC_POOL_LIMITS(两键)/advisor-async.mjs
44
+ // ADVISOR_POOL_LIMIT 逐键同值(运行时回退常量)——耦合锚 T-24a4 断言锁住——
45
+ // 勿单侧改默认。
46
+ poolLimits: { engCoder: 4, other: 4, advisor: 4 },
47
+ // §6.30.3 门三件③(D-TW5):timer 空闲自唤醒开关——默认开(工具描述面已承诺到点提醒);
48
+ // false ⇒ 端侧不武装闩(回既有回合边界投递——零调度器面)。键面随 DEFAULTS 派生进 settings 类型表。
49
+ timerWake: true,
50
+ // 注入开关(PROMPT-SYSTEM.md §6.24 · 台账 #1420):文档类 ∥ 记忆类自动注入——默认开;
51
+ // 判据 `!== false`(缺省 = 现行为零变)。消费点 = 核 `agent/setup.mjs` 判点(逐 run 现读)。
52
+ // 2026-10-15(§6.27 · 台账 #1465):① 项目指令块独立成键——`injectDocs` 收窄为只管 ② 相关文档块;
53
+ // 两键完全正交(关任一方另一方照常)。`injectProjectRules` 判点 = 核 `agent/setup.mjs`(`prepareRun` 项目指令段)。
54
+ injectDocs: true,
55
+ injectProjectRules: true,
56
+ injectMemories: true,
57
+ },
58
+ memory: {
59
+ dbPath: join(configDir, "memory.db"),
60
+ projectDir: ".thincoder/memory",
61
+ team: null,
62
+ // 索引自维护(§6.14 面⑤ · 台账 #1096):维护型备份保留枚数(轮转保最新 N——引擎钳 ≥ 1)
63
+ maintain: { backupKeep: 2 },
64
+ },
65
+ shell: null, // bash tool shell executable (e.g. "C:\\Program Files\\Git\\bin\\bash.exe" or "pwsh"); null = system default (cmd on Windows, /bin/sh elsewhere)
66
+ embedding: {
67
+ baseURL: "https://api.siliconflow.cn/v1",
68
+ model: "BAAI/bge-m3",
69
+ },
70
+ mcp: {
71
+ servers: [],
72
+ },
73
+ websearch: {
74
+ // Structured search via Tavily when a key is set — empty apiKey → Bing RSS/HTML fallback (zero-config).
75
+ apiKey: "", // Tavily key (tvly-...) — optional
76
+ // WEB-BACKEND-DEEPSEEK(CONFIG.md §6.5):`auto` = a configured DeepSeek provider wins(TOOLS.md §6.23);`local` = skip it.
77
+ backend: "auto",
78
+ },
79
+ // fetch 工具的后端选择(CONFIG.md §6.5)——语义同 `websearch.backend`(`local` = 本地直取)。
80
+ fetch: { backend: "auto" },
81
+ // 浏览器工具(BROWSER-TOOL.md §3.2):非空 ⇒ navigate 目标 host ∥ click/evaluate/type 的当前页 host 须命中
82
+ //(全等(大小写不敏感)∥ `*.` 前缀 = 子域通配)——收束面非沙箱;缺省空 = 不设限。
83
+ browser: { allowDomains: [] },
84
+ traces: {
85
+ enabled: false, // §18.6 D-TR6 修订(2026-09-05 用户裁定——发布隐私:"不希望用户那边也采集"):轨迹存档默认 OFF——新用户零采集;本地调试分析可显式开(~/.thincoder/config.json traces.enabled:true)
86
+ retentionHours: 24, // D-TR10:轨迹文件保留小时数——CLI 启动时删除超过该时长的文件(默认 24h)
87
+ },
88
+ // 诊断开关(用户面 ① 配置键——CONFIG.md §6.2 名录):`heapWatch` 默认开(设 false 关闭)——自愈分支
89
+ // 遥测;`heapSnapshot` 默认关——采集 opt-in(堆转储敏感度 ≥ traces ⇒ 对齐发布隐私先例 D-TR6;
90
+ // 2026-09-30 采集收网批;桌面端运行期热读 ∥ CLI 本进程下次启动)
91
+ diagnostics: { heapWatch: true, heapSnapshot: false },
92
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * config-normalize.mjs — load-time normalization: pure functions that wash a dirty disk face
3
+ * into a canonical shape (and the one-time warn flags that report what was dropped). Split out
4
+ * of config.mjs by responsibility (CONTEXT-COMPACTION.md §6.16.9 — C1): defaults (data) ∥ these
5
+ * rules (logic) ∥ request-budget arithmetic. Zero core imports — every function here is pure
6
+ * (the warn flags are process-level once-only guards), so the module adds no cycle.
7
+ *
8
+ * config.mjs re-exports the four load-bearing ones (`sanitizeSubagentModel` ·
9
+ * `sanitizeSubagentModels` · `normalizeProxy` · `normalizeTeamSection`) — importers keep their
10
+ * path. `sanitizeProviderHeaders` / `sanitizeConsultModels` / the two warn helpers are consumed
11
+ * by loadConfig only and are not part of the public config.mjs surface.
12
+ */
13
+
14
+ /** F-4 (IKCDMR) 软失败清洗(D-S1 范式——loadConfig 是 CLI 启动砖点):consultModels 非法
15
+ * 条目过滤不 throw——非数组 → []、形状非法/未知渠道丢弃、超 5 截断(keep 前 5)。
16
+ * 返回 { keep, dropped }——dropped 供一次性警告;merged.agent.consultModelsFiltered 挂载
17
+ * 供后续首帧引导消费(数据层——本批文件面无消费方——同 providerInvalidReason 载体先例)。 */
18
+ export function sanitizeConsultModels(cm, providerNames) {
19
+ if (cm === undefined || cm === null) return { keep: [], dropped: [] }
20
+ if (!Array.isArray(cm)) {
21
+ return { keep: [], dropped: [`agent.consultModels must be an array of { provider, model } entries (got ${typeof cm})`] }
22
+ }
23
+ const names = new Set(providerNames)
24
+ const keep = []
25
+ const dropped = []
26
+ for (const entry of cm) {
27
+ if (keep.length >= 5) { dropped.push(`over the 5-entry cap — dropped ${JSON.stringify(entry)}`); continue }
28
+ if (!entry || typeof entry !== "object" || typeof entry.provider !== "string" || !entry.provider.trim()
29
+ || typeof entry.model !== "string" || !entry.model.trim()) {
30
+ dropped.push(`invalid entry (expected { provider: string, model: string }) — got ${JSON.stringify(entry)}`)
31
+ continue
32
+ }
33
+ if (!names.has(entry.provider)) {
34
+ dropped.push(`entry "${entry.provider}:${entry.model}" references unknown provider "${entry.provider}" (available: ${[...names].join(", ") || "none"})`)
35
+ continue
36
+ }
37
+ keep.push(entry)
38
+ }
39
+ return { keep, dropped }
40
+ }
41
+
42
+ /** F-4:过滤警告——进程级一次性(loadConfig 同进程可多次调用:TUI 启动 + reloadConfig)。 */
43
+ let warnedConsultModels = false
44
+ export function warnConsultModelsFiltered(dropped, path) {
45
+ if (warnedConsultModels || dropped.length === 0) return
46
+ warnedConsultModels = true
47
+ console.warn(`[config] agent.consultModels: ${dropped.length} invalid entr${dropped.length === 1 ? "y ignored" : "ies ignored"} (filtered — startup continues; no crash):\n` +
48
+ dropped.map((d) => ` - ${d}`).join("\n") +
49
+ `\n Fix: clean agent.consultModels in ${path} or use /config → consult/escalate pool menu.`)
50
+ }
51
+
52
+ /** #861(AGENT-LOOP-SUBAGENT.md §6.7.1):subagent 模型覆盖加载期清洗——单源纯函数(核 loadConfig ∥ VSC 端壳 raw 读点同消费);非法形态不 throw(`subagentModel` 非法 ⇒ null、`subagentModels` 非对象 ⇒ {}、值非法 ⇒ 剔除该键)。 */
53
+ export function sanitizeSubagentModel(v) {
54
+ const ok = typeof v === "string" && v.trim()
55
+ const dropped = ok || v == null ? [] : [`agent.subagentModel must be a non-empty string ("provider:model" | model) — got ${Array.isArray(v) ? "array" : typeof v} — ignored (inherits the parent provider)`]
56
+ return { value: ok ? v : null, dropped }
57
+ }
58
+ export function sanitizeSubagentModels(v) {
59
+ if (v == null) return { value: {}, dropped: [] }
60
+ if (typeof v !== "object" || Array.isArray(v)) return { value: {}, dropped: [`agent.subagentModels must be an object mapping role → model override — got ${Array.isArray(v) ? "array" : typeof v} — ignored`] }
61
+ const value = {}, dropped = []
62
+ for (const [role, val] of Object.entries(v)) {
63
+ if (typeof val === "string" && val.trim()) { value[role] = val; continue }
64
+ dropped.push(`agent.subagentModels.${role} must be a non-empty string ("provider:model" | model) — got ${Array.isArray(val) ? "array" : typeof val} — dropped`)
65
+ }
66
+ return { value, dropped }
67
+ }
68
+
69
+ let warnedSubagentModels = false // #861:清洗警告——进程级一次性(同 consultModels 先例)
70
+ export function warnSubagentModelsFiltered(dropped, path) {
71
+ if (warnedSubagentModels || dropped.length === 0) return
72
+ warnedSubagentModels = true
73
+ console.warn(`[config] agent.subagentModel/subagentModels: ${dropped.length} invalid entr${dropped.length === 1 ? "y ignored" : "ies ignored"} (filtered — startup continues; no crash):\n` +
74
+ `${dropped.map((d) => ` - ${d}`).join("\n")}\n Fix: clean it in ${path} — "provider:model" | model.`)
75
+ }
76
+
77
+ /** Normalize proxy config to { uri, web } or undefined (uri/url both accepted; invalid types dropped) */
78
+ export function normalizeProxy(proxy) {
79
+ if (typeof proxy === "string") return proxy ? { uri: proxy, web: true } : undefined
80
+ if (!proxy || typeof proxy !== "object" || Array.isArray(proxy)) return undefined
81
+ const uri = proxy.uri || proxy.url || ""
82
+ if (typeof uri !== "string" || !uri) return undefined
83
+ return { uri, web: proxy.web !== false }
84
+ }
85
+
86
+ /** 团队登录段加载归一(B1 批——`docs/core/design/TEAM.md` §2.1 ∥ CONFIG.md §6.4):非对象 ⇒ null(软失败
87
+ * 不阻启动——沿 proxy 段先例);键级 = trim 非空字符串(非串 ∥ 空 ⇒ null);`member` 浅形 `{ username, name }`。
88
+ * **不入 `DEFAULTS`**(settings 工具面零涉——agent 不可写;`team.token` 经读面到达 = 敏感段词表遮罩)。 */
89
+ export function normalizeTeamSection(value) {
90
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return null
91
+ const str = (v) => (typeof v === "string" && v.trim() !== "" ? v.trim() : null)
92
+ const raw = value.member
93
+ const member = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? { username: str(raw.username), name: str(raw.name) } : null
94
+ return { server: str(value.server), member, label: str(value.label), token: str(value.token) }
95
+ }
96
+
97
+ /** Keep only { header: "string value" } pairs from a provider's headers field — anything
98
+ * else (null, arrays, nested objects) is dropped so it can never reach a fetch call.
99
+ * Authorization is built-in and cannot be overridden from headers (core.mjs spreads first). */
100
+ export function sanitizeProviderHeaders(p) {
101
+ if (p.headers == null || typeof p.headers !== "object" || Array.isArray(p.headers)) { delete p.headers; return p }
102
+ const clean = {}
103
+ for (const [k, v] of Object.entries(p.headers)) {
104
+ if (typeof v === "string" && k.toLowerCase() !== "authorization") clean[k] = v
105
+ }
106
+ if (Object.keys(clean).length > 0) p.headers = clean
107
+ else delete p.headers
108
+ return p
109
+ }