@thincoder/core 0.10.3 → 0.10.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 (68) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/advisor/citations.mjs +29 -3
  3. package/advisor/compaction.mjs +21 -2
  4. package/advisor/history.mjs +8 -6
  5. package/advisor/loop.mjs +29 -27
  6. package/advisor/messages.mjs +2 -1
  7. package/advisor/run.mjs +5 -1
  8. package/advisor/timeline.mjs +26 -0
  9. package/advisor.mjs +4 -2
  10. package/agent/completion.mjs +3 -1
  11. package/agent/digest-account.mjs +188 -0
  12. package/agent/family-tools.mjs +11 -11
  13. package/agent/helpers.mjs +8 -0
  14. package/agent/record-results.mjs +4 -0
  15. package/agent/run-stages.mjs +27 -4
  16. package/agent/run-start.mjs +49 -3
  17. package/agent/suspension.mjs +30 -8
  18. package/agent/turn-loop.mjs +25 -2
  19. package/agent/write-gate.mjs +13 -25
  20. package/agent-tools/advisor-async.mjs +14 -6
  21. package/agent-tools/advisor.mjs +34 -33
  22. package/agent-tools/async-settle.mjs +29 -5
  23. package/agent-tools/batch-lifecycle.mjs +26 -5
  24. package/agent-tools/batch-paths.mjs +22 -2
  25. package/agent-tools/batch-skeleton.mjs +31 -33
  26. package/agent-tools/batch.mjs +11 -3
  27. package/agent-tools/design-token.mjs +20 -0
  28. package/agent-tools/escalate-async.mjs +4 -2
  29. package/agent-tools/review-facts.mjs +143 -16
  30. package/agent-tools/subagent-actions-query.mjs +13 -6
  31. package/agent-tools/subagent-async.mjs +10 -3
  32. package/agent-tools/subagent-panel.mjs +2 -1
  33. package/agent-tools/subagent-spawn.mjs +1 -1
  34. package/agent-tools/subagent.mjs +1 -1
  35. package/agent-tools/zero-write-watch.mjs +77 -0
  36. package/agent.mjs +1 -1
  37. package/conventions.mjs +26 -6
  38. package/escape.mjs +1 -1
  39. package/i18n.mjs +5 -0
  40. package/ledger-tools.mjs +148 -85
  41. package/ledger-variant-notice.mjs +47 -0
  42. package/ledger.mjs +2 -2
  43. package/manifest-schema.mjs +6 -4
  44. package/manifest.mjs +5 -3
  45. package/package.json +1 -1
  46. package/prompts/common.md +2 -2
  47. package/prompts/persona-engineering.md +4 -3
  48. package/provider/anthropic.mjs +14 -2
  49. package/provider/errors.mjs +23 -0
  50. package/provider/google.mjs +13 -2
  51. package/provider/responses-request.mjs +3 -1
  52. package/provider/responses.mjs +38 -4
  53. package/shell-identity.mjs +107 -0
  54. package/tool-docs/advisor.md +1 -1
  55. package/tool-docs/bash.md +1 -1
  56. package/tool-docs/batch.md +1 -1
  57. package/tool-docs/delete.md +7 -5
  58. package/tool-docs/ledger.md +6 -0
  59. package/tool-docs/subagent.md +10 -0
  60. package/tools/bash.mjs +9 -4
  61. package/tools/index.mjs +1 -4
  62. package/tools/patch.mjs +8 -2
  63. package/tools/write-path.mjs +6 -4
  64. package/tool-docs/ledger_add.md +0 -1
  65. package/tool-docs/ledger_close.md +0 -1
  66. package/tool-docs/ledger_count.md +0 -1
  67. package/tool-docs/ledger_query.md +0 -1
  68. package/tool-docs/ledger_update.md +0 -1
