@thincoder/core 0.10.7 → 0.10.9

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 (149) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/advisor/compaction.mjs +1 -1
  3. package/advisor/run.mjs +1 -1
  4. package/agent/chat-call.mjs +40 -27
  5. package/agent/child-marks.mjs +1 -1
  6. package/agent/dispatch-gates.mjs +7 -4
  7. package/agent/dispatch-run.mjs +1 -0
  8. package/agent/family-tools.mjs +13 -8
  9. package/agent/helpers.mjs +12 -10
  10. package/agent/image-heal.mjs +65 -0
  11. package/agent/light-round.mjs +4 -4
  12. package/agent/live-beat.mjs +4 -4
  13. package/agent/post-turn.mjs +34 -0
  14. package/agent/record-results.mjs +9 -1
  15. package/agent/run-stages.mjs +45 -10
  16. package/agent/setup-reminders.mjs +14 -13
  17. package/agent/setup.mjs +72 -48
  18. package/agent/spawn-child.mjs +1 -1
  19. package/agent/subagent-event.mjs +1 -1
  20. package/agent/suspension.mjs +2 -1
  21. package/agent/timers.mjs +8 -8
  22. package/agent/turn-loop.mjs +7 -1
  23. package/agent-tools/advisor.mjs +1 -1
  24. package/agent-tools/async-discard.mjs +40 -5
  25. package/agent-tools/async-settle.mjs +3 -1
  26. package/agent-tools/audit-block.mjs +5 -3
  27. package/agent-tools/batch-gate.mjs +198 -0
  28. package/agent-tools/batch-lifecycle.mjs +216 -36
  29. package/agent-tools/batch-paths.mjs +20 -20
  30. package/agent-tools/batch-skeleton.mjs +87 -0
  31. package/agent-tools/batch.mjs +11 -3
  32. package/agent-tools/checkpoint.mjs +1 -1
  33. package/agent-tools/context.mjs +28 -5
  34. package/agent-tools/design-slots.mjs +20 -7
  35. package/agent-tools/doc-check.mjs +75 -0
  36. package/agent-tools/eng.mjs +5 -3
  37. package/agent-tools/manifest.mjs +18 -18
  38. package/agent-tools/parent-channel.mjs +3 -3
  39. package/agent-tools/review-facts.mjs +2 -2
  40. package/agent-tools/settings.mjs +1 -1
  41. package/agent-tools/spawn-gates.mjs +108 -47
  42. package/agent-tools/ssh-session.mjs +486 -0
  43. package/agent-tools/ssh-transport.mjs +324 -0
  44. package/agent-tools/ssh-vault.mjs +291 -0
  45. package/agent-tools/subagent-scheduler.mjs +6 -5
  46. package/agent-tools/subagent-spawn.mjs +18 -11
  47. package/agent-tools/subagent.mjs +17 -9
  48. package/agent-tools/task.mjs +1 -1
  49. package/agent-tools/timer.mjs +8 -2
  50. package/agent-tools/zero-write-watch.mjs +2 -2
  51. package/agent-tools.mjs +3 -2
  52. package/attachments.mjs +4 -4
  53. package/browser/cdp.mjs +2 -2
  54. package/browser/launch.mjs +50 -4
  55. package/browser/snapshot.mjs +1 -1
  56. package/config-defaults.mjs +92 -0
  57. package/config-io.mjs +7 -2
  58. package/config-normalize.mjs +109 -0
  59. package/config-watch.mjs +2 -2
  60. package/config.mjs +88 -190
  61. package/declaration.mjs +5 -1
  62. package/doc-check/anchors.mjs +142 -0
  63. package/doc-check/index.mjs +210 -0
  64. package/doc-check/version.mjs +84 -0
  65. package/doc-check/width.mjs +132 -0
  66. package/file-refs.mjs +6 -6
  67. package/generate-title.mjs +1 -1
  68. package/hooks.mjs +1 -1
  69. package/i18n.mjs +13 -1
  70. package/ledger-cmd.mjs +314 -62
  71. package/ledger-db.mjs +122 -31
  72. package/ledger-migrate.mjs +18 -12
  73. package/ledger-read.mjs +6 -7
  74. package/ledger-tools.mjs +138 -77
  75. package/ledger.mjs +34 -62
  76. package/log.mjs +17 -3
  77. package/manifest-discovery.mjs +21 -18
  78. package/manifest-schema.mjs +17 -14
  79. package/manifest.mjs +37 -22
  80. package/mcp/transport-stdio.mjs +1 -1
  81. package/mcp.mjs +2 -2
  82. package/memory/core.mjs +1 -1
  83. package/memory/scan.mjs +1 -1
  84. package/memory/sweep.mjs +1 -1
  85. package/model-specs.mjs +7 -7
  86. package/notify-policy.mjs +6 -6
  87. package/package.json +2 -1
  88. package/permission.mjs +21 -1
  89. package/prompts/advisor-design.md +53 -23
  90. package/prompts/advisor-round1.md +34 -14
  91. package/prompts/advisor-round2.md +32 -19
  92. package/prompts/advisor-round3.md +34 -21
  93. package/prompts/common.md +148 -79
  94. package/prompts/consult-base.md +14 -8
  95. package/prompts/discipline-engineering.md +70 -66
  96. package/prompts/discipline-normal.md +86 -77
  97. package/prompts/persona-coder.md +6 -6
  98. package/prompts/persona-eng-coder.md +19 -18
  99. package/prompts/persona-eng-designer.md +40 -31
  100. package/prompts/persona-engineering.md +124 -92
  101. package/prompts/persona-explore.md +2 -2
  102. package/prompts/persona-normal.md +20 -17
  103. package/prompts/persona-plan.md +7 -7
  104. package/prompts/subagent-base.md +4 -4
  105. package/provider/anthropic.mjs +4 -2
  106. package/provider/core.mjs +4 -2
  107. package/provider/errors.mjs +8 -0
  108. package/provider/google.mjs +8 -1
  109. package/provider/normalize.mjs +42 -12
  110. package/provider/responses-request.mjs +4 -3
  111. package/provider/responses.mjs +2 -2
  112. package/provider/wait-status.mjs +1 -1
  113. package/provider-flows.mjs +1 -1
  114. package/queued.mjs +2 -2
  115. package/rules.mjs +4 -4
  116. package/session-gc.mjs +1 -1
  117. package/session-lifecycle.mjs +7 -7
  118. package/session-slot-write.mjs +2 -2
  119. package/session-slots-manifest.mjs +1 -1
  120. package/session.mjs +1 -1
  121. package/shell-candidates.mjs +2 -2
  122. package/shell-identity.mjs +1 -1
  123. package/team.mjs +63 -5
  124. package/think-off.mjs +3 -3
  125. package/token-ttl.mjs +1 -1
  126. package/token-window.mjs +1 -1
  127. package/tool-docs/bash.md +1 -1
  128. package/tool-docs/batch.md +1 -1
  129. package/tool-docs/browser.md +1 -1
  130. package/tool-docs/doc_check.md +54 -0
  131. package/tool-docs/fetch.md +3 -2
  132. package/tool-docs/ledger.md +7 -5
  133. package/tool-docs/manifest.md +14 -2
  134. package/tool-docs/ssh.md +51 -0
  135. package/tool-docs/subagent.md +6 -9
  136. package/tool-docs/timer.md +2 -1
  137. package/tool-docs/websearch.md +3 -1
  138. package/tools/browser.mjs +1 -1
  139. package/tools/execute.mjs +1 -1
  140. package/tools/git-run.mjs +2 -2
  141. package/tools/git.mjs +8 -8
  142. package/tools/index.mjs +3 -0
  143. package/tools/ops.mjs +1 -1
  144. package/tools/process-tree.mjs +2 -2
  145. package/tools/ssh-askpass.mjs +35 -0
  146. package/tools/ssh.mjs +105 -0
  147. package/tools/web-deepseek.mjs +172 -0
  148. package/tools/web.mjs +29 -4
  149. package/vision-reader.mjs +4 -4
