thincoder 0.12.61 → 0.12.62

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 (57) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/bin/thincoder.mjs +12 -3
  3. package/package.json +1 -1
  4. package/src/acp.mjs +13 -5
  5. package/src/agent/run-stages.mjs +3 -1
  6. package/src/agent/spawn-child.mjs +17 -2
  7. package/src/agent-tools/async-settle.mjs +13 -0
  8. package/src/agent-tools/consult.mjs +5 -0
  9. package/src/agent-tools/escalate-async.mjs +6 -0
  10. package/src/agent-tools/read-history.mjs +18 -3
  11. package/src/agent-tools/subagent-actions.mjs +3 -0
  12. package/src/agent-tools/subagent-run.mjs +1 -1
  13. package/src/agent-tools/subagent-spawn.mjs +6 -0
  14. package/src/agent.mjs +4 -0
  15. package/src/context.mjs +12 -1
  16. package/src/crash-reports.mjs +31 -9
  17. package/src/generate-title.mjs +8 -3
  18. package/src/heap-watch.mjs +88 -0
  19. package/src/ledger.mjs +227 -0
  20. package/src/memory/code-sync.mjs +7 -5
  21. package/src/memory/core.mjs +8 -9
  22. package/src/memory/docs.mjs +7 -5
  23. package/src/memory/scan.mjs +95 -0
  24. package/src/prompts/advisor-design.md +1 -1
  25. package/src/prompts/advisor-round1.md +1 -1
  26. package/src/prompts/advisor-round2.md +1 -1
  27. package/src/prompts/advisor-round3.md +1 -1
  28. package/src/prompts/discipline-engineering.md +51 -10
  29. package/src/prompts/discipline-normal.md +10 -4
  30. package/src/prompts/persona-eng-designer.md +6 -1
  31. package/src/prompts/persona-engineering.md +1 -0
  32. package/src/session-gc.mjs +9 -2
  33. package/src/session-guard.mjs +12 -0
  34. package/src/session-segments.mjs +100 -0
  35. package/src/session-slots.mjs +3 -0
  36. package/src/session-store.mjs +441 -0
  37. package/src/session.mjs +78 -61
  38. package/src/text-budget.mjs +46 -0
  39. package/src/traces/trace-store.mjs +195 -64
  40. package/src/tui/cmd-clear.mjs +2 -0
  41. package/src/tui/cmd-new.mjs +5 -1
  42. package/src/tui/cmd-session.mjs +8 -4
  43. package/src/tui/display-budget.mjs +184 -0
  44. package/src/tui/index.mjs +31 -3
  45. package/src/tui/key-handler-search.mjs +9 -1
  46. package/src/tui/ledger-surface.mjs +69 -0
  47. package/src/tui/render-frame.mjs +7 -1
  48. package/src/tui/startup.mjs +45 -13
  49. package/src/tui/subagent-blocks.mjs +1 -0
  50. package/src/tui/subagent-children.mjs +86 -14
  51. package/src/tui/subagent-freeze.mjs +8 -2
  52. package/src/tui/suspension-drive.mjs +3 -1
  53. package/src/tui/tool-args.mjs +5 -2
  54. package/src/tui/tool-display.mjs +16 -2
  55. package/src/tui/tool-events.mjs +53 -14
  56. package/src/tui/tui-lifecycle.mjs +9 -2
  57. package/src/tui/wrapped-spawn.mjs +21 -5