@@ -0,0 +1,77 @@
1
+ /**
2
+ * zero-write-watch.mjs — 零落笔看门狗叶(批 subagent-zero-write-watchdog · 台账 #934)。
3
+ * 设计权威 = `docs/core/design/AGENT-LOOP-UPSTREAM.md` §6.32;需求 = `docs/core/requirements/AGENT-LOOP.md` §4.16。
4
+ *
5
+ * 两族(§6.32.2):子代理族 = eng-coder / eng-designer **连续** N 轮零文件写(采集点邻单点 =
6
+ * `agent/turn-loop.mjs` 轮顶——streak 更新与越阈判定同点);评审族 = 异步评审 **连续** N 轮零评审
7
+ * 文本产出(`advisor/loop.mjs` 循环局部计数 + 可选钩子 `seams.onStallRound`)。
8
+ *
9
+ * 送达 = 复用 §6.27 上行通道(`pushChildUpstream`——note 类:同队列同消费点、零唤醒、零新容器);
10
+ * **只推提醒**——不自动杀 / 不自动转向 / 不代父侧动作(§6.32.10-1)。
11
+ * 防轰炸 = 闩 + 写后重臂(子代理族:`_zeroWriteAlerted`;评审族:单次运行闩——§6.32.3)。
12
+ * 文案单源 = 本档两族文本函数(§6.32.4 逐字——父侧定稿;`from` + 轮数为唯一变量)。
13
+ * 零依赖叶:静态 import 只取 `parent-channel.mjs` 的 `pushChildUpstream`(不引池 / 端面 / 循环)。
14
+ */
15
+ import { pushChildUpstream } from "./parent-channel.mjs"
16
+
17
+ /** 阈值单源(连续轮数——两族共用;§6.32.2 阈值固定 50,不做配置键)。 */
18
+ export const ZERO_WRITE_ALERT_ROUNDS = 50
19
+
20
+ /** 子代理族触面(用户点名三角色中的两位——有文件写面;explore / plan / consult / 普通 coder / depth-0 不设)。 */
21
+ const WATCH_ROLES = new Set(["eng-coder", "eng-designer"])
22
+
23
+ /** 子代理族文案(§6.32.4 逐字——父侧定稿;`from` = `_upstream.label`,形 `role#id`)。 */
24
+ export function zeroWriteAlertText(from, rounds) {
25
+ return `[zero-write watchdog] ${from}: ${rounds} consecutive rounds with no file write — take a look (subagent action:'status' for the live view).`
26
+ }
27
+
28
+ /** 评审族文案(§6.32.4 逐字——父侧定稿;`from` = `advisor#<entry id>`)。 */
29
+ export function advisorAlertText(from, rounds) {
30
+ return `[zero-write watchdog] ${from}: ${rounds} consecutive rounds with no review output — take a look (subagent action:'status' / 'cancel').`
31
+ }
32
+
33
+ /**
34
+ * 子代理族判定 + 推送(§6.32.6)——门 = `_role` ∈ 触面 ∧ `_upstream.parent` 在场 ∧
35
+ * `_zeroWriteStreak` **为整数** ≥ 阈值(缺字段 / 非整数 ⇒ 不判——不冒充 `0`)∧ 未闩;
36
+ * 命中 ⇒ `pushChildUpstream({parent, from: _upstream.label, kind: "note", message})` + 置闩。
37
+ * **恒零抛**(兜底捕获兑现——自动源无模型可报错;失败面静默跳过)。
38
+ * @param {object} child 子 agent(eng-coder / eng-designer 形态;`_upstream` 由 spawn 站点装配)。
39
+ * @returns {boolean} 是否命中推送(测试面)。
40
+ */
41
+ export function maybeZeroWriteAlert(child) {
42
+ try {
43
+ if (!WATCH_ROLES.has(child?._role)) return false
44
+ const upstream = child._upstream
45
+ if (!upstream?.parent) return false
46
+ const streak = child._zeroWriteStreak
47
+ if (!Number.isInteger(streak) || streak < ZERO_WRITE_ALERT_ROUNDS) return false
48
+ if (child._zeroWriteAlerted) return false
49
+ pushChildUpstream({
50
+ parent: upstream.parent,
51
+ from: upstream.label,
52
+ kind: "note",
53
+ message: zeroWriteAlertText(upstream.label, streak),
54
+ })
55
+ child._zeroWriteAlerted = true
56
+ return true
57
+ } catch {
58
+ return false // 恒零抛兑现(§6.32.6——失败面静默跳过)
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 评审族推送(组合文案 + push——呼点 = 评审环路钩子 `seams.onStallRound`,单次运行至多一次)。
64
+ * **恒零抛**(实现面兜底捕获兑现——抛错不得上抛为评审 failed 面;§6.32.6)。
65
+ * @param {object} parent 父 agent(接收方——`pushChildUpstream` 载体吸收同款)。
66
+ * @param {string} from 来源标签(`advisor#<entry id>`)。
67
+ * @param {number} rounds 连续无产出轮数。
68
+ * @returns {boolean} 是否推送成功(测试面)。
69
+ */
70
+ export function pushAdvisorAlert(parent, from, rounds) {
71
+ try {
72
+ pushChildUpstream({ parent, from, kind: "note", message: advisorAlertText(from, rounds) })
73
+ return true
74
+ } catch {
75
+ return false
76
+ }
77
+ }
package/agent.mjs CHANGED
@@ -100,6 +100,6 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
100
100
  // 2026-09-05 实践轮:回合收尾(consult 清理/async 池分流/guard 继承——原 425-462
101
101
  // 段 + collectSettledAsync 466-494 整体迁 agent/run-stages.mjs finalizeAgentTurn——
102
102
  // CLI 对位 VS run-stages——finally 只剩一行调用 + 骨架注释)。
103
- await finalizeAgentTurn(agent, { signal, autoTurn, suspDriven, thrownError, depth })
103
+ await finalizeAgentTurn(agent, { signal, autoTurn, upstreamTurn, timerTurn, suspDriven, thrownError, depth })
104
104
  }
105
105
  }
package/conventions.mjs CHANGED
@@ -26,9 +26,10 @@
26
26
  * code — inside a declared code segment (default: the path segment `src`), or the
27
27
  * fallback; doc — documentation extension outside any code segment and not
28
28
  * scratch; temp — tmp-* name or .tmp/.temp extension; aux — a default
