@thincoder/core 0.10.8 → 0.10.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/agent/family-tools.mjs +2 -2
  3. package/agent/helpers.mjs +6 -6
  4. package/agent/post-turn.mjs +34 -0
  5. package/agent/run-stages.mjs +43 -9
  6. package/agent/setup.mjs +10 -2
  7. package/agent/turn-loop.mjs +6 -0
  8. package/agent-tools/audit-block.mjs +4 -2
  9. package/agent-tools/batch-gate.mjs +198 -0
  10. package/agent-tools/batch-lifecycle.mjs +216 -36
  11. package/agent-tools/batch-skeleton.mjs +87 -0
  12. package/agent-tools/batch.mjs +11 -3
  13. package/agent-tools/context.mjs +28 -5
  14. package/agent-tools/design-slots.mjs +7 -33
  15. package/agent-tools/doc-check.mjs +44 -6
  16. package/agent-tools/spawn-gates.mjs +100 -41
  17. package/agent-tools/ssh-session.mjs +108 -61
  18. package/agent-tools/ssh-transport.mjs +17 -4
  19. package/agent-tools/ssh-vault.mjs +291 -0
  20. package/agent-tools/subagent-spawn.mjs +18 -11
  21. package/agent-tools/subagent.mjs +13 -7
  22. package/agent-tools/timer.mjs +2 -2
  23. package/config-defaults.mjs +92 -0
  24. package/config-normalize.mjs +109 -0
  25. package/config.mjs +88 -197
  26. package/doc-check/anchors.mjs +54 -12
  27. package/doc-check/index.mjs +161 -12
  28. package/doc-check/version.mjs +84 -0
  29. package/doc-check/width.mjs +5 -4
  30. package/i18n.mjs +2 -0
  31. package/ledger-cmd.mjs +314 -61
  32. package/ledger-db.mjs +22 -10
  33. package/ledger-migrate.mjs +15 -9
  34. package/ledger-tools.mjs +115 -69
  35. package/ledger.mjs +13 -10
  36. package/log.mjs +2 -2
  37. package/manifest-schema.mjs +15 -1
  38. package/package.json +1 -1
  39. package/permission.mjs +7 -1
  40. package/prompts/advisor-design.md +21 -21
  41. package/prompts/advisor-round1.md +18 -18
  42. package/prompts/advisor-round2.md +22 -22
  43. package/prompts/advisor-round3.md +24 -24
  44. package/prompts/common.md +108 -93
  45. package/prompts/consult-base.md +8 -8
  46. package/prompts/discipline-engineering.md +68 -65
  47. package/prompts/discipline-normal.md +69 -68
  48. package/prompts/persona-coder.md +6 -6
  49. package/prompts/persona-eng-coder.md +19 -18
  50. package/prompts/persona-eng-designer.md +36 -32
  51. package/prompts/persona-engineering.md +114 -98
  52. package/prompts/persona-explore.md +2 -2
  53. package/prompts/persona-normal.md +16 -16
  54. package/prompts/persona-plan.md +7 -7
  55. package/prompts/subagent-base.md +4 -4
  56. package/provider/anthropic.mjs +4 -2
  57. package/provider/core.mjs +4 -2
  58. package/provider/google.mjs +5 -1
  59. package/provider/responses-request.mjs +3 -2
  60. package/session-lifecycle.mjs +6 -6
  61. package/team.mjs +2 -1
  62. package/token-window.mjs +1 -1
  63. package/tool-docs/batch.md +1 -1
  64. package/tool-docs/doc_check.md +30 -7
  65. package/tool-docs/fetch.md +3 -2
  66. package/tool-docs/ledger.md +7 -5
  67. package/tool-docs/ssh.md +23 -6
  68. package/tool-docs/subagent.md +6 -11
  69. package/tool-docs/websearch.md +3 -1
  70. package/tools/ssh.mjs +24 -11
  71. package/tools/web-deepseek.mjs +172 -0
  72. package/tools/web.mjs +29 -4
package/ledger-tools.mjs CHANGED
@@ -10,7 +10,7 @@
10
10
  * 接线:族出口 = `ledger.mjs`(re-export——消费面 agent/family-tools.mjs;旧读二装配面已清零);
11
11
  * 消费侧一律动态 import(`ledger-db.mjs` 静态 import node:sqlite ⇒ W8 契约②)。
12
12
  */
13
- import { ledgerAdd, ledgerClose, ledgerCount, ledgerQuery, ledgerUpdate } from "./ledger-cmd.mjs"
13
+ import { ledgerAdd, ledgerClose, ledgerCloseBatch, ledgerCount, ledgerQuery, ledgerUpdate, ledgerUpdateBatch } from "./ledger-cmd.mjs"
14
14
  import { resolveLedgerRoot } from "./ledger-db.mjs"
15
15
  import { DESC } from "./tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/ledger.md(DESC 单解析面)
16
16
 