package/src/ledger.mjs ADDED
@@ -0,0 +1,227 @@
1
+ /**
2
+ * ledger.mjs — 台账(需求池 / 技术待办)单源读取面(LEDGER-SURFACE 批——设计档 §2.30.3)。
3
+ *
4
+ * 数字单源(F7):解析(`scanGroups`)/ 计数 / 老化 / 阈值一处实现——机检器
5
+ * (`scripts/check-ledger.mjs`)与显示面(CLI TUI `src/tui/ledger-surface.mjs`)共用本口径;
6
+ * VSC 端为独立实现、语义同源(不跨仓 import)。
7
+ * 口径(需求档 §1.18):计数 = `##` 组内 `- [ ]` 未决条目;老化 = 技术组无 `触发=` 且行龄
8
+ * > 30 天(行龄未知不计);阈值 = 池 ≥ 3 或任一板块 ≥ 2;可动作 = 老化 > 0 或阈值达成。
9
+ * 行文本逐字契约 = 设计档 §2.30.3.3(本模块只产出文本,色 / 态归各端显示面)。
10
+ */
11
+ import { mkdirSync, opendirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs"
12
+ import { basename, dirname, join, resolve } from "node:path"
13
+ import { execFileSync } from "node:child_process"
14
+ import { configDir } from "./config.mjs"
15
+
16
+ /** 老化阈值(天——口径 = 需求档 §1.18;`days` 参数可覆盖)。 */
17
+ export const AGING_DAYS = 30
18
+ /** 「可开批」阈值:同一板块(池条目指针所指需求档文档名)未决 ≥ 2。 */
19
+ export const THRESHOLD_BOARD = 2
20
+ /** 「可开批」阈值:需求池未决 ≥ 3。 */
21
+ export const THRESHOLD_POOL = 3
22
+ /** 刷新周期(N2 实现常量;VSC 端另有换项目事件触发)。 */
23
+ export const REFRESH_MS = 120000
24
+ /** 变化行去重档(跨会话、跨端共享——需求档 §1.18 F5;configDir 先例 = crash-reports 同区)。 */
25
+ export const NOTIFY_FILE = join(configDir, "ledger-notify.json")
26
+ /** 空族提示(仅命令面——运行时面静默,N1 / 设计档 §2.30.3.3)。 */
27
+ export const EMPTY_FAMILY_LINE = "台账:未发现台账(docs/TODO.md)。"
28
+
29
+ const LEDGER_REL = join("docs", "TODO.md")
30
+ const H2_RE = /^##\s+(.*)$/
31
+ const DECL_RE = /((\d+)\s*条)/
32
+ const OPEN_ENTRY_RE = /^- \[ \]\s+/
33
+ const TRIGGER_RE = /触发\s*=\s*([^·\n]*)/
34
+ const BOARD_REF_RE = /requirements\/([A-Za-z0-9_.-]+\.md)/
35
+
36
+ /** `##` 组扫描(与机检器 L2/L3 同源——解析唯一实现):[{name,line,declared,entries:[{line,text}]}]。 */
37
+ export function scanGroups(lines) {
38
+ const groups = []
39
+ lines.forEach((l, i) => {
40
+ const h = H2_RE.exec(l)
41
+ if (h) groups.push({ name: h[1].trim(), line: i + 1, declared: Number((DECL_RE.exec(h[1]) ?? [])[1] ?? NaN), entries: [] })
42
+ else if (groups.length && OPEN_ENTRY_RE.test(l)) groups[groups.length - 1].entries.push({ line: i + 1, text: l })
43
+ })
44
+ return groups
45
+ }
46
+
47
+ /** 条目归一化文本 = 条目键(去 `- [ ] ` 前缀 + 去首尾空白 + 连续空白折叠单空格)。
48
+ * 位置无关(位移 / 他条编辑稳定);自身文本变更 = 键变(最坏一次重报)。§2.30.3.2。 */
49
+ export function normalizeEntry(text) {
50
+ return text.replace(/^- \[[ x]\]\s+/, "").trim().replace(/\s+/g, " ")
51
+ }
52
+
53
+ /** 条目标题:首个 `**…**` 段;无粗体段 → 归一化文本前 20 字(超出加 `…`)。§2.30.3.3 L3。 */
54
+ export function entryTitle(text) {
55
+ const m = /\*\*(.+?)\*\*/.exec(text)
56
+ if (m) return m[1]
57
+ const norm = normalizeEntry(text)
58
+ return norm.length > 20 ? norm.slice(0, 20) + "…" : norm
59
+ }
60
+
61
+ const isFile = (p) => { try { return statSync(p).isFile() } catch { return false } }
62
+
63
+ /** 向上(含自身)最近的含台账目录 → {root, ledger} / null(F4——CLI 锚 = cwd)。 */
64
+ export function findProject(anchor) {
65
+ let dir = resolve(anchor)
66
+ for (;;) {
67
+ const ledger = join(dir, LEDGER_REL)
68
+ if (isFile(ledger)) return { root: dir, ledger }
69
+ const parent = dirname(dir)
70
+ if (parent === dir) return null
71
+ dir = parent
72
+ }
73
+ }
74
+
75
+ /** 同级枚举上限(N2 成本有界:父目录超此规模 = 缓存 / 临时等非仓族形态 → 退化空集,只显当前项目)。 */
76
+ export const MAX_SIBLING_SCAN = 100
77
+
78
+ /** 目录下含台账的直接子目录(目录名升序;不可读 → [];**惰性枚举 + 上限**——大目录零全量扫描)。 */
79
+ function ledgerChildren(dir) {
80
+ const out = []
81
+ let handle
82
+ try { handle = opendirSync(dir) } catch { return out }
83
+ try {
84
+ let n = 0
85
+ for (;;) {
86
+ const ent = handle.readSync()
87
+ if (!ent) break
88
+ if (++n > MAX_SIBLING_SCAN) return []
89
+ if (!ent.isDirectory()) continue
90
+ const root = join(dir, ent.name)
91
+ const ledger = join(root, LEDGER_REL)
92
+ if (isFile(ledger)) out.push({ root, ledger })
93
+ }
94
+ } catch { /* 读中断 → 已得子集(尽力而为) */ }
95
+ finally { try { handle.closeSync() } catch { /* 已关 */ } }
96
+ return out.sort((a, b) => (a.root < b.root ? -1 : a.root > b.root ? 1 : 0))
97
+ }
98
+
99
+ /** 项目族发现:{current, projects}——projects = current + 其同级含台账目录(**发现序:current 在前**,
100
+ * 其余按目录名升序);current 缺(容器目录,K6)→ 上下文目录向上最近「含台账子目录」者取其子目录。 */
101
+ export function discoverFamily(anchor) {
102
+ const current = findProject(anchor)
103
+ if (current) {
104
+ const siblings = ledgerChildren(dirname(current.root)).filter((p) => p.root !== current.root)
105
+ return { current, projects: [current, ...siblings] }
106
+ }
107
+ let dir = resolve(anchor)
108
+ for (;;) {
109
+ const kids = ledgerChildren(dir)
110
+ if (kids.length) return { current: null, projects: kids }
111
+ const parent = dirname(dir)
112
+ if (parent === dir) return { current: null, projects: [] }
113
+ dir = parent
114
+ }
115
+ }
116
+
117
+ /** 行龄 Map:全档一次 `git blame --porcelain` → Map<行号, 天>;非 git / 不可判定 → null(零假阳降级)。 */
118
+ export function blameAges(abs) {
119
+ let out
120
+ try {
121
+ out = execFileSync("git", ["blame", "--porcelain", "--", abs], {
122
+ cwd: dirname(abs), encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 32 * 1024 * 1024,
123
+ })
124
+ } catch { return null }
125
+ const times = new Map(), ages = new Map()
126
+ const now = Date.now() / 1000
127
+ let sha = null, line = 0
128
+ for (const l of out.split("\n")) {
129
+ const h = /^([0-9a-f^]{7,40})\s+\d+\s+(\d+)(?:\s+\d+)?$/.exec(l)
130
+ if (h) { sha = h[1]; line = Number(h[2]); continue }
131
+ if (l.startsWith("author-time ")) { times.set(sha, Number(l.slice(12))); continue }
132
+ if (l.startsWith("\t")) {
133
+ const t = times.get(sha)
134
+ ages.set(line, t != null ? (now - t) / 86400 : null)
135
+ }
136
+ }
137
+ return ages
138
+ }
139
+
140
+ /** 单项目汇总 → {name,root,ledger,pool,tech,aged,agedKeys,agedTitles,thresholdReached,boards,actionable}。
141
+ * `ageOf(abs)` → Map<行号,天>|null(注入面——确定性用例);**无候选条目不调 ageOf**(N2)。 */
142
+ export function summarizeLedger(project, { days = AGING_DAYS, ageOf = blameAges } = {}) {
143
+ const groups = scanGroups(readFileSync(project.ledger, "utf8").split("\n"))
144
+ const poolEntries = groups.filter((g) => g.name.includes("需求池")).flatMap((g) => g.entries)
145
+ const techEntries = groups.filter((g) => g.name.includes("技术")).flatMap((g) => g.entries)
146
+ const candidates = techEntries.filter((e) => !TRIGGER_RE.test(e.text))
147
+ const ages = candidates.length ? ageOf(project.ledger) : null
148
+ const aged = candidates.filter((e) => { const a = ages?.get(e.line); return a != null && a > days })
149
+ const boards = new Map()
150
+ for (const e of poolEntries) {
151
+ const m = BOARD_REF_RE.exec(e.text)
152
+ if (m) boards.set(m[1], (boards.get(m[1]) ?? 0) + 1)
153
+ }
154
+ const thresholdReached = poolEntries.length >= THRESHOLD_POOL || [...boards.values()].some((n) => n >= THRESHOLD_BOARD)
155
+ return {
156
+ name: basename(project.root), root: project.root, ledger: project.ledger,
157
+ pool: poolEntries.length, tech: techEntries.length, aged: aged.length,
158
+ agedKeys: aged.map((e) => normalizeEntry(e.text)), agedTitles: aged.map((e) => entryTitle(e.text)),
159
+ thresholdReached, boards, actionable: aged.length > 0 || thresholdReached,
160
+ }
161
+ }
162
+
163
+ /** L1 状态标记(逐字——§2.30.3.3)。 */
164
+ export function formatMarker(scan) {
165
+ return scan ? `台账 ${scan.pool}·${scan.tech}` : null
166
+ }
167
+
168
+ /** L2 明细行(逐字——`(老化 <n>)` 恒显;阈值达成加 ` — 可开批`)。 */
169
+ export function formatDetailLine(scan) {
170
+ return `台账 ${scan.name}:需求池 ${scan.pool} · 技术待办 ${scan.tech}(老化 ${scan.aged})${scan.thresholdReached ? " — 可开批" : ""}`
171
+ }
172
+
173
+ /** L3 变化行·老化(titles = 新增老化条目标题;> 3 条时第三项后接 `;…`)。 */
174
+ export function formatAgingLine(scan, titles) {
175
+ return `台账变化:${scan.name} 老化首次越线 ${titles.length} 条(超 30 天未处置):${titles.slice(0, 3).join(";")}${titles.length > 3 ? ";…" : ""}`
176
+ }
177
+
178
+ /** L4 变化行·阈值。 */
179
+ export function formatThresholdLine(scan) {
180
+ return `台账变化:${scan.name} 需求池达阈值(${scan.pool} 条)— 可开批`
181
+ }
182
+
183
+ /** 明细行集 = {current} ∪ {可动作项目}(发现序——current 在前,其余按目录名升序;同项去重)。 */
184
+ export function detailScans(scans, current) {
185
+ const out = []
186
+ if (current) out.push(current)
187
+ for (const s of scans) if (s !== current && s.actionable) out.push(s)
188
+ return out
189
+ }
190
+
191
+ /** 变化行规划(纯函数):{lines:[{text,warn}], next:{aged,threshold}}(去重口径 §2.30.3.2)。 */
192
+ export function planChangeLines(prev, scan) {
193
+ const p = prev && typeof prev === "object" ? prev : {}
194
+ const before = new Set(Array.isArray(p.aged) ? p.aged : [])
195
+ const freshTitles = scan.agedKeys.map((k, i) => (before.has(k) ? null : scan.agedTitles[i])).filter((t) => t != null)
196
+ const lines = []
197
+ if (freshTitles.length) lines.push({ text: formatAgingLine(scan, freshTitles), warn: true })
198
+ if (scan.thresholdReached && !p.threshold) lines.push({ text: formatThresholdLine(scan), warn: true })
199
+ return { lines, next: { aged: scan.agedKeys, threshold: scan.thresholdReached } }
200
+ }
201
+
202
+ /** 去重档键 = 台账绝对路径·正斜杠 + **盘符大写**(跨端同规则——CLI 的 `process.cwd()` 盘符大写、
203
+ * VSC 的 `uri.fsPath` 小写(`session-slots.mjs` `normalizeCwd` 同一契约——session / checkpoint /
204
+ * trace 跨端共享即赖此);CLI 写的键 VSC 须逐字认得)。 */
205
+ export function notifyKey(ledger) {
206
+ return resolve(ledger).replace(/\\/g, "/").replace(/^([a-z]):/, (_, d) => `${d.toUpperCase()}:`)
207
+ }
208
+
209
+ /** 去重档读(缺失 / 坏 JSON → 空态——N1 降级不崩)。 */
210
+ export function loadNotifyState(file = NOTIFY_FILE) {
211
+ try {
212
+ const j = JSON.parse(readFileSync(file, "utf8"))
213
+ if (j && typeof j === "object" && j.ledgers && typeof j.ledgers === "object") return { version: 1, ledgers: j.ledgers }
214
+ } catch { /* 缺失 / 坏档 → 空态 */ }
215
+ return { version: 1, ledgers: {} }
216
+ }
217
+
218
+ /** 去重档写(temp + rename 防撕裂;写失败静默返回 false——唯一写面,仅送达后调)。 */
219
+ export function saveNotifyState(file, state) {
220
+ try {
221
+ mkdirSync(dirname(file), { recursive: true })
222
+ const tmp = `${file}.tmp-${process.pid}`
223
+ writeFileSync(tmp, JSON.stringify(state), "utf8")
224
+ renameSync(tmp, file)
225
+ return true
226
+ } catch { return false }
227
+ }
@@ -4,6 +4,7 @@
4
4
  import { readFile, stat } from "node:fs/promises"
5
5
  import { join, relative } from "node:path"
6
6
  import { embed, cosine, toBlob, fromBlob } from "../embedding.mjs"
7
+ import { scanVectors, createTopK } from "./scan.mjs"
7
8
  import { CODE_EXTS, DOC_EXTS, MAX_CODE_FILE_BYTES, MAX_DOC_FILE_BYTES } from "./schema.mjs"
8
9
  import { buildFtsQuery, ensureEmbeddings, EMBED_TEXT_MAX_LEN } from "./core.mjs"
9
10
  import { detectLanguage, _upsertCodeFile, _upsertDocFile, yieldTick } from "./code-index.mjs"
@@ -291,11 +292,12 @@ export async function codeSearch(memory, query, { limit = 5 } = {}) {
291
292
  console.error(`[code] query embedding failed, falling back to FTS-only: ${e.message}`)
292
293
  return ftsList.slice(0, limit)
293
294
  }
294
- const rows = memory.db.prepare(`SELECT rowid, embedding FROM code_chunks WHERE embedding IS NOT NULL ${vecOriginFilter}`).all(...originParams)
295
- const vecList = rows
296
- .map((r) => ({ rowid: r.rowid, score: cosine(qvec, fromBlob(r.embedding)) }))
297
- .sort((a, b) => b.score - a.score)
298
- .slice(0, Math.max(limit * 4, 20))
295
+ // TUI-OOM-ROOTCAUSE(MEMORY.md §10.3):分块扫描 + 有界 top-K(原全表 .all()——峰值 = 块 + K)
296
+ const top = createTopK(Math.max(limit * 4, 20))
297
+ scanVectors(memory.db, `SELECT rowid, embedding FROM code_chunks WHERE embedding IS NOT NULL ${vecOriginFilter}`, originParams, {
298
+ onRow: (r) => top.push({ id: r.rowid, rowid: r.rowid, score: cosine(qvec, fromBlob(r.embedding)) }),
299
+ })
300
+ const vecList = top.list().map((c) => ({ rowid: c.id, score: c.score }))
299
301
 
300
302
  const K = 60
301
303
  const scores = new Map()
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { parseEntry, serializeEntry, entryFilename } from "../markdown.mjs"
9
9
  import { embed, cosine, toBlob, fromBlob } from "../embedding.mjs"
10
+ import { scanVectors, createTopK } from "./scan.mjs"
10
11
  import { readFile, stat, readdir, writeFile, mkdir } from "node:fs/promises"
11
12
  import { join } from "node:path"
12
13
  import { segmentCJK, VALID_TYPES, SCHEMA_VERSION } from "./schema.mjs"
@@ -57,15 +58,13 @@ export async function search(memory, query, { limit = 5 } = {}) {
57
58
  }
58
59
  const vecFilter = memory.projectOrigin ? `AND (layer = 'team' OR origin = ?)` : ""
59
60
  const vecParams = memory.projectOrigin ? [memory.projectOrigin] : []
60
- const rows = memory.db.prepare(`
61
- SELECT 'personal:' || id AS uid, embedding FROM entries WHERE embedding IS NOT NULL
62
- UNION ALL
63
- SELECT layer || ':' || COALESCE(origin, '') || ':' || path AS uid, embedding FROM files WHERE embedding IS NOT NULL ${vecFilter}
64
- `).all(...vecParams)
65
- const vecList = rows
66
- .map((r) => ({ id: r.uid, score: cosine(qvec, fromBlob(r.embedding)) }))
67
- .sort((a, b) => b.score - a.score)
68
- .slice(0, Math.max(limit * 4, 20))
61
+ // TUI-OOM-ROOTCAUSE(MEMORY.md §10.3):分块扫描 + 有界 top-K(原全表 .all() 物化 +
62
+ // 全量排序——峰值 = 块 + K;召回语义不变)。两表各自游标扫描、共享同一 top-K。
63
+ const top = createTopK(Math.max(limit * 4, 20))
64
+ const onRow = (r) => top.push({ id: r.uid, score: cosine(qvec, fromBlob(r.embedding)) })
65
+ scanVectors(memory.db, `SELECT rowid, 'personal:' || id AS uid, embedding FROM entries WHERE embedding IS NOT NULL`, [], { onRow })
66
+ scanVectors(memory.db, `SELECT rowid, layer || ':' || COALESCE(origin, '') || ':' || path AS uid, embedding FROM files WHERE embedding IS NOT NULL ${vecFilter}`, vecParams, { onRow })
67
+ const vecList = top.list()
69
68
 
70
69
  // ---- RRF merge ----
71
70
  const K = 60
@@ -5,6 +5,7 @@
5
5
  import { readFile, stat } from "node:fs/promises"
6
6
  import { isAbsolute, join } from "node:path"
7
7
  import { embed, cosine, toBlob, fromBlob } from "../embedding.mjs"
8
+ import { scanVectors, createTopK } from "./scan.mjs"
8
9
  import { commitAndPush } from "../git/gitmem.mjs"
9
10
  import { MAX_DOC_FILE_BYTES } from "./schema.mjs"
10
11
  import { buildFtsQuery, put, search, putMarkdown, clearPersonal, EMBED_TEXT_MAX_LEN } from "./core.mjs"
@@ -109,11 +110,12 @@ export async function docSearch(memory, query, { limit = 5 } = {}) {
109
110
  console.error(`[docs] query embedding failed, falling back to FTS-only: ${e.message}`)
110
111
  return ftsList.slice(0, limit)
111
112
  }
112
- const rows = memory.db.prepare(`SELECT rowid, embedding FROM doc_chunks WHERE embedding IS NOT NULL ${vecOriginFilter}`).all(...originParams)
113
- const vecList = rows
114
- .map((r) => ({ rowid: r.rowid, score: cosine(qvec, fromBlob(r.embedding)) }))
115
- .sort((a, b) => b.score - a.score)
116
- .slice(0, Math.max(limit * 4, 20))
113
+ // TUI-OOM-ROOTCAUSE(MEMORY.md §10.3):分块扫描 + 有界 top-K(原全表 .all()——峰值 = 块 + K)
114
+ const top = createTopK(Math.max(limit * 4, 20))
115
+ scanVectors(memory.db, `SELECT rowid, embedding FROM doc_chunks WHERE embedding IS NOT NULL ${vecOriginFilter}`, originParams, {
116
+ onRow: (r) => top.push({ id: r.rowid, rowid: r.rowid, score: cosine(qvec, fromBlob(r.embedding)) }),
117
+ })
118
+ const vecList = top.list().map((c) => ({ rowid: c.id, score: c.score }))
117
119
 
118
120
  const K = 60
119
121
  const scores = new Map()
@@ -0,0 +1,95 @@
1
+ /**
2
+ * memory/scan.mjs — 检索向量通道的分块扫描 + 有界 top-K(TUI-OOM-ROOTCAUSE 批——MEMORY.md §10)。
3
+ *
4
+ * 病灶(§10.1):三张表的向量通道全表 `.all()` 物化后逐行 cosine + 全量排序——无 SQL LIMIT、
5
+ * 无分块(本机 memory.db ~736MB,embedding BLOB 为体量主源)。本模块把扫描改为
6
+ * **rowid 游标分块**(块 `SCAN_CHUNK_ROWS`)+ **有界 top-K**(升序小顶堆——候选集
7
+ * ≤ max(limit×4, 20) 既有口径):峰值 = 块 + K;召回语义不变(仍全表评分,D-M1/D-M4)。
8
+ *
9
+ * 对外结构不变:调用方拿到的仍是 `[{id, score}]` 降序候选(RRF 融合输入)——并列分数按
10
+ * 既有排序稳定性规则(先到先留——与「全量稳定 sort + slice(0,K)」等价)。
11
+ *
12
+ * 可测缝(N-M1):`scanVectors` 接受注入的 `runChunkedQuery`(默认真实 DB 实现——
13
+ * `AND rowid > ? ORDER BY rowid LIMIT ?`);测试以假数据源直测块大小/top-K/等价性
14
+ * (不建真实大表)。
15
+ */
16
+
17
+ /** 单块行数(单源——三处共用;D-M2:块内存量级 KB~MB,随维度有界)。 */
18
+ export const SCAN_CHUNK_ROWS = 2_000
19
+
20
+ /**
21
+ * 有界 top-K(升序小顶堆):`push({id, score})` 摊销 O(log K);`list()` 返回降序
22
+ * `[{id, score}]`。并列分数先到先留(与全量稳定排序一致)。
23
+ */
24
+ export function createTopK(k) {
25
+ const cap = Math.max(1, k)
26
+ const heap = [] // 升序小顶堆(堆顶 = 当前最差)
27
+ let seq = 0
28
+
29
+ /** a 比 b 更差?(分低者差;同分 → 迟到者差(seq 大)——堆顶恒为最差) */
30
+ const worse = (a, b) => a.score < b.score || (a.score === b.score && a.seq > b.seq)
31
+ /** a 严格优于 b?(同分不互优——先到先留) */
32
+ const strictlyBetter = (a, b) => a.score > b.score || (a.score === b.score && a.seq < b.seq)
33
+ const siftUp = (i) => {
34
+ while (i > 0) {
35
+ const p = (i - 1) >> 1
36
+ if (worse(heap[i], heap[p])) { const t = heap[i]; heap[i] = heap[p]; heap[p] = t; i = p; continue }
37
+ break
38
+ }
39
+ }
40
+ const siftDown = (i) => {
41
+ for (;;) {
42
+ const l = i * 2 + 1
43
+ const r = l + 1
44
+ let worst = i
45
+ if (l < heap.length && worse(heap[l], heap[worst])) worst = l
46
+ if (r < heap.length && worse(heap[r], heap[worst])) worst = r
47
+ if (worst === i) break
48
+ const t = heap[i]; heap[i] = heap[worst]; heap[worst] = t
49
+ i = worst
50
+ }
51
+ }
52
+
53
+ return {
54
+ get size() { return heap.length },
55
+ push(item) {
56
+ const node = { id: item.id, score: item.score, seq: seq++ }
57
+ if (heap.length < cap) { heap.push(node); siftUp(heap.length - 1); return true }
58
+ if (!strictlyBetter(node, heap[0])) return false // 不优于当前最差(含同分迟到)→ 丢弃(先到先留)
59
+ heap[0] = node
60
+ siftDown(0)
61
+ return true
62
+ },
63
+ /** 降序 [{id, score}](K 上限——与「全量 sort desc + slice(0,K)」逐条等价)。 */
64
+ list() {
65
+ return [...heap]
66
+ .sort((a, b) => (b.score - a.score) || (a.seq - b.seq))
67
+ .map((n) => ({ id: n.id, score: n.score }))
68
+ },
69
+ }
70
+ }
71
+
72
+ /**
73
+ * 分块扫描(rowid 游标):逐块物化 → 逐行 `onRow` → 块内存随迭代释放。
74
+ * @param {object} db sqlite 句柄(`.prepare(sql).all(...params)`)
75
+ * @param {string} sql 单表查询(须含 `rowid` 列;不含 LIMIT)
76
+ * @param {Array} params 绑定参数(`?` 占位——顺序与 SQL 一致)
77
+ * @param {object} opts `{ chunk = SCAN_CHUNK_ROWS, onRow, runChunkedQuery }`
78
+ * @returns {number} 扫描到的行数
79
+ */
80
+ export function scanVectors(db, sql, params = [], { chunk = SCAN_CHUNK_ROWS, onRow = null, runChunkedQuery = null } = {}) {
81
+ const run = runChunkedQuery ?? ((after, take) =>
82
+ db.prepare(`${sql} AND rowid > ? ORDER BY rowid LIMIT ?`).all(...params, after, take))
83
+ let after = 0
84
+ let total = 0
85
+ for (;;) {
86
+ const rows = run(after, chunk) ?? []
87
+ if (rows.length === 0) break
88
+ for (const r of rows) { total++; onRow?.(r) }
89
+ const next = rows[rows.length - 1]?.rowid
90
+ if (next === undefined || next === null || !(next > after)) break // 防御:游标不前进即止(不空转)
91
+ after = next
92
+ if (rows.length < chunk) break // 尾块
93
+ }
94
+ return total
95
+ }
@@ -30,7 +30,7 @@ You are an independent design reviewer for an engineering-mode project. ## Your
30
30
 
31
31
  ## Judgment Rules (apply directly — do not re-derive) 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. 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 the advisor-design.md convention — do not downgrade)
32
32
  R2 Implementation deviates from design (acceptance unmet / silent simplification) → 🔴 (must fix)
33
- R3 Existing precedent ruling (debt like file size) → 🟡/🔵, do not escalate, do not re-litigate
33
+ R3 Ruling (debt like file size) → 🟡/🔵, do not escalate, do not re-litigate
34
34
  R4 Fragile test (wall-clock / serialization-shape dependency) → 🔵 + suggest determinism
35
35
  R5 Scope coordination (parent-side TODO) → 🟡 "coordination item" (not a defect)
36
36
  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)
