@thincoder/core 0.10.10 → 0.10.12

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 (82) hide show
  1. package/CHANGELOG.md +36 -2
  2. package/advisor/messages.mjs +1 -1
  3. package/advisor/project-context.mjs +2 -2
  4. package/agent/completion.mjs +4 -1
  5. package/agent/family-tools.mjs +3 -3
  6. package/agent/helpers.mjs +13 -11
  7. package/agent/light-round.mjs +1 -4
  8. package/agent/record-results.mjs +6 -1
  9. package/agent/setup-reminders.mjs +1 -1
  10. package/agent/unbooked-pen.mjs +62 -0
  11. package/agent-tools/advisor.mjs +15 -31
  12. package/agent-tools/audit-block.mjs +16 -62
  13. package/agent-tools/batch-gate.mjs +74 -66
  14. package/agent-tools/batch-lifecycle.mjs +66 -9
  15. package/agent-tools/batch-paths.mjs +10 -8
  16. package/agent-tools/context.mjs +1 -1
  17. package/agent-tools/design-slots.mjs +72 -12
  18. package/agent-tools/design-token.mjs +27 -4
  19. package/agent-tools/digest-budget.mjs +2 -0
  20. package/agent-tools/doc-check.mjs +3 -14
  21. package/agent-tools/manifest.mjs +4 -5
  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 +42 -61
  27. package/agent-tools/subagent.mjs +15 -15
  28. package/browser/launch.mjs +14 -3
  29. package/browser/queue.mjs +31 -3
  30. package/browser/session.mjs +4 -0
  31. package/config-defaults.mjs +8 -0
  32. package/config-presets.mjs +1 -1
  33. package/config.mjs +60 -11
  34. package/doc-check/anchors.mjs +10 -9
  35. package/doc-check/index.mjs +27 -23
  36. package/doc-check/paragraph.mjs +119 -0
  37. package/fts-text.mjs +4 -1
  38. package/ledger-cmd.mjs +56 -41
  39. package/ledger-db.mjs +65 -58
  40. package/ledger-tools.mjs +60 -23
  41. package/ledger.mjs +13 -11
  42. package/manifest-discovery.mjs +58 -34
  43. package/manifest.mjs +34 -21
  44. package/model-context.mjs +98 -0
  45. package/model-specs.mjs +121 -50
  46. package/package.json +2 -2
  47. package/permission.mjs +3 -1
  48. package/prompts/advisor-design.md +16 -0
  49. package/prompts/common.md +12 -1
  50. package/prompts/discipline-engineering.md +42 -12
  51. package/prompts/discipline-normal.md +8 -7
  52. package/prompts/persona-coder.md +8 -1
  53. package/prompts/persona-eng-coder.md +8 -0
  54. package/prompts/persona-eng-designer.md +26 -4
  55. package/prompts/persona-engineering.md +37 -38
  56. package/prompts/persona-explore.md +6 -0
  57. package/prompts/persona-normal.md +6 -0
  58. package/prompts/persona-plan.md +6 -0
  59. package/provider/anthropic.mjs +127 -4
  60. package/provider/core.mjs +16 -6
  61. package/provider/google.mjs +8 -8
  62. package/provider/list-models.mjs +37 -5
  63. package/provider/normalize.mjs +2 -2
  64. package/provider/responses.mjs +7 -7
  65. package/provider/sse.mjs +6 -5
  66. package/proxy-transport.mjs +5 -3
  67. package/proxy.mjs +2 -1
  68. package/session-guard.mjs +7 -1
  69. package/session-lifecycle.mjs +23 -8
  70. package/session-segments.mjs +1 -0
  71. package/session-slot-claims.mjs +4 -0
  72. package/session-slot-write.mjs +27 -1
  73. package/session-slots-manifest.mjs +25 -4
  74. package/token-ttl.mjs +78 -4
  75. package/tool-docs/advisor.md +24 -5
  76. package/tool-docs/doc_check.md +23 -16
  77. package/tool-docs/ledger.md +2 -2
  78. package/tool-docs/subagent.md +41 -16
  79. package/tool-docs/wait_for.md +15 -6
  80. package/tools/web-deepseek.mjs +26 -11
  81. package/doc-check/version.mjs +0 -84
  82. package/doc-check/width.mjs +0 -87
@@ -39,7 +39,9 @@ import { mergeAdjacentAssistantEchoes } from "./context.mjs"
39
39
  // 打开态读数公式(§6.24 判据句 2——与 CLI 状态行 / VSC 同式;零新公式,只组合)。
40
40
  import { historyPercent } from "./token-window.mjs"
41
41
  import { scheduleSessionGC } from "./session-gc.mjs"
42
- import { restoreEngTokens } from "./token-ttl.mjs"
42
+ import { restoreEngTokens, tokenExpired } from "./token-ttl.mjs"
43
+ // 槽事件(docs/core/design/SESSION-OBSERVABILITY.md §3.1):`slot:new` 走核日志单一通道。
44
+ import { logEvent } from "./log.mjs"
43
45
  import { clearPlanMode } from "./agent-tools/plan.mjs"
44
46
  import { bindRecordStore, unbindRecordStore, RECORD_WINDOW_MESSAGES } from "./session-store.mjs"
45
47
  // 会话级档位(§6.21 判据句 4)判据单源:off 形族(`think-off.mjs`)+ 逐模型枚举
@@ -73,8 +75,9 @@ function stripTruncatedToolArgs(m) {
73
75
  return changed ? { ...m, tool_calls } : m
74
76
  }
75
77
 