@@ -37,37 +37,47 @@ const argPreview = (v) => {
37
37
  /** 枚举取值域文本(P3 / P5 文案同源)。 */
38
38
  const enumText = (values) => `{${values.join(", ")}}`
39
39
 
40
- /** P3 后缀(声明派生):枚举字段带取值域 / 非枚举必填串带「非空字符串」/ 数字字段仅「必填」。 */
41
- const requiredSuffix = (spec) => (spec.enum ? `;取值 ∈ ${enumText(spec.enum)}` : spec.type === "string" ? ";非空字符串" : "")
40
+ /** P3 后缀(声明派生):枚举字段带取值域 / 非枚举必填串带「非空字符串」/ 数字字段仅「必填」
41
+ * (`domain` = 按形条件判据给出的取值域——`whenPresent.enum` 优先于基本形)。 */
42
+ const requiredSuffix = (spec, domain) => (domain ? `;取值 ∈ ${enumText(domain)}` : spec.type === "string" ? ";非空字符串" : "")
43
+
44
+ /** 值非空字段(P6 字段级:空串 / 全空白 ⇒ 拒——声明面不表达值级判据,与 `title` 同源一处)。 */
45
+ const NON_EMPTY_FIELDS = ["title", "resume_when"]
42
46
 
43
47
  /**
44
- * 工具层参数守卫(设计档 §3.2 · Gitee #IKIQGK / 台账 #472):判序 P1–P6,文案逐字 = §3.2 模板;
45
- * 消费工具自身 `parameters` 声明派生(`required` / `type` / `enum`——枚举 / 必填 / 类型不在守卫内复写);
46
- * 只判不改(零缺省填充 / 零类型转换 / 零文本改写),返回入参对象(唯一归一 = 判序① 的缺省 / `null`
47
- * ⇒ `{}`)。非法 ⇒ throw——判于核函数之前(零写、零库动作)。
48
+ * 工具层参数守卫(设计档 §3.2 · Gitee #IKIQGK / 台账 #472):判序 P1–P7,文案逐字 = §3.2 模板;
49
+ * 消费工具自身声明派生(`required` / `type` / `enum` / `oneOfRequired` / `whenPresent`——枚举 / 必填 /
50
+ * 类型 / 组判据不在守卫内复写);只判不改(零缺省填充 / 零类型转换 / 零文本改写),返回入参对象
51
+ * (唯一归一 = 判序① 的缺省 / `null` ⇒ `{}`)。非法 ⇒ throw——判于核函数之前(零写、零库动作)。
52
+ * 按形条件判据(`whenPresent`):`when` 字段在本笔入参**在场**(非 `null`)⇒ 该条生效(`required` 按
53
+ * P3 判、`enum` 按 P5 判并取代基本形取值域);不在场 ⇒ 本条不生效、按基本形判。
48
54
  */
49
55
  function assertToolArgs(tool, args) {
50
56
  const name = tool.name
51
57
  const decl = tool.parameters ?? {}
52
58
  const props = decl.properties ?? {}
53
59
  const fields = Object.keys(props)
60
+ // 组判据两键居声明条目顶层(不入 `parameters`——自定义键不随声明交模型 / provider)
61
+ const conditional = tool.whenPresent ?? []
54
62
  // P1:入参缺省 / `null` ⇒ 视作 `{}`;非对象(数组 / 标量)⇒ 拒
55
63
  if (args == null) args = {}
56
64
  else if (typeof args !== "object" || Array.isArray(args)) throw new Error(`${name}:参数须为对象(收到 ${argPreview(args)})`)
57
65
  // P2:未知键 ⇒ 拒(判序先于缺参——typo 键先给可用参数集)
58
66
  const unknown = Object.keys(args).filter((k) => !Object.hasOwn(props, k))
59
67
  if (unknown.length > 0) throw new Error(`${name}:未知参数:${unknown.join(" / ")}(可用参数 = ${fields.join(" / ")})`)
60
- // P3–P6:逐字段(声明序)——缺失(必填;显式 `null` 同判)→ 类型 / 枚举 → `title` 空串 / 全空白
68
+ // P3–P6:逐字段(声明序)——缺失(必填;显式 `null` 同判)→ 类型 / 枚举 → 值非空
61
69
  for (const field of fields) {
62
70
  const spec = props[field]
63
71
  const value = args[field]
64
- const required = (decl.required ?? []).includes(field)
72
+ const rule = conditional.find((c) => c.field === field && args[c.when] != null)
73
+ const required = rule?.required ?? (decl.required ?? []).includes(field)
74
+ const domain = rule?.enum ?? spec.enum
65
75
  if (value === undefined || value === null) {
66
- if (required) throw new Error(`${name}:${field} 缺失(必填${requiredSuffix(spec)})`)
76
+ if (required) throw new Error(`${name}:${field} 缺失(必填${requiredSuffix(spec, domain)})`)
67
77
  continue // 可空字段 `null` ⇒ 放行(≡ 略去——与 `patch.x ?? 行值` 既有语义同源)
68
78
  }
69
- if (spec.enum) {
70
- if (!spec.enum.includes(value)) throw new Error(`${name}:${field} 非法:${argPreview(value)}(取值 ∈ ${enumText(spec.enum)})`)
79
+ if (domain) {
80
+ if (!domain.includes(value)) throw new Error(`${name}:${field} 非法:${argPreview(value)}(取值 ∈ ${enumText(domain)})`)
71
81
  } else if (spec.type === "string" && typeof value !== "string") {
72
82
  throw new Error(`${name}:${field} 非法:${argPreview(value)}(应为字符串)`)
73
83
  } else if (spec.type === "number" && typeof value !== "number") {
@@ -75,10 +85,16 @@ function assertToolArgs(tool, args) {
75
85
  } else if (spec.type === "boolean" && typeof value !== "boolean") {
76
86
  throw new Error(`${name}:${field} 非法:${argPreview(value)}(应为布尔)`)
77
87
  }
78
- if (field === "title" && typeof value === "string" && value.trim() === "") {
88
+ if (NON_EMPTY_FIELDS.includes(field) && typeof value === "string" && value.trim() === "") {
79
89
  throw new Error(`${name}:${field} 为空(${required ? "必填;" : ""}非空字符串)`)
80
90
  }
81
91
  }
92
+ // P7:组判据(声明派生 `oneOfRequired`)——组内恰携其一 ⇒ 过;同携 / 双缺 ⇒ 拒
93
+ for (const group of tool.oneOfRequired ?? []) {
94
+ if (group.filter((f) => args[f] != null).length === 1) continue
95
+ const received = Object.fromEntries(group.map((f) => [f, args[f] ?? null]))
96
+ throw new Error(`${name}:${group.join(" ∥ ")} 须恰携其一(收到 ${argPreview(received)})`)
97
+ }
82
98
  return args
83
99
  }
84
100
 
@@ -91,68 +107,90 @@ const READ_ACTIONS = ["query", "count"]
91
107
  /** cwd 参数描述(五前身声明同文——文本单点)。 */
92
108
  const CWD_DESC = "本次动作的目标路径——项目根由它推导:沿祖先链最近的项目目录(带 PROJECT-MANIFEST.json 或已有台账库);容器目录下收敛到子项目。台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话锚"
93
109
 
94
- /** 各 action 声明条目(= 前身旧工具 `parameters` 逐字;伪工具对象守卫 + 弃用壳共用单源)。 */
110
+ /** 各 action 声明条目(= 前身旧工具 `parameters` 逐字;伪工具对象守卫 + 弃用壳共用单源)。
111
+ * `parameters` 内的键 = 交模型 / provider 的那一份声明;**组判据两键(`oneOfRequired` / `whenPresent`)
112
+ * 居本条条目顶层、不入 `parameters`**——自定义键不随声明外泄(provider 面零风险),守卫仍只读声明。 */
95
113
  const ACTION_SPECS = {
96
114
  add: {
97
- type: "object",
98
- required: ["kind", "title"],
99
- properties: {
100
- cwd: { type: "string", description: CWD_DESC },
101
- kind: { type: "string", enum: ["requirement", "tech_todo"], description: "类别" },
102
- title: { type: "string", description: "条目标题" },
103
- board: { type: "string", description: "归属板块(需求档文档名,可空)" },
104
- req_doc: { type: "string", description: "需求档指针(可空)" },
105
- task_book: { type: "string", description: "任务书指针(可空;在途 / 待核销必填——咬合 CHECK)" },
106
- evidence: { type: "string", description: "最小证据行(可空)" },
107
- trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "技术待办触发(可空)" },
115
+ parameters: {
116
+ type: "object",
117
+ required: ["kind", "title"],
118
+ properties: {
119
+ cwd: { type: "string", description: CWD_DESC },
120
+ kind: { type: "string", enum: ["requirement", "tech_todo"], description: "类别" },
121
+ title: { type: "string", description: "条目标题" },
122
+ board: { type: "string", description: "归属板块(需求档文档名,可空)" },
123
+ req_doc: { type: "string", description: "需求档指针(可空)" },
124
+ task_book: { type: "string", description: "任务书指针(可空;在途 / 待核销必填——咬合 CHECK)" },
125
+ evidence: { type: "string", description: "最小证据行(可空)" },
126
+ trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "技术待办触发(可空)" },
127
+ resume_when: { type: "string", description: "复启条件句(可空;`trigger=条件` 行的一句话条件 + 可选锚 `@YYYY-MM-DD` ∥ `@事件件名`)——值非空:空串 / 全空白 ⇒ 拒" },
128
+ },
129
+ additionalProperties: false,
108
130
  },
109
- additionalProperties: false,
110
131
  },
111
132
  update: {
112
- type: "object",
113
- required: ["id"],
114
- properties: {
115
- cwd: { type: "string", description: CWD_DESC },
116
- id: { type: "number", description: "条目 id" },
117
- status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "目标状态(六态之一,缺省 = 不变)" },
118
- title: { type: "string", description: "标题(缺省 = 不变)" },
119
- board: { type: "string", description: "归属板块(缺省 = 不变)" },
120
- req_doc: { type: "string", description: "需求档指针(缺省 = 不变)" },
121
- task_book: { type: "string", description: "任务书指针(缺省 = 不变)" },
122
- evidence: { type: "string", description: "证据(缺省 = 不变;传入 = 追加——旧值非空 ⇒ 拼 `旧|新`(「|」零空格)、旧空直写;新值全空白 ⇒ 拒;覆盖 = 与 `evidenceReplace: true` 同传)" },
123
- evidenceReplace: { type: "boolean", description: "覆盖旗(布尔):与 evidence 同传 = 整段替换(旧文零保留);缺省 / false = 追加;旗独传(无新文)⇒ 拒" },
124
- trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "触发(缺省 = 不变)" },
125
- executor: { type: "string", description: "执行者 sessionId(可选——接手改写归属用;缺省 = 按状态迁移语义:进入待设计 / 在途自动写本会话;出在途 / 撤回清空)" },
133
+ parameters: {
134
+ type: "object",
135
+ properties: {
136
+ cwd: { type: "string", description: CWD_DESC },
137
+ id: { type: "number", description: "条目 id(与 batch 恰携其一)" },
138
+ batch: { type: "string", description: "批档指针(与 id 恰携其一)——批级批量:一次结算该批的行;携 status ⇒ 逐行按迁移表重判,不携 status ⇒ 纯字段补丁(status 零写)" },
139
+ status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "目标状态(六态之一,缺省 = 不变)" },
140
+ title: { type: "string", description: "标题(缺省 = 不变)" },
141
+ board: { type: "string", description: "归属板块(缺省 = 不变)" },
142
+ req_doc: { type: "string", description: "需求档指针(缺省 = 不变)" },
143
+ task_book: { type: "string", description: "任务书指针(缺省 = 不变)" },
144
+ evidence: { type: "string", description: "证据(缺省 = 不变;传入 = 追加——旧值非空 ⇒ 拼 `旧|新`(「|」零空格)、旧空直写;新值全空白 ⇒ 拒;覆盖 = 与 `evidenceReplace: true` 同传)" },
145
+ evidenceReplace: { type: "boolean", description: "覆盖旗(布尔):与 evidence 同传 = 整段替换(旧文零保留);缺省 / false = 追加;旗独传(无新文)⇒ 拒" },
146
+ trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "触发(缺省 = 不变)" },
147
+ resume_when: { type: "string", description: "复启条件句(缺省 = 不变;值非空——空串 / 全空白 ⇒ 拒)" },
148
+ executor: { type: "string", description: "执行者 sessionId(可选——接手改写归属用;缺省 = 按状态迁移语义:进入待设计 / 在途自动写本会话;出在途 / 撤回清空)" },
149
+ },
150
+ additionalProperties: false,
126
151
  },
127
- additionalProperties: false,
152
+ oneOfRequired: [["id", "batch"]],
128
153
  },
129
154
  close: {
130
- type: "object",
131
- required: ["id", "status"],
132
- properties: {
133
- cwd: { type: "string", description: CWD_DESC },
134
- id: { type: "number", description: "条目 id" },
135
- status: { type: "string", enum: ["已核销", "已废弃"], description: "目标态(勾销 / 追认核销 / 撤回)" },
136
- evidence: { type: "string", description: "最小证据行(可选;传入 = 追加缺省——旧值非空 ⇒ 拼 `旧|新`(「|」零空格)、旧空直写;新值全空白 ⇒ 拒;覆盖 = 与 `evidenceReplace: true` 同传。追认核销面判结果值须非空:缺 / 全空白 ⇒ 拒)" },
137
- evidenceReplace: { type: "boolean", description: "覆盖旗(布尔):与 evidence 同传 = 整段替换(旧文零保留);缺省 / false = 追加;旗独传(无新文)⇒ 拒" },
155
+ parameters: {
156
+ type: "object",
157
+ required: ["status"],
158
+ properties: {
159
+ cwd: { type: "string", description: CWD_DESC },
160
+ id: { type: "number", description: "条目 id(与 batch 恰携其一)" },
161
+ batch: { type: "string", description: "批档指针(与 id 恰携其一)——批级批量结算:只销已到 待核销 的行;待讨论 / 待设计 点名跳过(追认核销不入批量面)" },
162
+ status: { type: "string", enum: ["已核销", "已废弃"], description: "目标态(勾销 / 追认核销 / 撤回)——携 batch 时可不给(缺省 = 勾销单值),给了只能是 已核销" },
163
+ evidence: { type: "string", description: "最小证据行(可选;传入 = 追加缺省——旧值非空 ⇒ 拼 `旧|新`(「|」零空格)、旧空直写;新值全空白 ⇒ 拒;覆盖 = 与 `evidenceReplace: true` 同传。追认核销面判结果值须非空:缺 / 全空白 ⇒ 拒)" },
164
+ evidenceReplace: { type: "boolean", description: "覆盖旗(布尔):与 evidence 同传 = 整段替换(旧文零保留);缺省 / false = 追加;旗独传(无新文)⇒ 拒" },
165
+ },
166
+ additionalProperties: false,
138
167
  },
139
- additionalProperties: false,
168
+ oneOfRequired: [["id", "batch"]],
169
+ whenPresent: [{ when: "batch", field: "status", required: false, enum: ["已核销"] }],
140
170
  },
141
171
  query: {
142
- type: "object",
143
- properties: {
144
- cwd: { type: "string", description: CWD_DESC },
145
- status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "按状态过滤(六态之一,缺省 = 不过滤)" },
146
- kind: { type: "string", enum: ["requirement", "tech_todo"], description: "按类别过滤" },
147
- board: { type: "string", description: "按归属板块过滤(需求档文档名)" },
148
- trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "按技术待办触发过滤(三枚举,缺省 = 不过滤——无触发(NULL)不过滤)" },
172
+ parameters: {
173
+ type: "object",
174
+ properties: {
175
+ cwd: { type: "string", description: CWD_DESC },
176
+ status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "按状态过滤(六态之一,缺省 = 不过滤)" },
177
+ kind: { type: "string", enum: ["requirement", "tech_todo"], description: "按类别过滤" },
178
+ board: { type: "string", description: "按归属板块过滤(需求档文档名)" },
179
+ trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "按技术待办触发过滤(三枚举,缺省 = 不过滤——无触发(NULL)不过滤)" },
180
+ task_book: { type: "string", description: "按任务书指针过滤(原值等值——批→行反查;缺省 = 不过滤)" },
181
+ },
182
+ additionalProperties: false,
149
183
  },