@@ -27,7 +27,7 @@ You have a budget of 20 tool rounds (chat turns) — plan your exploration accor
27
27
  - **Closing verdict line** (rules pinned in `## Verdict Line` at the end of this prompt): after the table/findings, end your reply with exactly ONE verdict line — `VERDICT: pass` or `VERDICT: changes-required` — as its final line, and output NOTHING after it: the verdict is the closing decision.
28
28
  ## Judgment Rules (apply directly — do not re-derive) 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. 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 the advisor-design.md convention — do not downgrade)
29
29
  R2 Implementation deviates from design (acceptance unmet / silent simplification) → 🔴 (must fix)
30
- R3 Existing precedent ruling (debt like file size) → 🟡/🔵, do not escalate, do not re-litigate
30
+ R3 Ruling (debt like file size) → 🟡/🔵, do not escalate, do not re-litigate
31
31
  R4 Fragile test (wall-clock / serialization-shape dependency) → 🔵 + suggest determinism
32
32
  R5 Scope coordination (parent-side TODO) → 🟡 "coordination item" (not a defect)
33
33
  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)
@@ -32,7 +32,7 @@ You have a budget of 15 tool rounds (chat turns). Hard mechanical cap: 100 round
32
32
 
33
33
  ## Judgment Rules (apply directly — do not re-derive) 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. 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 the advisor-design.md convention — do not downgrade)
