@thincoder/core 0.10.4 → 0.10.6

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 (153) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +1 -1
  3. package/advisor/compaction.mjs +11 -6
  4. package/advisor/run.mjs +4 -4
  5. package/agent/assemble.mjs +1 -1
  6. package/agent/chat-call.mjs +1 -1
  7. package/agent/child-marks.mjs +2 -2
  8. package/agent/digest-account.mjs +5 -4
  9. package/agent/dispatch-gates.mjs +8 -0
  10. package/agent/dispatch.mjs +39 -18
  11. package/agent/family-tools.mjs +22 -11
  12. package/agent/helpers.mjs +4 -3
  13. package/agent/light-round.mjs +66 -0
  14. package/agent/post-turn.mjs +8 -4
  15. package/agent/run-stages.mjs +5 -3
  16. package/agent/run-start.mjs +3 -2
  17. package/agent/setup-reminders.mjs +1 -1
  18. package/agent/setup.mjs +18 -3
  19. package/agent/subagent-event.mjs +1 -1
  20. package/agent/suspension.mjs +8 -5
  21. package/agent/timers.mjs +56 -5
  22. package/agent/turn-loop.mjs +5 -3
  23. package/agent/write-gate.mjs +7 -7
  24. package/agent-tools/advisor-async.mjs +2 -2
  25. package/agent-tools/advisor-settle.mjs +2 -2
  26. package/agent-tools/advisor.mjs +1 -1
  27. package/agent-tools/async-discard.mjs +34 -0
  28. package/agent-tools/async-settle.mjs +3 -2
  29. package/agent-tools/audit-block.mjs +1 -1
  30. package/agent-tools/bash-async.mjs +1 -1
  31. package/agent-tools/batch-lifecycle.mjs +35 -14
  32. package/agent-tools/batch-paths.mjs +4 -4
  33. package/agent-tools/batch-skeleton.mjs +29 -11
  34. package/agent-tools/batch.mjs +28 -15
  35. package/agent-tools/browser-async.mjs +198 -0
  36. package/agent-tools/checkpoint.mjs +1 -1
  37. package/agent-tools/design-slots.mjs +232 -0
  38. package/agent-tools/digest-budget.mjs +1 -1
  39. package/agent-tools/manifest.mjs +193 -0
  40. package/agent-tools/parent-channel.mjs +7 -4
  41. package/agent-tools/review-facts.mjs +13 -13
  42. package/agent-tools/settings.mjs +1 -1
  43. package/agent-tools/skill.mjs +1 -1
  44. package/agent-tools/subagent-async.mjs +21 -12
  45. package/agent-tools/subagent-scheduler.mjs +8 -5
  46. package/agent-tools/subagent-spawn.mjs +29 -55
  47. package/agent-tools/subagent.mjs +21 -8
  48. package/agent-tools/timer.mjs +186 -12
  49. package/agent-tools/zero-write-watch.mjs +2 -2
  50. package/agent-tools.mjs +2 -1
  51. package/agent.mjs +1 -1
  52. package/attachments.mjs +1 -1
  53. package/browser/actions.mjs +163 -0
  54. package/browser/cdp.mjs +161 -0
  55. package/browser/clipboard.mjs +142 -0
  56. package/browser/input-actions.mjs +197 -0
  57. package/browser/input.mjs +256 -0
  58. package/browser/launch.mjs +143 -0
  59. package/browser/queue.mjs +171 -0
  60. package/browser/session.mjs +494 -0
  61. package/browser/snapshot.mjs +270 -0
  62. package/config-io.mjs +18 -13
  63. package/config-migrate.mjs +13 -29
  64. package/config-presets.mjs +33 -29
  65. package/config.mjs +68 -25
  66. package/context-degrade.mjs +1 -1
  67. package/context-echo.mjs +1 -1
  68. package/context-push.mjs +2 -2
  69. package/context.mjs +1 -1
  70. package/conventions.mjs +2 -2
  71. package/declaration.mjs +18 -2
  72. package/file-links.mjs +1 -1
  73. package/file-refs.mjs +1 -1
  74. package/history-window.mjs +1 -1
  75. package/i18n.mjs +2 -0
  76. package/ledger-cmd.mjs +62 -16
  77. package/ledger-db.mjs +6 -0
  78. package/ledger-surface.mjs +49 -5
  79. package/ledger-tools.mjs +16 -8
  80. package/ledger.mjs +9 -16
  81. package/log.mjs +1 -1
  82. package/manifest-discovery.mjs +1 -1
  83. package/manifest-schema.mjs +1 -1
  84. package/mcp/transport-stdio.mjs +1 -1
  85. package/mcp.mjs +1 -1
  86. package/memory/code-index.mjs +1 -1
  87. package/memory/code-search.mjs +1 -1
  88. package/memory/code-sync.mjs +1 -1
  89. package/memory/docs.mjs +6 -2
  90. package/memory/file-list.mjs +1 -1
  91. package/memory/file-walk.mjs +17 -5
  92. package/memory/maintain.mjs +410 -0
  93. package/memory/memory-tool.mjs +21 -6
  94. package/memory/schema.mjs +19 -7
  95. package/memory/sweep.mjs +8 -5
  96. package/memory/sync-tail.mjs +16 -10
  97. package/model-ref.mjs +16 -13
  98. package/model-specs.mjs +21 -9
  99. package/package.json +2 -1
  100. package/peer-instances.mjs +1 -1
  101. package/permission.mjs +33 -0
  102. package/process-probe.mjs +2 -2
  103. package/prompts/advisor-design.md +4 -2
  104. package/prompts/advisor-round1.md +1 -1
  105. package/prompts/advisor-round2.md +1 -1
  106. package/prompts/advisor-round3.md +1 -1
  107. package/prompts/common.md +36 -2
  108. package/prompts/consult-base.md +1 -1
  109. package/prompts/discipline-engineering.md +32 -15
  110. package/prompts/discipline-normal.md +46 -2
  111. package/prompts/persona-eng-designer.md +6 -6
  112. package/prompts/persona-engineering.md +14 -6
  113. package/prompts/persona-normal.md +2 -0
  114. package/prompts/subagent-base.md +3 -0
  115. package/provider/core.mjs +6 -0
  116. package/provider/errors.mjs +5 -3
  117. package/provider/google.mjs +9 -8
  118. package/provider/list-models.mjs +3 -2
  119. package/provider/sse.mjs +3 -0
  120. package/provider-flows.mjs +14 -15
  121. package/proxy-chunked.mjs +102 -0
  122. package/proxy-target.mjs +54 -0
  123. package/proxy-transport.mjs +298 -0
  124. package/proxy.mjs +17 -248
  125. package/session-gc.mjs +2 -2
  126. package/session-lifecycle.mjs +18 -14
  127. package/session-slot-claims.mjs +2 -2
  128. package/session-slots-manifest.mjs +1 -1
  129. package/session-slots.mjs +3 -3
  130. package/team-ledger.mjs +282 -0
  131. package/team.mjs +223 -0
  132. package/tool-docs/bash.md +1 -0
  133. package/tool-docs/batch.md +1 -1
  134. package/tool-docs/browser.md +48 -0
  135. package/tool-docs/ledger.md +3 -3
  136. package/tool-docs/manifest.md +16 -0
  137. package/tool-docs/memory.md +1 -0
  138. package/tool-docs/notify_parent.md +5 -0
  139. package/tool-docs/process.md +4 -4
  140. package/tool-docs/question.md +1 -0
  141. package/tool-docs/subagent.md +4 -3
  142. package/tool-docs/team_ledger.md +9 -0
  143. package/tool-docs/timer.md +6 -0
  144. package/tool-docs/wait_for.md +1 -0
  145. package/tools/browser.mjs +161 -0
  146. package/tools/git.mjs +1 -1
  147. package/tools/index.mjs +3 -0
  148. package/tools/ops.mjs +26 -10
  149. package/tools/repomap.mjs +1 -1
  150. package/tools/write-path.mjs +2 -2
  151. package/traces/trace-cleanup.mjs +1 -1
  152. package/traces/trace-store.mjs +1 -1
  153. package/vision-reader.mjs +10 -16
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * 载体注入(端差面 = 池 / pending / 标志挂谁):`ctx.carrier` = 字段集按核内异步面
12
12
  * 现行口径的对象(`_asyncSubagents` · `_asyncAdvisors` · `_consultSessions` ·
13
- * `_pendingAsyncResults` · `_suspended`;#9 后台 bash 任务池 `_bgTasks` 同列;
13
+ * `_pendingAsyncResults` · `_suspended`;#9 后台 bash 任务池 `_bgTasks` ∥ 批 browser-async-fix 池 `_browserTasks` 同列;
14
14
  * 消化账务批 · 2026-10-05 同列 `_daSession`(挂起会话标记——置位 / 复位单点)与
15
15
  * `_unsettledDigests`(升级账本——中止清场))
