@thincoder/core 0.9.1 → 0.9.3

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 (75) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/LICENSE +21 -0
  3. package/README.md +90 -0
  4. package/advisor/loop.mjs +2 -2
  5. package/advisor/run.mjs +1 -1
  6. package/agent/completion.mjs +3 -1
  7. package/agent/family-tools.mjs +24 -11
  8. package/agent/helpers.mjs +11 -2
  9. package/agent/run-stages.mjs +27 -5
  10. package/agent/setup-reminders.mjs +67 -11
  11. package/agent/setup.mjs +6 -0
  12. package/agent/write-gate.mjs +5 -5
  13. package/agent-tools/advisor-async.mjs +4 -4
  14. package/agent-tools/advisor.mjs +3 -3
  15. package/agent-tools/async-discard.mjs +1 -1
  16. package/agent-tools/audit-block.mjs +106 -0
  17. package/agent-tools/batch-lifecycle.mjs +301 -0
  18. package/agent-tools/batch-segment.mjs +16 -263
  19. package/agent-tools/batch-skeleton.mjs +156 -0
  20. package/agent-tools/batch.mjs +410 -0
  21. package/agent-tools/context.mjs +174 -0
  22. package/agent-tools/eng.mjs +4 -0
  23. package/agent-tools/goal.mjs +7 -0
  24. package/agent-tools/parent-channel.mjs +18 -1
  25. package/agent-tools/plan.mjs +39 -5
  26. package/agent-tools/read-history.mjs +122 -24
  27. package/agent-tools/settings.mjs +4 -2
  28. package/agent-tools/subagent-async.mjs +3 -3
  29. package/agent-tools/subagent-spawn.mjs +29 -101
  30. package/agent-tools/task.mjs +11 -0
  31. package/agent-tools.mjs +9 -2
  32. package/agent.mjs +10 -4
  33. package/config.mjs +1 -1
  34. package/context.mjs +66 -121
  35. package/fts-text.mjs +41 -0
  36. package/generate-title.mjs +6 -6
  37. package/i18n.mjs +4 -4
  38. package/ledger-cmd.mjs +30 -7
  39. package/ledger-db.mjs +22 -2
  40. package/ledger-executors.mjs +103 -0
  41. package/ledger-surface.mjs +15 -8
  42. package/ledger.mjs +15 -4
  43. package/manifest.mjs +172 -60
  44. package/memory/core.mjs +4 -18
  45. package/memory/schema.mjs +4 -11
  46. package/package.json +22 -1
  47. package/prompts/advisor-design.md +1 -1
  48. package/prompts/advisor-round2.md +1 -1
  49. package/prompts/advisor-round3.md +1 -1
  50. package/prompts/common.md +3 -3
  51. package/prompts/discipline-engineering.md +17 -2
  52. package/prompts/persona-eng-coder.md +1 -1
  53. package/prompts/persona-eng-designer.md +1 -1
  54. package/prompts/persona-engineering.md +9 -6
  55. package/session-gc.mjs +129 -74
  56. package/session-index-build.mjs +298 -0
  57. package/session-index-cmd.mjs +61 -0
  58. package/session-index-pass.mjs +95 -0
  59. package/session-index-query.mjs +102 -0
  60. package/session-index.mjs +285 -0
  61. package/session-lifecycle.mjs +18 -5
  62. package/session-slots-manifest.mjs +55 -3
  63. package/session-stale.mjs +247 -0
  64. package/token-window.mjs +188 -0
  65. package/tools/bash.mjs +4 -15
  66. package/tools/execute.mjs +5 -13
  67. package/tools/git-checkpoint.mjs +1 -1
  68. package/tools/git-ext.mjs +23 -20
  69. package/tools/git-run.mjs +141 -0
  70. package/tools/git.mjs +56 -45
  71. package/tools/index.mjs +3 -1
  72. package/tools/process-tree.mjs +20 -0
  73. package/tools/shared.mjs +8 -4
  74. package/traces/trace-cleanup.mjs +109 -0
  75. package/traces/trace-store.mjs +32 -36
@@ -12,12 +12,13 @@
12
12
  * 常驻标记,渲染端消费;**不受启动行门约束**——F2 按标记态照显)。
13
13
  * 只读台账;唯一写面 = 去重档(`~/.thincoder/ledger-notify.json`)。
14
14
  */
15
- import { buildScan, detailScans, discoverFamily, formatDetailLine, formatMarker, loadNotifyState, NOTIFY_FILE, notifyKey, planChangeLines, REFRESH_MS, saveNotifyState } from "./ledger.mjs"
15
+ import { buildScan, detailScans, discoverFamily, formatDetailLine, formatMarker, loadNotifyState, NOTIFY_FILE, notifyKey, planChangeLines, REFRESH_MS, resolveExecutorStates, saveNotifyState } from "./ledger.mjs"
16
16
 
17
- /** 单次扫描(直驱面——timer 包装见 `startLedgerSurface`)。
17
+ /** 单次扫描(直驱面——timer 包装见 `startLedgerSurface`)。**async**(F-LX1:await 判活解析——
18
+ * LEDGER.md §7.3.1 ④ 读面不阻塞事件循环)。
18
19
  * `startup=true` = 会话首扫(补启动行;变化行在前、明细行在后)。
19
20
  * `colors` = 端注入的渲染色表(`{ warn, dim }`);未注入 ⇒ `{}`(`undefined` 色值 = 纯文本)。 */
