@thincoder/core 0.9.5 → 0.10.1

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 (215) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/abort-provenance.mjs +10 -10
  4. package/advisor/citations.mjs +5 -5
  5. package/advisor/compaction.mjs +16 -15
  6. package/advisor/loop.mjs +12 -12
  7. package/advisor/messages.mjs +6 -6
  8. package/advisor/project-context.mjs +5 -5
  9. package/advisor/repos.mjs +3 -3
  10. package/advisor/run.mjs +22 -12
  11. package/advisor.mjs +4 -4
  12. package/agent/assemble.mjs +111 -0
  13. package/agent/chat-call.mjs +68 -0
  14. package/agent/child-marks.mjs +2 -2
  15. package/agent/completion.mjs +9 -2
  16. package/agent/continue-decision.mjs +25 -0
  17. package/agent/dispatch-gates.mjs +140 -0
  18. package/agent/dispatch-run.mjs +167 -0
  19. package/agent/dispatch.mjs +54 -296
  20. package/agent/family-tools.mjs +5 -5
  21. package/agent/helpers.mjs +53 -3
  22. package/agent/live-beat.mjs +49 -0
  23. package/agent/post-turn.mjs +4 -8
  24. package/agent/record-results.mjs +23 -20
  25. package/agent/relay-prefix.mjs +2 -2
  26. package/agent/run-stages.mjs +15 -11
  27. package/agent/run-start.mjs +111 -0
  28. package/agent/setup-reminders.mjs +9 -9
  29. package/agent/setup.mjs +32 -4
  30. package/agent/spawn-child.mjs +9 -9
  31. package/agent/suspension.mjs +89 -29
  32. package/agent/timers.mjs +113 -0
  33. package/agent/turn-loop.mjs +244 -0
  34. package/agent/write-gate.mjs +103 -6
  35. package/agent-tools/advisor-async.mjs +9 -9
  36. package/agent-tools/advisor-settle.mjs +12 -12
  37. package/agent-tools/advisor.mjs +36 -41
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +21 -18
  40. package/agent-tools/audit-block.mjs +7 -7
  41. package/agent-tools/bash-async.mjs +281 -0
  42. package/agent-tools/batch-lifecycle.mjs +57 -18
  43. package/agent-tools/batch-paths.mjs +52 -8
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +12 -65
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +56 -56
  49. package/agent-tools/context.mjs +4 -20
  50. package/agent-tools/design-token.mjs +3 -3
  51. package/agent-tools/digest-budget.mjs +1 -1
  52. package/agent-tools/eng.mjs +5 -5
  53. package/agent-tools/escalate-async.mjs +16 -11
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +2 -2
  56. package/agent-tools/parent-channel.mjs +3 -13
  57. package/agent-tools/plan.mjs +3 -4
  58. package/agent-tools/read-history.mjs +12 -28
  59. package/agent-tools/recent-changes.mjs +4 -5
  60. package/agent-tools/review-facts.mjs +1 -1
  61. package/agent-tools/settings.mjs +47 -20
  62. package/agent-tools/skill.mjs +3 -5
  63. package/agent-tools/spawn-gates.mjs +3 -3
  64. package/agent-tools/subagent-actions-query.mjs +270 -0
  65. package/agent-tools/subagent-actions.mjs +21 -265
  66. package/agent-tools/subagent-async.mjs +25 -20
  67. package/agent-tools/subagent-panel.mjs +33 -4
  68. package/agent-tools/subagent-run.mjs +36 -21
  69. package/agent-tools/subagent-scheduler.mjs +28 -27
  70. package/agent-tools/subagent-spawn.mjs +45 -14
  71. package/agent-tools/subagent.mjs +33 -53
  72. package/agent-tools/task.mjs +3 -9
  73. package/agent-tools/timer.mjs +12 -12
  74. package/agent-tools/verify.mjs +14 -13
  75. package/agent-tools.mjs +0 -1
  76. package/agent.mjs +18 -355
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +11 -6
  80. package/config-watch.mjs +83 -0
  81. package/config.mjs +17 -4
  82. package/context-degrade.mjs +81 -0
  83. package/context-echo.mjs +88 -0
  84. package/context-push.mjs +51 -0
  85. package/context.mjs +8 -187
  86. package/conventions.mjs +110 -161
  87. package/declaration.mjs +194 -0
  88. package/embedding.mjs +3 -1
  89. package/env-ref.mjs +81 -0
  90. package/explore-distill.mjs +2 -3
  91. package/file-links.mjs +53 -0
  92. package/file-refs.mjs +103 -0
  93. package/generate-title.mjs +28 -4
  94. package/history-window.mjs +17 -2
  95. package/i18n.mjs +2 -0
  96. package/index-discover.mjs +7 -7
  97. package/ledger-cmd.mjs +34 -126
  98. package/ledger-db.mjs +10 -2
  99. package/ledger-tools.mjs +188 -0
  100. package/ledger.mjs +9 -4
  101. package/log.mjs +12 -7
  102. package/manifest-discovery.mjs +131 -0
  103. package/manifest-schema.mjs +166 -0
  104. package/manifest.mjs +18 -217
  105. package/mcp.mjs +4 -1
  106. package/memory/code-search.mjs +149 -0
  107. package/memory/code-sync.mjs +89 -230
  108. package/memory/core.mjs +47 -32
  109. package/memory/docs.mjs +63 -262
  110. package/memory/file-list.mjs +89 -0
  111. package/memory/file-walk.mjs +7 -4
  112. package/memory/memory-tool.mjs +229 -0
  113. package/memory/schema.mjs +18 -3
  114. package/memory/sweep.mjs +289 -0
  115. package/memory/sync-tail.mjs +81 -0
  116. package/memory-status.mjs +69 -0
  117. package/memory.mjs +3 -0
  118. package/model-specs.mjs +16 -7
  119. package/notify-policy.mjs +51 -0
  120. package/package.json +1 -1
  121. package/peer-claims.mjs +2 -2
  122. package/peer-domains.mjs +7 -11
  123. package/peer-instances.mjs +9 -10
  124. package/process-probe-exec.mjs +187 -0
  125. package/process-probe.mjs +27 -175
  126. package/prompt-overlays.mjs +11 -10
  127. package/prompts/advisor-design.md +2 -2
  128. package/prompts/advisor-round2.md +1 -1
  129. package/prompts/advisor-round3.md +1 -1
  130. package/prompts/common.md +16 -8
  131. package/prompts/discipline-engineering.md +57 -16
  132. package/prompts/discipline-normal.md +6 -3
  133. package/prompts/persona-coder.md +0 -1
  134. package/prompts/persona-eng-coder.md +1 -5
  135. package/prompts/persona-eng-designer.md +2 -3
  136. package/prompts/persona-engineering.md +32 -11
  137. package/prompts/persona-explore.md +0 -1
  138. package/prompts/persona-normal.md +1 -1
  139. package/prompts/persona-plan.md +3 -4
  140. package/prompts/subagent-base.md +14 -0
  141. package/provider/core.mjs +11 -48
  142. package/provider/list-models.mjs +3 -0
  143. package/provider/responses-request.mjs +237 -0
  144. package/provider/responses.mjs +6 -228
  145. package/provider-flows.mjs +248 -0
  146. package/queued.mjs +90 -0
  147. package/rules.mjs +209 -2
  148. package/session-guard.mjs +27 -7
  149. package/session-lifecycle.mjs +90 -18
  150. package/session-rename.mjs +5 -1
  151. package/session-slot-claims.mjs +230 -0
  152. package/session-slot-scan.mjs +299 -0
  153. package/session-slot-verify.mjs +304 -0
  154. package/session-slot-write.mjs +74 -8
  155. package/session-slots-manifest.mjs +163 -241
  156. package/session-slots.mjs +99 -71
  157. package/session.mjs +30 -10
  158. package/shell-candidates.mjs +78 -0
  159. package/text-budget.mjs +2 -2
  160. package/think-off.mjs +49 -0
  161. package/token-ttl.mjs +15 -4
  162. package/tool-args.mjs +73 -0
  163. package/tool-docs/advisor.md +9 -0
  164. package/tool-docs/batch.md +1 -0
  165. package/tool-docs/code_search.md +1 -0
  166. package/tool-docs/consult_start.md +5 -0
  167. package/tool-docs/consult_stop.md +3 -0
  168. package/tool-docs/context.md +7 -0
  169. package/tool-docs/doc_search.md +1 -0
  170. package/tool-docs/edit.md +2 -13
  171. package/tool-docs/eng.md +1 -0
  172. package/tool-docs/execute.md +1 -9
  173. package/tool-docs/git.md +25 -51
  174. package/tool-docs/goal.md +6 -0
  175. package/tool-docs/ledger_add.md +1 -0
  176. package/tool-docs/ledger_close.md +1 -0
  177. package/tool-docs/ledger_count.md +1 -0
  178. package/tool-docs/ledger_query.md +1 -0
  179. package/tool-docs/ledger_update.md +1 -0
  180. package/tool-docs/main_history.md +1 -0
  181. package/tool-docs/memory.md +10 -0
  182. package/tool-docs/notify_parent.md +9 -0
  183. package/tool-docs/peer_instances.md +1 -0
  184. package/tool-docs/plan.md +5 -0
  185. package/tool-docs/process.md +7 -3
  186. package/tool-docs/read_history.md +7 -0
  187. package/tool-docs/recent_changes.md +1 -0
  188. package/tool-docs/repo_outline.md +1 -0
  189. package/tool-docs/settings.md +6 -0
  190. package/tool-docs/skill.md +5 -0
  191. package/tool-docs/subagent.md +31 -0
  192. package/tool-docs/task.md +7 -0
  193. package/tool-docs/timer.md +6 -0
  194. package/tool-docs/verify.md +1 -0
  195. package/tool-docs/wait_for.md +2 -1
  196. package/tools/bash.mjs +28 -4
  197. package/tools/edit-batch.mjs +4 -4
  198. package/tools/edit-diff.mjs +4 -23
  199. package/tools/exec-run.mjs +102 -4
  200. package/tools/execute.mjs +5 -2
  201. package/tools/file.mjs +4 -9
  202. package/tools/git-run.mjs +1 -1
  203. package/tools/git.mjs +0 -24
  204. package/tools/index.mjs +2 -1
  205. package/tools/linter.mjs +2 -1
  206. package/tools/node-child.mjs +21 -0
  207. package/tools/ops.mjs +50 -13
  208. package/tools/patch.mjs +4 -2
  209. package/tools/repomap.mjs +20 -9
  210. package/tools/shared.mjs +8 -6
  211. package/tools/web.mjs +5 -2
  212. package/tools/write-path.mjs +1 -1
  213. package/traces/trace-store.mjs +17 -14
  214. package/vision-reader.mjs +76 -0
  215. package/agent-tools/batch-segment.mjs +0 -18