76
- /** Apply loaded session data onto an agent object; returns true if the runtime composite
77
- * was switched. 2026-08-31 会诊 F1:清空 _slot/_slotMtime 缓存——切换后下次保存重新认领。
78
+ /** Apply loaded session data onto an agent object; returns `{ switched, droppedDesignTokens }`
79
+ * (`switched` = 运行态渠道是否被切——原布尔值;`droppedDesignTokens` = 离开本会话的未过期
80
+ * 设计凭证数,设计档 §3.2)。2026-08-31 会诊 F1:清空 _slot/_slotMtime 缓存——切换后下次保存重新认领。
78
81
  *
79
82
  * MODEL-MERGE-SESSION(F-4)applySession 重写为两支 + 删旧支(评审 #7/#9 措辞——防
80
83
  * "三支"误读):
@@ -121,8 +124,8 @@ export function applySession(agent, data, opts = {}) {
121
124
  agent.goal = data.goal ?? null
122
125
  agent._pendingReminders = data.pendingReminders ?? []
123
126
  agent._sessionStart = data.sessionStart ?? null
124
- // R16 token 字段恢复(过期过滤——见 token-ttl.mjs restoreEngTokens)
125
- restoreEngTokens(agent, data)
127
+ // R16 token 字段恢复(过期过滤——见 token-ttl.mjs restoreEngTokens);返回值 = 离开集计数(§3.2)
128
+ const restored = restoreEngTokens(agent, data)
126
129
  // engineering is session-level (2026-08-29): the slot value is the CLI session's authority
127
130
  // — config.json is only the initial default / cross-end mirror. A legacy slot without the
128
131
  // field keeps whatever config.json seeded (unchanged behavior).
@@ -207,11 +210,11 @@ export function applySession(agent, data, opts = {}) {
207
210
  agent.provider.reasoningEffort = data.effort
208
211
  }
209
212
  }
210
- return switched
213
+ return { switched, droppedDesignTokens: restored.droppedDesignTokens }
211
214
  }
212
215
  // ② 槽无 key ∥ 不在册 → D-S3 保留——静默保持现状(落 config 链:config 统一解析产物已是
213
216
  // `agent.provider`;不报错不纠正——两方都无效由调用侧复验 validateProvider 弹重选)
214
- return false
217
+ return { switched: false, droppedDesignTokens: restored.droppedDesignTokens }
215
218
  }
216
219
 
217
220
  /**
@@ -266,6 +269,7 @@ export async function newSession(cwd, opts = {}) {
266
269
  writeSessionFile(slotPath(cwd, slot), data)
267
270
  // 摘要 `ts` 地板入参(#503 同族随动——保存面写后取 mtime;`slotMtime` 读失败 ⇒ null ⇒ 裸墙钟)
268
271
  m.slots[slot] = slotDigest(data, slotMtime(cwd, slot))
272
+ const prevSlot = Number.isInteger(m.active) ? m.active : null // 覆盖前 active(无则 null)
269
273
  m.active = slot
270
274
  // 2026-08-31 advisor round1 🟡:与 ensureActive 认领模型一致——立即记录新 slot 所有权,
271
275
  // 否则 /new 后到首次保存之间并发方(VS Code/另一 CLI)会把新 active 槽当"空闲可恢复"
@@ -287,19 +291,29 @@ export async function newSession(cwd, opts = {}) {
287
291
  saveManifest(cwd, m, deletions, opts.releaseStale ? { setActive: true, release: [slot] } : { setActive: true })
288
292
  // 2026-09-05 §6.10 D-4:/new 落点写本端记录(显式切换跟随——T-M8)
289
293
  writeEndMarker(cwd, slot)
294
+ // 槽事件(设计档 §3.1):落定后一行——字段闭集,不带标题 / 路径。
295
+ logEvent("slot:new", { slot, prevSlot, reason: "new-session" })
290
296
  return slot
291
297
  }
292
298
 
293
299
  /** 清空会话运行态(/new 用,2026-08-31 会诊 F3):_fullHistory/title/_sessionStart 等
294
300
  * 全部会话级状态 + 一次性注入标志必须全清——否则新会话首次落盘把旧会话完整人类线 +
295
301
  * 旧标题写进新槽(实锤 .19/.3 双副本);注入标志不清则 /new 后新会话永不注入
296
- * OS/cwd reminder(2026-09-01 会诊 glm 🟡)。autoApprove 是用户偏好,跨会话保留(有意)。 */
302
+ * OS/cwd reminder(2026-09-01 会诊 glm 🟡)。autoApprove 是用户偏好,跨会话保留(有意)。
303
+ * 返回 `{ droppedDesignTokens }`(设计档 §3.2)——重建 Map 前数一遍的未过期凭证数。 */
297
304
  export function resetSessionState(agent) {
298
305
  agent._fullHistory = []
299
306
  agent.history = []
300
307
  agent.title = ""
301
308
  agent.tasks = []
302
309
  agent._sessionStart = null
310
+ // 设计档 §3.2(R2):重建 Map 前数一遍「未过期」条目——返回值 = 端壳报明口径(离开集)
311
+ let droppedDesignTokens = 0
312
+ if (agent._engDesignTokens instanceof Map) {
313
+ for (const t of agent._engDesignTokens.values()) {
314
+ if (!(typeof t === "string" && tokenExpired(t))) droppedDesignTokens++
315
+ }
316
+ }
303
317
  // DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像 `_engDesignToken` 退役——不再维护
304
318
  agent._engDesignTokens = new Map() // multi-design slots die with the session (2026-09-01 fix #2)
305
319
  agent._compressFailures = 0
@@ -322,6 +336,7 @@ export function resetSessionState(agent) {
322
336
  agent._lastEngState = false
323
337
  // /new 或切换:记录存储解绑(新槽由 cmd-new / 首保存补绑重新挂载——§6.14 绑定与对账)
324
338
  unbindRecordStore(agent)
339
+ return { droppedDesignTokens }
325
340
  }
326
341
 
327
342
  /** Switch the manifest active pointer to a slot. Returns the slot's session data
@@ -41,6 +41,7 @@ export function slimForDisplay(m) {
41
41
  if (m && m.role === "assistant" && Array.isArray(m.tool_calls)) {
42
42
  let changed = false
43
43
  const tool_calls = m.tool_calls.map((tc) => {
44
+ if (!tc || typeof tc !== "object") return tc // 空元素原样带走:读取侧不许崩(#31)
44
45
  const args = tc.function?.arguments
45
46
  if (typeof args === "string" && args.length > 300) {
46
47
  changed = true
@@ -22,6 +22,8 @@ import { probeOwnersSync, ownerState, filterDeadOwners } from "./process-probe.m
22
22
  import { getSessionId, manifestPath, slotPath } from "./session-slots.mjs"
23
23
  // 清单读写(落盘单源住原档——静态环见头注;反向 re-export 保 import 面)。
24
24
  import { loadManifest, saveManifest } from "./session-slots-manifest.mjs"
25
+ // 槽事件(docs/core/design/SESSION-OBSERVABILITY.md §3.1):分到全新号时落 `slot:allocate`。
26
+ import { logEvent } from "./log.mjs"
25
27
 
26
28
  /** 本进程**残留认领集**(F-CR1 释放谓词 —— **判据单源** · SESSION.md §6.2 / §6.16):
27
29
  * 「释放 A ⟺ `slotSessions[A]` 为本进程 ∧ `A ∉ 保留集`」。`keep` = 保留集 = 落点槽 ∪ 本进程
@@ -177,9 +179,11 @@ export function allocateFresh(cwd, m, deadParam = null, bundle = null) {
177
179
  }
178
180
  let newSlot = allSlots.length > 0 ? allSlots[allSlots.length - 1] + 1 : 1
179
181
  while (liveClaimed(newSlot) || existsSync(slotPath(cwd, newSlot))) newSlot++
182
+ const prevSlot = Number.isInteger(m.active) ? m.active : null // 分配前 active(无则 null)
180
183
  m.active = newSlot
181
184
  m.slotSessions[newSlot] = mySessionId
182
185
  saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
186
+ logEvent("slot:allocate", { slot: newSlot, prevSlot, reason: "all-slots-live" })
183
187
  return newSlot
184
188
  }
185
189
 
@@ -18,7 +18,9 @@
18
18
  *
19
19
  * token 台账字段(`engDesignTokens`)的**槽 I/O 面**不在本档——单源 = `token-ttl.mjs`
20
20
  * (settle 落盘 / 门禁回读 / 链终清理三面齐备);本档只承载其**保存面合并规则**
21
- * (`mergeEngTokensForSave`——空态保留 + 并集 + 新铸者胜,纯函数)。槽标题写面同样不在
21
+ * (`mergeEngTokensForSave`——空态保留 + 并集 + 新铸者胜,纯函数)。批次归属旁列
22
+ * (`engDesignTokenBatches`)同款分层:槽 I/O 在 `token-ttl.mjs`,本档只持其保存面合并规则
23
+ * (`mergeEngTokenBatchesForSave`)。槽标题写面同样不在
22
24
  * 本档——单源 = `session-rename.mjs` 的 `renameSlot`(同 `{ok, reason}` 契约 + mtime 门控)。
23
25
  */
24
26
 
@@ -26,6 +28,8 @@ import { existsSync, readFileSync, renameSync, statSync } from "node:fs"
26
28
 
27
29
  import { getSessionId, loadManifest, saveManifest, slotDigest, slotPath, writeSessionFile, sessionEnd } from "./session-slots.mjs"
28
30
  import { loadSlotFile, isLegacyTransient } from "./session.mjs"
31
+ // 槽事件(docs/core/design/SESSION-OBSERVABILITY.md §3.1):走核日志单一通道 `logEvent`。
32
+ import { logEvent } from "./log.mjs"
29
33
  // token 形态单源(格式校验 + 到期时刻)——本档的保存面合并规则复用同一判定。
30
34
  import { tokenExpiryMs } from "./token-ttl.mjs"
31
35
  // 会话级偏好写面(§6.21 判据句 3)的判据单源——档位值域(`specForModel(...).reasoningEffortEnum`)
@@ -77,9 +81,11 @@ function rotateIfForeign(p, slot, data) {
77
81
  const diskLonger = Array.isArray(disk?.history) && Array.isArray(data?.history) &&
78
82
  disk.history.filter((m) => !isLegacyTransient(m)).length > data.history.length
79
83
  if (!(diskIsNewer || diskForeign || (diskStart && diskStart !== myStart) || diskLonger)) return null
84
+ const reason = diskIsNewer ? "newer-version" : diskForeign ? "foreign-cwd" : diskLonger ? "longer-record" : "other-session"
80
85
  const bak = `${p}.bak-${Date.now()}`
81
86
  renameSync(p, bak)
82
87
  console.error(`[session] slot ${slot} holds ${diskIsNewer ? `a newer-version file (v${disk.version})` : diskForeign ? `a foreign-cwd file (${disk.cwd})` : diskLonger ? `a longer record (${disk.history.length} > ${data.history.length} messages — concurrent append)` : `another session (start ${diskStart}, ours ${myStart})`} — preserved as ${bak}`)
88
+ logEvent("slot:rotate", { slot, reason, bak: true, via: "write" })
83
89
  return bak
84
90
  } catch (e) {
85
91
  // 文件存在但不可读(损坏 / 半写):改名保留现场,不覆盖
@@ -89,6 +95,8 @@ function rotateIfForeign(p, slot, data) {
89
95
  }
90
96
  // 排障面:区分「现场不可读」与「轮转失败」——静默会让半写/占用现场无从定位。
91
97
  console.error(`[session] slot ${slot}: pre-write rotation check failed (${e?.message ?? e}) —${preserved ? " preserved as .corrupted" : " the scene could NOT be preserved"}`)
98
+ // 判别式(设计档 §3.1):已保全 ⇒ unreadable;未保全 ⇒ rotate-failed(同一个 catch 的两个现场)。
99
+ logEvent("slot:rotate", { slot, reason: preserved ? "unreadable" : "rotate-failed", bak: false, via: "write" })
92
100
  return null
93
101
  }
94
102
  }