@@ -162,6 +162,93 @@ export function readSectionStatusWord(src, seg) {
162
162
  return null
163
163
  }
164
164
 
165
+ /**
166
+ * 「不适用」声明行形态单源(§4.18-A——**一行一档**):`> 不适用:§N —— <理由>`,§N ∈ {2,3,4}、
167
+ * 理由非空(`\S.*` = 至少一个非空白字符起头)。识别域 = §1 段体内(声明住 §1——D-BR30:工程模式
168
+ * 主 agent 的 append 域 = §1/§4/§6,写不进 §2/§3,而声明恰发生在 designer / 评审本轮不跑时 ⇒
169
+ * 段内声明形在需要的场合不可写)。声明 = 链门(`sectionContentState`)的合法满足面。
170
+ */
171
+ const INAPPLICABLE_RE = /^>\s*不适用[::]\s*§(\d+)\s*——\s*(\S.*)$/
172
+ /** 可声明的段号集(§5 段号豁免——免声明;§6 不可声明——收口段必须带验证内容)。 */
173
+ export const INAPPLICABLE_SEGMENTS = [2, 3, 4]
174
+ /** §1 声明行的识别前缀(形不合扫描的宽网——行首命中它而完整形不合 ⇒ 拒,不静默当作无声明)。 */
175
+ const INAPPLICABLE_PREFIX_RE = /^>\s*不适用/
176
+
177
+ /** §N 段体行集(段头缺失 ⇒ null——create 是骨架唯一权威入口;行切分 = /\r?\n/——EOL 形态无关)。 */
178
+ function sectionBodyLines(src, seg) {
179
+ const hdr = sectionHeaderRe(seg).exec(src)
180
+ if (!hdr) return null
181
+ const nextRe = /^## §\d/gm
182
+ nextRe.lastIndex = hdr.index + hdr[0].length
183
+ const next = nextRe.exec(src)
184
+ const body = src.slice(hdr.index + hdr[0].length, next ? next.index : src.length)
185
+ const nl = body.indexOf("\n") // 跳过段头行余文(`## §N …` 的标题部分)
186
+ return (nl >= 0 ? body.slice(nl + 1) : "").split(/\r?\n/)
187
+ }
188
+
189
+ /**
190
+ * 段内容判据**单源**(§4.18-A closeout chain gate 的判空口径;D-BR30 判据单源——**不许两套**;
191
+ * §4.13 的 §6 旧判据 `sectionSixHasContent` 改由本谓词承担——§6 为唯一缺段时拒串仍取旧函数)。
192
+ * 返回三态:`content`(段体至少一行非排除类)∥ `not-applicable`(§1 声明该段不适用且理由非空——
193
+ * 仅 §2/§3/§4)∥ `empty`。排除类(判空依据——「标题在、正文零行」判空):空行 ∥ 标题行
194
+ * (`^#{1,6}\s`)∥ `**状态行**:` 行 ∥ `<§N 模板占位:…>` 行 ∥ 「不适用」声明行。
195
+ * 口径收正注:旧 `sectionSixHasContent` 对「只有子标题(`### …`)」判非空;本谓词按 §4.18-A 判空
196
+ * (子标题属排除类标题行)——设计档 §4.18-A 明文收窄。
197
+ * @param {string} src — 档全文
198
+ * @param {number|string} seg — 段号(§1–§6;§1 恒 `content`——主 agent 自述段不设前置)
199
+ * @returns {"content"|"not-applicable"|"empty"} 段内容三态
200
+ */
201
+ export function sectionContentState(src, seg) {
202
+ const n = Number(seg)
203
+ if (n === 1) return "content"
204
+ const lines = sectionBodyLines(src, n)
205
+ if (lines === null) return "empty" // 段头缺失 = 空(骨架由 create 一次预写;缺失即无内容)
206
+ if (lines.some((l) => NONEMPTY_RE.test(l) && !EXCLUDED_LINE_RE.test(l))) return "content"
207
+ // 段体无实质内容 ⇒ 查 §1 声明(仅 §2/§3/§4 可声明;理由非空——INAPPLICABLE_RE 的 (\S.*) 已保证)
208
+ if (INAPPLICABLE_SEGMENTS.includes(n)) {
209
+ for (const l of sectionBodyLines(src, 1) ?? []) {
210
+ const m = INAPPLICABLE_RE.exec(l)
211
+ if (m && Number(m[1]) === n) return "not-applicable"
212
+ }
213
+ }
214
+ return "empty"
215
+ }
216
+
217
+ /** 非空白行谓词(空行排除)。 */
218
+ const NONEMPTY_RE = /\S/
219
+ /** 排除类行判据:标题行(`^#{1,6}\s`)∥ `**状态行**:` 行 ∥ `<§N 模板占位:…>` 行 ∥ 「不适用」声明行
220
+ * (空行由 NONEMPTY_RE 单独排除——四类合一次判定)。 */
221
+ const EXCLUDED_LINE_RE = /^#{1,6}\s|^\s*\*\*状态行\*\*[::]|^<§\d+ 模板占位[::]|^>\s*不适用/
222
+
223
+ /**
224
+ * 声明形不合拒绝句(§4.18-A J3——形不合 ⇒ 拒,**不静默当作无声明**):行首命中
225
+ * `^>\s*不适用` 却不满足完整形(缺 `——` ∥ 缺理由 ∥ § 号 ∉ {2,3,4})。
226
+ * 逐字文本 = `TOOLS.md` §6.24 J3。
227
+ * @param {string} line — 违规行原文
228
+ * @returns {string} 完整拒绝句(调用方 throw new Error(...))
229
+ */
230
+ export function inapplicableDeclarationError(line) {
231
+ return "batch: 「不适用」声明形不合(malformed inapplicability declaration)— one line per section: " +
232
+ "`> 不适用:§N —— <reason>` with a non-empty reason, §N ∈ {2,3,4} (§5 needs no declaration; §6 is never declarable). " +
233
+ `Offending line: ${line}. Nothing was written.`
234
+ }
235
+
236
+ /**
237
+ * §1 声明行机械扫描(链门前置判据——形不合 ⇒ 拒,不静默):识别域 = §1 段体。行首命中
238
+ * `^>\s*不适用` 而完整形不合(INAPPLICABLE_RE 不中 ∥ § 号 ∉ {2,3,4})⇒ 返回违规行原文;
239
+ * § 号 ∈ {2,3,4} 且理由非空 ⇒ 合法。无违规 ⇒ null。
240
+ * @param {string} src — 档全文
241
+ * @returns {string|null} 首个形不合声明行原文 / null
242
+ */
243
+ export function malformedInapplicableLine(src) {
244
+ for (const l of sectionBodyLines(src, 1) ?? []) {
245
+ if (!INAPPLICABLE_PREFIX_RE.test(l)) continue
246
+ const m = INAPPLICABLE_RE.exec(l)
247
+ if (!m || !INAPPLICABLE_SEGMENTS.includes(Number(m[1]))) return l
248
+ }
249
+ return null
250
+ }
251
+
165
252
  /**
166
253
  * §N 段内「`**状态行**:` 行存在性」判据(#422③——close 对无状态行档自建机读位的**采集面**:
167
254
  * 与 readBatchStatusLine 的「值可解析」正交——无行 ⇒ 可自建;有行而值不可解析 ⇒ 仍 fail-closed)。
@@ -32,6 +32,7 @@ import { readFileSync, writeFileSync } from "node:fs"
32
32
  import { DESC } from "../tools/shared.mjs"
33
33
  import { SEGMENT_BY_ROLE, SEGMENT_BY_ROLE_NORMAL, DEPTH0_SEGMENTS, readBatchStatusLine, sectionHeaderRe, findPlaceholderResidue, placeholderResidueError } from "./batch-skeleton.mjs"
34
34
  import { batchDocBases, resolveBatchDocPath } from "./batch-paths.mjs"
35
+ import { gateReceiptLine, gateRefusalMessage, runBatchGate } from "./batch-gate.mjs"
35
36
  import { closeBatchRecord, createBatchRecord, findInFlightBatch, statusBatchRecord } from "./batch-lifecycle.mjs"
36
37
 
37
38
  export { SEGMENT_BY_ROLE, batchDocBases, resolveBatchDocPath }
@@ -154,7 +155,8 @@ function insertIntoSection(src, seg, text) {
154
155
  * eng-coder → §5 · coder(普通模式分支增表)→ §5(迁移错误面,"batch_segment:" 前缀**逐字保持**);
155
156
  * - 目标定位(D-BR21):depth-0 path 可选(缺省 = findInFlightBatch 唯一在飞批);子代理/评审
156
157
  * 传 path ⇒ 拒,目标 = spawn/实例绑定;
157
- * - 其余(gate / text 校验 / 剥凭证 / 骨架保护 / **死占位机检(F11-C)** / 插入 / 记账 / 回执)逐字保持原语义。
158
+ * - 其余(gate / text 校验 / 剥凭证 / 骨架保护 / **死占位机检(F11-C)** / **文档机检门(§3.20 结构兜底 ①——
159
+ * 子代理写 §2 / §5:REFUSED ⇒ 拒写,ERROR ⇒ 写 + 回执明示)** / 插入 / 记账 / 回执)逐字保持原语义。
158
160
  */