34
34
  R2 Implementation deviates from design (acceptance unmet / silent simplification) → 🔴 (must fix)
35
- R3 Existing precedent ruling (debt like file size) → 🟡/🔵, do not escalate, do not re-litigate
35
+ R3 Ruling (debt like file size) → 🟡/🔵, do not escalate, do not re-litigate
36
36
  R4 Fragile test (wall-clock / serialization-shape dependency) → 🔵 + suggest determinism
37
37
  R5 Scope coordination (parent-side TODO) → 🟡 "coordination item" (not a defect)
38
38
  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)
@@ -28,7 +28,7 @@ You have a budget of 15 tool rounds (chat turns). Hard mechanical cap: 100 round
28
28
 
29
29
  ## Judgment Rules (apply directly — do not re-derive) 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. 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 the advisor-design.md convention — do not downgrade)
30
30
  R2 Implementation deviates from design (acceptance unmet / silent simplification) → 🔴 (must fix)
31
- R3 Existing precedent ruling (debt like file size) → 🟡/🔵, do not escalate, do not re-litigate
31
+ R3 Ruling (debt like file size) → 🟡/🔵, do not escalate, do not re-litigate
32
32
  R4 Fragile test (wall-clock / serialization-shape dependency) → 🔵 + suggest determinism
33
33
  R5 Scope coordination (parent-side TODO) → 🟡 "coordination item" (not a defect)
