@thincoder/core 0.9.5 → 0.10.2
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.
- package/CHANGELOG.md +49 -0
- package/README.md +1 -1
- package/abort-provenance.mjs +10 -10
- package/advisor/citations.mjs +5 -5
- package/advisor/compaction.mjs +16 -15
- package/advisor/loop.mjs +12 -12
- package/advisor/messages.mjs +6 -6
- package/advisor/project-context.mjs +5 -5
- package/advisor/repos.mjs +3 -3
- package/advisor/run.mjs +22 -12
- package/advisor.mjs +4 -4
- package/agent/assemble.mjs +111 -0
- package/agent/chat-call.mjs +68 -0
- package/agent/child-marks.mjs +2 -2
- package/agent/completion.mjs +9 -2
- package/agent/continue-decision.mjs +25 -0
- package/agent/dispatch-gates.mjs +140 -0
- package/agent/dispatch-run.mjs +167 -0
- package/agent/dispatch.mjs +54 -296
- package/agent/family-tools.mjs +5 -5
- package/agent/helpers.mjs +53 -3
- package/agent/live-beat.mjs +49 -0
- package/agent/post-turn.mjs +4 -8
- package/agent/record-results.mjs +23 -20
- package/agent/relay-prefix.mjs +2 -2
- package/agent/run-stages.mjs +15 -11
- package/agent/run-start.mjs +111 -0
- package/agent/setup-reminders.mjs +9 -9
- package/agent/setup.mjs +32 -4
- package/agent/spawn-child.mjs +9 -9
- package/agent/suspension.mjs +89 -29
- package/agent/timers.mjs +113 -0
- package/agent/turn-loop.mjs +244 -0
- package/agent/write-gate.mjs +103 -6
- package/agent-tools/advisor-async.mjs +9 -9
- package/agent-tools/advisor-settle.mjs +12 -12
- package/agent-tools/advisor.mjs +36 -41
- package/agent-tools/async-discard.mjs +44 -1
- package/agent-tools/async-settle.mjs +21 -18
- package/agent-tools/audit-block.mjs +7 -7
- package/agent-tools/bash-async.mjs +281 -0
- package/agent-tools/batch-lifecycle.mjs +57 -18
- package/agent-tools/batch-paths.mjs +52 -8
- package/agent-tools/batch-skeleton.mjs +19 -1
- package/agent-tools/batch.mjs +12 -65
- package/agent-tools/checkpoint.mjs +121 -0
- package/agent-tools/child-permission.mjs +5 -3
- package/agent-tools/consult.mjs +56 -56
- package/agent-tools/context.mjs +4 -20
- package/agent-tools/design-token.mjs +3 -3
- package/agent-tools/digest-budget.mjs +1 -1
- package/agent-tools/eng.mjs +5 -5
- package/agent-tools/escalate-async.mjs +16 -11
- package/agent-tools/goal.mjs +3 -10
- package/agent-tools/panel-blocks.mjs +2 -2
- package/agent-tools/parent-channel.mjs +3 -13
- package/agent-tools/plan.mjs +3 -4
- package/agent-tools/read-history.mjs +12 -28
- package/agent-tools/recent-changes.mjs +4 -5
- package/agent-tools/review-facts.mjs +1 -1
- package/agent-tools/settings.mjs +47 -20
- package/agent-tools/skill.mjs +3 -5
- package/agent-tools/spawn-gates.mjs +3 -3
- package/agent-tools/subagent-actions-query.mjs +270 -0
- package/agent-tools/subagent-actions.mjs +21 -265
- package/agent-tools/subagent-async.mjs +25 -20
- package/agent-tools/subagent-panel.mjs +33 -4
- package/agent-tools/subagent-run.mjs +36 -21
- package/agent-tools/subagent-scheduler.mjs +28 -27
- package/agent-tools/subagent-spawn.mjs +45 -14
- package/agent-tools/subagent.mjs +33 -53
- package/agent-tools/task.mjs +3 -9
- package/agent-tools/timer.mjs +12 -12
- package/agent-tools/verify.mjs +14 -13
- package/agent-tools.mjs +0 -1
- package/agent.mjs +18 -355
- package/attachments.mjs +125 -0
- package/auto-think.mjs +1 -1
- package/config-presets.mjs +11 -6
- package/config-watch.mjs +83 -0
- package/config.mjs +29 -12
- package/context-degrade.mjs +81 -0
- package/context-echo.mjs +88 -0
- package/context-push.mjs +51 -0
- package/context.mjs +8 -187
- package/conventions.mjs +110 -161
- package/declaration.mjs +194 -0
- package/embedding.mjs +3 -1
- package/env-ref.mjs +81 -0
- package/explore-distill.mjs +2 -3
- package/file-links.mjs +53 -0
- package/file-refs.mjs +103 -0
- package/generate-title.mjs +28 -4
- package/history-window.mjs +17 -2
- package/i18n.mjs +2 -0
- package/index-discover.mjs +7 -7
- package/ledger-cmd.mjs +34 -126
- package/ledger-db.mjs +10 -2
- package/ledger-tools.mjs +188 -0
- package/ledger.mjs +9 -4
- package/log.mjs +12 -7
- package/manifest-discovery.mjs +131 -0
- package/manifest-schema.mjs +166 -0
- package/manifest.mjs +18 -217
- package/mcp.mjs +4 -1
- package/memory/code-search.mjs +149 -0
- package/memory/code-sync.mjs +89 -230
- package/memory/core.mjs +47 -32
- package/memory/docs.mjs +63 -262
- package/memory/file-list.mjs +89 -0
- package/memory/file-walk.mjs +7 -4
- package/memory/memory-tool.mjs +229 -0
- package/memory/schema.mjs +18 -3
- package/memory/sweep.mjs +289 -0
- package/memory/sync-tail.mjs +81 -0
- package/memory-status.mjs +69 -0
- package/memory.mjs +3 -0
- package/model-ref.mjs +96 -3
- package/model-specs.mjs +16 -7
- package/notify-policy.mjs +51 -0
- package/package.json +1 -1
- package/peer-claims.mjs +2 -2
- package/peer-domains.mjs +7 -11
- package/peer-instances.mjs +9 -10
- package/process-probe-exec.mjs +187 -0
- package/process-probe.mjs +27 -175
- package/prompt-overlays.mjs +11 -10
- package/prompts/advisor-design.md +2 -2
- package/prompts/advisor-round2.md +1 -1
- package/prompts/advisor-round3.md +1 -1
- package/prompts/common.md +16 -8
- package/prompts/discipline-engineering.md +57 -16
- package/prompts/discipline-normal.md +6 -3
- package/prompts/persona-coder.md +0 -1
- package/prompts/persona-eng-coder.md +1 -5
- package/prompts/persona-eng-designer.md +2 -3
- package/prompts/persona-engineering.md +32 -11
- package/prompts/persona-explore.md +0 -1
- package/prompts/persona-normal.md +1 -1
- package/prompts/persona-plan.md +3 -4
- package/prompts/subagent-base.md +14 -0
- package/provider/core.mjs +11 -48
- package/provider/list-models.mjs +3 -0
- package/provider/responses-request.mjs +237 -0
- package/provider/responses.mjs +6 -228
- package/provider-flows.mjs +248 -0
- package/queued.mjs +90 -0
- package/rules.mjs +209 -2
- package/session-guard.mjs +27 -7
- package/session-lifecycle.mjs +108 -27
- package/session-rename.mjs +5 -1
- package/session-slot-claims.mjs +230 -0
- package/session-slot-scan.mjs +299 -0
- package/session-slot-verify.mjs +304 -0
- package/session-slot-write.mjs +74 -8
- package/session-slots-manifest.mjs +163 -241
- package/session-slots.mjs +99 -71
- package/session.mjs +30 -10
- package/shell-candidates.mjs +78 -0
- package/text-budget.mjs +2 -2
- package/think-off.mjs +49 -0
- package/token-ttl.mjs +15 -4
- package/tool-args.mjs +73 -0
- package/tool-docs/advisor.md +9 -0
- package/tool-docs/batch.md +1 -0
- package/tool-docs/code_search.md +1 -0
- package/tool-docs/consult_start.md +5 -0
- package/tool-docs/consult_stop.md +3 -0
- package/tool-docs/context.md +7 -0
- package/tool-docs/doc_search.md +1 -0
- package/tool-docs/edit.md +2 -13
- package/tool-docs/eng.md +1 -0
- package/tool-docs/execute.md +1 -9
- package/tool-docs/git.md +25 -51
- package/tool-docs/goal.md +6 -0
- package/tool-docs/ledger_add.md +1 -0
- package/tool-docs/ledger_close.md +1 -0
- package/tool-docs/ledger_count.md +1 -0
- package/tool-docs/ledger_query.md +1 -0
- package/tool-docs/ledger_update.md +1 -0
- package/tool-docs/main_history.md +1 -0
- package/tool-docs/memory.md +10 -0
- package/tool-docs/notify_parent.md +9 -0
- package/tool-docs/peer_instances.md +1 -0
- package/tool-docs/plan.md +5 -0
- package/tool-docs/process.md +7 -3
- package/tool-docs/read_history.md +7 -0
- package/tool-docs/recent_changes.md +1 -0
- package/tool-docs/repo_outline.md +1 -0
- package/tool-docs/settings.md +6 -0
- package/tool-docs/skill.md +5 -0
- package/tool-docs/subagent.md +31 -0
- package/tool-docs/task.md +7 -0
- package/tool-docs/timer.md +6 -0
- package/tool-docs/verify.md +1 -0
- package/tool-docs/wait_for.md +2 -1
- package/tools/bash.mjs +28 -4
- package/tools/edit-batch.mjs +4 -4
- package/tools/edit-diff.mjs +4 -23
- package/tools/exec-run.mjs +102 -4
- package/tools/execute.mjs +5 -2
- package/tools/file.mjs +4 -9
- package/tools/git-run.mjs +1 -1
- package/tools/git.mjs +0 -24
- package/tools/index.mjs +2 -1
- package/tools/linter.mjs +2 -1
- package/tools/node-child.mjs +21 -0
- package/tools/ops.mjs +50 -13
- package/tools/patch.mjs +4 -2
- package/tools/repomap.mjs +20 -9
- package/tools/shared.mjs +8 -6
- package/tools/web.mjs +5 -2
- package/tools/write-path.mjs +1 -1
- package/traces/trace-store.mjs +17 -14
- package/vision-reader.mjs +76 -0
- package/agent-tools/batch-segment.mjs +0 -18
package/tools/edit-diff.mjs
CHANGED
|
@@ -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
|
-
*
|
|
393
|
-
*
|
|
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
|
-
|
|
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
|
}
|
package/tools/exec-run.mjs
CHANGED
|
@@ -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
|
|
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
|
-
* 超时错误文本——带重试引导(
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
|
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 (
|
|
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 批 §
|
|
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 批 §
|
|
187
|
-
* (agent._asyncAdvisors ∪ history._asyncAdvisors)无 running/queued 条目(与 §
|
|
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
|
|
232
|
-
// 不再读子代理池的 role==="advisor" 条目(该池永无此类条目);语义与 §
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 =
|
|
224
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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)
|