@@ -187,6 +195,24 @@ export function mergeEngTokensForSave(incoming, existing) {
187
195
  return out
188
196
  }
189
197
 
198
+ /**
199
+ * 保存面「批次归属旁列」合并规则(DESIGN-TOKEN-SETTLEMENT §10 / KD-BG7——纯函数):
200
+ * 沿用 `mergeEngTokensForSave` 的「槽独有项保留」半边(忙时保存不得把内存快照的缺席当成删除),
201
+ * 同 key 冲突**以新写者胜**——旁列值是路径,无 mint 时刻可比,不比 `expiresAt`。
202
+ * - `incoming === undefined`(调用方未携带该字段)⇒ 保留槽值;
203
+ * - incoming 空值 ⇒ 槽有值则保留槽、槽也空 ⇒ null(写 null = 清字段);
204
+ * - 两端都有 ⇒ 并集:槽独有项保留、incoming 覆盖同 key。
205
+ * 理由:忙时保存把归属写丢 ⇒ 收口 no-op ⇒ 凭证留存,正是 §10 要消的窗口。
206
+ */
207
+ export function mergeEngTokenBatchesForSave(incoming, existing) {
208
+ if (incoming === undefined) return existing ?? null
209
+ const hasNew = incoming && typeof incoming === "object" && !Array.isArray(incoming) && Object.keys(incoming).length > 0
210
+ const slotHas = existing && typeof existing === "object" && !Array.isArray(existing) && Object.keys(existing).length > 0
211
+ if (!hasNew) return slotHas ? existing : null
212
+ if (!slotHas) return incoming
213
+ return { ...existing, ...incoming }
214
+ }
215
+
190
216
  /** 会话级偏好(§6.21)键闭集——表外键 = 整 patch 拒(写未发生)。 */