159
161
  function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
160
162
  const agent = ctx?.agent ?? {}
@@ -228,11 +230,17 @@ function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
228
230
  // (段体引用豁免——判据可辨性收正,`TOOLS.md` §6.20;骨架枚举单源 = skeleton 档 TEMPLATE_PLACEHOLDERS)。
229
231
  const residues = findPlaceholderResidue(src)
230
232
  if (residues.length) throw new Error(placeholderResidueError(residues))
233
+ // 文档机检门(§3.20「结构兜底」①——门位 = 既有拒因全部之后、insertIntoSection / 写盘之前):
234
+ // 只挂子代理写 §2 / §5(depth-0 与评审不检);REFUSED ⇒ throw;PASS ⇒ 照写;ERROR / 门自身异常 ⇒ 写 + 回执明示。
235
+ const gate = !depth0 && !review && (n === 2 || n === 5)
236
+ ? runBatchGate({ abs, cwd, agent, scope: "writer" })
237
+ : null
238
+ if (gate?.state === "refused") throw new Error(gateRefusalMessage(gate, "append"))
231
239
  const { written, roundN } = insertIntoSection(src, n, body)
232
240
  writeFileSync(abs, written)
233
241
  // #84:记账面按端注入(缺省 no-op = CLI 语义零行为变——端装配经 configureBatchSegment 覆盖)。
234
242
  onWritten?.(agent, abs)
