@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
@@ -372,7 +372,7 @@ export async function runSingleEdit(args, ctx) {
372
372
  op: "write",
373
373
  record: { type: "edit", startLine: out.editStartLine, shift: out.lineShift },
374
374
  })
375
- const diff = gitDiffOne(ctx.cwd, abs)
375
+ const diff = await gitDiffOne(ctx.cwd, abs)
376
376
  const baseResult = out.deleted
377
377
  ? `Deleted ${deleteTarget(out)} of ${args.path}${diff ? "\n" + diff : ""}${await autoSyntaxCheck(abs)}`
378
378
  : `Edited ${args.path}: replaced ${out.occurrences} occurrence(s)${out.note ? ` — ${out.note}` : ""}${diff ? "\n" + diff : ""}${await autoSyntaxCheck(abs)}`
@@ -387,29 +387,10 @@ export async function runSingleEdit(args, ctx) {
387
387
  })
388
388
  }
389
389
 
390
- // ─── edit 回执形态注入缝(#69——「回执形态按端注入」,形态参 §2.13.5 注入缝)──────────
391
390
  /**
392
- * 回执组装注入位(**缺省不覆盖** = CLI 回执形态(base + 写入点上下文)——零行为变;
393
- * 端装配层可覆盖为本端回执形态)。核内零端名分支(契约 5——本档只认 `format` 函数名)。
394
- * 契约:`format(fields) → string | null | undefined | Promise<同>`——null / undefined =
395
- * 未处理 ⇒ 回默认回执;其余返回值即该条回执。
396
- */
397
- let injectedReceipt = null
398
- export function configureEditReceipt(impl) {
399
- injectedReceipt = impl && typeof impl.format === "function" ? impl : null
400
- }
401
- /** 撤销注入(测试与端装配生命周期用——缺省态 = CLI 回执)。 */
402
- export function resetEditReceipt() { injectedReceipt = null }
403
-
404
- /**
405
- * edit 回执组装单点(单形态 / 数组形态共用——#69):先组默认回执(base + 写入点上下文),
406
- * 注入面命中 ⇒ 以其 format(fields) 返回值为准(`fields.default` = 默认回执原文,供端侧改写)。
391
+ * edit 回执组装单点(单形态 / 数组形态共用):`base` + 写入点上下文——唯一形态
392
+ * (#69 注入面已退场;余字段为调用面统一形,不参与组装)。
407
393
  */
408
394
  export async function composeEditReceipt(fields) {
409
- const defaultText = await appendWriteContext(fields.abs, fields.writeLine, fields.base)
410
- if (injectedReceipt) {
411
- const out = await injectedReceipt.format({ ...fields, default: defaultText })
412
- if (out !== null && out !== undefined) return String(out)
413
- }
414
- return defaultText
395
+ return await appendWriteContext(fields.abs, fields.writeLine, fields.base)
415
396
  }
@@ -14,10 +14,17 @@
14
14
  * · `run(cmd, args, opts) → Promise<string> | string`——成功给 stdout;
15
15
  * · 失败以 Error reject——`.stdout` / `.stderr` / `.code` 与 `execFileSync` 错误同形
16
16
  * (端侧现形 = 宿主 `runInterruptible`);
