@thincoder/core 0.9.4 → 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 (221) hide show
  1. package/CHANGELOG.md +63 -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 +14 -14
  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 +23 -13
  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 +55 -294
  20. package/agent/family-tools.mjs +11 -11
  21. package/agent/helpers.mjs +63 -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 +19 -15
  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 +90 -30
  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 +11 -11
  36. package/agent-tools/advisor-settle.mjs +17 -17
  37. package/agent-tools/advisor.mjs +38 -43
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +23 -20
  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 +65 -34
  43. package/agent-tools/batch-paths.mjs +186 -0
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +22 -119
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +58 -58
  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 +18 -13
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +3 -3
  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 +29 -273
  66. package/agent-tools/subagent-async.mjs +36 -31
  67. package/agent-tools/subagent-panel.mjs +38 -9
  68. package/agent-tools/subagent-run.mjs +44 -29
  69. package/agent-tools/subagent-scheduler.mjs +29 -28
  70. package/agent-tools/subagent-spawn.mjs +57 -25
  71. package/agent-tools/subagent.mjs +46 -66
  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 +20 -351
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +12 -7
  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/git/checkpoint.mjs +4 -8
  95. package/history-window.mjs +17 -2
  96. package/i18n.mjs +2 -0
  97. package/index-discover.mjs +7 -7
  98. package/ledger-cmd.mjs +34 -126
  99. package/ledger-db.mjs +35 -7
  100. package/ledger-migrate.mjs +299 -0
  101. package/ledger-tools.mjs +188 -0
  102. package/ledger.mjs +11 -5
  103. package/log.mjs +12 -7
  104. package/manifest-discovery.mjs +131 -0
  105. package/manifest-schema.mjs +166 -0
  106. package/manifest.mjs +18 -217
  107. package/mcp.mjs +4 -1
  108. package/memory/code-search.mjs +149 -0
  109. package/memory/code-sync.mjs +89 -230
  110. package/memory/core.mjs +47 -32
  111. package/memory/docs.mjs +63 -262
  112. package/memory/file-list.mjs +89 -0
  113. package/memory/file-walk.mjs +7 -4
  114. package/memory/memory-tool.mjs +229 -0
  115. package/memory/schema.mjs +18 -3
  116. package/memory/sweep.mjs +289 -0
  117. package/memory/sync-tail.mjs +81 -0
  118. package/memory-status.mjs +69 -0
  119. package/memory.mjs +3 -0
  120. package/model-specs.mjs +131 -66
  121. package/notify-policy.mjs +51 -0
  122. package/package.json +1 -1
  123. package/peer-claims.mjs +263 -0
  124. package/peer-domains.mjs +100 -71
  125. package/peer-instances.mjs +9 -10
  126. package/process-probe-exec.mjs +187 -0
  127. package/process-probe.mjs +27 -175
  128. package/prompt-overlays.mjs +11 -10
  129. package/prompts/advisor-design.md +2 -2
  130. package/prompts/advisor-round2.md +1 -1
  131. package/prompts/advisor-round3.md +1 -1
  132. package/prompts/common.md +19 -7
  133. package/prompts/discipline-engineering.md +57 -16
  134. package/prompts/discipline-normal.md +6 -3
  135. package/prompts/persona-coder.md +0 -1
  136. package/prompts/persona-eng-coder.md +1 -5
  137. package/prompts/persona-eng-designer.md +1 -2
  138. package/prompts/persona-engineering.md +41 -11
  139. package/prompts/persona-explore.md +0 -1
  140. package/prompts/persona-normal.md +10 -1
  141. package/prompts/persona-plan.md +3 -4
  142. package/prompts/subagent-base.md +14 -0
  143. package/provider/core.mjs +11 -48
  144. package/provider/list-models.mjs +3 -0
  145. package/provider/responses-request.mjs +237 -0
  146. package/provider/responses.mjs +6 -228
  147. package/provider-flows.mjs +248 -0
  148. package/queued.mjs +90 -0
  149. package/rules.mjs +209 -2
  150. package/session-gc.mjs +67 -6
  151. package/session-guard.mjs +27 -7
  152. package/session-lifecycle.mjs +90 -18
  153. package/session-rename.mjs +5 -1
  154. package/session-slot-claims.mjs +230 -0
  155. package/session-slot-scan.mjs +299 -0
  156. package/session-slot-verify.mjs +304 -0
  157. package/session-slot-write.mjs +74 -8
  158. package/session-slots-manifest.mjs +163 -236
  159. package/session-slots.mjs +99 -71
  160. package/session.mjs +30 -10
  161. package/shell-candidates.mjs +78 -0
  162. package/text-budget.mjs +2 -2
  163. package/think-off.mjs +49 -0
  164. package/token-ttl.mjs +15 -4
  165. package/tool-args.mjs +73 -0
  166. package/tool-docs/advisor.md +9 -0
  167. package/tool-docs/batch.md +1 -0
  168. package/tool-docs/code_search.md +1 -0
  169. package/tool-docs/consult_start.md +5 -0
  170. package/tool-docs/consult_stop.md +3 -0
  171. package/tool-docs/context.md +7 -0
  172. package/tool-docs/doc_search.md +1 -0
  173. package/tool-docs/edit.md +2 -13
  174. package/tool-docs/eng.md +1 -0
  175. package/tool-docs/execute.md +1 -9
  176. package/tool-docs/git.md +25 -51
  177. package/tool-docs/goal.md +6 -0
  178. package/tool-docs/ledger_add.md +1 -0
  179. package/tool-docs/ledger_close.md +1 -0
  180. package/tool-docs/ledger_count.md +1 -0
  181. package/tool-docs/ledger_query.md +1 -0
  182. package/tool-docs/ledger_update.md +1 -0
  183. package/tool-docs/main_history.md +1 -0
  184. package/tool-docs/memory.md +10 -0
  185. package/tool-docs/notify_parent.md +9 -0
  186. package/tool-docs/peer_instances.md +1 -0
  187. package/tool-docs/plan.md +5 -0
  188. package/tool-docs/process.md +7 -3
  189. package/tool-docs/question.md +1 -0
  190. package/tool-docs/read_history.md +7 -0
  191. package/tool-docs/recent_changes.md +1 -0
  192. package/tool-docs/repo_outline.md +1 -0
  193. package/tool-docs/settings.md +6 -0
  194. package/tool-docs/skill.md +5 -0
  195. package/tool-docs/subagent.md +31 -0
  196. package/tool-docs/task.md +7 -0
  197. package/tool-docs/timer.md +6 -0
  198. package/tool-docs/verify.md +1 -0
  199. package/tool-docs/wait_for.md +2 -1
  200. package/tools/bash.mjs +28 -4
  201. package/tools/edit-batch.mjs +10 -11
  202. package/tools/edit-diff.mjs +33 -25
  203. package/tools/exec-run.mjs +102 -4
  204. package/tools/execute.mjs +5 -2
  205. package/tools/file.mjs +11 -12
  206. package/tools/git-run.mjs +1 -1
  207. package/tools/git.mjs +0 -24
  208. package/tools/glob-dialect.mjs +3 -3
  209. package/tools/index.mjs +2 -1
  210. package/tools/linter.mjs +2 -1
  211. package/tools/node-child.mjs +21 -0
  212. package/tools/ops.mjs +50 -13
  213. package/tools/patch.mjs +4 -2
  214. package/tools/repomap.mjs +20 -9
  215. package/tools/search.mjs +2 -2
  216. package/tools/shared.mjs +9 -7
  217. package/tools/web.mjs +5 -2
  218. package/tools/write-path.mjs +1 -1
  219. package/traces/trace-store.mjs +17 -14
  220. package/vision-reader.mjs +76 -0
  221. package/agent-tools/batch-segment.mjs +0 -18