29
- * auxiliary-path segment sequence (test / tests / scripts / .thincoder/tmp —
30
- * F9) that matched no code segment and neither doc nor temp.
31
- * Precedence: code segment → temp → doc → aux → fallback code.
29
+ * auxiliary-path segment sequence (test / tests / scripts / .thincoder/tmp — F9)
30
+ * that matched no code segment and neither doc nor temp; state — a project state
31
+ * file (`PROJECT-MANIFEST.json` basename ∥ `.thincoder/conventions.json` pair — #941).
32
+ * Precedence: code segment → temp → doc → aux → state → fallback code.
32
33
  *
33
34
  * Segment matching runs on the PROJECT-ROOT-RELATIVE FACE of a path (D17 · 2026-09-28): the
34
35
  * machine layout ABOVE the project root never participates. An absolute path outside the
@@ -128,18 +129,32 @@ function hasAuxSegment(p, conv) {
128
129
  return hasSegmentSequence(p, DEFAULT_AUX_PATHS, conv?.root, false)
129
130
  }
130
131
 
131
- /** "code" | "doc" | "temp" | "aux" — the single classification decision.
132
+ /** Project state-file markers (data — #941 · PORTABILITY D19): manifest basename (any layer) ∥
133
+ * retired-carrier pair; single sources: `MANIFEST_REL` ∥ `RETIRED_REL_PATH`. */
134
+ const STATE_BASENAME = "project-manifest.json"
135
+ const STATE_PAIR = ".thincoder/conventions.json"
136
+
137
+ /** True when `s` names a project state file (basename any layer ∥ segment pair; case-insensitive — no face, D17). */
138
+ function isStateFile(s) {
139
+ const segs = segmentsOf(s)
140
+ if (segs.length > 0 && lowerSeg(segs[segs.length - 1]) === STATE_BASENAME) return true
141
+ return hasSequenceIn(segs, [STATE_PAIR])
142
+ }
143
+
144
+ /** "code" | "doc" | "temp" | "aux" | "state" — the single classification decision.
132
145
  * Precedence: code segment first (src/** stays product code even when the name
133
146
  * looks scratch — the pre-existing unconditional-src rule), then temp, then a
134
147
  * documentation extension, then an auxiliary-path segment sequence (test / tests
135
- * / scripts / .thincoder/tmp — F9), else code (anything not doc/temp/aux is product
136
- * code). Both segment checks run on the project-root-relative face (D17 — `faceOf`). */
148
+ * / scripts / .thincoder/tmp — F9), then a project state file (#941), else code
149
+ * (anything not doc/temp/aux/state is product code). Segment checks face-bound (D17);
150
+ * state matches by name marker. */
137
151
  export function classifyPath(p, conv) {
138
152
  const s = String(p ?? "")
139
153
  if (hasCodeSegment(s, conv)) return "code"
140
154
  if (TEMP_FILE.test(s)) return "temp"
141
155
  if (DOC_FILE.test(s)) return "doc"
142
156
  if (hasAuxSegment(s, conv)) return "aux"
157
+ if (isStateFile(s)) return "state"
143
158
  return "code"
144
159
  }
145
160
 
@@ -165,6 +180,11 @@ export function isAuxPath(p, conv) {
165
180
  return classifyPath(p, conv) === "aux"
166
181
  }
167
182
 
183
+ /** True when the path is a project state file (`classifyPath === "state"` — #941; derived form). */
184
+ export function isStatePath(p, conv) {
185
+ return classifyPath(p, conv) === "state"
186
+ }
187
+
168
188
  // ─────────────────────────────────────────────────────────────────────────────
169
189
  // 迁出面(2026-10-01 拆分批 · #755 ∥ #786):声明装载面外提 `declaration.mjs`——本档经
170
190
  // 转口保名(消费面 / 批内件 import 面零改;`DEFAULT_DECLARATION` 另供本档分类裁判缺省取用)。
package/escape.mjs CHANGED
@@ -52,7 +52,7 @@ export function escapeLiteralEscapes(text) {
52
52
  return out
53
53
  }
54
54
 
55
- /** 净化孤立 UTF-16 代理对(毒源②):高代理无低代理跟随 / 低代理无高代理前置 → 替换为 。 */
55
+ /** 净化孤立 UTF-16 代理对(毒源②):高代理无低代理跟随 / 低代理无高代理前置 → 剥除(出串零该码元——码 :66/:68 实读为准)。 */
56
56
  export function sanitizeLoneSurrogates(text) {
57
57
  text = String(text ?? "")
58
58
  let out = ""
package/i18n.mjs CHANGED
@@ -41,6 +41,9 @@ export const CORE_MESSAGES = Object.freeze({
41
41
  "digest.turnLabelAsk": { en: "[auto-turn: answering ${from}: ${msg}]", zh: "自动回合:答复 ${from}:${msg}" },
42
42
  "digest.capAuto": { en: "[auto-turn: continuing past turn cap…]", zh: "自动回合:越过轮次上限,继续推进…" },
43
43
  "digest.capStop": { en: "[auto-turn stopped at ${turns} turns — partial digest; finished reports stay in history]", zh: "自动回合在 ${turns} 轮处停止——部分消化;已完成的报告保留在历史中" },
44
+ // 消化账务批 · 2026-10-05 · 台账 #930(§6.31.6 端面残余行——判据 = `unsettledCount(carrier)`;
45
+ // 消费面 = CLI 痕行 ∥ 桌面 / VSC 行元素;三端共读本键)
46
+ "digest.residue": { en: "${n} background report(s) not accounted — they will be re-delivered", zh: "有 ${n} 份后台报告未销账——将自动重投" },
44
47
  // ── 限流 / 供应商状态行 ──
45
48
  "status.rateWait": { en: "TPM throttle wait ~${s}s", zh: "TPM 限流等待 ~${s}s" },
46
49
  "status.rateLimited": { en: "Rate-limited 429, retry in ${s}s", zh: "限流 429,${s}s 后重试" },
@@ -69,6 +72,8 @@ export const CORE_MESSAGES = Object.freeze({
69
72
  "status.turn": { en: "turn ${n}/${m}", zh: "轮次 ${n}/${m}" },
70
73
  "tool.running": { en: "running…", zh: "执行中…" },
71
74
  "tool.done": { en: "done", zh: "完成" },
75
+ // ── 台账(ledger)· F-LX3 变体键库首跑提示(批 2026-10-05-ledger-variant-db-notice · 台账 #935 · LEDGER.md §12.2 逐字;消费面 = CLI TUI 启动行)
76
+ "ledger.variantDbNotice": { en: "Legacy variant-key ledger DB found for this project (pre-upgrade; rows may be outside the current ledger): thincoder ledger audit to inspect; thincoder ledger migrate to reconcile", zh: "检测到本项目的存量变体键台账库(升级遗留,数据可能未并入当前库):thincoder ledger audit 查看;thincoder ledger migrate 收正" },
72
77
  })
73
78
  // 深度冻结(机器消费面冻结——嵌套 { en, zh } 也冻结;浅冻结下值可被静默改写)。
74
79
  for (const entry of Object.values(CORE_MESSAGES)) Object.freeze(entry)
package/ledger-tools.mjs CHANGED
@@ -1,14 +1,16 @@
1
1
  /**
2
- * ledger-tools.mjs — 台账五工具定义(查询二 = 只读全角色 / 写三 = 仅主 agent 装配)+ 工具层参数守卫。
3
- * 自 `ledger-cmd.mjs` 拆出(2026-09-28 守卫族微修批 · 台账 #473——纯搬移、语义零变;核函数留原档)。
4
- * 守卫 = **声明派生**(设计档 §3.2:枚举 / 必填 / 类型取自工具自身 `parameters`,不在守卫内复写——
5
- * D2「声明即校验」);判于核函数之前 ⇒ 非法入参零写、零库动作、零 SQLite 原文外泄。读面二具同拍
6
- * (#473:非法过滤参数 ⇒ 拒——不再「静默空集 / 绑原文」)。
7
- * 接线:族出口 = `ledger.mjs`(re-export——消费面 tools/index.mjs · agent/family-tools.mjs 零改);
8
- * 消费侧一律动态 import `ledger.mjs`(ledger-db.mjs 静态 import node:sqlite ⇒ W8 契约②)。
2
+ * ledger-tools.mjs — 台账**统一入口** `ledger`(action 分派)+ 五旧名弃用壳 + 工具层参数守卫。
3
+ * 自 `ledger-cmd.mjs` 拆出(2026-09-28 守卫族微修批 · 台账 #473);2026-10-05 统一入口批
4
+ * (台账 #923)合五工具为一:模型面恰一名 `ledger`(两变体同名单对象、schema 窄化——
5
+ * 全量 = depth-0 五 action ∥ 只读 = depth>0 二 action);旧五名 = code 面弃用壳(不入任何装配面)。
6
+ * 守卫 = **声明派生**(设计档 §3.2 / §11.3:判序① 入参归一 → ② 入口级 action 判(消费 action)→
7
+ * ③ 余键喂该 action 伪工具对象走 P2–P6——声明源 = 前身旧工具 `parameters` 逐字;helper `assertToolArgs`
8
+ * 零改);判于核函数之前 ⇒ 非法入参零写、零库动作、零 SQLite 原文外泄。
9
+ * 接线:族出口 = `ledger.mjs`(re-export——消费面 agent/family-tools.mjs;旧读二装配面已清零);
10
+ * 消费侧一律动态 import(`ledger-db.mjs` 静态 import node:sqlite ⇒ W8 契约②)。
9
11
  */
10
12
  import { ledgerAdd, ledgerClose, ledgerCount, ledgerQuery, ledgerUpdate } from "./ledger-cmd.mjs"
11
- import { DESC } from "./tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/ledger_*.md(DESC 单解析面)
13
+ import { DESC } from "./tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/ledger.md(DESC 单解析面)
12
14
 
13
15
  /** 工具 cwd 供值面(缺省 → 当前项目根)。 */
14
16
  const cwdOf = (ctx) => ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
@@ -68,51 +70,22 @@ function assertToolArgs(tool, args) {
68
70
  return args
69
71
  }
70
72
 
71
- // ── 工具定义(接线:查询 → tools/index.mjs 全角色面;写 → family-tools.mjs depthOnly 分支) ──
73
+ // ── 统一入口(设计档 §11.2 / §11.3) ─────────────────────────────────────────
72
74
 
73
- export const ledgerQueryTool = {
74
- name: "ledger_query",
75
- description: DESC("ledger_query"), // #15 外置:文本单点 = tool-docs/ledger_query.md(两池词表逐字保留)
76
- parameters: {
77
- type: "object",
78
- properties: {
79
- cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
80
- status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "按状态过滤(六态之一,缺省 = 不过滤)" },
81
- kind: { type: "string", enum: ["requirement", "tech_todo"], description: "按类别过滤" },
82
- board: { type: "string", description: "按归属板块过滤(需求档文档名)" },
83
- },
84
- additionalProperties: false,
85
- },
86
- readonly: true,
87
- async execute(args, ctx) {
88
- args = assertToolArgs(ledgerQueryTool, args)
89
- return JSON.stringify(ledgerQuery({ cwd: args.cwd ?? cwdOf(ctx), status: args.status, kind: args.kind, board: args.board }), null, 2)
90
- },
91
- }
75
+ /** action 枚举(全量 / 只读两变体;顺序 = 文档与按钮序)。 */
76
+ const FULL_ACTIONS = ["add", "update", "close", "query", "count"]
77
+ const READ_ACTIONS = ["query", "count"]
92
78
 
93
- export const ledgerCountTool = {
94
- name: "ledger_count",
95
- description: DESC("ledger_count"), // #15 外置:文本单点 = tool-docs/ledger_count.md
96
- parameters: {
97
- type: "object",
98
- properties: { cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" } },
99
- additionalProperties: false,
100
- },
101
- readonly: true,
102
- async execute(args, ctx) {
103
- args = assertToolArgs(ledgerCountTool, args)
104
- return JSON.stringify({ count: ledgerCount({ cwd: args.cwd ?? cwdOf(ctx) }) })
105
- },
106
- }
79
+ /** cwd 参数描述(五前身声明同文——文本单点)。 */
80
+ const CWD_DESC = "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根"
107
81
 
108
- export const ledgerAddTool = {
109
- name: "ledger_add",
110
- description: DESC("ledger_add"), // #15 外置:文本单点 = tool-docs/ledger_add.md(两池词表逐字保留)
111
- parameters: {
82
+ /** 各 action 声明条目(= 前身旧工具 `parameters` 逐字;伪工具对象守卫 + 弃用壳共用单源)。 */
83
+ const ACTION_SPECS = {
84
+ add: {
112
85
  type: "object",
113
86
  required: ["kind", "title"],
114
87
  properties: {
115
- cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
88
+ cwd: { type: "string", description: CWD_DESC },
116
89
  kind: { type: "string", enum: ["requirement", "tech_todo"], description: "类别" },
117
90
  title: { type: "string", description: "条目标题" },
118
91
  board: { type: "string", description: "归属板块(需求档文档名,可空)" },
@@ -123,23 +96,11 @@ export const ledgerAddTool = {
123
96
  },
124
97
  additionalProperties: false,
125
98
  },
126
- readonly: false,
127
- async execute(args, ctx) {
128
- args = assertToolArgs(ledgerAddTool, args)
129
- const { cwd, ...row } = args
130
- const id = ledgerAdd({ cwd: cwd ?? cwdOf(ctx), row })
131
- return JSON.stringify({ id, status: "待讨论" })
132
- },
133
- }
134
-
135
- export const ledgerUpdateTool = {
136
- name: "ledger_update",
137
- description: DESC("ledger_update"), // #15 外置:文本单点 = tool-docs/ledger_update.md
138
- parameters: {
99
+ update: {
139
100
  type: "object",
140
101
  required: ["id"],
141
102
  properties: {
142
- cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
103
+ cwd: { type: "string", description: CWD_DESC },
143
104
  id: { type: "number", description: "条目 id" },
144
105
  status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "目标状态(六态之一,缺省 = 不变)" },
145
106
  title: { type: "string", description: "标题(缺省 = 不变)" },
@@ -152,37 +113,139 @@ export const ledgerUpdateTool = {
152
113
  },
153
114
  additionalProperties: false,
154
115
  },
155
- readonly: false,
156
- async execute(args, ctx) {
157
- args = assertToolArgs(ledgerUpdateTool, args)
158
- const { cwd, id, ...patch } = args
159
- let executorSessionId
160
- if (patch.executor == null) {
161
- // 显式 `null` ≡ 略去(§3.1 `null` 口径 · #474——与缺省同分支);动态 import——零新静态边(K-LX1);
162
- // 工具层供值,核心函数保持参数注入纯函数
163
- const { getSessionId } = await import("./session-slots.mjs")
164
- executorSessionId = getSessionId()
165
- }
166
- return JSON.stringify(ledgerUpdate({ cwd: cwd ?? cwdOf(ctx), id, patch, executorSessionId }))
167
- },
168
- }
169
-
170
- export const ledgerCloseTool = {
171
- name: "ledger_close",
172
- description: DESC("ledger_close"), // #15 外置:文本单点 = tool-docs/ledger_close.md
173
- parameters: {
116
+ close: {
174
117
  type: "object",
175
118
  required: ["id", "status"],
176
119
  properties: {
177
- cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
120
+ cwd: { type: "string", description: CWD_DESC },
178
121
  id: { type: "number", description: "条目 id" },
179
122
  status: { type: "string", enum: ["已核销", "已废弃"], description: "目标态(勾销 / 追认核销 / 撤回)" },
180
123
  },
181
124
  additionalProperties: false,
182
125
  },
183
- readonly: false,
184
- async execute(args, ctx) {
185
- args = assertToolArgs(ledgerCloseTool, args)
186
- return JSON.stringify(ledgerClose({ cwd: args.cwd ?? cwdOf(ctx), id: args.id, status: args.status }))
126
+ query: {
127
+ type: "object",
128
+ properties: {
129
+ cwd: { type: "string", description: CWD_DESC },
130
+ status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "按状态过滤(六态之一,缺省 = 不过滤)" },
131
+ kind: { type: "string", enum: ["requirement", "tech_todo"], description: "按类别过滤" },
132
+ board: { type: "string", description: "按归属板块过滤(需求档文档名)" },
133
+ },
134
+ additionalProperties: false,
135
+ },
136
+ count: {
137
+ type: "object",
138
+ properties: { cwd: { type: "string", description: CWD_DESC } },
139
+ additionalProperties: false,
140
+ },
141
+ }
142
+
143
+ /** 并集 schema(§11.2——`action` 枚举 + 五 action 参数并集;per-action 必填由运行时守卫裁)。 */
144
+ const UNION_PARAMETERS = {
145
+ type: "object",
146
+ required: ["action"],
147
+ properties: {
148
+ action: { type: "string", enum: FULL_ACTIONS, description: "" }, // 变体构造时填描述
149
+ cwd: { type: "string", description: CWD_DESC },
150
+ kind: { type: "string", enum: ["requirement", "tech_todo"], description: "类别(add 必填 / query 过滤;缺省 = 不过滤)" },
151
+ title: { type: "string", description: "条目标题(add 必填;update 可改——空串 / 全空白 ⇒ 拒)" },
152
+ board: { type: "string", description: "归属板块(需求档文档名;add 可空 / update 缺省 = 不变 / query 过滤)" },
153
+ req_doc: { type: "string", description: "需求档指针(add 可空 / update 缺省 = 不变)" },
154
+ task_book: { type: "string", description: "任务书指针(add 可空 / update 缺省 = 不变;在途 / 待核销必填——咬合 CHECK)" },
155
+ evidence: { type: "string", description: "最小证据行(add 可空 / update 缺省 = 不变;追认核销须非空)" },
156
+ trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "技术待办触发(add / update 可空)" },
157
+ id: { type: "number", description: "条目 id(update / close 必填)" },
158
+ status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "状态(close 必填 ∈ {已核销, 已废弃};update 迁移 / query 过滤——六态之一)" },
159
+ executor: { type: "string", description: "执行者 sessionId(update 可选——接手改写归属;缺省 = 按状态迁移语义)" },
187
160
  },
161
+ additionalProperties: false,
162
+ }
163
+
164
+ /** 变体构造(同名单对象、schema 窄化、两形永不同装配——先例 = `subagent` 工具 depth 面变体):
165
+ * 全量(depth-0 五 action;`writeFace:true` —— `readonly:false` + 动作级分类钩子,写面保持侧效门,
166
+ * query / count 归只读类 = 旧读二 `readonly:true` 同行为:planMode 放行 / 免审批)∥ 只读(depth>0
167
+ * 二 action;`readonly:true`,写三 schema 级不可达)。形态显式声明(不按 actions 长度推导——增量
168
+ * 变体不改错标文案)。 */
169
+ function makeLedgerTool(actions, { readonly = false, writeFace = false } = {}) {
170
+ const entryTool = { name: "ledger", parameters: { type: "object", required: ["action"], properties: { action: { type: "string", enum: actions } } } }
171
+ return {
172
+ name: "ledger",
173
+ description: DESC("ledger"), // 文本单点 = tool-docs/ledger.md(五档要点合一)
174
+ parameters: {
175
+ ...UNION_PARAMETERS,
176
+ properties: {
177
+ ...UNION_PARAMETERS.properties,
178
+ action: {
179
+ ...UNION_PARAMETERS.properties.action,
180
+ enum: actions,
181
+ description: writeFace
182
+ ? "操作(五值):add 新增 / update 更新 / close 收口 / query 查询 / count 计数——写三仅主 agent 装配面"
183
+ : "操作(只读二值):query 查询 / count 计数",
184
+ },
185
+ },
186
+ },
187
+ readonly,
188
+ isReadonlyAction(args) { return READ_ACTIONS.includes(args?.action) },
189
+ async execute(args, ctx) {
190
+ // ① 入参归一(判序①——P1 同式;缺省 / null ⇒ {})
191
+ if (args == null) args = {}
192
+ else if (typeof args !== "object" || Array.isArray(args)) throw new Error(`ledger:参数须为对象(收到 ${argPreview(args)})`)
193
+ // ② 入口级 action 判(P3 缺失 / P5 非法——消费 action;取值域 = 本装配面枚举)
194
+ const { action, ...rest0 } = args
195
+ assertToolArgs(entryTool, { action })
196
+ // ③ 余键喂该 action 伪工具对象(声明源 = 前身工具 parameters 逐字——`action` 键不入 P2 域)
197
+ const rest = assertToolArgs({ name: `ledger(${action})`, parameters: ACTION_SPECS[action] }, rest0)
198
+ // ④ 路由既有核函数(包装细节携自原工具 execute 逐字保留:cwd 缺省 / JSON 缩进 / getSessionId 动态 import)
199
+ switch (action) {
200
+ case "add": {
201
+ const { cwd, ...row } = rest
202
+ const id = ledgerAdd({ cwd: cwd ?? cwdOf(ctx), row })
203
+ return JSON.stringify({ id, status: "待讨论" })
204
+ }
205
+ case "update": {
206
+ const { cwd, id, ...patch } = rest
207
+ let executorSessionId
208
+ if (patch.executor == null) {
209
+ // 显式 `null` ≡ 略去(§3.1 `null` 口径 · #474——与缺省同分支);动态 import——零新静态边(K-LX1)
210
+ const { getSessionId } = await import("./session-slots.mjs")
211
+ executorSessionId = getSessionId()
212
+ }
213
+ return JSON.stringify(ledgerUpdate({ cwd: cwd ?? cwdOf(ctx), id, patch, executorSessionId }))
214
+ }
215
+ case "close":
216
+ return JSON.stringify(ledgerClose({ cwd: rest.cwd ?? cwdOf(ctx), id: rest.id, status: rest.status }))
217
+ case "query":
218
+ return JSON.stringify(ledgerQuery({ cwd: rest.cwd ?? cwdOf(ctx), status: rest.status, kind: rest.kind, board: rest.board }), null, 2)
219
+ case "count":
220
+ return JSON.stringify({ count: ledgerCount({ cwd: rest.cwd ?? cwdOf(ctx) }) })
221
+ default:
222
+ // 不可达(② 枚举守门已先拒)——fail-loud 兜底(防未来变体增量 drift)
223
+ throw new Error(`ledger:action 非法:${argPreview(action)}(取值 ∈ ${enumText(actions)})`)
224
+ }
225
+ },
226
+ }
188
227
  }
228
+
229
+ /** 统一入口两变体(同名 `ledger`——depth-0 装配全量 ∥ depth>0 各角色段装配只读)。 */
230
+ export const ledgerTool = makeLedgerTool(FULL_ACTIONS, { writeFace: true })
231
+ export const ledgerReadTool = makeLedgerTool(READ_ACTIONS, { readonly: true })
232
+
233
+ // ── 旧五名弃用壳(设计档 §11.4——code 面保留、不入任何装配面) ────────────────
234
+
235
+ /** 弃用文案(含旧名 + 统一入口 + 对应 action 指引;execute 抛它 ⇒ 零库动作)。
236
+ * description 同文静态化——不经 `DESC`(旧五档退场后 DESC 加载必炸;壳面规避)。 */
237
+ const deprecatedLine = (old, action) => `${old} 已退役(本名不再装配)——请改用统一入口 ledger:action="${action}"(参数面与原工具相同)。`
238
+
239
+ const deprecatedShell = (old, action, readonly) => ({
240
+ name: old,
241
+ description: deprecatedLine(old, action),
242
+ parameters: ACTION_SPECS[action],
243
+ readonly,
244
+ async execute() { throw new Error(deprecatedLine(old, action)) },
245
+ })
246
+
247
+ export const ledgerQueryTool = deprecatedShell("ledger_query", "query", true)
248
+ export const ledgerCountTool = deprecatedShell("ledger_count", "count", true)
249
+ export const ledgerAddTool = deprecatedShell("ledger_add", "add", false)
250
+ export const ledgerUpdateTool = deprecatedShell("ledger_update", "update", false)
251
+ export const ledgerCloseTool = deprecatedShell("ledger_close", "close", false)
@@ -0,0 +1,47 @@
1
+ /**
2
+ * ledger-variant-notice.mjs — 变体键库首跑检测提示(F-LX3 · 设计档 `docs/core/design/LEDGER.md` §12 ·
3
+ * 批 `docs/batches/2026-10-05-ledger-variant-db-notice.md` · 台账 #935)。
4
+ *
5
+ * 升级到含键归一的版本后,存量变体键库(旧产物写入的盘符翻转拼写键——`legacyKeyVariants`)不再无声搁浅:
6
+ * CLI 主入口启动时对**当前项目根**检测,命中 ⇒ 单行 i18n 提示(`thincoder ledger audit` 查看 ∥
7
+ * `thincoder ledger migrate` 收正);**只提示,零自动动作**(不迁移 / 不删档 / 不建库 / 零写)。
8
+ * 判据 = 变体键档**文件存在**(KD-VN1——零开库:不触 SQLite / 不建 WAL;坏档 / 空库不漏报)。
9
+ * 每进程至多一次 = 模块级闩(首唤检测 / 后唤零动作——重置缝 `_resetLedgerVariantNoticeForTest`,
10
+ * 先例 = `_resetLedgerDirForTest`)。降级 = 全径 try ⇒ 返回 `null`(零抛零阻塞——启动链不受累)。
11
+ * 本档静态链入 `ledger-migrate.mjs`(node:sqlite)⇒ 消费侧一律动态 import(W8 契约②)。
12
+ * 检测面零新建哈希式:键式直引 `ledgerKey` ∥ 变体枚举直引 `legacyKeyVariants`(单源不二写)。
13
+ */
14
+ import { statSync } from "node:fs"
15
+ import { join, resolve } from "node:path"
16
+
17
+ import { t } from "./i18n.mjs"
18
+ import { ledgerDirPath, ledgerKey } from "./ledger-db.mjs"
19
+ import { legacyKeyVariants } from "./ledger-migrate.mjs"
20
+ import { resolveProjectRoot } from "./manifest.mjs"
21
+
22
+ /** 默认探针(吞错形——与 `migrate` 源枚举过滤同判据):`statSync(p).isFile()`,任何 stat 失败 ⇒ false。 */
23
+ const defaultExists = (p) => { try { return statSync(p).isFile() } catch { return false } }
24
+
25
+ /** 一次为限闩(「每进程」= ESM 模块实例的天然粒度——首唤检测 / 后唤零动作)。 */
26
+ let noticeChecked = false
27
+
28
+ /** 测试重置缝(同 `_resetLedgerDirForTest` 先例——闩为进程级事实,直测需复位)。 */
29
+ export function _resetLedgerVariantNoticeForTest() { noticeChecked = false }
30
+
31
+ /**
32
+ * 检测当前项目根的变体键库:命中 ⇒ 单行提示文案(i18n 键 `ledger.variantDbNotice`——zh ∥ en 单源),
33
+ * 无 ∥ 已查过 ∥ 任何异常(根解析 / 探针 / 文案解析)⇒ `null`(静默降级——零抛)。
34
+ * 命中判据 = 变体键(`k !== 主键`)中有档在盘(文件存在——零开库);主库在否不影响判定。
35
+ * @param {{cwd?: string, dir?: string, locale?: string, exists?: (p: string) => boolean}} [opts]
36
+ * @returns {string|null} 单行文案 / null
37
+ */
38
+ export function ledgerVariantNotice({ cwd = process.cwd(), dir = ledgerDirPath(), locale = "en", exists = defaultExists } = {}) {
39
+ if (noticeChecked) return null
40
+ noticeChecked = true
41
+ try {
42
+ const root = resolveProjectRoot(cwd) ?? resolve(cwd ?? ".")
43
+ const targetKey = ledgerKey(root)
44
+ const hit = legacyKeyVariants(root).filter((k) => k !== targetKey).some((k) => exists(join(dir, `${k}.db`)))
45
+ return hit ? t("ledger.variantDbNotice", {}, locale) : null
46
+ } catch { return null }
47
+ }
package/ledger.mjs CHANGED
@@ -6,7 +6,7 @@
6
6
  * 锁死、COUNT 单源计数、事务保证收口原子、归档 = 软删除。行龄源 = SQLite 时间戳(无 git blame)。
7
7
  *
8
8
  * 面:① 命令族(查询 = 只读全角色 / 写 = 仅主 agent——核函数 `ledger-cmd.mjs` + 工具定义 `ledger-tools.mjs`
9
- * 两源,接线见 tools/index.mjs 与 agent/family-tools.mjs)
9
+ * 两源,接线见 `agent/family-tools.mjs`——统一入口 `ledger` 两变体,2026-10-05 统一入口批)
10
10
  * ② 族发现(findProject / discoverFamily——标记 = ledger.db)③ scan 组装(buildScan——行集 + ledgerCount
11
11
  * → scan 对象,形状契约 = pool/tech/aged/thresholdReached/actionable/root/name/ledger)
12
12
  * ④ 格式 helper(L1 标记 ∥ L2 明细行逐字契约——含标记范围归约 `scopeMarkerOf`,§7.2)。
@@ -188,6 +188,6 @@ export function detailScans(scans, current) {
188
188
  // ── re-export(拆分件接口——命令面接线 = 动态 import 本档,KD-M2-3) ──
189
189
  export { ALLOWED_MIGRATIONS, ledgerDbPath, ledgerKey, ledgerDirPath, openLedger, PENDING_STATUSES, _setLedgerDirForTest, _resetLedgerDirForTest, ensureExecutorColumn } from "./ledger-db.mjs"
190
190
  export { ledgerAdd, ledgerClose, ledgerCount, ledgerQuery, ledgerUpdate } from "./ledger-cmd.mjs"
191
- export { ledgerAddTool, ledgerCloseTool, ledgerCountTool, ledgerQueryTool, ledgerUpdateTool } from "./ledger-tools.mjs"
191
+ export { ledgerTool, ledgerReadTool, ledgerAddTool, ledgerCloseTool, ledgerCountTool, ledgerQueryTool, ledgerUpdateTool } from "./ledger-tools.mjs"
192
192
  export { runLedgerAudit, runLedgerMigrate } from "./ledger-migrate.mjs"
193
193
  export { resolveExecutorStates, executorTail, _setExecutorProbeTtlForTest } from "./ledger-executors.mjs"
@@ -12,12 +12,14 @@
12
12
 
13
13
  /**
14
14
  * `docRoot` 子键**值形态判据**(F7 / KD-M1-6 / KD-M1-8——判据单源):非空字符串(`trim` 后
15
- * 非空),或非空数组且元素皆非空字符串(**同款口径**:元素 `trim` 后非空);其余(空串 /
16
- * 空白串 / 空数组 / 数组含非串 / 空串 / 空白串元素 / 非串非数组)为非法。
15
+ * 非空),或非空数组且元素皆非空字符串(**同款口径**:元素 `trim` 后非空),或 `null`
16
+ * (**「本面无」合法哨兵**——KD-M1-37);其余(空串 / 空白串 / 空数组 / 数组含非串 / 空串 /
17
+ * 空白串元素 / 非串非数组)为非法。
17
18
  * @param {unknown} value docRoot 某子键的值
18
19
  * @returns {boolean}
19
20
  */
20
21
  export function isValidDocRootValue(value) {
22
+ if (value === null) return true
21
23
  if (typeof value === "string") return value.trim() !== ""
22
24
  if (!Array.isArray(value) || value.length === 0) return false
23
25
  return value.every((p) => typeof p === "string" && p.trim() !== "")
@@ -110,9 +112,9 @@ export function validateManifest(obj) {
110
112
  }
111
113
  for (const sub of subkeys) {
112
114
  if (!(sub in value)) { missingKeys.push(`${nested}.${sub}`); continue } // 子键路径(AC-3 / T3b)
113
- // docRoot 子键值形态(F7 / KD-M1-7):串 | 非空串数组;非法 → 拒(不静默跳过)。
115
+ // docRoot 子键值形态(F7 / KD-M1-7 / KD-M1-37):串 | 非空串数组 | null(本面无);非法 → 拒(不静默跳过)。
114
116
  if (nested === "docRoot" && !isValidDocRootValue(value[sub])) {
115
- errors.push(`docRoot.${sub} 值形态非法:${JSON.stringify(value[sub])}(须为非空字符串或非空字符串数组——KD-M1-6 / KD-M1-7)`)
117
+ errors.push(`docRoot.${sub} 值形态非法:${JSON.stringify(value[sub])}(须为非空字符串 / 非空字符串数组 / null(本面无)——KD-M1-6 / KD-M1-7 / KD-M1-37)`)
116
118
  }
117
119
  // checkConfig.lineCounts 元素层形态(#546):数组,元素 = { doc, section } 非空串;非法 → 拒(不静默跳过)。
118
120
  if (nested === "checkConfig" && sub === "lineCounts" && !isLineCountsValue(value[sub])) {
package/manifest.mjs CHANGED
@@ -10,7 +10,7 @@
10
10
  * 未知键(#802 / KD-M1-36)→ 产 unknownKeys + 一行可见告警(console.warn + logEvent;
11
11
  * 同档同键集去重、读到零未知键清该档 memo)——非拒定性零变。
12
12
  * - validateManifest(obj) → { ok, errors, missingKeys, unknownKeys }:枚举 / version 数值 /
13
- * docRoot 子键值形态(串 | 数组,F7)+ checkConfig.lineCounts 元素层形态——**纯函数、零 fs**;不落盘。
13
+ * docRoot 子键值形态(串 | 数组 | null(本面无——KD-M1-37),F7)+ checkConfig.lineCounts 元素层形态——**纯函数、零 fs**;不落盘。
14
14
  * - 三族声明键(`codePaths` / `index.{codeExtensions,docExtensions,publicRepos}` / `advisor.{docMap,standardsDoc}`——
15
15
  * KD-M1-31 / M1-32):缺键补默认;形态错 = 档非法(fail-closed,与 docRoot 子键同款);
16
16
  * 读向 = `declaration.mjs`(2026-10-01 自 `conventions.mjs` 迁出——经其转口可达)经 `readManifest` /
@@ -101,13 +101,15 @@ export function docRootBase(cwd) {
101
101
  * `docRoot` 子键值 → **绝对路径数组**(F7 / §2.7 解析管线——全在本模块一处):展开(串 /
102
102
  * 数组统一成列表)→ 逐元素 `trim` + `\` 归一 → `resolve(docRootBase(cwd), p)`(基数 =
103
103
  * 项目根,不回退原始 cwd)→ 去重(保序)。非法形态 → `[]`(零根——拒面在 `validateManifest`,
104
- * 本函数不判错;两处共用 `isValidDocRootValue`,判据单源)。
105
- * @param {unknown} value docRoot 某子键的值(串 | 数组)
104
+ * 本函数不判错;两处共用 `isValidDocRootValue`,判据单源)。`null`(本面无)⇒ `[]`(**合法
105
+ * 零根——非错**;#944 / KD-M1-37)。
106
+ * @param {unknown} value docRoot 某子键的值(串 | 数组 | `null`)
106
107
  * @param {string} [cwd] 会话锚(基数由 docRootBase 解析为项目根)
107
108
  * @returns {string[]} 绝对路径(去重保序)
108
109
  */
109
110
  export function docRootPaths(value, cwd) {
110
111
  if (!isValidDocRootValue(value)) return []
112
+ if (value === null) return [] // 「本面无」(#944):合法零根——非错(不展开、不报错)
111
113
  const base = docRootBase(cwd)
112
114
  const list = (Array.isArray(value) ? value : [value]).map((p) => resolve(base, p.trim().replace(/\\/g, "/")))
113
115
  return [...new Set(list)]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thincoder/core",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "description": "ThinCoder shared core — shared mechanism modules + shared prompt content (CLI ↔ VS Code extension).",
5
5
  "keywords": [
6
6
  "ai",
package/prompts/common.md CHANGED
@@ -155,13 +155,13 @@ Batch independent read-only tool calls into a single reply (they run concurrentl
155
155
  **What it is**: the project's **todo book** — a **requirement pool** (user requirement points) plus **tech todos** (engineering debt); persists across sessions, and it is the **only todo-tracking surface** (checklist retired).
156
156
  **Where it lives**: keyed by **project root**, stored in the **user data directory** (outside the work tree, **never in git**, no file left in the project); an empty read = that project has no ledger yet (the first write creates it).
157
157
  **Six states**: 待讨论 → 待设计 → 在途 → 待核销 (the four **unsettled states**); **已核销 / 已废弃 = archive states** (soft delete — settled entries leave the unsettled surface).
158
- **Who reads / writes**: **reads = every role** (`ledger_query` / `ledger_count`; `cwd` defaults to the session project root — pass an absolute path to touch another project); **writes = the main agent only** (subagent tool assemblies carry no write command).
158
+ **Who reads / writes**: **reads = every role** (`ledger` (action=query/count); `cwd` defaults to the session project root — pass an absolute path to touch another project); **writes = the main agent only** (subagent tool assemblies carry no write command).
159
159
  **Subagent view**: the ledger is your **input** (this batch's entries come with the spawn), **not your write surface** — new findings go back to your parent, which books them.
160
160
  **A note is not a ledger**: an **unresolved** gap / difference / limitation must not rest in prose, comments or inline notes — fix it, surface it upward, or get it booked, so it lands on a **visible todo surface**; 「登记」-style in-doc registration is valid **only for ruled items** (judgment + reasons + resolution window).
161
161
  **Anchors & detail**: entries hang on **pointers** (requirement-doc section / batch-record section / evidence line).
162
162
 
163
163
  ## 批次档常识(Batch record — the carrier of task flow)
164
- **What it is**: the **batch record** = the **carrier** of task flow (the single file threading one implementation round from start to closeout); the path takes the form `docs/batches/<batch>-<topic>.md` — the actual location is the `batchDoc` passed at spawn, never hard-coded.
164
+ **What it is**: the **batch record** = the **carrier** of task flow (the single file threading one implementation round from start to closeout); the path takes the form `<repo>/docs/batches/<batch>-<topic>.md` (relative paths resolve against the session cwd first, then candidate project roots — or absolute) — the actual location is the `batchDoc` passed at spawn, never hard-coded.
165
165
  **Mode scope**: the documentation system is the same across all modes; the difference from engineering mode = whether strong process constraints apply.
166
166
  **Six-segment map (one segment, one author)**: §1 discussion = the main agent · §2 batch task & design = eng-designer · §3 design review findings = the review subagent (advisor) · §4 user approval = the main agent · §5 implementation record = eng-coder · §6 verification & closeout = the main agent (the parent, as seen by subagents).
167
167
  **Writing means**: the `batch` tool — **no path parameter** (the target record is bound to you at spawn); **the segment number follows from your identity**; if the write does not land ⇒ say "§× 未写入" plainly in your report.