17
- * · `opts = { cwd?, timeout?, signal? }`——**默认径忽略 signal**(execFileSync 不可中断,
18
- * 与现行 CLI 行为逐字同);注入径自决(如 abort ⇒ 树杀)。
17
+ * · `opts = { cwd?, timeout?, signal?, env? }`——**默认径忽略 signal**(execFileSync 不可中断,
18
+ * 与现行 CLI 行为逐字同);`env` 缺省(undefined)不设 = 继承(逐字零变),给定则透传
19
+ * (Electron 宿主旗标径 = TOOLS.md §6.18);注入径自决(如 abort ⇒ 树杀)。
20
+ *
21
+ * **可中断执行器上提(R3 · 桌面功能对位批 · #523②)**:`runInterruptible` 自端侧现形
22
+ * (`thincoder-vscode/src/tools/shared.mjs`)**纯搬**落核(零语义改 —— KD-T2);两端改指核件
23
+ * (VSC `configureExecRun({ run: runInterruptible })` 同轮改指;桌面 `src/main/exec-run.mjs` 消费)。
24
+ * 树杀 = 本仓 `tools/process-tree.mjs` 单源(`killProcessTree` —— 转口,零第二实现)。
19
25
  */
20
- import { execFileSync } from "node:child_process"
26
+ import { execFileSync, spawn } from "node:child_process"
27
+ import { killProcessTree } from "./process-tree.mjs"
21
28
 
22
29
  let injected = null
23
30
 
@@ -29,10 +36,11 @@ export function configureExecRun(impl) {
29
36
  /** 撤销注入(测试与端装配生命周期用——缺省态 = execFileSync 默认径)。 */
30
37
  export function resetExecRun() { injected = null }
31
38
 
32
- /** 默认径(CLI 语义——utf8 + timeout + stdio ignore/pipe/pipe;cwd 未给则不设)。 */
39
+ /** 默认径(CLI 语义——utf8 + timeout + stdio ignore/pipe/pipe;cwd ∕ env 未给则不设)。 */
33
40
  function defaultRun(cmd, args, opts) {
34
41
  const options = { encoding: "utf8", timeout: opts.timeout, stdio: ["ignore", "pipe", "pipe"] }
35
42
  if (opts.cwd !== undefined) options.cwd = opts.cwd
43
+ if (opts.env !== undefined) options.env = opts.env
36
44
  return execFileSync(cmd, args, options)
37
45
  }
38
46
 
@@ -41,3 +49,93 @@ export async function runCommand(cmd, args, opts = {}) {
41
49
  if (injected) return await injected.run(cmd, args, opts)
42
50
  return defaultRun(cmd, args, opts)
43
51
  }
52
+
53
+ /**
54
+ * 可中断执行器(**上提件** —— 端侧 `configureExecRun` 供值;原住 `thincoder-vscode/src/tools/shared.mjs`,
55
+ * 逐字搬运 —— R3 · #523②):`spawn`(非 `execSync` —— 同步执行阻塞事件循环:长 lint ∕ verify 期间
56
+ * Stop 点击连“送达”都做不到)跑命令,abort ∕ timeout ⇒ 整树杀(本档 `killProcessTree` 单源)。
57
+ *
58
+ * 成功 → resolve stdout 串(与 `execFileSync` 调用点兼容)。
59
+ * 非零退出 ∕ spawn 失败 ∕ 超时 ∕ 中止 → reject 携 `.stdout` / `.stderr` / `.code`(同 `execFileSync` 错误形)。
60
+ */
61
+ export function runInterruptible(cmd, args, opts = {}) {
62
+ const { cwd, timeout, signal, env } = opts
63
+ return new Promise((resolve, reject) => {
64
+ let child
65
+ try {
66
+ child = spawn(cmd, args, {
67
+ cwd, env: env ?? process.env, stdio: ["ignore", "pipe", "pipe"], windowsHide: true,
68
+ // First-line abort guard: Node kills the direct child on signal abort.
69
+ ...(signal ? { signal } : {}),
70
+ })
71
+ } catch (e) {
72
+ reject(e)
73
+ return
74
+ }
75
+ let stdout = "", stderr = "", settled = false, timer = null, kickTimer = null, mode = null
76
+
77
+ const KICK_MS = 3000
78
+ const finish = (err, out) => {
79
+ if (settled) return
80
+ settled = true
81
+ if (timer) clearTimeout(timer)
82
+ if (kickTimer) clearTimeout(kickTimer)
83
+ signal?.removeEventListener("abort", onAbort)
84
+ if (err) {
85
+ err.stdout = stdout
86
+ err.stderr = stderr
87
+ reject(err)
88
+ } else {
89
+ resolve(out)
90
+ }
91
+ }
92
+ // Kill the whole tree on abort/timeout — grandchildren (npm test's children)
93
+ // must die too, or they keep the stdout/stderr pipes and "close" never fires.
94
+ const killTree = () => { try { killProcessTree(child) } catch { /* already gone */ } }
95
+ // Kick-clockback: settle even if "close" never arrives (a grandchild that
96
+ // mocks kill signal, or a wedged pipe). Prevents a hang + leaked pipes.
97
+ const armKick = (err) => {
98
+ if (kickTimer) return
99
+ kickTimer = setTimeout(() => finish(err), KICK_MS)
100
+ }
101
+
102
+ const onAbort = () => {
103
+ if (mode) return
104
+ mode = "abort"
105
+ const e = new Error("aborted by user (Stop)")
106
+ e.name = "AbortError"
107
+ killTree()
108
+ armKick(e)
109
+ }
110
+
111
+ if (timeout) {
112
+ timer = setTimeout(() => {
113
+ if (mode) return
114
+ mode = "timeout"
115
+ const e = new Error(`timed out after ${timeout}ms`)
116
+ e.name = "TimeoutError"
117
+ killTree()
118
+ armKick(e)
119
+ }, timeout)
120
+ }
121
+
122
+ child.stdout?.on("data", (d) => { stdout += d })
123
+ child.stderr?.on("data", (d) => { stderr += d })
124
+ child.on("error", (e) => { if (e.name === "AbortError") return; finish(e) })
125
+ child.on("close", (code) => {
126
+ if (mode === "abort") { const e = new Error("aborted by user (Stop)"); e.name = "AbortError"; return finish(e) }
127
+ if (mode === "timeout") { const e = new Error(`timed out after ${timeout}ms`); e.name = "TimeoutError"; return finish(e) }
128
+ if (code === 0) finish(null, stdout)
129
+ else {
130
+ const e = new Error(`command failed with exit code ${code}`)
131
+ e.code = code
132
+ finish(e)
133
+ }
134
+ })
135
+
136
+ if (signal) {
137
+ if (signal.aborted) { onAbort(); return }
138
+ signal.addEventListener("abort", onAbort, { once: true })
139
+ }
140
+ })
141
+ }
package/tools/execute.mjs CHANGED
@@ -28,13 +28,14 @@ import { spawn } from "node:child_process"
28
28
  import { resolve } from "node:path"
29
29
  import { DESC } from "./shared.mjs"
30
30
  import { killProcessTree } from "./process-tree.mjs"
31
+ import { nodeChildEnv } from "./node-child.mjs"
31
32
 
32
33
  const MAX_SCRIPT = 50_000
33
34
  const MAX_OUTPUT = 50_000
34
35
  const DEFAULT_TIMEOUT = 30_000
35
36
 
36
37
  /**
37
- * 超时错误文本——带重试引导(TOOLS.md §14.1 D14.1.2——"下一跳"):数字 = 实际生效的
38
+ * 超时错误文本——带重试引导("下一跳"):数字 = 实际生效的
38
39
  * timeoutMs(Math.min(t, 600_000) 或默认 30s)——上限 600000 与 schema/头注/execute.md 一致。
39
40
  */
40
41
  const timeoutErrorText = (timeoutMs) =>
@@ -83,6 +84,8 @@ function runNode(childArgs, baseDir, timeoutMs, signal) {
83
84
  return new Promise((resolvePromise) => {
84
85
  const child = spawn(process.execPath, childArgs, {
85
86
  cwd: baseDir,
87
+ // TOOLS.md §6.18:Electron 宿主 execPath 属 Electron 族二进制——补旗标按 node 语义启动
88
+ env: nodeChildEnv(),
86
89
  stdio: ["ignore", "pipe", "pipe"],
87
90
  windowsHide: true,
88
91
  // POSIX detached → 子进程为组首——killProcessTree 的 -pid 组杀可达孙进程(win 用
@@ -223,7 +226,7 @@ export const executeTool = {
223
226
  if (code.length > MAX_SCRIPT) {
224
227
  return `Error: script too large (${code.length} > ${MAX_SCRIPT} bytes). Split into smaller scripts or use individual tools.`
225
228
  }
226
- // inline mode: pure node ESM — no prelude, nothing injected (TOOLS.md §12).
229
+ // inline mode: pure node ESM — no prelude, nothing injected.
227
230
  childArgs = ["--input-type=module", "--eval", code]
228
231
  }
229
232
 
package/tools/file.mjs CHANGED
@@ -19,7 +19,7 @@ import { runSingleEdit } from "./edit-diff.mjs";
19
19
  import { writeThroughPath, clearDirty, clearLastWrite, lastWriteOf, isDirty } from "./write-path.mjs";
20
20
  import { specForModel } from "../config.mjs";
21
21
  import { createHash } from "node:crypto";
22
- import { mkdir, readFile, stat, unlink } from "node:fs/promises";
22
+ import { mkdir, readFile, stat } from "node:fs/promises";
23
23
  import { join, relative, dirname } from "node:path";
24
24
 
25
25
  const MAX_FILE_READ_BYTES = 10_000_000
@@ -178,11 +178,6 @@ export const readImageTool = {
178
178
  text: `[read_image: ${args.path} (${mime}, ${bytes} bytes)]`,
179
179
  images: [{ type: "image_url", image_url: { url: `data:${mime};base64,${b64}` } }],
180
180
  })
181
- // Paste-created temp files: delete after use, no litter
182
- const basename = abs.includes("/") ? abs.slice(abs.lastIndexOf("/") + 1) : abs.slice(abs.lastIndexOf("\\") + 1)
183
- if (basename.startsWith(".thincoder-paste-")) {
184
- try { await unlink(abs) } catch { /* can't delete, so be it */ }
185
- }
186
181
  return result
187
182
  },
188
183
  }
@@ -214,7 +209,7 @@ export const writeTool = {
214
209
  const content = eol === "\r\n" ? normalizeEOL(args.content).replace(/\n/g, "\r\n") : args.content
215
210
  // 单一写路径点:默认 = writeFile + 记账;端侧注入 ⇒ 编辑器径(§2.13.5)。
216
211
  await writeThroughPath(abs, content, { op: "write", record: { type: "write", startLine: 1, shift: 0 } }) // 全文重写——全文件受影响
217
- const diff = gitDiffOne(ctx.cwd, abs)
212
+ const diff = await gitDiffOne(ctx.cwd, abs)
218
213
  return `Wrote ${args.content.length} chars to ${args.path}${diff ? "\n" + diff : ""}${await autoSyntaxCheck(abs)}`
219
214
  },
220
215
  }
@@ -365,7 +360,7 @@ export const insertAfterTool = {
365
360
  op: "write",
366
361
  record: { type: "insert", startLine: targetLine, shift: normalizeEOL(args.content).split("\n").length },
367
362
  })
368
- const diff = gitDiffOne(ctx.cwd, abs)
363
+ const diff = await gitDiffOne(ctx.cwd, abs)
369
364
  const baseResult = `Inserted after line ${targetLine} in ${args.path}${diff ? "\n" + diff : ""}${await autoSyntaxCheck(abs)}`
370
365
  return await appendWriteContext(abs, targetLine + 1, baseResult)
371
366
  },
@@ -460,7 +455,7 @@ export const hashlineEditTool = {
460
455
  op: "write",
461
456
  record: { type: "edit", startLine: pos + 1, shift: newLines.length - target.length },
462
457
  })