@@ -12,24 +12,23 @@
12
12
  */
13
13
  import { readFile } from "node:fs/promises"
14
14
  import { resolveInCwd, normalizeEOL, joinWithEol, gitDiffOne, autoSyntaxCheck } from "./shared.mjs"
15
- // edit-batch ↔ edit-diff 循环引用(#69 后同型):两侧导入的都是函数声明(提升初始化),
15
+ // edit-batch ↔ edit-diff 循环引用:两侧导入的都是函数声明(提升初始化),
16
16
  // 仅在调用期使用——ESM 循环下安全(无模块求值期取值)。
17
- // 写盘经单一写路径点(§2.13.5)——本档不再直调 fs;回执组装迁 composeEditReceipt(#69)。
17
+ // 写盘经单一写路径点(§2.13.5)——本档不再直调 fs;回执组装共用 edit-diff 的 composeEditReceipt。
18
18
  import { writeThroughPath } from "./write-path.mjs"
19
19
  // EDIT.md §6:批量条目判定+应用共用 edit-diff(EDIT.md §4 分支 0 单行替换 + 行级 LCS——零重叠→替换即删);
20
20
  // EDIT.md §5:edits 互斥错误文本随前置校验分支迁出至 edit-diff.mjs。
21
- // #69:回执组装共用 edit-diff 的 composeEditReceipt(回执形态按端注入——缺省 = CLI 形态)。
22
- import { assertEditArgsExclusive, validateEditEntry, computeEditEntry, splitLines, EMPTY_NEW_STRING_LINE, deleteTarget, composeEditReceipt } from "./edit-diff.mjs"
21
+ // 回执组装共用 edit-diff 的 composeEditReceipt(单点——缺省回执 = base + 写入点上下文)。
22
+ import { assertEditArgsExclusive, assertEditsContainer, assertEditEntries, validateEditEntry, computeEditEntry, splitLines, EMPTY_NEW_STRING_LINE, EDIT_ENTRY_NO_PATH, EDIT_ABORT_PREFIX, editEntryLabel, deleteTarget, composeEditReceipt } from "./edit-diff.mjs"
23
23
 