package/file-links.mjs ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * file-links.mjs — 工具结果文本的**验存**文件链接抽取(上提核件 —— 处理流批 R2 · 2026-09-28)。
3
+ * 上提源 = VSC `thincoder-vscode/src/extension/file-links.mjs`(token 抽取 ∕ 去重 ∕ 封顶)× 桌面同档
4
+ * (相对径需项目根——无根 ⇒ 相对 token 零判据,不落 `process.cwd()` 第二解析基);本档 = 单源
5
+ * (消费面 = 桌面 `thincoder-desktop/src/main/file-links.mjs` · VSC `thincoder-vscode/src/extension/file-links.mjs`——已随
6
+ * parity-b4 迁移轮改指本档(2026-09-29,双写窗口收口))。
7
+ * 判据 = **盘上存在闸**(存在 ∧ 是文件):路径 token 只在真实存在时才成链接 —— URL ∕ 日志噪音 ∕
8
+ * 版本号恒不成链接(存在检查 = 最后一道闸)。**验存(`existsSync` ∕ `statSync`)转注入缝**:本档零
9
+ * `node:fs`——盘面探针由调用方注入(`probe = { existsSync, statSync }`);缺 ∕ 形违 ⇒ 抛(fail-loud——
10
+ * 存在闸未接线不得静默扮成「零链接」)。返回 `[{ raw, path, line }]`:`raw` = 含行后缀原文 · `path` =
11
+ * 盘上绝对路径 · `line` = 数 ∕ 缺 ⇒ `null`;零抛面 = fs 判据(探针逐条 try/catch:不可达 = 不成链接)。
12
+ */
13
+
14
+ import { isAbsolute, resolve } from "node:path"
15
+
16
+ /** 路径 token:可选 Windows 盘符 + 路径字符 + 点扩展名 + 可选 `:line` / `:line:col` 后缀;m[1] = 路径本体(供探针)、m[2] = 行号、m[0] = 渲染面包裹面 `raw`。 */
17
+ const PATH_TOKEN_RE = /((?:[A-Za-z]:)?[~./\\\w][\w./\\~-]*\.\w{1,10})(?::(\d+)(?::\d+)?)?/g
18
+
19
+ /** 逐结果封顶(值同源 = VSC 同档 `MAX_LINKS`)。 */
20
+ export const MAX_LINKS = 50
21
+
22
+ /**
23
+ * 工具结果文本 ⇒ 验存文件链接(去重 + 封顶)。`cwd` = 项目根(相对 token 的解析基);缺 ∕ 空 ⇒ 相对 token
24
+ * 一律不检(禁假链接);绝对 token 与 cwd 无关,照检。`probe` = 盘面探针注入缝(`{ existsSync(p), statSync(p) }`——
25
+ * `node:fs` 同名面;桌面端侧注入)。零文本(`""` ∕ 非串)先于探针校验早退(零判据面 —— 探针不检)。
26
+ */
27
+ export function extractFileLinks(cwd, text, probe) {
28
+ if (typeof text !== "string" || text === "") return []
29
+ if (typeof probe?.existsSync !== "function" || typeof probe?.statSync !== "function") {
30
+ throw new TypeError("extractFileLinks: probe seam missing — inject { existsSync, statSync } (the existence guard must not silently degrade)")
31
+ }
32
+ const base = typeof cwd === "string" && cwd !== "" ? cwd : null
33
+ const links = []
34
+ const seen = new Set()
35
+ for (const m of text.matchAll(PATH_TOKEN_RE)) {
36
+ const pathPart = m[1]
37
+ // 协议相对 URL 残体(`//example.com/a.png`)—— 恒非工作区路径。
38
+ if (pathPart.startsWith("//") || pathPart.startsWith("\\\\")) continue
39
+ const absolute = isAbsolute(pathPart)
40
+ if (!absolute && base === null) continue // 无项目根 ⇒ 相对 token 零判据(禁假链接)
41
+ const line = m[2] ? Number(m[2]) : null
42
+ const abs = absolute ? pathPart : resolve(base, pathPart)
43
+ const key = `${abs}:${line ?? ""}`
44
+ if (seen.has(key)) continue
45
+ try {
46
+ if (!probe.existsSync(abs) || !probe.statSync(abs).isFile()) continue
47
+ } catch { continue }
48
+ seen.add(key)
49
+ links.push({ raw: m[0], path: abs, line })
50
+ if (links.length >= MAX_LINKS) break
51
+ }
52
+ return links
53
+ }
package/file-refs.mjs ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * file-refs.mjs — @ 文件引用**注入 ∕ 剥离核件**(缺面族批补 #632 · 2026-09-29 上提)。
3
+ * 上提源 = VSC `thincoder-vscode/src/extension/file-refs.mjs` 自持档(逐字搬——regex ∕ 4000 截断 ∕
4
+ * `[File: …]` 围栏 ∕ 摘要块格式零语义改良);本档 = **单源**;两端各持薄壳(端侧 `node:fs` 探针注入):
5
+ * VSC `thincoder-vscode/src/extension/file-refs.mjs` ∥ 桌面 `thincoder-desktop/src/main/file-refs.mjs`。
6
+ * 消费面:VSC 注入 `panel-chat.mjs:187` ∕ 恢复剥离 `panel-session.mjs:185` ∕ 标题剥离
7
+ * `panel-session-write.mjs:139`;桌面注入缝 = `turn-driver.mjs`(起跑前单点,`turn-face.mjs`)∥
8
+ * 恢复剥离 = `session-slots.mjs` `pageHistory` ∥ 标题剥离 = 核 `generate-title.mjs:132`(读源处)。
9
+ * 文本形逐字冻结(`docs/vsc/design/WEBVIEW.md:154`「落线文本逐字不变」契约):上提不携任何行为面改动。
10
+ * 判据 = **盘面探针注入缝**(`probe = { readFileSync, existsSync, statSync }` —— `node:fs` 同名面;
11
+ * 缺 ∕ 形违 ⇒ 抛 —— fail-loud:盘面闸未接线不得静默扮成「零引用」,沿 `file-links.mjs:29-31` 先例)。
12
+ * `stripAtRefs` = 注入语法的逆变换:fail-closed 判据逐字依赖注入输出格式 —— 语法与其逆变换同住一处,
13
+ * 零第二实现(本档为唯一产者)。
14
+ */
15
+ import { join } from "node:path"
16
+
17
+ /** 探针三件(`node:fs` 同名面)——缺一即缝违。 */
18
+ const PROBE_KEYS = ["readFileSync", "existsSync", "statSync"]
19
+
20
+ /** 探针校验(fail-loud):三件皆须为函数 —— 缺 ∕ 形违 ⇒ 抛。 */
21
+ function probeOf(probe) {
22
+ if (PROBE_KEYS.some((key) => typeof probe?.[key] !== "function")) {
23
+ throw new TypeError("injectAtRefs: probe seam missing — inject { readFileSync, existsSync, statSync } (the disk probe must not silently degrade)")
24
+ }
25
+ return probe
26
+ }
27
+
28
+ /** Scan text for @path references and replace with inline file content.
29
+ * 零文本(非串 ∥ 空串)先于探针校验早退(零判据面——探针不检,同序沿核 `file-links.mjs:28`)。 */
30
+ export function injectAtRefs(text, cwd, probe) {
31
+ if (typeof text !== "string" || text === "") return text
32
+ const io = probeOf(probe)
33
+ const re = /@([^\s,;:()[\]{}"'`]+)/g
34
+ const refs = []
35
+ let m
36
+ while ((m = re.exec(text)) !== null) {
37
+ const raw = m[1]
38
+ // Skip things that aren't paths (emails, mentions like @user)
39
+ if (!/^[a-zA-Z0-9_./\\-]+$/.test(raw)) continue
40
+ if (raw.includes("@")) continue
41
+ // Resolve relative to cwd
42
+ const abs = join(cwd, raw)
43
+ if (!io.existsSync(abs)) continue
44
+ try {
45
+ const stat = io.statSync(abs)
46
+ if (!stat.isFile()) continue
47
+ let content = io.readFileSync(abs, "utf8")
48
+ if (content.length > 4000) content = content.slice(0, 4000) + "\n... (truncated)"
49
+ refs.push({ raw, abs, content })
50
+ } catch (e) {
51
+ console.error(`[file-refs] failed to read @${raw}:`, e.message)
52
+ }
53
+ }
54
+ if (refs.length === 0) return text
55
+
56
+ // Replace @refs in the text with inline content blocks
57
+ for (const ref of refs) {
58
+ const placeholder = `@${ref.raw}`
59
+ const injection = `[File: ${ref.raw}]\n\`\`\`\n${ref.content}\n\`\`\``
60
+ text = text.split(placeholder).join(injection)
61
+ }
62
+
63
+ // Append a summary of referenced files
64
+ const list = refs.map(r => ` - ${r.raw} (${r.content.length} chars)`).join("\n")
65
+ text += `\n\n[Referenced files:\n${list}\n]`
66
+
67
+ return text
68
+ }
69
+
70
+ /**
71
+ * 复原注入前的用户原文(F-W15 端侧显示边界剥离 · D-W17)。
72
+ *
73
+ * 契约:同一会话中用户消息的**恢复面**文本 ≡ 活面气泡文本(= 用户所打原文,`@路径` 保持简洁形)。
74
+ * 与产者同档(本档 = 注入语法的唯一产者——语法与其逆变换同住一处,零第二实现)。
75
+ *
76
+ * 剥离判据(fail-closed):仅当文本尾部为注入**摘要块**(`[Referenced files:` 头 +
77
+ * 逐行 ` - <raw> (<N> chars)` + 尾 `]`)时,按「`[File: <raw>]` 头 + 围栏 + 恰 N 字符正文 +
78
+ * 收尾围栏 ⇒ `@<raw>`」逐条还原并删摘要块;**任一条不吻合 ⇒ 整条原样返回**(用户手打的
79
+ * `[File: x]` / 形近文本零误伤)。落线 / 机读线(模型输入)零触碰——本函数只服务显示边界。
80
+ */
81
+ export function stripAtRefs(text) {
82
+ const s = typeof text === "string" ? text : ""
83
+ const tail = /(?:^|\n\n)\[Referenced files:\n([\s\S]*)\n\]$/.exec(s)
84
+ if (!tail) return s
85
+ const entries = []
86
+ for (const line of tail[1].split("\n")) {
87
+ const m = /^ {2}- (.*) \((\d+) chars\)$/.exec(line)
88
+ if (!m) return s // 摘要块形近(行不吻合)⇒ 原样返回
89
+ entries.push({ raw: m[1], len: Number(m[2]) })
90
+ }
91
+ if (entries.length === 0) return s
92
+ let head = s.slice(0, tail.index) // 删摘要块(含其前空行)
93
+ for (const { raw, len } of entries) {
94
+ const open = `[File: ${raw}]\n\`\`\`\n`
95
+ const at = head.indexOf(open)
96
+ if (at < 0) return s // 头部缺 / 不对应 ⇒ 原样返回
97
+ const bodyStart = at + open.length
98
+ const bodyEnd = bodyStart + len
99
+ if (head.slice(bodyEnd, bodyEnd + 4) !== "\n\`\`\`") return s // 字符数不符 ⇒ 原样返回
100
+ head = head.slice(0, at) + `@${raw}` + head.slice(bodyEnd + 4)
101
+ }
102
+ return head
103
+ }
@@ -9,9 +9,17 @@
9
9
  */
10
10
 
11
11
  import { proxyFetch } from "./proxy.mjs"
12
+ import { resolveProviderSecrets } from "./env-ref.mjs"
12
13
  // D7(2026-09-21 块标题行对齐批——标题链单源):首条真实 user 消息谓词收核单源
13
14
  // (`isRealUserMsg`——纯函数零依赖,无环)。
14
15
  import { isRealUserMsg } from "./history-window.mjs"
16
+ // #632(缺面族批补 · @ 文件引用对齐 · 2026-09-29):标题源剥离单源——注入形不得进标题请求(核件
17
+ // `file-refs.mjs`;CLI 无注入 ⇒ 零动作;VSC 同位转口 = `panel-session-write.mjs:139`)。
18
+ import { stripAtRefs } from "./file-refs.mjs"
19
+ // #363③(off 形族单源——MODEL-SPECS.md §16.2 / §16.4):禁思考形与「是否可禁」一律引核
20
+ // (`thinkOffShape` / `thinkOffPath`——本档零副本);spec 查表同单源(`specForModel`)。
21
+ import { specForModel } from "./model-specs.mjs"
22
+ import { thinkOffShape, thinkOffPath } from "./think-off.mjs"
15
23
 
16
24
  // Test seam (_-prefix, mirrors run.mjs seams): lets the proxy-branch regression
17
25
  // test swap the proxy fetch. The branch it exercises used to carry a dynamic
@@ -31,7 +39,16 @@ export async function generateTitle(userContent, provider) {
31
39
  if (typeof userText !== "string" || userText.length < 10) return null
32
40
  if (!provider?.apiKey || !provider?.baseURL || !provider?.model) return null
33
41
 
42
+ // #363③:禁思考形(三格式支共用)——off 取形与可宣称性引核单源:`thinkOffPath` 假
43
+ // (thinkAlwaysOn 族)或 `thinkOffShape` 为 `null`(effort 族——`null` 形是核载荷门的
44
+ // 约定,本档无门)⇒ 不发禁形(假标记:被服务端拒 / 静默失效)。
45
+ const spec = specForModel(provider.model)
46
+ const offShape = thinkOffPath(spec) ? thinkOffShape(spec) : null
47
+
34
48
  try {
49
+ // #57(provider-config-family 批):凭据值位 `${env:VAR}` 消费侧解析(CONFIG.md §6.3)——解析抛错
50
+ // 落本函数既有非致命兜底(下方 catch ⇒ 标题回落 null——不破坏回合,与「无 key 不生成标题」同形)。
51
+ provider = resolveProviderSecrets(provider)
35
52
  const system = "Generate a concise title (max 40 chars, no quotes) for this conversation. Reply ONLY with the title."
36
53
  const text = userText.slice(0, 200)
37
54
  let url, body, extract
@@ -43,7 +60,7 @@ export async function generateTitle(userContent, provider) {
43
60
  messages: [{ role: "user", content: text }],
44
61
  max_tokens: MAX_TITLE_TOKENS,
45
62
  stream: false,
46
- thinking: { type: "disabled" }, // IK9UZ8:不让 reasoning 吃掉输出预算
63
+ ...(offShape !== null ? { thinking: offShape } : {}), // IK9UZ8:不让 reasoning 吃掉输出预算
47
64
  })
48
65
  extract = (data) => data.content?.map((b) => b.text || "").join("")
49
66
  } else if (provider.format === "google") {
@@ -51,7 +68,11 @@ export async function generateTitle(userContent, provider) {
51
68
  body = JSON.stringify({
52
69
  systemInstruction: { parts: [{ text: system }] },
53
70
  contents: [{ role: "user", parts: [{ text }] }],
54
- generationConfig: { maxOutputTokens: MAX_TITLE_TOKENS, thinkingConfig: { thinkingLevel: "none" } }, // IK9UZ8
71
+ generationConfig: {
72
+ maxOutputTokens: MAX_TITLE_TOKENS,
73
+ // google 格式自身的关思考形(wire 原生——非族别取形副本);无有效 off 路径 ⇒ 零发
74
+ ...(offShape !== null ? { thinkingConfig: { thinkingLevel: "none" } } : {}),
75
+ }, // IK9UZ8
55
76
  })
56
77
  extract = (data) => data.candidates?.[0]?.content?.parts?.map((p) => p.text || "").join("")
57
78
  } else {
@@ -65,7 +86,9 @@ export async function generateTitle(userContent, provider) {
65
86
  // leave content empty (IK9UZ8). Providers that don't accept the field ignore it
66
87
  // (OpenAI-compatible convention). A 40-char title wants ~60–80 tokens, so 100 is
67
88
  // ~2.5x headroom (design decision — docs/core/design/SESSION.md §6.7).
68
- thinking: { type: "disabled" },
89
+ // #363③:形自核单源(`thinkOffShape`——type 族 = `{type:"disabled"}`;无有效 off 路径
90
+ // 族不发——见上方 offShape 取形)。
91
+ ...(offShape !== null ? { thinking: offShape } : {}),
69
92
  max_tokens: MAX_TITLE_TOKENS,
70
93
  stream: false,
71
94
  }
@@ -113,7 +136,8 @@ export async function ensureSessionTitle(agent) {
113
136
  let firstUser = agent._recordStore?.firstUserMessage?.() ?? null
114
137
  if (!firstUser) firstUser = (agent._fullHistory ?? agent.history).find(isRealUserMsg)
115
138
  if (firstUser) {
116
- const title = await generateTitle(firstUser.content, agent.provider)
139
+ // #632:读源处剥离(注入形不得入标题请求——与恢复面同读一函数,零第二实现)
140
+ const title = await generateTitle(stripAtRefs(firstUser.content), agent.provider)
117
141
  if (title) agent.title = title
118
142
  }
119
143
  } catch {
@@ -9,6 +9,11 @@
9
9
  *
10
10
  * 承接:会话数据面的「人读线」窗口切分(分段恢复 / 逐页回看)——与磁盘会话档
11
11
  * 同源(`{hash}.json.N`),本档只读数组、不触盘。
12
+ *
13
+ * 留档记录直通(opt-in · 消化面留档批 · #719):`digest` ∕ `subagent` 两型记录
14
+ * 原样入窗(携 `idx`;字段零改名);默认关 ⇒ 输出与改前逐字等价(CLI ∕ VSC 读面
15
+ * 零改零破)——`kindOf` 认 `kind` 键;形 / 在场 / 判据单源 =
16
+ * `docs/desktop/design/RENDERER.md` §1.1「留档记录」条。
12
17
  */
13
18
 
14
19
  /** Real user message predicate (shared by the slot metadata digest and the window).
@@ -32,7 +37,7 @@ function tsOf(m) {
32
37
  }
33
38
 
34
39
  function kindOf(m) {
35
- return m?.type ?? m?.role
40
+ return m?.type ?? m?.role ?? m?.kind
36
41
  }
37
42
 
38
43
  function toolCallsOf(m) {
@@ -103,10 +108,16 @@ function pairToolEntries(history) {
103
108
  * takes the LAST page (first paint); otherwise the page [s, e) ending just before
104
109
  * `before`. Half-open [s, e) keeps loadOlder pages from re-rendering the boundary
105
110
  * message. `idx` values are GLOBAL history indexes — pagination never renumbers.
111
+ *
112
+ * `opts.records === true`(opt-in —— 留档批 · #719)⇒ `digest` ∕ `subagent` 两型
113
+ * 记录原样入窗(`{ ...record, idx }`,字段零改名)并占条目位(游标按条目推进);
114
+ * 记录不参与 turn ∕ 可见性谓词(`visible` 面语义不动)。**默认关** ⇒ 输出与改前
115
+ * 逐字等价(负控腿);缺 `opts` ∕ 非 `true` ⇒ 同默认径。
106
116
  */
107
- export function historyWindow(history, before, pageSize = HISTORY_PAGE_SIZE) {
117
+ export function historyWindow(history, before, pageSize = HISTORY_PAGE_SIZE, opts = {}) {
108
118
  const total = Array.isArray(history) ? history.length : 0
109
119
  if (total === 0) return { messages: [], hasOlder: false }
120
+ const records = opts?.records === true
110
121
  const { frameResults, ownerOf } = pairToolEntries(history)
111
122
  const end = before == null ? total : Math.max(0, Math.min(before, total))
112
123
  const start = Math.max(0, end - pageSize)
@@ -161,6 +172,10 @@ export function historyWindow(history, before, pageSize = HISTORY_PAGE_SIZE) {
161
172
  turnStart: prevVisibleKind === null || prevVisibleKind === "user",
162
173
  tools,
163
174
  })
175
+ } else if (records && (kind === "digest" || kind === "subagent")) {
176
+ // 留档记录直通(opt-in):人读线条目原样入窗 + 携位次(字段零改名 —— 形单源 =
177
+ // `docs/desktop/design/RENDERER.md` §1.1「留档记录」条)
178
+ messages.push({ ...m, idx: i })
164
179
  } else if (kind === "tool") {
165
180
  // A consumed tool entry produces no message of its own (rendered with its frame) —
166
181
  // only a true orphan falls back to the top level (rules 3/4)
package/i18n.mjs CHANGED
@@ -46,6 +46,8 @@ export const CORE_MESSAGES = Object.freeze({
46
46
  "status.rateLimited": { en: "Rate-limited 429, retry in ${s}s", zh: "限流 429,${s}s 后重试" },
47
47
  "status.overloaded": { en: "Server overloaded, retrying in ${s}s", zh: "服务过载,${s}s 后重试" },
48
48
  "status.quota": { en: "quota exhausted: ${msg}", zh: "配额耗尽:${msg}" },
49
+ // ── 停滞轻显形(在飞回合静默读数——TUI.md §7.7 · stall-indicator 批 2026-09-29)──
50
+ "status.quiet": { en: "Quiet ${s}s", zh: "已静默 ${s}s" },
49
51
  // ── 异步池状态词 ──
50
52
  "sub.running": { en: "running", zh: "运行中" },
51
53
  "sub.queued": { en: "queued", zh: "排队中" },
@@ -8,14 +8,14 @@
8
8
  * PORTABILITY VSC mirror · VP-9 (P9): the extension tables were 14 code + 5 doc
9
9
  * entries with no declaration surface — a project in any other mainstream language
10
10
  * was invisible AND undeclarable, silently. Now: the tables carry the CLI-aligned
11
- * additions, `.thincoder/conventions.json` (index.codeExtensions / index.docExtensions)
12
- * unions in project-specific extensions, and files whose extension is in NO list are
11
+ * additions, the manifest's `index.codeExtensions` / `index.docExtensions` union in
12
+ * project-specific extensions, and files whose extension is in NO list are
13
13
  * TALLIED (`collectUnlisted`) so "my .xyz files are not searchable" stops being
14
14
  * invisible (buildIndex → panel hint + a log event).
15
15
  */
16
16
  import { readdirSync } from "node:fs"
17
17
  import { join, relative } from "node:path"
18
- import { DEFAULT_CONVENTIONS, loadConventions } from "./conventions.mjs"
18
+ import { DEFAULT_DECLARATION, loadProjectDeclaration } from "./conventions.mjs"
19
19
 
20
20
  const CODE_EXTS = new Set([
21
21
  ".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx", ".py", ".rs", ".go", ".java", ".c", ".cpp", ".h", ".hpp",
@@ -36,11 +36,11 @@ function extensionOf(p) {
36
36
  }
37
37
 
38
38
  /** Effective extension sets for one conventions object: defaults ∪ declared
39
- * (declaration is a UNION — §4.1: index.*Extensions APPEND). Cached per frozen
39
+ * (declaration is a UNION — §3.1: index.*Extensions APPEND). Cached per frozen
40
40
  * conventions object (the loader hands out stable per-root instances). */
41
41
  const _knownCache = new WeakMap()
42
42
  function knownExtensions(conv) {
43
- const key = conv ?? DEFAULT_CONVENTIONS
43
+ const key = conv ?? DEFAULT_DECLARATION
44
44
  let hit = _knownCache.get(key)
45
45
  if (!hit) {
46
46
  const code = new Set(CODE_EXTS)
@@ -126,7 +126,7 @@ function isIndexableName(name, allExts) {
126
126
  * files array (backward-compatible with needsRebuild / the memory self-check).
127
127
  */
128
128
  export function discoverFiles(cwd, signal, { collectUnlisted = false } = {}) {
129
- const res = collectFiles(cwd, cwd, false, signal, loadConventions(cwd), collectUnlisted)
129
+ const res = collectFiles(cwd, cwd, false, signal, loadProjectDeclaration(cwd), collectUnlisted)
130
130
  return collectUnlisted ? res : res.files
131
131
  }
132
132
 
@@ -135,7 +135,7 @@ export function discoverFiles(cwd, signal, { collectUnlisted = false } = {}) {
135
135
  export function discoverFilesUnder(cwd, subdir, { collectUnlisted = false } = {}) {
136
136
  const base = String(subdir ?? "").replaceAll("\\", "/").replace(/\/+$/, "")
137
137
  if (!base) return collectUnlisted ? { files: [], unlisted: { count: 0, exts: [] } } : []
138
- const res = collectFiles(join(cwd, base), cwd, false, undefined, loadConventions(cwd), collectUnlisted)
138
+ const res = collectFiles(join(cwd, base), cwd, false, undefined, loadProjectDeclaration(cwd), collectUnlisted)
139
139
  return collectUnlisted ? res : res.files
140
140
  }
141
141
 
package/ledger-cmd.mjs CHANGED
@@ -1,15 +1,20 @@
1
1
  /**
2
- * ledger-cmd.mjs — 台账命令族(查询 = 只读全角色 / 写 = 仅主 agent 装配)+ 工具定义
3
- * (M2 模块拆分件——设计档 docs/core/design/modules/ENGINEERING-MODE-V2-MODULE-LEDGER.md §2.3 拆分计划)。
2
+ * ledger-cmd.mjs — 台账核心函数档(查询 = 只读全角色 / 写 = 仅主 agent 装配)。
3
+ * 工具定义 + 工具层参数守卫 = 拆分件 `thincoder-core/ledger-tools.mjs`(2026-09-28 守卫族微修批 ·
4
+ * 台账 #473——纯搬移、语义零变;拆分触发 = 本档 298 行贴 300 顾问线,本批读面守卫增量必越线);
5
+ * 核函数(`ledgerQuery` / `ledgerCount` / `ledgerAdd` / `ledgerUpdate` / `ledgerClose`)留本档,
6
+ * 工具档消费之。
4
7
  *
5
- * 接线(设计档 §2.2 命令面接线):查询命令 ← tools/index.mjs(assembleBuiltinTools 全角色面);
6
- * 写命令 ← agent/family-tools.mjs(assembleFamilyTools 的 depthOnly 分支)——一律**动态 import** 本模块链
7
- * (ledger-db.mjs 静态 import node:sqlite ⇒ 消费侧静态 import 会破 W8 契约②)。
8
+ * 接线(设计档 §2.2 命令面接线):族出口 = `ledger.mjs`(re-export 两源——KD-M2-3);查询命令 ←
9
+ * tools/index.mjs(assembleBuiltinTools 全角色面);写命令 ← agent/family-tools.mjs(assembleFamilyTools
10
+ * 的 depthOnly 分支)——一律**动态 import** 本模块链(ledger-db.mjs 静态 import node:sqlite ⇒
11
+ * 消费侧静态 import 会破 W8 契约②)。
8
12
  */
9
13
  import { statSync } from "node:fs"
10
- import { resolve } from "node:path"
14
+ import { basename, resolve } from "node:path"
11
15
  import { ALLOWED_MIGRATIONS, nowIso, openLedger, PENDING_STATUSES } from "./ledger-db.mjs"
12
16
  import { resolveProjectRoot } from "./manifest.mjs"
17
+ import { declaredPublicRoots } from "./declaration.mjs"
13
18
 
14
19
  /** 查询(只读,全角色):SELECT 行集(status/kind/board 过滤,缺省 = 全部行;id 升序)。库不在 = 空账。 */
15
20
  export function ledgerQuery({ cwd, status = null, kind = null, board = null } = {}) {
@@ -37,21 +42,31 @@ export function ledgerCount({ cwd } = {}) {
37
42
  /** 路径 = 存在的**档**?(目录 / 缺失 → false——写门存在性判据用)。 */
38
43
  const isFile = (p) => { try { return statSync(p).isFile() } catch { return false } }
39
44
 
40
- /** 写门·指针存在性(设计档 §6.1 · 台账 #38 · AC-M2-9):写命令落盘前判**结果行**——`status ∈
41
- * {在途, 待核销}` 且 `task_book` 非空 ⇒ 文件部分(首个 `§` 前子串,trim)须经基准 `resolve(base, …)`
42
- * 指向存在的档;缺文件部分(`§2` / 全空白)⇒ 拒;不在册 / 非文件 ⇒ 拒(throw,行不变)。
45
+ /** 写门·指针存在性(设计档 §6.1 · 台账 #38 · AC-M2-10):写命令落盘前判**结果行**——`status ∈
46
+ * {在途, 待核销}` ⇒ 判 `task_book`:`null`(缺指针)⇒ 拒(「必填」文案);非 `null` ⇒ 文件部分
47
+ * (首个 `§` 前子串,trim)须经基准 `resolve(base, …)` 指向存在的档;缺文件部分(`§2` / 空串 /
48
+ * 全空白)⇒ 拒;不在册 / 非文件 ⇒ 拒(throw,行不变)。
43
49
  * 判位与迁移表判同层(落盘前);文案前缀 = 调用函数名(与同函数既有两条文案同款)。
44
50
  * 基准 `base` = **与台账库关联键同源**(2026-09-18 fix 轮 · O1 收正):`resolveProjectRoot(cwd) ??
45
51
  * resolve(cwd ?? ".")`——同表达式见 `ledger-db.mjs` `ledgerDbPath`;容器根会话(锚 cwd 下唯一注册
46
- * 子仓 P)与子仓会话 ⇒ 库键与指针基准同取 P(同库同基准;原实现 `resolve(cwd, …)` = 原始 cwd 会误拒)。 */
52
+ * 子仓 P)与子仓会话 ⇒ 库键与指针基准同取 P(同库同基准;原实现 `resolve(cwd, …)` = 原始 cwd 会误拒)。
53
+ * #832:声明源前缀形同判(§6.15——首段 = 声明公共仓目录名 ⇒ 于该声明根解析;可核 ⇒ 通过;缺位拒)。 */
47
54
  function assertTaskBookGate(cwd, fnName, status, taskBook) {
48
55
  if (status !== "在途" && status !== "待核销") return
49
- if (taskBook == null || taskBook === "") return
56
+ if (taskBook == null) throw new Error(`${fnName}:task_book 必填(在途 / 待核销 须携任务书指针)`)
50
57
  const part = String(taskBook).split("§")[0].trim()
51
58
  if (!part) throw new Error(`${fnName}:task_book 不可解析(缺文件部分):${taskBook}`)
52
59
  const base = resolveProjectRoot(cwd) ?? resolve(cwd ?? ".")
53
60
  const abs = resolve(base, part)
54
- if (!isFile(abs)) throw new Error(`${fnName}:task_book 指向的档不存在:${taskBook}(解析 = ${abs})`)
61
+ // §6.15 声明源候选(#832 · LEDGER.md §6.1-4):首段 = 声明公共仓目录名 ⇒ 该声明根解析余段(同名多仓 ⇒ 声明序首者);未声明 ∥ 缺位 ⇒ 照旧拒。
62
+ if (isFile(abs)) return
63
+ const segs = part.replace(/\\/g, "/").split("/").filter(Boolean)
64
+ for (const root of declaredPublicRoots(base)) {
65
+ if (segs[0] !== basename(root)) continue
66
+ if (segs.length > 1 && isFile(resolve(root, segs.slice(1).join("/")))) return
67
+ break
68
+ }
69
+ throw new Error(`${fnName}:task_book 指向的档不存在:${taskBook}(解析 = ${abs})`)
55
70
  }
56
71
 
57
72
  /** 新增(写命令,仅主 agent):INSERT——入待讨论(六态状态机入口)。 */
@@ -101,7 +116,9 @@ export function ledgerUpdate({ cwd, id, patch, executorSessionId }) {
101
116
  } finally { db.close() }
102
117
  }
103
118
 
104
- /** 收口(写命令,仅主 agent):勾销(待核销 → 已核销)/ 撤回(任意态 → 已废弃),事务包裹;归档 = 软删除。 */
119
+ /** 收口(写命令,仅主 agent):核销两源(勾销:待核销 → 已核销;追认核销:待讨论 / 待设计 → 已核销,
120
+ * 行 `evidence` 非空,缺 / 全空白 ⇒ 拒)/ 撤回(任意态 → 已废弃),事务包裹;归档 = 软删除。
121
+ * 判序(同函数体同层):目标集判 → 行取 → 源态判 → `evidence` 门 → UPDATE(LEDGER.md §3 收口两源)。 */
105
122
  export function ledgerClose({ cwd, id, status }) {
106
123
  if (status !== "已核销" && status !== "已废弃") throw new Error(`ledgerClose:目标态 ${status} ∉ {已核销, 已废弃}`)
107
124
  const db = openLedger(cwd, { create: true })
@@ -110,123 +127,14 @@ export function ledgerClose({ cwd, id, status }) {
110
127
  try {
111
128
  const row = db.prepare("SELECT * FROM items WHERE id = ?").get(id)
112
129
  if (!row) throw new Error(`ledgerClose:行 ${id} 不存在`)
113
- if (status === "已核销" && row.status !== "待核销") throw new Error(`ledgerClose:勾销仅限待核销(现态 ${row.status})`)
130
+ // 判序:源态判(核销仅三源——在途不可跳)→ 追认口 `evidence` 门(行字段非空,缺 / 全空白 ⇒ 拒)
131
+ if (status === "已核销" && !["待讨论", "待设计", "待核销"].includes(row.status)) throw new Error(`ledgerClose:核销仅限 待讨论 / 待设计 / 待核销(现态 ${row.status})`)
132
+ if (status === "已核销" && ["待讨论", "待设计"].includes(row.status) && (row.evidence == null || String(row.evidence).trim() === "")) throw new Error(`ledgerClose:追认核销须带 evidence(现态 ${row.status})`)
114
133
  const now = nowIso()
115
- // 撤回(任意态 → 已废弃——含在途直撤)同步 executor = NULL(LEDGER.md §3.1 ④);勾销路径 executor 已在离场迁移清空,零触碰
134
+ // 撤回(任意态 → 已废弃——含在途直撤)同步 executor = NULL(LEDGER.md §3.1 ④);核销两源(勾销 / 追认)executor 零触碰(非在途出边)
116
135
  db.prepare("UPDATE items SET status = ?, closed_at = ?, updated_at = ?, executor = ? WHERE id = ?").run(status, now, now, status === "已废弃" ? null : row.executor, id)
117
136
  db.exec("COMMIT")
118
137
  return { id, status }
119
138
  } catch (e) { db.exec("ROLLBACK"); throw e }
120
139
  } finally { db.close() }
121
140
  }
122
-
123
- // ── 工具定义(接线:查询 → tools/index.mjs 全角色面;写 → family-tools.mjs depthOnly 分支) ──
124
-
125
- /** 工具 cwd 供值面(缺省 → 当前项目根)。 */
126
- const cwdOf = (ctx) => ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
127
-
128
- export const ledgerQueryTool = {
129
- name: "ledger_query",
130
- description: "台账查询(需求池 / 技术待办)——SQLite 行集,只读、全角色可用。未决四态 = 待讨论 / 待设计 / 在途 / 待核销;已核销 / 已废弃 = 归档态(软删除,行保留)。过滤参数缺省 = 全部行。",
131
- parameters: {
132
- type: "object",
133
- properties: {
134
- cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
135
- status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "按状态过滤(六态之一,缺省 = 不过滤)" },
136
- kind: { type: "string", enum: ["requirement", "tech_todo"], description: "按类别过滤" },
137
- board: { type: "string", description: "按归属板块过滤(需求档文档名)" },
138
- },
139
- },
140
- readonly: true,
141
- async execute(args, ctx) {
142
- return JSON.stringify(ledgerQuery({ cwd: args.cwd ?? cwdOf(ctx), status: args.status, kind: args.kind, board: args.board }), null, 2)
143
- },
144
- }
145
-
146
- export const ledgerCountTool = {
147
- name: "ledger_count",
148
- description: "台账计数单源:未决四态(待讨论 / 待设计 / 在途 / 待核销)COUNT(*),只读、全角色可用。",
149
- parameters: {
150
- type: "object",
151
- properties: { cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" } },
152
- },
153
- readonly: true,
154
- async execute(args, ctx) {
155
- return JSON.stringify({ count: ledgerCount({ cwd: args.cwd ?? cwdOf(ctx) }) })
156
- },
157
- }
158
-
159
- export const ledgerAddTool = {
160
- name: "ledger_add",
161
- description: "台账新增条目(写命令,仅主 agent)——入待讨论(六态状态机入口)。kind = requirement(需求池)/ tech_todo(技术待办);trigger = 归批 / 条件 / 认账不排期(可空)。",
162
- parameters: {
163
- type: "object",
164
- required: ["kind", "title"],
165
- properties: {
166
- cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
167
- kind: { type: "string", enum: ["requirement", "tech_todo"], description: "类别" },
168
- title: { type: "string", description: "条目标题" },
169
- board: { type: "string", description: "归属板块(需求档文档名,可空)" },
170
- req_doc: { type: "string", description: "需求档指针(可空)" },
171
- task_book: { type: "string", description: "任务书指针(可空;在途 / 待核销必填——咬合 CHECK)" },
172
- evidence: { type: "string", description: "最小证据行(可空)" },
173
- trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "技术待办触发(可空)" },
174
- },
175
- },
176
- readonly: false,
177
- async execute(args, ctx) {
178
- const { cwd, ...row } = args
179
- const id = ledgerAdd({ cwd: cwd ?? cwdOf(ctx), row })
180
- return JSON.stringify({ id, status: "待讨论" })
181
- },
182
- }
183
-
184
- export const ledgerUpdateTool = {
185
- name: "ledger_update",
186
- description: "台账更新条目(写命令,仅主 agent)——状态迁移按六态允许迁移表:待讨论→待设计 / 待设计→在途 / 在途→待核销 / 待核销→已核销 / 任意态→已废弃;表外迁移被拒。status 缺省 = 仅更新字段。",
187
- parameters: {
188
- type: "object",
189
- required: ["id"],
190
- properties: {
191
- cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
192
- id: { type: "number", description: "条目 id" },
193
- status: { type: "string", description: "目标状态(六态之一,缺省 = 不变)" },
194
- title: { type: "string", description: "标题(缺省 = 不变)" },
195
- board: { type: "string", description: "归属板块(缺省 = 不变)" },
196
- req_doc: { type: "string", description: "需求档指针(缺省 = 不变)" },
197
- task_book: { type: "string", description: "任务书指针(缺省 = 不变)" },
198
- evidence: { type: "string", description: "证据(缺省 = 不变)" },
199
- trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "触发(缺省 = 不变)" },
200
- executor: { type: "string", description: "执行者 sessionId(可选——接手改写归属用;缺省 = 按状态迁移语义:进在途自动写本会话 / 出在途自动清空 / 其余不变)" },
201
- },
202
- },
203
- readonly: false,
204
- async execute(args, ctx) {
205
- const { cwd, id, ...patch } = args
206
- let executorSessionId
207
- if (patch.executor === undefined) {
208
- // 动态 import——零新静态边(K-LX1);工具层供值,核心函数保持参数注入纯函数
209
- const { getSessionId } = await import("./session-slots.mjs")
210
- executorSessionId = getSessionId()
211
- }
212
- return JSON.stringify(ledgerUpdate({ cwd: cwd ?? cwdOf(ctx), id, patch, executorSessionId }))
213
- },
214
- }
215
-
216
- export const ledgerCloseTool = {
217
- name: "ledger_close",
218
- description: "台账收口(写命令,仅主 agent)——勾销:待核销 → 已核销;撤回:任意态 → 已废弃。归档 = 软删除(写 closed_at,行保留)。",
219
- parameters: {
220
- type: "object",
221
- required: ["id", "status"],
222
- properties: {
223
- cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
224
- id: { type: "number", description: "条目 id" },
225
- status: { type: "string", enum: ["已核销", "已废弃"], description: "目标态(勾销 / 撤回)" },
226
- },
227
- },
228
- readonly: false,
229
- async execute(args, ctx) {
230
- return JSON.stringify(ledgerClose({ cwd: args.cwd ?? cwdOf(ctx), id: args.id, status: args.status }))
231
- },
232
- }
package/ledger-db.mjs CHANGED
@@ -18,7 +18,7 @@ import { join, resolve } from "node:path"
18
18
  import { DatabaseSync } from "node:sqlite"
19
19
 
20
20
  import { configDir } from "./config-io.mjs"
21
- import { resolveProjectRoot } from "./manifest.mjs"
21
+ import { projectRootView, resolveProjectRoot } from "./manifest.mjs"
22
22
  // 归一步单源(KD-LN1——直引,不抽新 util:先例 = peer-domains.mjs / traces/trace-store.mjs)。
23
23
  import { normalizeCwd } from "./session-slots.mjs"
24
24
 
@@ -102,9 +102,17 @@ export function ensureExecutorColumn(db, { exists } = {}) {
102
102
 
103
103
  /** 开库(cwd = 项目根——仅作关联键)→ DatabaseSync 句柄 + 幂等建表 + 老库幂等迁移(executor 列)。
104
104
  * 读面(create=false):库文件不存在 → 返回 null(空账——读面不建库、无副作用)。
105
- * 写面(create=true):库文件不存在自动建(台账目录随之创建);项目根目录不存在 → 抛友好错误。 */
105
+ * 写面(create=true):库文件不存在自动建(台账目录随之创建);项目根目录不存在 → 抛友好错误。
106
+ * **歧义锚**(≥2 候选——`projectRootView` `ambiguous` 态)⇒ **显式拒**(列候选 + 显式项目根指引;
107
+ * 零写——不静默回退锚;族锚「项目不可解析」,AC-M2-17);兜底 `resolve(cwd ?? ".")` 只属 none 态。 */
106
108
  export function openLedger(cwd, { create = false } = {}) {
107
109
  const dir = resolve(cwd ?? ".")
110
+ // 歧义锚 ⇒ 显式拒(零写——消「缺省空读 / 错写落锚」双面症;读 / 写五工具同经本门)。
111
+ const view = projectRootView(dir)
112
+ if (view.state === "ambiguous") {
113
+ const list = view.candidates.map((c) => `- ${c}`).join("\n")
114
+ throw new Error(`项目不可解析:cwd ${dir} 下候选项目不是恰好一个(下列 ${view.candidates.length} 个)——台账按项目根键控,请显式给出项目根 cwd:\n${list}`)
115
+ }
108
116
  const file = ledgerDbPath(dir)
109
117
  if (!existsSync(dir)) {
110
118
  throw new Error(`项目目录不存在:${dir}——cwd 请给存在的项目根(相对路径按当前工作目录解析;缺省 = 会话项目根)`)