463
- const diff = gitDiffOne(ctx.cwd, abs)
458
+ const diff = await gitDiffOne(ctx.cwd, abs)
464
459
  const baseResult = `Edited ${args.path}: replaced ${target.length} line(s) at L${pos + 1} with ${newLines.length} line(s)${diff ? "\n" + diff : ""}${await autoSyntaxCheck(abs)}${corrupted ? `\n${FFFD_WARNING}` : ""}`
465
460
  return await appendWriteContext(abs, pos + 1, baseResult)
466
461
  },
package/tools/git-run.mjs CHANGED
@@ -33,7 +33,7 @@ export const GIT_NET_TIMEOUT_MS = 300_000
33
33
  /** 网路面五动作(300s 档——合法耗时可远超本地;依据 = 需求档 §4.7 TTY-DRIVE N3 候选参照值)。 */
34
34
  const GIT_NET_ACTIONS = new Set(["push", "fetch", "pull", "clone", "ls-remote"])
35
35
 
36
- /** 测试态缝(模块级 · 缺省 null = 生产零行为变——先例 `manifest.mjs:40-41` · `session-gc.mjs:136`)。
36
+ /** 测试态缝(模块级 · 缺省 null = 生产零行为变——先例 `manifest-discovery.mjs:15-20` · `session-gc.mjs:186`)。
37
37
  * 用例 `finally` 复位;不经用户参数面(边界:不新增参数 / 用户选项)。 */