20
- export function runLedgerScan({ state, agent = null, anchor = null, notifyFile = NOTIFY_FILE, pushLine = () => {}, render = () => {}, startup = false, colors = {} } = {}) {
21
+ export async function runLedgerScan({ state, agent = null, anchor = null, notifyFile = NOTIFY_FILE, pushLine = () => {}, render = () => {}, startup = false, colors = {} } = {}) {
21
22
  const base = anchor ?? agent?.cwd ?? process.cwd()
22
23
  const family = discoverFamily(base)
23
24
  const scans = []
@@ -25,6 +26,7 @@ export function runLedgerScan({ state, agent = null, anchor = null, notifyFile =
25
26
  try { scans.push(buildScan({ cwd: p.root })) } catch { /* 台账不可读 → 该项目跳过(余者照常——N1/T110①) */ }
26
27
  }
27
28
  const current = family.current ? scans.find((s) => s.root === family.current.root) ?? null : null
29
+ await resolveExecutorStates(scans) // 判活批量解析(一次探束 + TTL 缓存;无在途零 exec)
28
30
  const notify = loadNotifyState(notifyFile)
29
31
  const plans = scans.map((s) => ({ s, plan: planChangeLines(notify.ledgers[notifyKey(s.ledger)], s) }))
30
32
  const changeLines = plans.flatMap(({ plan }) => plan.lines)
@@ -46,19 +48,24 @@ export function runLedgerScan({ state, agent = null, anchor = null, notifyFile =
46
48
  for (const { s, plan } of plans) ledgers[notifyKey(s.ledger)] = { ...plan.next, updatedAt: Date.now() }
47
49
  saveNotifyState(notifyFile, { version: 1, ledgers })
48
50
  }
49
- state.ledger = { marker: current ? formatMarker(current) : null, warn: Boolean(current && current.aged > 0), scannedAt: Date.now() }
51
+ // warn 判位钉在判活解析之后(裁定 #7):属主已死也是可动作态
52
+ state.ledger = { marker: current ? formatMarker(current) : null, warn: Boolean(current && (current.aged > 0 || current.deadExecutors > 0)), scannedAt: Date.now() }
50
53
  render()
51
54
  }
52
55
 
53
56
  /** 挂载:首扫(`setImmediate`——不抢首帧)+ 周期(`REFRESH_MS`;`state.processing` 期间跳过本轮)。
54
- * 端注入的渲染面(`ctx.colors`)与推送面(`ctx.pushLine` / `ctx.render`)照传。 */
57
+ * 端注入的渲染面(`ctx.colors`)与推送面(`ctx.pushLine` / `ctx.render`)照传。
58
+ * `tick` 异步化(F-LX1——runLedgerScan async):await 且 catch 不崩(N1)。重叠防衛:
59
+ * 判活探测秒级可达,上一轮未落定时跳过本轮(防探束堆积)。 */
55
60
  export function startLedgerSurface(ctx = {}) {
56
61
  let disposed = false
57
62
  let timer = null
58
- const tick = (startup) => {
59
- if (disposed) return
63
+ let inflight = false
64
+ const tick = async (startup) => {
65
+ if (disposed || inflight) return
60
66
  if (!startup && ctx.state?.processing) return // 扫描不打断回合帧
61
- try { runLedgerScan({ ...ctx, startup }) } catch { /* 可见面尽力——不崩(N1) */ }
67
+ inflight = true
68
+ try { await runLedgerScan({ ...ctx, startup }) } catch { /* 可见面尽力——不崩(N1) */ } finally { inflight = false }
62
69
  }
63
70
  setImmediate(() => {
64
71
  if (disposed) return
package/ledger.mjs CHANGED
@@ -9,6 +9,8 @@
9
9
  * ② 族发现(findProject / discoverFamily——标记 = ledger.db)③ scan 组装(buildScan——行集 + ledgerCount
10
10
  * → scan 对象,形状契约 = pool/tech/aged/thresholdReached/actionable/root/name/ledger)
11
11
  * ④ 通知去重(送达门 + 一次性去重——去重档跨会话、跨端共享)⑤ 格式 helper(行文本逐字契约)。
12
+ * 判活展示 = 拆分件 `ledger-executors.mjs`(在途 executor 解析 + 三态尾段——本档 re-export,
13
+ * 消费面仍只认本档)。
12
14
  *
13
15
  * W8 契约②:ledger-db.mjs 静态 import node:sqlite ⇒ 消费侧一律**动态 import** 本档(禁止静态 import)。
14
16
  */
@@ -17,6 +19,7 @@ import { basename, dirname, join, resolve } from "node:path"
17
19
  import { configDir } from "./config.mjs"
18
20
  import { ledgerDbPath, PENDING_STATUSES } from "./ledger-db.mjs"
19
21
  import { ledgerQuery } from "./ledger-cmd.mjs"
22
+ import { executorTail } from "./ledger-executors.mjs"
20
23
 
21
24
  /** 老化阈值(天——口径 = 需求档;`days` 参数可覆盖)。 */
22
25
  export const AGING_DAYS = 30
@@ -105,7 +108,9 @@ export function discoverFamily(anchor) {
105
108
  /** scan 组装(SQLite 行集 + 计数单源 → scan 对象)——形状契约(设计档 §2.2):pool/tech/aged/
106
109
  * agedKeys/agedTitles/thresholdReached/boards/actionable/root/name/ledger。行龄 = 时间戳
107
110
  * (updated_at ?? created_at 距今 > days;未知不计——零假阳降级,同 v1 行龄未知口径)。
108
- * `now`/`days` = 注入面(确定性用例——替代 v1 ageOf git 注入面)。 */
111
+ * `now`/`days` = 注入面(确定性用例——替代 v1 ageOf git 注入面)。
112
+ * `inflightExecutors`(F-LX1 · sync 零变——K-LX2):在途且 executor 非空行集原样携带
113
+ * {executor, updated_at, created_at}——判活解析由 `resolveExecutorStates` async 单源负责,本函数零探测。 */
109
114
  export function buildScan({ cwd, days = AGING_DAYS, now = Date.now() } = {}) {
110
115
  const rows = ledgerQuery({ cwd })
111
116
  const pending = rows.filter((r) => PENDING_STATUSES.includes(r.status))
@@ -123,11 +128,15 @@ export function buildScan({ cwd, days = AGING_DAYS, now = Date.now() } = {}) {
123
128
  }
124
129
  const thresholdReached = poolEntries.length >= THRESHOLD_POOL || [...boards.values()].some((n) => n >= THRESHOLD_BOARD)
125
130
  const root = resolve(cwd)
131
+ const inflightExecutors = pending
132
+ .filter((r) => r.status === "在途" && r.executor)
133
+ .map((r) => ({ executor: r.executor, updated_at: r.updated_at ?? null, created_at: r.created_at ?? null }))
126
134
  return {
127
135
  name: basename(root), root, ledger: ledgerDbPath(root),
128
136
  pool: poolEntries.length, tech: techEntries.length, aged: aged.length,
129
137
  agedKeys: aged.map((r) => normalizeEntry(r.title)), agedTitles: aged.map((r) => entryTitle(r.title)),
130
138
  thresholdReached, boards, actionable: aged.length > 0 || thresholdReached,
139
+ inflightExecutors,
131
140
  }
132
141
  }
133
142
 
@@ -136,9 +145,10 @@ export function formatMarker(scan) {
136
145
  return scan ? `台账 ${scan.pool}·${scan.tech}` : null
137
146
  }
138
147
 
139
- /** L2 明细行(逐字——`(老化 <n>)` 恒显;阈值达成加 ` — 可开批`)。 */
148
+ /** L2 明细行(逐字——`(老化 <n>)` 恒显;阈值达成加 ` — 可开批`;判活尾段 = executorTail 三态——
149
+ * 在途 executor 缺席时尾段空串,既有逐字断言零破)。 */
140
150
  export function formatDetailLine(scan) {
141
- return `台账 ${scan.name}:需求池 ${scan.pool} · 技术待办 ${scan.tech}(老化 ${scan.aged})${scan.thresholdReached ? " — 可开批" : ""}`
151
+ return `台账 ${scan.name}:需求池 ${scan.pool} · 技术待办 ${scan.tech}(老化 ${scan.aged})${scan.thresholdReached ? " — 可开批" : ""}${executorTail(scan)}`
142
152
  }
143
153
 
144
154
  /** L3 变化行·老化(titles = 新增老化条目标题;> 3 条时第三项后接 `;…`)。 */
@@ -198,5 +208,6 @@ export function saveNotifyState(file, state) {
198
208
  }
199
209
 
200
210
  // ── re-export(拆分件接口——命令面接线 = 动态 import 本档,KD-M2-3) ──
201
- export { ALLOWED_MIGRATIONS, ledgerDbPath, openLedger, PENDING_STATUSES, _setLedgerDirForTest, _resetLedgerDirForTest } from "./ledger-db.mjs"
211
+ export { ALLOWED_MIGRATIONS, ledgerDbPath, openLedger, PENDING_STATUSES, _setLedgerDirForTest, _resetLedgerDirForTest, ensureExecutorColumn } from "./ledger-db.mjs"
202
212
  export { ledgerAdd, ledgerAddTool, ledgerClose, ledgerCloseTool, ledgerCount, ledgerCountTool, ledgerQuery, ledgerQueryTool, ledgerUpdate, ledgerUpdateTool } from "./ledger-cmd.mjs"
213
+ export { resolveExecutorStates, executorTail, _setExecutorProbeTtlForTest } from "./ledger-executors.mjs"
package/manifest.mjs CHANGED
@@ -12,10 +12,16 @@
12
12
  * - isValidDocRootValue(value) / docRootPaths(value, cwd)(F7 判据单源 KD-M1-8):值形态谓词
13
13
  * + 值 → 绝对路径数组(展开 / trim + `\` 归一 / 基数 = 项目根 / 去重保序)。
14
14
  * - requireManifest(cwd) → 装配钩子入口 = readManifest(cwd)。
15
- * - discoverRepos(cwd) → { kind: self|unique|none|ambiguous, root, candidates }:仓发现**单源**
16
- * (KD-M1-22——manifest 面与 `git` 工具共用;`candidates` 按名排序);`resolveProjectRoot` = 其薄包装。
15
+ * - discoverProjects(cwd) → { kind, root, candidates, matched }:**项目梯**(五级 / git 非前提——KD-M1-23);
16
+ * `discoverRepos(cwd)` 同形态 = **仓梯**(`.git` 视图——`git` 工具经它接线,KD-M1-22)。
17
+ * 两梯共一模块私有 walk 内核 `scanChildren`(单源);`candidates` 按名排序、`matched` 记档位。
18
+ * - owningProject(target) → 归属形单点(沿祖先链取**最近带档目录**——KD-M1-30)。
19
+ * - projectView(target) → { state, root, path, manifest?, candidates?, errors?, matched }:**按用点解析**
20
+ * 的读侧单点(归属 ∨ 发现兜底 / 五态 / 非抛错 / **零写**——KD-M1-24)。
21
+ * - resolveProjectRoot(cwd) = owningProject(cwd) ?? discoverProjects(cwd).root(KD-M1-30)。
17
22
  * - resolveEngineeringManifest(cwd, { writer, init }) → 入口决策树(**非抛错**——KD-M1-20):
18
- * 两端入口钩子(抛错薄包装)与翻转面(拒翻)共用同一张树(判据单源;§2.8 F1)。
23
+ * 两端入口钩子与翻转面(拒翻)共用同一张树(判据单源;§2.8 F1)——失败码五枚
24
+ * `missing` / `invalid` / `no-project` / `ambiguous` / `init-failed`(KD-M1-28)。
19
25
  * - manifestFilePath(cwd) → 数据档绝对路径(档路径单源 KD-M1-18——读 / 写 / mtime 门控三处同源)。
20
26
  * - initManifest(cwd, { writer = 'subagent' } = {}) → 经写门写 DEFAULT_MANIFEST,缺省拒。
21
27
  * - writeManifest(cwd, manifest, { writer = 'subagent' } = {}) → 落盘前先校验(ok:false 拒
@@ -25,54 +31,151 @@
25
31
  * N3 可迁移:本模块不硬编码任何本仓路径(docRoot / checkConfig 由被开发项目声明)。
26
32
  */
27
33
  import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"
28
- import { join, resolve } from "node:path"
34
+ import { dirname, join, resolve } from "node:path"
29
35
 
30
- /** 数据档文件名(每个 git 仓一份——项目根 = 仓根;2026-09-17 用户裁定)。 */
36
+ /** 数据档文件名(**每个项目一份**——项目根 = 带档目录;**git 非前提**。2026-09-17 / 2026-09-21 用户裁定)。 */
31
37
  export const MANIFEST_REL = "PROJECT-MANIFEST.json"
32
-
33
38
  /** 测试注入:强制项目根(测试 tmp 非 git 仓——同 ledger `_setLedgerDirForTest` 先例)。 */
34
39
  let _projectRootOverride = null
35
40
  export function _setProjectRootForTest(dir) { _projectRootOverride = dir }
36
41
  export function _resetProjectRootForTest() { _projectRootOverride = null }
37
42
 
38
43
  /**
39
- * 仓发现(**单源**——KD-M1-22 / 设计 `docs/core/design/MANIFEST.md` §2.2 · `docs/core/design/TOOLS.md` §6.13;
40
- * 2026-09-17 用户裁定 + #62 批):判据 = git 目录(`.git`)+ 数据档存在性——
41
- * `_projectRootOverride` 在场 ⇒ `self` + 覆盖值(测试注入面,短路先于真判据);
42
- * 锚(cwd `resolve`)自身含 `.git` ⇒ `self`(锚即仓根);
43
- * 否则**向下看锚的直接子目录一层**(不递归、不向上):含 `.git` **∧** 含 `MANIFEST_REL`(只判存在性、
44
- * 不解析档内容)——恰一 `unique`;零 ⇒ `none`;≥2 ⇒ `ambiguous`(全列候选,不猜)。
45
- * `candidates` 一律**按名排序**(歧义消息确定可判)——`self` / `none` ⇒ `[]`。
46
- * 纯 fs(不读档内容 / 不查模式 / 不依赖 agent)· **不抛**(锚不可读 ⇒ `none`)· 不缓存。
47
- * @param {string} [cwd] 会话锚(缺省 → 进程 cwd)
48
- * @returns {{kind:'self'|'unique'|'none'|'ambiguous', root:string|null, candidates:string[]}}
44
+ * 一层扫描内核(**模块私有**——两条梯表共用;KD-M1-23 / 设计 §2.2「单源结构」):枚举锚的
45
+ * **直接子目录**一层(不递归)+ 按名排序 + 谓词过滤 命中表(绝对路径)。
46
+ * 全档唯一 `readdirSync` 落点(结构机判 T54);锚不可读 ⇒ `[]`(不抛——调用方按「零命中」处置)。
47
+ * @param {string} anchor 锚绝对路径
48
+ * @param {(dir:string)=>boolean} isHit 子目录谓词
49
+ * @returns {string[]} 命中子目录绝对路径(按名排序)
49
50
  */
50
- export function discoverRepos(cwd) {
51
- if (_projectRootOverride) return { kind: "self", root: resolve(_projectRootOverride), candidates: [] }
52
- const anchor = resolve(cwd ?? ".")
53
- if (existsSync(join(anchor, ".git"))) return { kind: "self", root: anchor, candidates: [] }
54
- let found = []
51
+ function scanChildren(anchor, isHit) {
55
52
  try {
56
- found = readdirSync(anchor, { withFileTypes: true })
53
+ return readdirSync(anchor, { withFileTypes: true })
57
54
  .filter((e) => e.isDirectory())
58
55
  .map((e) => e.name)
59
56
  .sort()
60
57
  .map((e) => join(anchor, e))
61
- .filter((d) => existsSync(join(d, ".git")) && existsSync(join(d, MANIFEST_REL)))
62
- } catch { /* 不可读 → 空(none) */ }
63
- if (found.length === 1) return { kind: "unique", root: found[0], candidates: [...found] }
64
- if (found.length === 0) return { kind: "none", root: null, candidates: [] }
65
- return { kind: "ambiguous", root: null, candidates: found }
58
+ .filter(isHit)
59
+ } catch { return [] }
60
+ }
61
+
62
+ /**
63
+ * **项目发现**(项目梯五级——KD-M1-23 / 设计 `docs/core/design/MANIFEST.md` §2.2;**git 非前提**,
64
+ * 2026-09-21 用户裁定):① 锚自身带 `MANIFEST_REL` ⇒ 锚;② 锚含 `.git` ⇒ 锚(缺档 = 建档机会);
65
+ * ③ 直接子目录中**带档**者优先(非空即只看此级:恰一 ⇒ `unique` / ≥2 ⇒ `ambiguous`);
66
+ * ④ 零带档才看含 `.git` 的**裸仓**(同判);⑤ 均无 ⇒ `none`(⇒ 建档流,落点 = 会话锚)。
67
+ * `candidates` 一律**按名排序**(`self` / `none` ⇒ `[]`);`matched` ∈ `manifest` / `git` / `null`
68
+ * 记命中(或歧义)出自哪一级。**纯 fs**(只判存在性——不解析档内容)· **不抛** · **不向上** · **不递归**。
69
+ * **覆盖位**(`_setProjectRootForTest` 在场)⇒ **头部短路**(测试注入面——先于真判据,同 `discoverRepos`)。
70
+ * @param {string} [cwd] 会话锚(缺省 → 进程 cwd)
71
+ * @returns {{kind:'self'|'unique'|'none'|'ambiguous', root:string|null, candidates:string[], matched:'manifest'|'git'|null}}
72
+ */
73
+ export function discoverProjects(cwd) {
74
+ if (_projectRootOverride) return { kind: "self", root: resolve(_projectRootOverride), candidates: [], matched: null }
75
+ const anchor = resolve(cwd ?? ".")
76
+ if (existsSync(join(anchor, MANIFEST_REL))) return { kind: "self", root: anchor, candidates: [], matched: "manifest" }
77
+ if (existsSync(join(anchor, ".git"))) return { kind: "self", root: anchor, candidates: [], matched: "git" }
78
+ const withManifest = scanChildren(anchor, (d) => existsSync(join(d, MANIFEST_REL)))
79
+ if (withManifest.length > 1) return { kind: "ambiguous", root: null, candidates: withManifest, matched: "manifest" }
80
+ if (withManifest.length === 1) return { kind: "unique", root: withManifest[0], candidates: [...withManifest], matched: "manifest" }
81
+ const bareRepos = scanChildren(anchor, (d) => existsSync(join(d, ".git")))
82
+ if (bareRepos.length > 1) return { kind: "ambiguous", root: null, candidates: bareRepos, matched: "git" }
83
+ if (bareRepos.length === 1) return { kind: "unique", root: bareRepos[0], candidates: [...bareRepos], matched: "git" }
84
+ return { kind: "none", root: null, candidates: [], matched: null }
66
85
  }
67
86
 
68
87
  /**
69
- * 项目根解析(2026-09-17 用户裁定,**最终定案:纯向下,绝不向上**)——`discoverRepos(cwd).root`
70
- * **薄包装**(零语义——KD-M1-22:`self` / `unique`路径,`none` / `ambiguous``null`,与批前逐字同)。
71
- * 判据本体 = 上(单源);调用方(`manifestFilePath` / `docRootBase` / `ledger-db.mjs`)零改。
88
+ * **仓发现**(仓梯——KD-M1-23 / `docs/core/design/TOOLS.md` §6.13;**单源** KD-M1-22——`git` 工具
89
+ * 经此符号接线,禁第二份实现):① 锚含 `.git` ⇒ 锚;② 直接子目录中 `.git` **∧** 带档者恰一 命中
90
+ * (≥2 歧义);③ **零个此类时才看**含 `.git` 的裸仓(恰一 ⇒ 命中 / ≥2 ⇒ 歧义);④ 均无 ⇒ `none`。
91
+ * 与 `discoverProjects` = **同一 walk 内核**(`scanChildren`)的两种梯表——差异只在谓词与级序。
92
+ * `candidates` 按名排序;`matched` 记档位;纯 fs / 不抛 / 不递归 / 不向上;覆盖位 ⇒ 头部短路。
93
+ * @param {string} [cwd] 会话锚(缺省 → 进程 cwd)
94
+ * @returns {{kind:'self'|'unique'|'none'|'ambiguous', root:string|null, candidates:string[], matched:'manifest'|'git'|null}}
95
+ */
96
+ export function discoverRepos(cwd) {
97
+ if (_projectRootOverride) return { kind: "self", root: resolve(_projectRootOverride), candidates: [], matched: null }
98
+ const anchor = resolve(cwd ?? ".")
99
+ if (existsSync(join(anchor, ".git"))) return { kind: "self", root: anchor, candidates: [], matched: "git" }
100
+ const scoped = scanChildren(anchor, (d) => existsSync(join(d, ".git")) && existsSync(join(d, MANIFEST_REL)))
101
+ if (scoped.length > 1) return { kind: "ambiguous", root: null, candidates: scoped, matched: "manifest" }
102
+ if (scoped.length === 1) return { kind: "unique", root: scoped[0], candidates: [...scoped], matched: "manifest" }
103
+ const bare = scanChildren(anchor, (d) => existsSync(join(d, ".git")))
104
+ if (bare.length > 1) return { kind: "ambiguous", root: null, candidates: bare, matched: "git" }
105
+ if (bare.length === 1) return { kind: "unique", root: bare[0], candidates: [...bare], matched: "git" }
106
+ return { kind: "none", root: null, candidates: [], matched: null }
107
+ }
108
+
109
+ /**
110
+ * **归属形单点**(KD-M1-30 / 设计 §2.9 A——2026-09-21 用户裁定「最近者优先」):自 `target`
111
+ * (目录含自身;文件路径自其父目录起)沿**祖先链**逐级上溯至盘根,取**最近**带 `MANIFEST_REL`
112
+ * 的目录;无 ⇒ `null`(⇒ 调用方走发现兜底)。**嵌套合法**:子内归子、根其余归根。
113
+ * **纯 fs**(只判存在性——不解析档内容 / 不问模式)· **不跨兄弟** · **无全局优先级**。
114
+ * **覆盖位**(`_setProjectRootForTest` 在场)⇒ **头部短路**:直接返回覆盖值(不查档存在性——
115
+ * 保「覆盖即覆盖值」语义,回归守卫 = `test/manifest.test.mjs` T42 覆盖断言)。
116
+ * @param {string} [target] 目标路径(目录 / 文件)
117
+ * @returns {string|null} 最近带档祖先目录绝对路径 / null
118
+ */
119
+ export function owningProject(target) {
120
+ if (_projectRootOverride) return resolve(_projectRootOverride)
121
+ let dir = resolve(target ?? ".")
122
+ for (;;) {
123
+ if (existsSync(join(dir, MANIFEST_REL))) return dir
124
+ const parent = dirname(dir)
125
+ if (parent === dir) return null // 盘根 → 祖先链无档
126
+ dir = parent
127
+ }
128
+ }
129
+
130
+ /**
131
+ * 项目根解析(**归属 ∨ 发现**——KD-M1-30 / M1-24):`owningProject(cwd) ?? discoverProjects(cwd).root`。
132
+ * 带档路径与批前**逐字同**;变更面两条(设计 §2.2):① 项目树内路径(祖先带档)⇒ **该项目根**
133
+ * (批前回落 `resolve(cwd)`——错层建档面,本批修);② 裸仓恰一 ⇒ 该仓根(零档降级 = 建档机会)。
134
+ * `none` / `ambiguous` ⇒ `null`。调用方(`manifestFilePath` / `docRootBase` / `ledger-db.mjs` /
135
+ * `ledger-cmd.mjs`)**零改**(行为随语义变更——设计 §2.5 键面条 / 错层条)。
72
136
  * @returns {string|null} 项目根绝对路径 / null
73
137
  */
74
138
  export function resolveProjectRoot(cwd) {
75
- return discoverRepos(cwd).root
139
+ return owningProject(cwd) ?? discoverProjects(cwd).root
140
+ }
141
+
142
+ /**
143
+ * **按用点解析**的读侧单点(KD-M1-24 / M1-30——**非抛错 / 零写 / 无缓存**):两段合成——
144
+ * · **归属(第一段——§⑥ 归属形)**:`owningProject(target)` 沿祖先链取最近带档目录
145
+ * (子优于根 / 不跨兄弟 / 无全局优先级)⇒ 读该档 ⇒ `ok` / `invalid`。
146
+ * · **发现兜底(第二段——§⑥ 发现规则,纯向下)**:祖先链无档 ⇒ `discoverProjects(target)` ⇒ 命中
147
+ * (`self` / `unique`)⇒ 读其档(带档 ⇒ `ok` / `invalid`;缺档 ⇒ `missing`);`ambiguous` ⇒
148
+ * `ambiguous`(+ `candidates`);`none` ⇒ `no-project`。
149
+ * 写侧(建档)**不在**此函数(归入口决策树——KD-M1-25 / M1-29)⇒ 注入器 / 只读消费面不会变写点。
150
+ * `matched` 契约:归属段命中 ⇒ `'manifest'`;发现段 ⇒ `discoverProjects.matched` 逐字;
151
+ * `no-project`(/ 覆盖位短路)⇒ `null`——报明行歧义变体按它分野(§2.6 条 1b)。
152
+ * 读错(非 ENOENT——权限 / 目录等)收为 `invalid`(非抛错契约;`readManifest` 语义零改)。
153
+ * @param {string} [target] 目标路径(目录 / 文件——动作作用于哪个项目的路径)
154
+ * @returns {{state:'ok'|'missing'|'no-project'|'ambiguous'|'invalid', root:string|null, path:string|null,
155
+ * manifest?:object, candidates?:string[], errors?:string[], matched:'manifest'|'git'|null}}
156
+ */
157
+ export function projectView(target) {
158
+ const anchor = resolve(target ?? ".")
159
+ const owning = owningProject(anchor)
160
+ if (owning) return viewAtRoot(owning, "manifest")
161
+ const d = discoverProjects(anchor)
162
+ if (d.kind === "ambiguous") return { state: "ambiguous", root: null, path: null, candidates: d.candidates, matched: d.matched }
163
+ if (d.kind === "none") return { state: "no-project", root: null, path: null, matched: null }
164
+ return viewAtRoot(d.root, d.matched)
165
+ }
166
+
167
+ /** `projectView` 公共尾段:给定项目根 ⇒ 读 + 校验 ⇒ `ok` / `missing` / `invalid`(非抛错)。 */
168
+ function viewAtRoot(root, matched) {
169
+ const path = join(root, MANIFEST_REL)
170
+ let m
171
+ try {
172
+ m = readManifest(root)
173
+ } catch (e) {
174
+ return { state: "invalid", root, path, errors: [e?.message ?? String(e)], matched } // 读错(权限 / 目录等)⇒ 非抛错收口
175
+ }
176
+ if (m.ok) return { state: "ok", root, path, manifest: m.manifest, matched }
177
+ if (m.reason === "missing") return { state: "missing", root, path, matched }
178
+ return { state: "invalid", root, path, errors: m.errors, matched }
76
179
  }
77
180
 
78
181
  /**
@@ -114,8 +217,8 @@ export function docRootPaths(value, cwd) {
114
217
  return [...new Set(list)]
115
218
  }
116
219
 
117
- /** 落盘根:git 根优先(仓内任意子目录调用都落仓根——防错层);无 .git(临时项目 / 测试)→ 退回 cwd
118
- * (旧行为,不更坏)。防「写进 monorepo 容器根」的主闸 = 提示词(主 agent 会话锚必须进仓内)。 */
220
+ /** 落盘根:**项目根**优先(项目树内任意子目录调用都落项目根——防错层,KD-M1-30 归属形);
221
+ * 无项目(梯⑤ 存档 / 临时目录 / 测试注入)→ 退回 cwd(既有行为,不更坏)。 */
119
222
  function writeRoot(cwd) {
120
223
  return resolveProjectRoot(cwd) ?? resolve(cwd ?? ".")
121
224
  }
@@ -255,7 +358,8 @@ export function readManifest(cwd) {
255
358
 
256
359
  /**
257
360
  * 装配钩子入口(模块设计 §2.2)——同 readManifest(cwd)。ok:true 返回补默认值后的 manifest;
258
- * reason:'missing' 由调用方走初始化分支(壳面拒进正常循环直至初始化完成)。
361
+ * reason:'missing' 由调用方走**建档流**(**工程模式会话**口径——梯②④⑤ 就地建档,**不拒会话**;
362
+ * KD-M1-29);普通会话 = 装配钩子零 manifest I/O(KD-M1-12)。
259
363
  */
260
364
  export function requireManifest(cwd) {
261
365
  return readManifest(cwd)
@@ -264,44 +368,44 @@ export function requireManifest(cwd) {
264
368
  /**
265
369
  * 入口决策树(**非抛错**形态——KD-M1-20;docs/core/design/MANIFEST.md §2.8 F1):三面共用——
266
370
  * ① CLI 装配 / 重估薄包装 ② VSC `hydrateRun` 钩子块 ③ 翻转面(`eng` 工具 / `/eng` /
267
- * `/session` / ACP——「先判后翻」,拒翻分支零副作用)。判据树只此一处(判据单源——KD-M1-8 同族)。
371
+ * `/session` / ACP——「先判后翻」,拒翻分支零副作用)。判据树只此一处(判据单源——KD-M1-8
372
+ * 同族);读侧解析与状态归位 = `projectView`(KD-M1-24——非抛错 / 零写)。
268
373
  *
269
- * 分支(四出口):
270
- * 档合法 → { ok:true, manifest, created:false }(manifest = 补默认值后的档内容)
271
- * 缺档 + init 根可解析 ? `initManifest(cwd, { writer })`(抛错 → `init-failed`)
272
- * : { ok:false, code:'root-unresolvable', message }
273
- * 缺档 + !init → { ok:false, code:'missing' }(不拒——调用方自决;VSC depth>0 分支用)
274
- * 档非法 → { ok:false, code:'invalid', message, errors }
374
+ * 分支(两分支六出口——§2.8 F1 树):
375
+ * 档合法 → { ok:true, manifest, created:false }(manifest = 补默认值后的档内容)
376
+ * 缺档 + init 梯②④⑤(锚 = 裸仓 / 裸仓命中 / 无项目)⇒ `initManifest(cwd, { writer })`
377
+ * (抛错 → `init-failed`);歧义(≥2 候选)⇒ `{ ok:false, code:'ambiguous',
378
+ * message, candidates }`(**不建 / 不猜**)
379
+ * 缺档 + !init 梯⑤(无项目)⇒ `{ ok:false, code:'no-project' }`(KD-M1-28;`init:false` 面);
380
+ * 梯②④(裸仓可解析、档缺)⇒ `{ ok:false, code:'missing' }`;歧义 ⇒ 同上
381
+ * 档非法 → { ok:false, code:'invalid', message, errors }
275
382
  *
276
383
  * `writer` 由调用点**显式**传(生产调用点全传 `'main'`):缺省 `'subagent'` 是写门 fail-closed
277
- * 缺省(KD-M1-3),误用缺省 ⇒「缺档 + 根可解析」退化为 `init-failed`(拒翻——与 §2.8 F2 /
384
+ * 缺省(KD-M1-3),误用缺省 ⇒「缺档 + 梯②④⑤」退化为 `init-failed`(拒翻——与 §2.8 F2 /
278
385
  * AC-20② 语义相反;T38 反证格)。
279
- * 边界:`readManifest` 的非 ENOENT 读错(权限 / 目录等)按既有契约**上抛**(不伪装成缺失——
280
- * readManifest 语义零改);本树只承诺上列四出口。
386
+ * 失败码两态文案不同(KD-M1-28):无项目 可在锚处落地 / 歧义 ⇒ 列候选不猜;文案族 =
387
+ * 「项目不可解析」(稳定锚句 `/项目不可解析/`——拒翻面与报明面共用)。
388
+ * 边界:`readManifest` 的非 ENOENT 读错(权限 / 目录等)由 `projectView` 收为 `invalid`(非抛错
389
+ * 契约——`readManifest` 返回语义零改);本树只承诺上列六出口。
281
390
  * @param {string} [cwd] 会话锚(缺省 → 进程 cwd)
282
391
  * @returns {{ok:true, manifest:object, created:boolean}
283
- * | {ok:false, code:'missing'|'invalid'|'root-unresolvable'|'init-failed', message?:string, errors?:string[]}}
392
+ * | {ok:false, code:'missing'|'invalid'|'no-project'|'ambiguous'|'init-failed',
393
+ * message?:string, errors?:string[], candidates?:string[]}}
284
394
  */
285
395
  export function resolveEngineeringManifest(cwd, { writer = "subagent", init = true } = {}) {
286
- const m = readManifest(cwd)
287
- if (m.ok) return { ok: true, manifest: m.manifest, created: false }
288
- if (m.reason === "invalid") {
396
+ const view = projectView(cwd)
397
+ if (view.state === "ok") return { ok: true, manifest: view.manifest, created: false }
398
+ if (view.state === "invalid") {
289
399
  return {
290
- ok: false, code: "invalid", errors: m.errors,
291
- message: `${MANIFEST_REL} 非法(fail-closed 拒进正常循环):${m.errors.join(";")}`,
400
+ ok: false, code: "invalid", errors: view.errors,
401
+ message: `项目不可解析:${MANIFEST_REL} 非法(fail-closed):${(view.errors ?? []).join(";")}`,
292
402
  }
293
403
  }
294
- if (!init) return { ok: false, code: "missing" }
295
- // 项目根 = git 仓根(2026-09-17 用户裁定:判据 = .git,向下找;每个仓一份 manifest):
296
- // cwd 非仓且子仓中无带 manifest 的仓(零个 = 无项目,多个 = 歧义)→ 不自动建档。
297
- if (!resolveProjectRoot(cwd)) {
298
- return {
299
- ok: false, code: "root-unresolvable",
300
- message:
301
- `工程模式启动拒绝:会话锚 ${cwd} 不是 git 仓,且其子仓中带 ${MANIFEST_REL} 的不是恰好一个` +
302
- `(零个 = 无项目;多个 = 歧义)——每个仓库一份 manifest,请锚在仓内或为子仓建档`,
303
- }
404
+ if (view.state === "ambiguous") {
405
+ return { ok: false, code: "ambiguous", candidates: view.candidates, message: ambiguousProjectMessage(cwd, view.candidates) }
304
406
  }
407
+ // 缺档(梯②④⑤):init ⇒ 就地建档(内容 = DEFAULT_MANIFEST,经写门 writer:'main');!init ⇒ 归码。
408
+ if (!init) return { ok: false, code: view.state === "no-project" ? "no-project" : "missing" }
305
409
  try {
306
410
  return { ok: true, manifest: initManifest(cwd, { writer }), created: true }
307
411
  } catch (e) {
@@ -309,6 +413,14 @@ export function resolveEngineeringManifest(cwd, { writer = "subagent", init = tr
309
413
  }
310
414
  }
311
415
 
416
+ /** 歧义消息(文案族「项目不可解析」——KD-M1-28):候选**全列**(绝对路径、按名排序)+ 指引
417
+ * 显式指定目标——**不猜**(与报明行同族;拒翻面经 F3 明示面逐字转发)。 */
418
+ function ambiguousProjectMessage(cwd, candidates) {
419
+ const list = (candidates ?? []).map((c) => `- ${c}`).join("\n")
420
+ return `项目不可解析:会话锚 ${cwd} 下候选项目不是恰好一个(下列 ${(candidates ?? []).length} 个)——` +
421
+ `每个项目一份 ${MANIFEST_REL};请显式指定目标项目(机制不猜):\n${list}`
422
+ }
423
+
312
424
  /**
313
425
  * 写门落盘(模块设计 §2.2 / KD-M1-3):仅 writer === 'main' 放行(fail-closed 缺省拒);
314
426
  * 落盘前先 validateManifest(manifest)(ok:false → 拒落盘,防写非法档)。
package/memory/core.mjs CHANGED
@@ -12,11 +12,14 @@ import { normalizeOrigin } from "./origin.mjs"
12
12
  import { readFile, stat, readdir, writeFile, mkdir } from "node:fs/promises"
13
13
  import { join } from "node:path"
14
14
  import { segmentCJK, VALID_TYPES, SCHEMA_VERSION } from "./schema.mjs"
15
+ // FTS 语言面单源外提(会话索引批 · SESSION.md §6.19):本档只取用 + re-export 保名面
16
+ // (零行为变更——实现住叶子档 `fts-text.mjs`)。
17
+ import { buildFtsQuery } from "../fts-text.mjs"
18
+ export { buildFtsQuery } from "../fts-text.mjs"
15
19
  import { safeSliceUTF16 } from "../text-budget.mjs"
16
20
 
17
21
  const EMBED_BATCH_SIZE = 256
18
22
  export const EMBED_TEXT_MAX_LEN = 2000
19
- const FTS_TOKEN_MAX = 16
20
23
  const DEFAULT_LIST_LIMIT = 50
21
24
 
22
25
  /**
@@ -299,20 +302,3 @@ export function clearPersonal(memory) {
299
302
  const { changes } = memory.db.prepare(`DELETE FROM entries`).run()
300
303
  return changes
301
304
  }
302
-
303
- /**
304
- * Build an FTS5 query: first split by whitespace/punctuation into tokens,
305
- * then apply CJK character segmentation to each token.
306
- * This keeps multi-character CJK words as FTS5 phrases ("分号" → "分 号" → phrase query, exact adjacency match),
307
- * while different tokens are joined with OR ("命名 规范" → "命 名" OR "规 范", each phrase requires its own adjacency).
308
- */
309
- export function buildFtsQuery(query) {
310
- const terms = query
311
- .split(/[\s,,。、;;!!??()()"`]+/)
312
- .map((t) => t.trim())
313
- .filter(Boolean)
314
- .slice(0, FTS_TOKEN_MAX)
315
- .map((t) => segmentCJK(t))
316
- if (terms.length === 0) return ""
317
- return terms.map((t) => `"${t.replaceAll('"', '""')}"`).join(" OR ")
318
- }
package/memory/schema.mjs CHANGED
@@ -9,6 +9,10 @@
9
9
  import { DatabaseSync } from "node:sqlite"
10
10
  import { mkdirSync } from "node:fs"
11
11
  import { dirname } from "node:path"
12
+ // FTS 语言面单源外提(会话索引批 · SESSION.md §6.19):本档只取用 + re-export 保名面
13
+ // (零行为变更——实现住叶子档 `fts-text.mjs`,会话索引面同源)。
14
+ import { segmentCJK } from "../fts-text.mjs"
15
+ export { segmentCJK } from "../fts-text.mjs"
12
16
 
13
17
  export const VALID_TYPES = new Set(["rule", "knowledge", "decision", "pattern"])
14
18
  export const SCHEMA_VERSION = 9
@@ -50,17 +54,6 @@ export const MAX_CODE_FILE_BYTES = 1024 * 1024 // 1 MB
50
54
  export const MAX_DOC_FILE_BYTES = 512 * 1024 // 512 KB
51
55
  export const BIG_FILE_LINES = 2000
52
56
 
53
- /**
54
- * CJK character-by-character spacing: makes unicode61 treat each Han/Kana/Hangul character as an independent token.
55
- * Both write and query must use the same processing for retrieval to match.
56
- */
57
- export function segmentCJK(text) {
58
- return text.replace(
59
- /[぀-ヿ㐀-䶿一-鿿豈-﫿가-힯]+/g,
60
- (run) => [...run].join(" "),
61
- )
62
- }
63
-
64
57
  /**
65
58
  * Open/initialize the memory store. dbPath is auto-created if missing.
66
59
  * The returned memory object is the interface; all subsequent functions take it as their first argument.
package/package.json CHANGED
@@ -1,21 +1,42 @@
1
1
  {
2
2
  "name": "@thincoder/core",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "ThinCoder shared core — shared mechanism modules + shared prompt content (CLI ↔ VS Code extension).",
5
+ "keywords": [
6
+ "ai",
7
+ "agent",
8
+ "core",
9
+ "shared",
10
+ "llm",
11
+ "mcp"
12
+ ],
5
13
  "type": "module",
6
14
  "scripts": {
7
15
  "test": "node test/run.mjs",
8
16
  "prepublishOnly": "npm test"
9
17
  },
10
18
  "license": "MIT",
19
+ "author": "liwei <liwei@51marine.com> (上海新舶)",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/xinbo-tech/thincoder.git"
23
+ },
24
+ "homepage": "https://github.com/xinbo-tech/thincoder",
25
+ "bugs": {
26
+ "url": "https://github.com/xinbo-tech/thincoder/issues"
27
+ },
11
28
  "engines": {
12
29
  "node": ">=22.13.0"
13
30
  },
14
31
  "exports": {
15
32
  "./*": "./*"
16
33
  },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
17
37
  "files": [
18
38
  "*.mjs",
39
+ "CHANGELOG.md",
19
40
  "advisor/",
20
41
  "agent/",
21
42
  "agent-tools/",
@@ -26,7 +26,7 @@ You are an independent design reviewer for an engineering-mode project. ## Your
26
26
  - Do NOT run git diff or look for code changes — there are none at this stage.
27
27
 
28
28
  ## 批次档 §3 落档(仅设计评审——工具已挂载时)
29
- 设计评审专用(**仅当本评审为设计评审、且工具面里已挂载 `batch_segment` 时**——代码评审无此工具,本节不适用):在报告之外,用 `batch_segment({segment:"§3", text})` 把本轮**发现表 + VERDICT + 计数逐字**写进批次档 §3(不给路径参数;工具自带 `### 轮次 N(评审子代理)` 来源戳,勿自写标题)。
29
+ 设计评审专用(**仅当本评审为设计评审、且工具面里已挂载 `batch`(过渡别名 `batch_segment`)时**——代码评审无此工具,本节不适用):在报告之外,用 `batch({action:"append", segment:"§3", text})` 把本轮**发现表 + VERDICT + 计数逐字**写进批次档 §3(不给路径参数;工具自带 `### 轮次 N(评审子代理)` 来源戳,勿自写标题)。
30
30
  写不进去(被拒/失败)→ 报告里明说「§3 未写入」——不得静默略过,也不得假装写过(父侧代写必须打标)。
31
31
 
32
32
  ## 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 this convention — do not downgrade)
@@ -27,7 +27,7 @@ You have a budget of 15 tool rounds (chat turns). Hard mechanical cap: 100 round
27
27
  - Stop calling tools once you are ready to produce the review table.
28
28
 
29
29
  ## 批次档 §3 落档(仅设计评审——工具已挂载时)
30
- 设计评审专用(**仅当本评审为设计评审、且工具面里已挂载 `batch_segment` 时**——代码评审无此工具,本节不适用):在报告之外,用 `batch_segment({segment:"§3", text})` 把本轮**发现表 + VERDICT + 计数逐字**写进批次档 §3(不给路径参数;工具自带 `### 轮次 N(评审子代理)` 来源戳,勿自写标题)。
30
+ 设计评审专用(**仅当本评审为设计评审、且工具面里已挂载 `batch`(过渡别名 `batch_segment`)时**——代码评审无此工具,本节不适用):在报告之外,用 `batch({action:"append", segment:"§3", text})` 把本轮**发现表 + VERDICT + 计数逐字**写进批次档 §3(不给路径参数;工具自带 `### 轮次 N(评审子代理)` 来源戳,勿自写标题)。
31
31
  写不进去(被拒/失败)→ 报告里明说「§3 未写入」——不得静默略过,也不得假装写过(父侧代写必须打标)。
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 this convention — do not downgrade)
@@ -23,7 +23,7 @@ You have a budget of 15 tool rounds (chat turns). Hard mechanical cap: 100 round
23
23
  - Stop calling tools once you are ready to produce the review table.
24
24
 
25
25
  ## 批次档 §3 落档(仅设计评审——工具已挂载时)
26
- 设计评审专用(**仅当本评审为设计评审、且工具面里已挂载 `batch_segment` 时**——代码评审无此工具,本节不适用):在报告之外,用 `batch_segment({segment:"§3", text})` 把本轮**发现表 + VERDICT + 计数逐字**写进批次档 §3(不给路径参数;工具自带 `### 轮次 N(评审子代理)` 来源戳,勿自写标题)。
26
+ 设计评审专用(**仅当本评审为设计评审、且工具面里已挂载 `batch`(过渡别名 `batch_segment`)时**——代码评审无此工具,本节不适用):在报告之外,用 `batch({action:"append", segment:"§3", text})` 把本轮**发现表 + VERDICT + 计数逐字**写进批次档 §3(不给路径参数;工具自带 `### 轮次 N(评审子代理)` 来源戳,勿自写标题)。
27
27
  写不进去(被拒/失败)→ 报告里明说「§3 未写入」——不得静默略过,也不得假装写过(父侧代写必须打标)。
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 this convention — do not downgrade)
package/prompts/common.md CHANGED
@@ -110,8 +110,8 @@ Batch independent read-only tool calls into a single reply (they run concurrentl
110
110
 
111
111
  **Destructive-command red lines**:
112
112
  - **Never hand-roll delete verbs**: `rm` / `rmdir` / `del` / `rd` / `Remove-Item` and the like are never written into a command — deletions go through the existing tool face (`delete` / `git rm`, or a very narrow allowlist).
113
- - **Diagnostics are read-only**: existence / state checks use read-only commands only (`dir` / `ls` / `where` / `type`) — never smuggle a write or delete verb in, and never tag a real action "no-op / read-only".
114
- - **No silent masking**: no `2>nul` error-swallowing on destructive / write commands, no `&` (as opposed to `&&`) chaining — a failure must be visible.
113
+ - **Diagnostics are read-only**: existence / state checks use read-only commands only (`dir` / `ls` / `where` / `type`) — never smuggle a write or delete verb in, and never tag a real action "no-op / read-only". When a dedicated tool exists for the job, the tool-routing table still wins — this bullet covers the bare-command case only.
114
+ - **No silent masking**: no `2>nul` / `2>/dev/null` error-swallowing on destructive / write commands, no `&` / `;` (as opposed to `&&`) chaining — a failure must be visible.
115
115
  - **Confirm before irreversible actions**: stop before an irreversible action — the main session asks the user; a subagent raises an upstream `ask` (`notify_parent`).
116
116
  - **Boundary**: nothing at the tool layer catches this for you (no mechanical gate, no tool-semantics change) — you write the command, so you are the first line of defense.
117
117
 
@@ -153,6 +153,6 @@ Batch independent read-only tool calls into a single reply (they run concurrentl
153
153
  ## 批次档常识(Batch record — the carrier of engineering-mode task flow)
154
154
  **What it is**: the **batch record** = the **carrier** of an engineering-mode task (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.
155
155
  **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).
156
- **Writing means**: `batch_segment` — **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.
156
+ **Writing means**: the `batch` tool (transition alias `batch_segment` — same append executor) — **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.
157
157
  **The task book itself**: `batchDoc` = the batch-record path = **your task book** (mandatory on every eng-designer / eng-coder spawn; **unreadable ⇒ refused** — do not execute, bounce it back).
158
158
  **Structure authority**: segment structure / gates / lifecycle are defined by the project's own batch-record mechanism (this section gives the map only — no mechanism restatement).