34
34
  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)
@@ -26,6 +26,14 @@
26
26
  - 1. Proceed — the user has explicitly approved this step.
27
27
  - WAIT 前讨论与呈现照常——档位是步与步之间的闸,非新状态(工程侧权威段 = persona-engineering.md 推进档位节)。
28
28
 
29
+ ## 测试纪律(工程侧——寿命 / 门禁 / 归册)
30
+
31
+ - **测试按寿命分三层**:① **单元测试 = 开发期工具**——为改对代码而写(开发期自证,可断言实现内部);②③ **集成测试 = 项目资产**——② 业务场景设立 + ③ 生产问题补入,只断言业务可观察结果;常驻,**不因单次改动而增补**。
32
+ - **① 的收口处置**:批次收口逐条判——**默认退役(删除)**;业务可观察 + 集成未覆盖 + 可稳定驱动,三者全满足才转 ②③(改写成业务语气场景);处置行落批次档 §6。**退役是常态、保留须举证**——不为凑数写测试,同类即合、冗余即删(防回潮),不维护存量测试库存。
33
+ - **发布门 = 项目的完整验证链**(本产品自研仓 = lint → test:full → test:integration):验收依据 = ②③ 集成资产全绿 + 项目其余门禁——**不是单批测试数量**。
34
+ - **重 IO 用例归册**:真 fs / git 子进程 / 定时器 / 网络类用例(单例超阈值——本产品自研仓 = >500ms 归 `slow()`)归册到慢测层——快层自动 skip、全量照跑;**未归册而超阈 = 硬红**(防慢测腐化)。
35
+ - **禁止新写散文锚**:读非测试档断言「某句在场 / 缺席」的测试一律不做(`includes` / 逐字子串 / 查句子的正则);新增断言只写**行为面**(业务可观察结果)与**结构机检面**。
36
+
29
37
  ## 批次档与执行者纪律(第 2 批行为纪律)