38
38
  let gitTimeoutOverride = null
39
39
  export function _setGitTimeoutForTest(ms) { gitTimeoutOverride = ms }
package/tools/git.mjs CHANGED
@@ -26,9 +26,6 @@ async function runGitRaw(cwd, cmdArgs, config = []) {
26
26
  }
27
27
  }
28
28
 
29
-
30
-
31
-
32
29
  /** Resolve workdir relative to cwd — no boundary assertion
33
30
  * (§10.1 2026-09-02: workspace confinement removed; git itself is not
34
31
  * directory-limited — same boundary as bash). */
@@ -53,22 +50,6 @@ function ambiguousRepoMessage(candidates, cwd, matched) {
53
50
  return `${head} — pass workdir to run git in the intended one (no silent pick):\n` + candidates.map((c) => `- ${c}`).join("\n")
54
51
  }
55
52
 
56
- // ─── 审批门注入缝(#59——「只读判定 / 审批门按端注入」,形态参 §2.13.5 注入缝)────────
57
- /**
58
- * 审批门注入位(**缺省不覆盖** = 无审批面:全部动作照常执行——CLI 语义,零行为变;端装配层
59
- * 可覆盖为本端审批 / 只读判定。核内零端名分支(契约 5——本档只认 `gate` 函数名)。
60
- * 契约:`gate(args, ctx) → string | null | Promise<同>`——非空字符串 = 拒执行(该串原样作为
61
- * 工具结果返回);null / undefined / 其他值 = 放行。每个 execute 恰问一次(workdir 归一后)。
62
- */
63
- let injectedApproval = null
64
- export function configureGitApproval(impl) {
65
- injectedApproval = impl && typeof impl.gate === "function" ? impl.gate : null
66
- }
67
- /** 撤销注入(测试与端装配生命周期用——缺省态 = 无审批门)。 */
68
- export function resetGitApproval() { injectedApproval = null }
69
-
70
-
71
-
72
53
  export const gitTool = {
73
54
  name: "git",
74
55
  description: DESC("git"),
@@ -127,11 +108,6 @@ export const gitTool = {
127
108
  repoNote = `(repo: ${discovered.root})`
128
109
  }
129
110
  }
130
- // #59:审批门(端注入面)——缺省无门(CLI 语义);非空字符串返回 ⇒ 拒执行(拒串原样、不加注记)。
131
- if (injectedApproval) {
132
- const refusal = await injectedApproval(args, ctx)
133
- if (typeof refusal === "string" && refusal.length > 0) return refusal
134
- }
135
111
  const out = await gitActionCore.dispatch(args, ctx)
136
112
  return repoNote ? `${repoNote}\n${out}` : out
137
113
  },
