@thincoder/core 0.9.2 → 0.9.4

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 (54) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +1 -0
  3. package/agent/completion.mjs +3 -1
  4. package/agent/family-tools.mjs +12 -9
  5. package/agent/helpers.mjs +11 -2
  6. package/agent/run-stages.mjs +27 -5
  7. package/agent/setup.mjs +6 -0
  8. package/agent/write-gate.mjs +5 -5
  9. package/agent-tools/advisor-async.mjs +4 -4
  10. package/agent-tools/advisor.mjs +1 -1
  11. package/agent-tools/async-discard.mjs +1 -1
  12. package/agent-tools/audit-block.mjs +106 -0
  13. package/agent-tools/batch-lifecycle.mjs +72 -16
  14. package/agent-tools/batch-skeleton.mjs +69 -7
  15. package/agent-tools/batch.mjs +19 -6
  16. package/agent-tools/context.mjs +174 -0
  17. package/agent-tools/goal.mjs +7 -0
  18. package/agent-tools/parent-channel.mjs +2 -2
  19. package/agent-tools/plan.mjs +6 -6
  20. package/agent-tools/read-history.mjs +122 -24
  21. package/agent-tools/settings.mjs +4 -2
  22. package/agent-tools/subagent-async.mjs +3 -3
  23. package/agent-tools/subagent-spawn.mjs +29 -101
  24. package/agent-tools/task.mjs +11 -0
  25. package/agent-tools.mjs +4 -1
  26. package/agent.mjs +10 -4
  27. package/config-presets.mjs +2 -2
  28. package/config.mjs +1 -1
  29. package/context.mjs +66 -121
  30. package/fts-text.mjs +41 -0
  31. package/memory/core.mjs +4 -18
  32. package/memory/schema.mjs +4 -11
  33. package/model-specs.mjs +22 -7
  34. package/package.json +5 -1
  35. package/prompts/common.md +2 -2
  36. package/prompts/discipline-engineering.md +17 -2
  37. package/prompts/persona-engineering.md +1 -1
  38. package/session-gc.mjs +11 -0
  39. package/session-index-build.mjs +298 -0
  40. package/session-index-cmd.mjs +61 -0
  41. package/session-index-pass.mjs +95 -0
  42. package/session-index-query.mjs +102 -0
  43. package/session-index.mjs +285 -0
  44. package/session-slots-manifest.mjs +19 -0
  45. package/token-window.mjs +188 -0
  46. package/tools/bash.mjs +4 -15
  47. package/tools/execute.mjs +5 -13
  48. package/tools/git-checkpoint.mjs +1 -1
  49. package/tools/git-ext.mjs +23 -20
  50. package/tools/git-run.mjs +141 -0
  51. package/tools/git.mjs +42 -36
  52. package/tools/index.mjs +3 -1
  53. package/tools/process-tree.mjs +20 -0
  54. package/tools/shared.mjs +8 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,76 @@
1
+ # Changelog(@thincoder/core)
2
+
3
+ All notable changes to the core package are documented here.
4
+ Format: Keep a Changelog · 中文 · 号在发布时定(CalVer——见 `docs/RELEASE.md` §4)。
5
+
6
+ ## [0.9.4] — 2026-09-22
7
+
8
+ > 0.9.3 → 0.9.4(月内序号——发布时定号)
9
+
10
+ ### Added
11
+
12
+ - **MiMo V2.6 三款规格行**(`mimo-v2.6-pro` / `mimo-v2.6-flash` / `mimo-v2.6-pro-ultraspeed`——同日上架):此前缺行落兜底(128K / 32K / 无视觉),现按真机实测登记——1M 上下文 · 131_072 输出 · 思考默认开(`thinking.type`)· 全模态 · 自动缓存。
13
+
14
+ ### Changed
15
+
16
+ - **MiMo v2.5 两行存量对齐**(同日实测):`maxOutput` 128_000 → 131_072 + 补自动缓存登记;族头注与 `assistantToolCallMessage` 文书句按 2026-09-22 复测改写(原「缺字段 → 400」句已不可复现——回显策略保持保守不变)。
17
+ - **MiMo 预设改指**:`mimo` / `mimoplan` 默认模型 → `mimo-v2.6-pro`(v2.5 官网标注即将下线;Token Plan 端点同平台推断)。
18
+
19
+ ## [0.9.3] — 2026-09-22
20
+
21
+ > 0.9.2 → 0.9.3(月内序号——发布时定号)
22
+
23
+ ### Added
24
+
25
+ - **派生会话索引库**(`node:sqlite` · 零新依赖):`read_history` 超大会话不再整档拒(>50k 消息经索引可查)+ `path:"all"` 跨会话检索(行携 `session.file` 回查锚)+ `tool_calls` 携参数;索引从记录段增量建 · 可重建(丢/坏自愈)· 主存(会话文件)零改。
26
+ - **子代理「系统固块」**:机制性指令(审计五锚 + 批档行)改由 spawn 单点写入并拼进 system 面(前缀缓存契约——同 child 连跑 `systemPrompt` 逐字节相等)。
27
+ - **进程族单源**:`killProcessTree` 收口为 `tools/process-tree.mjs`(核 / VSC 本地副本全消除)。
28
+ - **退出即释放会话认领**:正常退出释放本进程认领(端标记保留作路标)——重启恢复不再依赖进程探测兜底。
29
+ - **认领释放扩面**:ACP `session/close` · 跨 cwd 释放 · 被占槽返回可区分信号(零写)。
30
+
31
+ ### Changed
32
+
33
+ - **批档工具词面协议结构化**:`batch status` enum 化(散文走 `note`)+ `create` 补 `source` / `prev` 前缀归一 / 占位自动;append/status 占位残留机检(fail-closed)。
34
+ - **工程模式 `task` 工具机械停用**(追踪面 = 批档 + 台账;普通模式零改)。
35
+ - **设置面**:敏感键谓词导出 + 非敏感父对象 JSON 化渲染(`[object Object]` 瑕疙消除)。
36
+ - FTS 语言面外提 `fts-text.mjs`(`segmentCJK` / `buildFtsQuery` 单源 + re-export 面)。
37
+
38
+ ### Fixed
39
+
40
+ - 会话索引自愈链(坏库 / 删库现场保留 + 重建)· 段轮转与等长改写的水位判别 · 源消失级联清行。
41
+
42
+ ## [0.9.2] — 2026-09-21
43
+
44
+ > 0.9.1 → 0.9.2(月内序号——发布时定号)
45
+
46
+ ### Added
47
+
48
+ - **PROJECT-MANIFEST 模型(按用点解析)**:项目发现五级梯(会话锚 → 带档祖先 → 一层子档 → 工作区……)+ owning-ancestor 归属 + 建档流;整档缺失不再静默(启动降级 + 明示提示)。
49
+ - **批档生命周期工具** `batch`(create / append / status / close——原 `batch_segment` 更名,别名保留)。
50
+ - **台账执行人列**(F-LX1):executor 归属 + 判存活显示。
51
+ - **信号行两档标签**(digest / ask)+ ask 参数 + 起始行(F-UC8)。
52
+ - **会话认领释放**(F-CR1/CR2):释放 + 拒绝零写入。
53
+
54
+ ### Changed
55
+
56
+ - **非阻塞启动**:异步会话 GC · 陈旧清扫 · traces 清理(启动不再被大扫除卡住)。
57
+ - **工程模式排除 `plan` 角色**(FR31——装配 / 命令 / 残留三面一致)。
58
+ - **包面**:README 补建(static 工具口径收正)+ LICENSE + npm 元数据;`files` 白名单不变。
59
+
60
+ ### Fixed
61
+
62
+ - 批别名残留(测试断言 / 提示词文档引用 / 错误串)· VSC 子代理块标题行与 CLI 对齐(标题段补全)。
63
+
64
+ ## [0.9.1] — 2026-09-21
65
+
66
+ > 核首发(2026-09 当月推导——registry 无号 ⇒ `0.<当月>.1`;首发即本段 ✗ 无更早历史——核内容此前散在两产品仓 ✗ 无独立版本号可考 ✗)。
67
+
68
+ ### Added
69
+
70
+ - **核包首发 `@thincoder/core@0.9.1`**:两产品共享机制收敛后的单一权威源——agent 执行环 / 子代理调度(spawn-gate · async 池 · notify_parent)· advisor 评审 · provider 层(SSE 流式 · thinking 映射 · 重试)· 记忆三层 · 工具系统 · 会话存储 · MCP 客户端 · git 集成 · 台账。
71
+ - **提示词与工具文档随包分发**:`prompts/` 15 档 + `tool-docs/` 24 档(一致性断言 D 对象——与 CLI 装机目录 / vsix 三处逐字相等)。
72
+ - **发布门禁**:`prepublishOnly` = `npm test`(与 CLI/VSC 门禁对称——2026-09-21 首发 补上)。
73
+
74
+ ### Changed
75
+
76
+ - 两产品 `dependencies` 同步收正 `^0.1.0 → ^0.9.1`(声明面与 registry 实发对齐——断言 A)。
package/README.md CHANGED
@@ -75,6 +75,7 @@ Version numbers are calendar-based (CalVer): `year.month.monthly-count`, where t
75
75
  2026 (`0` = 2026). `0.9.1` is therefore the first core release of September 2026.