191
217
  const SLOT_PREF_KEYS = new Set(["provider", "model", "effort"])
192
218
 
@@ -37,6 +37,8 @@ import { getSessionId, manifestPath, slotPath, writeSessionFile } from "./sessio
37
37
  // 认领面(2026-09-28 拆分外提——见头注):本档 import `staleClaims` 供 dropStaleClaims 取用,
38
38
  // 并在下方 re-export 全族保既有 import 面。
39
39
  import { staleClaims } from "./session-slot-claims.mjs"
40
+ // 槽事件(docs/core/design/SESSION-OBSERVABILITY.md §3.1):拒写 / 读回失败两处写时点。
41
+ import { logEvent } from "./log.mjs"
40
42
 
41
43
  /** Extract slot metadata from history (shared by slotDigest and loadSlotMeta) */
42
44
  export function extractSlotMeta(history, activeProvider, updatedAt, title = "") {
@@ -98,15 +100,32 @@ export function slotMtime(cwd, slot) {
98
100
  try { return statSync(slotPath(cwd, slot)).mtimeMs } catch { return null }
99
101
  }
100
102
 
101
- export function loadManifest(cwd) {
103
+ /** 清单读(两形——DESIGN-TOKEN-SETTLEMENT §10「槽号解析层同判」· KD-BG8):缺省宽容 ∥ `strict`。
104
+ * 判据 = 「没有」与「读不到」分清:文件不存在(`ENOENT`)⇒ 默认空账本(无槽号 = 合法缺省,**两形同**);
105
+ * 在盘但读失败(`EACCES` ∥ `EISDIR`…)∥ 解析失败 ⇒ 宽容形照旧吞错回默认,**严格形抛**(携 manifest
106
+ * 路径 + 底层消息)。收口门面(`consumeBatchDesignTokens`)用严格形——manifest 损坏若被读成「无槽号」,
107
+ * 收口就落成「已收口而凭证仍在」,正是 F-D8 要消的窗口。
108
+ * 判别用 `err.code === "ENOENT"`——**不用 `existsSync`**:它把权限错也吞成「不存在」,而收口面要拒的正是那类。
109
+ * **形状非法**(可读但解析成 `null` ∥ 非对象)按「读不到」处置(fail-closed;与 `saveManifest` 的 `shape-invalid`
110
+ * 同判)——严格形抛、拒文沿用「could NOT be read」:方向只更严,措辞不另立第三类。
111
+ * 与 `readEngTokenBatchesFromSlot`(token-ttl.mjs)同一两形先例:同一读函数、同一判据、缺省宽容;
112
+ * 展示 / 尽力面一律缺省(现行调用点零改),收口门面才传 `{ strict: true }`。 */
113
+ export function loadManifest(cwd, { strict = false } = {}) {
114
+ let p = "(unresolved manifest path)"
102
115
  try {
103
- const p = manifestPath(cwd)
104
- if (!existsSync(p)) return { slots: {}, sessionId: null }
116
+ p = manifestPath(cwd)
105
117
  const m = JSON.parse(readFileSync(p, "utf8"))
106
118
  if (!m.slots) m.slots = {} // 2026-09-01 会诊 deepseek 🔵:损坏的 {} manifest 不再让调用方抛 TypeError
107
119
  if (!m.sessionId) m.sessionId = null
108
120
  return m
109
- } catch { return { slots: {}, sessionId: null } }
121
+ } catch (e) {
122
+ // 「没有」:文件不存在 = 合法缺省(新会话 / 首建前)——两形一致,严格形也不抛
123
+ if (e?.code === "ENOENT") return { slots: {}, sessionId: null }
124
+ if (strict) {
125
+ throw new Error(`loadManifest: the session manifest could NOT be read (${p}: ${e?.message ?? e}) — the slot number cannot be resolved, so "no slot" cannot be told apart from "cannot read"; the engineering closeout is refused with the record untouched (fix the manifest file or its permissions and retry).`)
126
+ }
127
+ return { slots: {}, sessionId: null }
128
+ }
110
129
  }
111
130
 
112
131
  /** 可信合并基座判据(§6.23 判据句 1 · ②):顶层非 null 非数组对象 ∧(`slots` / `slotSessions`
@@ -145,6 +164,7 @@ export function ledgerHealth(cwd) {
145
164
  function refuseManifestWrite(p, reason, preserved = null) {
146
165
  noteRefusal(reason, p)
147
166
  console.error(`[session] saveManifest: write refused (reason=${reason}) path=${p}${preserved ? ` preserved=${preserved}` : ""}`)
167
+ logEvent("slot:refuse", { reason })
148
168
  return false
149
169
  }
150
170
 
@@ -162,6 +182,7 @@ function writeManifestBase(p, m) {
162
182
  if (trusted) return true
163
183
  noteRefusal("readback-failed", p)
164
184
  console.error(`[session] saveManifest: readback-failed path=${p} preserved=${preserveScene(p)}`)
185
+ logEvent("slot:refuse", { reason: "readback-failed" })
165
186
  return false
166
187
  }
167
188
 
package/token-ttl.mjs CHANGED
@@ -20,6 +20,9 @@
20
20
  * - **D3**:**单值镜像 `_engDesignToken` 退役**——AC3 零写 + 仅一次性迁移读。本模块
21
21
  * 不再写镜像(engTokenSlotFields 只产多槽表);restoreEngTokens 是唯一迁移读点(旧
22
22
  * slot 残留 engDesignToken → 迁入 Map,此后不写不读镜像)。
23
+ * - **§10(2026-10-26)**:批次归属旁列 `engDesignTokenBatches`(designId → 批档绝对路径)与
24
+ * 槽多槽表**同点序列化 / 同拍恢复 / 同处落盘**;轻读面 = `readEngTokenBatchesFromSlot`。
25
+ * token 值形零改(仍 `<uuid>:<expiryMs>`)——旁列只承载归属,不承载授权。
23
26
  *
24
27
  * 依赖方向:本模块只 import 底层槽 I/O(session-slots.mjs 原语 + session-guard.mjs 轮转
25
28
  * 守卫)——session.mjs 同时 import 本模块(engTokenSlotFields/restoreEngTokens),形成
@@ -108,12 +111,17 @@ export function purgeExpiredDesignTokens(agent) {
108
111
  * (JSON-safe);空/缺 Map → undefined → JSON.stringify 丢 key——清过的会话写
109
112
  * NO field,不从上次 save 复活槽。
110
113
  * DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像 `engDesignToken` 退役——
111
- * saveSession 不再写镜像字段(AC3 零写);只产多槽表 engDesignTokens。 */
114
+ * saveSession 不再写镜像字段(AC3 零写);只产多槽表 engDesignTokens。
115
+ * 批次归属旁列 `engDesignTokenBatches`(DESIGN-TOKEN-SETTLEMENT §10)同处序列化——
116
+ * 与槽表并列、同空态语义(空表 ⇒ undefined ⇒ 字段不落盘);承载归属,不承载恢复。 */
112
117
  export function engTokenSlotFields(agent) {
113
118
  return {
114
119
  engDesignTokens: agent._engDesignTokens instanceof Map && agent._engDesignTokens.size > 0
115
120
  ? Object.fromEntries(agent._engDesignTokens)
116
121
  : undefined,
122
+ engDesignTokenBatches: agent._engDesignTokenBatches instanceof Map && agent._engDesignTokenBatches.size > 0
123
+ ? Object.fromEntries(agent._engDesignTokenBatches)
124
+ : undefined,
117
125
  }
118
126
  }
119
127
 
@@ -129,6 +137,13 @@ export function engTokenSlotFields(agent) {
129
137
  * 残留——一次性语义。镜像若已过期/畸形 → 不迁移(drop——与 Map 过期项同处理,
130
138
  * 清盘闭环)。永不写 agent._engDesignToken。 */
131
139
  export function restoreEngTokens(agent, data) {
140
+ // 设计档 §3.2(R2):整体替换前快照旧 Map 的「未过期 id 集」,落定后求差 = 离开集(非集合大小差)。
141
+ const before = new Set()
142
+ if (agent._engDesignTokens instanceof Map) {
143
+ for (const [id, t] of agent._engDesignTokens) {
144
+ if (!(typeof t === "string" && tokenExpired(t))) before.add(id)
145
+ }
146
+ }
132
147
  const map = new Map()
133
148
  if (data.engDesignTokens && typeof data.engDesignTokens === "object" && !Array.isArray(data.engDesignTokens)) {
134
149
  for (const [id, t] of Object.entries(data.engDesignTokens)) {
@@ -144,16 +159,32 @@ export function restoreEngTokens(agent, data) {
144
159
  }
145
160
  if (map.size > 0) agent._engDesignTokens = map
146
161
  else delete agent._engDesignTokens
162
+ // 批次归属旁列同拍读入(§10):值 = 批档绝对路径,无 TTL 可言(承载归属,不承载授权)。
163
+ // 槽因过期被丢而旁列行仍在 = 惰性残留:消费面按 id 反查时自然落成 no-op(不猜归属)。
164
+ const batches = new Map()
165
+ if (data.engDesignTokenBatches && typeof data.engDesignTokenBatches === "object" && !Array.isArray(data.engDesignTokenBatches)) {
166
+ for (const [id, b] of Object.entries(data.engDesignTokenBatches)) {
167
+ if (typeof b === "string" && b) batches.set(id, b)
168
+ }
169
+ }
170
+ if (batches.size > 0) agent._engDesignTokenBatches = batches
171
+ else delete agent._engDesignTokenBatches
172
+ let droppedDesignTokens = 0
173
+ for (const id of before) if (!map.has(id)) droppedDesignTokens++
174
+ return { droppedDesignTokens }
147
175
  }
148
176
 
149
177
  // ── 会话槽权威台账 I/O(DESIGN-TOKEN-SETTLEMENT D1/D2/D3——槽文件 = 权威结算台账) ──
150
178
 
151
179
  /** 当前会话槽号(读侧——无认领副作用):粘性 _slot 优先;未钉槽时读 manifest active
152
- * (loadManifest 只读——不在门禁路径触发认领写)。无槽 → null。 */
153
- function currentSlotNoReadOnly(agent) {
180
+ * (loadManifest 只读——不在门禁路径触发认领写)。无槽 → null。
181
+ * `strict` 透传 manifest 读形(§10「槽号解析层同判」):缺省宽容(manifest 不可读 ⇒ 无槽号);
182
+ * 收口面 `{strict:true}` ⇒ manifest 在盘但读失败 ∥ 解析失败即抛;manifest 不存在 ∥ `active` 缺席
183
+ * 仍是无槽号(合法缺省——「读到了,说没有」)。 */
184
+ function currentSlotNoReadOnly(agent, { strict = false } = {}) {
154
185
  const slot = agent?._slot
155
186
  if (Number.isInteger(slot)) return slot
156
- const active = loadManifest(agent?.cwd)?.active
187
+ const active = loadManifest(agent?.cwd, { strict })?.active
157
188
  return Number.isInteger(active) ? active : null
158
189
  }
159
190
 
@@ -175,6 +206,46 @@ export function readEngTokensFromSlot(agent) {
175
206
  } catch { return null }
176
207
  }
177
208
 
209
+ /** 读当前会话槽文件的批次归属旁列(DESIGN-TOKEN-SETTLEMENT §10——不做 TTL 过滤、不改盘)。
210
+ * 与 `readEngTokensFromSlot` 同形同读法(JSON.parse 轻读、零 reconcile 副作用);两者不能合成
211
+ * 一个函数——既有读者的字段名硬编码(只返回 `data.engDesignTokens`)⇒ 同形兄弟函数。
212
+ * **两形(§10「读形(收口面严格 ∥ 清点面宽容)」)**:`strict:false`(缺省 · 清点面)读不到 ⇒ `null`,
213
+ * 批列照旧如实降级 `—`(F-SL1);`strict:true`(收口面)读不到 ⇒ **抛**(close 被拒、档零字节变、可重试)。
214
+ * 判据 = **「没有」与「读不到」分清**:无槽号 ∥ 文件不存在(`ENOENT`)∥ 无该字段 ⇒ 空集(合法缺省,
215
+ * 两形同);其余 I/O 错(`EACCES` ∥ `EISDIR` ∥ `EIO`…)∥ `JSON.parse` 失败 ⇒ 宽容形 `null` / 严格形抛。
216
+ * **槽号解析层同判(manifest 层)**:槽号来自 `currentSlotNoReadOnly(agent, { strict })`——`_slot` 未钉时
217
+ * 读 manifest;strict 形把 manifest 的「读不到」原样抛出(拒因点名 manifest,不被本函数重包成旁列读失败)。
218
+ * 不用 `existsSync` 兜底——它把权限错也吞成「不存在」,正是收口面必须拒的那一类(「无法判定归属 =
219
+ * 无法证明清账」;§10「严格读的边界」)。
220
+ * 返回 {designId: <批档绝对路径>} 对象或 null(无槽/无文件/无该字段;宽容形的不可读亦 `null`)。 */
221
+ export function readEngTokenBatchesFromSlot(agent, { strict = false } = {}) {
222
+ // 槽号解析在 try 之外:manifest 层的「读不到」自带 manifest 坐标(§10「槽号解析层同判」),
223
+ // 不在这里被重包成「旁列读不到」——两层拒因各自点名。宽容形此步不抛(不可读 ⇒ 无槽号 ⇒ null)。
224
+ const slot = currentSlotNoReadOnly(agent, { strict })
225
+ if (slot == null) return null
226
+ let p = "(unresolved slot path)"
227
+ try {
228
+ p = slotPath(agent.cwd, slot)
229
+ let raw
230
+ try {
231
+ raw = readFileSync(p, "utf8")
232
+ } catch (e) {
233
+ if (e?.code === "ENOENT") return null // 「没有」:文件不存在(新会话 / 尚无槽)——合法缺省
234
+ throw e // 「读不到」:其余 I/O 错交外层按读形处置
235
+ }
236
+ const data = JSON.parse(raw)
237
+ return (data && typeof data === "object" && !Array.isArray(data.engDesignTokenBatches)
238
+ && data.engDesignTokenBatches && typeof data.engDesignTokenBatches === "object")
239
+ ? data.engDesignTokenBatches
240
+ : null
241
+ } catch (e) {
242
+ if (strict) {
243
+ throw new Error(`readEngTokenBatchesFromSlot: the session slot file's batch-attribution side column could NOT be read (${p}: ${e?.message ?? e}) — the closeout consume cannot tell which slots belong to this batch, so the closeout is refused with the record untouched; fix the slot file or its permissions and retry.`)
244
+ }
245
+ return null
246
+ }
247
+ }
248
+
178
249
  /** 内存 miss 时从权威槽 reconcile(D2 spawn 门 + D3 dispatch 写门同源):把槽文件中
179
250
  * **未过期**项并进内存 Map(TTL 过滤保留——expired 不并入,fail-closed;格式/畸形串
180
251
  * 与 restoreEngTokens 同语义读回——门禁格式拒);槽无活项 → 内存原样。返回判定用 Map。
@@ -271,6 +342,9 @@ export function persistEngTokens(agent) {
271
342
  const fields = engTokenSlotFields(agent)
272
343
  if (fields.engDesignTokens !== undefined) data.engDesignTokens = fields.engDesignTokens
273
344
  else delete data.engDesignTokens
345
+ // 批次归属旁列逐字段复制同处(§10「保存 / 合并路径」第二处):空表 ⇒ 字段删(不从上次落盘复活)。
346
+ if (fields.engDesignTokenBatches !== undefined) data.engDesignTokenBatches = fields.engDesignTokenBatches
347
+ else delete data.engDesignTokenBatches
274
348
  // 单值镜像字段永不复写(D3 零写)——残留 engDesignToken 随本次写一并清(更快退役,
275
349
  // 杜绝 stale 镜像 + 空 Map 组合在下次 restore 二次迁移复活死 token 的边角)。
276
350
  delete data.engDesignToken
@@ -1,9 +1,28 @@
1
- Run an independent review of your work. `type` is REQUIRED — one of exactly two values: 'design' reviews design / requirement documents BEFORE implementation (documents=[...] with the explicit doc paths — relative paths resolve against the session cwd first, then candidate project roots (project-root-relative e.g. <repo>/docs/…, or absolute; relative forms are also tried against the session cwd's ancestor directories); plus batchDoc when a batch record is in flight); 'code' reviews the code you changed AFTER implementation (paths=[...] to scope files/directories; documents=[...] adds acceptance-criteria context). A call without a type (or with a conflicting object.type) is refused — no default, no silent fallback.
1
+ Run an independent review of your work. `type` is REQUIRED — one of exactly two values, no default and no silent fallback.
2
2
 
3
- The advisor is an independent read-only sub-agent (explores the codebase, reads files, traces callers via grep/lsp). Code review converges: round 1 full review → round 2 verifies the agent's fix claims → round 3+ re-checks only the fix claims. Design review is a single pass against methodology and requirements. Criteria come from .thincoder/advisor.md (if present) or sensible defaults. After the review you MUST produce a response table (format = discipline rules); on all-clear, call verify.
3
+ - `type="design"` — design review: reviews design / requirement documents BEFORE implementation. Pass `documents=[...]` (the explicit list), plus `batchDoc` when a batch record is in flight.
4
+ - `type="code"` — code review: reviews the code you changed AFTER implementation. Pass `paths=[...]` to scope files/directories; `documents=[...]` adds acceptance-criteria context.
4
5
 
5
- object={type,target,status,reason,exclude} optionally anchors the review target — mechanically injected at the start of the review message; absent = no injection.
6
+ `documents` — one rule: a relative path resolves against the session project root (e.g. `docs/core/design/ADVISOR-GUARDS.md`); an absolute path is used as-is.
6
7
 
7
- ASYNC (depth 0 only): the review runs in the BACKGROUND by default — ack immediately, the report arrives automatically in a digest turn. One shared pool (agent.poolLimits.advisor, default 4 — configurable via /config 并发池 or config.json; pool-full and same-scope launches are refused stating the current limit) — launch reviews one at a time. Inside a child (depth>0) reviews are always synchronous (async:true rejected).
8
+ The relative form needs the session anchor to be inside a project. When the anchor is a workspace root (several projects below it, no manifest of its own) there is no single project root to resolve against, and a relative path is refused with `no-project-root` — write the absolute path there.
8
9
 
9
- Returns the verdict: all-clear (call verify) or a findings list to fix.
10
+ Accepted = an existing `.md` file anywhere inside a project — a doc sitting at the repo root counts. Anything else is refused with the resolved path, the reason and the two expected forms.
11
+
12
+ A call without a type (or with a conflicting `object.type`) is refused.
13
+
14
+ The advisor is an independent read-only sub-agent (explores the codebase, reads files, traces callers via grep/lsp).
15
+
16
+ - Code review converges: round 1 full review → round 2 verifies the agent's fix claims → round 3+ re-checks only the fix claims.
17
+ - Design review is a single pass against methodology and requirements.
18
+ - Criteria come from `.thincoder/advisor.md` (if present) or sensible defaults.
19
+ - After the review you MUST produce a response table (format = discipline rules); on all-clear, call `verify`.
20
+
21
+ `object={type,target,status,reason,exclude}` optionally anchors the review target — mechanically injected at the start of the review message; absent = no injection.
22
+
23
+ ASYNC (depth 0 only): the review runs in the BACKGROUND by default — ack immediately, the report arrives automatically in a digest turn.
24
+
25
+ - One shared pool: `agent.poolLimits.advisor`, default 4 — configurable via `/config` 并发池 or config.json. Pool-full and same-scope launches are refused stating the current limit; launch reviews one at a time.
26
+ - Inside a child (depth>0) reviews are always synchronous (`async:true` rejected).
27
+
28
+ Returns the verdict: all-clear (call `verify`) or a findings list to fix.
@@ -1,53 +1,59 @@
1
1
  Run a built-in documentation-discipline check on a project and report its findings.
2
2
  Read-only. In the default `report` mode it is non-gating: it reports, it never blocks, never sets an exit code, and its output is never a reason to refuse a task or a user instruction.
3
- When the engine files are newer than this session's start time (or unreadable), `report` prefixes its output with one
4
- `[doc_check note]` line naming the stale files — that reading came from an engine loaded at startup and is not usable as
5
- delivery evidence. Nothing else changes: `report` still produces no verdict and no existing line is altered (no stale files ⇒
6
- the report is byte-identical).
7
3
 
8
4
  What it checks — exactly two criteria, both built into the engine:
9
5
  - Anchors — every documentation pointer (a `*.md` token, optionally carrying directory segments, optionally followed by a section
10
6
  indicator `§N` / `§N.M` / `§N.M.K`) must resolve: the file must exist on disk (`档缺` when it does not), and when a section
11
7
  indicator is given the target file must carry a heading numbered with it (`节缺` when it does not). Line-number coordinates
8
+
12
9
  (`<doc>:12`) are not section references and are not judged as such. Files inside fenced code blocks and executable lines
13
10
  (command / pattern code spans) are skipped whole.
14
11
 
15
12
  ~~~text
16
- **Operand names.** Six names count as operands rather than documents: the model writes them, or reads a file by that name. On
17
- the prompt face (`thincoder-core/prompts/**`) they are stripped from a line before extraction. On other faces four of the six are
13
+ **Operand names.** Seven names count as operands rather than documents: the model writes them, or reads a file by that name. On
14
+ the prompt face (`thincoder-core/prompts/**`) they are stripped from a line before extraction. On other faces four of the seven are
15
+
18
16
  exempt as file-name classes — `AGENTS.md`, `SKILL.md`, `.thincoder/advisor.md`, `project_rules.md` — while `README.md` and
19
- `MANIFEST.md` are still judged as pointers. The six:
17
+ `MANIFEST.md` are still judged as pointers. `RTM.md` is stripped on the prompt face; on other faces it is judged as a pointer — write it path-qualified (e.g. `test/RTM.md`) so it resolves. The seven:
20
18
 
21
19
  ```text
22
- AGENTS.md SKILL.md README.md MANIFEST.md .thincoder/advisor.md project_rules.md
20
+ AGENTS.md SKILL.md README.md MANIFEST.md .thincoder/advisor.md project_rules.md RTM.md
23
21
  ```
24
22
  ~~~
25
23
 
26
- - Line width — a line whose character count (CJK counts as 1) exceeds the built-in threshold, unless it is a table row, inside a
27
- fenced code block, an executable line, a quote-block line, or inside an exempt zone.
24
+ - Paragraph length — on a document face, adjacent non-blank body lines merge into one paragraph whose total character
25
+ count (CJK counts as 1) is judged against the built-in threshold; blank lines, table rows, headings, list items,
26
+ quote-block lines and fenced code blocks do not count into a paragraph and break it, and lines inside an exempt zone
27
+
28
+ are excluded. Breaking a paragraph into more lines changes nothing — only splitting it into more paragraphs or
29
+ writing it shorter passes.
30
+ - Line length — a prompt-face file (`thincoder-core/prompts/**`) is judged line by line: the model reads raw text, so
31
+ one line is one block, and any single line over the threshold counts with no structural exemption.
28
32
 
29
33
  It cannot — in `report` mode — write anything, gate anything, or turn a finding into a refusal.
30
34
 
31
35
  Arguments — `target` (optional): the project root to check; defaults to the session anchor, relative paths resolve against it.
32
36
  A workspace target (several projects below it) is reported with the full candidate list — pass the project root you mean.
37
+
33
38
  `mode` (optional) — `report` (default: a non-gating report over the documents you list) or `gate` (the delivery gate below).
34
39
  `docs` (required) — the documents to judge, as paths relative to the project root or absolute; each is judged whole and nothing
40
+
35
41
  else is read except the targets those documents point at. Omitted in `gate` mode ⇒ the Markdown documents this session wrote.
36
42
  There is no default range in `report` mode: omitting `docs`, or passing an empty list, is refused rather than widened — nothing
43
+
37
44
  is scanned unless you name it. A path that is not on disk or is not a Markdown document is an error in `report` mode and nothing
38
45
  is judged; in `gate` mode such paths are listed as skipped instead. A pointer resolves against the project root (full path) or
46
+
39
47
  against the pointing document's own directory — bare names are not resolved across other directories.
40
48
 
41
49
  Delivery gate — `mode: "gate"`, for the end of a design or development round. Every listed doc is judged **whole**: a
42
- pre-existing dangling pointer or overlong line anywhere in it counts, not only this round's edited lines. The first line is the
50
+ pre-existing dangling pointer or overlong paragraph / line anywhere in it counts, not only this round's edited lines. The first line is the
51
+
43
52
  machine-readable verdict — `DOC-CHECK GATE: PASS` / `DOC-CHECK GATE: REFUSED(命中 K 条)` / `DOC-CHECK GATE: ERROR(reason)` —
44
53
  and consumers judge by that prefix; the hits are fixed in the same round and are never booked to the ledger. The gate refuses
54
+
45
55
  **one delivery**, not a user instruction: an explicit user go-ahead is followed, with the hits written into the delivery.
46
- Before running the criteria the gate checks engine-version consistency: the six engine files (`doc-check/index.mjs` /
47
- `doc-check/anchors.mjs` / `doc-check/width.mjs` / `doc-check/version.mjs` / `agent-tools/doc-check.mjs` / this tool description) each have their mtime
48
- compared with this process's start time — newer, or unreadable, ⇒ `DOC-CHECK GATE: ERROR(引擎已更新…)` instead of a PASS,
49
- because a session keeps the engine modules it loaded at startup. `ERROR` is fail-closed (no PASS means not passed): restart the
50
- session and re-run; a multi-project workspace ⇒ name the project root, per the stated cause.
56
+ `ERROR` is fail-closed (no PASS means not passed): a multi-project workspace ⇒ name the project root, per the stated cause.
51
57
 
52
58
  Do not wire it as a gate or CI step in `report` mode, and do not treat a finding as a reason to refuse an instruction — a
53
59
  machine check never outranks a user instruction.
@@ -59,6 +65,7 @@ Side effects and permissions — none. The tool is read-only: no writes, no appr
59
65
 
60
66
  Parameters and scope — both criteria run on built-in constants (`DOC_CHECK_BUILTINS`: threshold 300 characters, no exempt zones,
61
67
  no exemptions), and the judged set is the document list the caller passes — the judged set and the resolution set are the same
68
+
62
69
  documents, so there is no scan-dir / domain / root / exclude surface and no whole-repository scan. Changing a criterion or a
63
70
  threshold is a code change and goes through review; no project declaration changes these readings.
64
71
 
@@ -1,8 +1,8 @@
1
1
  台账统一入口——action = add / update / close / query / count。query / count 只读全角色;add / update / close 写仅主 agent。
2
2
  - add:新增条目入待讨论。kind = requirement(需求池)/ tech_todo(技术待办);trigger = 归批 / 条件 / 认账不排期(可空);resume_when = 复启条件句(可空;值非空——空串 / 全空白拒;句内可带锚 `@YYYY-MM-DD`(机判到期)或 `@事件件名`(机器不判))。task_book 写门(add ∥ update 同判,判本笔写入面):结果行 在途 / 待核销 ∥ trigger = 归批 ⇒ task_book 必填(缺 / 空串 / 全空白拒);本笔新写 task_book(非空值)∥ 本笔新写 trigger = 归批 ⇒ 该指针的文件部分(首个 `§` 前子串)须可解析且指出的档在盘,否则拒。本笔不动指针 ⇒ 零判零阻——旧指针所指档已被清理不阻 status / evidence / 其它字段更新(「档在不在盘」是读数 taskBookOnDisk,不是门)。
3
- - update:按六态允许表迁移:待讨论→待设计 / 待设计→在途 / 在途→待核销 / 待核销→已核销 / 任意态→已废弃;表外拒。id ∥ batch 恰携其一。status 缺省 = 仅更新字段;executor 缺省 = 进入待设计 / 在途自动写本会话,出在途 / 撤回清空;resume_when 同 update 可空字段(缺省 = 不变)。evidence 传入 = 追加(旧值非空 ⇒ 拼接 `旧|新`——「|」零空格;旧空直写);`evidenceReplace: true` 与 evidence 同传 = 整段替换;旗独传(无新文)∥ 新值全空白 ⇒ 拒。
3
+ - update:按六态允许表迁移:待讨论→待设计 / 待设计→在途 / 在途→待核销 / 待核销→已核销 / 任意态→已废弃;表外拒;迁移入 已核销 时结果行标题携「收尾链待跑」同拒(同笔携清标记 title 放行)。id ∥ batch 恰携其一。status 缺省 = 仅更新字段;executor 缺省 = 进入待设计 / 在途自动写本会话,出在途 / 撤回清空;resume_when 同 update 可空字段(缺省 = 不变)。evidence 传入 = 追加(旧值非空 ⇒ 拼接 `旧|新`——「|」零空格;旧空直写);`evidenceReplace: true` 与 evidence 同传 = 整段替换;旗独传(无新文)∥ 新值全空白 ⇒ 拒。
4
4
  - update --batch:批级批量迁移(batch = 批档指针,与该档 task_book 同址的行一次处理)——携 status ⇒ 逐行按迁移表重判;不携 status ⇒ 纯字段补丁(status 零写)。回执 { batch, status, applied, skipped };被跳过的行逐行点名 { id, from, why }(why = 逐行拒因原文),其余行照落;事务级失败整批回滚。batch 值须可解析且批档在盘,否则拒。
5
- - close:勾销(待核销 → 已核销);追认核销(待讨论 / 待设计 → 已核销,须有 evidence——可随本调用传入(一跳核销),判结果值:缺 / 全空白拒);在途 → 已核销 不可跳;撤回(任意态 → 已废弃)。软删除(写 closed_at,行保留)。id ∥ batch 恰携其一。evidence 传入 = 追加(旧值非空 ⇒ 拼接 `旧|新`;旧空直写);`evidenceReplace: true` 同传 = 整段替换;旗独传 / 新值全空白 ⇒ 拒;缺省 = 判行值。
5
+ - close:勾销(待核销 → 已核销);追认核销(待讨论 / 待设计 → 已核销,须有 evidence——可随本调用传入(一跳核销),判结果值:缺 / 全空白拒);在途 → 已核销 不可跳;核销标记门:结果行标题携「收尾链待跑」⇒ 拒(收尾链未跑完,不许核销——先跑收尾全链,再由父侧清标记);撤回(任意态 → 已废弃)不拦。软删除(写 closed_at,行保留)。id ∥ batch 恰携其一。evidence 传入 = 追加(旧值非空 ⇒ 拼接 `旧|新`;旧空直写);`evidenceReplace: true` 同传 = 整段替换;旗独传 / 新值全空白 ⇒ 拒;缺省 = 判行值。
6
6
  - close --batch:批级批量结算——只销已到 待核销 的行(勾销);待讨论 / 待设计 逐行点名跳过(追认核销不入批量面,转单行 close),在途 逐行点名跳过(不可跳);已核销 / 已废弃 行静默不计。status 可不给(缺省 = 已核销),给了只能是 已核销。
7
7
  - query:SQLite 行集,等值过滤(status / kind / board / trigger / task_book)缺省 = 全部行;行集逐行携 aged(未决四态 ∧ 行龄 > 30 天)/ evidenceOversize(evidence 超软上限 300 字——读数非拒写)/ taskBookOnDisk(task_book 文件部分解析成立 ∧ 该档在盘 ⇒ true;缺指针 / 不可解析 / 不在盘 ⇒ false——读数非门,不拦任何写入)/ overdue(出口态字串 ∥ null:resume_due / dangling / unlabeled / aged,首中优先)。六态 = 待讨论 / 待设计 / 在途 / 待核销 / 已核销 / 已废弃。
8
8
  - count:回报 { count, overdue, evidenceOversize }——count = 未决四态(待讨论 / 待设计 / 在途 / 待核销)行数;overdue = 其中命中出口态的行数;evidenceOversize = 其中 evidence 超限行数。task_book 等值过滤。