24
24
  /**
25
25
  * Apply the `edits` array form: multi-file atomic replacement. Throws on any
26
26
  * failure (atomic — nothing written). Returns the per-entry result text (joined).
27
27
  */
28
28
  export async function applyEditBatch(args, ctx) {
29
- if (!Array.isArray(args.edits) || args.edits.length === 0) {
30
- throw new Error("edits must be a non-empty array of {path, old_string | line/startLine+endLine, new_string}")
31
- }
29
+ assertEditsContainer(args.edits)
32
30
  assertEditArgsExclusive(args)
31
+ assertEditEntries(args.edits) // #325 条目预扫(非对象条目 ⇒ 含下标成形错误;不读盘)
33
32
  // 原子:先全量检查(所有文件的替换都可执行)——任一失败全不写。
34
33
  // 2026-09-01 缺陷修复(TOOLS.md §9 ②"同文件多条规则"):同一 path 的多条编辑
35
34
  // 按序**串行累积应用**——第 n 条基于前 n-1 条已应用后的累积内容做匹配与替换
@@ -40,9 +39,9 @@ export async function applyEditBatch(args, ctx) {
40
39
  // 2026-09-05 用户裁定:顶层 path + edits 并存合法化——`e.path || args.path`(条目优先;
41
40
  // 缺省入口仅在此兜底);两者皆无 → 原错误文本(措辞补顶层选项)。
42
41
  const p = e.path || args.path
43
- if (!p) throw new Error("each edit must have a path — give each entry its own path or pass a top-level path")
42
+ if (!p) throw new Error(EDIT_ENTRY_NO_PATH)
44
43
  // 前置校验(空 old / 非字符串 new——文本同 edit-diff 批量形态;读盘前校验)
45
- validateEditEntry(e, { label: `edit for ${p}: `, rich: false })
44
+ validateEditEntry(e, { label: editEntryLabel(p), rich: false })
46
45
  const abs = resolveInCwd(ctx, p)
47
46
  let g = groups.get(abs)
48
47
  if (!g) {
@@ -61,7 +60,7 @@ export async function applyEditBatch(args, ctx) {
61
60
  const out = computeEditEntry(g.content, e, {
62
61
  path: g.path,
63
62
  absPath: g.abs,
64
- abortPrefix: "edit aborted (atomic — no files written): ",
63
+ abortPrefix: EDIT_ABORT_PREFIX,
65
64
  })
66
65
  prepared.push({
67
66
  g,
@@ -92,7 +91,7 @@ export async function applyEditBatch(args, ctx) {
92
91
  for (const p of prepared) {
93
92
  // #4(2026-09-01 交付评审尾巴):与单文件路径对齐——每条结果附 git diff +
94
93
  // autoSyntaxCheck(同文件多条会重复 diff/检查,换取格式一致、实现零分支)
95
- const diff = gitDiffOne(ctx.cwd, p.g.abs)
94
+ const diff = await gitDiffOne(ctx.cwd, p.g.abs)
96
95
  const base = p.deleted
97
96
  ? `Deleted ${deleteTarget(p)} of ${p.g.path}${diff ? "\n" + diff : ""}${await autoSyntaxCheck(p.g.abs)}`
98
97
  : `Edited ${p.g.path}: replaced ${p.occurrences} occurrence(s)${p.note ? ` — ${p.note}` : ""}${diff ? "\n" + diff : ""}${await autoSyntaxCheck(p.g.abs)}`
@@ -45,6 +45,18 @@ export const EMPTY_NEW_STRING = "empty new_string — for deletion, keep the con
45
45
  export const EMPTY_NEW_STRING_LINE = "empty new_string with line-based targeting is an explicit error — OMIT new_string to delete the line/range (deleting by number is explicit intent; an empty replacement is a mistake, not a delete)"
46
46
  export const EDIT_ARGS_MUTEX = "edits array is mutually exclusive with top-level old_string/new_string/line/startLine/endLine — a top-level path is allowed (default for entries without their own path); provide each change's targeting (old_string or line range) and new_string inside its edits entry"
47
47
 
48
+ // ─── #325 入参守卫文案(单源——EDIT.md §5 / §8 D-7:核 edit-batch 与 ACP 桥 bridge.mjs 同调用)───
49
+ /** 容器级成形错误(真值非数组 / 空数组——同一错误面)。 */
50
+ export const EDITS_CONTAINER_ERROR = "edits must be a non-empty array of {path, old_string | line/startLine+endLine, new_string}"
51
+ /** 条目级成形错误(i = 数组下标;非对象条目 = null / 字符串 / 数字 / 数组)。 */
52
+ export const editsEntryError = (i) => `edits[${i}] must be an object of {path, old_string | line/startLine+endLine, new_string}`
53
+ /** 条目缺 path 且无顶层默认路径。 */
54
+ export const EDIT_ENTRY_NO_PATH = "each edit must have a path — give each entry its own path or pass a top-level path"
55
+ /** 批量原子失败前缀(条目判定失败时拼在消息前的 abortPrefix)。 */
56
+ export const EDIT_ABORT_PREFIX = "edit aborted (atomic — no files written): "
57
+ /** 批量条目校验前缀(validateEditEntry 的 opts.label)。 */
58
+ export const editEntryLabel = (path) => `edit for ${path}: `
59
+
48
60
  /** D1(2026-09-08):条目带按行号改参数(line / startLine / endLine 任一)。 */
49
61
  export function hasLineParams(entry) {
50
62
  return entry.line !== undefined || entry.startLine !== undefined || entry.endLine !== undefined
@@ -132,13 +144,28 @@ export function assertEditArgsExclusive(args) {
132
144
  }
133
145
  }
134
146
 
147
+ /** 入参容器守卫(#325——EDIT.md §5):edits 真值非数组 / 空数组 ⇒ 成形错误(同句单源——
148
+ * 核 edit-batch 与 ACP 桥 bridge.mjs 同调用)。批量判定序:容器 → 互斥 → 条目预扫 → 条目循环。 */
149
+ export function assertEditsContainer(edits) {
150
+ if (!Array.isArray(edits) || edits.length === 0) throw new Error(EDITS_CONTAINER_ERROR)
151
+ }
152
+
153
+ /** 入参条目守卫(#325——EDIT.md §5):条目非对象(null / 字符串 / 数字 / 数组)⇒ 含下标成形
154
+ * 错误——预扫于条目循环之前(不读盘、不触盘、零部分应用)。 */
155
+ export function assertEditEntries(edits) {
156
+ for (let i = 0; i < edits.length; i++) {
157
+ const e = edits[i]
158
+ if (e === null || typeof e !== "object" || Array.isArray(e)) throw new Error(editsEntryError(i))
159
+ }
160
+ }
161
+
135
162
  /**
136
163
  * 前置校验——内容形态:空 old / 非字符串 new;按行号改形态(D1,2026-09-08;阶段 2
137
164
  * 删行——EDIT.md §5):与 old_string 互斥 / line 与 startLine|endLine 互斥 /
138
165
  * startLine+endLine 须成对 / 正整数 / endLine ≥ startLine / replace_all 不适用 /
139
166
  * new_string:省略(undefined)= 删行 / 显式空串 = 显式错(防误删)/ 其他类型报错。
140
167
  * error 文本按调用形态(单形态 rich / 批量 label)。
141
- * opts: { label = ""(批量前缀 "edit for <path>: "), rich = true(单形态完整文本) }
168
+ * opts: { label = ""(批量前缀 = editEntryLabel(path)), rich = true(单形态完整文本) }
142
169
  */
143
170
  export function validateEditEntry(entry, opts = {}) {
144
171
  const label = opts.label ?? ""
@@ -213,7 +240,7 @@ function isSingleLineReplace(entry) {
213
240
  * LCS 替换 / replace_all 字面)→
214
241
  * 元数据(受影响区首行/行数差/次数——recordWrite 与结果回显用)。
215
242
  * 返回 { updated, editStartLine, lineShift, occurrences, note?, deleted? };失败抛错(含路径/引导)。
216
- * opts: { path, abortPrefix(批量 "edit aborted (atomic — no files written): ") }
243
+ * opts: { path, abortPrefix(批量 = EDIT_ABORT_PREFIX) }
217
244
  */
218
245
  /**
219
246
  * P15.11(2026-09-05 用户裁定——edit 连续失败分析):old_string 逐字 not-found 时——
@@ -345,7 +372,7 @@ export async function runSingleEdit(args, ctx) {
345
372
  op: "write",
346
373
  record: { type: "edit", startLine: out.editStartLine, shift: out.lineShift },
347
374
  })
348
- const diff = gitDiffOne(ctx.cwd, abs)
375
+ const diff = await gitDiffOne(ctx.cwd, abs)
349
376
  const baseResult = out.deleted
350
377
  ? `Deleted ${deleteTarget(out)} of ${args.path}${diff ? "\n" + diff : ""}${await autoSyntaxCheck(abs)}`
351
378
  : `Edited ${args.path}: replaced ${out.occurrences} occurrence(s)${out.note ? ` — ${out.note}` : ""}${diff ? "\n" + diff : ""}${await autoSyntaxCheck(abs)}`
@@ -360,29 +387,10 @@ export async function runSingleEdit(args, ctx) {
360
387
  })
361
388
  }
362
389
 
363
- // ─── edit 回执形态注入缝(#69——「回执形态按端注入」,形态参 §2.13.5 注入缝)──────────
364
390
  /**
365
- * 回执组装注入位(**缺省不覆盖** = CLI 回执形态(base + 写入点上下文)——零行为变;
366
- * 端装配层可覆盖为本端回执形态)。核内零端名分支(契约 5——本档只认 `format` 函数名)。
367
- * 契约:`format(fields) → string | null | undefined | Promise<同>`——null / undefined =
368
- * 未处理 ⇒ 回默认回执;其余返回值即该条回执。
369
- */
370
- let injectedReceipt = null
371
- export function configureEditReceipt(impl) {
372
- injectedReceipt = impl && typeof impl.format === "function" ? impl : null
373
- }
374
- /** 撤销注入(测试与端装配生命周期用——缺省态 = CLI 回执)。 */
375
- export function resetEditReceipt() { injectedReceipt = null }
376
-
377
- /**
378
- * edit 回执组装单点(单形态 / 数组形态共用——#69):先组默认回执(base + 写入点上下文),
379
- * 注入面命中 ⇒ 以其 format(fields) 返回值为准(`fields.default` = 默认回执原文,供端侧改写)。
391
+ * edit 回执组装单点(单形态 / 数组形态共用):`base` + 写入点上下文——唯一形态
392
+ * (#69 注入面已退场;余字段为调用面统一形,不参与组装)。
380
393
  */
381
394
  export async function composeEditReceipt(fields) {
382
- const defaultText = await appendWriteContext(fields.abs, fields.writeLine, fields.base)
383
- if (injectedReceipt) {
384
- const out = await injectedReceipt.format({ ...fields, default: defaultText })
385
- if (out !== null && out !== undefined) return String(out)
386
- }
387
- return defaultText
395
+ return await appendWriteContext(fields.abs, fields.writeLine, fields.base)
388
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
  }
@@ -257,11 +252,15 @@ export const editTool = {
257
252
  touchedPaths(args) {
258
253
  // 2026-09-05 用户裁定:顶层 path = 批默认——仅当有条目缺 path 时计入(条目全带 path
259
254
  // 时顶层 path 不实际使用——不虚报进 _touchedFiles)
260
- if (args.edits) {
261
- const out = args.edits.map((e) => e.path).filter(Boolean)
262
- if (args.path && args.edits.some((e) => !e.path)) out.push(args.path)
255
+ // #325:本钩子**零抛**(execute 之前被多处消费,首个未守卫点即会逸出裸错)——形态
256
+ // 拒绝统一归 execute(edit-batch/桥 同单源成形错误);真值非数组 ⇒ 返 []、零触达
257
+ // (不虚报顶层 path);非对象条目按「缺 path」尽力提取(e?.path)。
258
+ if (Array.isArray(args.edits)) {
259
+ const out = args.edits.map((e) => e?.path).filter(Boolean)
260
+ if (args.path && args.edits.some((e) => !e?.path)) out.push(args.path)
263
261
  return out
264
262
  }
263
+ if (args.edits) return [] // 真值非数组:调用必被 execute 拒绝(成形错误)——零触达
265
264
  return args.path ? [args.path] : []
266
265
  },
267
266
  async execute(args, ctx) {
@@ -361,7 +360,7 @@ export const insertAfterTool = {
361
360
  op: "write",
362
361
  record: { type: "insert", startLine: targetLine, shift: normalizeEOL(args.content).split("\n").length },
363
362
  })
364
- const diff = gitDiffOne(ctx.cwd, abs)
363
+ const diff = await gitDiffOne(ctx.cwd, abs)
365
364
  const baseResult = `Inserted after line ${targetLine} in ${args.path}${diff ? "\n" + diff : ""}${await autoSyntaxCheck(abs)}`
366
365
  return await appendWriteContext(abs, targetLine + 1, baseResult)
367
366
  },
@@ -456,7 +455,7 @@ export const hashlineEditTool = {
456
455
  op: "write",
457
456
  record: { type: "edit", startLine: pos + 1, shift: newLines.length - target.length },
458
457
  })
459
- const diff = gitDiffOne(ctx.cwd, abs)
458
+ const diff = await gitDiffOne(ctx.cwd, abs)
460
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}` : ""}`
461
460
  return await appendWriteContext(abs, pos + 1, baseResult)
462
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
  },
@@ -1,5 +1,5 @@
1
1
  /**
2
- * glob-dialect.mjs — glob 方言(TOOLS.md §17 — 2026-09-06)单一权威模块(CLI)。
2
+ * glob-dialect.mjs — glob 方言(TOOLS.md §6.7 — 2026-09-06)单一权威模块(CLI)。
3
3
  *
4
4
  * `{a,b}` brace expansion is handled BEFORE the sentinel/escape flow (braces must
5
5
  * not be literal-escaped — a bare "star-star slash star .{js,txt}" pattern
@@ -12,7 +12,7 @@
12
12
  * intersects includes and excludes.
13
13
  *
14
14
  * Extracted from shared.mjs (2026-09-06 advisor #5 — shared.mjs exceeded the
15
- * 500-line cap after §17); shared.mjs re-exports these symbols so all existing
15
+ * 500-line cap after the TOOLS.md §6.7 move); shared.mjs re-exports these symbols so all existing
16
16
  * importers (system.mjs / ls filter / tests) keep their import paths.
17
17
  */
18
18
 
@@ -77,7 +77,7 @@ export function globToRegex(pattern) {
77
77
  /** Whitespace-split a glob expression into parts WITHOUT splitting inside {a,b}
78
78
  * brace groups and WITHOUT splitting literal spaces inside a single pattern
79
79
  * (2026-09-06 audit F1 / advisor #4): a whitespace run is a pattern separator
80
- * ONLY when the next non-space token starts an exclusion ("!" — the §17
80
+ * ONLY when the next non-space token starts an exclusion ("!" — the TOOLS.md §6.7
81
81
  * "include !exclude" form). So a BARE pattern like "docs/my file/*.md" and an
82
82
  * exclude like "!docs/my file/**" each stay ONE part; the standard
83
83
  * "js-pattern 空格 !test-pattern" form still splits into its two parts. */
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
+ }