16
16
  * ——CLI 形 = `agent` 对象;VSC 形 = depth-0
@@ -59,12 +59,14 @@ function consultRunningChildren(carrier) {
59
59
  /** 后台池存活判据(D-S2/F5 口径——CLI 为准):running/queued 的池条目,或已 settle 未注入
60
60
  * (pending 非空 = D-S3「未注入」),或运行中 consult children(会诊跨回合)。回合尾与每次
61
61
  * 轮末都评估退出。**#9 三域并入**:`_bgTasks`(后台 bash 任务)在途亦为 live——settle 经
62
- * pending 停靠驱动消化轮(`bash-async.mjs` 恒停靠)。 */
62
+ * pending 停靠驱动消化轮(`bash-async.mjs` 恒停靠);**批 browser-async-fix 第四域**:
63
+ * `_browserTasks` 同判(`browser-async.mjs` 恒停靠)。 */
63
64
  export function poolLive(carrier) {
64
65
  const sub = carrier?._asyncSubagents
65
66
  const adv = carrier?._asyncAdvisors
66
67
  const bg = carrier?._bgTasks
67
- return (sub && sub.size > 0) || (adv && adv.size > 0) || (bg && bg.size > 0)
68
+ const br = carrier?._browserTasks
69
+ return (sub && sub.size > 0) || (adv && adv.size > 0) || (bg && bg.size > 0) || (br && br.size > 0)
68
70
  || (carrier?._pendingAsyncResults?.length ?? 0) > 0
69
71
  || consultRunningChildren(carrier) > 0
70
72
  }
@@ -111,7 +113,7 @@ function consultLiveCount(carrier) {
111
113
  }
112
114
 
113
115
  /**
114
- * 退出清场(单点——驱动 finally 与单测共用):
116
+ * 退出清场(单点——驱动 finally 与单元测试共用):
115
117
  * - abort:**只清已死**(§6.20 口径——增补 D:`discardAbortedPool` ∕ `discardAbortedAdvisors`
116
118
  * 单点,与核 run-stages 回合尾中止同式;存活 ∕ 已 settle 条目留池——settled 报告沿后续
117
119
  * 回合边界注入到达,不静默丢)+ pending 单容器清 + 会诊会话清理标记;
@@ -128,10 +130,11 @@ function consultLiveCount(carrier) {
128
130
  */
129
131
  export async function finishSuspension(carrier, { aborted = false, injectResidual = null } = {}) {
130
132
  if (aborted) {
131
- const { discardAbortedPool, discardAbortedAdvisors, discardAbortedBgTasks } = await import("../agent-tools/async-discard.mjs")
133
+ const { discardAbortedPool, discardAbortedAdvisors, discardAbortedBgTasks, discardAbortedBrowserTasks } = await import("../agent-tools/async-discard.mjs")
132
134
  discardAbortedPool(carrier)
133
135
  discardAbortedAdvisors(carrier)
134
136
  discardAbortedBgTasks(carrier) // #9 收尾档②:后台 bash 任务逐条杀树 + 出池 + 墓碑(杀 ⟺ 控制器已中止)
137
+ discardAbortedBrowserTasks(carrier) // 批 browser-async-fix:后台浏览器动作同面(在飞中止 + 出池 + 墓碑——不杀浏览器)
135
138
  carrier._pendingAsyncResults = []
136
139
  carrier._unsettledDigests = [] // §6.31.5 清账面(消化账务批):会话中止 ⇒ 升级账本随 pending 一并清(旧账不续)
137
140
  if (carrier._consultSessions instanceof Map) {
package/agent/timers.mjs CHANGED
@@ -2,12 +2,15 @@
2
2
  * agent/timers.mjs — timer 到期件(载体①:在途 timer 的单一权威读面)+ 空闲唤醒机制(闩 ∕ 派发 ∕ 火策略)。
3
3
  *
4
4
  * 机制单源 = `docs/core/design/AGENT-LOOP-ASYNC-POOL.md` §6.30.2(载体定形 D-TW1):
5
- * 读面三件纯函数住本档——最近到期时点 / 到期出列(幂等)/ 历史注入单点。消费面三处共用
5
+ * 读面纯函数住本档——最近到期时点 / 到期出列(幂等 · 含周期重排)/ 历史注入单点 / 下一到期时点
6
+ * (`nextFireAt`——§6.30.18)。消费面三处共用
6
7
  * 同一读点(零第二份过滤语义):① 回合边界轮询(`agent/post-turn.mjs`——行为零变);
7
8
  * ② 空闲闩 deadline;③ 挂起窗 deadline;显示面(状态行标记 / `/timers`)同读源。
8
9
  *
9
- * 载体仍在 agent 字段 `_pendingTimers`(条目形 `{ id, expiresAt, message }`)——**唯一写点** =
10
- * timer 工具 `thincoder-core/agent-tools/timer.mjs`;跨 run 存活 = 规范语义(D-TW3,无 per-run 复位)。
10
+ * 载体仍在 agent 字段 `_pendingTimers`(条目形 `{ id, expiresAt, message }` + 可选 `everyMs` ∥ `daily`——§6.30.18;
11
+ * 绝对时刻条目另携 `at` 来源标记位(非载重——父侧裁定 2026-10-12);一次性条目形逐字零变);写点两处 = timer 工具 `thincoder-core/agent-tools/timer.mjs`
12
+ * (设 ∕ 取消)+ 本档 `takeExpiredTimers` 内**到点重排**(出列+重排原子——§6.30.18 D-TW10);
13
+ * 跨 run 存活 = 规范语义(D-TW3,无 per-run 复位)。
11
14
  *
12
15
  * 空闲唤醒族(2026-09-28 流程批 R4 **上提核件** · 纯搬 + 转口 · 零语义改;上提源 = VSC
13
16
  * `thincoder-vscode/src/extension/timer-watch.mjs:46` ∕ `:73` ∕ `:94`——as-of 2026-09-28 R4 读数;两端现址(B3 收编后,实读 2026-09-29)=
@@ -21,6 +24,13 @@
21
24
  * 形态先例 = 队满「拒 + 提示 + 保留」)。与 CLI 排队容量 `QUEUED_MAX_ITEMS`(8)同值同形。 */
22
25
  export const TIMER_MAX_PENDING = 8
23
26
 
27
+ /** `every` 间隔下限(毫秒——§6.30.18 D-TW16):工具面**硬拒**低于此值(不静默钳制);
28
+ * 核重排面不设界(防御 = 非有限正数 ⇒ `nextFireAt` 返 `null` 照消费——下限校验在工具面)。 */
29
+ export const TIMER_MIN_EVERY_MS = 60_000
30
+
31
+ /** 日历式 `HH:MM` 形判据(防御面——工具面为唯一生产者,两处同形)。 */
32
+ const DAILY_RE = /^(?:[01]\d|2[0-3]):[0-5]\d$/
33
+
24
34
  /** 最近到期时点(毫秒)或 `null`(空在途)——端闩 / 挂起窗 deadline / 显示面共用同源(每帧活读)。 */
25
35
  export function pendingTimerDeadline(agent) {
26
36
  const timers = agent?._pendingTimers ?? []
@@ -33,13 +43,54 @@ export function pendingTimerDeadline(agent) {
33
43
  return min
34
44
  }
35
45
 
46
+ /** 下一到期时点(毫秒)或 `null`(无重排)——§6.30.18「载体扩展 ∥ 重排」单源:
47
+ * ① 固定间隔(`everyMs`):基准 = `entry.expiresAt`(首设刻);`k = max(1, floor((now − base) / everyMs) + 1)`;
48
+ * 返 `base + k × everyMs`——**零漂移** ∥ 错过任意拍直落下一格(**跳过不补投**——一次出列对一条至多一投);
49
+ * ② 日历式(`daily` = `HH:MM`):宿主本地**严格 `> now`** 的下一 occurrence(今日该刻未过 ⇒ 今日,已过 ⇒
50
+ * 明日;秒位归零——DST 按宿主日历规则走,不作特例);
51
+ * ③ 一次性 ∥ 绝对时刻(无两字段)⇒ `null`;守卫:`everyMs` 非有限正数 ∥ 基准非有限 ∥ `daily` 形不合
52
+ * ⇒ `null`(照消费——防御面;下限校验在工具面)。
53
+ * 首设换算同函数:`every` ⇒ `nextFireAt({ everyMs, expiresAt: now }, now)`(= 设时 + N)∥ `daily` ⇒
54
+ * `nextFireAt({ daily }, now)`;`at` ⇒ 直解算(不经本函数)。 */
55
+ export function nextFireAt(entry, now) {
56
+ const everyMs = entry?.everyMs
57
+ if (everyMs !== undefined && everyMs !== null) {
58
+ const base = entry.expiresAt
59
+ if (typeof everyMs !== "number" || !Number.isFinite(everyMs) || everyMs <= 0) return null
60
+ if (typeof base !== "number" || !Number.isFinite(base)) return null
61
+ const k = Math.max(1, Math.floor((now - base) / everyMs) + 1)
62
+ return base + k * everyMs
63
+ }
64
+ const daily = entry?.daily
65
+ if (daily !== undefined && daily !== null) {
66
+ if (typeof daily !== "string" || !DAILY_RE.test(daily)) return null
67
+ const [hh, mm] = daily.split(":").map(Number)
68
+ const at = new Date(now)
69
+ at.setHours(hh, mm, 0, 0) // 秒位归零(该刻整分投递)
70
+ if (at.getTime() <= now) {
71
+ at.setDate(at.getDate() + 1) // 已过 ⇒ 明日(跨日 = `Date` 日历运算承载——零特例代码)
72
+ at.setHours(hh, mm, 0, 0) // 跨日归一后再归零(DST 走宿主日历规则)
73
+ }
74
+ return at.getTime()
75
+ }
76
+ return null
77
+ }
78
+
36
79
  /** 到期出列(**幂等**):取走 `expiresAt <= now` 的条目并返回(在途表内移除)——二次调用零返
37
- * (§6.30.3 成本闸②「到期即消费」——绝不重复投递)。未到期项原序保留。 */
80
+ * (§6.30.3 成本闸②「到期即消费」——绝不重复投递)。未到期项原序保留。
81
+ * 携 `everyMs`/`daily` 的到期项**同 id 克隆回插**(新 `expiresAt = nextFireAt(t, now)`,严格 `> now`——
82
+ * §6.30.18 重排:出列+重排原子 ∥ 净零不触帽;重排先于交付 ⇒「到点即重排」与「交付幂等」互不依赖);
83
+ * 一次性 ∥ 绝对时刻照旧被消费(无重排)。 */
38
84
  export function takeExpiredTimers(agent, now = Date.now()) {
39
85
  const timers = agent?._pendingTimers ?? []
40
86
  const expired = timers.filter((t) => t.expiresAt <= now)
41
87
  if (expired.length === 0) return []
42
- agent._pendingTimers = timers.filter((t) => t.expiresAt > now)
88
+ const rest = timers.filter((t) => t.expiresAt > now)
89
+ for (const t of expired) {
90
+ const next = nextFireAt(t, now)
91
+ if (next !== null) rest.push({ ...t, expiresAt: next })
92
+ }
93
+ agent._pendingTimers = rest
43
94
  return expired
44
95
  }
45
96
 
@@ -22,7 +22,7 @@ import {
22
22
 
23
23
  export async function runTurnLoop(agent, {
24
24
  maxTurns, threshold, toolSchemas, toolByName, systemPrompt,
25
- depth, signal, autoTurn, streamOutput,
25
+ depth, signal, autoTurn, suspDriven, streamOutput,
26
26
  consumeInjected, consumeQueuedInput, callbacks, distillSignal,
27
27
  }) {
28
28
  let guardPushbacks = 0
@@ -105,7 +105,9 @@ export async function runTurnLoop(agent, {
105
105
  // consumeInjected 回调承载(异步子代理专属——缺省 null:主会话/阻塞子代理零开销)。
106
106
  consumeInjected?.(agent)
107
107
  // 子代理在飞消息(子 → 父;§6.27):空队列 no-op;非空 ⇒ 恰一条合并 user 消息注入。
108
- drainChildUpstream(agent)
108
+ // 批 async-delivery-formalize(§6.34.2 行 2):驱动面收窄「交付轮轮首」(`turn === 0` ∧ `autoTurn`——
109
+ // 用户回合零 drain);回退面(`!suspDriven`)逐字保持(每轮每步)。
110
+ if (!suspDriven || (turn === 0 && autoTurn === true)) drainChildUpstream(agent)
109
111
  // ③ 用户 → 主会话投递(步边界 pickup——queue-visible 批 fix 轮 2026-09-24 · F16):循环头
110
112
  // 回合边界缝(核只给缝、不给策略——取批 / pushReal / 呈现归端侧闭包);缺省 null,系统轮不传
111
113
  // (`AGENT-LOOP-ASYNC-POOL.md` §6.8「步边界 pickup」· 适用面 = 用户回合)。
@@ -260,7 +262,7 @@ export async function runTurnLoop(agent, {
260
262
  // Commit tool results (pairing, multimodal deferral, mutation accounting, reindex)
261
263
  await recordToolResults(agent, toolByName, results)
262
264
 
263
- injectPostTurn(agent, results, recentCallSigs, callbacks, turn)
265
+ injectPostTurn(agent, results, recentCallSigs, callbacks, turn, { suspDriven })
264
266
  }
265
267
 
266
268
  // #417(撞帽载荷「本段零落盘轮数」):收尾轮(撞帽轮)若零落盘同样计入——轮顶计数只
@@ -18,8 +18,8 @@
18
18
  * M6 的 `advisor.mjs` design-review 分类分支消费本导出
19
19
  * (KD-M6-1——同源不重复实现;落 dispatch.mjs 会让 advisor 反向 import 门禁簇成环)。
20
20
  * 2. `freezeWindowConflict(agent, absPaths)` —— D5 冻结窗口判据组装:被审文件集 =
21
- * 声明文档集(`inflightDesignReviewConflict` 同源 docAbs 腿)+ 批次档(`run.batchDoc`
22
- * 腿——评审绑定批档在途时,写批档同样致 stale)。dispatch.mjs
21
+ * 声明文档集(`inflightDesignReviewConflict` 同源 docAbs 分支)+ 批次档(`run.batchDoc`
22
+ * 分支——评审绑定批档在途时,写批档同样致 stale)。dispatch.mjs
23
23
  * 只 import 消费,不再各自直连 `advisor-settle.mjs`。
24
24
  * 3. `batchRecordWriteConflict(agent, depth, absPaths)` —— 批次档写门判据组装(#309 ·
25
25
  * `AGENT-LOOP-SUBAGENT.md` §6.29.1):判据与拒绝文案同址(文案字面只出本档)。
@@ -62,9 +62,9 @@ export function resolveReviewTargetPaths(agent) {
62
62
  /**
63
63
  * D5 冻结窗口判据组装(M4 §2.1#2):评审在途(点火 → 结算)期间父侧对被审文件集
64
64
  * (声明文档集 + 批次档)的写入 → 返回冲突 { id, path };无冲突 → null。
65
- * 声明文档集腿 = `inflightDesignReviewConflict`(同 docAbs / 同 normAbs,仅 running
66
- * 未取消的设计条目);批次档腿 = 同池条目 `run.batchDoc`(评审绑定批档在途时写批档
67
- * 同样致 stale——被审文件集合流点)。两腿同形返回,消费端拒绝文案零改。
65
+ * 声明文档集分支 = `inflightDesignReviewConflict`(同 docAbs / 同 normAbs,仅 running
66
+ * 未取消的设计条目);批次档分支 = 同池条目 `run.batchDoc`(评审绑定批档在途时写批档
67
+ * 同样致 stale——被审文件集合流点)。两分支同形返回,消费端拒绝文案零改。
68
68
  * @param {Object} agent
69
69
  * @param {string[]} absPaths — 本次写将触碰的路径(ABS——调用方 resolve(agent.cwd, p))
70
70
  * @returns {{id: string, path: string}|null} 命中评审 id + 冲突路径(ABS),无冲突 null
@@ -132,10 +132,10 @@ function isOwnBatchCompanion(targetKey, boundKey) {
132
132
  /**
133
133
  * 批次档写门判据(#309 · `docs/core/design/AGENT-LOOP-SUBAGENT.md` §6.29.1——containment;
134
134
  * 2026-09-29 收窄 · 台账 #545):
135
- * 工程角色子代理(`depth > 0` ∧ `agent._batchDoc` 在场)对**批次档文件**的写 ⇒ 目标 ≠ 绑定档
135
+ * 携绑定的子代理(`depth > 0` ∧ `agent._batchDoc` 在场——工程角色 ∥ 普通面 coder,`BATCH-RECORD.md` §4.16)对**批次档文件**的写 ⇒ 目标 ≠ 绑定档
136
136
  * **且非绑定批次的伴随件**(见 `isOwnBatchCompanion`)⇒ 冲突(fail-closed;写自己绑定的档 ∥
137
137
  * 自身批次伴随件 = 正常面放行)。比较键 = 绝对路径 + win32 大小写归一。
138
- * 界面:depth 0(主 agent——跨档处置是父侧职责)· 无 `_batchDoc`(非工程绑定族)· 主基底外的
138
+ * 界面:depth 0(主 agent——跨档处置是父侧职责)· 无 `_batchDoc`(未携绑定族——普通面 coder 亦入绑定面)· 主基底外的
139
139
  * 普通文件写 —— 三种形态均不进本门(返回 null,行为零变)。判据集由调用方限定 = `FILE_MUTATORS`
140
140
  * (`file_ops` / 读类不在门内——§6.29.1 边界)。
141
141
  * 拒绝文案**单源 = 本档**(两端直取 `message`——零字面副本)。
@@ -31,7 +31,7 @@
31
31
  * non-stale code review marks _calledAdvisorThisRun; a passing design review
32
32
  * issues its token under the designId slot at settle time.
33
33
  * 2026-09-11 第 11 批拆分:settle 记账 + 变更日志 + 陈旧/冻结判定迁 `advisor-settle.mjs`
34
- * (本文件 500 行 = 硬帽在册);既有 import 面经本文件 re-export 保持不变。
34
+ * (本文件 500 行——2026-09-11 拆分时);既有 import 面经本文件 re-export 保持不变。
35
35
  *
36
36
  * The sync path (depth>0 eng-coder self-review / explicit async:false) reuses
37
37
  * the same instance resolution (rounds/prior/cap) — its accounting still lands
@@ -392,7 +392,7 @@ export function launchAsyncAdvisor(parent, ctx, launch) {
392
392
  id, role: "advisor", reviewType, run,
393
393
  reviewId: run.reviewId, designId, designToken, documents, paths, object,
394
394
  launchSeq: mutationSeqOf(parent),
395
- // docAbs 产出面同源归一(2026-10-04 · #921 · A 随件):四腿单源 resolved(裸形/前缀形
395
+ // docAbs 产出面同源归一(2026-10-04 · #921 · A 随件):四分支单源 resolved(裸形/前缀形
396
396
  // 命中候选项目根即归一为绝对路径)——冻结窗/陈旧判定对**裸形受理档**照护成立(否则 A
397
397
  // 开冻结窗保护洞);绝对形与旧 normAbs 面逐字等价。
398
398
  docAbs: reviewType === "design" && Array.isArray(documents)
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * advisor-settle.mjs — advisor settle accounting + mutation log + stale / freeze
3
- * determination(2026-09-11 第 11 批自 advisor-async.mjs 拆出——500 行 = 硬帽在册,
4
- * 任何新增必越;既有 import 面经 advisor-async.mjs **re-export 保持不变**
3
+ * determination(2026-09-11 第 11 批自 advisor-async.mjs 拆出;
4
+ * 既有 import 面经 advisor-async.mjs **re-export 保持不变**
5
5
  * (noteMutations / mutationSeqOf / reviewIsStale / settleAdvisorRun)。
6
6
  *
7
7
  * 内容 = 迁入(逐字搬移含注释)+ 本批两处接线:
@@ -10,7 +10,7 @@
10
10
  import { runAdvisorReview, advisorIncompleteMarker, ADVISOR_LAUNCH_REFUSAL_PREFIX } from "../advisor/run.mjs"
11
11
  import { resolveBatchDocPath } from "./batch.mjs"
12
12
  // M6(模块设计 §2.1 F3):评审对象来源读 manifest docRoot(声明面)——单一权威源 =
13
- // review-facts.mjs 的 resolveReviewDocPaths(2026-10-04 · #921 · KD-2——分类四腿 + B 文案
13
+ // review-facts.mjs 的 resolveReviewDocPaths(2026-10-04 · #921 · KD-2——分类四分支 + B 文案
14
14
  // 诊断数据单源;write-gate 同名再出口保 normAbs 面)。不 import dispatch.mjs(簇间回边,环风险)。
15
15
  import { normAbs } from "../agent/write-gate.mjs"
16
16
  import { resolveReviewDocPaths } from "./review-facts.mjs"
@@ -17,6 +17,8 @@
17
17
  * ——其控制器链只作**判据**(杀 ⟺ 控制器已中止),杀树动作经本收尾面 `spec.dispose` 钩子落
18
18
  * `bash-async.mjs` 杀单点(`killBgTree`);回合中断(Ctrl+I)经 `bindChildController`
19
19
  * interrupt 豁免不中止控制器 ⇒ 本面零动作(F2 同款豁免——池保留)。
20
+ * 批 browser-async-fix(2026-10-07):browser 后台动作族同面并入(`BROWSER_SPEC`——dispose = 条目
21
+ * 控制器 abort ⇒ 动作展开 / 排队丢队;**不杀浏览器**)。
20
22
  *
21
23
  * 判据口径(D-AD6):两接线点**均不传 `ctx`** ⇒ 实走 controller 支(`parentAborted(null, entry)`)
22
24
  * ——与对侧有效判据同判(对侧传入的 `ctx` 为死参);`ctx` 保留为签名备用面(直调用例 / 对侧收敛)。
@@ -59,6 +61,11 @@ const bgReminderText = (n, list) =>
59
61
  `[System reminder: ${n} background bash task(s) were killed by the user's Stop — their processes are gone and no digest will arrive: ${list}. ` +
60
62
  "Full output logs remain on disk (paths were given in the start ack).]"
61
63
 
64
+ /** browser 后台动作族提醒模板(同族形态——取消无摘要:终止凭据 = 墓碑 + 杀点确认;浏览器本体不杀)。 */
65
+ const browserReminderText = (n, list) =>
66
+ `[System reminder: ${n} background browser task(s) were cancelled by the user's Stop — no digest will arrive: ${list}. ` +
67
+ "The browser session itself was not killed (run `close` to reset it).]"
68
+
62
69
  /** 列表词(wasStatus 数据源):queued → "(was queued — never started)";其余 "(was running)"。 */
63
70
  const wasPhrase = (wasStatus) => (wasStatus === "queued" ? " (was queued — never started)" : " (was running)")
64
71
 
@@ -151,6 +158,21 @@ const BG_SPEC = {
151
158
  },
152
159
  }
153
160
 
161
+ /** browser 后台动作族 spec(批 browser-async-fix——收尾档同面):dispose = 中止在飞(条目控制器
162
+ * abort ⇒ running 动作展开 / queued 丢队从未运行)+ 标记 discarded(settle 面据此不注入——摘要不可达);
163
+ * 无队列面(帽 = 起跑显式拒)。**不杀浏览器**(会话重置是独立动作 `close`)。 */
164
+ const BROWSER_SPEC = {
165
+ pool: "browser",
166
+ describe: (entry) => ({ id: entry.id, role: "browser", action: entry.action }),
167
+ roleOf: () => "browser",
168
+ listPhrase: (d) => `browser#${d.id} (${d.action})`,
169
+ reminder: browserReminderText,
170
+ dispose: (entry) => {
171
+ entry.discarded = true
172
+ entry.controller?.abort?.({ abortTrigger: "discard" })
173
+ },
174
+ }
175
+
154
176
  /**
155
177
  * 中止收尾(F1–F3):只清已死子代理条目——写 `discarded` 墓碑 + 出池 + 队列剔除 + 整批一次提醒。
156
178
  * @param parent agent 形态(载体双形经 `getAsyncPool` / `writeTombstone` 吸收)
@@ -184,3 +206,15 @@ export function discardAbortedAdvisors(parent, ctx = null) {
184
206
  export function discardAbortedBgTasks(parent, ctx = null) {
185
207
  return discardRole(parent, BG_SPEC, ctx)
186
208
  }
209
+
210
+ /**
211
+ * 中止收尾(browser 后台动作族——同面并入):只清已死条目——逐条中止在飞(running ⇒ 动作展开;
212
+ * queued ⇒ 丢队从未运行)+ `discarded` 墓碑 + 出池 + 整批一次提醒;回合中断(Ctrl+I)不中止控制器
213
+ * ⇒ 本面零动作(池保留——与 bg 同豁免)。
214
+ * @param parent agent 形态
215
+ * @param ctx 判据 ctx(接线点不传——controller 支,同上)
216
+ * @returns {{discarded: Array<{id, role: "browser", action}>, kept: number}}
217
+ */
218
+ export function discardAbortedBrowserTasks(parent, ctx = null) {
219
+ return discardRole(parent, BROWSER_SPEC, ctx)
220
+ }
@@ -113,11 +113,12 @@ export function bindChildController(ctrl, baseSignal) {
113
113
 
114
114
  /** 池 accessor(D1):role "advisor" → `_asyncAdvisors`(独立评审池——无队列独立调度)·
115
115
  * role "consult" → `_consultSessions`(会话池)· role "bg" → `_bgTasks`(#9 后台 bash
116
- * 任务池)· 其余 → `_asyncSubagents`(子代理/飞刀共享槽位队列池)。载体经 `carrierField`
116
+ * 任务池)· role "browser" → `_browserTasks`(批 browser-async-fix 后台浏览器动作池)·
117
+ * 其余 → `_asyncSubagents`(子代理/飞刀共享槽位队列池)。载体经 `carrierField`
117
118
  * 吸收(#94);未初始化返 null——调用方以 `?? new Map()` / 可选链处置。 */
118
119
  export function getAsyncPool(parent, role) {
119
120
  const field = role === "advisor" ? "_asyncAdvisors" : role === "consult" ? "_consultSessions"
120
- : role === "bg" ? "_bgTasks" : "_asyncSubagents"
121
+ : role === "bg" ? "_bgTasks" : role === "browser" ? "_browserTasks" : "_asyncSubagents"
121
122
  return carrierField(parent, field) ?? null
122
123
  }
123
124
 
@@ -2,7 +2,7 @@
2
2
  * audit-block.mjs — 审计块构造器(AGENT-LOOP-SUBAGENT.md §6.26 · 台账 #23)。
3
3
  *
4
4
  * 自 `subagent-spawn.mjs` **逐字搬移**(D23-3 外提:spawn 档越 300 软线 + 块构造成纯函数
5
- * ⇒ 判据面可直测;D23-4:块内文本零改——含块内自指词,整块搬移不改真值)。本档承载
5
+ * ⇒ 判据可直测;D23-4:块内文本零改——含块内自指词,整块搬移不改真值)。本档承载
6
6
  * spawn 级固定机制性指令的第二块(审计模板 + A2 任务书摘要 + touched 快照):
7
7
  * 装配侧只收集(`thincoder-core/agent-tools/subagent-spawn.mjs`),下发面 = system 固块
8
8
  * (`child._spawnSystemBlock` → 核 `prepareRun` 拼接——`thincoder-core/agent/setup.mjs`)。
@@ -56,7 +56,7 @@ const TAIL_KEEP_CHARS = 32 * 1024
56
56
  /** exit→close 宽限(同同步径后台注记面:孙进程握管道时 close 不来——`bash.mjs` 1500ms 同值)。 */
57
57
  const EXIT_GRACE_MS = 1500
58
58
 
59
- /** 测试缝:log 目录重定向(单测沙箱——生产从不调用;`_setDigestOffloadDirForTest` 同式)。 */
59
+ /** 测试缝:log 目录重定向(单元测试沙箱——生产从不调用;`_setDigestOffloadDirForTest` 同式)。 */
60
60
  let _bgTaskLogDir = null
61
61
  export function _setBgTaskLogDirForTest(dir) { _bgTaskLogDir = dir }
62
62
  function bgTaskLogDir() { return _bgTaskLogDir ?? join(configDir, "tool-results") }
@@ -12,14 +12,15 @@
12
12
  * 身份判据(D-BR17/D-BR18/D-BR21):主 agent 的工具调用 ctx 带 `depth === 0`(dispatch 装配);
13
13
  * eng 子代理 `ctx.depth > 0`;评审实例 ctx 无 depth 但工具以 `review: true` 绑定。create/close
14
14
  * 仅 depth-0 放行(BR-19/BR-24「… is main-agent-only」);status 写域 = 调用者自己段
15
- * (eng-designer → §2 · 评审 → §3 · eng-coder → §5 · 主 agent → §1——轮 2 #3 裁定②,§4/§6
16
- * 状态面走普通文档写);depth-0 的 status/path 面按 D-BR21(可选 path,缺省 = 在飞批唯一时取)。
15
+ * (eng-designer → §2 · 评审 → §3 · eng-coder → §5 · coder → §5【普通模式分支增表】· 主 agent →
16
+ * 工程 §1 ∥ 普通 §1/§2/§5【取段——§4.16】——§4/§6 状态面走普通文档写);depth-0 的 status/path 面
17
+ * 按 D-BR21(可选 path,缺省 = 在飞批唯一时取)。
17
18
  */
18
19
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
19
20
  import { dirname, resolve } from "node:path"
20
21
 
21
22
  import {
22
- SEGMENT_BY_ROLE, STATUS_WORDS, STATUS_LINE_RE, readBatchStatusLine, sectionHasStatusLine, sectionHeaderRe, batchSkeleton,
23
+ SEGMENT_BY_ROLE, SEGMENT_BY_ROLE_NORMAL, DEPTH0_SEGMENTS, STATUS_WORDS, STATUS_LINE_RE, readBatchStatusLine, sectionHasStatusLine, sectionHeaderRe, batchSkeleton,
23
24
  TEMPLATE_PLACEHOLDERS, findPlaceholderResidue, placeholderResidueError,
24
25
  } from "./batch-skeleton.mjs"
25
26
  import { resolveBatchCreatePath } from "./batch-paths.mjs"
@@ -122,6 +123,8 @@ export function findInFlightBatch(cwd, bases) {
122
123
  * ⇒ create 即拒,不留死锁);`prev` 传入值**幂等剥**「前情 = 」前缀(值规范化;默认值零变)。
123
124
  * 2026-10-05(A-2):`ledger` / `board` 可选参**建即填**台账行(归一契约 = §4.11;回执两态 =
124
125
  * 两参皆给 ⇒ 标注已填 ∥ 未给(任一)⇒ 填法句)。
126
+ * 2026-10-09(§4.16):骨架**按模式位取形**(`batchSkeleton({…, mode})`——执行期实读;普通面
127
+ * 作者标签四处替换,工程面骨架产出逐字不变)。
125
128
  * @returns {string} 成功消息(含落盘绝对路径)
126
129
  */
127
130
  export function createBatchRecord({ args, ctx, review, cwd, bases, onWritten }) {
@@ -161,7 +164,9 @@ export function createBatchRecord({ args, ctx, review, cwd, bases, onWritten })
161
164
  const prevRaw = typeof args?.prev === "string" ? args.prev.trim() : ""
162
165
  const prev = prevRaw ? (prevRaw.replace(/^(?:前情\s*[=::]\s*)+/g, "").trim() || "无(独立批)") : "无(独立批)"
163
166
  mkdirSync(dirname(abs), { recursive: true })
164
- writeFileSync(abs, batchSkeleton({ date, topic, source, prev, ledger, board }))
167
+ // 模式位执行期实读(§4.16——骨架取形时点 = 建档即固;翻转不回写既有骨架)
168
+ const mode = ctx?.agent?.config?.agent?.engineering ? "engineering" : "normal"
169
+ writeFileSync(abs, batchSkeleton({ date, topic, source, prev, ledger, board, mode }))
165
170
  onWritten?.(ctx?.agent ?? {}, abs)
166
171
  // 回执两态(`TOOLS.md` §6.20 逐字):两参皆给 ⇒ 标注已填;未给(任一)⇒ 填法句(填法 = 文件编辑)
167
172
  const headerNote = ledger !== null && board !== null
@@ -281,24 +286,40 @@ function assertStatusNote(note) {
281
286
  }
282
287
 
283
288
  /**
284
- * status——状态行流转(§4.12,段属主)。写域 = 调用者自己段内 `**状态行**:` 行(eng-designer →
285
- * §2 · 评审 → §3 · eng-coder → §5 · 主 agent → §1——轮 2 #3 裁定②);值域 = STATUS_WORDS 该段项
286
- * 恰一词(**余核 = 关键词**——F11-A 谓词收紧),散文说明走独立 `note` 字段(落状态行括注);
287
- * 冻结真值不变(gate 只读 §1)。path 参数 = 仅 depth-0(D-BR21)——子代理/评审传 path
288
- * ⇒ 拒(目标 = spawn/实例注入,语法上写不到别处)。F11-C 挂点 = 写盘前(assertStatusValue 后):
289
- * 档头结构行含骨架死占位 ⇒ 拒(段体引用豁免;close 不拦——收口是主 agent 终态动作)。
289
+ * status——状态行流转(§4.12,段属主;§4.16 模式感知)。写域 = 调用者自己段内 `**状态行**:`
290
+ * 行(评审 → §3 · eng-designer → §2 · eng-coder / coder → §5 · 主 agent:工程 §1 ∥ 普通
291
+ * §1/§2/§5 取段——缺省 §1);值域 = STATUS_WORDS 该段项恰一词(**余核 = 关键词**——F11-A
292
+ * 谓词收紧),散文说明走独立 `note` 字段(落状态行括注);冻结真值不变(gate 只读 §1)。
293
+ * path 参数 = 仅 depth-0(D-BR21)——子代理/评审传 path ⇒ 拒(目标 = spawn/实例注入,语法上
294
+ * 写不到别处)。F11-C 挂点 = 写盘前(assertStatusValue 后):档头结构行含骨架死占位 ⇒ 拒
295
+ * (段体引用豁免;close 不拦——收口是主 agent 终态动作)。
290
296
  * @returns {string} 成功消息
291
297
  */
292
298
  export function statusBatchRecord({ args, ctx, review, pickTarget, onWritten }) {
293
299
  const depth0 = isDepthZero(ctx)
294
- const seg = review ? 3 : depth0 ? 1 : SEGMENT_BY_ROLE[ctx?.agent?._role] ?? null
300
+ // 模式位执行期实读(§4.16——同款先例 = task.mjs execute 门;翻转即时生效——D-BR26)。
301
+ // 写域:评审 → §3 · 子代理 → 身份段(普通模式分支合并 coder 增表——工程模式分支不查)·
302
+ // depth-0 工程 → §1 ∥ 普通 → §1/§2/§5(取段——缺省 §1)。
303
+ const engineering = !!ctx?.agent?.config?.agent?.engineering
304
+ let seg = review ? 3 : depth0 ? 1 : SEGMENT_BY_ROLE[ctx?.agent?._role] ?? (engineering ? null : SEGMENT_BY_ROLE_NORMAL[ctx?.agent?._role] ?? null)
295
305
  if (seg === null) {
296
306
  throw new Error("batch: no segment is writable by this caller — status writes the caller's OWN section (eng-designer → §2, design review → §3, eng-coder → §5, main agent → §1).")
297
307
  }
298
- // 声明段核对(BR-22「eng-designer 对 §1 调 status ⇒ 段白名单拒绝」——镜像 append 面:
299
- // segment 可声明,但声明段 ≠ 身份写域段 ⇒ 拒;身份写域段可省略——身份即写域)。
308
+ // 段域判据(BR-22「eng-designer 对 §1 调 status ⇒ 段白名单拒」——镜像 append 面):
309
+ // depth-0 普通面(§4.16)= **取段**——写域集 §1/§2/§5、缺省 §1、越出集合 ⇒ 拒(模式化新串);
310
+ // depth-0 工程面 ∥ 子代理 ∥ 评审 = **声明段核对**(声明段 ≠ 身份写域段 ⇒ 拒;身份写域段可省略)。
300
311
  const declared = args?.segment === undefined || args?.segment === null ? null : segmentNumber(args.segment)
301
- if (declared !== null && declared !== seg) {
312
+ // 传值而解析不可得 ⇒ 显式拒(#1116——镜像 append 面「unknown segment」串形;未传 = 缺省语义零变)。
313
+ if (declared === null && args?.segment !== undefined && args?.segment !== null) {
314
+ throw new Error(`batch: unknown segment ${JSON.stringify(args?.segment ?? null)} — pass the section number you write (e.g. "§${seg}"), or omit it. Nothing was written.`)
315
+ }
316
+ if (depth0 && !review && !engineering) {
317
+ const target = declared ?? 1
318
+ if (!DEPTH0_SEGMENTS.status.normal.includes(target)) {
319
+ throw new Error(`batch: §${declared} is not yours to write — in normal mode status writes §1/§2/§5 only (一段一作者: §3 = design review; §4/§6 carry no status word list). Nothing was written.`)
320
+ }
321
+ seg = target
322
+ } else if (declared !== null && declared !== seg) {
302
323
  throw new Error(`batch: §${declared} is not yours to write — status writes YOUR OWN section §${seg} only (一段一作者: eng-designer → §2, design review → §3, eng-coder → §5, main agent → §1). Nothing was written.`)
303
324
  }
304
325
  if (args?.path !== undefined && args?.path !== null && !depth0) {
@@ -7,10 +7,10 @@
7
7
  * 契约(§4.15 逐字):**候选序** = cwd → 项目根 → 逐基底(基底 = `batchDocBases` 声明面单源);
8
8
  * **判据分野** = 读面取首个**可读**文件 / create 取首个**落基底根内**者;**锚定防嵌套** = 串以任一基底的
9
9
  * 「项目根相对前缀」打头且前缀后为**路径段边界** ⇒ 只解析项目根形(不二次拼接;不落基底内 ⇒ fail-closed);
10
- * **歧义锚**(条 5 · 2026-10-02 · #828)= create 按目标所属项目落基底 ∥ 无所属显式拒;读面候选腿追加;
10
+ * **歧义锚**(条 5 · 2026-10-02 · #828)= create 按目标所属项目落基底 ∥ 无所属显式拒;读面候选分支追加;
11
11
  * 在飞 ∥ 写门基底集 = 候选并集(`batchDocBases`);**零变面** = 绝对路径照旧、三个错误文案逐字、
12
12
  * 「参数在 + 路径可读」判据句、`ok` / `none` 两态。
13
- * **基底自有项目根腿 + 基底相对单段收窄**(条 6 · 2026-10-05 · #942)= 逐基底 `owningProject` 恰一落点收 ∥ 多落点显式拒(`ambiguousLandingError`);基底相对腿限单段(多段串不二次拼接)。
13
+ * **基底自有项目根分支 + 基底相对单段收窄**(条 6 · 2026-10-05 · #942)= 逐基底 `owningProject` 恰一落点收 ∥ 多落点显式拒(`ambiguousLandingError`);基底相对分支限单段(多段串不二次拼接)。
14
14
  *
15
15
  * 依赖(KD-4 单向):本档为**叶档**——只 import `node:*` 与 `../manifest.mjs`;主档 re-export 保既有面。
16
16
  */
@@ -101,7 +101,7 @@ const anchoredEscapeError = (raw) => new Error(`batch: create path is anchored a
101
101
  const ambiguousCreateError = (raw, candidates) => new Error(`batch: ambiguous session anchor — create target "${raw}" belongs to no candidate project; pass an explicit project path (e.g. "${candidates[0]}/docs/batches/<file>.md"). Candidates: ${candidates.join(", ")}`)
102
102
  const ambiguousLandingError = (raw, landings) => new Error(`batch: create path resolves under more than one declared batch-record base — refusing to pick one (fail-closed); pass an explicit path (absolute preferred). Path: ${raw}. Candidates: ${landings.join(", ")}`)
103
103
 
104
- /** 歧义锚读面候选腿(条 5):逐候选项目——项目根形(`resolve(candidate, p)`)∥ 声明基底形
104
+ /** 歧义锚读面候选分支(条 5):逐候选项目——项目根形(`resolve(candidate, p)`)∥ 声明基底形
105
105
  * (候选按名排序、并集保序——「首个可读」在此即为确定判读)。 */
106
106
  function ambiguousReadLegs(cwd, p) {
107
107
  const amb = ambiguousCandidates(cwd)
@@ -194,7 +194,7 @@ export function resolveBatchCreatePath(cwd, given, bases) {
194
194
  }
195
195
  if (owned.length > 1) throw ambiguousLandingError(raw, owned)
196
196
  if (owned.length === 1) return owned[0]
197
- // ④ 基底相对形收窄单段(归一后无 `/` 才走原基底腿——多段串不二次拼接)
197
+ // ④ 基底相对形收窄单段(归一后无 `/` 才走原基底分支——多段串不二次拼接)
198
198
  const leafRel = roots.length ? relative(roots[0], resolve(roots[0], p)) : ""
199
199
  if (leafRel && !leafRel.startsWith("..") && !leafRel.includes(sep)) {
200
200
  for (const b of roots) {
@@ -29,7 +29,7 @@ export const sectionHeaderRe = (seg) => new RegExp(`^## §${seg}(?=\\s|$)`, "m")
29
29
  * 内嵌另一冻结词的值(含「进行中…已收口」双词 ⇒ 已收口优先误冻结——2026-09-20 词面纪律)。
30
30
  * emoji / 括注 / 日期后缀容忍(**读侧** gate 判定 = 子串包含,同 gate——写入面 value 谓词
31
31
  * 自本批起收紧为「剥装饰白名单后余核 = 关键词」,见 batch-lifecycle.mjs);
32
- * 全词表并集 = 各段项去重集合(note 字段零命中的判据面——括注永不误触 gate)。
32
+ * 全词表并集 = 各段项去重集合(note 字段零命中的判据——括注永不误触 gate)。
33
33
  */
34
34
  export const STATUS_WORDS = Object.freeze({
35
35
  1: Object.freeze({ open: "进行中", closed: "已收口" }),
@@ -39,16 +39,29 @@ export const STATUS_WORDS = Object.freeze({
39
39
  })
40
40
 
41
41
  /** 身份 → 可写段号(append 段白名单的**子代理面**——原 batch-segment.mjs 逐字迁移;主 agent
42
- * 的 depth-0 白名单(append = §1/§4/§6 · status = §1——轮 2 #3 裁定②)由身份判据在
43
- * batch.mjs / batch-lifecycle.mjs 分支,不经本表。旧 batch-segment.mjs 导出面等价保持——
44
- * 本符号经 batch.mjs re-export(过渡 shim 面零触)。 */
42
+ * 的 depth-0 白名单(模式感知——工程 §1/§4/§6 · §1 ∥ 普通 §1/§2/§4/§5/§6 · §1/§2/§5,§4.16)
43
+ * 由身份判据在 batch.mjs / batch-lifecycle.mjs 分支,不经本表。旧 batch-segment.mjs 导出面等价
44
+ * 保持——本符号经 batch.mjs re-export(过渡 shim 面零触)。**表零字面改**——普通模式另立增表
45
+ * `SEGMENT_BY_ROLE_NORMAL`(合并查只在普通模式分支);工程面「逐字不变」判据据此成立。 */
45
46
  export const SEGMENT_BY_ROLE = { "eng-designer": 2, "eng-coder": 5 }
46
47
 
48
+ /** 普通模式增表(§4.16——**合并查只在普通模式分支**,工程模式分支不消费本表):coder 承办 ⇒ §5
49
+ * 实施记录(可选绑定自写——未绑定由主 agent 代记、行内注明来源)。 */
50
+ export const SEGMENT_BY_ROLE_NORMAL = { "coder": 5 }
51
+
52
+ /** depth-0 段白名单(§4.16——**模式键两集**;工程面判定字面逐字保持、普通面新增):
53
+ * append:工程 = §1/§4/§6 ∥ 普通 = §1/§2/§4/§5/§6(§2 = 设计师席并入主 agent;§5 = 直做实施记录);
54
+ * status:工程 = §1 ∥ 普通 = §1/§2/§5(写域仍「调用者自己段」;`segment` 缺省 = §1)。 */
55
+ export const DEPTH0_SEGMENTS = Object.freeze({
56
+ append: Object.freeze({ engineering: Object.freeze([1, 4, 6]), normal: Object.freeze([1, 2, 4, 5, 6]) }),
57
+ status: Object.freeze({ engineering: Object.freeze([1]), normal: Object.freeze([1, 2, 5]) }),
58
+ })
59
+
47
60
  /** §1 状态行前缀形态(`**状态行**:` 独立行——表格行 / 块引用行不命中;判定只认关键字)。 */
48
61
  export const STATUS_LINE_RE = /^\s*\*\*状态行\*\*[::]\s*(.*)$/
49
62
 
50
63
  /**
51
- * 骨架死占位**枚举单源**(F11-C 判据面——create 骨架产出过的全部死占位字面):
64
+ * 骨架死占位**枚举单源**(F11-C 判据——create 骨架产出过的全部死占位字面):
52
65
  * - `#<编号>` / `<板块>` = 现行骨架仍在产出的两处(台账行——填充人 = 主 agent,时点 =
53
66
  * 建档后、本档首个 append/status 之前);
54
67
  * - `<BATCH-ID>` / `<讨论来源>` = 改形**删除**的两处旧占位(create 不再产出;旧档残留同判)。
@@ -178,22 +191,27 @@ export function sectionHasStatusLine(src, seg) {
178
191
  * 留存时(未给参)由主 agent 文件编辑填(台账登记 = 主 agent 既有义务,create 不代建——填充
179
192
  * 时点 = 建档后、本档首个 append/status 之前)。
180
193
  * 本批改形删除(F11-B):旧 `# …(<BATCH-ID>)` 后缀与 `来源 = <讨论来源>` 占位实参化。
194
+ * 模式取形(§4.16——普通模式变体**四处替换**、余行逐字同;`mode` 缺省 = "engineering" ⇒
195
+ * 工程产出逐字不变):① 汇总行 §2/§5 标签;② `## §2` 段头;③ §2 状态占位句;④ `## §5` 段头。
181
196
  */
182
- export function batchSkeleton({ date, topic, source, prev, ledger = null, board = null }) {
197
+ export function batchSkeleton({ date, topic, source, prev, ledger = null, board = null, mode = "engineering" }) {
198
+ const normal = mode === "normal"
199
+ const designerSeat = normal ? "主 agent" : "eng-designer" // ① 汇总行 §2 标签 ∥ ② §2 段头
200
+ const implementerSeat = normal ? "实施者:主 agent ∥ coder" : "eng-coder" // ① 汇总行 §5 标签 ∥ ④ §5 段头
183
201
  return [
184
202
  `# ${date} · ${topic}`,
185
- "> 六段 append-only,一段一作者:§1 讨论(主 agent)· §2 批次任务与设计(eng-designer)· §3 设计评审(评审子代理)· §4 用户批准(主 agent)· §5 实施记录(eng-coder)· §6 验证与收口(父代理)。",
203
+ `> 六段 append-only,一段一作者:§1 讨论(主 agent)· §2 批次任务与设计(${designerSeat})· §3 设计评审(评审子代理)· §4 用户批准(主 agent)· §5 实施记录(${implementerSeat})· §6 验证与收口(父代理)。`,
186
204
  `> 编制:主 agent · ${date} · 来源 = ${source}。`,
187
205
  `> 台账 = ${ledger ?? "#<编号>"}(${board ?? "<板块>"} · 归批)。前情 = ${prev}。`,
188
206
  "## §1 讨论(主 agent)",
189
207
  `**状态行**:🔄 ${STATUS_WORDS[1].open}(…)`,
190
208
  "<§1 模板占位:本批条目 / 关键判据 / 授权口径>",
191
- "## §2 批次任务与设计(eng-designer)",
192
- "**状态行**:(eng-designer 写入时更新)",
193
- "<§2 模板占位:本批条目(覆盖) / 设计档落点 / 机制设计 / 受影响文件与测试面 / 验收对照 / 关键决策 / 上抛项>",
209
+ `## §2 批次任务与设计(${designerSeat})`,
210
+ `**状态行**:(${normal ? "主 agent" : "eng-designer"} 写入时更新)`, // ③ §2 状态占位句
211
+ "<§2 模板占位:本批条目(覆盖) / 设计档落点 / 机制设计 / 受影响文件与测试覆盖 / 验收对照 / 关键决策 / 上抛项>",
194
212
  "## §3 设计评审(评审子代理)",
195
213
  "## §4 用户批准(主 agent)",
196
- "## §5 实施记录(eng-coder)",
214
+ `## §5 实施记录(${implementerSeat})`,
197
215
  "## §6 验证与收口(父代理)",
198
216
  "",
199
217
  ].join("\n")