235
- return `batch_segment: appended ${body.length} characters to §${n} of the batch record${roundN ? ` (### 轮次 ${roundN}(评审子代理))` : ""}.`
243
+ return `batch_segment: appended ${body.length} characters to §${n} of the batch record${roundN ? ` (### 轮次 ${roundN}(评审子代理))` : ""}.${gateReceiptLine(gate)}`
236
244
  }
237
245
 
238
246
  /**
@@ -271,7 +279,7 @@ export function batchTool(batchDoc = null, { review = false } = {}) {
271
279
  },
272
280
  note: {
273
281
  type: "string",
274
- description: "status only — prose that lands as a parenthetical on the status line (`**状态行**:进行中(note)`). Single line; must carry no STATUS_WORDS keyword (those go in `value`) and no skeleton placeholders. Omit for a plain value.",
282
+ description: "status only — prose that lands as a parenthetical on the status line (`**状态行**:进行中(note)`). Single line; must carry no STATUS_WORDS keyword (those go in `value`) and no skeleton placeholders. Omit for a plain value. On the reopen path (a frozen record, §1 → 进行中) `note` is REQUIRED and lands verbatim in the §1 reopen record instead of the status-line parenthetical (it may name the frozen keyword there).",
275
283
  },
276
284
  topic: {
277
285
  type: "string",
@@ -72,7 +72,7 @@ export function registerTurnCapCheckpoint(child, carrier, { turn = 0, signal } =
72
72
  const onAbort = () => finish(false)
73
73
  rec.resolve = finish
74
74
  signal?.addEventListener?.("abort", onAbort, { once: true })
75
- // 注册后复检(`permission-gate.mjs:80-81` 同款序):监听只认注册后的状态转移,
75
+ // 注册后复检(`permission-gate.mjs` 的 `permissionGate` 同款序):监听只认注册后的状态转移,
76
76
  // 窗口内已中止 ⇒ 监听不落地,此处兜住。
77
77
  if (signal?.aborted) { abortedNow = true; onAbort() }
78
78
  })
@@ -3,9 +3,10 @@
3
3
  * 单一工具三操作(用户定形 F-CC5):`stats`(感知 F-CC1)· `prune`(噪声清理 F-CC3)· `compact`(主动压缩 F-CC2,携 focus + 自动附任务/目标 + 可回查锚——裁令 A/D)。
4
4
  * 描述外置(tool-docs/context.md——#15 统一面:文本单点 = tool-docs,DESC 加载);装配面 = 登记册 `agent-tools.mjs`(#83)+ `agent/family-tools.mjs` 的 depth-0 段(`depthOnly`)——两端自动同表;**depth>0 不给**(§6.16.6)。
5
5
  * 本档同时是**注记文案 / 轻推行 / 回执文案的单源**(核与 VSC 的 run-stages 消费点经动态 import 取用——零新增静态边)。
6
- * 机械面:`stats` 不新增第三口径(阈值取本回合判定值;百分比走 `historyPercent` 与状态行同式);`prune` = 合格集(`token-window.mjs` 单源)+「原位换内容」应用面(`context.mjs`);`compact` **当次零执行**(只登记 `agent._pendingCompact`,由下一安全点消费——§6.16.2)。
6
+ * 机械面:`stats` 不新增第三口径(首行主口径 = 窗口占用 total / window,压缩线以具名数值 + 距线呈现;第 4 行走 `historyPercent` 与状态行同式);`prune` = 合格集(`token-window.mjs` 单源)+「原位换内容」应用面(`context.mjs`);`compact` **当次零执行**(只登记 `agent._pendingCompact`,由下一安全点消费——§6.16.2)。
7
7
  */
8
8
  import { contextUsage, estimateTokens, collectStaleToolOutputs, historyPercent, PRUNE_MIN_TOKENS } from "../token-window.mjs"
9
+ import { requestBudget } from "../config.mjs"
9
10
  import { pruneStaleToolOutputs, COMPRESS_FAILURE_LIMIT } from "../context.mjs"
10
11
  import { slotPath } from "../session-slots.mjs"
11
12
  // #15 描述外置:描述文本单点 = tool-docs/context.md(DESC 单解析面,缺档抛错语义不变)
@@ -72,16 +73,38 @@ function statsReport(agent) {
72
73
  toolCount++
73
74
  toolOut += estimateTokens([m])
74
75
  }
75
- const pct = Math.round((usage.total / usage.threshold) * 100)
76
+ const pct = Math.round((usage.total / usage.window) * 100) // 主口径 = 窗口占用(§6.16.4 首行定稿 / D-CC29)
76
77
  const toGo = Math.max(0, usage.threshold - usage.total) // 已越阈值 ⇒ 0(自动压缩将在下一安全点接手)
77
78
  const info = agent?._lastCompressInfo
78
79
  const compaction = !info ? "none"
79
80
  : info.mode === "summary" ? `last summary freed ${info.tokensFreed} tokens`
80
81
  : "last fallback truncation"
82
+ // Cache line (batch 2026-10-18-context-cache-visibility): the provider reports
83
+ // prompt_cache_hit/miss per response; the core now tallies them so the question
84
+ // "is the prefix cache actually helping?" is answered by a reading, not a guess.
85
+ const cacheHit = agent?._cacheHitTokens ?? 0
86
+ const cacheMiss = agent?._cacheMissTokens ?? 0
87
+ const cacheTurns = agent?._usageTurns ?? 0
88
+ const cacheTotal = cacheHit + cacheMiss
89
+ const cacheLine = cacheTurns === 0
90
+ ? "Cache: no provider usage readings yet (hit/miss arrive with the first response)"
91
+ : `Cache: hit ${cacheHit} · miss ${cacheMiss} tokens (${Math.round((cacheHit / cacheTotal) * 100)}% hit) across ${cacheTurns} response(s)`
81
92
  return [
82
- `Context ≈${usage.total} tokens vs compaction threshold ${usage.threshold} (${pct}% — ${toGo} to go); window ${usage.window}`,
93
+ `Context ≈${usage.total} tokens (${pct}% of ${usage.window} window); compaction line ${usage.threshold} (${toGo} to the line)`,
94
+ // §6.16.4 字面单源(该行为唯一字面,逐字;预算面同源 requestBudget——不自算第二套):
95
+ // window {window} ({spec|provider-override}) · max_tokens {maxTokens|not sent} ({explicit|spec|absent})
96
+ // · prompt {total} · to compaction line {threshold − total} · to window limit {limit − total}
97
+ // 两距离带符号(负 = 已越);maxTokens 未发写 not sent、来源写 absent(该档守卫不覆盖——§6.17 射程)。
98
+ (() => {
99
+ const budget = requestBudget(agent?.provider)
100
+ const mt = budget.maxTokens === undefined ? "not sent" : String(budget.maxTokens)
101
+ return `Request budget: window ${budget.window} (${budget.windowSource}) · max_tokens ${mt} (${budget.maxTokensSource})`
102
+ + ` · prompt ${usage.total} · to compaction line ${usage.threshold - usage.total}`
103
+ + ` · to window limit ${budget.limit - usage.total}`
104
+ })(),
83
105
  `Segments ≈: system ${usage.system} · tools ${usage.tools} · history ${usage.history} (basis: ${usage.basis})`,
84
106
  `History: tool outputs ${toolOut} tokens in ${toolCount} msgs · prunable stale ${stale.tokens} tokens in ${stale.indexes.length} msgs · protected tail ${stale.tailCount} msgs`,
107
+ cacheLine,
85
108
  `Status line: context ${historyPercent(agent?.history ?? [], agent?.provider)}% (${usage.history} history estimate / ${usage.window} window)`,
86
109
  `Compaction so far: ${compaction} · failures ${agent?._compressFailures ?? 0}/${COMPRESS_FAILURE_LIMIT}`,
87
110
  ].join("\n")
@@ -100,8 +123,8 @@ function pruneReceipt(agent) {
100
123
 
101
124
  /**
102
125
  * 可回查锚(父侧裁令 A · §6.16.7 两分支逐字):回执尾段给出「压掉的可一步取回」的坐标与调用形。
103
- * 取回面 = `read_history` 既有 `path=`(**不新建通道**);未绑定槽(会话首次保存前 / VSC 端面板槽
104
- * 不在 agent 上)⇒ `cwd:` 发现面(等效取回,已登记的端差)。
126
+ * 取回面 = `read_history` 既有 `path=`(**不新建通道**);未绑定槽(会话首次保存前——CLI;VSC 恒绑定、不落本支——§6.16.7)
127
+ * ⇒ `cwd:` 发现面(等效取回——`cwd` 缺省时字面 `cwd:undefined` 的降级形,工具内不抛)。
105
128
  */
106
129
  function retrievabilityAnchor(agent) {
107
130
  const slot = agent?._slot
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * design-slots.mjs — design-token 槽位清点 + 消费核(2026-10-07 批 ledger-tool · 台账 #927 并入)。
3
- * 设计单源 = `docs/core/design/DESIGN-TOKEN-SETTLEMENT.md` §10(F-SL1 清点面 / F-SL2 批量消费)。
3
+ * 设计单源 = `docs/core/design/DESIGN-TOKEN-SETTLEMENT.md` §9(F-SL1 清点面 / F-SL2 批量消费)。
4
4
  *
5
5
  * 面一 `executeDesignSlotsAction`(`subagent` action `design-slots`——只读):
6
6
  * 数据源 = 槽文件权威表(`readEngTokensFromSlot`——无 TTL 过滤原样)∪ 内存 Map(并集去重,
7
7
  * 冲突以槽为准——D-S3 同源);**零写**(不 reconcile 回写、不落盘)。列表面 = 逐槽一行
8
- * (designId ∥ live/expired ∥ ageDays ∥ expiresAt ∥ 批;龄序 = 最老在前)+ 计数尾行;
9
- * 「批」= `_advisorRuns` 按 designId 命中 run 的 `docSetKey` 中 `batches/` 段档取 basename
8
+ * (designId ∥ live/expired ∥ ageDays ∥ expiresAt ∥ 批 ∥ designToken(末位,恒携);龄序 = 最老在前)
9
+ * + 计数尾行;「批」= `_advisorRuns` 按 designId 命中 run 的 `docSetKey` 中 `batches/` 段档取 basename
10
10
  * (KD-SL2——零新存储;不可得 ⇒ `—`)。
11
+ * `designId` 过滤单槽(trim 精确匹配;空串视同缺省;未知 ⇒ 拒 + 列在场 id);缺省 = 现行清点面逐字零变;零写同前。
11
12
  * 面二 `executeConsumeDesignAction`(`subagent` action `consume-design` 消费核——自
12
13
  * `subagent-spawn.mjs` 迁入):选择器恰一(`designId` ∥ `designIds` ∥ `expired` ∥
13
14
  * `olderThanDays`——KD-SL3 fail-closed 不猜);目标集 = 并集视图按选择器解析 → 逐枚移出
@@ -84,6 +85,8 @@ const byOldest = (a, b) => (b.ageDays ?? -Infinity) - (a.ageDays ?? -Infinity)
84
85
  * `subagent` action `design-slots`(F-SL1——只读清点面):工程模式父侧限定(与 consume-design
85
86
  * 同门)。输出 = 行文本(逐槽行 + 计数尾行);字段 / 序 / 降级标记 = 契约,行内措辞 = 实现面。
86
87
  * 分类 = 只读(planMode 放行 / 免审批 / digest 放行——谓词在 `agent/dispatch-gates.mjs`)。
88
+ * `designId` 过滤单槽(trim 精确匹配;空串视同缺省;未知 ⇒ 拒 + 列在场 id);
89
+ * 缺省 = 全量清点(不改变行契约——令牌列恒携);零写(不 reconcile / 不落盘)。
87
90
  */
88
91
  export function executeDesignSlotsAction(args, ctx) {
89
92
  const parent = ctx?.agent
@@ -94,10 +97,20 @@ export function executeDesignSlotsAction(args, ctx) {
94
97
  const ttlMs = effectiveTokenTtlMs(parent)
95
98
  const entries = [...unionSlots(parent)].map(([id, t]) => slotFacts(parent, id, t, now, ttlMs))
96
99
  entries.sort(byOldest)
97
- const lines = entries.map((e) =>
98
- `- designId=${e.designId} status=${e.expired ? "expired" : "live"} ageDays=${e.ageDays === null ? "—" : e.ageDays.toFixed(1)} expiresAt=${e.expiresAt === null ? "—" : new Date(e.expiresAt).toISOString()} batch=${e.batch}`)
99
- const live = entries.filter((e) => !e.expired).length
100
- lines.push(`design-slots: ${entries.length} slot(s) — ${live} live / ${entries.length - live} expired`)
100
+ // 参数面(fail-closed——不猜):`designId` trim 后精确匹配、空串视同缺省;未知 ⇒ 拒 + 列在场 id。
101
+ const rawId = args?.designId
102
+ const designId = rawId === undefined || rawId === null ? "" : String(rawId).trim()
103
+ let selected = entries
104
+ if (designId) {
105
+ selected = entries.filter((e) => e.designId === designId)
106
+ if (selected.length === 0) {
107
+ const held = entries.length > 0 ? entries.map((e) => e.designId).join(", ") : "(none)"
108
+ throw new Error(`design-slots: unknown designId ${JSON.stringify(designId)} — no slot holds this id (held design ids: ${held}).`)
109
+ }
110
+ }
111
+ const lines = selected.map((e) => `- designId=${e.designId} status=${e.expired ? "expired" : "live"} ageDays=${e.ageDays === null ? "—" : e.ageDays.toFixed(1)} expiresAt=${e.expiresAt === null ? "—" : new Date(e.expiresAt).toISOString()} batch=${e.batch} designToken=${e.expiresAt === null ? "—" : e.token}`)
112
+ const live = selected.filter((e) => !e.expired).length
113
+ lines.push(`design-slots: ${selected.length} slot(s) — ${live} live / ${selected.length - live} expired${designId ? " (filtered by designId)" : ""}`)
101
114
  return lines.join("\n")
102
115
  }
103
116
 
@@ -0,0 +1,75 @@
1
+ /**
2
+ * agent-tools/doc-check.mjs — `doc_check` 工具:文档纪律机检(产品内建 · 两种问法)。
3
+ *
4
+ * 设计 = `docs/core/design/DOC-DISCIPLINE.md` §3.20 条目 S(判据 = 锚 ∥ 行宽 · 声明面驱动)。
5
+ * 工具形:**无动作枚举**——`mode` = 同一条判据的两种问法(`report` = 全域报告态 ∥ `gate` = 交付时点
6
+ * 的范围化裁定);**只读**(零副作用 · 免审批 · 可并行);参数 `target` / `mode` / `docs`
7
+ * (`target` 指向多项目工作区 ⇒ 报明候选并要求指名,同 `manifest` 工具面)。
8
+ * 判据参数读目标项目的 `PROJECT-MANIFEST.json` `checkConfig`——不硬编码任何本仓路径(换项目开箱可用)。
9
+ * `report` 非门禁(硬):输出不得被读作拒执行的理由(用户令 > 机检——§2 总则族);
10
+ * `gate` 的强制面住交付协议 + 批次档 §6 前置,工具本体同样不阻断任何动作。
11
+ * gate 前置(fail-closed)= 引擎版本一致:本会话进程持启动时加载的 ESM 引擎,引擎档晚于启动 ⇒ ERROR
12
+ * (判据单源 = `doc-check/version.mjs`——与 `batch-gate.mjs` 共用一份清单,不各持一份)。
13
+ * 描述外置:模型可见文本单点 = `tool-docs/doc_check.md`(`DESC` 单一解析面)。
14
+ * 装配段 = `agent/family-tools.mjs` 三处(depth-0 / eng-coder / eng-designer)——写文档 / 设计档的角色面。
15
+ */
16
+ import { resolve } from "node:path"
17
+ import { projectRootView } from "../manifest.mjs"
18
+ import { runDocCheck } from "../doc-check/index.mjs"
19
+ import { staleEngineFiles, gateEngineError, withEngineNote } from "../doc-check/version.mjs"
20
+ import { DESC } from "../tools/shared.mjs"
21
+
22
+ // 版本函数单源 = `doc-check/version.mjs`(两消费面:本档 gate 分支 ∥ `batch-gate.mjs` 两机械点)——
23
+ // 本档只转口,导出面不变(原五档清单已随之六档:`version.mjs` 自身入清单)。
24
+ export { staleEngineFiles, gateEngineError }
25
+
26
+ export const docCheckTool = {
27
+ name: "doc_check",
28
+ description: DESC("doc_check"),
29
+ parameters: {
30
+ type: "object",
31
+ properties: {
32
+ target: { type: "string", description: "Explicit project root to check — defaults to the session anchor; relative paths resolve against it. A workspace target (several projects below it) is reported with the full candidate list — pass the project root you mean." },
33
+ mode: { type: "string", enum: ["report", "gate"], description: "How to ask the same two-criteria check. \"report\" (default): the whole declared domain, non-gating report. \"gate\": the delivery gate — only the documents this round touched, whole-file judged, first line a machine-readable verdict (DOC-CHECK GATE: PASS / REFUSED / ERROR)." },
34
+ docs: { type: "array", items: { type: "string" }, description: "Gate only: the documents this round touched (paths relative to the project root, or absolute). Omitted in gate mode ⇒ the documents this session wrote. Passing docs with mode \"report\" is refused, not silently ignored." },
35
+ },
36
+ },
37
+ readonly: true,
38
+ execute(args, ctx) {
39
+ const anchor = ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
40
+ const target = args?.target ? resolve(anchor, String(args.target)) : resolve(anchor)
41
+ const mode = args?.mode === undefined ? "report" : String(args.mode)
42
+ if (mode !== "report" && mode !== "gate") return `doc_check: unknown mode "${mode}" — use "report" (default: the whole declared domain) or "gate" (this round's documents).`
43
+ const explicit = Array.isArray(args?.docs) ? args.docs.map(String) : null
44
+ if (args?.docs !== undefined && !Array.isArray(args.docs)) {
45
+ return `doc_check: \`docs\` must be an array of paths (got ${typeof args.docs}) — a bare string is refused rather than read as a whole-domain request; pass ["<path>"].`
46
+ }
47
+ if (mode === "report" && explicit) {
48
+ return "doc_check: `docs` is only valid with mode:\"gate\" — report mode always checks the whole declared domain; pass mode:\"gate\" to scope the check to the documents this round touched."
49
+ }
50
+ const view = projectRootView(target)
51
+ if (view.state === "workspace") {
52
+ if (mode === "gate") {
53
+ return [
54
+ "DOC-CHECK GATE: ERROR(多项目工作区:请指名 target)",
55
+ `候选:${view.candidates.join(" ∥ ")}`,
56
+ "处置:指名项目根(`target`)后复跑",
57
+ ].join("\n")
58
+ }
59
+ return `doc_check: ${target} is a workspace holding ${view.candidates.length} projects — pass the project root explicitly via target:\n` +
60
+ view.candidates.map((c) => `- ${c}`).join("\n")
61
+ }
62
+ const root = view.state === "ok" ? view.root : target
63
+ if (mode === "gate") {
64
+ // 引擎版本校验先于引擎(A-DD30 ①):stale 非空 ⇒ 直接 ERROR 串,不进引擎
65
+ const version = staleEngineFiles()
66
+ if (version.stale.length) return gateEngineError(version)
67
+ // 会话候选先取 `.md`(触达记账含代码档——代码档不是文档,不该进范围行);域求交归引擎单源
68
+ const docs = explicit ?? (Array.isArray(ctx?.agent?._touchedFiles) ? ctx.agent._touchedFiles : [])
69
+ .filter((p) => typeof p === "string" && p.toLowerCase().endsWith(".md"))
70
+ return runDocCheck(root, { mode: "gate", docs, docsSource: explicit ? "explicit" : "session" }).report
71
+ }
72
+ // 报告态读数标注(A-DD30 ①):引擎陈旧 ⇒ 首行前一行注记;无陈旧 ⇒ 报面逐字同改前
73
+ return withEngineNote(runDocCheck(root).report, staleEngineFiles())
74
+ },
75
+ }
@@ -12,6 +12,7 @@
12
12
  * (subagent-spawn.mjs). See docs/design/ENG-TOKEN-BINDING-TUNING.md §5/§5.1 (F-R16).
13
13
  * #41(2026-09-18):enter 分支改「先判后翻」——判据 = 入口决策树单源
14
14
  * `resolveEngineeringManifest`(docs/core/design/MANIFEST.md §2.8 F2 / KD-M1-20/21);
15
+ * **可拒面 = 仅「档非法」一格**(工作区 `workspace` 放行——零附着,模式照开);
15
16
  * 拒翻 ⇒ 返回原因串 + **零副作用**(模式保持 OFF)。OFF 方向与幂等 enter 零改。
16
17
  */
17
18
  import { ENG_ON_REMINDER, ENG_OFF_REMINDER } from "../agent.mjs"
@@ -74,15 +75,16 @@ export const engTool = {
74
75
  return "Engineering mode already active. Existing design tokens stay valid."
75
76
  }
76
77
  // #41 先判后翻(MANIFEST.md §2.8 F2 / KD-M1-21):判据 = 入口决策树**单源**
77
- // (resolveEngineeringManifest——KD-M1-20),判据通过才写态。拒翻 ⇒ 返回原因串 +
78
+ // (resolveEngineeringManifest——KD-M1-20),判据通过才写态。**可拒面 = 仅「档非法」格**
79
+ // (工作区 `workspace` ⇒ 放行——**零附着**:无项目档可附,模式照开);拒翻 ⇒ 返回原因串 +
78
80
  // **零副作用**(不清 token / 不重置 _advisorRuns / 不入列 ENG_ON_REMINDER /
79
81
  // 不动 _lastEngState / 不触镜像缝);准 ⇒ 缺档格就地建档(writer:'main')+
80
82
  // agent.manifest ← 结果(翻转面唯一赋值点——相位行当回合起活,§2.8 F2 放行行)。
81
83
  const r = resolveEngineeringManifest(ctx.agent.cwd ?? ctx.cwd ?? process.cwd(), { writer: "main" })
82
- if (!r.ok) {
84
+ if (!r.ok && r.code !== "workspace") {
83
85
  return `Error: cannot enter engineering mode — ${r.message} (mode unchanged)`
84
86
  }
85
- ctx.agent.manifest = r.manifest
87
+ ctx.agent.manifest = r.ok ? r.manifest : null // 工作区放行 ⇒ 零附着(显式置 null——§2.8 F2 放行行)
86
88
  ctx.agent.config.agent.engineering = true
87
89
  // FR31 ③ / KD10(翻转点①):工程模式 ⇒ planMode 恒 false——中途开模式时清残留
88
90
  //(planMode 位 + reminder 计数 + 未注入的 plan 提示语),半状态(工程纪律 × plan 只读)不产生。
@@ -3,7 +3,7 @@
3
3
  * agent 侧读写出口(2026-10-08 · 台账 #1098;权威设计 = `docs/core/design/MANIFEST.md` §2.10)。
4
4
  *
5
5
  * 三动作(KD-M1-38):
6
- * - `read` ⇒ `projectView(target)` 五态报明(稳定锚 `state=ok|missing|invalid|ambiguous|no-project`);
6
+ * - `read` ⇒ `projectView(target)` 五态报明(稳定锚 `state=ok|missing|invalid|workspace|no-project`);
7
7
  * `ok` 态再 `readManifest(view.root)` 取细节面(`projectView` 面不含 `missingKeys` /
8
8
  * `unknownKeys`)——**不抛**(缺档报明,非拒)。
9
9
  * - `init` ⇒ 缺档建默认档(`initManifest`——内容 = `DEFAULT_MANIFEST`);**已带档 ⇒ 拒**
@@ -15,12 +15,12 @@
15
15
  * (`version` / `phase` / `promptsLanding` / `codePaths` 整键)+ `nestedKeys` 子键
16
16
  * (`docRoot.*` / `checkConfig.*` / `index.*`(含 `publicRepos`)/ `advisor.*`);**四族整键写
17
17
  * (传整个对象)⇒ 拒**;白名单外 ⇒ 拒(拼写保护——未知键写盘 = 静默死键,KD-M1-36 问题本体)。
18
- * `value` 解析 = 同款口径 `settings.mjs:216-228`(先 `JSON.parse`,失败取字面串;`"null"` ⇒ `null`
18
+ * `value` 解析 = 同款口径 `settings.mjs` 的 `parseValue`(先 `JSON.parse`,失败取字面串;`"null"` ⇒ `null`
19
19
  * ——`docRoot.*` 的「本面无」,KD-M1-37)。
20
20
  *
21
21
  * 目标面(KD-M1-39——判据复用零新解析器):`read` = `projectView`;`init` / `write` =
22
- * `projectRootView`(歧义 ⇒ 拒 + 候选全列——不猜);`resolveProjectRoot` 对歧义塌缩 `null` 再回落 cwd
23
- * ⇒ 写 / 建两动作**不得**直用。
22
+ * `projectRootView`(工作区 ⇒ 拒 + 下辖项目全列 + 显式 `target` 指引);`resolveProjectRoot` 对
23
+ * 工作区塌缩 `null` 再回落 cwd ⇒ 写 / 建两动作**不得**直用。
24
24
  *
25
25
  * 写路径复用(零新写路径):落盘恒经 `writeManifest` / `initManifest`——`writer` 恒显式 `'main'`
26
26
  * (缺省 `'subagent'` = 写门 fail-closed 缺省,KD-M1-3 / AC-M1-5 零改)。
@@ -43,7 +43,7 @@ function refuse(action, reason) {
43
43
  return new Error(`manifest ${action}: refused — ${reason}`)
44
44
  }
45
45
 
46
- /** `target` 解析(先例 `eng.mjs:81` 缺省会话锚 ∥ `git.mjs:32-35` 相对形归一):缺省 = 会话锚
46
+ /** `target` 解析(先例 `eng.mjs` 的 `resolveEngineeringManifest` 缺省会话锚 ∥ `git.mjs` 的 `resolveBaseDir` 相对形归一):缺省 = 会话锚
47
47
  * (`ctx.agent.cwd ?? ctx.cwd ?? process.cwd()`);显式相对形按会话锚归一(绝对形原样)。 */
48
48
  function resolveTarget(args, ctx) {
49
49
  const base = ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
@@ -65,7 +65,7 @@ function isWritableKey(key) {
65
65
  return false
66
66
  }
67
67
 
68
- /** `value` 解析(同款口径 = `settings.mjs:216-228`):先 `JSON.parse`(`"null"` ⇒ `null`;数值 /
68
+ /** `value` 解析(同款口径 = `settings.mjs` 的 `parseValue`):先 `JSON.parse`(`"null"` ⇒ `null`;数值 /
69
69
  * 布尔 / 数组皆 JSON 形),失败取字面串(裸串如 `docs/design`)。 */
70
70
  function parseValue(raw) {
71
71
  const s = String(raw)
@@ -83,14 +83,14 @@ function setKeyPath(obj, path, value) {
83
83
  cur[segs[segs.length - 1]] = value
84
84
  }
85
85
 
86
- /** 歧义拒文案(候选全列——不猜;init / write 共用):多候选 = 显式指定 target,机制不代选。 */
87
- function ambiguousReason(target, candidates) {
88
- return `ambiguous target ${target} — ${candidates.length} candidate projects (the mechanism never picks one); pass target explicitly:\n` +
86
+ /** 工作区拒文案(下辖项目全列 + 显式 `target` 下一步——init / write 共用;事实行,非二义框定)。 */
87
+ function workspaceReason(target, candidates) {
88
+ return `${target} is a workspace holding ${candidates.length} projects — pass the project root explicitly via target:\n` +
89
89
  candidates.map((c) => `- ${c}`).join("\n")
90
90
  }
91
91
 
92
92
  /** read 动作(五态报明——**不抛**):`state=` 稳定锚 + 各态附行(ok 细节面 / missing 引导 /
93
- * ambiguous 候选全列 / invalid 错误行)。 */
93
+ * workspace 下辖项目全列 / invalid 错误行)。 */
94
94
  function readAction(target) {
95
95
  const view = projectView(target)
96
96
  const lines = [`state=${view.state}`]
@@ -105,10 +105,10 @@ function readAction(target) {
105
105
  lines.push(`hint: run manifest action=init to create the default manifest here`)
106
106
  } else if (view.state === "no-project") {
107
107
  lines.push(`hint: no project on the ancestor chain and none below — manifest action=init can create one at the target`)
108
- } else if (view.state === "ambiguous") {
109
- lines.push(`candidates (${(view.candidates ?? []).length}):`)
108
+ } else if (view.state === "workspace") {
109
+ lines.push(`projects (${(view.candidates ?? []).length}):`)
110
110
  for (const c of view.candidates ?? []) lines.push(`- ${c}`)
111
- lines.push(`hint: pass target explicitly — the mechanism never picks one`)
111
+ lines.push(`hint: pass target explicitly — the target must be one of the projects above`)
112
112
  } else if (view.state === "invalid") {
113
113
  if (view.errors?.length) lines.push(`errors=${view.errors.join(";")}`)
114
114
  }
@@ -122,13 +122,13 @@ export const manifestTool = {
122
122
  type: "object",
123
123
  properties: {
124
124
  action: { type: "string", enum: ["read", "init", "write"], description: "read — current manifest + validation state (five-state anchor, never throws); init — create the default manifest when missing (refused when one already exists); write — point-edit one node (key + value) of an existing manifest" },
125
- target: { type: "string", description: "Explicit target directory — defaults to the session anchor; relative paths resolve against it. Ambiguous targets are refused with the full candidate list (never picks one)." },
125
+ target: { type: "string", description: "Explicit target directory — defaults to the session anchor; relative paths resolve against it. A workspace target (several projects below it) is refused with the full project list — pass the project root you mean." },
126
126
  key: { type: "string", description: "(write) Dot path of the node to set — version / phase / promptsLanding / codePaths, or a sub-key of docRoot / checkConfig / index / advisor (e.g. docRoot.design / index.publicRepos / advisor.docMap); whole-object writes of those four families are refused", },
127
127
  value: { type: "string", description: "(write) New value — parsed as JSON first (null / true / 5 / [\"a\"]), otherwise kept as a literal string; \"null\" is the explicit no-such-face for docRoot.<key>" },
128
128
  },
129
129
  required: ["action"],
130
130
  },
131
- readonly: false, // 动作级分类(dispatch-gates `isSubagentReadonlyAction`):read = 只读类;init / write 保持侧效门
131
+ readonly: false, // 动作级分类(dispatch-gates `isReadonlyAction`):read = 只读类;init / write 保持侧效门
132
132
  async execute(args, ctx) {
133
133
  const action = args?.action
134
134
  if (!["read", "init", "write"].includes(action)) {
@@ -138,10 +138,10 @@ export const manifestTool = {
138
138
 
139
139
  if (action === "read") return readAction(target)
140
140
 
141
- // init / write:目标面 = `projectRootView`(歧义直读形——不猜;KD-M1-39)
141
+ // init / write:目标面 = `projectRootView`(直读形——工作区不成目标;KD-M1-39)
142
142
  const rootView = projectRootView(target)
143
- if (rootView.state === "ambiguous") {
144
- throw refuse(action, ambiguousReason(target, rootView.candidates))
143
+ if (rootView.state === "workspace") {
144
+ throw refuse(action, workspaceReason(target, rootView.candidates))
145
145
  }
146
146
 
147
147
  if (action === "init") {
@@ -34,7 +34,7 @@
34
34
  * 两端共用的提示行携参(队首 ask 的 `from` + 单行归一截断的 `message`);两端各持一份 = 漂移源(D2)。
35
35
  *
36
36
  * 模块图:静态 import 核单点 `async-settle.mjs`(`carrierField` / `getAsyncPool` /
37
- * `tombstoneOf` / `wakeAsyncWaiters`——同层既有导出,同 `async-discard.mjs:35` 先例;唤醒走
37
+ * `tombstoneOf` / `wakeAsyncWaiters`——同层既有导出,同 `async-discard.mjs` 的 `async-settle.mjs` 静态导入先例;唤醒走
38
38
  * 既有静态边,零新增)+ `../context.mjs`(`pushReal`)
39
39
  * / `../agent/helpers.mjs`(`escapeXml`)/ `../log.mjs`(`logEvent`);单向、叶子向、无环。
40
40
  * 登记册导出(`agent-tools.mjs`)→ 家族矩阵装配(`agent/family-tools.mjs` depth>0 段)。
@@ -55,7 +55,7 @@ export const UPSTREAM_ASK_MAX_INFLIGHT = 1
55
55
 
56
56
  /** 工具返回注(异步形——sync: false;答复按普通指令在下回合边界到达)。 */
57
57
  const ASYNC_NOTE =
58
- "delivered to your parent's queue — consumed at the parent's next turn boundary (non-blocking). " +
58
+ "delivered to your parent's queue — consumed at the parent's next delivery round (idle wake or suspension window; not during a user turn), non-blocking. " +
59
59
  "Keep working on the unaffected parts; a reply arrives as an ordinary instruction at your next turn boundary. " +
60
60
  "If your run ends first, report the unanswered part as not done."
61
61
 
@@ -73,7 +73,7 @@ const refused = (error) => JSON.stringify({ status: "error", error })
73
73
  * ① 父字段在场 ⇒ 父对象(CLI 形主容器——写侧不变);
74
74
  * ② 缺 ⇒ `carrierField` 回退 `history` 命中 ⇒ **借用同一容器**(不另建分叉);
75
75
  * ③ 两者皆无 ⇒ 建在父字段 + **载体别名**(`parent.history._childUpstream = parent._childUpstream`
76
- * ——同 `writeTombstone` 借用规则扩张句的既有形态,`agent-tools/async-settle.mjs:73-83`)。
76
+ * ——同 `writeTombstone` 借用规则扩张句的既有形态,`agent-tools/async-settle.mjs` 的 `writeTombstone`)。
77
77
  */
78
78
  function upstreamHolder(parent) {
79
79
  if (!Array.isArray(parent._childUpstream)) {
@@ -142,11 +142,11 @@ export function noteReviewDelivered(agent, run) {
142
142
  }
143
143
 
144
144
  /** 分支③候选底座(`MANIFEST.md` §2.5「路径归一增量」族第四笔 · 2026-10-05 · #945):现行
145
- * `projectRootView(cwd)` 集(ok → [root];ambiguous → candidates 按名序;none → 空集)+
145
+ * `projectRootView(cwd)` 集(ok → [root];workspace → candidates 按名序;none → 空集)+
146
146
  * cwd 祖先链全量目录(自父目录起至盘根、最近→远、去重、现行集在前)——容器相对形在子仓锚下
147
147
  * 可解析;受理判据(目标解析位最近 manifest 声明面)∥ 围栏 ∥ 可读唯一化 ∥ fail-closed 歧义零改。 */
148
148
  function candidateRoots(view, cwd) {
149
- const current = view.state === "ok" ? [view.root] : view.state === "ambiguous" ? view.candidates : []
149
+ const current = view.state === "ok" ? [view.root] : view.state === "workspace" ? view.candidates : []
150
150
  const ancestors = []
151
151
  for (let d = dirname(resolve(cwd)); ; ) {
152
152
  ancestors.push(d)
@@ -127,7 +127,7 @@ function _checkShape(path, spec, value) {
127
127
  }
128
128
  return
129
129
  }
130
- // nonEmptyString —— defaultModel 另加 provider:model 形态面(与 model-ref.mjs:25-36 同判据;
130
+ // nonEmptyString —— defaultModel 另加 provider:model 形态面(与 model-ref.mjs 的 `parseModelRef` 同判据;
131
131
  // 存在性面不查——D-S2.5)
132
132
  if (typeof value !== "string" || !value.trim()) throw _expectsError(path, spec.expects, value)
133
133
  if (spec.providerModel) {