thincoder 0.12.58 → 0.12.60
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 +78 -2
- package/README.md +3 -3
- package/bin/thincoder.mjs +88 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +135 -26
- package/src/advisor/messages.mjs +57 -4
- package/src/advisor/run.mjs +119 -79
- package/src/advisor.mjs +34 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +182 -22
- package/src/agent/helpers.mjs +71 -4
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +107 -20
- package/src/agent/spawn-child.mjs +54 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +133 -109
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +154 -104
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +26 -30
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +284 -0
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +152 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +470 -0
- package/src/agent-tools/subagent-async.mjs +382 -0
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +343 -0
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +203 -377
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +198 -238
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +145 -242
- package/src/auto-think.mjs +14 -0
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +4 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +61 -8
- package/src/context.mjs +81 -163
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +30 -12
- package/src/escape.mjs +6 -4
- package/src/explore-distill.mjs +155 -0
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +11 -72
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +206 -87
- package/src/memory.mjs +3 -1
- package/src/model-specs.mjs +15 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +18 -39
- package/src/prompts/advisor-round1.md +20 -32
- package/src/prompts/advisor-round2.md +16 -16
- package/src/prompts/advisor-round3.md +16 -16
- package/src/prompts/coder.md +7 -28
- package/src/prompts/consult-base.md +4 -11
- package/src/prompts/discipline.md +31 -44
- package/src/prompts/eng-coder.md +9 -34
- package/src/prompts/engineering-sub.md +10 -8
- package/src/prompts/engineering.md +61 -264
- package/src/prompts/explore.md +4 -14
- package/src/prompts/main.md +18 -35
- package/src/prompts/methodology-template.md +32 -38
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +18 -35
- package/src/provider/core.mjs +62 -69
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +131 -44
- package/src/tools/edit-diff.mjs +348 -0
- package/src/tools/edit.md +20 -13
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +55 -24
- package/src/tools/file.mjs +25 -70
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -3
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +84 -13
- package/src/tools/question.md +5 -1
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +18 -25
- package/src/tools/system.mjs +50 -30
- package/src/tools/tree.md +2 -1
- package/src/tools/wait_for.md +22 -0
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +179 -27
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +77 -16
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +17 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +11 -11
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +31 -96
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +46 -0
- package/src/tui/pickers.mjs +51 -25
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +182 -0
- package/src/tui/startup.mjs +40 -0
- package/src/tui/subagent-blocks.mjs +272 -262
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +125 -12
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +127 -231
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +76 -0
- package/src/tui/wizard.mjs +48 -12
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
package/src/tools/ops.mjs
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ops.mjs — operational tools: file_ops (move/copy/rename), process (list),
|
|
3
|
-
* get_current_time. Each exists so the model reaches
|
|
4
|
-
* instead of shelling out to `bash` for the same operation
|
|
3
|
+
* get_current_time, wait_for (condition wait). Each exists so the model reaches
|
|
4
|
+
* for a dedicated tool instead of shelling out to `bash` for the same operation
|
|
5
|
+
* (parity with thinworker).
|
|
5
6
|
*/
|
|
6
7
|
import { DESC, resolveInCwd, truncate } from "./shared.mjs"
|
|
7
8
|
import { cp, rename, rm } from "node:fs/promises"
|
|
9
|
+
import { existsSync } from "node:fs"
|
|
8
10
|
import { execFileSync } from "node:child_process"
|
|
11
|
+
import net from "node:net"
|
|
9
12
|
|
|
10
13
|
// ─── file_ops ──────────────────────────────────────────────────
|
|
11
14
|
|
|
@@ -111,4 +114,173 @@ export const getCurrentTimeTool = {
|
|
|
111
114
|
const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
|
|
112
115
|
return `Date: ${now.toISOString()} (UTC)\nTimezone: ${tz}\nWeekday: ${days[now.getDay()]}\nLocal: ${now.toLocaleString()}`
|
|
113
116
|
},
|
|
114
|
-
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── wait_for ───────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
/** wait_for bounds (TOOLS.md §16): a BUILT-IN ceiling replaces the unbounded
|
|
122
|
+
* sleep-then-wait — timeout_ms defaults to 30s (config.json agent.waitForTimeoutMs
|
|
123
|
+
* overrides), capped at WAIT_FOR_MAX_TIMEOUT_MS (600s — the execute-tool timeout
|
|
124
|
+
* convention, Math.min(t, 600_000)); interval_ms defaults to 1s with a 100ms
|
|
125
|
+
* floor so polling never busy-spins. */
|
|
126
|
+
export const WAIT_FOR_DEFAULT_TIMEOUT_MS = 30_000
|
|
127
|
+
export const WAIT_FOR_MAX_TIMEOUT_MS = 600_000
|
|
128
|
+
export const WAIT_FOR_DEFAULT_INTERVAL_MS = 1_000
|
|
129
|
+
export const WAIT_FOR_MIN_INTERVAL_MS = 100
|
|
130
|
+
|
|
131
|
+
// Condition-source seam (§16 评审 #4): production uses the real evaluator
|
|
132
|
+
// (evaluateWaitForCondition); tests inject a deterministic fake source via
|
|
133
|
+
// setWaitForConditionSource (default null — production path unchanged).
|
|
134
|
+
let injectedConditionSource = null
|
|
135
|
+
export function setWaitForConditionSource(source) {
|
|
136
|
+
injectedConditionSource = typeof source === "function" ? source : null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Parse a wait_for condition into { kind, arg }. Unknown conditions are an
|
|
140
|
+
* EXPLICIT error enumerating the supported forms (评审 #2 — never a silent
|
|
141
|
+
* wait on a misspelled condition). */
|
|
142
|
+
export function parseWaitForCondition(condition) {
|
|
143
|
+
const c = String(condition ?? "").trim()
|
|
144
|
+
let m
|
|
145
|
+
if (c === "advisor settled") return { kind: "advisor", arg: null }
|
|
146
|
+
if (c === "consult done") return { kind: "consult", arg: null }
|
|
147
|
+
if ((m = c.match(/^subagent id:\s*(\d+) done$/))) return { kind: "subagent", arg: String(Number(m[1])) }
|
|
148
|
+
if ((m = c.match(/^file exists:\s*(.+)$/))) return { kind: "file", arg: m[1].trim() }
|
|
149
|
+
if ((m = c.match(/^port open:\s*(\d+)$/))) return { kind: "port", arg: Number(m[1]) }
|
|
150
|
+
throw new Error(
|
|
151
|
+
`wait_for: unsupported condition "${c}" — supported: "advisor settled", "subagent id:N done", "consult done", "file exists:path", "port open:N"`,
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Async-subagent pool of an agent — ctx.agent 可达性(评审 #1):dispatch 把
|
|
156
|
+
* agent 实例放进工具 ctx(agent/dispatch.mjs toolCtx.agent),池挂在 agent 上
|
|
157
|
+
* (深度 0 的 VS Code 侧 agent._asyncSubagents 与 history._asyncSubagents 同
|
|
158
|
+
* 一 Map——见 agent.mjs;CLI 侧直接是 agent._asyncSubagents)。 */
|
|
159
|
+
function asyncPool(agent) {
|
|
160
|
+
if (!agent) return new Map()
|
|
161
|
+
if (agent._asyncSubagents instanceof Map) return agent._asyncSubagents
|
|
162
|
+
return agent.history?._asyncSubagents instanceof Map ? agent.history._asyncSubagents : new Map()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** "subagent id:N done" — the async entry for N settled (status done / done flag)
|
|
166
|
+
* or already left the pool (moved to pending results / digested / never spawned —
|
|
167
|
+
* nothing is left to wait on). Absent ids are done by vacuity: the model waits on
|
|
168
|
+
* ids its own spawn ack just returned, so an id no longer in the pool means done.
|
|
169
|
+
* Dual key lookup (2026-09-06 advisor #1): VS Code pool keys are the numeric
|
|
170
|
+
* spawn-time id, the CLI keys them as String(id) — accept both. */
|
|
171
|
+
function asyncEntryDone(agent, id) {
|
|
172
|
+
const pool = asyncPool(agent)
|
|
173
|
+
const key = String(id)
|
|
174
|
+
const entry = pool.get(key) ?? pool.get(Number(key)) ?? null
|
|
175
|
+
if (!entry) return true
|
|
176
|
+
return entry.done === true || entry.status === "done"
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Any async entry matching pred that is still running/queued (not done). */
|
|
180
|
+
function hasRunningAsync(agent, pred) {
|
|
181
|
+
for (const e of asyncPool(agent).values()) {
|
|
182
|
+
if (pred(e) && !(e.done === true || e.status === "done")) return true
|
|
183
|
+
}
|
|
184
|
+
return false
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** "consult done" — every consult session has drained (pending 0) or was
|
|
188
|
+
* explicitly stopped (its children were aborted). No sessions → true (vacuously). */
|
|
189
|
+
function consultAllDone(agent) {
|
|
190
|
+
const sessions = agent?._consultSessions
|
|
191
|
+
if (!sessions || sessions.size === 0) return true
|
|
192
|
+
for (const s of sessions.values()) {
|
|
193
|
+
if (!s.stopped && (s.pending ?? 0) > 0) return false
|
|
194
|
+
}
|
|
195
|
+
return true
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** "port open:N" — 127.0.0.1 probe with a short connect timeout (never hangs a poll). */
|
|
199
|
+
function portOpenProbe(port) {
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
const socket = net.connect({ host: "127.0.0.1", port, timeout: 400 })
|
|
202
|
+
let settled = false
|
|
203
|
+
const finish = (v) => {
|
|
204
|
+
if (settled) return
|
|
205
|
+
settled = true
|
|
206
|
+
socket.destroy()
|
|
207
|
+
resolve(v)
|
|
208
|
+
}
|
|
209
|
+
socket.once("connect", () => finish(true))
|
|
210
|
+
socket.once("error", () => finish(false))
|
|
211
|
+
socket.once("timeout", () => finish(false))
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Real condition evaluator (production path) — returns a boolean; throws on an
|
|
216
|
+
* unsupported condition string. Polling is async (await setTimeout) — the event
|
|
217
|
+
* loop keeps running so background async work (subagents/consult) can settle
|
|
218
|
+
* between polls (评审 #1 — no blocking spin). */
|
|
219
|
+
export async function evaluateWaitForCondition(condition, ctx) {
|
|
220
|
+
const parsed = parseWaitForCondition(condition)
|
|
221
|
+
const agent = ctx?.agent
|
|
222
|
+
switch (parsed.kind) {
|
|
223
|
+
case "advisor":
|
|
224
|
+
// Advisor reviews run as blocking tool calls (runAdvisorReview) or as
|
|
225
|
+
// advisor-role async children — "settled" = no advisor-role entry still
|
|
226
|
+
// running/queued in the async pool.
|
|
227
|
+
return !hasRunningAsync(agent, (e) => e.role === "advisor")
|
|
228
|
+
case "subagent":
|
|
229
|
+
return asyncEntryDone(agent, parsed.arg)
|
|
230
|
+
case "consult":
|
|
231
|
+
return consultAllDone(agent)
|
|
232
|
+
case "file":
|
|
233
|
+
return existsSync(resolveInCwd(ctx, parsed.arg))
|
|
234
|
+
case "port":
|
|
235
|
+
return await portOpenProbe(parsed.arg)
|
|
236
|
+
default:
|
|
237
|
+
return false
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function evalConditionSource(condition, ctx) {
|
|
242
|
+
if (injectedConditionSource) return injectedConditionSource(condition, ctx)
|
|
243
|
+
return evaluateWaitForCondition(condition, ctx)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export const waitForTool = {
|
|
247
|
+
name: "wait_for",
|
|
248
|
+
description: DESC("wait_for"),
|
|
249
|
+
parameters: {
|
|
250
|
+
type: "object",
|
|
251
|
+
properties: {
|
|
252
|
+
condition: { type: "string", description: 'Condition expression — "advisor settled", "subagent id:N done", "consult done", "file exists:path", "port open:N"' },
|
|
253
|
+
interval_ms: { type: "integer", description: "Poll interval in ms (default 1000, floor 100)" },
|
|
254
|
+
timeout_ms: { type: "integer", description: "Overall timeout in ms (default 30000, cap 600000; config.json agent.waitForTimeoutMs overrides the default)" },
|
|
255
|
+
},
|
|
256
|
+
required: ["condition"],
|
|
257
|
+
},
|
|
258
|
+
readonly: true,
|
|
259
|
+
async execute(args, ctx = {}) {
|
|
260
|
+
const condition = typeof args?.condition === "string" ? args.condition.trim() : ""
|
|
261
|
+
if (!condition) return "Error: condition is required"
|
|
262
|
+
const intervalMs = Math.max(WAIT_FOR_MIN_INTERVAL_MS, Math.floor(args?.interval_ms ?? WAIT_FOR_DEFAULT_INTERVAL_MS))
|
|
263
|
+
const cfgTimeout = ctx?.agent?.config?.agent?.waitForTimeoutMs
|
|
264
|
+
const base = Number.isFinite(args?.timeout_ms)
|
|
265
|
+
? args.timeout_ms
|
|
266
|
+
: Number.isFinite(cfgTimeout) && cfgTimeout > 0 ? cfgTimeout : WAIT_FOR_DEFAULT_TIMEOUT_MS
|
|
267
|
+
const timeoutMs = Math.min(Math.max(1, Math.floor(base)), WAIT_FOR_MAX_TIMEOUT_MS)
|
|
268
|
+
const started = Date.now()
|
|
269
|
+
let polls = 0
|
|
270
|
+
for (;;) {
|
|
271
|
+
// Cancel/interrupt-safe exit(T-W7):Ctrl+C / 定向 abort 传播——不吞信号、
|
|
272
|
+
// 不把用户停变成工具错误(dispatch 对 aborted signal 原样再抛)。
|
|
273
|
+
if (ctx.signal?.aborted) throw new Error("wait_for: interrupted by abort signal")
|
|
274
|
+
polls++
|
|
275
|
+
const ok = await evalConditionSource(condition, ctx)
|
|
276
|
+
if (ok === true) {
|
|
277
|
+
return `wait_for: condition satisfied after ${Date.now() - started}ms (${polls} check${polls === 1 ? "" : "s"}): "${condition}"`
|
|
278
|
+
}
|
|
279
|
+
const remaining = started + timeoutMs - Date.now()
|
|
280
|
+
if (remaining <= 0) {
|
|
281
|
+
return `wait_for: timed out after ${timeoutMs}ms waiting for "${condition}" (${polls} checks — condition never became true)`
|
|
282
|
+
}
|
|
283
|
+
await new Promise((r) => setTimeout(r, Math.min(intervalMs, remaining)))
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
}
|
package/src/tools/patch.mjs
CHANGED
|
@@ -15,8 +15,23 @@ import { relative, dirname } from "node:path";
|
|
|
15
15
|
/**
|
|
16
16
|
* Parse a unified diff: returns [{ path, isNew, hunks: [{ ops: [{type:" "|"-"|"+", text}] }] }]
|
|
17
17
|
* Consume hunk lines by the line counts in the @@ header — LLMs often strip context blank lines to pure empty lines,
|
|
18
|
-
* so we use counts rather than first characters to determine hunk boundaries
|
|
18
|
+
* so we use counts rather than first characters to determine hunk boundaries.
|
|
19
|
+
* APPLY-PATCH.md §3: a bare "@@" header (no coordinates) is accepted — the hunk body runs until the next hunk/file header
|
|
20
|
+
* ("@@" / "--- " / "+++ " / "diff " / "index "), purely located by its ops.
|
|
19
21
|
*/
|
|
22
|
+
/**
|
|
23
|
+
* P15.10(2026-09-05 用户裁定——「符合模型直觉」):文件头判定——
|
|
24
|
+
* 完整头(`--- x` 后随 `+++ `)任意老路径形态均认(git 规范);
|
|
25
|
+
* 容缺头(`+++ b/<path>` 配对行省略——模型单文件补丁自然形态)仅认 a//b/ 前缀——
|
|
26
|
+
* newPath 推导 = oldPath;`/dev/null` 容缺仍拒(新文件名从 --- 侧不可推导——parsePatch 内特报);
|
|
27
|
+
* 其他 `--- x` = 普通删行内容(行首标记 - + 内容 `-- x`)——不是文件头——hunk 体不得误断。
|
|
28
|
+
*/
|
|
29
|
+
function isFileHeader(line, nextLine) {
|
|
30
|
+
if (!line.startsWith("--- ")) return false
|
|
31
|
+
if (nextLine?.startsWith("+++ ")) return true
|
|
32
|
+
return /^[ab]\//.test(line.slice(4).trim())
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
function parsePatch(patch) {
|
|
21
36
|
// Patch text often comes from CRLF terminals/model output; trailing \r mixed into hunk content breaks context matching, strip uniformly
|
|
22
37
|
const lines = patch.replace(/\r(?=\n|$)/g, "").split("\n")
|
|
@@ -29,18 +44,69 @@ function parsePatch(patch) {
|
|
|
29
44
|
if (line.startsWith("--- ")) {
|
|
30
45
|
const oldPath = line.slice(4).trim()
|
|
31
46
|
const plus = lines[i + 1]
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
if (plus?.startsWith("+++ ")) {
|
|
48
|
+
const newPath = plus.slice(4).trim()
|
|
49
|
+
if (newPath === "/dev/null") throw new Error("Deleting files via patch is not supported — use the delete tool")
|
|
50
|
+
cur = { path: stripPrefix(newPath), isNew: oldPath === "/dev/null", hunks: [] }
|
|
51
|
+
files.push(cur)
|
|
52
|
+
i += 2
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
// P15.10:容缺头——`--- a/<path>`(或 b/ 前缀)后直接跟 hunk = 对同路径的修改。
|
|
56
|
+
if (/^[ab]\//.test(oldPath)) {
|
|
57
|
+
cur = { path: stripPrefix(oldPath), isNew: false, hunks: [] }
|
|
58
|
+
files.push(cur)
|
|
59
|
+
i += 1
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
if (oldPath === "/dev/null") {
|
|
63
|
+
throw new Error(`"--- /dev/null" needs a "+++ b/<path>" line naming the new file — the --- side does not carry the file name`)
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`Malformed patch: expected "+++" line after "${line}"`)
|
|
39
66
|
}
|
|
40
67
|
if (line.startsWith("@@")) {
|
|
41
68
|
if (!cur) throw new Error("Malformed patch: hunk header before any file header")
|
|
42
69
|
const m = line.match(/^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/)
|
|
43
|
-
if (!m)
|
|
70
|
+
if (!m) {
|
|
71
|
+
// APPLY-PATCH.md §3:坐标裸 "@@" 头——hunk 完全靠操作行定位。
|
|
72
|
+
// 无行数可用:操作行以 " " / "-" / "+" 开头(空行宽容为上下文行),
|
|
73
|
+
// 直到下一个 hunk 头 / 文件头 "@@"/"--- "/"+++ "/"diff "/"index " 为止。
|
|
74
|
+
if (!/^@@(?: @@)?\s*$/.test(line)) {
|
|
75
|
+
throw new Error(`Malformed patch: bad hunk header "${line}" (need @@ -old,count +new,count @@ or bare @@)`)
|
|
76
|
+
}
|
|
77
|
+
const hunk = { ops: [] }
|
|
78
|
+
i++
|
|
79
|
+
while (i < lines.length) {
|
|
80
|
+
const hl = lines[i]
|
|
81
|
+
if (hl.startsWith("@") || isFileHeader(hl, lines[i + 1]) || hl.startsWith("+++ ") || hl.startsWith("diff ") || hl.startsWith("index ")) break
|
|
82
|
+
if (hl.startsWith("\\")) { i++; continue } // ""
|
|
83
|
+
// 宽容空行=上下文行——但 patch 文本末尾(或 hunk 之间/文件头之前)的 "" 是
|
|
84
|
+
// 分隔产物而非内容:仅当后继仍是操作行时才当作上下文消费。
|
|
85
|
+
// 复评 #1(2026-09-04):文件头 "--- "/"+++ " 以 -/+ 开头会被 op 前缀判定误收——
|
|
86
|
+
// 文件头前的分隔空行不得吞成幽灵上下文行(会把 - 锚序列尾部拼上 ""——跨文件
|
|
87
|
+
// 零上下文 hunk 因此误报 not-found)。
|
|
88
|
+
const next = lines[i + 1]
|
|
89
|
+
if (hl === "" && (next == null || !/^[ +\-\\]/.test(next) || isFileHeader(next, lines[i + 2]) || next.startsWith("+++ "))) break
|
|
90
|
+
const tag = hl === "" ? " " : hl[0]
|
|
91
|
+
if (tag !== " " && tag !== "-" && tag !== "+") break // metadata / file section end
|
|
92
|
+
hunk.ops.push({ type: tag, text: hl === "" ? "" : hl.slice(1) })
|
|
93
|
+
i++
|
|
94
|
+
}
|
|
95
|
+
if (hunk.ops.length === 0) throw new Error(`Malformed patch: empty coordinate-less hunk "${line.trim()}"`)
|
|
96
|
+
const ctxCount = hunk.ops.filter((o) => o.type === " ").length
|
|
97
|
+
// §3 (APPLY-PATCH.md——2026-09-04):context<2 且含 ≥1 个 - 行 → 接受——定位锚 = hunk 内
|
|
98
|
+
// 匹配行序列(空格上下文行 + - 行——按出现序)连续——唯一匹配即应用(applyHunks 既有锚匹配域——
|
|
99
|
+
// 多匹配 / not-found 语义不变)。0 上下文与 1 上下文同待遇(评审 #4a)。
|
|
100
|
+
// 纯 +(无 - 锚)且 context<2 仍拒——插入位置不可判——报错引导加锚(NF15.8c)。
|
|
101
|
+
const removedCount = hunk.ops.filter((o) => o.type === "-").length
|
|
102
|
+
if (ctxCount < 2 && removedCount === 0) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Coordinate-less hunk ${cur.hunks.length + 1} in ${cur.path} has ${ctxCount} context line(s) — add more context lines`,
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
cur.hunks.push(hunk)
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
44
110
|
let oldNeed = m[1] == null ? 1 : Number(m[1])
|
|
45
111
|
let newNeed = m[2] == null ? 1 : Number(m[2])
|
|
46
112
|
const hunk = { ops: [] }
|
|
@@ -62,8 +128,10 @@ function parsePatch(patch) {
|
|
|
62
128
|
}
|
|
63
129
|
i++ // skip diff --git / index / blank lines and other metadata
|
|
64
130
|
}
|
|
65
|
-
|
|
66
|
-
|
|
131
|
+
// P15.10:容缺/完整空段头(头后无任何 hunk)过滤——不虚报 touchedPaths、不触发无谓 read+write
|
|
132
|
+
const withHunks = files.filter((f) => f.hunks.length > 0)
|
|
133
|
+
if (withHunks.length === 0) throw new Error("No file changes found in patch (need --- / +++ headers)")
|
|
134
|
+
return withHunks
|
|
67
135
|
}
|
|
68
136
|
|
|
69
137
|
/** Apply hunks sequentially onto an in-memory line array; any failure throws (caller guarantees nothing is written to disk). Ignores trailing \r when comparing; context lines retain original bytes */
|
|
@@ -84,7 +152,10 @@ function applyHunks(fileLines, hunks, eol, path) {
|
|
|
84
152
|
const preview = oldSeq.slice(0, 3).join(" ⏎ ")
|
|
85
153
|
throw new Error(`Hunk ${h + 1} in ${path} does not apply — context not found: "${preview}${oldSeq.length > 3 ? "…" : ""}". Read the file first and regenerate the patch from actual content.`)
|
|
86
154
|
}
|
|
87
|
-
if (matches.length > 1)
|
|
155
|
+
if (matches.length > 1) {
|
|
156
|
+
const preview = oldSeq.slice(0, 3).join(" ⏎ ")
|
|
157
|
+
throw new Error(`Hunk ${h + 1} in ${path} matches ${matches.length} locations — add more context lines to make it unique. Anchor: "${preview}${oldSeq.length > 3 ? "…" : ""}"`)
|
|
158
|
+
}
|
|
88
159
|
const pos = matches[0]
|
|
89
160
|
const out = []
|
|
90
161
|
let src = pos
|
|
@@ -103,7 +174,7 @@ export const applyPatchTool = {
|
|
|
103
174
|
parameters: {
|
|
104
175
|
type: "object",
|
|
105
176
|
properties: {
|
|
106
|
-
patch: { type: "string", description: "Unified diff. May span multiple files (multiple --- / +++ header pairs — including creating MULTIPLE new files via --- /dev/null); --- / +++ headers per file, @@ -old,count +new,count @@ hunks." },
|
|
177
|
+
patch: { type: "string", description: "Unified diff. May span multiple files (multiple --- / +++ header pairs — including creating MULTIPLE new files via --- /dev/null); --- / +++ headers per file, @@ -old,count +new,count @@ hunks (a bare @@ header is also accepted — coordinate-less hunks are located by their anchor lines: context lines plus the removed (-) lines, matched as a contiguous sequence — a unique match applies; a zero/one-context hunk is accepted only when it removes (-) at least one line and that anchor sequence is unique, while anchor-free pure-+ (insert) hunks need at least 2 context lines). The +++ b/<path> pair may be omitted for existing files — a lone --- a/<path> (or --- b/<path>) header followed directly by hunks applies to that path (new files still need --- /dev/null + +++ b/<path>)." },
|
|
107
178
|
},
|
|
108
179
|
required: ["patch"],
|
|
109
180
|
},
|
package/src/tools/question.md
CHANGED
|
@@ -7,5 +7,9 @@ Parameters:
|
|
|
7
7
|
Notes:
|
|
8
8
|
- The agent loop pauses until the user answers
|
|
9
9
|
- The answer is injected as the next user message
|
|
10
|
+
- Returns the user's answer — the chosen option or free text — as the next message; the loop resumes when it arrives.
|
|
10
11
|
- Use sparingly — prefer making reasonable decisions when possible
|
|
11
|
-
-
|
|
12
|
+
- Ask ONE question per call — never bundle multiple sub-questions into one question string; ask the next one after the answer arrives.
|
|
13
|
+
- Keep the question text short — one or two sentences. Background, context, and analysis belong in your normal reply text, NOT in the question.
|
|
14
|
+
- Routine confirmations (confirm gates) belong in your plain reply text — the user answers in their next message. Use this tool ONLY when you need the user's decision or input to proceed.
|
|
15
|
+
- After receiving an answer about a design convention, tool preference, or recurring pattern: save it with the memory tool (action: put). This prevents asking the same question in future sessions — the user shouldn't have to repeat their preferences.
|
package/src/tools/repomap.mjs
CHANGED
|
@@ -297,7 +297,7 @@ export function repoOutlineTool(db, cwd) {
|
|
|
297
297
|
return {
|
|
298
298
|
name: "repo_outline",
|
|
299
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.",
|
|
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.",
|
|
301
301
|
parameters: {
|
|
302
302
|
type: "object",
|
|
303
303
|
properties: {
|
package/src/tools/shared.mjs
CHANGED
|
@@ -296,8 +296,8 @@ export function shellSegments(command) {
|
|
|
296
296
|
|
|
297
297
|
/**
|
|
298
298
|
* Blank out quoted regions (single/double/backtick) with spaces, preserving length.
|
|
299
|
-
* Lets
|
|
300
|
-
* e.g. `node -e "if (a > b) …"` comparisons are not
|
|
299
|
+
* Lets detectDanger ignore shell metacharacters inside quoted script bodies —
|
|
300
|
+
* e.g. `node -e "if (a > b) …"` comparisons are not danger signals.
|
|
301
301
|
*/
|
|
302
302
|
function blankQuoted(command) {
|
|
303
303
|
let out = ""
|
|
@@ -308,8 +308,8 @@ function blankQuoted(command) {
|
|
|
308
308
|
if (ch === "\\" && quote !== "'") { out += " "; i++; out += " "; continue }
|
|
309
309
|
if (ch === quote) { quote = null; out += " "; continue }
|
|
310
310
|
// Backticks are COMMAND SUBSTITUTION — the content executes, so it must
|
|
311
|
-
// stay visible to
|
|
312
|
-
//
|
|
311
|
+
// stay visible to detectDanger (echo `cat > /tmp/x` still runs). Only '
|
|
312
|
+
// and " are literal regions.
|
|
313
313
|
out += quote === "`" ? ch : " "
|
|
314
314
|
} else if (ch === "'" || ch === '"' || ch === "`") {
|
|
315
315
|
quote = ch
|
|
@@ -321,14 +321,6 @@ function blankQuoted(command) {
|
|
|
321
321
|
return out
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
-
/** Detect shell output/input redirection (> >> < followed by filename) outside quoted regions.
|
|
325
|
-
* Backtick contents count (command substitution executes); fd-prefixed forms
|
|
326
|
-
* (2> file, 1>> file) count too. */
|
|
327
|
-
export function hasFileRedirection(command) {
|
|
328
|
-
const bare = blankQuoted(command)
|
|
329
|
-
return /(^|[\s;&|0-9])>{1,2}\s*\S/.test(bare) || /(^|[\s;&|0-9])<\s*\S/.test(bare)
|
|
330
|
-
}
|
|
331
|
-
|
|
332
324
|
/**
|
|
333
325
|
* Whether a single command segment is destructive — ALWAYS FALSE (deliberate).
|
|
334
326
|
*
|
|
@@ -371,19 +363,20 @@ export function detectDanger(command) {
|
|
|
371
363
|
}
|
|
372
364
|
|
|
373
365
|
|
|
374
|
-
/**
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
366
|
+
/** Glob dialect (TOOLS.md §17) lives in glob-dialect.mjs — single authority for
|
|
367
|
+
* brace expansion / exclusion / explicit syntax errors (extracted 2026-09-06 —
|
|
368
|
+
* advisor #5: shared.mjs exceeded 500 lines). Re-exported so existing importers
|
|
369
|
+
* (system.mjs tools, ls filter, tests) keep their import paths unchanged. */
|
|
370
|
+
export {
|
|
371
|
+
globToRegex,
|
|
372
|
+
splitGlobPatterns,
|
|
373
|
+
compileGlobMatchers,
|
|
374
|
+
GLOB_EXTGLOB_ERROR,
|
|
375
|
+
GLOB_EMPTY_BRACE_ERROR,
|
|
376
|
+
GLOB_UNCLOSED_BRACE_ERROR,
|
|
377
|
+
GLOB_NESTED_BRACE_ERROR,
|
|
378
|
+
} from "./glob-dialect.mjs"
|
|
379
|
+
|
|
387
380
|
|
|
388
381
|
/** Decode a numeric HTML entity to its code point — invalid/out-of-range
|
|
389
382
|
* values (e.g. �) must not throw RangeError; keep the source
|
package/src/tools/system.mjs
CHANGED
|
@@ -6,8 +6,9 @@ import {
|
|
|
6
6
|
BASH_TIMEOUT_MS,
|
|
7
7
|
IGNORED_DIRS,
|
|
8
8
|
resolveInCwd,
|
|
9
|
-
hasFileRedirection,
|
|
10
9
|
globToRegex,
|
|
10
|
+
splitGlobPatterns,
|
|
11
|
+
compileGlobMatchers,
|
|
11
12
|
normalizeEOL,
|
|
12
13
|
} from "./shared.mjs";
|
|
13
14
|
import { spawn, execFileSync } from "node:child_process";
|
|
@@ -51,20 +52,6 @@ function posixSyntaxHint(command) {
|
|
|
51
52
|
// bash — command execution with safety gates
|
|
52
53
|
// ====================================================================
|
|
53
54
|
|
|
54
|
-
/**
|
|
55
|
-
* Pre-execution safety checks for bash commands.
|
|
56
|
-
* Layers: file redirection (guides toward structured tools, not a security gate).
|
|
57
|
-
* Destructive commands (rm -rf, DROP TABLE, ...) are deliberately NOT rejected:
|
|
58
|
-
* a determined model bypasses text matching anyway — real security is at the
|
|
59
|
-
* tool approval layer plus snapshot backups (gitGuardSnapshot / checkpoint).
|
|
60
|
-
* Git destructive ops: snapshot-then-proceed, never block.
|
|
61
|
-
*/
|
|
62
|
-
function checkBashSafety(command, cwd) {
|
|
63
|
-
if (hasFileRedirection(command)) {
|
|
64
|
-
throw new Error("File redirection via bash is not allowed — use the write/edit/insert_after tools instead")
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
55
|
/**
|
|
69
56
|
* Build environment for child process.
|
|
70
57
|
* Passes through all parent env vars, with non-interactive overrides (EDITOR/PAGER/TERM).
|
|
@@ -113,11 +100,12 @@ function killProcessTree(child) {
|
|
|
113
100
|
* cannot help (it was taken before the code was written); only a snapshot taken
|
|
114
101
|
* immediately before the destructive command can.
|
|
115
102
|
*
|
|
116
|
-
* This is
|
|
117
|
-
* runs
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
103
|
+
* This is the defense-in-depth guard: a WIDE match that snapshots before the command
|
|
104
|
+
* runs — the command itself is NEVER blocked (a determined model bypasses text
|
|
105
|
+
* matching anyway; the real gate is the approval layer). A snapshot alone is not
|
|
106
|
+
* enough: the model may retry a variant that slips through the exact matcher
|
|
107
|
+
* (e.g. `git checkout HEAD -- .`), or run git outside the bash tool. The snapshot
|
|
108
|
+
* taken here survives all of those paths.
|
|
121
109
|
*
|
|
122
110
|
* Matching is intentionally WIDE (false positives are harmless — one extra snapshot;
|
|
123
111
|
* a missed match is a data-loss disaster).
|
|
@@ -159,6 +147,11 @@ function runBash(command, cwd, { timeout, signal, onOutput, shell }) {
|
|
|
159
147
|
detached: process.platform !== "win32",
|
|
160
148
|
stdio: ["ignore", "pipe", "pipe"],
|
|
161
149
|
env: buildBashEnv(),
|
|
150
|
+
// 双保险(2026-09-05——裸 spawn 无 signal 教训:abort 只靠 killTree 手动杀——taskkill
|
|
151
|
+
// best-effort 可能失败/竞态)——Node signal option = 第一道(abort 时自动杀**直接**子
|
|
152
|
+
// 进程——cmd——语义保证);killTree 仍是必需兜底(孙进程握管道使 close 不触发——见
|
|
153
|
+
// L191 注释——spawn 级 kill 不达孙进程)
|
|
154
|
+
...(signal ? { signal } : {}),
|
|
162
155
|
})
|
|
163
156
|
|
|
164
157
|
const killTree = () => killProcessTree(child)
|
|
@@ -195,10 +188,18 @@ function runBash(command, cwd, { timeout, signal, onOutput, shell }) {
|
|
|
195
188
|
settled = true
|
|
196
189
|
clearTimeout(timer)
|
|
197
190
|
clearTimeout(graceTimer)
|
|
191
|
+
// 2026-09-05(advisor 🟡#2):收尾移除 abort 监听器——ctx.signal 为长生命周期对象,
|
|
192
|
+
// 残留 once 监听器每次 bash 调用累积(闭包持有 child/输出缓冲直到 abort 才释放)
|
|
193
|
+
if (signal) signal.removeEventListener("abort", killTree)
|
|
198
194
|
resolve(result)
|
|
199
195
|
}
|
|
200
196
|
|
|
201
197
|
child.on("error", (error) => {
|
|
198
|
+
// signal 双保险(2026-09-05):abort 时 Node signal option 杀直接子进程 → 本事件
|
|
199
|
+
// 以 AbortError 先触发——**跳过 finish**(不 settled)——exit 事件随后必到(实证
|
|
200
|
+
// 2ms 内)走 L195 分支带已收集输出收尾(user interrupted——不丢 partial);
|
|
201
|
+
// 非 abort 的真 spawn 错误(command not found 等)保持原分支
|
|
202
|
+
if (error.name === "AbortError") return
|
|
202
203
|
finish(truncate(`Command failed: ${error.message}\n[stdout]:\n${outBuf || "(empty)"}`))
|
|
203
204
|
})
|
|
204
205
|
|
|
@@ -266,7 +267,6 @@ export const bashTool = {
|
|
|
266
267
|
// guard anyway. Instead: snapshot every uncommitted file first, then ALLOW
|
|
267
268
|
// the command. The snapshot makes the rollback reversible (defense in depth:
|
|
268
269
|
// the wide matcher also covers variants like `git checkout HEAD -- .`).
|
|
269
|
-
checkBashSafety(args.command, ctx.cwd)
|
|
270
270
|
const guard = await gitGuardSnapshot(args.command, ctx.cwd)
|
|
271
271
|
const result = await runBash(args.command, ctx.cwd, {
|
|
272
272
|
timeout: args.timeout ?? BASH_TIMEOUT_MS,
|
|
@@ -289,7 +289,7 @@ export const globTool = {
|
|
|
289
289
|
parameters: {
|
|
290
290
|
type: "object",
|
|
291
291
|
properties: {
|
|
292
|
-
pattern: { type: "string", description: "Glob pattern" },
|
|
292
|
+
pattern: { type: "string", description: "Glob pattern — supports **, *, ?, [..], {a,b} braces, and space-separated exclusion (\"**/*.js !test/**\")" },
|
|
293
293
|
path: { type: "string", description: "Directory to search in (default cwd)" },
|
|
294
294
|
},
|
|
295
295
|
required: ["pattern"],
|
|
@@ -297,10 +297,19 @@ export const globTool = {
|
|
|
297
297
|
readonly: true,
|
|
298
298
|
async execute(args, ctx) {
|
|
299
299
|
const base = resolveInCwd(ctx, args.path ?? ".")
|
|
300
|
-
|
|
300
|
+
// §17 调用侧拆分(评审 #5 职责分层):空格分隔 include !exclude 多模式在这里拆;
|
|
301
|
+
// globToRegex 只收单个模式(数组由 compileGlobMatchers 收)。
|
|
302
|
+
let match
|
|
303
|
+
try {
|
|
304
|
+
const parts = splitGlobPatterns(args.pattern)
|
|
305
|
+
if (parts.length === 0) return "Error: pattern is required"
|
|
306
|
+
match = compileGlobMatchers(parts).test
|
|
307
|
+
} catch (e) {
|
|
308
|
+
return `glob error: invalid pattern "${args.pattern}": ${e.message}`
|
|
309
|
+
}
|
|
301
310
|
const results = []
|
|
302
311
|
for await (const relPath of walkFiles(base)) {
|
|
303
|
-
if (
|
|
312
|
+
if (match(relPath)) {
|
|
304
313
|
results.push(relPath)
|
|
305
314
|
if (results.length >= 1000) break
|
|
306
315
|
}
|
|
@@ -343,7 +352,7 @@ export const grepTool = {
|
|
|
343
352
|
properties: {
|
|
344
353
|
pattern: { type: "string", description: "Regular expression, or a literal string when literal=true" },
|
|
345
354
|
path: { type: "string", description: "Directory or file to search (default cwd)" },
|
|
346
|
-
glob: { type: "string", description: "Only search files matching this glob (
|
|
355
|
+
glob: { type: "string", description: "Only search files matching this glob — supports **, *, ?, [..], {a,b} braces and space-separated exclusion (\"**/*.js !test/**\")" },
|
|
347
356
|
ignoreCase: { type: "boolean", description: "Case-insensitive match (default false)" },
|
|
348
357
|
literal: { type: "boolean", description: "Literal string match — no regex interpretation (default false)" },
|
|
349
358
|
before: { type: "integer", description: "Lines of context to show before each match (grep -B). Default 0" },
|
|
@@ -361,7 +370,16 @@ export const grepTool = {
|
|
|
361
370
|
} catch (e) {
|
|
362
371
|
throw new Error(`grep pattern /${args.pattern}/ is not a valid regex: ${e.message}`, { cause: e })
|
|
363
372
|
}
|
|
364
|
-
|
|
373
|
+
// §17 调用侧拆分:glob 参数支持空格分隔 include !exclude 多模式(include/exclude 求交)。
|
|
374
|
+
let fileTest = null
|
|
375
|
+
if (args.glob) {
|
|
376
|
+
try {
|
|
377
|
+
const parts = splitGlobPatterns(args.glob)
|
|
378
|
+
if (parts.length > 0) fileTest = compileGlobMatchers(parts).test
|
|
379
|
+
} catch (e) {
|
|
380
|
+
throw new Error(`grep glob /${args.glob}/ is invalid: ${e.message}`, { cause: e })
|
|
381
|
+
}
|
|
382
|
+
}
|
|
365
383
|
const before = Math.max(0, Math.floor(args.before ?? 0))
|
|
366
384
|
const after = Math.max(0, Math.floor(args.after ?? 0))
|
|
367
385
|
const wantCtx = before > 0 || after > 0
|
|
@@ -388,13 +406,15 @@ export const grepTool = {
|
|
|
388
406
|
}
|
|
389
407
|
}
|
|
390
408
|
|
|
391
|
-
async function walk(target) {
|
|
409
|
+
async function walk(target, rel) {
|
|
392
410
|
if (hits.length >= 200) return
|
|
393
411
|
// Use lstat to avoid following symlinks — prevents ./evil → /etc from making grep scan the entire system
|
|
394
412
|
let s
|
|
395
413
|
try { s = await lstat(target) } catch { return }
|
|
396
414
|
if (!s.isDirectory()) {
|
|
397
|
-
|
|
415
|
+
// rel 为相对搜索基的路径(排除前缀如 !test/** 必须按目录路径作用——不能用裸文件名);
|
|
416
|
+
// 单文件目标(path 指向文件)时 rel 为空 → 退化为按文件名过滤(既有语义)。
|
|
417
|
+
if (!fileTest || fileTest(rel || target.split(/[\\/]/).pop())) await search(target)
|
|
398
418
|
return
|
|
399
419
|
}
|
|
400
420
|
let entries
|
|
@@ -405,11 +425,11 @@ export const grepTool = {
|
|
|
405
425
|
}
|
|
406
426
|
for (const e of entries) {
|
|
407
427
|
if (e.isDirectory() && IGNORED_DIRS.has(e.name)) continue
|
|
408
|
-
await walk(join(target, e.name))
|
|
428
|
+
await walk(join(target, e.name), rel ? `${rel}/${e.name}` : e.name)
|
|
409
429
|
}
|
|
410
430
|
}
|
|
411
431
|
|
|
412
|
-
await walk(base)
|
|
432
|
+
await walk(base, "")
|
|
413
433
|
if (hits.length === 0) return "(no matches)"
|
|
414
434
|
|
|
415
435
|
// No context: keep original path:line: content format
|
package/src/tools/tree.md
CHANGED
|
@@ -10,4 +10,5 @@ Parameters:
|
|
|
10
10
|
Notes:
|
|
11
11
|
- Capped at 200 entries.
|
|
12
12
|
- Directories end with `/`; tree-drawing uses `├──`/`└──`/`│`.
|
|
13
|
-
- Use depth for a shallow overview; use `ls` for one directory, `glob` for a specific file pattern.
|
|
13
|
+
- Use depth for a shallow overview; use `ls` for one directory, `glob` for a specific file pattern.
|
|
14
|
+
- Returns the directory text tree — directories first (`dir/`), files after, both sorted, capped at 200 entries.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Wait until a condition becomes true — polls the condition every interval_ms and returns as soon as it holds or the timeout_ms ceiling passes. Use it ONLY for genuinely asynchronous waits: an async subagent/consult still settling, a file appearing, a port opening. Synchronous tools (advisor, blocking subagent spawn) return when done — waiting after them is NOT needed and wastes time.
|
|
2
|
+
|
|
3
|
+
Parameters:
|
|
4
|
+
- condition (required): a semantic condition expression:
|
|
5
|
+
- `advisor settled` — an in-flight advisor review session has finished (async advisor-role children settled)
|
|
6
|
+
- `subagent id:N done` — async subagent N (the id your async spawn ack returned) has settled
|
|
7
|
+
- `consult done` — every consult_start session has drained
|
|
8
|
+
- `file exists:path` — the file at path exists (path relative to cwd)
|
|
9
|
+
- `port open:N` — something is listening on 127.0.0.1 port N
|
|
10
|
+
- The agent-internal conditions (advisor / subagent / consult) apply to ASYNC sessions only — a synchronous call already completed before it returned and has nothing to wait for. An unknown condition is an explicit error (`wait_for: unsupported condition "..."` — the supported forms are listed above), never a silent wait.
|
|
11
|
+
- interval_ms: poll interval (default 1000, floor 100 — polling never busy-spins)
|
|
12
|
+
- timeout_ms: overall ceiling (default 30000; config.json `agent.waitForTimeoutMs` overrides the default; hard cap 600000 like the execute tool)
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
- `wait_for: condition satisfied after Nms (N checks): "<condition>"` — condition held
|
|
16
|
+
- `wait_for: timed out after Nms waiting for "<condition>" ...` — ceiling passed with the condition still false (never burns beyond the ceiling)
|
|
17
|
+
- interrupts (Ctrl+C / cancel) exit immediately; malformed conditions and unsupported syntax error out explicitly
|
|
18
|
+
|
|
19
|
+
Notes:
|
|
20
|
+
- Read-only and non-destructive — it only observes (agent pools, the filesystem, a local port probe).
|
|
21
|
+
- Blocking by design — call it ALONE in a turn, not batched with calls that depend on its result.
|
|
22
|
+
- Do not use it to wait after synchronous tools (advisor / blocking subagent spawn / verify return only when done).
|