package/tools/index.mjs CHANGED
@@ -68,7 +68,8 @@ export async function assembleBuiltinTools({ memory, cwd, projectDir = null, aut
68
68
  ...memoryTools(memory, { cwd, projectDir, author, team }),
69
69
  codeSearchTool(memory),
70
70
  docSearchTool(memory),
71
- repoOutlineTool(memory.db, cwd),
71
+ // memory 缺位容错:组装期零解引用(memory?.db);缺位不抛(懒失败留执行期——与 memory ∕ code_search ∕ doc_search 同族,dispatch 收成 Error 回模型);有 memory 逐字零变
72
+ repoOutlineTool(memory?.db, cwd),
72
73
  settingsTool(),
73
74
  peerInstancesTool,
74
75
  // 台账查询命令族(M2 设计 §2.2 命令面接线——查询命令全角色面;写命令住 family-tools depthOnly 分支)
package/tools/linter.mjs CHANGED
@@ -4,6 +4,7 @@ import { DESC, resolveInCwd } from "./shared.mjs"
4
4
  import { runCommand } from "./exec-run.mjs"
5
5
  import { existsSync } from "node:fs"
6
6
  import { join } from "node:path"
7
+ import { nodeChildEnv } from "./node-child.mjs"
7
8
 
8
9
  export const lintTool = {
9
10
  name: "lint",
@@ -47,7 +48,7 @@ async function nodeCheckResult(abs, signal) {
47
48
  return `lint (check): only JS/TS-family files supported for fast syntax check; use full=true for other languages. Path: ${abs}`
48
49
  }
49
50
  try {
50
- await runCommand(process.execPath, ["--check", abs], { timeout: 10000, signal })
51
+ await runCommand(process.execPath, ["--check", abs], { timeout: 10000, signal, env: nodeChildEnv() })
51
52
  return `Syntax OK: ${abs}`
52
53
  } catch (e) {
53
54
  const msg = (e.stderr || e.stdout || e.message || "").trim()
@@ -0,0 +1,21 @@
1
+ /**
2
+ * tools/node-child.mjs — node 语义子进程启动单源(TOOLS.md §6.18 · D-TO14 · 台账 #602)。
3
+ *
4
+ * 缺口:核内 spawn `process.execPath` 的隐含前提 = 「execPath 为 node」。桌面宿主主进程 =
5
+ * `electron.exe` ⇒ execPath 属 Electron 族二进制——子进程按 Electron 应用启动、不执行代码
6
+ * (execute 悬挂至超时)。本档 = 那一跳:Electron 宿主补 `ELECTRON_RUN_AS_NODE: "1"`,
7
+ * 同一二进制以 node 语义启动。
8
+ *
9
+ * 判据 = `process.versions.electron`(运行时能力探测——非端名分支):三端同判据、同式执行;
10
+ * 已处 node 模式的 Electron 宿主(VS Code 扩展宿主——env 携旗标)为同值幂等。
11
+ * 消费 = `execute.runNode` spawn 选项 ∕ `lint` 快路径(经 `exec-run` `opts.env` 透传)。
12
+ */
13
+
14
+ /**
15
+ * node 语义子进程 env:Electron 宿主 ⇒ `{ ...base, ELECTRON_RUN_AS_NODE: "1" }` 副本
16
+ * (不改父进程 env);否则原样返回 `base`(同引用——与 spawn 缺省继承逐字等价,零行为变)。
17
+ * 第二参 = 判据注入(机检用——先例 `_setGitTimeoutForTest` 族)。
18
+ */
19
+ export function nodeChildEnv(base = process.env, isElectron = !!process.versions.electron) {
20
+ return isElectron ? { ...base, ELECTRON_RUN_AS_NODE: "1" } : base
21
+ }
package/tools/ops.mjs CHANGED
@@ -1,8 +1,10 @@
1
1
  /**
2
- * ops.mjs — operational tools: file_ops (move/copy/rename), process (list),
2
+ * ops.mjs — operational tools: file_ops (move/copy/rename), process (list/kill),
3
3
  * get_current_time, wait_for (condition wait). Each exists so the model reaches
4
4
  * for a dedicated tool instead of shelling out to `bash` for the same operation
5
5
  * (parity with thinworker).
6
+ * #9:process 收编 `kill`(靶 = pid ∥ 后台 bash 任务 id——欠面收正:原须绕 bash taskkill);
7
+ * wait_for 条件族 +`bash id:N done`。list ∥ 既有条件面零变。
6
8
  */
7
9
  import { DESC, resolveInCwd, truncate } from "./shared.mjs"
8
10
  // 单一写路径点(编辑器编辑径注入缝——CORE-UNIFICATION §2.13.5):路径操作也经它落盘
@@ -51,11 +53,21 @@ export const processTool = {
51
53
  parameters: {
52
54
  type: "object",
53
55
  properties: {
54
- name: { type: "string", description: "Optional name substring filter (case-insensitive)" },
56
+ action: { type: "string", enum: ["list", "kill"], description: "list (default) — show running processes; kill — terminate a process tree (target: pid) or a background bash task (target: id)" },
57
+ name: { type: "string", description: "Optional name substring filter for list (case-insensitive)" },
58
+ pid: { type: "number", description: "kill target: process id — the whole tree is terminated (taskkill /T /F on win32; POSIX group kill)" },
59
+ id: { type: "number", description: "kill target: background bash task id (bash#N from the bash async ack)" },
55
60
  },
56
61
  },
57
- readonly: true,
58
- async execute({ name }, ctx) {
62
+ // #9:动作分级——list 保持只读分类(dispatch-gates `isSubagentReadonlyAction`:planMode 放行/
63
+ // 免审批);kill 是破坏性动作(审批门)。`parallel` 保既有调度形(list 恒可批并行)。
64
+ readonly: false,
65
+ parallel: true,
66
+ async execute(args = {}, ctx = {}) {
67
+ const action = args?.action ?? "list"
68
+ if (action === "kill") return await executeKill(args, ctx)
69
+ if (action !== "list") return `Error: action must be list | kill (got "${action}")`
70
+ const { name } = args
59
71
  const filter = typeof name === "string" && name.trim() ? name.trim().toLowerCase() : null
60
72
  let rows
61
73
  try {
@@ -69,6 +81,24 @@ export const processTool = {
69
81
  },
70
82
  }
71
83
 
84
+ /** #9 kill 执行体(靶 = pid ∥ 后台任务 id 二选一;两靶皆树杀)。动态 import(W8 契约②:
85
+ * agent-tools 链静态达 node:sqlite——端壳静态闭包须保持零命中)。 */
86
+ async function executeKill({ pid, id }, ctx) {
87
+ const hasPid = pid !== undefined && pid !== null && String(pid).trim() !== ""
88
+ const hasId = id !== undefined && id !== null && String(id).trim() !== ""
89
+ if (hasPid === hasId) {
90
+ return "Error: kill requires exactly one target — pid (process id) or id (background bash task id)"
91
+ }
92
+ const { killBgTask, killBgPid } = await import("../agent-tools/bash-async.mjs")
93
+ if (hasPid) {
94
+ if (!killBgPid(pid)) return `Error: kill pid: "${String(pid)}" is not a valid process id`
95
+ return `killed process tree ${Number(pid)} (taskkill /T /F on win32; POSIX group kill)`
96
+ }
97
+ const res = killBgTask(ctx?.agent, id)
98
+ if (res.status === "error") return `Error: ${res.error}`
99
+ return `killed background bash#${res.id} — its process tree was terminated; no digest will arrive${res.logPath ? ` — log so far: ${res.logPath}` : ""}`
100
+ }
101
+
72
102
  function listWindows() {
73
103
  // tasklist /FO CSV /NH → lines: "name.exe","1234","Console","1","12,345 K"
74
104
  const out = execFileSync("tasklist", ["/FO", "CSV", "/NH"], { encoding: "utf8", timeout: 10000 })
@@ -112,7 +142,7 @@ export const getCurrentTimeTool = {
112
142
 
113
143
  // ─── wait_for ───────────────────────────────────────────────────
114
144
 
115
- /** wait_for bounds (TOOLS.md §16): a BUILT-IN ceiling replaces the unbounded
145
+ /** wait_for bounds: a BUILT-IN ceiling replaces the unbounded
116
146
  * sleep-then-wait — timeout_ms defaults to 30s (config.json agent.waitForTimeoutMs
117
147
  * overrides), capped at WAIT_FOR_MAX_TIMEOUT_MS (600s — the execute-tool timeout
118
148
  * convention, Math.min(t, 600_000)); interval_ms defaults to 1s with a 100ms
@@ -122,7 +152,7 @@ export const WAIT_FOR_MAX_TIMEOUT_MS = 600_000
122
152
  export const WAIT_FOR_DEFAULT_INTERVAL_MS = 1_000
123
153
  export const WAIT_FOR_MIN_INTERVAL_MS = 100
124
154
 
125
- // Condition-source seam (§16 评审 #4): production uses the real evaluator
155
+ // Condition-source seam (评审 #4): production uses the real evaluator
126
156
  // (evaluateWaitForCondition); tests inject a deterministic fake source via
127
157
  // setWaitForConditionSource (default null — production path unchanged).
128
158
  let injectedConditionSource = null
@@ -139,10 +169,11 @@ export function parseWaitForCondition(condition) {
139
169
  if (c === "advisor settled") return { kind: "advisor", arg: null }
140
170
  if (c === "consult done") return { kind: "consult", arg: null }
141
171
  if ((m = c.match(/^subagent id:\s*(\d+) done$/))) return { kind: "subagent", arg: String(Number(m[1])) }
172
+ if ((m = c.match(/^bash id:\s*(\d+) done$/))) return { kind: "bash", arg: String(Number(m[1])) }
142
173
  if ((m = c.match(/^file exists:\s*(.+)$/))) return { kind: "file", arg: m[1].trim() }
143
174
  if ((m = c.match(/^port open:\s*(\d+)$/))) return { kind: "port", arg: Number(m[1]) }
144
175
  throw new Error(
145
- `wait_for: unsupported condition "${c}" — supported: "advisor settled", "subagent id:N done", "consult done", "file exists:path", "port open:N"`,
176
+ `wait_for: unsupported condition "${c}" — supported: "advisor settled", "subagent id:N done", "consult done", "bash id:N done", "file exists:path", "port open:N"`,
146
177
  )
147
178
  }
148
179
 
@@ -170,7 +201,7 @@ function asyncEntryDone(agent, id) {
170
201
  return entry.done === true || entry.status === "done"
171
202
  }
172
203
 
173
- /** 评审池双载体(第 10 批 §18.3 #2):agent ∪ history——未初始化的载体不进集合。 */
204
+ /** 评审池双载体(第 10 批 AGENT-LOOP-ASYNC-POOL.md §6.11 #2):agent ∪ history——未初始化的载体不进集合。 */
174
205
  function advisorPools(agent) {
175
206
  return [agent?._asyncAdvisors, agent?.history?._asyncAdvisors].filter((m) => m instanceof Map)
176
207
  }
@@ -183,8 +214,8 @@ function hasRunningIn(pool, pred) {
183
214
  return false
184
215
  }
185
216
 
186
- /** "advisor settled" 真判据(第 10 批 §18.3 #2 修正):**评审池真实态**——双载体
187
- * (agent._asyncAdvisors ∪ history._asyncAdvisors)无 running/queued 条目(与 §11.2 的
217
+ /** "advisor settled" 真判据(第 10 批 AGENT-LOOP-ASYNC-POOL.md §6.11 #2 修正):**评审池真实态**——双载体
218
+ * (agent._asyncAdvisors ∪ history._asyncAdvisors)无 running/queued 条目(与 AGENT-LOOP-ASYNC-POOL.md §6.10 的
188
219
  * 未决评审判定 advisorReviewPending 同源语义)。修前读**子代理池**的 role==="advisor"
189
220
  * 条目——评审条目只在 _asyncAdvisors → 恒无命中 → 恒真 0ms 秒过(用户实证)。 */
190
221
  function advisorSettled(agent) {
@@ -228,12 +259,18 @@ export async function evaluateWaitForCondition(condition, ctx) {
228
259
  const agent = ctx?.agent
229
260
  switch (parsed.kind) {
230
261
  case "advisor":
231
- // 第 10 批修正(§18.3 #2——修前恒真 0ms 秒过):判据 = 评审池真实态(双载体)——
232
- // 不再读子代理池的 role==="advisor" 条目(该池永无此类条目);语义与 §11.2 未决
262
+ // 第 10 批修正(AGENT-LOOP-ASYNC-POOL.md §6.11 #2——修前恒真 0ms 秒过):判据 = 评审池真实态(双载体)——
263
+ // 不再读子代理池的 role==="advisor" 条目(该池永无此类条目);语义与 §6.10 未决
233
264
  // 评审判定同源(advisorReviewPending)。
234
265
  return advisorSettled(agent)
235
266
  case "subagent":
236
267
  return asyncEntryDone(agent, parsed.arg)
268
+ case "bash": {
269
+ // #9:后台 bash 任务判据(池内 done ∥ 出池即 done——`bash-async.mjs` 单点)。
270
+ // 动态 import(W8 契约②:agent-tools 链静态达 node:sqlite——端壳静态闭包零命中)。
271
+ const { bgTaskDone } = await import("../agent-tools/bash-async.mjs")
272
+ return bgTaskDone(agent, parsed.arg)
273
+ }
237
274
  case "consult":
238
275
  return consultAllDone(agent)
239
276
  case "file":
@@ -256,7 +293,7 @@ export const waitForTool = {
256
293
  parameters: {
257
294
  type: "object",
258
295
  properties: {
259
- condition: { type: "string", description: 'Condition expression — "advisor settled", "subagent id:N done", "consult done", "file exists:path", "port open:N"' },
296
+ condition: { type: "string", description: 'Condition expression — "advisor settled", "subagent id:N done", "consult done", "bash id:N done", "file exists:path", "port open:N"' },
260
297
  interval_ms: { type: "integer", description: "Poll interval in ms (default 1000, floor 100)" },
261
298
  timeout_ms: { type: "integer", description: "Overall timeout in ms (default 30000, cap 600000; config.json agent.waitForTimeoutMs overrides the default)" },
262
299
  },
package/tools/patch.mjs CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  majorityEol
8
8
  } from "./shared.mjs";
9
9
  import { writeThroughPath, TMP_SUFFIX, markDirty } from "./write-path.mjs";
10
- import { execFileSync } from "node:child_process";
10
+ import { spawnGit } from "./git-run.mjs";
11
11
  import { mkdir, readFile, lstat, unlink } from "node:fs/promises";
12
12
  import { existsSync } from "node:fs";
13
13
  import { relative, dirname } from "node:path";
@@ -275,7 +275,9 @@ export const deleteTool = {
275
275
  const rel = relative(ctx.cwd, abs).replace(/\\/g, "/")
276
276
  let tracked = false
277
277
  try {
278
- execFileSync("git", ["ls-files", "--error-unmatch", "--", rel], { cwd: ctx.cwd, stdio: "ignore" })
278
+ // #208①(2026-09-25):表外同步 spawn 收口——探针转 `spawnGit` 单点(GIT_ENV 加固 + 超时 + 树杀);
279
+ // 失败语义零变(未跟踪 / 非仓 ⇒ tracked 保持 false)。
280
+ await spawnGit(ctx.cwd, ["ls-files", "--error-unmatch", "--", rel])
279
281
  tracked = true
280
282
  } catch {
281
283
  // untracked / non-git repo
package/tools/repomap.mjs CHANGED
@@ -6,7 +6,8 @@
6
6
  import { existsSync } from "node:fs"
7
7
  import { readFile, stat } from "node:fs/promises"
8
8
  import { join } from "node:path"
9
- import { normalizeEOL } from "./shared.mjs"
9
+ import { DESC, normalizeEOL } from "./shared.mjs" // #15 描述外置:文本单点 = tool-docs/repo_outline.md(DESC 单解析面)
10
+ import { INDEX_ORIGIN_ROW_WARN } from "../memory/schema.mjs" // §6.14 B4 文件数上界(WARN 值复用)
10
11
 
11
12
  /** Extract JS/TS file import paths (normalize by stripping .ts/.js/.mjs suffixes) */
12
13
  function parseImports(lines, ext) {
@@ -111,13 +112,22 @@ function normalizeExt(p) {
111
112
  return p.replace(/\.(m?js|jsx|tsx?)$/i, "")
112
113
  }
113
114
 
115
+ /** B4 越界提示行(跳过大纲构建;指路 = 声明 ∕ 剪枝)——两入口(buildSummary ∕ buildOutline)同文;
116
+ * `origin` 给出时才缀「for this origin」(repo_outline 工具缝不传 origin——跨 origin 行不假称本 origin)。 */
117
+ function outlineOverLimit(origin = null) {
118
+ return `(code index too large for an outline: > ${INDEX_ORIGIN_ROW_WARN} files${origin ? " for this origin" : ""} — declare index.excludePaths in PROJECT-MANIFEST.json, or prune: thincoder memory sweep --origin <o> --path <sub>)`
119
+ }
120
+
114
121
  /**
115
122
  * Internal: scan all files, build forward dependency graph + reverse reference graph.
116
123
  * Returns { deps, importers, fileCount } shared by buildOutline / buildSummary.
117
124
  */
118
- async function _buildDepGraph(db, cwd) {
119
- const allFiles = db.prepare(`SELECT DISTINCT path FROM code_chunks ORDER BY path`).all().map((r) => r.path)
125
+ async function _buildDepGraph(db, cwd, { origin = null } = {}) {
126
+ // §6.14 B4:origin 限定 + 文件数上界(文件行 `LIMIT = WARN+1` 形——越界不构建大纲,返回信号)
127
+ const limit = INDEX_ORIGIN_ROW_WARN + 1
128
+ const allFiles = db.prepare(`SELECT DISTINCT path FROM code_chunks ${origin ? "WHERE origin = ? " : ""}ORDER BY path LIMIT ${limit}`).all(...(origin ? [origin] : [])).map((r) => r.path)
120
129
  if (allFiles.length === 0) return null
130
+ if (allFiles.length >= limit) return { overLimit: true, fileCount: allFiles.length }
121
131
 
122
132
  const deps = new Map() // path → { imports: Set, exports: Set, size: number, dir: string }
123
133
  const importers = new Map() // importee → Set<importer>
@@ -182,8 +192,9 @@ async function _buildDepGraph(db, cwd) {
182
192
  * 3. Entry points (files with no importers — startup/top-level entry points)
183
193
  * Output is naturally bounded (~1000-2000 chars), no more OUTLINE_INJECT_MAX hard truncation.
184
194
  */
185
- export async function buildSummary(db, cwd) {
186
- const graph = await _buildDepGraph(db, cwd)
195
+ export async function buildSummary(db, cwd, { origin = null } = {}) {
196
+ const graph = await _buildDepGraph(db, cwd, { origin })
197
+ if (graph?.overLimit) return outlineOverLimit(origin)
187
198
  if (!graph) return "(no indexed source files; run codeSync or /reindex first)"
188
199
  const { deps, importers, fileCount } = graph
189
200
 
@@ -257,8 +268,9 @@ export async function buildSummary(db, cwd) {
257
268
  }
258
269
 
259
270
  /** Get known file list from code_chunks (reuse index), parse by path to generate outline text */
260
- export async function buildOutline(db, cwd, focusPath) {
261
- const graph = await _buildDepGraph(db, cwd)
271
+ export async function buildOutline(db, cwd, focusPath, { origin = null } = {}) {
272
+ const graph = await _buildDepGraph(db, cwd, { origin })
273
+ if (graph?.overLimit) return outlineOverLimit(origin)
262
274
  if (!graph) return "(no indexed source files; run codeSync or /reindex first)"
263
275
  const { deps, importers } = graph
264
276
 
@@ -296,8 +308,7 @@ export async function buildOutline(db, cwd, focusPath) {
296
308
  export function repoOutlineTool(db, cwd) {
297
309
  return {
298
310
  name: "repo_outline",
299
- description:
300
- "Show the project's file dependency outline: which files import/export from which, and what symbols they export. Use when you need to understand the project structure, find where a function is defined, or see what files depend on a module. Pass a path to focus on a single file's relationships. Find code by keyword or snippet with code_search.",
311
+ description: DESC("repo_outline"), // #15 外置:文本单点 = tool-docs/repo_outline.md(工具名逐字对核)
301
312
  parameters: {
302
313
  type: "object",
303
314
  properties: {
package/tools/shared.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  * Imported by tools/file.mjs / system.mjs / web.mjs / git.mjs
4
4
  */
5
5
 
6
- import { spawn, execFileSync, execFile } from "node:child_process"
6
+ import { execFile } from "node:child_process"
7
7
  import { readFileSync, existsSync, realpathSync, readdirSync, statSync, openSync, readSync, closeSync } from "node:fs"
8
8
  import { dirname, join, resolve } from "node:path"
9
9
  import { fileURLToPath } from "node:url"
@@ -217,12 +217,14 @@ export function makeDecoder() {
217
217
  }
218
218
  }
219
219
 
220
- /** Single-file git diff. Silently returns empty on failure. Large diffs that exceed maxBuffer are truncated rather than swallowed. */
221
- export function gitDiffOne(cwd, abs) {
220
+ /** Single-file git diff. Silently returns empty on failure. Large diffs that exceed maxBuffer are truncated rather than swallowed.
221
+ * #208①(2026-09-25):体转 async 薄壳委托 `spawnGit` 单点(GIT_ENV 加固 + 两档超时 + 树杀 + 非阻塞);
222
+ * 形保真 = trim / 200 行截断 / 10MB 溢出取部分输出 / 失败静默回 `""`(调用面 5 处 `await` 化)。 */
223
+ export async function gitDiffOne(cwd, abs) {
222
224
  try {
223
- const diff = execFileSync("git", ["--no-pager", "diff", "--no-color", "--", abs], {
224
- cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 10 * 1024 * 1024,
225
- }).trim()
225
+ const diff = (await spawnGit(cwd, ["--no-pager", "diff", "--no-color", "--", abs], {
226
+ maxBuffer: 10 * 1024 * 1024,
227
+ })).trim()
226
228
  if (!diff) return ""
227
229
  const lines = diff.split("\n")
228
230
  if (lines.length <= 200) return diff
package/tools/web.mjs CHANGED
@@ -1,10 +1,11 @@
1
1
  import { DESC, truncate, stripTags, htmlToText, isPrivateHost } from "./shared.mjs";
2
2
  import { URL } from "node:url";
3
3
  import { proxyFetch } from "../proxy.mjs";
4
+ import { resolveEnvRefs } from "../env-ref.mjs";
4
5
 
5
6
  export const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
6
7
  const FETCH_TIMEOUT = 15_000
7
- // §14 D-TF3:网络失败错误文本追加 proxy 提示——纯文本提示,不自动路由(2026-08-31 裁定:proxy 显式传不自动应用)
8
+ // D-TF3:网络失败错误文本追加 proxy 提示——纯文本提示,不自动路由(2026-08-31 裁定:proxy 显式传不自动应用)
8
9
  const PROXY_HINT = "network failure — retry with proxy: 'http://host:port' if the target is blocked"
9
10
 
10
11
  // ── Web search (Bing; direct by default, through proxy when configured and web toggle on) ──
@@ -46,7 +47,9 @@ const ENGINE_NAMES = ENGINES.map(e => e.name)
46
47
  * { engine, results } or null to fall back to Bing HTML scraping.
47
48
  * proxyUri: explicit per-call proxy (args.proxy) — never the config.json one. */
48
49
  async function fetchTavily(query, limit, ctx, proxyUri) {
49
- const apiKey = ctx?.agent?.config?.websearch?.apiKey
50
+ // #57(provider-config-family 批):值位 `${env:VAR}` 消费侧解析(CONFIG.md §6.3)——未设 ∕ 空串 ∕
51
+ // 畸形 ⇒ 抛错(不静默字面透传);未配置 key 仍走 Bing 回落。
52
+ const apiKey = resolveEnvRefs(ctx?.agent?.config?.websearch?.apiKey)
50
53
  if (!apiKey) return null
51
54
  const ctrl = new AbortController()
52
55
  const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT)