@thincoder/core 0.10.7 → 0.10.8

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 (125) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/advisor/compaction.mjs +1 -1
  3. package/advisor/run.mjs +1 -1
  4. package/agent/chat-call.mjs +40 -27
  5. package/agent/child-marks.mjs +1 -1
  6. package/agent/dispatch-gates.mjs +7 -4
  7. package/agent/dispatch-run.mjs +1 -0
  8. package/agent/family-tools.mjs +11 -6
  9. package/agent/helpers.mjs +6 -4
  10. package/agent/image-heal.mjs +65 -0
  11. package/agent/light-round.mjs +4 -4
  12. package/agent/live-beat.mjs +4 -4
  13. package/agent/record-results.mjs +9 -1
  14. package/agent/run-stages.mjs +2 -1
  15. package/agent/setup-reminders.mjs +14 -13
  16. package/agent/setup.mjs +62 -46
  17. package/agent/spawn-child.mjs +1 -1
  18. package/agent/subagent-event.mjs +1 -1
  19. package/agent/suspension.mjs +2 -1
  20. package/agent/timers.mjs +8 -8
  21. package/agent/turn-loop.mjs +1 -1
  22. package/agent-tools/advisor.mjs +1 -1
  23. package/agent-tools/async-discard.mjs +40 -5
  24. package/agent-tools/async-settle.mjs +3 -1
  25. package/agent-tools/audit-block.mjs +1 -1
  26. package/agent-tools/batch-paths.mjs +20 -20
  27. package/agent-tools/checkpoint.mjs +1 -1
  28. package/agent-tools/design-slots.mjs +44 -5
  29. package/agent-tools/doc-check.mjs +37 -0
  30. package/agent-tools/eng.mjs +5 -3
  31. package/agent-tools/manifest.mjs +18 -18
  32. package/agent-tools/parent-channel.mjs +3 -3
  33. package/agent-tools/review-facts.mjs +2 -2
  34. package/agent-tools/settings.mjs +1 -1
  35. package/agent-tools/spawn-gates.mjs +8 -6
  36. package/agent-tools/ssh-session.mjs +439 -0
  37. package/agent-tools/ssh-transport.mjs +311 -0
  38. package/agent-tools/subagent-scheduler.mjs +6 -5
  39. package/agent-tools/subagent.mjs +7 -5
  40. package/agent-tools/task.mjs +1 -1
  41. package/agent-tools/timer.mjs +6 -0
  42. package/agent-tools/zero-write-watch.mjs +2 -2
  43. package/agent-tools.mjs +3 -2
  44. package/attachments.mjs +4 -4
  45. package/browser/cdp.mjs +2 -2
  46. package/browser/launch.mjs +50 -4
  47. package/browser/snapshot.mjs +1 -1
  48. package/config-io.mjs +7 -2
  49. package/config-watch.mjs +2 -2
  50. package/config.mjs +7 -0
  51. package/declaration.mjs +5 -1
  52. package/doc-check/anchors.mjs +100 -0
  53. package/doc-check/index.mjs +61 -0
  54. package/doc-check/width.mjs +131 -0
  55. package/file-refs.mjs +6 -6
  56. package/generate-title.mjs +1 -1
  57. package/hooks.mjs +1 -1
  58. package/i18n.mjs +11 -1
  59. package/ledger-cmd.mjs +4 -5
  60. package/ledger-db.mjs +100 -21
  61. package/ledger-migrate.mjs +3 -3
  62. package/ledger-read.mjs +6 -7
  63. package/ledger-tools.mjs +24 -9
  64. package/ledger.mjs +23 -54
  65. package/log.mjs +17 -3
  66. package/manifest-discovery.mjs +21 -18
  67. package/manifest-schema.mjs +2 -13
  68. package/manifest.mjs +37 -22
  69. package/mcp/transport-stdio.mjs +1 -1
  70. package/mcp.mjs +2 -2
  71. package/memory/core.mjs +1 -1
  72. package/memory/scan.mjs +1 -1
  73. package/memory/sweep.mjs +1 -1
  74. package/model-specs.mjs +7 -7
  75. package/notify-policy.mjs +6 -6
  76. package/package.json +2 -1
  77. package/permission.mjs +14 -0
  78. package/prompts/advisor-design.md +43 -13
  79. package/prompts/advisor-round1.md +25 -5
  80. package/prompts/advisor-round2.md +17 -4
  81. package/prompts/advisor-round3.md +17 -4
  82. package/prompts/common.md +60 -6
  83. package/prompts/consult-base.md +8 -2
  84. package/prompts/discipline-engineering.md +11 -10
  85. package/prompts/discipline-normal.md +22 -14
  86. package/prompts/persona-coder.md +1 -1
  87. package/prompts/persona-eng-designer.md +8 -3
  88. package/prompts/persona-engineering.md +21 -5
  89. package/prompts/persona-normal.md +4 -1
  90. package/provider/errors.mjs +8 -0
  91. package/provider/google.mjs +3 -0
  92. package/provider/normalize.mjs +42 -12
  93. package/provider/responses-request.mjs +1 -1
  94. package/provider/responses.mjs +2 -2
  95. package/provider/wait-status.mjs +1 -1
  96. package/provider-flows.mjs +1 -1
  97. package/queued.mjs +2 -2
  98. package/rules.mjs +4 -4
  99. package/session-gc.mjs +1 -1
  100. package/session-lifecycle.mjs +1 -1
  101. package/session-slot-write.mjs +2 -2
  102. package/session-slots-manifest.mjs +1 -1
  103. package/session.mjs +1 -1
  104. package/shell-candidates.mjs +2 -2
  105. package/shell-identity.mjs +1 -1
  106. package/team.mjs +61 -4
  107. package/think-off.mjs +3 -3
  108. package/token-ttl.mjs +1 -1
  109. package/tool-docs/bash.md +1 -1
  110. package/tool-docs/browser.md +1 -1
  111. package/tool-docs/doc_check.md +31 -0
  112. package/tool-docs/manifest.md +14 -2
  113. package/tool-docs/ssh.md +34 -0
  114. package/tool-docs/subagent.md +2 -0
  115. package/tool-docs/timer.md +2 -1
  116. package/tools/browser.mjs +1 -1
  117. package/tools/execute.mjs +1 -1
  118. package/tools/git-run.mjs +2 -2
  119. package/tools/git.mjs +8 -8
  120. package/tools/index.mjs +3 -0
  121. package/tools/ops.mjs +1 -1
  122. package/tools/process-tree.mjs +2 -2
  123. package/tools/ssh-askpass.mjs +35 -0
  124. package/tools/ssh.mjs +92 -0
  125. package/vision-reader.mjs +4 -4
@@ -12,7 +12,7 @@ import { basename, dirname, join, resolve } from "node:path"
12
12
  import { DatabaseSync } from "node:sqlite"
13
13
 
14
14
  import { resolveDeclaredRef } from "./declaration.mjs"
15
- import { ledgerDirPath, ledgerKey, openLedger } from "./ledger-db.mjs"
15
+ import { ledgerDirPath, ledgerKey, openLedger, resolveLedgerRoot } from "./ledger-db.mjs"
16
16
  import { resolveProjectRoot } from "./manifest.mjs"
17
17
 
18
18
  /** 12 数据列(**等值比对列集**——不含 id:id 重发,§2.2 语义保全 / AC-M2-12)。 */
@@ -92,9 +92,9 @@ function fromKeyRefusal(fromKeys, dir) {
92
92
  return null
93
93
  }
94
94
 
95
- /** 迁移计划(dry-run / confirm 共用)。cwd 口径 = 写门口径 2 同一子表达式(`resolveProjectRoot` ∨ 兜底)。 */
95
+ /** 迁移计划(dry-run / confirm 共用)。cwd 口径 = 写门口径 2 同一子表达式(根推导单源 `resolveLedgerRoot(cwd)?.root` ∨ 兜底)——与实开库(`openLedger`)同源。 */
96
96
  export function planLedgerMigration(cwd, { fromKeys = [] } = {}) {
97
- const projectRoot = resolveProjectRoot(cwd) ?? resolve(cwd ?? ".")
97
+ const projectRoot = resolveLedgerRoot(cwd)?.root ?? resolve(cwd ?? ".")
98
98
  const dir = ledgerDirPath()
99
99
  const targetKey = ledgerKey(projectRoot)
100
100
  const targetPath = join(dir, `${targetKey}.db`)
package/ledger-read.mjs CHANGED
@@ -10,19 +10,18 @@
10
10
  * (列集单源 = `DATA_COLUMNS`——D-10 直引,不另立副本;`evidence` 仅 full 入行);缺列(旧库)按
11
11
  * null 归一;`schemaVersion` = 库 `PRAGMA user_version` 如实回读(旧库未标 = 0);库不在 / 非台账库
12
12
  * ⇒ 项目不出项(空集)。
13
- * 范围(§2.4):单范围 = 解析后项目根一项(`resolveProjectRoot(cwd) ?? resolve(cwd ?? ".")`——与库键
14
- * 同源);`--family` = `discoverFamily` 发现序逐项(不可读跳过——既有语义)。
13
+ * 范围(§2.4):单范围 = 推导所得项目根一项(`resolveLedgerRoot(cwd)`——与库键同源;无根 ⇒ 空集);
14
+ * `--family` = `discoverFamily` 发现序逐项(不可读跳过——既有语义)。
15
15
  *
16
16
  * W8 契约②:本档静态入 `ledger-db.mjs`(node:sqlite)⇒ 消费侧一律**动态 import** 本档。
17
17
  * D-11:`ledger.mjs` 零编辑——族发现只 import `discoverFamily`。
18
18
  */
19
- import { basename, resolve } from "node:path"
19
+ import { basename } from "node:path"
20
20
 
21
21
  import { ledgerQuery } from "./ledger-cmd.mjs"
22
22
  import { discoverFamily } from "./ledger.mjs"
23
23
  import { DATA_COLUMNS } from "./ledger-migrate.mjs"
24
- import { resolveProjectRoot } from "./manifest.mjs"
25
- import { openLedger } from "./ledger-db.mjs"
24
+ import { openLedger, resolveLedgerRoot } from "./ledger-db.mjs"
26
25
 
27
26
  /** 行出形(§3.2):`id` + 数据列序(`evidence` 仅 full);缺列按 null 归一。 */
28
27
  function exportRow(r, full) {
@@ -50,8 +49,8 @@ function readProject(root, full) {
50
49
  export function ledgerExport({ cwd, family = false, full = false } = {}) {
51
50
  const anchor = cwd ?? process.cwd()
52
51
  if (!family) {
53
- const root = resolveProjectRoot(anchor) ?? resolve(anchor)
54
- const item = readProject(root, full)
52
+ const derived = resolveLedgerRoot(anchor)
53
+ const item = derived ? readProject(derived.root, full) : null
55
54
  return { projects: item ? [item] : [] }
56
55
  }
57
56
  const projects = []
package/ledger-tools.mjs CHANGED
@@ -11,11 +11,20 @@
11
11
  * 消费侧一律动态 import(`ledger-db.mjs` 静态 import node:sqlite ⇒ W8 契约②)。
12
12
  */
13
13
  import { ledgerAdd, ledgerClose, ledgerCount, ledgerQuery, ledgerUpdate } from "./ledger-cmd.mjs"
14
+ import { resolveLedgerRoot } from "./ledger-db.mjs"
14
15
  import { DESC } from "./tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/ledger.md(DESC 单解析面)
15
16
 
16
- /** 工具 cwd 供值面(缺省 → 当前项目根)。 */
17
+ /** 工具 cwd 供值面(缺省 → 当前会话锚——「动作目标路径」;项目根由核 `resolveLedgerRoot` 推导)。 */
17
18
  const cwdOf = (ctx) => ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
18
19
 
20
+ /** 根推导**报明面**(设计档 §3.8「报明形态」):向下发现(`basis === 'descendant'`——容器锚收敛序)⇒
21
+ * 对象形回执增 `root` 键;祖先支(自明路径)⇒ `null`(展开零键——回执逐字零改)。供值面 = 核同一
22
+ * 单源 `resolveLedgerRoot`(工具层零自算根;单实现、多调用点)。 */
23
+ const reportedRoot = (cwd) => {
24
+ const derived = resolveLedgerRoot(cwd)
25
+ return derived && derived.basis === "descendant" ? { root: derived.root } : null
26
+ }
27
+
19
28
  // ── 工具层参数守卫(设计档 §3.2 · 2026-09-27 快车道修复 · Gitee #IKIQGK / 台账 #472) ──
20
29
 
21
30
  /** 值预览(P1–P5 文案的 `<预览>`——设计档 §3.2):`JSON.stringify`,超 80 字符截断加 `…`
@@ -80,7 +89,7 @@ const FULL_ACTIONS = ["add", "update", "close", "query", "count"]
80
89
  const READ_ACTIONS = ["query", "count"]
81
90
 
82
91
  /** cwd 参数描述(五前身声明同文——文本单点)。 */
83
- const CWD_DESC = "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根"
92
+ const CWD_DESC = "本次动作的目标路径——项目根由它推导:沿祖先链最近的项目目录(带 PROJECT-MANIFEST.json 或已有台账库);容器目录下收敛到子项目。台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话锚"
84
93
 
85
94
  /** 各 action 声明条目(= 前身旧工具 `parameters` 逐字;伪工具对象守卫 + 弃用壳共用单源)。 */
86
95
  const ACTION_SPECS = {
@@ -207,25 +216,31 @@ function makeLedgerTool(actions, { readonly = false, writeFace = false } = {}) {
207
216
  switch (action) {
208
217
  case "add": {
209
218
  const { cwd, ...row } = rest
210
- const id = ledgerAdd({ cwd: cwd ?? cwdOf(ctx), row })
211
- return JSON.stringify({ id, status: "待讨论" })
219
+ const target = cwd ?? cwdOf(ctx)
220
+ const id = ledgerAdd({ cwd: target, row })
221
+ return JSON.stringify({ id, status: "待讨论", ...reportedRoot(target) })
212
222
  }
213
223
  case "update": {
214
224
  const { cwd, id, ...patch } = rest
225
+ const target = cwd ?? cwdOf(ctx)
215
226
  let executorSessionId
216
227
  if (patch.executor == null) {
217
228
  // 显式 `null` ≡ 略去(§3.1 `null` 口径 · #474——与缺省同分支);动态 import——零新静态边(K-LX1)
218
229
  const { getSessionId } = await import("./session-slots.mjs")
219
230
  executorSessionId = getSessionId()
220
231
  }
221
- return JSON.stringify(ledgerUpdate({ cwd: cwd ?? cwdOf(ctx), id, patch, executorSessionId }))
232
+ return JSON.stringify({ ...ledgerUpdate({ cwd: target, id, patch, executorSessionId }), ...reportedRoot(target) })
233
+ }
234
+ case "close": {
235
+ const target = rest.cwd ?? cwdOf(ctx)
236
+ return JSON.stringify({ ...ledgerClose({ cwd: target, id: rest.id, status: rest.status, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace }), ...reportedRoot(target) })
222
237
  }
223
- case "close":
224
- return JSON.stringify(ledgerClose({ cwd: rest.cwd ?? cwdOf(ctx), id: rest.id, status: rest.status, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace }))
225
238
  case "query":
226
239
  return JSON.stringify(ledgerQuery({ cwd: rest.cwd ?? cwdOf(ctx), status: rest.status, kind: rest.kind, board: rest.board, trigger: rest.trigger }), null, 2)
227
- case "count":
228
- return JSON.stringify({ count: ledgerCount({ cwd: rest.cwd ?? cwdOf(ctx) }) })
240
+ case "count": {
241
+ const target = rest.cwd ?? cwdOf(ctx)
242
+ return JSON.stringify({ count: ledgerCount({ cwd: target }), ...reportedRoot(target) })
243
+ }
229
244
  default:
230
245
  // 不可达(② 枚举守门已先拒)——fail-loud 兜底(防未来变体增量 drift)
231
246
  throw new Error(`ledger:action 非法:${argPreview(action)}(取值 ∈ ${enumText(actions)})`)
package/ledger.mjs CHANGED
@@ -15,10 +15,8 @@
15
15
  *
16
16
  * W8 契约②:ledger-db.mjs 静态 import node:sqlite ⇒ 消费侧一律**动态 import** 本档(禁止静态 import)。
17
17
  */
18
- import { opendirSync, statSync } from "node:fs"
19
- import { basename, dirname, join, resolve } from "node:path"
20
- import { projectRootView } from "./manifest.mjs"
21
- import { ledgerDbPath, PENDING_STATUSES, AGING_DAYS } from "./ledger-db.mjs"
18
+ import { basename, dirname, resolve } from "node:path"
19
+ import { AGING_DAYS, PENDING_STATUSES, ledgerChildren, ledgerDbPath, resolveLedgerRoot } from "./ledger-db.mjs"
22
20
  import { ledgerQuery } from "./ledger-cmd.mjs"
23
21
  import { executorTail } from "./ledger-executors.mjs"
24
22
 
@@ -45,55 +43,20 @@ export function entryTitle(text) {
45
43
  return norm.length > 20 ? norm.slice(0, 20) + "…" : norm
46
44
  }
47
45
 
48
- const isFile = (p) => { try { return statSync(p).isFile() } catch { return false } }
49
-
50
- /** 向上(含自身)最近的已注册台账项目根 → {root, ledger} / null(F4——CLI 锚 = cwd;
51
- * 标记 = 用户数据目录里以该项目路径为键的台账库——项目目录内不落任何标记文件)。 */
46
+ /** 向上(含自身)最近的**已注册**项目根 → {root, ledger} / null(F4——CLI 锚 = cwd)。
47
+ * 薄委托 `resolveLedgerRoot(anchor, { descend: false })`(设计档 §3.8——**取祖先支**):`root` = 推导所
48
+ * 得项目根(非锚);容器 ∥ 工作区锚 ⇒ `null` ⇒ 族发现按容器形(族求和——§7.2 / AC-M2-18 / AC-M2-19)。 */
52
49
  export function findProject(anchor) {
53
- let dir = resolve(anchor)
54
- for (;;) {
55
- const ledger = ledgerDbPath(dir)
56
- if (isFile(ledger)) return { root: dir, ledger }
57
- const parent = dirname(dir)
58
- if (parent === dir) return null
59
- dir = parent
60
- }
61
- }
62
-
63
- /** 同级枚举上限(N2 成本有界:父目录超此规模 = 缓存 / 临时等非仓族形态 → 退化空集,只显当前项目)。 */
64
- export const MAX_SIBLING_SCAN = 100
65
-
66
- /** 目录下已注册台账的直接子目录(目录名升序;不可读 → [];**惰性枚举 + 上限**——大目录零全量扫描)。
67
- * 导出面 = 族发现(本档)∥ 轻通道闸候选枚举(`agent/light-round.mjs`——同级判据单源)。 */
68
- export function ledgerChildren(dir) {
69
- const out = []
70
- let handle
71
- try { handle = opendirSync(dir) } catch { return out }
72
- try {
73
- let n = 0
74
- for (;;) {
75
- const ent = handle.readSync()
76
- if (!ent) break
77
- if (++n > MAX_SIBLING_SCAN) return []
78
- if (!ent.isDirectory()) continue
79
- const root = join(dir, ent.name)
80
- const ledger = ledgerDbPath(root)
81
- if (isFile(ledger)) out.push({ root, ledger })
82
- }
83
- } catch { /* 读中断 → 已得子集(尽力而为) */ }
84
- finally { try { handle.closeSync() } catch { /* 已关 */ } }
85
- return out.sort((a, b) => (a.root < b.root ? -1 : a.root > b.root ? 1 : 0))
50
+ const derived = resolveLedgerRoot(anchor, { descend: false })
51
+ return derived ? { root: derived.root, ledger: ledgerDbPath(derived.root) } : null
86
52
  }
87
53
 
88
- /** 项目族发现:{current, projects}——projects = current + 其同级含台账库目录(**发现序:current 在前**,
89
- * 其余按目录名升序);current 缺(容器目录,K6)→ 锚本地:取锚自身含台账库的直接子目录(不沿祖先链上找)。
90
- * **歧义根不充当项目**(轻通道轮 · 2026-10-04):命中锚若项目解析 `ambiguous`(≥2 候选——`projectRootView`)
91
- * ⇒ 该锚不可扫描(`openLedger` 歧义拒)——其键控库(先于 #828 写门的存量空壳 / 幽灵)不得遮蔽族发现;
92
- * 按容器形落子目录族(需求:打开容器根 ⇒ 族内合计——标记范围批 §1 原话)。 */
54
+ /** 项目族发现:{current, projects}——current = `findProject`(**祖先支**:容器 ∥ 工作区锚 ⇒ `null` ⇒ 按
55
+ * 容器形落子目录族);projects = current + 其**同级已注册**目录(**发现序:current 在前**,其余按目录
56
+ * 名升序);current 缺(容器目录,K6)→ **锚本地**:取锚自身**已注册**的直接子目录(不沿祖先链上找)。
57
+ * 「工作区根不充当项目」的判据单源 = `resolveLedgerRoot` 的库臂排除(§2.1)——本函数零自持判据。 */
93
58
  export function discoverFamily(anchor) {
94
- const hit = findProject(anchor)
95
- // 歧义根跳过(键控库存在 ≠ 可作项目——扫描面 `buildScan` 对歧义锚必拒;遮蔽则标记静默缺席)。
96
- const current = hit && projectRootView(hit.root).state === "ambiguous" ? null : hit
59
+ const current = findProject(anchor)
97
60
  if (current) {
98
61
  const siblings = ledgerChildren(dirname(current.root)).filter((p) => p.root !== current.root)
99
62
  return { current, projects: [current, ...siblings] }
@@ -105,10 +68,16 @@ export function discoverFamily(anchor) {
105
68
  * agedKeys/agedTitles/thresholdReached/boards/actionable/root/name/ledger。行龄 = 时间戳
106
69
  * (updated_at ?? created_at 距今 > days;未知不计——零假阳降级,同 v1 行龄未知口径)。
107
70
  * `now`/`days` = 注入面(确定性用例——替代 v1 ageOf git 注入面)。
71
+ * **根推导**(设计档 §3.8):`root` / `name` / `ledger` = `resolveLedgerRoot(anchor)` 推导所得项目根(非锚
72
+ * ——容器 ∥ 工作区锚取收敛序之根)——与行集读取同一单源;无根 ⇒ `ledger` = `null`(不落兜底根——
73
+ * 该态下 `root` / `name` 回落锚、行集为空,且该 scan 不进族枚举(`discoverFamily` 容器支只列已注册子目录))。
108
74
  * `inflightExecutors`(F-LX1 · sync 零变——K-LX2):在途且 executor 非空行集原样携带
109
75
  * {executor, updated_at, created_at}——判活解析由 `resolveExecutorStates` async 单源负责,本函数零探测。 */
110
76
  export function buildScan({ cwd, days = AGING_DAYS, now = Date.now() } = {}) {
111
- const rows = ledgerQuery({ cwd })
77
+ const anchor = resolve(cwd)
78
+ const derived = resolveLedgerRoot(anchor)
79
+ const root = derived ? derived.root : anchor
80
+ const rows = ledgerQuery({ cwd: anchor })
112
81
  const pending = rows.filter((r) => PENDING_STATUSES.includes(r.status))
113
82
  const poolEntries = pending.filter((r) => r.kind === "requirement")
114
83
  const techEntries = pending.filter((r) => r.kind === "tech_todo")
@@ -123,12 +92,11 @@ export function buildScan({ cwd, days = AGING_DAYS, now = Date.now() } = {}) {
123
92
  if (key) boards.set(key, (boards.get(key) ?? 0) + 1)
124
93
  }
125
94
  const thresholdReached = poolEntries.length >= THRESHOLD_POOL || [...boards.values()].some((n) => n >= THRESHOLD_BOARD)
126
- const root = resolve(cwd)
127
95
  const inflightExecutors = pending
128
96
  .filter((r) => r.status === "在途" && r.executor)
129
97
  .map((r) => ({ executor: r.executor, updated_at: r.updated_at ?? null, created_at: r.created_at ?? null }))
130
98
  return {
131
- name: basename(root), root, ledger: ledgerDbPath(root),
99
+ name: basename(root), root, ledger: derived ? ledgerDbPath(root) : null,
132
100
  pool: poolEntries.length, tech: techEntries.length, aged: aged.length,
133
101
  agedKeys: aged.map((r) => normalizeEntry(r.title)), agedTitles: aged.map((r) => entryTitle(r.title)),
134
102
  thresholdReached, boards, actionable: aged.length > 0 || thresholdReached,
@@ -178,8 +146,9 @@ export function detailScans(scans, current) {
178
146
  }
179
147
 
180
148
  // ── re-export(拆分件接口——命令面接线 = 动态 import 本档,KD-M2-3;`AGING_DAYS` 常量单源 = `ledger-db.mjs`
181
- // ——2026-10-07 批 ledger-tool 自本档迁入,公共面零变) ──
182
- export { AGING_DAYS, ALLOWED_MIGRATIONS, ledgerDbPath, ledgerKey, ledgerDirPath, openLedger, PENDING_STATUSES, _setLedgerDirForTest, _resetLedgerDirForTest, ensureExecutorColumn } from "./ledger-db.mjs"
149
+ // ——2026-10-07 批 ledger-tool 自本档迁入,公共面零变;`ledgerChildren` / `MAX_SIBLING_SCAN` / `resolveLedgerRoot`
150
+ // ——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"
183
152
  export { ledgerAdd, ledgerClose, ledgerCount, ledgerQuery, ledgerUpdate } from "./ledger-cmd.mjs"
184
153
  export { ledgerTool, ledgerReadTool, ledgerAddTool, ledgerCloseTool, ledgerCountTool, ledgerQueryTool, ledgerUpdateTool } from "./ledger-tools.mjs"
185
154
  export { runLedgerAudit, runLedgerMigrate } from "./ledger-migrate.mjs"
package/log.mjs CHANGED
@@ -16,8 +16,10 @@
16
16
  * ① 字段名精确匹配黑名单(apiKey/designToken/password/secret/token,大小写不敏感;
17
17
  * 另加防御性名称 authorization/proxy/proxyUri——防凭据型 URL/头字段泄漏——§2.5)
18
18
  * → 整个字段丢弃;
19
- * ② 内容只扫密钥形态(sk-xxx / Bearer xxx / key=… 等)→ 截断到形态之前
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
23
  * 工具事件不记 args;URL 不入事件(llm/tool 事件从不携带 URL;err 文本经形态扫描)。
22
24
  * - 摘要截断(B 方案,2026-09-03 用户裁定):截断处带 "…" 标记(截后仍 ≤上限)。
23
25
  * - 测试隔离:node --test 进程(NODE_TEST_CONTEXT)默认不写盘——防测试事件污染真实
@@ -37,8 +39,8 @@ import { join } from "node:path"
37
39
  export const MAX_LINE = 512
38
40
  /** 字段名精确黑名单(NF-L3/§2.4——2026-09-03 评审 refinement #7:与 §2.4 词表一致含 token) */
39
41
  const BLACKLIST_FIELDS = new Set(["apikey", "designtoken", "password", "secret", "token", "authorization", "proxyuri", "proxy"])
40
- /** 内容密钥形态扫描(大小写不敏感)——命中即截断到形态前 */
41
- const SECRET_FORM = /(\bsk-[A-Za-z0-9_-]{6,}|\bBearer\s+[A-Za-z0-9._~+/=-]{6,}|\b(?:api[_-]?key|key|token|secret|password|pwd|passwd)\s*[=:]\s*['"]?[A-Za-z0-9._~+/=-]{6,})/i
42
+ /** 内容密钥形态扫描(大小写不敏感)——命中即截断到形态前(含 token 值形 `uuid:epoch`——同 batch.mjs 凭据第三形判据) */
43
+ const SECRET_FORM = /(\bsk-[A-Za-z0-9_-]{6,}|\bBearer\s+[A-Za-z0-9._~+/=-]{6,}|\b(?:api[_-]?key|key|token|secret|password|pwd|passwd)\s*[=:]\s*['"]?[A-Za-z0-9._~+/=-]{6,}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}:\d{10,16})/i
42
44
  /** 字符串字段长度兜底:head ≤300 / err ≤200 / 其余 ≤120 */
43
45
  const FIELD_CAPS = { head: 300, err: 200 }
44
46
 
@@ -154,6 +156,18 @@ export function redactSecret(fieldKey, value) {
154
156
  return hit ? s.slice(0, hit.index) + "[redacted]" : s
155
157
  }
156
158
 
159
+ /** 结构面遮蔽(#1432——tool-errors args 落痕前过同一链):键级 ∥ 内容级复用 `redactSecret`;嵌套对象 ∥ 数组递归;
160
+ * 非串值仅在键名命中黑名单时整键遮蔽(否则原样 ∥ 递归)——「宁可丢信息不漏密钥」同判据。 */
161
+ export function redactArgs(value) {
162
+ if (Array.isArray(value)) return value.map(redactArgs)
163
+ if (value && typeof value === "object") {
164
+ const out = {}
165
+ for (const [k, v] of Object.entries(value)) out[k] = typeof v === "string" || BLACKLIST_FIELDS.has(String(k).toLowerCase()) ? redactSecret(k, v) : redactArgs(v)
166
+ return out
167
+ }
168
+ return typeof value === "string" ? redactSecret("", value) : value
169
+ }
170
+
157
171
  /** 内容级净化:密钥形态截断到形态前(②);超长截断 + "…" 标记。 */
158
172
  export function sanitizeString(key, value) {
159
173
  let s = String(value)
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * manifest-discovery.mjs — **发现 ∕ 归属族**出档(2026-09-29 structure-split-2 · 台账 #620):自 `manifest.mjs`
3
- * 全量迁出〔原 `:39-143`〕——`MANIFEST_REL` · 测试注入位(`_setProjectRootForTest` / `_resetProjectRootForTest`)·
3
+ * 全量迁出——`MANIFEST_REL` · 测试注入位(`_setProjectRootForTest` / `_resetProjectRootForTest`)·
4
4
  * `scanChildren`(两条梯表共用 walk 内核)· `discoverProjects`(项目梯)· `discoverRepos`(仓梯)·
5
5
  * `owningProject`(归属形)· `resolveProjectRoot`(归属 ∨ 发现);**结构拆分零语义**(面不变 ∕ 判据不变,
6
6
  * 只换宿主档;切点 ∕ 缝单源 = 批档 `docs/batches/2026-09-29-structure-split-2.md` §2.2-C)。
@@ -41,13 +41,14 @@ function scanChildren(anchor, isHit) {
41
41
  /**
42
42
  * **项目发现**(项目梯五级——KD-M1-23 / 设计 `docs/core/design/MANIFEST.md` §2.2;**git 非前提**,
43
43
  * 2026-09-21 用户裁定):① 锚自身带 `MANIFEST_REL` ⇒ 锚;② 锚含 `.git` ⇒ 锚(缺档 = 建档机会);
44
- * ③ 直接子目录中**带档**者优先(非空即只看此级:恰一 ⇒ `unique` / ≥2 ⇒ `ambiguous`);
45
- * ④ 零带档才看含 `.git` 的**裸仓**(同判);⑤ 均无 ⇒ `none`(⇒ 建档流,落点 = 会话锚)。
44
+ * ③ 直接子目录中**带档**者优先(非空即只看此级:恰一 ⇒ `unique` / ≥2 ⇒ `workspace`——下辖多项目
45
+ * = 工作区常态,非错误态);④ 零带档才看含 `.git` 的**裸仓**(同判);⑤ 均无 ⇒ `none`(⇒ 建档流,
46
+ * 落点 = 会话锚)。
46
47
  * `candidates` 一律**按名排序**(`self` / `none` ⇒ `[]`);`matched` ∈ `manifest` / `git` / `null`
47
- * 记命中(或歧义)出自哪一级。**纯 fs**(只判存在性——不解析档内容)· **不抛** · **不向上** · **不递归**。
48
+ * 记命中(或工作区)出自哪一级。**纯 fs**(只判存在性——不解析档内容)· **不抛** · **不向上** · **不递归**。
48
49
  * **覆盖位**(`_setProjectRootForTest` 在场)⇒ **头部短路**(测试注入面——先于真判据,同 `discoverRepos`)。
49
50
  * @param {string} [cwd] 会话锚(缺省 → 进程 cwd)
50
- * @returns {{kind:'self'|'unique'|'none'|'ambiguous', root:string|null, candidates:string[], matched:'manifest'|'git'|null}}
51
+ * @returns {{kind:'self'|'unique'|'none'|'workspace', root:string|null, candidates:string[], matched:'manifest'|'git'|null}}
51
52
  */
52
53
  export function discoverProjects(cwd) {
53
54
  if (_projectRootOverride) return { kind: "self", root: resolve(_projectRootOverride), candidates: [], matched: null }
@@ -55,10 +56,10 @@ export function discoverProjects(cwd) {
55
56
  if (existsSync(join(anchor, MANIFEST_REL))) return { kind: "self", root: anchor, candidates: [], matched: "manifest" }
56
57
  if (existsSync(join(anchor, ".git"))) return { kind: "self", root: anchor, candidates: [], matched: "git" }
57
58
  const withManifest = scanChildren(anchor, (d) => existsSync(join(d, MANIFEST_REL)))
58
- if (withManifest.length > 1) return { kind: "ambiguous", root: null, candidates: withManifest, matched: "manifest" }
59
+ if (withManifest.length > 1) return { kind: "workspace", root: null, candidates: withManifest, matched: "manifest" }
59
60
  if (withManifest.length === 1) return { kind: "unique", root: withManifest[0], candidates: [...withManifest], matched: "manifest" }
60
61
  const bareRepos = scanChildren(anchor, (d) => existsSync(join(d, ".git")))
61
- if (bareRepos.length > 1) return { kind: "ambiguous", root: null, candidates: bareRepos, matched: "git" }
62
+ if (bareRepos.length > 1) return { kind: "workspace", root: null, candidates: bareRepos, matched: "git" }
62
63
  if (bareRepos.length === 1) return { kind: "unique", root: bareRepos[0], candidates: [...bareRepos], matched: "git" }
63
64
  return { kind: "none", root: null, candidates: [], matched: null }
64
65
  }
@@ -66,21 +67,22 @@ export function discoverProjects(cwd) {
66
67
  /**
67
68
  * **仓发现**(仓梯——KD-M1-23 / `docs/core/design/TOOLS.md` §6.13;**单源** KD-M1-22——`git` 工具
68
69
  * 经此符号接线,禁第二份实现):① 锚含 `.git` ⇒ 锚;② 直接子目录中 `.git` **∧** 带档者恰一 ⇒ 命中
69
- * (≥2 ⇒ 歧义);③ **零个此类时才看**含 `.git` 的裸仓(恰一 ⇒ 命中 / ≥2 ⇒ 歧义);④ 均无 ⇒ `none`。
70
+ * (≥2 ⇒ `workspace`——下辖多仓 = 常态);③ **零个此类时才看**含 `.git` 的裸仓(恰一 ⇒ 命中 /
71
+ * ≥2 ⇒ `workspace`);④ 均无 ⇒ `none`。
70
72
  * 与 `discoverProjects` = **同一 walk 内核**(`scanChildren`)的两种梯表——差异只在谓词与级序。
71
73
  * `candidates` 按名排序;`matched` 记档位;纯 fs / 不抛 / 不递归 / 不向上;覆盖位 ⇒ 头部短路。
72
74
  * @param {string} [cwd] 会话锚(缺省 → 进程 cwd)
73
- * @returns {{kind:'self'|'unique'|'none'|'ambiguous', root:string|null, candidates:string[], matched:'manifest'|'git'|null}}
75
+ * @returns {{kind:'self'|'unique'|'none'|'workspace', root:string|null, candidates:string[], matched:'manifest'|'git'|null}}
74
76
  */
75
77
  export function discoverRepos(cwd) {
76
78
  if (_projectRootOverride) return { kind: "self", root: resolve(_projectRootOverride), candidates: [], matched: null }
77
79
  const anchor = resolve(cwd ?? ".")
78
80
  if (existsSync(join(anchor, ".git"))) return { kind: "self", root: anchor, candidates: [], matched: "git" }
79
81
  const scoped = scanChildren(anchor, (d) => existsSync(join(d, ".git")) && existsSync(join(d, MANIFEST_REL)))
80
- if (scoped.length > 1) return { kind: "ambiguous", root: null, candidates: scoped, matched: "manifest" }
82
+ if (scoped.length > 1) return { kind: "workspace", root: null, candidates: scoped, matched: "manifest" }
81
83
  if (scoped.length === 1) return { kind: "unique", root: scoped[0], candidates: [...scoped], matched: "manifest" }
82
84
  const bare = scanChildren(anchor, (d) => existsSync(join(d, ".git")))
83
- if (bare.length > 1) return { kind: "ambiguous", root: null, candidates: bare, matched: "git" }
85
+ if (bare.length > 1) return { kind: "workspace", root: null, candidates: bare, matched: "git" }
84
86
  if (bare.length === 1) return { kind: "unique", root: bare[0], candidates: [...bare], matched: "git" }
85
87
  return { kind: "none", root: null, candidates: [], matched: null }
86
88
  }
@@ -107,23 +109,24 @@ export function owningProject(target) {
107
109
  }
108
110
  }
109
111
 
110
- /** **项目根解析的歧义直读形**(判定单点——设计 `MANIFEST.md` §2.2 `projectRootView` 契约行 / §2.5
111
- * 「多档解析消费面收口」条):`ok`(唯一解析:归属命中 ∥ 发现 `self` / `unique`)/ `ambiguous`
112
- * (≥2 候选 ⇒ `root` null + 候选全列**按名排序**)/ `none`(无项目 ⇒ 双空);覆盖位在场 ⇒
113
- * 头部短路(`ok` + 覆盖值)。**非抛错 / 零写**;`resolveProjectRoot` 薄委托本函数(行为逐字零变)。
114
- * @returns {{state:'ok'|'ambiguous'|'none', root:string|null, candidates:string[]}} */
112
+ /** **项目根解析的直读形**(判定单点——设计 `MANIFEST.md` §2.2 `projectRootView` 契约行 / §2.5
113
+ * 「单项目解析律与四处会话级缺省收口」条):`ok`(唯一解析:归属命中 ∥ 发现 `self` / `unique`)/
114
+ * `workspace`(锚自身无档 ∧ 下辖 **≥2** 项目 ⇒ `root` null + 下辖项目全列**按名排序**——工作区
115
+ * 常态,非错误态)/ `none`(无项目 ⇒ 双空);覆盖位在场 ⇒ 头部短路(`ok` + 覆盖值)。
116
+ * **非抛错 / 零写**;`resolveProjectRoot` 薄委托本函数(行为逐字零变)。
117
+ * @returns {{state:'ok'|'workspace'|'none', root:string|null, candidates:string[]}} */
115
118
  export function projectRootView(cwd) {
116
119
  const owning = owningProject(cwd)
117
120
  if (owning) return { state: "ok", root: owning, candidates: [] }
118
121
  const d = discoverProjects(cwd)
119
122
  if (d.root) return { state: "ok", root: d.root, candidates: [] }
120
- if (d.kind === "ambiguous") return { state: "ambiguous", root: null, candidates: d.candidates }
123
+ if (d.kind === "workspace") return { state: "workspace", root: null, candidates: d.candidates }
121
124
  return { state: "none", root: null, candidates: [] }
122
125
  }
123
126
 
124
127
  /** 项目根解析(**归属 ∨ 发现**——KD-M1-30 / M1-24):**薄委托** `projectRootView(cwd).root`(判定
125
128
  * 单点见上——#828)。带档路径与批前**逐字同**;变更面两条(设计 §2.2 同条):① 项目树内路径
126
- * (祖先带档)⇒ 该项目根;② 裸仓恰一 ⇒ 该仓根(零档降级 = 建档机会)。`none` / `ambiguous` ⇒
129
+ * (祖先带档)⇒ 该项目根;② 裸仓恰一 ⇒ 该仓根(零档降级 = 建档机会)。`none` / `workspace` ⇒
127
130
  * `null`。调用方零改(行为随语义变更——设计 §2.5 键面条 / 错层条)。
128
131
  * @returns {string|null} 项目根绝对路径 / null */
129
132
  export function resolveProjectRoot(cwd) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * manifest-schema.mjs — **校验 ∕ 默认值族**出档(2026-09-29 structure-split-2 · 台账 #620):自 `manifest.mjs`
3
- * 全量迁出〔原 `:194-205` + `:240-369` 两段非连续〕——`isValidDocRootValue`(docRoot 子键值形态判据)·
4
- * `DEFAULT_MANIFEST`(默认八键)· `MANIFEST_SCHEMA`(判据单源)· 元素层谓词两件 · `validateManifest` ·
3
+ * 全量迁出——`isValidDocRootValue`(docRoot 子键值形态判据)·
4
+ * `DEFAULT_MANIFEST`(默认八键)· `MANIFEST_SCHEMA`(判据单源)· 元素层谓词一件 · `validateManifest` ·
5
5
  * `fillDefaults`;**结构拆分零语义**(面不变 ∕ 判据不变,只换宿主档;切点 ∕ 缝单源 =
6
6
  * 批档 `docs/batches/2026-09-29-structure-split-2.md` §2.2-C)。
7
7
  *
@@ -43,7 +43,6 @@ export const DEFAULT_MANIFEST = Object.freeze({
43
43
  widthExemptZones: Object.freeze([]),
44
44
  anchors: Object.freeze({ domain: "docs", exclude: Object.freeze(["_archive", "batches"]) }),
45
45
  exemptions: Object.freeze([]),
46
- lineCounts: Object.freeze([]), // 行数面机检声明(#546):[] = 未载惰性(doc-check 行数族)
47
46
  }),
48
47
  // 项目声明三族(2026-09-27 声明载体唯一化批并入——键名 / 形态逐字承前;来源档已退役)。
49
48
  codePaths: Object.freeze(["src"]),
@@ -72,12 +71,6 @@ function isNonEmptyStringArray(v) {
72
71
  return Array.isArray(v) && v.every((e) => typeof e === "string" && e.trim() !== "")
73
72
  }
74
73
 
75
- /** `checkConfig.lineCounts` 元素层判据(#546):数组且各元素为非数组对象、`doc` / `section` 皆非空串(`trim` 后非空);`[]` = 未载惰性。 */
76
- function isLineCountsValue(v) {
77
- return Array.isArray(v) && v.every((e) => e !== null && typeof e === "object" && !Array.isArray(e)
78
- && typeof e.doc === "string" && e.doc.trim() !== "" && typeof e.section === "string" && e.section.trim() !== "")
79
- }
80
-
81
74
  /**
82
75
  * 形状校验(模块设计 §2.2)——枚举 / version 数值恒做;**纯函数、零 fs**(原 `{ cwd }`
83
76
  * 指针分支已随字段整链裁撤——KD-M1-5 墓志);不落盘。键存在性入 missingKeys(非拒):
@@ -116,10 +109,6 @@ export function validateManifest(obj) {
116
109
  if (nested === "docRoot" && !isValidDocRootValue(value[sub])) {
117
110
  errors.push(`docRoot.${sub} 值形态非法:${JSON.stringify(value[sub])}(须为非空字符串 / 非空字符串数组 / null(本面无)——KD-M1-6 / KD-M1-7 / KD-M1-37)`)
118
111
  }
119
- // checkConfig.lineCounts 元素层形态(#546):数组,元素 = { doc, section } 非空串;非法 → 拒(不静默跳过)。
120
- if (nested === "checkConfig" && sub === "lineCounts" && !isLineCountsValue(value[sub])) {
121
- errors.push(`checkConfig.lineCounts 值形态非法:${JSON.stringify(value[sub])}(须为数组,元素 = { doc: 非空串, section: 非空串 }——#546)`)
122
- }
123
112
  }
124
113
  }
125
114
  // 三族声明键形态(KD-M1-32——fail-closed:非法即 errors,不静默跳过;缺键仍在 missingKeys)。
package/manifest.mjs CHANGED
@@ -10,7 +10,7 @@
10
10
  * 未知键(#802 / KD-M1-36)→ 产 unknownKeys + 一行可见告警(console.warn + logEvent;
11
11
  * 同档同键集去重、读到零未知键清该档 memo)——非拒定性零变。
12
12
  * - validateManifest(obj) → { ok, errors, missingKeys, unknownKeys }:枚举 / version 数值 /
13
- * docRoot 子键值形态(串 | 数组 | null(本面无——KD-M1-37),F7)+ checkConfig.lineCounts 元素层形态——**纯函数、零 fs**;不落盘。
13
+ * docRoot 子键值形态(串 | 数组 | null(本面无——KD-M1-37),F7)——**纯函数、零 fs**;不落盘。
14
14
  * - 三族声明键(`codePaths` / `index.{codeExtensions,docExtensions,publicRepos}` / `advisor.{docMap,standardsDoc}`——
15
15
  * KD-M1-31 / M1-32):缺键补默认;形态错 = 档非法(fail-closed,与 docRoot 子键同款);
16
16
  * 读向 = `declaration.mjs`(2026-10-01 自 `conventions.mjs` 迁出——经其转口可达)经 `readManifest` /
@@ -25,10 +25,10 @@
25
25
  * - projectView(target) → { state, root, path, manifest?, candidates?, errors?, matched }:**按用点解析**
26
26
  * 的读侧单点(归属 ∨ 发现兜底 / 五态 / 非抛错 / **零写**——KD-M1-24)。
27
27
  * - resolveProjectRoot(cwd):项目根(归属 ∨ 发现——KD-M1-30)= `projectRootView(cwd).root`(薄委托);
28
- * projectRootView(cwd) → { state: ok|ambiguous|none, root, candidates }:歧义直读形(判定单点——§2.2)。
28
+ * projectRootView(cwd) → { state: ok|workspace|none, root, candidates }:项目根直读形(判定单点——§2.2)。
29
29
  * - resolveEngineeringManifest(cwd, { writer, init }) → 入口决策树(**非抛错**——KD-M1-20):
30
30
  * 两端入口钩子与翻转面(拒翻)共用同一张树(判据单源;§2.8 F1)——失败码五枚
31
- * `missing` / `invalid` / `no-project` / `ambiguous` / `init-failed`(KD-M1-28)。名中 `Engineering` = 历史命名(沿用不改名);消费面 = 两端入口钩子 + 翻转面——**非**「模式拥有本模块」。
31
+ * `missing` / `invalid` / `no-project` / `workspace` / `init-failed`(KD-M1-28)。名中 `Engineering` = 历史命名(沿用不改名);消费面 = 两端入口钩子 + 翻转面——**非**「模式拥有本模块」。
32
32
  * - manifestFilePath(cwd) → 数据档绝对路径(档路径单源 KD-M1-18——读 / 写 / mtime 门控三处同源)。
33
33
  * - initManifest(cwd, { writer = 'subagent' } = {}) → 经写门写 DEFAULT_MANIFEST,缺省拒。
34
34
  * - writeManifest(cwd, manifest, { writer = 'subagent' } = {}) → 落盘前先校验(ok:false 拒
@@ -53,14 +53,14 @@ export { DEFAULT_MANIFEST, MANIFEST_SCHEMA, isValidDocRootValue, validateManifes
53
53
  * · **归属(第一段——§⑥ 归属形)**:`owningProject(target)` 沿祖先链取最近带档目录
54
54
  * (子优于根 / 不跨兄弟 / 无全局优先级)⇒ 读该档 ⇒ `ok` / `invalid`。
55
55
  * · **发现兜底(第二段——§⑥ 发现规则,纯向下)**:祖先链无档 ⇒ `discoverProjects(target)` ⇒ 命中
56
- * (`self` / `unique`)⇒ 读其档(带档 ⇒ `ok` / `invalid`;缺档 ⇒ `missing`);`ambiguous` ⇒
57
- * `ambiguous`(+ `candidates`);`none` ⇒ `no-project`。
56
+ * (`self` / `unique`)⇒ 读其档(带档 ⇒ `ok` / `invalid`;缺档 ⇒ `missing`);`workspace` ⇒
57
+ * `workspace`(+ `candidates`);`none` ⇒ `no-project`。
58
58
  * 写侧(建档)**不在**此函数(归入口决策树——KD-M1-25 / M1-29)⇒ 注入器 / 只读消费面不会变写点。
59
59
  * `matched` 契约:归属段命中 ⇒ `'manifest'`;发现段 ⇒ `discoverProjects.matched` 逐字;
60
- * `no-project`(/ 覆盖位短路)⇒ `null`——报明行歧义变体按它分野(§2.6 条 1b)。
60
+ * `no-project`(/ 覆盖位短路)⇒ `null`——报明行工作区变体按它分野(§2.6 条 1b)。
61
61
  * 读错(非 ENOENT——权限 / 目录等)收为 `invalid`(非抛错契约;`readManifest` 语义零改)。
62
62
  * @param {string} [target] 目标路径(目录 / 文件——动作作用于哪个项目的路径)
63
- * @returns {{state:'ok'|'missing'|'no-project'|'ambiguous'|'invalid', root:string|null, path:string|null,
63
+ * @returns {{state:'ok'|'missing'|'no-project'|'workspace'|'invalid', root:string|null, path:string|null,
64
64
  * manifest?:object, candidates?:string[], errors?:string[], matched:'manifest'|'git'|null}}
65
65
  */
66
66
  export function projectView(target) {
@@ -68,7 +68,7 @@ export function projectView(target) {
68
68
  const owning = owningProject(anchor)
69
69
  if (owning) return viewAtRoot(owning, "manifest")
70
70
  const d = discoverProjects(anchor)
71
- if (d.kind === "ambiguous") return { state: "ambiguous", root: null, path: null, candidates: d.candidates, matched: d.matched }
71
+ if (d.kind === "workspace") return { state: "workspace", root: null, path: null, candidates: d.candidates, matched: d.matched }
72
72
  if (d.kind === "none") return { state: "no-project", root: null, path: null, matched: null }
73
73
  return viewAtRoot(d.root, d.matched)
74
74
  }
@@ -187,6 +187,16 @@ export function requireManifest(cwd) {
187
187
  return readManifest(cwd)
188
188
  }
189
189
 
190
+ /** 建档(init 支)成功后的同步通知位(#1376——**反向注册**:`declaration.mjs` 已向依赖本模块,
191
+ * 订阅方由对侧注册 ⇒ 本模块保持零出边(MANIFEST.md KD-M1-33);形同 `config-io.mjs` `onConfigSelfWrite`
192
+ * ——返回退订函数;订阅方失败不影响建档)。 */
193
+ const manifestInitFns = new Set()
194
+ export function onManifestInit(fn) {
195
+ if (typeof fn !== "function") return () => {}
196
+ manifestInitFns.add(fn)
197
+ return () => { manifestInitFns.delete(fn) }
198
+ }
199
+
190
200
  /**
191
201
  * 入口决策树(**非抛错**形态——KD-M1-20;docs/core/design/MANIFEST.md §2.8 F1):三面共用——
192
202
  * ① CLI 装配 / 重估薄包装 ② VSC `hydrateRun` 钩子块 ③ 翻转面(`eng` 工具 / `/eng` /
@@ -196,22 +206,22 @@ export function requireManifest(cwd) {
196
206
  * 分支(两分支六出口——§2.8 F1 树):
197
207
  * 档合法 → { ok:true, manifest, created:false }(manifest = 补默认值后的档内容)
198
208
  * 缺档 + init → 梯②④⑤(锚 = 裸仓 / 裸仓命中 / 无项目)⇒ `initManifest(cwd, { writer })`
199
- * (抛错 → `init-failed`);歧义(≥2 候选)⇒ `{ ok:false, code:'ambiguous',
200
- * message, candidates }`(**不建 / 不猜**)
209
+ * (抛错 → `init-failed`);工作区(锚自身无档 ∧ 下辖 ≥2 项目)⇒
210
+ * `{ ok:false, code:'workspace', message, candidates }`(**不建 / 不猜**)
201
211
  * 缺档 + !init → 梯⑤(无项目)⇒ `{ ok:false, code:'no-project' }`(KD-M1-28;`init:false` 面);
202
- * 梯②④(裸仓可解析、档缺)⇒ `{ ok:false, code:'missing' }`;歧义 ⇒ 同上
212
+ * 梯②④(裸仓可解析、档缺)⇒ `{ ok:false, code:'missing' }`;工作区 ⇒ 同上
203
213
  * 档非法 → { ok:false, code:'invalid', message, errors }
204
214
  *
205
215
  * `writer` 由调用点**显式**传(生产调用点全传 `'main'`):缺省 `'subagent'` 是写门 fail-closed
206
216
  * 缺省(KD-M1-3),误用缺省 ⇒「缺档 + 梯②④⑤」退化为 `init-failed`(拒翻——与 §2.8 F2 /
207
217
  * AC-20② 语义相反;T38 反证格)。
208
- * 失败码两态文案不同(KD-M1-28):无项目 ⇒ 可在锚处落地 / 歧义 ⇒ 列候选不猜;文案族 =
209
- * 「项目不可解析」(稳定锚句 `/项目不可解析/`——拒翻面与报明面共用)。
218
+ * 失败码文案分野(KD-M1-28):无项目 ⇒ 可在锚处落地 / 工作区 ⇒ 报明事实 + 点名项目根;文案族 =
219
+ * 「项目不可解析」(稳定锚句 `/项目不可解析/`)——**仅「档非法」格**(§2.8 F2 拒翻面唯一拒格)。
210
220
  * 边界:`readManifest` 的非 ENOENT 读错(权限 / 目录等)由 `projectView` 收为 `invalid`(非抛错
211
221
  * 契约——`readManifest` 返回语义零改);本树只承诺上列六出口。
212
222
  * @param {string} [cwd] 会话锚(缺省 → 进程 cwd)
213
223
  * @returns {{ok:true, manifest:object, created:boolean}
214
- * | {ok:false, code:'missing'|'invalid'|'no-project'|'ambiguous'|'init-failed',
224
+ * | {ok:false, code:'missing'|'invalid'|'no-project'|'workspace'|'init-failed',
215
225
  * message?:string, errors?:string[], candidates?:string[]}}
216
226
  */
217
227
  export function resolveEngineeringManifest(cwd, { writer = "subagent", init = true } = {}) {
@@ -223,24 +233,29 @@ export function resolveEngineeringManifest(cwd, { writer = "subagent", init = tr
223
233
  message: `项目不可解析:${MANIFEST_REL} 非法(fail-closed):${(view.errors ?? []).join(";")}`,
224
234
  }
225
235
  }
226
- if (view.state === "ambiguous") {
227
- return { ok: false, code: "ambiguous", candidates: view.candidates, message: ambiguousProjectMessage(cwd, view.candidates) }
236
+ if (view.state === "workspace") {
237
+ return { ok: false, code: "workspace", candidates: view.candidates, message: workspaceProjectMessage(cwd, view.candidates) }
228
238
  }
229
239
  // 缺档(梯②④⑤):init ⇒ 就地建档(内容 = DEFAULT_MANIFEST,经写门 writer:'main');!init ⇒ 归码。
230
240
  if (!init) return { ok: false, code: view.state === "no-project" ? "no-project" : "missing" }
231
241
  try {
232
- return { ok: true, manifest: initManifest(cwd, { writer }), created: true }
242
+ const manifest = initManifest(cwd, { writer })
243
+ // #1376:建档支成功 ⇒ 同步广播(进程内声明 ∥ 范围缓存即刻随动——如 eng 翻转面结束后
244
+ // 同进程的声明复读;订阅方失败不阻断建档;失败路径不广播)。
245
+ for (const fn of manifestInitFns) { try { fn() } catch (e) { console.warn(`[manifest] init subscriber failed: ${e?.message ?? e}`) } }
246
+ return { ok: true, manifest, created: true }
233
247
  } catch (e) {
234
248
  return { ok: false, code: "init-failed", message: e?.message ?? String(e) }
235
249
  }
236
250
  }
237
251
 
238
- /** 歧义消息(文案族「项目不可解析」——KD-M1-28):候选**全列**(绝对路径、按名排序)+ 指引
239
- * 显式指定目标——**不猜**(与报明行同族;拒翻面经 F3 明示面逐字转发)。 */
240
- function ambiguousProjectMessage(cwd, candidates) {
252
+ /** 工作区消息(**事实行**——§2.5「单项目解析律」条 / §2.8 F2 工作区行):念明下辖项目**全列**
253
+ * (绝对路径、按名排序)+ 可执行下一步(点名项目根 / 目标路径)——报明事实,非错误框定
254
+ * (不书「项目不可解析」/ 候选二义)。 */
255
+ function workspaceProjectMessage(cwd, candidates) {
241
256
  const list = (candidates ?? []).map((c) => `- ${c}`).join("\n")
242
- return `项目不可解析:会话锚 ${cwd} 下候选项目不是恰好一个(下列 ${(candidates ?? []).length} 个)——` +
243
- `每个项目一份 ${MANIFEST_REL};请显式指定目标项目(机制不猜):\n${list}`
257
+ return `工作区:会话锚 ${cwd} 自身无 ${MANIFEST_REL},下辖 ${(candidates ?? []).length} 个项目(下列全部)——` +
258
+ `每个项目一份 ${MANIFEST_REL};请显式给出目标项目根或目标路径(动作按用点路径解析):\n${list}`
244
259
  }
245
260
 
246
261
  /**
@@ -5,7 +5,7 @@ import { spawn, spawnSync } from "node:child_process"
5
5
  import { rpcId, CALL_TIMEOUT_MS, withTimeout, quoteArg } from "./helpers.mjs"
6
6
 
7
7
  /** 退出相位标志(D-MC19):模块加载期注册的 exit 钩子置位——退出相位无事件循环,
8
- * 只同步操作可落地(注册序恒早于宿主 cleanup——`thincoder-cli/src/tui/index.mjs:125`)。 */
8
+ * 只同步操作可落地(注册序恒早于宿主 cleanup——`thincoder-cli/src/tui/index.mjs` 的 `createExitCleanup`)。 */
9
9
  let exiting = false
10
10
  process.on("exit", () => { exiting = true })
11
11
 
package/mcp.mjs CHANGED
@@ -93,14 +93,14 @@ async function createConnectedTransport(rawConfig, serverName) {
93
93
  * 零副作用:不进 _sessions、不动 agent.tools、无 onDead 挂钩;finally close
94
94
  * (closed=true 使 onDead 重连不会触发)。initialize 与 tools/list 同受
95
95
  * INIT_TIMEOUT_MS 约束(见 doInitialize)。不复用 connectMcpServer(避免污染
96
- * session 幂等表)。 */
96
+ * session 幂等表)。读数 = `{ ok, toolCount, tools, latencyMs }`(`tools` = 工具名数组——控制台具名读数,2026-10-14 走查轮增补)。 */
97
97
  export async function probeMcpServer(config) {
98
98
  const start = Date.now()
99
99
  let transport
100
100
  let mcpTools
101
101
  try {
102
102
  ;({ transport, mcpTools } = await createConnectedTransport(config, config.name ?? config.command ?? config.url ?? config.wsUrl))
103
- return { ok: true, toolCount: mcpTools.length, latencyMs: Date.now() - start }
103
+ return { ok: true, toolCount: mcpTools.length, tools: mcpTools.map((t) => (typeof t?.name === "string" ? t.name : "")).filter((n) => n !== ""), latencyMs: Date.now() - start }
104
104
  } catch (error) {
105
105
  return { ok: false, error: error?.message ?? String(error) }
106
106
  } finally {
package/memory/core.mjs CHANGED
@@ -132,7 +132,7 @@ export function fetchEntry(memory, uid) {
132
132
  const lastColon = uid.lastIndexOf(":")
133
133
  const origin = lastColon > layer.length ? uid.slice(layer.length + 1, lastColon) : ""
134
134
  const path = lastColon > layer.length ? uid.slice(lastColon + 1) : uid.slice(layer.length + 1)
135
- const projectOrigin = normalizeOrigin(memory.projectOrigin) // §6.11 读缝归一(uid 解析面零改——`:131` 余量兜底仍在)
135
+ const projectOrigin = normalizeOrigin(memory.projectOrigin) // §6.11 读缝归一(uid 解析面零改——`fetchEntry` 余量兜底仍在)
136
136
  if (layer === "project" && projectOrigin) {
137
137
  const r = memory.db.prepare(`SELECT type, title, content, tags, author FROM files WHERE layer = ? AND origin = ? AND path = ?`).get(layer, origin || projectOrigin, path)
138
138
  if (r) return { ...r, layer, id: uid }