150
- additionalProperties: false,
151
184
  },
152
185
  count: {
153
- type: "object",
154
- properties: { cwd: { type: "string", description: CWD_DESC } },
155
- additionalProperties: false,
186
+ parameters: {
187
+ type: "object",
188
+ properties: {
189
+ cwd: { type: "string", description: CWD_DESC },
190
+ task_book: { type: "string", description: "按任务书指针过滤(原值等值)" },
191
+ },
192
+ additionalProperties: false,
193
+ },
156
194
  },
157
195
  }
158
196
 
@@ -171,8 +209,10 @@ const UNION_PARAMETERS = {
171
209
  evidence: { type: "string", description: "最小证据行(add 可空;update / close 传入 = 追加缺省——旧空直写、旧非空拼 `旧|新`(「|」零空格)、新值全空白 ⇒ 拒;覆盖 = 与 `evidenceReplace: true` 同传;update 缺省 = 不变 / close 缺省 = 判行值;追认核销须非空)" },
172
210
  evidenceReplace: { type: "boolean", description: "覆盖旗(布尔):与 evidence 同传 = 整段替换;缺省 / false = 追加;旗独传(无新文)⇒ 拒(update / close)" },
173
211
  trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "技术待办触发(add / update 可空 / query 过滤)" },
174
- id: { type: "number", description: "条目 id(update / close 必填)" },
175
- status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "状态(close 必填 ∈ {已核销, 已废弃};update 迁移 / query 过滤——六态之一)" },
212
+ resume_when: { type: "string", description: "复启条件句(add / update 可空;值非空——空串 / 全空白 ⇒ 拒;`trigger=条件` 行的一句话条件 + 可选锚 `@YYYY-MM-DD` ∥ `@事件件名`)" },
213
+ id: { type: "number", description: "条目 id(update / close:与 batch 恰携其一)" },
214
+ batch: { type: "string", description: "批档指针(update / close:与 id 恰携其一)——批级批量:一次结算该批的行(update 携 status ⇒ 逐行迁移表重判;不携 status ⇒ 纯字段补丁;close 只销 待核销 行、status 可空)" },
215
+ status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "状态(单行 close 必填 ∈ {已核销, 已废弃};close 携 batch 时可空、给了只能是 已核销;update 迁移 / query 过滤——六态之一)" },
176
216
  executor: { type: "string", description: "执行者 sessionId(update 可选——接手改写归属;缺省 = 按状态迁移语义:进入待设计 / 在途自动写本会话;出在途 / 撤回清空)" },