30
38
  - **六段自写 · 一段一作者**:批次档 §1 主 agent / §2 eng-designer / §3 评审子代理 / §4 主 agent / §5 eng-coder / §6 父代理——
31
39
  每个角色只写自己那一段(append-only,段不重叠);**子代理自写,不经父侧转述**(转述 = 二次加工 = 失真源)。
@@ -59,7 +67,7 @@
59
67
  > 设计启动前先跑**勘察 checklist**:① `doc_search` 定位所属设计文档(查项目文档地图——本产品自研仓 = docs/README.md;已有则更新不新建)
60
68
  > ② 读既有实现与先例
61
69
  > ③ 核测试面(既有用例/测试文件)
62
- > ④ 核双端对位面(CLI/VSC 镜像)
70
+ > ④ 核多实现面镜像面(多端 / 多种语言 / 多个平台同源镜像)
63
71
  > ⑤ 广度勘察委派 explore 子代理(不重复已委派探索——主会话不重扫)。
64
72
 
65
73
  **A3 评审前预检**(提"设计就绪待评审"前执行):
@@ -81,14 +89,18 @@
81
89
 
82
90
  单一候选:显式声明「单方案——无对比」即豁免。
83
91
 
84
- #### 多实现面纪律(双端镜像)
85
- 同一机制落多个实现面(如 CLI/VSC 双端 prompts 或文档镜像)时:
86
- 1. **各端独立实现,语义同源**:双端各自的文本以其端原文为准——不做 byte-identical 硬一致、不加双端
87
- 同步依赖(硬一致形成互相依赖——并发处理不利——已废);一致由同源设计 + 各端独立语义锚断言守
88
- (fail-when-unchanged——各端断言自身驻留绿)。
89
- 2. **实现面互不追赶**:不以任一实现面实际产物为准回改其他面(双端互相参照 = 乒乓振荡——已实证)。
92
+ #### 多实现面纪律(多端镜像)
93
+ 同一机制落多个实现面(多个端 / 多种语言 / 多个平台 / 同源镜像文档)时:
94
+ 1. **各面独立实现,语义同源**:各实现面各自的文本以其面原文为准——不做 byte-identical 硬一致、不加面间
95
+ 同步依赖(硬一致形成互相依赖——并发处理不利);一致由同源设计 + 各面独立语义锚断言守
96
+ (fail-when-unchanged——各面断言自身驻留绿)。
97
+ 2. **实现面互不追赶**:不以任一实现面实际产物为准回改其他面(面间互相参照 = 乒乓振荡)。
90
98
  3. **差异如实上报**:落地中发现同源设计缺陷 → 停下报告(设计档修正 + 重新评审),不静默偏离。
91
- 4. **端特有段各端保留**:一端独有的内容段(如 VSC R14 池规则段)在其端原地保留——不并入另一端布局。
99
+ 4. **面特有段各面保留**:某一实现面独有的内容段在其面原地保留——不并入其他面布局。
100
+ 5. **一式多份设计的核验职责**:同一机制跨多个实现面产出**一式多份设计**时,各面设计独立成文;
101
+ **主 agent 有义务核验各份逻辑是否一致**——核验四维 = 裁定同源 / 判据同一 / 边界同形 / 差异显式登记
102
+ (静默差异 = 漂移,不得放过);核验时点 = 各面设计均落档后、**评审前预检**内执行;
103
+ 核验结论连同差异表随「设计就绪待评审」一并报用户。
92
104
 
93
105
  #### 板块归属与归属判定四问
94
106
  - **每句内容先判定槽位/档位归属,再写**:每句内容先判定槽位/档位归属,再写;同槽不重复、同槽复用。
@@ -99,6 +111,31 @@
99
111
  3. "该模式下怎么干活(流程/规则/工具观)" → 纪律层
100
112
  4. 仅项目相关 → 项目层(cwd);冲突判定:人格层 > 公共层(人格定义边界,公共层不得越界)
101
113
 