76
76
 
77
77
  The number marks release time, not API compatibility — read `CHANGELOG.md` for what changed.
78
+ Per-release notes and artifacts: `CHANGELOG.md` in this package and the [GitHub Releases](https://github.com/xinbo-tech/thincoder/releases) page.
78
79
 
79
80
  ## Contributing
80
81
 
@@ -53,7 +53,9 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
53
53
  // pending item can't be resolved). After the single reminder the model is free
54
54
  // to finish — updating the task list (task tool) resets the budget, so a fresh
55
55
  // list state earns one fresh reminder.
56
- if (depth === 0 && agent.tasks.some((t) => t.status === "pending") && (agent._taskPushbacks ?? 0) < 1) {
56
+ // F10(催更门连带):工程模式下 task 工具机械停用(装配摘除 + execute 拒)⇒ 不再发指向被拒
57
+ // 工具的提醒(死胡同提醒防线);列表本体与 `_taskPushbacks` 预算语义零改。
58
+ if (depth === 0 && !agent.config?.agent?.engineering && agent.tasks.some((t) => t.status === "pending") && (agent._taskPushbacks ?? 0) < 1) {
57
59
  agent._taskPushbacks = (agent._taskPushbacks ?? 0) + 1
58
60
  const pending = agent.tasks.filter((t) => t.status === "pending").map((t) => t.title).join(", ")
59
61
  pushReal(agent, { role: "assistant", content: response.content })
@@ -7,7 +7,8 @@
7
7
  * consult 池来源)经 `decorate` 注入——**不传 = 核默认形态**(CLI = 迁出前逐字)。
8
8
  *
9
9
  * 返回 = 家族段数组(**非**工程模式:`[task, plan, timer, ...depth 家族 / 角色段]`;工程模式:
10
- * 固定段 = `[task, timer]`——plan 不入表,FR31 ① / KD8)——**不含** `agent.tools`
10
+ * 固定段 = `[timer]`——plan 不入表(FR31 ① / KD8)· task 不入表(F10——工程模式机械停用,
11
+ * 同 KD8 卸载处置))——**不含** `agent.tools`
11
12
  * 展开与 `extraTools`(调用点各自展开:核 `agent/setup.mjs` 两段式)。
12
13
  *
13
14
  * 登记册**动态**载入:`agent-tools.mjs` 静态图经 consult/subagent 族可达核 agent 栈
@@ -25,7 +26,7 @@ export async function assembleFamilyTools({
25
26
  decorate = null, // object 端差面:{ subagent?, consultStart?, consultStop?, settings? }
26
27
  } = {}) {
27
28
  // CORE-UNIFICATION TOOLS #83:consult 家族随统一登记册自 `../agent-tools.mjs` 取用(单一来源)
28
- const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool, engTool, readHistoryTool, batchTool, consultStartTool, consultStopTool, parentChannelTool } = await import("../agent-tools.mjs")
29
+ const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool, engTool, readHistoryTool, contextTool, batchTool, consultStartTool, consultStopTool, parentChannelTool } = await import("../agent-tools.mjs")
29
30
  // 写命令(主 agent 专用)——动态 import 且**仅 depth===0 载入**(ledger 链静态达 node:sqlite——
30
31
  // W8 契约②;子代理路径不注册 = 零载入——depth>0 解构得空、不引用即无副作用)
31
32
  const { ledgerAddTool, ledgerUpdateTool, ledgerCloseTool } = depth === 0 ? await import("../ledger.mjs") : {}