177
217
  },
178
218
  additionalProperties: false,
@@ -210,8 +250,10 @@ function makeLedgerTool(actions, { readonly = false, writeFace = false } = {}) {
210
250
  // ② 入口级 action 判(P3 缺失 / P5 非法——消费 action;取值域 = 本装配面枚举)
211
251
  const { action, ...rest0 } = args
212
252
  assertToolArgs(entryTool, { action })
213
- // ③ 余键喂该 action 伪工具对象(声明源 = 前身工具 parameters 逐字——`action` 键不入 P2 域)
214
- const rest = assertToolArgs({ name: `ledger(${action})`, parameters: ACTION_SPECS[action] }, rest0)
253
+ // ③ 余键喂该 action 伪工具对象(声明源 = 前身工具 parameters 逐字——`action` 键不入 P2 域;
254
+ // 组判据两键在条目顶层,与 `parameters` 同送守卫)
255
+ const spec = ACTION_SPECS[action]
256
+ const rest = assertToolArgs({ name: `ledger(${action})`, ...spec }, rest0)
215
257
  // ④ 路由既有核函数(包装细节携自原工具 execute 逐字保留:cwd 缺省 / JSON 缩进 / getSessionId 动态 import)
216
258
  switch (action) {
217
259
  case "add": {
@@ -221,7 +263,7 @@ function makeLedgerTool(actions, { readonly = false, writeFace = false } = {}) {
221
263
  return JSON.stringify({ id, status: "待讨论", ...reportedRoot(target) })
222
264
  }
223
265
  case "update": {
224
- const { cwd, id, ...patch } = rest
266
+ const { cwd, id, batch, ...patch } = rest
225
267
  const target = cwd ?? cwdOf(ctx)
226
268
  let executorSessionId
227
269
  if (patch.executor == null) {
@@ -229,17 +271,21 @@ function makeLedgerTool(actions, { readonly = false, writeFace = false } = {}) {
229
271
  const { getSessionId } = await import("./session-slots.mjs")
230
272
  executorSessionId = getSessionId()
231
273
  }
274
+ // 批级批量(§3.3)——`batch` 与 `id` 恰携其一(守卫 P7 已先裁)
275
+ if (batch != null) return JSON.stringify({ ...ledgerUpdateBatch({ cwd: target, batch, patch, executorSessionId }), ...reportedRoot(target) })
232
276
  return JSON.stringify({ ...ledgerUpdate({ cwd: target, id, patch, executorSessionId }), ...reportedRoot(target) })
233
277
  }
234
278
  case "close": {
235
279
  const target = rest.cwd ?? cwdOf(ctx)
280
+ // 批级批量结算(§3.3——勾销径)
281
+ if (rest.batch != null) return JSON.stringify({ ...ledgerCloseBatch({ cwd: target, batch: rest.batch, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace }), ...reportedRoot(target) })
236
282
  return JSON.stringify({ ...ledgerClose({ cwd: target, id: rest.id, status: rest.status, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace }), ...reportedRoot(target) })
237
283
  }
238
284
  case "query":
239
- return JSON.stringify(ledgerQuery({ cwd: rest.cwd ?? cwdOf(ctx), status: rest.status, kind: rest.kind, board: rest.board, trigger: rest.trigger }), null, 2)
285
+ return JSON.stringify(ledgerQuery({ cwd: rest.cwd ?? cwdOf(ctx), status: rest.status, kind: rest.kind, board: rest.board, trigger: rest.trigger, task_book: rest.task_book }), null, 2)
240
286
  case "count": {
241
287
  const target = rest.cwd ?? cwdOf(ctx)
242
- return JSON.stringify({ count: ledgerCount({ cwd: target }), ...reportedRoot(target) })
288
+ return JSON.stringify({ ...ledgerCount({ cwd: target, task_book: rest.task_book }), ...reportedRoot(target) })
243
289
  }
244
290
  default:
245
291
  // 不可达(② 枚举守门已先拒)——fail-loud 兜底(防未来变体增量 drift)
@@ -262,7 +308,7 @@ const deprecatedLine = (old, action) => `${old} 已退役(本名不再装配
262
308
  const deprecatedShell = (old, action, readonly) => ({
263
309
  name: old,
264
310
  description: deprecatedLine(old, action),
265
- parameters: ACTION_SPECS[action],
311
+ parameters: ACTION_SPECS[action].parameters,
266
312
  readonly,
267
313
  async execute() { throw new Error(deprecatedLine(old, action)) },
268
314
  })
package/ledger.mjs CHANGED
@@ -65,9 +65,11 @@ export function discoverFamily(anchor) {
65
65
  }
66
66
 
67
67
  /** scan 组装(SQLite 行集 + 计数单源 → scan 对象)——形状契约(设计档 §2.2):pool/tech/aged/
68
- * agedKeys/agedTitles/thresholdReached/boards/actionable/root/name/ledger。行龄 = 时间戳
68
+ * agedKeys/agedTitles/overdue/thresholdReached/boards/actionable/root/name/ledger。行龄 = 时间戳
69
69
  * (updated_at ?? created_at 距今 > days;未知不计——零假阳降级,同 v1 行龄未知口径)。
70
70
  * `now`/`days` = 注入面(确定性用例——替代 v1 ageOf git 注入面)。
71
+ * **`days` 只作用于展示面 `aged`**(本函数局部阈值);`overdue`(出口态)恒按核常量 `AGING_DAYS`
72
+ * (`overdueOf` 单源)——故 `days` 调小时 `aged` 会先于 `overdue` 涨(两数口径不同,非矛盾)。
71
73
  * **根推导**(设计档 §3.8):`root` / `name` / `ledger` = `resolveLedgerRoot(anchor)` 推导所得项目根(非锚
72
74
  * ——容器 ∥ 工作区锚取收敛序之根)——与行集读取同一单源;无根 ⇒ `ledger` = `null`(不落兜底根——
73
75
  * 该态下 `root` / `name` 回落锚、行集为空,且该 scan 不进族枚举(`discoverFamily` 容器支只列已注册子目录))。
@@ -77,7 +79,7 @@ export function buildScan({ cwd, days = AGING_DAYS, now = Date.now() } = {}) {
77
79
  const anchor = resolve(cwd)
78
80
  const derived = resolveLedgerRoot(anchor)
79
81
  const root = derived ? derived.root : anchor
80
- const rows = ledgerQuery({ cwd: anchor })
82
+ const rows = ledgerQuery({ cwd: anchor, now }) // `now` 透传(两读数同时钟——`overdue` 的行键由核同一 `now` 算)
81
83
  const pending = rows.filter((r) => PENDING_STATUSES.includes(r.status))
82
84
  const poolEntries = pending.filter((r) => r.kind === "requirement")
83
85
  const techEntries = pending.filter((r) => r.kind === "tech_todo")
@@ -98,6 +100,7 @@ export function buildScan({ cwd, days = AGING_DAYS, now = Date.now() } = {}) {
98
100
  return {
99
101
  name: basename(root), root, ledger: derived ? ledgerDbPath(root) : null,
100
102
  pool: poolEntries.length, tech: techEntries.length, aged: aged.length,
103
+ overdue: pending.filter((r) => r.overdue != null).length, // 待处置(§3.7——机判单源 = 行集 `overdue` 键,行值与 `now` 均同源)
101
104
  agedKeys: aged.map((r) => normalizeEntry(r.title)), agedTitles: aged.map((r) => entryTitle(r.title)),
102
105
  thresholdReached, boards, actionable: aged.length > 0 || thresholdReached,
103
106
  inflightExecutors,
@@ -113,8 +116,8 @@ export function formatMarker(scan) {
113
116
  * `root` = `family.current.root` 的 scan(命中但不在 `scans`——不可读 ⇒ 空范围,不掺兄弟);
114
117
  * `current` 缺席(容器根锚)⇒ 范围 = `scans` 全体(族内已读项目——构建期不可读已跳过)。
115
118
  * 空范围 ⇒ `{ marker: null, warn: false }`(不落 `0·0`);范围非空 ⇒ `marker` = `formatMarker`
116
- * 逐字(两池分列求和——F1);`warn` = 范围内任一项 `aged>0 ∨ deadExecutors>0`(端零重算)。
117
- * 消费面 = 核拍面(`ledger-surface.mjs`)∥ VSC item——端零自算单源。 */
119
+ * 逐字(两池分列求和——F1);`warn` = 范围内任一项 `overdue>0 ∨ deadExecutors>0`(§3.7——`aged ⊂
120
+ * overdue` ⇒ 警示色不缩;端零重算)。消费面 = 核拍面(`ledger-surface.mjs`)∥ VSC item。 */
118
121
  export function scopeMarkerOf(scans, family) {
119
122
  const list = Array.isArray(scans) ? scans : []
120
123
  const currentRoot = family?.current?.root ?? null
@@ -126,15 +129,15 @@ export function scopeMarkerOf(scans, family) {
126
129
  for (const s of range) {
127
130
  pool += s.pool
128
131
  tech += s.tech
129
- if (s.aged > 0 || s.deadExecutors > 0) warn = true
132
+ if (s.overdue > 0 || s.deadExecutors > 0) warn = true
130
133
  }
131
134
  return { marker: formatMarker({ pool, tech }), warn }
132
135
  }
133
136
 
134
- /** L2 明细行(逐字——`(老化 <n>)` 恒显;阈值达成加 ` — 可开批`;判活尾段 = executorTail 三态——
135
- * 在途 executor 缺席时尾段空串,既有逐字断言零破)。 */
137
+ /** L2 明细行(逐字——`(老化 <n> · 待处置 <m>)` 两数恒显(含 0——§3.7 出口面恒显位扩);
138
+ * 阈值达成加 ` — 可开批`;判活尾段 = executorTail 三态——在途 executor 缺席时尾段空串)。 */
136
139
  export function formatDetailLine(scan) {
137
- return `台账 ${scan.name}:需求池 ${scan.pool} · 技术待办 ${scan.tech}(老化 ${scan.aged})${scan.thresholdReached ? " — 可开批" : ""}${executorTail(scan)}`
140
+ return `台账 ${scan.name}:需求池 ${scan.pool} · 技术待办 ${scan.tech}(老化 ${scan.aged} · 待处置 ${scan.overdue})${scan.thresholdReached ? " — 可开批" : ""}${executorTail(scan)}`
138
141
  }
139
142
 
140
143
  /** 明细行集 = {current} ∪ {可动作项目}(发现序——current 在前,其余按目录名升序;同项去重)。 */
@@ -148,8 +151,8 @@ export function detailScans(scans, current) {
148
151
  // ── re-export(拆分件接口——命令面接线 = 动态 import 本档,KD-M2-3;`AGING_DAYS` 常量单源 = `ledger-db.mjs`
149
152
  // ——2026-10-07 批 ledger-tool 自本档迁入,公共面零变;`ledgerChildren` / `MAX_SIBLING_SCAN` / `resolveLedgerRoot`
150
153
  // ——2026-10-16 批 ledger-root-derivation 随迁 / 新增,公共面形零改) ──
151
- export { AGING_DAYS, ALLOWED_MIGRATIONS, MAX_SIBLING_SCAN, ledgerChildren, ledgerDbPath, ledgerKey, ledgerDirPath, openLedger, PENDING_STATUSES, resolveLedgerRoot, _setLedgerDirForTest, _resetLedgerDirForTest, ensureExecutorColumn } from "./ledger-db.mjs"
152
- export { ledgerAdd, ledgerClose, ledgerCount, ledgerQuery, ledgerUpdate } from "./ledger-cmd.mjs"
154
+ export { AGING_DAYS, ALLOWED_MIGRATIONS, EVIDENCE_SOFT_LIMIT, MAX_SIBLING_SCAN, ledgerChildren, ledgerDbPath, ledgerKey, ledgerDirPath, openLedger, PENDING_STATUSES, resolveLedgerRoot, _setLedgerDirForTest, _resetLedgerDirForTest, ensureColumn, ensureExecutorColumn } from "./ledger-db.mjs"
155
+ export { ledgerAdd, ledgerBatchRows, ledgerClose, ledgerCloseBatch, ledgerCount, ledgerEvidenceOversize, ledgerQuery, ledgerReanchorBatch, ledgerUpdate, ledgerUpdateBatch, overdueOf } from "./ledger-cmd.mjs"
153
156
  export { ledgerTool, ledgerReadTool, ledgerAddTool, ledgerCloseTool, ledgerCountTool, ledgerQueryTool, ledgerUpdateTool } from "./ledger-tools.mjs"
154
157
  export { runLedgerAudit, runLedgerMigrate } from "./ledger-migrate.mjs"
155
158
  export { resolveExecutorStates, executorTail, _setExecutorProbeTtlForTest } from "./ledger-executors.mjs"
package/log.mjs CHANGED
@@ -18,8 +18,8 @@
18
18
  * → 整个字段丢弃;
19
19
  * ② 内容只扫密钥形态(sk-xxx / Bearer xxx / key=… / token 值形等)→ 截断到形态之前
20
20
  * ——宁可丢信息不漏密钥。
21
- * token 值形(`uuid:epoch`——2026-10-14 design-token-recovery 批并入):design-slots
22
- * reveal 结果经 `tool:done` head 落盘时不落值(与 batch.mjs 凭据第三形同判据)。
21
+ * token 值形(`uuid:epoch`——2026-10-14 批并入):工具结果经 `tool:done` head 落盘
22
+ * 时在形态前截断、不落值(与 batch.mjs 凭据第三形同判据)。
23
23
  * 工具事件不记 args;URL 不入事件(llm/tool 事件从不携带 URL;err 文本经形态扫描)。
24
24
  * - 摘要截断(B 方案,2026-09-03 用户裁定):截断处带 "…" 标记(截后仍 ≤上限)。
25
25
  * - 测试隔离:node --test 进程(NODE_TEST_CONTEXT)默认不写盘——防测试事件污染真实
@@ -25,6 +25,20 @@ export function isValidDocRootValue(value) {
25
25
  return value.every((p) => typeof p === "string" && p.trim() !== "")
26
26
  }
27
27
 
28
+ /** `checkConfig.anchors.roots` **单元素值形判据**(判据单源——解析根 = 相对项目根的目录):
29
+ * 合法 ⇒ `null`;非法 ⇒ 理由句(非空字符串 ∥ 相对路径 ∥ 无 `..` 越界段)。
30
+ * 消费面两处:产品引擎 `doc-check/anchors.mjs`(报告态逐元素弃用)∥ `doc-check/index.mjs`
31
+ * (gate 面 `configError` 拒,fail-closed)。
32
+ * @param {unknown} value 解析根元素
33
+ * @returns {string|null}
34
+ */
35
+ export function anchorRootError(value) {
36
+ if (typeof value !== "string" || value.trim() === "") return "元素须为非空字符串"
37
+ if (/^(?:[A-Za-z]:[\\/]|[\\/])/.test(value)) return "元素须为相对项目根的路径(不得为绝对路径)"
38
+ if (value.split(/[\\/]/).includes("..")) return "元素不得含 `..` 越界段"
39
+ return null
40
+ }
41
+
28
42
  /** 默认八键(架构 §2.3 E1 五键 + 三族声明键——KD-M1-31)——整档初始化的写源与缺键 fallback 的补源。 */
29
43
  export const DEFAULT_MANIFEST = Object.freeze({
30
44
  version: 1,
@@ -41,7 +55,7 @@ export const DEFAULT_MANIFEST = Object.freeze({
41
55
  scanDirs: Object.freeze(["docs"]),
42
56
  lineWidth: 300,
43
57
  widthExemptZones: Object.freeze([]),
44
- anchors: Object.freeze({ domain: "docs", exclude: Object.freeze(["_archive", "batches"]) }),
58
+ anchors: Object.freeze({ domain: "docs", exclude: Object.freeze(["_archive", "batches"]), roots: Object.freeze([]) }),
45
59
  exemptions: Object.freeze([]),
46
60
  }),
47
61
  // 项目声明三族(2026-09-27 声明载体唯一化批并入——键名 / 形态逐字承前;来源档已退役)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thincoder/core",
3
- "version": "0.10.8",
3
+ "version": "0.10.9",
4
4
  "description": "ThinCoder shared core — shared mechanism modules + shared prompt content (CLI ↔ VS Code extension).",
5
5
  "keywords": [
6
6
  "ai",
package/permission.mjs CHANGED
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { createInterface } from "node:readline"
16
16
  import { lastPageUrl } from "./browser/session.mjs"
17
+ import { composeTaskBookBrief } from "./agent-tools/spawn-gates.mjs"
17
18
 
18
19
  /** CLI tool arg summary (truncated long JSON) */
19
20
  export function summarize(toolArgs) {
@@ -34,7 +35,7 @@ export function formatPermission(name, args) {
34
35
  return `${args.path}\n${oldLines}\n ↓\n${newLines}`
35
36
  }
36
37
  if (base === "delete") return `${args.path}${args.force ? "(force:跟踪文件也删)" : ""}`
37
- if (base === "subagent") return cap(args.task ?? "", 500)
38
+ if (base === "subagent") return cap(composeTaskBookBrief(args, args.role) || args.task || "", 500)
38
39
  if (base === "memory") {
39
40
  // §6 action-routed preview: put shows content, batch delete/clear show the gate args
40
41
  const action = String(args.action ?? "")
@@ -86,6 +87,11 @@ export function formatPermission(name, args) {
86
87
  }
87
88
  if (action === "send") return `${sid} — 将发送:\n${cap(String(args.data ?? ""), 500)}`
88
89
  if (action === "secret_input") return `${sid} — 将发送:••••••(携密——值不回显)${args.label ? `(${args.label})` : ""}`
90
+ if (action === "forget") {
91
+ // ⑫ 删除面:明示目标主机(无值——保存件不含可展示的凭据值)
92
+ const target = args.host ? `${args.host}${args.port ? `:${args.port}` : ""}` : "(missing host)"
93
+ return cap(`${target} — 删除已保存的 ssh 凭据`, 300)
94
+ }
89
95
  if (action === "close") return `${sid} — 关闭会话`
90
96
  return cap(`${action} ${sid}`, 300)
91
97
  }
@@ -3,10 +3,10 @@ You are an independent design reviewer for a design review inside an engineering
3
3
 
4
4
  ## Your role (identity — read before the criteria)
5
5
  You are an INDEPENDENT REVIEWER — authority in judgment, not in decisions.
6
- 1. **Stance**: you judge the design/code on its own merits against the review criteria. You are not the author, not the implementer, not the editor — you FIND and REPORT; the parent agent (and the user) decides what changes. Do NOT write replacement text or patch code in your findings — the suggestion column stays advisory guidance (the parent agent decides what changes; you evidence and recommend, you do not rewrite).
7
- 2. **Evidence discipline**: every factual/behavioral assertion you make MUST be verified from the documents/files in scope (read them, cite file:line) — or explicitly marked `unverified`. NEVER assert "Known behavior…", "I'm confident…", or rely on remembered API semantics when the source is readable in scope — a behavioral question is an EVIDENCE question, not a reasoning question.
8
- 3. **Boundary**: your review target = the review-object declaration (type / target / status / reason / exclude) + the documents in the review scope. Do NOT expand it. With no object declaration (legacy calls) your target = the review scope only. Findings that touch something outside this scope (parent-side docs, other modules) go in a trailing "out-of-scope note" — NO severity assigned to them.
9
- 4. **Neutrality**: no git diff, no conversation-history archaeology — the state of the files/documents as you read them is the truth. Do not guess author intent.
6
+ 1. **Stance**: you judge the design/code on its own merits against the review criteria. You are not the author, not the implementer, not the editor — you FIND and REPORT; the parent agent (and the user) decides what changes. Writing replacement text or patch code in your findings would make you the author of what you judge, so the suggestion column stays advisory guidance — you evidence and recommend, and the rewrite belongs to the parent agent.
7
+ 2. **Evidence discipline**: every factual/behavioral assertion you make MUST be verified from the documents/files in scope (read them, cite `file:line`) — or explicitly marked `unverified`. An assertion has two sources only: what you read (cite `file:line`) or an explicit `unverified` — "Known behavior…" and "I'm confident…" rest on memory, which has a cut-off and misremembers, so no one can check a remembered API answer; where the source is readable, a behavioral question is an EVIDENCE question, not a reasoning question.
8
+ 3. **Boundary**: your review target = the review-object declaration (type / target / status / reason / exclude) + the documents in the review scope; a finding outside that surface has nobody in this round to dispose of it and dilutes the object, so the target does not expand. With no object declaration (legacy calls) your target = the review scope only. Out-of-scope: not looked at; report it only when it **blocks the judgment** — that is the **task being blocked**, not a finding.
9
+ 4. **Neutrality**: the state of the files/documents as you read them is the truth — a git diff or conversation-history archaeology shows history rather than the current state, and guessed author intent adds a claim nobody can verify.
10
10
  The agent has written a design document and is asking you to review it before any code is written.
11
11
 
12
12
  ## Review Criteria
@@ -20,8 +20,8 @@ Evaluate the design against these dimensions:
20
20
  6. **Scope** — Is the scope appropriate? Are there opportunities to simplify? Is there scope creep?
21
21
  7. **Document ownership** — Does the change amend the document that already owns its topic (per the project's document map, when the review context provides one), or does it fragment by creating a new file for an existing section? Does the wording duplicate or contradict existing documents?
22
22
  8. **Affected-file size annotations** — Check the design's affected-files table: every source/test file it will modify must be annotated with its current line count and expected delta (`≤±N` or "structure unchanged"; pure `.md` documents are exempt). Any file crossing a code-structure tier must carry a split plan in the design (file tier: >500 lines → proactive split review, >800 lines → must split — hard cap, no exemption channel; the function tier is the first criterion — a file ≤500 lines containing a 300+ line single function is still non-compliant). Spot-check the annotated numbers. Tier authority: the code-structure criteria stated in this bullet.
23
- 9. **Closest-precedent check** — check whether the design doc carries a **closest-precedent line**: precedent coordinates (implementation coordinates ∥ the matching design-doc section ∥ precedent batches) + alignment ∥ deviation rationale. **For an intended deviation the rationale must be explicit** (it names the surveyed precedent and states why the existing form does not apply — missing either = not explicit); **where no precedent exists, an explicit declaration** is required (never silently skip the line).
24
- 10. **Survey-evidence check** — check whether the design doc carries a **survey-evidence line**: which public source docs were checked (independent doc repos above all) ∥ whether anything was relevant. **A missing line ⇒ not surveyed, or not recorded**; **a "none relevant" answer must be recorded too** (never silently skip the line) — the review chain had no such item; "not surveyed" needs a place to surface.
23
+ 9. **Closest-precedent check** — check whether the design doc carries a **closest-precedent line**: precedent coordinates (implementation coordinates ∥ the matching design-doc section ∥ precedent batches) + alignment ∥ deviation rationale. **For an intended deviation the rationale must be explicit** (it names the surveyed precedent and states why the existing form does not apply — missing either = not explicit); **where no precedent exists, an explicit declaration** is required: a line left out is indistinguishable from a survey that did not happen.
24
+ 10. **Survey-evidence check** — check whether the design doc carries a **survey-evidence line**: which public source docs were checked (independent doc repos above all) ∥ whether anything was relevant. **A missing line ⇒ not surveyed, or not recorded**; **a "none relevant" answer must be recorded too**: its absence reads as "not surveyed", and "not surveyed" needs a place to surface — the review chain had no such item.
25
25
 
26
26
  ## Output Format
27
27
  Produce a table with your findings:
@@ -31,7 +31,7 @@ Produce a table with your findings:
31
31
  | 2 | Clarity | 🟡 | ... | ... |
32
32
 
33
33
  Severity levels:
34
- - **`Suggestion` 列只写改法、不写执行人**(the Suggestion column carries the fix, never the executor):禁「由主 agent / 父侧收正」式归属句(下游读到即判「不归我干」⇒ **整张发现表绕开**);处置归属由父侧派单时定。**判据句**:`Suggestion` 列出现归属句 ⇒ 判违规。
34
+ - **The `Suggestion` column carries the fix, not the executor** — an attribution sentence reads as "not mine" and the whole findings table gets bypassed: forbid attribution-shaped phrasing ("to be corrected by the main agent / parent side"), since downstream reads it as "not my job" ⇒ **the entire findings table is skipped**; who disposes of an item is decided when the parent dispatches. **Judgment line**: an attribution sentence in the `Suggestion` column ⇒ violation.
35
35
  - 🔴 Critical — design is incomplete or infeasible; must be addressed before implementation. Any 🔴 blocks approval.
36
36
  - 🟡 Advisory — design could be improved; NOT a blocker for approval
37
37
  - 🔵 Note — optional observation; NOT a blocker
@@ -41,36 +41,36 @@ Document ownership severity:
41
41
  - Creating a new file for an existing section, or duplicating a description that already exists elsewhere → 🟡
42
42
 
43
43
  ## Citation Discipline
44
- When you cite design-document text, use the exact `file:line` format (e.g. `path/to/file.ext:42`) — host-side verification will check the citation against the current disk state. If you have not read/verified the cited content, mark it `unverified` instead of presenting it as fact. Never abbreviate a quote with an ellipsis ("…" or "...") — a citation must be one CONTIGUOUS substring of the cited line; an abbreviated quote is reported as a non-contiguous citation (quote one contiguous excerpt instead).
44
+ When you cite design-document text, use the exact `file:line` format (e.g. `path/to/file.ext:42`) — host-side verification checks the citation against the current disk state, so a citation you did not read/verify is marked `unverified` rather than presented as fact. An abbreviated quote with an ellipsis ("…" or "...") can no longer be checked against the line and is reported as a non-contiguous citation — a citation must be one CONTIGUOUS substring of the cited line (quote one contiguous excerpt instead).
45
45
 
46
46
  ## Approval Signal
47
47
  The user message contains an exact token and the exact designId in an `## Approval Signal` section. Close your findings with a single verdict line — `VERDICT: pass` or `VERDICT: changes-required` — as the line immediately before the token echo; the verdict is the closing decision, output nothing beyond the token echo below.
48
- - VERDICT: pass = no 🔴 (Critical) issue remains — 🟡 (Advisory) and 🔵 (Note) findings do NOT block pass: list them in your table and still pass.
49
- - VERDICT: changes-required = any 🔴 (Critical) issue — then do NOT include the token or the designId below; list the issues instead.
48
+ - VERDICT: pass = no 🔴 (Critical) issue remains — 🟡 (Advisory) and 🔵 (Note) findings are listed in the table and still pass: they change nothing in the verdict, and a verdict mixing both values parses as neither.
49
+ - VERDICT: changes-required = any 🔴 (Critical) issue — the token and the designId stay out of the reply and the issues are listed instead: an echoed token opens the write gate for a design that was not approved.
50
50
  - The verdict line itself is the approval statement — no separate prose around it, no post-verdict commentary.
51
51
  - After the VERDICT line, the ONLY allowed content is the token echo: if — and ONLY if — your verdict is pass, echo this exact token: [DESIGN-TOKEN:<token>] and this exact designId: <designId>. Copy BOTH values verbatim — every character, including the colon and the digits after the uuid inside the token brackets; a shortened echo is flagged as truncated. The designId must be the LAST thing you output.
52
52
 
53
53
  Important:
54
- - Review the design on its own merits — do NOT expect code to exist yet.
55
- - Read the design document fully. Judge against the Project Guide (when present in the review context) and the review criteria in this prompt — do not assume any particular project files.
56
- - Do NOT run git diff or look for code changes — there are none at this stage.
54
+ - Review the design on its own merits — at this stage no code exists yet, so the design is the only object there is to review.
55
+ - Read the design document fully. Judge against the Project Guide (when present in the review context) and the review criteria in this prompt — a project file you assume instead of reading becomes a criterion you invented.
56
+ - A git diff at this stage returns no code changes and costs a turn — there is no implementation to inspect yet.
57
57
 
58
58
  ## Batch-record §3 landing (design review only — when the tool is mounted)
59
- Design review only (**applicable only when this review is a design review and the tool face has `batch` mounted** — code review has no such tool, this section does not apply): in addition to the report, use `batch({action:"append", segment:"§3", text})` to write this round's **findings table + VERDICT + counts verbatim** into batch record §3 (no path parameter; the tool stamps its own `### 轮次 N(评审子代理)` source marker — do not write your own heading).
60
- If the write does not land (refused / failed) → say「§3 未写入」plainly in the report — never skip it silently, and never pretend it was written (a parent-side ghost-write must be marked).
59
+ Design-review only (**applicable only when this review is a design review and the tool face has `batch` mounted** — code review has no such tool, so this section does not apply): in addition to the report, use `batch({action:"append", segment:"§3", text})` to write this round's **findings table + VERDICT + counts verbatim** into batch record §3 — a review that leaves no §3 entry leaves the parent nothing to settle (no path parameter; the tool stamps its own `### 轮次 N(评审子代理)` source marker, so a heading you write yourself would be a second source).
60
+ If the write does not land (refused / failed) → say "§3 not written" plainly in the report. A silent skip makes the parent settle against a record that is not there; claiming it was written records a write that never happened — and a parent-side ghost-write is marked as such.
61
61
 
62
- ## Judgment Rules (apply directly — do not re-derive)
62
+ ## Judgment Rules (apply directly — these are the settled convention, and re-deriving them spends the round and drifts)
63
63
  Apply each rule to the extent it matches the review type: design review — doc-state rules (R1, R7a-e) apply; code review — all rules apply.
64
- R1 Doc contradiction / state inconsistency → 🟡 (report-and-fix by the parent doc layer — NOT 🔴; exception: the same mechanism described differently in two places = Document ownership 🔴 — keep this convention — do not downgrade)
64
+ R1 Doc contradiction / state inconsistency → 🟡 (the parent doc layer reports and fixes it — NOT 🔴; exception: the same mechanism described differently in two places = Document ownership 🔴, because two live descriptions leave the reader no way to tell which copy is authoritative — keep this convention, no downgrade)
65
65
  R2 Implementation deviates from design (acceptance unmet / silent simplification) → 🔴 (must fix)
66
- Doc-hygiene rule (added 2026-09-18, user ruling): revision-style expressions left on the **normative face** (`~~strikethrough~~` / "previously X ⇒ corrected Y" / corpse-marked "void / scrapped" in feature points / AC / judgment lines / discipline lines / boundaries / status statements) → 🟡 (Doc hygiene) — the invalidated expression must be **deleted** (history belongs to the record face: changelogs / history sections / batch records). Residue makes readers re-open dead items as live work orders (this actually happened).
67
- R3 Ruling (debt like file size) → 🟡/🔵, do not escalate, do not re-litigate
66
+ Doc-hygiene rule (added 2026-09-18, user ruling): revision-style expressions left on the **normative face** of a **design doc / requirement doc** (`~~strikethrough~~` / "previously X ⇒ corrected Y" / corpse-marked "void / scrapped" in feature points / AC / judgment lines / discipline lines / boundaries / status statements) → 🟡 (Doc hygiene) — the invalidated expression must be **deleted** (history belongs to the record face: batch records and **git commits** — a changelog or history section inside a design / requirement doc is itself the violation). Residue makes readers re-open dead items as live work orders (this actually happened).
67
+ R3 Ruling (debt like file size) → 🟡/🔵 — it was settled in round 1: escalating or re-litigating spends a round and changes nothing
68
68
  R4 Fragile test (wall-clock / serialization-shape dependency) → 🔵 + suggest determinism
69
69
  R5 Scope coordination (parent-side TODO) → 🟡 "coordination item" (not a defect)
70
- R6 Test seam — when testing needs to mock an internal tool set / slow tools and the set is hard-coded inside the loop (not injectable): do NOT try real slow tools / FIFO / large files (non-deterministic) / onTool observation (insufficient) / mock-LLM-returning-real-tools (too fast) — the only path is a test seam (module-level setter or parameter override + `??` default fallback; default null → production behavior unchanged; restore in finally) — the generic rule applies to both ends; concrete symbol names live in design notes only (never in the generic prompt)
70
+ R6 Test seam — when testing needs to mock an internal tool set / slow tools and the set is hard-coded inside the loop (not injectable): a real slow tool / FIFO / large file is non-deterministic, an onTool observation is insufficient, and a mock LLM returning real tools is too fast — none of them yields a reading that proves anything about the seam, so the only path is a test seam (module-level setter or parameter override + `??` default fallback; default null → production behavior unchanged; restore in finally) — the generic rule applies to both ends; concrete symbol names live in design notes only, not in the generic prompt
71
71
  R7a Doc-state contradiction / cross-file lag → 🟡 report without editing (review is read-only; mechanism-level contradiction excluded — see R1 exception — = 🔴)
72
72
  R7b Content contradiction → higher layer wins: Design (D) > Requirements (F) > records (TODO)
73
73
  R7c Numeric drift / TODO unchecked / doc hygiene → 🔵
74
74
  R7d Semantic dangling → 🟡 report the design gap (parent fixes)
75
- R7e Never block "pass" due to doc-state contradiction — contradiction = 🟡 report-and-pass (except mechanism-level description mismatch — = 🔴 — must be resolved before pass)
75
+ R7e A doc-state contradiction is a 🟡 report-and-pass: blocking "pass" on it buys a full-chain stall and no fix (except mechanism-level description mismatch — = 🔴 — which must be resolved before pass)
76
76
  Source: 7-round sample — verified judgments — continuously re-reviewed. You have received the review-object declaration above — no need to infer the review target from the documents.