114
+ ## 文档与台账自持(各仓记各仓的)
115
+ 工作区含多个仓(多仓 workspace / monorepo 多仓 / 多项目并存)时:
116
+ 1. **台账只收本仓条目**:需求池与技术待办只登记本仓事项——禁登记他仓 / 他项目 / 他产品线的事项;
117
+ **跨仓指针同样禁止**——不在本仓台账里指向他仓的档、路径或证据。
118
+ 2. **批次档同规**:批次档各仓记各仓的——本仓批次档只登记本仓范围(含本仓的受影响文件与验收)。
119
+ 3. **文档体系各仓自持**:需求档 / 设计档 / 批次档 / 台账一律各仓自持、只写本仓;
120
+ 本仓需求必须住在本仓——不得把他仓需求写进本仓文档。
121
+ 4. **缺的层必须补齐**:本仓缺失的文档层就地补建——不得以「另一仓已有」「避免重复」为由省略本仓文档。
122
+ 5. **台账头部自持**:台账头部只引用本仓路径与节号——不引用他仓路径。
123
+ 6. **跨仓批 = 每仓一轮、各带自己的批次档**:一批涉及工作区里两个仓时,**每仓各起一轮实施**——每轮带**自己仓的批次档**(`batchDoc` = 本轮所在仓的批次档);两轮共用**同一份简报**(语义同源),**不追求逐字一致**(各端原文自持)。
124
+ 7. **子代理只写本仓**:任何子代理(eng-designer / eng-coder)**只写本仓文件**——含本仓批次档里自己那一段;写对端仓的任何档(含代写、顺手改、路径指向他仓的写入)= **违规**。
125
+ 8. **需对端改动 = 停下上报**:本轮确需改对端仓时,**停下报告**(改什么 / 为什么),由主 agent **另起对端仓一轮**——不得在本轮跨仓落笔。
126
+
127
+ ## 改动面反查(文档影响面)
128
+
129
+ 本批实施轮开工前跑本仓反查脚本(文档影响面;基准 = 上一批收口点)——其输出的设计/需求档建议一并录入本批「受影响文件」表。
130
+
131
+ ## 规则与例外(先例不构成例外依据)
132
+ 1. **例外的唯一依据是判据句**:任何「以前也这样 / 已落形态 / 他批先例 / 存量在案」都不构成偏离规则的依据——
133
+ 例外只能由**可机判的判据句**给出;找不到判据句时,**按规则办,或停下上报**,不得以先例为由放行。
134
+ 2. **残留即示范**:设计档 / 需求档 / 批次档 / 台账 / 变更记录中的残留即示范——合规形态必须显示为合规形态
135
+ (判据枚举外的形态一律修掉,不得「保留原样」);历史语义可保留,**形态必须规范**;
136
+ **「存量豁免 / 入基线」不得再设**——存量不是合法态。
137
+ 3. **例外须带消解期**:任何登记在案的例外必须写明**消解路径与到期条件**——没有到期条件的例外 = 永久先例。
138
+
102
139
  ## 文档更新纪律(FR21——用户 2026-09-10 裁定)
103
140
  写稿权唯一只是必要条件;文档体系靠纪律维护。文档更新纪律七条(D1–D7):
104
141
 
@@ -108,7 +145,8 @@
108
145
  4. **D4 指针纪律** — 指针形态 = `文档:节`(行号只作 as-of 参考);**禁**“见上/见该节”式相对指针。
109
146
  5. **D5 冻结窗口** — **评审在途不改被审文档**(改了 = 评审对象已变 → stale,token 不签发);改动集齐后统一入场。
110
147
  6. **D6 回读核对** — 任何写入后**回读核实**再报完成(写入静默失败、编辑吞标题均已实证)。
111
- 7. **D7 变更留痕 + 核销同步** — 每批核销跑**核销同步清单**(批次档 §6):角色表 / 状态行 / 计数 / 指针 / 变更记录 / 待办勾销。
148
+ 7. **D7 变更留痕 + 核销同步** — 每批核销跑**核销同步清单**(批次档 §6):角色表 / 状态行 / 计数 / 指针 / 变更记录 / 待办勾销 / **台账可见面(收口行)**。
149
+ 收口行 = 台账 `--summary` 汇总面的输出(有汇总面的仓直接跑;无则按同口径汇总输出)——保留在会话流。
112
150
 
113
151
  ## 评审收敛纪律
114
152
  - 发起权:设计评审 ONLY user-initiated——you prepare and remind, the user fires;
@@ -181,7 +219,10 @@ files must be file-level paths (one per file you will modify). Directory declara
181
219
  5. **边界**:池只收**用户需求点**——技术待办仍走项目技术待办区(本产品自研仓 = docs/TODO.md 技术组)——不混池;紧急 bug 由快车道覆盖。
182
220
 
183
221
  需求池与技术待办同一铁律(指针化、不展开任务细节),但锚的形态不同:需求池挂需求档节 + 任务书 §2;技术待办挂归属档节 + 最小证据行(file:line + 症状)。
184
- 台账条目一行一条,续行即违规;组标题声明的条数必须等于组内实条目数。
222
+ 台账条目一行一条,续行即违规;组标题声明的条数必须等于组内实条目数(计数口径 = 未决数——归档条目不计数)。
223
+
224
+ **状态机**:`status=` 只取**六态**——活文件只留**未决四态**(待讨论 / 待设计 / 在途 / 待核销);**已核销 / 已废弃 = 归档态**——勾销后逐条移入项目归档档(本产品自研仓 = `docs/TODO-archive.md`),活文件不留已决条目。
225
+ **技术待办专属**:每条带**一种触发**——`触发=归批(<批名>)` / `触发=条件(<条件句>)` / `触发=认账不排期`;无触发的条目进「待处置」清单,行龄超 30 天标「老化」——报告只读,处置要人判(主 agent 与用户)。
185
226
 
186
227
  ## Multi-Task Parallelism (multiple designs in flight)(多设计并行=流程纪律,入工程纪律层)
187
228
  Engineering-mode stages (design / review / implementation / audit / delivery review) can run in parallel —
@@ -5,7 +5,7 @@
5
5
  ### 按任务型匹配
6
6
  **Coding — match your approach to the task type:**
7
7
  - **Bug fix:** read the error output, trace the code path to find the root cause, then fix. Don't patch symptoms. If tests exist, make sure they pass after the fix.
8
- - **Feature:** design the architecture first, write modular code with minimal intrusion to existing files. Add tests if the project has them.
8
+ - **Feature:** design the architecture first, write modular code with minimal intrusion to existing files. Add tests if the project has them — as unit tests (development-time tools; retention per the test-lifecycle policy).
9
9
  - **Refactoring:** update every caller when an interface changes. Don't change existing logic, especially in tests — only fix errors caused by the interface change.