@@ -138,7 +139,7 @@ export async function assembleFamilyTools({
138
139
  : []
139
140
 
140
141
  const depthOnly = depth === 0
141
- ? [decorate?.subagent ?? filteredSubagent, skillTool, goalTool, engTool, verifyTool, recentChangesTool, readHistoryTool, advisorTool, batchTool(null),
142
+ ? [decorate?.subagent ?? filteredSubagent, skillTool, goalTool, engTool, verifyTool, recentChangesTool, readHistoryTool, contextTool, advisorTool, batchTool(null),
142
143
  ...consultTools,
143
144
  // 台账写命令(M2——仅主 agent;查询面 ledger_count 住基础集 tools/index.mjs)。
144
145
  // fail-closed:子代理不挂载 = 写面机械不可达。
@@ -162,7 +163,7 @@ export async function assembleFamilyTools({
162
163
  // 挂载形态已随单名化收口退役(过渡别名 §4.14 不入生产挂载面);depth-0 主 agent 同表挂载
163
164
  // `batch`(D-BR18 扩权——create/close + append §1/§4/§6 + status §1(轮 2 裁定②:§4/§6
164
165
  // 状态面走普通文档写),目标走可选 path / 在飞扫描)。
165
- // SUBAGENT-UPSTREAM-CHANNEL(AGENT-LOOP-SUBAGENT.md §6.27.4 装配接线):子代理上行通道
166
+ // SUBAGENT-UPSTREAM-CHANNEL(AGENT-LOOP-UPSTREAM.md §6.27.4 装配接线):子代理上行通道
166
167
  // (`notify_parent`)随 depth>0 段**前置**——4 处携带 = eng-coder / eng-designer / coder / 兜底段
167
168
  // (未列名 depth>0 role 落同一兜底段 ⇒ 亦装配;语义 =「depth>0 且非 consult 皆装配」);
168
169
  // 计数口径:`consult` 分支不入 ⇒ 「5 个插入点」读法已作废(实读 `thincoder-core/agent/family-tools.mjs:165-169`)。
@@ -173,12 +174,14 @@ export async function assembleFamilyTools({
173
174
  : role === "consult" ? [recentChangesTool]
174
175
  : [parentChannelTool]
175
176
 
176
- // 固定段(装配序 = agent.tools → 固定段 → 家族段 → extraTools):task/timer 全模式全深度;
177
- // plan 随模式位——ENG-PLAN-EXCLUSION(FR31 ① · KD8 卸载而非「注册 + 报错」· KD11 全深度两端):
178
- // plan **不入表**(模型不可见——看不见的选项不会被选);普通模式固定段逐字不变(FR31 边界)。
177
+ // 固定段(装配序 = agent.tools → 固定段 → 家族段 → extraTools):timer 全模式全深度;
178
+ // plan 随模式位——ENG-PLAN-EXCLUSION(FR31 ① · KD8 卸载而非「注册 + 报错」· KD11 全深度两端);
179
+ // task 随模式位——F10(task 工程模式机械停用;同 KD8 处置:**不入表**——模型不可见、零 token 税;
180
+ // execute 层另有机械拒兜底,见 `agent-tools/task.mjs`)。
181
+ // 工程模式固定段 = plan / task 皆**不入表**;普通模式固定段逐字不变(FR31 边界 / F10 边界)。
179
182
  // 判据 = 单一模式位 `engineering`(不带深度分支——子代理面随同排除,role enum
180
- // `:44-49` + spawn 门已同向)。
183
+ // `:44-49` + spawn 门 / task execute 门已同向)。
181
184
  return engineering
182
- ? [taskTool, timerTool, ...depthOnly]
185
+ ? [timerTool, ...depthOnly]
183
186
  : [taskTool, planTool, timerTool, ...depthOnly]
184
187
  }
package/agent/helpers.mjs CHANGED
@@ -386,13 +386,22 @@ export const ENG_OFF_REMINDER =
386
386
  "Changes go through the normal workflow: you may edit files directly, advisor/verify " +
387
387
  "guards apply per config.]"
388
388
 
389
- /** Manual-tier auto-turn digest domain (AGENT-LOOP.md §17 D-S6): organize-only.
389
+ /** Manual-tier auto-turn digest domain (normal-mode base engineering variant =
390
+ * AUTO_TURN_DIGEST_DOMAIN_ENG below): organize-only. Mechanism = AGENT-LOOP-ASYNC-POOL.md §6.8;
391
+ * mode variant text = TOOLS.md §6.15.3.
390
392
  * Injected per manual auto-turn run — writes/execute/spawns/questions are also
391
393
  * mechanically denied (no permission handler + spawn gate); this steers first. */
392
394
  export const AUTO_TURN_DIGEST_DOMAIN =
393
395
  "[System reminder: auto-turn — background async subagents finished while there was no user message, and this turn runs automatically to digest their reports (the finished-report reminders above). No one is waiting for this reply, so organize only: 1) summarize each finished report's key points into this conversation for the user to read later; 2) update the task list with the task tool (allowed) to mark finished work done; 3) write decision points with a suggested next step as text — do not execute it. FORBIDDEN this turn (mechanically enforced): modifying files, bash/execute/verify, spawning subagents, asking questions — those need a real user message. End the turn once the summaries are written.]"
394
396
 
395
- /** Up-stream wake-turn domain (AGENT-LOOP-SUBAGENT.md §6.27.12.8): a running subagent sent
397
+ /** Engineering-mode auto-turn digest domain variant (mode = `agent.config.agent.engineering`;
398
+ * clause 2 drops the task-tool pointer — F10: the task tool is disabled in engineering mode,
399
+ * the batch record + ledger are the tracking authority). Single line, verbatim from the
400
+ * design doc (TOOLS.md §6.15.3); mechanism = AGENT-LOOP-ASYNC-POOL.md §6.8. Content authority = parent side. */
401
+ export const AUTO_TURN_DIGEST_DOMAIN_ENG =
402
+ "[System reminder: auto-turn — background async subagents finished while there was no user message, and this turn runs automatically to digest their reports (the finished-report reminders above). No one is waiting for this reply, so organize only: 1) summarize each finished report's key points into this conversation for the user to read later; 2) (engineering mode: the task tool is disabled — no task-list update is expected this turn; the batch record + ledger are the tracking authority and are updated in real user turns); 3) write decision points with a suggested next step as text — do not execute it. FORBIDDEN this turn (mechanically enforced): modifying files, bash/execute/verify, spawning subagents, asking questions — those need a real user message. End the turn once the summaries are written.]"
403
+
404
+ /** Up-stream wake-turn domain (AGENT-LOOP-UPSTREAM.md §6.27.12.8): a running subagent sent
396
405
  * an in-flight message and is waiting for the reply — the digest domain's "No one is waiting
397
406
  * for this reply" is the opposite of the truth, so the wake turn gets its own text.
398
407
  * Verbatim from the design doc (single line — no newlines). Content authority = parent side. */
@@ -11,10 +11,10 @@ import { compressIfNeeded, compressFallback, COMPRESS_FAILURE_LIMIT } from "../c
11
11
  import { ensureAutoReminder, injectEngineeringReminder, ContinueError, snapshotGuard } from "./helpers.mjs"
12
12
  import { pushManifestStateReminder } from "./setup-reminders.mjs"
13
13
  import { cleanupConsultSessions } from "../agent-tools/consult.mjs"
14
- import { logEvent } from "../log.mjs"
14
+ import { logEvent, errText } from "../log.mjs"
15
15
  // ASYNC-RESULT-CONTAINER.md D1:池 accessor(absorb 双池——advisor 独立池无队列)
16
16
  import { getAsyncPool, releaseSettledEntry } from "../agent-tools/async-settle.mjs"
17
- // 批 4 CLI-ASYNC-DISCARD(AGENT-LOOP-SUBAGENT.md §6.20):中止分支「只清已死」收尾单点
17
+ // 批 4 CLI-ASYNC-DISCARD(AGENT-LOOP-ASYNC-POOL.md §6.20):中止分支「只清已死」收尾单点
18
18
  import { discardAbortedPool, discardAbortedAdvisors } from "../agent-tools/async-discard.mjs"
19
19
  // R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3a):回合末域登记 flush(写工具钩子累积 →
20
20
  // 整写一次本实例 peers 文件——无写入跳过;失败容忍不抛)
@@ -59,11 +59,28 @@ export function injectResponseReminders(agent, response) {
59
59
  * 计数复位(recentCallSigs——runAgent 回合级局部——经 ctx 传对象引用)/压缩完成事件
60
60
  * /AUTO 提醒重注入。失败:AbortError 透传;计数 + Q3 onCompressFail;
61
61
  * COMPRESS_FAILURE_LIMIT 连续失败降级 compressFallback(确定性截断——不发 LLM)。
62
+ * 模型主动压缩(`context` 工具)由本点消费(§6.16.2——取用即清槽;no-op 与失败各落一行注记)。
62
63
  */
63
64
  export async function runCompactionCheck(agent, ctx) {
64
65
  const { threshold, callbacks, compactionOverhead, signal, recentCallSigs } = ctx
66
+ // F-CC2(CONTEXT-COMPACTION.md §6.16.2):模型主动压缩(`context` 工具)在**本安全点**消费——
67
+ // 取用即清槽(失败不重放;重发由模型再调);`force` 只跳过阈值早退(模型判定优先于阈值),
68
+ // 其余全同(同 splitHistory / 同尾部预算 / 同摘要链 / 同回注 / 同基线失效)。
69
+ const pending = agent._pendingCompact
70
+ agent._pendingCompact = null
65
71
  try {
66
- if (await compressIfNeeded(agent, threshold, callbacks, compactionOverhead, signal)) {
72
+ let compacted = await compressIfNeeded(
73
+ agent, threshold, callbacks,
74
+ pending ? { ...compactionOverhead, force: true, focus: pending.focus } : compactionOverhead,
75
+ signal,
76
+ )
77
+ if (!compacted && pending) {
78
+ // 强制面无可压(短历史无中段)⇒ 记 no-op 注记后**照常**跑阈值面(阈值面不因模型请求让位)
79
+ const { compactNoopNote } = await import("../agent-tools/context.mjs")
80
+ agent.history.push({ role: "user", content: compactNoopNote(), transient: true })
81
+ compacted = await compressIfNeeded(agent, threshold, callbacks, compactionOverhead, signal)
82
+ }
83
+ if (compacted) {
67
84
  agent._compressFailures = 0
68
85
  agent._planReminderAtLen = 0 // After compression history shrinks, reset cadence so reminders resume
69
86
  recentCallSigs.length = 0 // After compression history is rebuilt, reset stall detection counter
@@ -79,6 +96,11 @@ export async function runCompactionCheck(agent, ctx) {
79
96
  // Q3 (CONTEXT-COMPACTION §7 D-C1): a failed compression is surfaced to the panel;
80
97
  // COMPRESS_FAILURE_LIMIT consecutive failures still degrade to compressFallback.
81
98
  callbacks?.onCompressFail?.(compressError)
99
+ if (pending) {
100
+ // 失败注记(§6.16.2 回执面②):模型面告知缺口——既有失败链 / 面板面零改,仅补一行机器注记
101
+ const { compactFailureNote } = await import("../agent-tools/context.mjs")
102
+ agent.history.push({ role: "user", content: compactFailureNote(errText(compressError), agent._compressFailures), transient: true })
103
+ }
82
104
  if (agent._compressFailures >= COMPRESS_FAILURE_LIMIT) {
83
105
  agent._compressFailures = 0
84
106
  if (compressFallback(agent)) callbacks.onCompress?.(agent._lastCompressInfo ?? {})
@@ -151,7 +173,7 @@ export async function finalizeAgentTurn(agent, ctx) {
151
173
  // driver aborts them on its own abort unwind.
152
174
  // Async subagent turn-end handling (AGENT-LOOP.md §15 D-A3 + §17 D-S1). Lifecycle:
153
175
  // - Ctrl+C (plain abort): children were aborted with the parent signal — discard dead
154
- // entries (tombstone/out-of-pool/notice — AGENT-LOOP-SUBAGENT.md §6.20; no stale
176
+ // entries (tombstone/out-of-pool/notice — AGENT-LOOP-ASYNC-POOL.md §6.20; no stale
155
177
  // errors injected — user explicitly stopped); consultation sessions are cross-turn
156
178
  // background work since R17 — the abort branch is the ONLY normal-path place that
157
179
  // aborts them (cleanupConsultSessions marks stopped → they never reach the digest).
@@ -205,7 +227,7 @@ export async function finalizeAgentTurn(agent, ctx) {
205
227
  }
206
228
 
207
229
  /**
208
- * Turn-end async subagent collection (AGENT-LOOP.md §17 D-S1 + §17.5 supersede):
230
+ * Turn-end async subagent collection (AGENT-LOOP-ASYNC-POOL.md §6.8 D-S1 + suspension supersede):
209
231
  * two modes, selected by the caller's driver context (17.5.2/17.5.4 #2):
210
232
  * - suspDriven=false (fallback — headless/direct runAgent callers without a
211
233
  * suspension driver): inject every entry that SETTLED during this run
package/agent/setup.mjs CHANGED
@@ -212,6 +212,12 @@ export async function prepareRun(agent, input, callbacks, {
212
212
  // let——下方按 projectRules / skills listing 尾部追加(L326/L331 +=)再赋值;
213
213
  // const 声明会 TypeError: Assignment to constant variable(每次 run 必炸)。
214
214
  let systemPrompt = base
215
+ // 派单固块(台账 #23 · AGENT-LOOP-SUBAGENT.md §6.26):spawn 级固定的机制性指令(批次档路径行 /
216
+ // 审计模板)随 system 面下发——固块值在 child 生命周期内恒定 ⇒ run 内前缀逐字节稳定(前缀缓存
217
+ // 不破),且不在 `agent.history` 内 ⇒ 压缩只重建 history(吞不掉)。拼接位 = 槽位装配之后、
218
+ // 项目指令之前;`_spawnSystemBlock` 为 null/undefined ⇒ 分支不进(depth-0 / explore / plan /
219
+ // consult 的 system 逐字节同改前)。
220
+ if (agent._spawnSystemBlock) systemPrompt += `\n\n${agent._spawnSystemBlock}`
215
221
 
216
222
  // Time injection deliberately does NOT live here: system prompts must be byte-identical
217
223
  // across runs (provider prefix caches). The time rides a transient user reminder per turn
@@ -10,9 +10,9 @@
10
10
  * 展开走 `docRootPaths`)。缺 `docRoot` 键 → `readManifest`
11
11
  * 的 fillDefaults 已补默认(架构 §2.3 E2 便利 fallback 落点);整档缺失 / 非法 JSON →
12
12
  * `{ok:false}` → 回退 DEFAULT_MANIFEST.docRoot(本函数是壳门之下的便利层——E2 的
13
- * 「整档缺失 拒进正常循环」由壳面 requireManifest 在启动期拦(**工程模式会话**口径——
14
- * 普通会话装配钩子零 manifest I/O,KD-M1-12):CLI make-agent.mjs / VSC
15
- * setup.mjs fail-closed);读错(权限等)→ 上抛不伪装成缺失(manifest.mjs 契约)。
13
+ * 「整档缺失」由壳面 requireManifest 按用点处置(判 + 报明 / 建档,**不抛**——
14
+ * 启动侧恒不拒,KD-M1-25;**工程模式会话**口径——普通会话装配钩子零 manifest I/O,
15
+ * KD-M1-12):CLI make-agent.mjs / VSC setup.mjs 同判);读错(权限等)→ 上抛不伪装成缺失(manifest.mjs 契约)。
16
16
  * M6 的 `advisor.mjs` design-review 分类分支消费本导出
17
17
  * (KD-M6-1——同源不重复实现;落 dispatch.mjs 会让 advisor 反向 import 门禁簇成环)。
18
18
  * 2. `freezeWindowConflict(agent, absPaths)` —— D5 冻结窗口判据组装:被审文件集 =
@@ -37,8 +37,8 @@ const REVIEW_ROOT_KEYS = ["requirements", "specs", "design", "modules", "batches
37
37
  * 评审目标解析单点(M4 §2.1#1):读 manifest `docRoot` → 评审对象 / 被审文件的绝对路径
38
38
  * 集合(目录级,反斜杠归一;子键值形态 = 串 | 多根数组,逐键经 `docRootPaths` 展开)。
39
39
  * 缺 `docRoot` 键 → readManifest 已补默认;整档缺失 / 非法 →
40
- * `{ok:false}` → 回退 DEFAULT_MANIFEST.docRoot(便利层——E2 的「拒进正常循环」由壳面
41
- * requireManifest 在启动期拦(**工程模式会话**口径——KD-M1-12),本函数不重复拦);
40
+ * `{ok:false}` → 回退 DEFAULT_MANIFEST.docRoot(便利层——E2 的「整档缺失」由壳面
41
+ * requireManifest 按用点处置(判 + 报明 / 建档,**不抛**——启动侧恒不拒,KD-M1-25;**工程模式会话**口径——KD-M1-12),本函数不重复拦);
42
42
  * 读错(权限等)→ 上抛 fail-closed。
43
43
  * @param {Object} agent
44
44
  * @returns {string[]} 评审目标目录的绝对路径(去重)
@@ -1,5 +1,5 @@
1
1
  /**
2
- * advisor-async.mjs — async advisor reviews (AGENT-LOOP-SUBAGENT.md §6.10 — R13, 2026-09-06).
2
+ * advisor-async.mjs — async advisor reviews (AGENT-LOOP-ASYNC-POOL.md §6.10 — R13, 2026-09-06).
3
3
  *
4
4
  * The advisor tool runs reviews in a background pool at depth 0 (default async —
5
5
  * ruling ②-3 A): the launch returns an ack, the turn ends naturally, the session
@@ -20,7 +20,7 @@
20
20
  * 3. the async pool (agent._asyncAdvisors — ADVISOR_POOL_LIMIT = 4 default,
21
21
  * agent.poolLimits.advisor configurable — POOL-CONFIG-UNIFIED; same-scope
22
22
  * running launches are refused at once — never queued (F-5); over-limit +
23
- * DIFFERENT scope launches QUEUE (ED-4 2026-09-16 — AGENT-LOOP-SUBAGENT.md §6.10:
23
+ * DIFFERENT scope launches QUEUE (ED-4 2026-09-16 — AGENT-LOOP-ASYNC-POOL.md §6.10:
24
24
  * agent._asyncAdvisorQueue, ack {queued, position}, slot release auto-start,
25
25
  * queued cancel dequeue+renumber — the former over-limit refusal ②-6a is
26
26
  * retired); cancel (ruling ②-6b — directed abort → cancelled settle: no
@@ -280,7 +280,7 @@ function relayAdvisorOutput(callbacks, prefix, chunk) {
280
280
  }
281
281
  }
282
282
 
283
- // ─── ED-4(2026-09-16 · AGENT-LOOP-SUBAGENT.md §6.10):评审池排队(槽释放自动起跑)────────
283
+ // ─── ED-4(2026-09-16 · AGENT-LOOP-ASYNC-POOL.md §6.10):评审池排队(槽释放自动起跑)────────
284
284
 
285
285
  /** 排队出队单点(ED-4):`_asyncAdvisorQueue` 剔除目标条目 + 余项 position 按 `1..n`
286
286
  * 重编号;读面经**载体吸收**(`carrierField`——#21⑤b):部分 parent(只携池 + `history`)
@@ -368,7 +368,7 @@ export function launchAsyncAdvisor(parent, ctx, launch) {
368
368
  const idFields = { type: reviewType, scope: scopeSummary(documents?.length ? documents : paths), round: `${(run?.round ?? 0) + 1}/uncapped`, criterion: "scope-in-flight" }
369
369
  return { error: withIdentityLine(`Advisor: 此 scope 已有评审在跑——settle 后逐个发起 — ${scopeNote}; round/prior continuation would be ambiguous while it is in flight — wait for it to settle, then launch the next review.`, idFields) }
370
370
  }
371
- // ED-4(AGENT-LOOP-SUBAGENT.md §6.10):池满 + 异 scope ⇒ 排队(非拒)——ack 含 queued +
371
+ // ED-4(AGENT-LOOP-ASYNC-POOL.md §6.10):池满 + 异 scope ⇒ 排队(非拒)——ack 含 queued +
372
372
  // position;running 计数只算 running 条目(排队不占槽)。原 ②-6a 拒发退役。
373
373
  const queued = runningAdvisorCount(parent) >= limit
374
374
  parent._asyncAdvisors ??= new Map()
@@ -215,7 +215,7 @@ export const advisorTool = {
215
215
  ? ";D5 冻结窗口:被审文档(含批次档)在报告送达前零写入——在途写入会被拒绝,写入将使本轮结算为陈旧 (pass 不发 token)"
216
216
  : ""
217
217
  if (ack.queued) {
218
- // ED-4(2026-09-16 · AGENT-LOOP-SUBAGENT.md §6.10):排队 ack——模型可见状态如实 queued +
218
+ // ED-4(2026-09-16 · AGENT-LOOP-ASYNC-POOL.md §6.10):排队 ack——模型可见状态如实 queued +
219
219
  // position(评审槽空自动启动——不误导模型等待即刻 digest)。
220
220
  return JSON.stringify({
221
221
  id: ack.id, kind: "advisor", status: "queued", position: ack.position,
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * async-discard.mjs — 中止清池的「只清已死」收尾单点(批 4 CLI-ASYNC-DISCARD——CLI 侧对称;
3
- * 设计权威 = `docs/core/design/AGENT-LOOP-SUBAGENT.md` §6.20,需求 = `docs/core/requirements/AGENT-LOOP.md` §4.10)。
3
+ * 设计权威 = `docs/core/design/AGENT-LOOP-ASYNC-POOL.md` §6.20,需求 = `docs/core/requirements/AGENT-LOOP.md` §4.10)。
4
4
  *
5
5
  * 原作 = 两接线点中止分支的无差别清池(`thincoder-core/agent/run-stages.mjs` 回合尾中止 ·
6
6
  * `thincoder-cli/src/tui/suspension-drive.mjs` 挂起会话中止)——`_asyncSubagents.clear()` +
@@ -0,0 +1,106 @@
1
+ /**
2
+ * audit-block.mjs — 审计块构造器(AGENT-LOOP-SUBAGENT.md §6.26 · 台账 #23)。
3
+ *
4
+ * 自 `subagent-spawn.mjs` **逐字搬移**(D23-3 外提:spawn 档越 300 软线 + 块构造成纯函数
5
+ * ⇒ 判据面可直测;D23-4:块内文本零改——含块内自指词,整块搬移不改真值)。本档承载
6
+ * spawn 级固定机制性指令的第二块(审计模板 + A2 任务书摘要 + touched 快照):
7
+ * 装配侧只收集(`thincoder-core/agent-tools/subagent-spawn.mjs`),下发面 = system 固块
8
+ * (`child._spawnSystemBlock` → 核 `prepareRun` 拼接——`thincoder-core/agent/setup.mjs`)。
9
+ */
10
+
11
+ /**
12
+ * §18.7 D-TS5 (A2): mechanically summarize the parent spawn task book for the
13
+ * audit spawn — the three audit-relevant elements VERBATIM (design doc paths /
14
+ * affected-file list / acceptance criteria); verbose context/background is
15
+ * dropped (the auditor can read the design docs themselves — they stay
16
+ * available outside this input). Independence preserved: the input is
17
+ * _engTaskInput (mechanically kept by the parent spawn) — never the
18
+ * eng-coder's self-report. Sections are located by header marker, prioritizing
19
+ * header lines (structured task books: "## 文件清单 …") and falling back to
20
+ * inline markers (flat one-line task books); a section runs to the next header
21
+ * of the SAME OR HIGHER level ("## 文件清单" survives a "### 修改" sub-header).
22
+ * Marker not found → the section is reported as missing (never fabricate).
23
+ */
24
+ export function summarizeEngTaskBook(taskInput) {
25
+ if (!taskInput) return "(unavailable)"
26
+ const SECTIONS = [
27
+ { name: "Design docs involved", markers: [/Docs? involved/i, /涉及文档/] },
28
+ { name: "Affected-file list", markers: [/Files? (?:list|to (?:modify|change)|modified)/i, /受影响文件/, /文件清单/, /涉及文件/] },
29
+ { name: "Acceptance criteria", markers: [/Acceptance(?: criteria)?/i, /验收标准/] },
30
+ ]
31
+ const lines = taskInput.split("\n")
32
+ const headerLevel = (l) => {
33
+ const m = l.match(/^\s*(#{1,6})\s/)
34
+ return m ? m[1].length : 0
35
+ }
36
+ const headerIdx = lines.map((l, i) => (headerLevel(l) > 0 ? i : -1)).filter((i) => i >= 0)
37
+ const boundsFor = (from, level) => {
38
+ for (const j of headerIdx) {
39
+ if (j > from && (level === 0 || headerLevel(lines[j]) <= level)) return j
40
+ }
41
+ return lines.length
42
+ }
43
+ const out = []
44
+ for (const { name, markers } of SECTIONS) {
45
+ let from = -1
46
+ let level = 0
47
+ for (const i of headerIdx) {
48
+ if (markers.some((m) => m.test(lines[i]))) { from = i; level = headerLevel(lines[i]); break }
49
+ }
50
+ if (from === -1) {
51
+ for (let i = 0; i < lines.length; i++) {
52
+ if (markers.some((m) => m.test(lines[i]))) { from = i; level = 0; break }
53
+ }
54
+ }
55
+ if (from === -1) { out.push(`${name}: (not found in the parent task book)`); continue }
56
+ const body = lines.slice(from, boundsFor(from, level)).join("\n").trim()
57
+ out.push(body || `${name}: (empty section)`)
58
+ }
59
+ return out.join("\n\n")
60
+ }
61
+
62
+ /**
63
+ * 审计块(spawn 级固定 ⇒ system 固块——§6.26 分类裁定表第 2 行):纯函数——touched 快照
64
+ * (`ctx.agent._touchedFiles`——机制面证据,非 eng-coder 自述)+ A2 任务书摘要
65
+ * (`summarizeEngTaskBook(ctx.agent._engTaskInput)`)+ 指令 / 范围 / 零 git 权威 / 预算 /
66
+ * 报告格式五段模板**逐字**。零副作用(只读 ctx);块内文本与搬移前逐字节一致。
67
+ * @param {{ agent?: { _touchedFiles?: string[], _engTaskInput?: string } }} ctx
68
+ * @returns {string} 块文本(不含与其它块之间的分隔符——分隔由装配侧 `join("\n\n")` 施加)
69
+ */
70
+ export function buildAuditBlock(ctx) {
71
+ const touched = (ctx.agent._touchedFiles ?? []).map((f) => `- ${f}`).join("\n") || "- (none yet)"
72
+ return `[Audit scope — mechanical context, independent of the eng-coder's self-report:]\n` +
73
+ // §18.7 D-TS4 A1:审计指令模板(四类偏差 + 范围限制 + 校验清单格式)——审计语义
74
+ // 不再靠模型自悟;范围限制是 §18.5 D-AG3 声明(下方 Zero-git scope authority)
75
+ // 的同源一句指注,不重复声明。
76
+ `[Audit instructions — mechanical template:]\n` +
77
+ `You are auditing an eng-coder delivery against its approved design — audit for EXACTLY these four deviation categories:\n` +
78
+ `- PARTIAL: an acceptance criterion implemented partially or not at all;\n` +
79
+ `- SILENT-SIMPLIFICATION: a "simpler approximation" of a specified behavior substituted for the spec;\n` +
80
+ `- DOC-DRIFT: code changed without the owning design-doc section (module map / affected-files table) updated in the same delivery;\n` +
81
+ `- OUT-OF-LIST: changes outside the approved file list.\n` +
82
+ `Audit scope = _touchedFiles above UNION the files confirmed by the parent task book (single source — the Zero-git scope authority note below; NOT a second copy): ` +
83
+ `workspace changes not listed there are unrelated to this delivery and are NOT grounds for an out-of-list finding.\n` +
84
+ `Scope discipline (F-TS6 A1): read ONLY the audited files and the design-doc sections relevant to this delivery — do NOT re-read whole documents.\n` +
85
+ `Every deviation item MUST be fieldized: file:line + design reference (doc path + section/AC id) + severity + evidence (quoted code or doc text).\n` +
86
+ // §18.7 D-TS5 A2:任务书从全量 verbatim 改机械摘要块(三要素逐字——排除冗长上下文)。
87
+ `[Parent spawn task book — mechanical summary: design docs + affected-file list + acceptance criteria verbatim; verbose context/background dropped — the design docs are still available for reading outside this input:]\n` +
88
+ `${summarizeEngTaskBook(ctx.agent._engTaskInput)}\n` +
89
+ `Files actually touched by the eng-coder (mechanical union — audit these against the file list):\n${touched}\n` +
90
+ // §18.5 D-AG3(2026-09-04):审计零 git 范围权威声明——本审计任务零 git(不注入
91
+ // git 上下文——§18.5 全角色零 git);_touchedFiles 为审计范围;工作区未列于
92
+ // _touchedFiles 的改动与本任务无关,不作超清单依据(VS Code auditTaskBook 同款措辞)。
93
+ "Zero-git scope authority: this audit task receives NO git context — nothing is injected. " +
94
+ "The evidence base is the design documents, the current disk state (read/glob/grep), and the _touchedFiles list above. " +
95
+ "Workspace changes NOT listed in _touchedFiles are unrelated to this delivery — they are NOT grounds for an out-of-file-list finding." +
96
+ // §18.13 D-A1.2:审计预算句——A1 指令模板 + A2 摘要块之后、A3 报告模板之前(定序——评审 #7)。
97
+ // 逐字设计锚(D-A1.2 代码块):只读该读的——10 轮机械预算——超时报 PROBLEM 下结论。
98
+ // 前导 \n 与 A3 同款块分隔约定(上一句 Zero-git 句末无换行——不触碰既有句)。
99
+ `\n[Audit budget — mechanical]: read ONLY the touched files listed above and the design-doc sections the parent task book names (affected-files table, acceptance criteria, status line). Do NOT read whole documents. Budget = 10 tool rounds max — if you cannot conclude within it, report PROBLEM (inconclusive) rather than continuing to explore.\n` +
100
+ // §18.7 D-TS6 A3:审计输出报告格式模板(三态——字段化行——不让模型自由发挥)。
101
+ `\n[Audit report format — mechanical template:]\n` +
102
+ `Report EXACTLY one of three states:\n` +
103
+ `- CLEAN — no deviation across the four categories: reply the line "Four deviation categories: none found." (四类偏差均未发现);\n` +
104
+ `- DEVIATIONS — one row per deviation, every row fieldized: | category | file:line | design reference | severity | evidence |;\n` +
105
+ `- PROBLEM — the audit itself could not run / inconclusive: state what blocked it.\n`
106
+ }
@@ -4,7 +4,8 @@
4
4
  * create(建档,§4.11)/ status(段属主状态行流转,§4.12)/ close(收口冻结,§4.13)+
5
5
  * depth-0 在飞批定位(findInFlightBatch——D-BR21)。判定字面全部单源自 batch-skeleton.mjs
6
6
  * (SEGMENT_BY_ROLE / STATUS_WORDS / STATUS_LINE_RE / readBatchStatusLine / sectionHeaderRe /
7
- * batchSkeleton);路径解析单源(resolveBatchDocPath / batchDocBases)与 #84 记账缝住 batch.mjs
7
+ * batchSkeleton / TEMPLATE_PLACEHOLDERS / findPlaceholderResidue / placeholderResidueError);
8
+ * 路径解析单源(resolveBatchDocPath / batchDocBases)与 #84 记账缝住 batch.mjs
8
9
  * 主档——**依赖单向(KD-4):skeleton ← lifecycle ← 主档**,主档把解析后的 cwd/bases/目标闭包
9
10
  * 传进来,本档不回 import 主档(防环)。
10
11
  *
@@ -19,6 +20,7 @@ import { dirname, isAbsolute, resolve, sep } from "node:path"
19
20
 
20
21
  import {
21
22
  SEGMENT_BY_ROLE, STATUS_WORDS, STATUS_LINE_RE, readBatchStatusLine, sectionHeaderRe, batchSkeleton,
23
+ TEMPLATE_PLACEHOLDERS, findPlaceholderResidue, placeholderResidueError,
22
24
  } from "./batch-skeleton.mjs"
23
25
 
24
26
  /** 可读文件判据(存在且为文件——目录/缺失同判不可读;与主档同名 helper 同型——KD-4 单向
@@ -100,6 +102,8 @@ function assertInsideBases(abs, bases, raw) {
100
102
  * create——建档(§4.11,仅 depth-0)。六段骨架一次预齐(骨架模板单源 = batchSkeleton),
101
103
  * 建档即过 gate(§1 占位状态行含「进行中」);fail-closed:非 .md / 越基底 / 目标已存在 ⇒ throw
102
104
  * (不覆盖既有批档,BR-20);目录缺失 ⇒ mkdir -p 后落位(BR-25)。不代建台账条目。
105
+ * F11-B:`source` **必填**(topic 同款空拒——骨架编制行实参化;不传 = 死占位残留必被 F11-C 拒
106
+ * ⇒ create 即拒,不留死锁);`prev` 传入值**幂等剥**「前情 = 」前缀(值规范化;默认值零变)。
103
107
  * @returns {string} 成功消息(含落盘绝对路径)
104
108
  */
105
109
  export function createBatchRecord({ args, ctx, review, cwd, bases, onWritten }) {
@@ -124,10 +128,20 @@ export function createBatchRecord({ args, ctx, review, cwd, bases, onWritten })
124
128
  if (!topic) {
125
129
  throw new Error("batch: create requires topic — the batch subject word shared by the record header (档名与档头共用——keep the file name aligned with it). Nothing was written.")
126
130
  }
131
+ const source = typeof args?.source === "string" ? args.source.trim() : ""
132
+ if (!source) {
133
+ throw new Error("batch: create requires source — the record's origin line (来源 = …). Pass where this batch came from (the request / discussion that started it); it lands in the header's 编制 line. Nothing was written.")
134
+ }
135
+ if (/\r?\n/.test(source)) {
136
+ throw new Error("batch: create source must be a single line — a multi-line source would break the header's 编制 line. Nothing was written.")
137
+ }
127
138
  const date = typeof args?.date === "string" && args.date.trim() ? args.date.trim() : todayLocal()
128
- const prev = typeof args?.prev === "string" && args.prev.trim() ? args.prev.trim() : "无(独立批)"
139
+ // F11-B:prev 幂等 strip——调用方自带「前情 = 」前缀(单或重复)一并剥净(`+` 量词;归一化目的
140
+ // = 值规范化,无前缀值原样通过)。默认值形态「无(独立批)」零变。
141
+ const prevRaw = typeof args?.prev === "string" ? args.prev.trim() : ""
142
+ const prev = prevRaw ? (prevRaw.replace(/^(?:前情\s*[=::]\s*)+/g, "").trim() || "无(独立批)") : "无(独立批)"
129
143
  mkdirSync(dirname(abs), { recursive: true })
130
- writeFileSync(abs, batchSkeleton({ date, topic, prev }))
144
+ writeFileSync(abs, batchSkeleton({ date, topic, source, prev }))
131
145
  onWritten?.(ctx?.agent ?? {}, abs)
132
146
  return `batch: created ${abs} — six-section skeleton written (§1 status line carries the gate-legal 「进行中」 placeholder; register the ledger entry yourself — create does not).`
133
147
  }
@@ -168,30 +182,65 @@ function assertGateOpen(src) {
168
182
  }
169
183
  }
170
184
 
171
- /** status 值域校验(fix #1 + 词面纪律):值在**全词表 union**(各段项去重并集——含他段
172
- * 生命周期词与 §1 gate 词对)中必须恰命中一个关键字,且该关键字属于本段词表项——0 个 ⇒ 词表外
173
- * 拒;≥2 混词拒;唯一命中不属本段 ⇒ 词表外拒(该段值域不含)。「进行中…已收口」双词无论
174
- * 写哪段皆拒(最小词面:已收口优先误冻结防线——2026-09-20 词面纪律)。 */
185
+ /** 全词表去重并集(各段项——value 命中判据与 note 零命判据共用同一口径)。 */
186
+ function statusVocabulary() {
187
+ return [...new Set(Object.values(STATUS_WORDS).flatMap((w) => Object.values(w)))]
188
+ }
189
+
190
+ /** 状态行值装饰白名单(F11-A 谓词收紧):首尾非字母数字符号(emoji / 标点 / 空白)+ 尾部
191
+ * ISO 日期(YYYY-MM-DD)+ 首尾空白。剥白名单后**余核必须逐字等于**该关键词——余核 ≠ 关键词
192
+ * (散文内嵌)⇒ 拒(散文说明走独立 `note` 字段落括注)。
193
+ * 读侧(gate)零变:`readBatchStatusLine` / `assertGateOpen` 仍子串包含——冻结门语义不动;
194
+ * 收紧只在写入面 value。 */
195
+ const LEAD_DECOR_RE = /^[^\p{L}\p{N}]+/u
196
+ const TAIL_DECOR_RE = /[^\p{L}\p{N}]+$/u
197
+ const ISO_TAIL_RE = /\d{4}-\d{2}-\d{2}\s*$/
198
+ function stripDecorations(value) {
199
+ return value.replace(LEAD_DECOR_RE, "").replace(ISO_TAIL_RE, "").replace(TAIL_DECOR_RE, "")
200
+ }
201
+
202
+ /** status 值域校验(fix 轮 #1 + 词面纪律 + 本批 F11-A 谓词收紧):值在**全词表 union**中必须恰
203
+ * 命中一个关键字(判读序不动:≥2 命中 ⇒ 混词拒在前;0 命中 / 唯一命中不属本段 ⇒ 词表外拒),
204
+ * 且剥装饰白名单后**余核逐字 = 该关键词**(余核 ≠ 关键词 = 散文内嵌 ⇒ 词表外拒同串)。
205
+ * 「进行中…已收口」双词无论写哪段皆拒(最小词面:已收口优先误冻结防线——2026-09-20 词面纪律)。 */
175
206
  function assertStatusValue(seg, value) {
176
207
  const words = Object.values(STATUS_WORDS[seg] ?? {})
177
208
  if (!words.length) {
178
209
  throw new Error(`batch: §${seg} has no status word list — status is undefined for this section (STATUS_WORDS 分段词表无该项). Nothing was written.`)
179
210
  }
180
- const vocabulary = [...new Set(Object.values(STATUS_WORDS).flatMap((w) => Object.values(w)))]
181
- const hits = vocabulary.filter((w) => value.includes(w))
182
- if (hits.length === 0 || (hits.length === 1 && !words.includes(hits[0]))) {
183
- throw new Error(`batch: status value ${JSON.stringify(value)} is not in the legal keyword set for §${seg} (${words.join(" / ")}) — STATUS_WORDS 分段词表是唯一值域(D-BR19). Nothing was written.`)
184
- }
211
+ const hits = statusVocabulary().filter((w) => value.includes(w))
185
212
  if (hits.length > 1) {
186
213
  throw new Error(`batch: status value contains multiple keywords (${hits.join(" + ")}) — one status line carries exactly ONE keyword (词面纪律: mixed values mis-freeze via 已收口-priority). Nothing was written.`)
187
214
  }
215
+ if (hits.length === 0 || !words.includes(hits[0]) || stripDecorations(value) !== hits[0]) {
216
+ throw new Error(`batch: status value ${JSON.stringify(value)} is not in the legal keyword set for §${seg} (${words.join(" / ")}) — STATUS_WORDS 分段词表是唯一值域(D-BR19;合法值 = 恰一关键词 + 装饰白名单〔首尾符号 / 尾部 ISO 日期 / 空白〕,散文说明走 note 字段落括注). Nothing was written.`)
217
+ }
218
+ }
219
+
220
+ /** status `note` 字段校验(F11-A——落盘形态 = 状态行行内括注):单行 + **全词表零命中**
221
+ * (括注永不误触冻结门——§1 判定是子串包含;已收口优先误冻结防线的第二道)+ 死占位判据零命中
222
+ * (括注不得成为骨架占位的新驻留面)。 */
223
+ function assertStatusNote(note) {
224
+ if (/\r?\n/.test(note)) {
225
+ throw new Error("batch: status note must be a single line — a multi-line note would break the one-line status-line form. Nothing was written.")
226
+ }
227
+ const hits = statusVocabulary().filter((w) => note.includes(w))
228
+ if (hits.length) {
229
+ throw new Error(`batch: status note must not contain any STATUS_WORDS keyword (${hits.join(" + ")}) — a parenthetical carrying a keyword would be read as gate truth (§1 substring predicate). Put the keyword in value, the prose in note. Nothing was written.`)
230
+ }
231
+ const residues = TEMPLATE_PLACEHOLDERS.filter((ph) => note.includes(ph))
232
+ if (residues.length) {
233
+ throw new Error(`batch: status note must not carry skeleton placeholders (${residues.join(" · ")}) — fill them in the record header instead of parking them in the status line. Nothing was written.`)
234
+ }
188
235
  }
189
236
 
190
237
  /**
191
238
  * status——状态行流转(§4.12,段属主)。写域 = 调用者自己段内 `**状态行**:` 行(eng-designer →
192
239
  * §2 · 评审 → §3 · eng-coder → §5 · 主 agent → §1——轮 2 #3 裁定②);值域 = STATUS_WORDS 该段项
193
- * 恰一词;冻结真值不变(gate 只读 §1)。path 参数 = 仅 depth-0(D-BR21)——子代理/评审传 path
194
- * 拒(目标 = spawn/实例注入,语法上写不到别处)。
240
+ * 恰一词(**余核 = 关键词**——F11-A 谓词收紧),散文说明走独立 `note` 字段(落状态行括注);
241
+ * 冻结真值不变(gate 只读 §1)。path 参数 = 仅 depth-0(D-BR21)——子代理/评审传 path
242
+ * ⇒ 拒(目标 = spawn/实例注入,语法上写不到别处)。F11-C 挂点 = 写盘前(assertStatusValue 后):
243
+ * 档头 / 目标段含骨架死占位 ⇒ 拒(close 不拦——收口是主 agent 终态动作)。
195
244
  * @returns {string} 成功消息
196
245
  */
197
246
  export function statusBatchRecord({ args, ctx, review, pickTarget, onWritten }) {
@@ -216,14 +265,21 @@ export function statusBatchRecord({ args, ctx, review, pickTarget, onWritten })
216
265
  if (/\r?\n/.test(value)) {
217
266
  throw new Error("batch: status value must be a single line — a multi-line value would break the one-line status-line form. Nothing was written.")
218
267
  }
268
+ // note(可选):散文说明的承载面——落状态行行内括注(value 内散文不再容忍)。缺省 / 空串 = 无括注。
269
+ const note = typeof args?.note === "string" ? args.note.trim() : ""
219
270
  assertStatusValue(seg, value)
271
+ if (note) assertStatusNote(note)
272
+ const lineValue = note ? `${value}(${note})` : value
220
273
  const abs = pickTarget(args?.path)
221
274
  const src = readFileSync(abs, "utf8")
222
275
  assertGateOpen(src)
223
- const written = updateSectionStatusLine(src, seg, value)
276
+ // F11-C(写盘前):档头 + 本段死占位残留 拒(他段占位不归本段作者管)
277
+ const residues = findPlaceholderResidue(src, seg)
278
+ if (residues.length) throw new Error(placeholderResidueError(residues))
279
+ const written = updateSectionStatusLine(src, seg, lineValue)
224
280
  writeFileSync(abs, written)
225
281
  onWritten?.(ctx?.agent ?? {}, abs)
226
- return `batch: §${seg} status line updated to ${JSON.stringify(value)} — the freeze gate (§1) is a read-only domain for non-§1 writes (frozen truth stays = the §1 line).`
282
+ return `batch: §${seg} status line updated to ${JSON.stringify(lineValue)} — the freeze gate (§1) is a read-only domain for non-§1 writes (frozen truth stays = the §1 line).`
227
283
  }
228
284
 
229
285
  /**