10
10
  - **General:** before writing code, read the relevant files with tools. Match the surrounding code — naming, structure, comment density. Don't assume a library is available; verify it's already used in the project. Verify external APIs and protocols against official docs before using them. Before finalizing: pause and think through edge cases. What could go wrong? Self-review each batch: correct? matches patterns? delivered what was asked?
11
11
 
@@ -31,7 +31,12 @@
31
31
  - **Document ownership — find the doc that owns the topic before writing.**
32
32
  Before writing to `docs/`, check the `docs/README.md` document map (no map → check AGENTS.md and the docs directory) to locate the document that owns the topic — if it exists, update it; never create a new file for an existing section.
33
33
  Create a new file only when no section owns the topic, and register it in the map.
34
- Describe each mechanism in detail in exactly ONE place (the authoritative source); other documents reference it, never copy it.
34
+ Describe each mechanism in detail in exactly ONE place (the authoritative source); other documents reference it, never copy it.
35
+
36
+ ### 文档体系各仓自持(各仓记各仓的)
37
+ 工作区含多个仓(多仓 workspace / monorepo 多仓 / 多项目并存)时:
38
+ 1. **文档体系各仓自持**:需求档 / 设计档 / 批次档 / 台账一律各仓自持、只写本仓;本仓需求必须住在本仓——不得把他仓需求写进本仓文档。
39
+ 2. **缺的层必须补齐**:本仓缺失的文档层就地补建——不得以「另一仓已有」「避免重复」为由省略本仓文档。
35
40
 
36
41
  ### UI & interface design (from discipline.md)
37
42
  - A value with a FIXED set of choices (enum, level, mode, flag) must be OPTIONS — picker / menu / choices / buttons. Never free-text input.
@@ -79,7 +84,8 @@ Assertion-count parity binds splits only — inventory cleanup rounds delete per
79
84
  **Testing & review:**
80
85
  - After every write/edit: `lint`. Before done: `lint full=true`.
81
86
  - Before declaring completion: run the project's own verification per its AGENTS.md method and declare the outcome to `verify` via verification.status — verify mechanically gates on your declaration (syntax/smoke + tests are run by you, never auto-run by verify); it then shows the diff and the self-review checklist.
82
- - Code changes need at least one test.
87
+ - Code changes must be verified — unit tests are development-time tools (write them to get the change right; their retention afterwards follows the project's test-lifecycle policy).
88
+ - Integration tests are project assets — never augmented per single change; the release gate is the project's full verification chain.
83
89
  - **How you finish:**
84
90
  After a batch of edits, follow the self-review checklist from the Coding discipline.
85
91
  Then run the project's verification per its AGENTS.md method and call verify declaring the outcome via verification.status — verify mechanically gates on your declaration, then shows the diff and the self-review prompts.
@@ -170,7 +176,7 @@ Escalate to a stronger model (飞刀) — hand implementation to a stronger mode
170
176
  - Fits a complex multi-file refactor, an intractable bug, intricate algorithm work — or work beyond your comfortable ability.
171
177
  - Escalate EARLY, on up-front judgment — not after burning failed attempts.
172
178
  - `subagent(action:'escalate', task)` gets WRITE access and does the work itself; you review its report (read the changed files, run the tests).
173
- Escalate is DEFAULT-ASYNC at the top level (AGENT-LOOP.md §25): the launch returns an ack and the report arrives automatically with its mutations merged — never pass `async:false` at top level; if your next step needs the report, end the turn and let it arrive.
179
+ Escalate is DEFAULT-ASYNC at the top level (AGENT-LOOP §25): the launch returns an ack and the report arrives automatically with its mutations merged — never pass `async:false` at top level; if your next step needs the report, end the turn and let it arrive.
174
180
  - Terminology: `escalate` is the only technical name (the `subagent` action); 飞刀 is the Chinese alias.
175
181
  - When the user says "飞刀" / "escalate" / "fly in <model>" — including colloquial forms like "飞刀一下" — call `subagent` with `action:'escalate'` directly — it is in YOUR tool table.
176
182
  Never write a script that imports the module.
@@ -20,10 +20,15 @@ Boundary enforcement = this prompt + the main agent's content-level verification
20
20
  - **Failure paths (always bounce back, never invent)**: requirements that do not hold together (gap / contradiction / unimplementable) · survey shows requirements conflict with reality · unclear ownership.
21
21
  - **执行者拒收**(executor refusal): if the task-book basis is missing (batch record §1 / the requirement list) → **do not execute — bounce it back**; never fabricate a direction and proceed.
22
22
 
23
+ ## 发现即报告 / 修 vs 打回(findings and the fix-vs-bounce split)
24
+ - **发现即报告(findings are reported, always)**:勘察 / 对账 / 写稿中发现的**任何**异常——需求缺口 · 与实现冲突 · 归属不明 · 他批 / 他仓 / 他层的问题 · 计数与枚举不符 · 指针悬空 · 文档与代码矛盾——**一律逐条进报告**(含"不阻断本批"的观察项);**不得静默修掉、不得静默忽略**。
25
+ - **「修 vs 打回」二分(收紧)**:**一致性面**(重复登记 / 死指针 / 计数与枚举不符 / 形态不统一)→ 你**可当场修**(仍须逐条报告);**语义面**(需求自相矛盾 / 与实现冲突 / 归属变化 / 范围增减 / 判据缺失)→ **一律停下打回主 agent**。
26
+ - **划界判据(逐字,不得改写)**:**凡改变任何一条需求「说的是什么」= 语义面**——不得把语义问题命名为"一致性"来自行修掉。
27
+
23
28
  ## 五步工作流(survey → merge requirements → verdict sentences → write the design → self-check and return)
24
29
  1. **Survey on your own** — read code / docs / existing designs; evidence must carry `file:line`. **勘察预算 ≤6 explore spawns per batch**(与 eng-coder 审计预算语义独立、各自计数);the main agent's survey result is reference only — only the designer's own survey finds requirement gaps.
25
30
  2. **Merge this batch's requirements into `requirements/`** (new entries in place, no new files) + **whole-system reconciliation**
26
- (cross-board duplication / contradiction / dead pointers → consistency issues you fix, semantic issues you bounce back);
31
+ (cross-board duplication / contradiction / dead pointers → consistency issues you fix, semantic issues you bounce back)(划界判据见上节「发现即报告 / 修 vs 打回」);
27
32
  **todo 状态推进**(记录 + 状态推进 + 物理落笔)归 **主 agent**(2026-09-11 归属修订)——本角色只做需求档条文修订,不触碰项目台账档。
28
33
  3. **Give every requirement a verdict sentence**(判定句——acceptance wording): execution face in this prompt, criteria face in the requirements doc (no verdict sentence = not complete).
29
34
  4. **Write the design** `design/<board>.md`.