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/file.mjs
CHANGED
|
@@ -10,10 +10,10 @@ import {
|
|
|
10
10
|
detectFileEol,
|
|
11
11
|
joinWithEol,
|
|
12
12
|
majorityEol,
|
|
13
|
-
findCandidates,
|
|
14
13
|
FFFD_WARNING,
|
|
15
14
|
} from "./shared.mjs";
|
|
16
15
|
import { applyEditBatch } from "./edit-batch.mjs";
|
|
16
|
+
import { runSingleEdit } from "./edit-diff.mjs";
|
|
17
17
|
import { specForModel } from "../config.mjs";
|
|
18
18
|
import { createHash } from "node:crypto";
|
|
19
19
|
import { mkdir, readFile, stat, writeFile, unlink } from "node:fs/promises";
|
|
@@ -73,6 +73,7 @@ export const readTool = {
|
|
|
73
73
|
type: "object",
|
|
74
74
|
properties: {
|
|
75
75
|
path: { type: "string", description: "File path (relative to cwd or absolute)" },
|
|
76
|
+
filePath: { type: "string", description: "Alias for path (supported for API compatibility)" },
|
|
76
77
|
offset: { type: "number", description: "1-based line number to start from" },
|
|
77
78
|
limit: { type: "number", description: `Max lines to return (default ${MAX_READ_LINES})` },
|
|
78
79
|
allowExternal: { type: "boolean", description: "No-op retained for API compatibility — path resolution no longer asserts a working-directory boundary (all paths resolve relative to cwd)." },
|
|
@@ -82,7 +83,8 @@ export const readTool = {
|
|
|
82
83
|
},
|
|
83
84
|
readonly: true,
|
|
84
85
|
async execute(args, ctx) {
|
|
85
|
-
const
|
|
86
|
+
const p = args.path ?? args.filePath
|
|
87
|
+
const abs = args.allowExternal ? resolveExternal(ctx, p) : resolveInCwd(ctx, p)
|
|
86
88
|
// Large file guard: check size first, reject reading entire file if >10MB (offset/limit only affect the returned slice, not buffering)
|
|
87
89
|
const st = await stat(abs).catch(() => null)
|
|
88
90
|
if (st && st.size > MAX_FILE_READ_BYTES) throw new Error(`File too large (${Math.round(st.size / 1_000_000)}MB > 10MB limit). Use bash with head/tail or grep for targeted extraction.`)
|
|
@@ -214,22 +216,27 @@ export const editTool = {
|
|
|
214
216
|
parameters: {
|
|
215
217
|
type: "object",
|
|
216
218
|
properties: {
|
|
217
|
-
path: { type: "string", description: "File path" },
|
|
218
|
-
old_string: { type: "string", description: "Exact text to replace" },
|
|
219
|
-
new_string: { type: "string", description: "Replacement text" },
|
|
219
|
+
path: { type: "string", description: "File path — single form: required; with the edits array: optional top-level default for entries without their own path" },
|
|
220
|
+
old_string: { type: "string", description: "Exact text to replace (tolerant matching: exact → whitespace-only variant → fuzzy match at ≥90% line equality after whitespace/indent/quote normalization). Mutually exclusive with line/startLine/endLine" },
|
|
221
|
+
new_string: { type: "string", description: "Replacement text — content-based edits require it (empty is an explicit error); with line-based targeting (line/startLine/endLine) give it to replace the line/range, or OMIT it to delete (an explicit empty string is an error — omission is the delete signal)" },
|
|
222
|
+
line: { type: "integer", description: "1-based line number — replace that single line with new_string, or OMIT new_string to DELETE the line; no old_string needed (mutually exclusive with old_string and startLine/endLine)" },
|
|
223
|
+
startLine: { type: "integer", description: "1-based first line of the range to replace with new_string (inclusive — requires endLine; mutually exclusive with old_string); OMIT new_string to DELETE the range" },
|
|
224
|
+
endLine: { type: "integer", description: "1-based last line of the range to replace with new_string (inclusive — requires startLine; mutually exclusive with old_string); OMIT new_string to DELETE the range" },
|
|
220
225
|
replace_all: { type: "boolean", description: "Replace all occurrences (default false)" },
|
|
221
226
|
edits: {
|
|
222
227
|
type: "array",
|
|
223
|
-
description: "Batch form — multiple edits in ONE call, atomic (any failure writes nothing; same-file entries apply serially, each based on the previous result). Use it for multiple changes to the same file AND for independent changes across multiple files — prefer one batched call over N single edits. Mutually exclusive with
|
|
228
|
+
description: "Batch form — multiple edits in ONE call, atomic (any failure writes nothing; same-file entries apply serially, each based on the previous result). Use it for multiple changes to the same file AND for independent changes across multiple files — prefer one batched call over N single edits. A top-level path is allowed — it defaults entries without their own path (entry paths win). Mutually exclusive with top-level old_string/new_string/line/startLine/endLine — provide each change's targeting (old_string, or line / startLine+endLine) inside its edits entry; new_string replaces when given, and line-targeted entries may omit it to DELETE the line/range.",
|
|
224
229
|
items: {
|
|
225
230
|
type: "object",
|
|
226
231
|
properties: {
|
|
227
232
|
path: { type: "string" },
|
|
228
233
|
old_string: { type: "string" },
|
|
229
234
|
new_string: { type: "string" },
|
|
235
|
+
line: { type: "integer" },
|
|
236
|
+
startLine: { type: "integer" },
|
|
237
|
+
endLine: { type: "integer" },
|
|
230
238
|
replace_all: { type: "boolean" },
|
|
231
239
|
},
|
|
232
|
-
required: ["path", "old_string", "new_string"],
|
|
233
240
|
},
|
|
234
241
|
},
|
|
235
242
|
},
|
|
@@ -237,7 +244,13 @@ export const editTool = {
|
|
|
237
244
|
},
|
|
238
245
|
readonly: false,
|
|
239
246
|
touchedPaths(args) {
|
|
240
|
-
|
|
247
|
+
// 2026-09-05 用户裁定:顶层 path = 批默认——仅当有条目缺 path 时计入(条目全带 path
|
|
248
|
+
// 时顶层 path 不实际使用——不虚报进 _touchedFiles)
|
|
249
|
+
if (args.edits) {
|
|
250
|
+
const out = args.edits.map((e) => e.path).filter(Boolean)
|
|
251
|
+
if (args.path && args.edits.some((e) => !e.path)) out.push(args.path)
|
|
252
|
+
return out
|
|
253
|
+
}
|
|
241
254
|
return args.path ? [args.path] : []
|
|
242
255
|
},
|
|
243
256
|
async execute(args, ctx) {
|
|
@@ -245,67 +258,9 @@ export const editTool = {
|
|
|
245
258
|
// (应用逻辑在 edit-batch.mjs——2026-09-01 拆出,500 行硬限,先例 git-ext.mjs)
|
|
246
259
|
if (args.edits) return applyEditBatch(args, ctx)
|
|
247
260
|
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
throw new Error("old_string must not be empty (empty string matches everywhere and would corrupt the file)")
|
|
252
|
-
}
|
|
253
|
-
// #5(2026-09-01 交付评审尾巴):new_string 非字符串(含 undefined)在写盘前拒绝——
|
|
254
|
-
// 原缺陷:replace 回调返回 undefined 被字符串化成 "undefined" 写入盘,随后
|
|
255
|
-
// args.new_string.split 才 TypeError——文件已损坏 + 错误信息不知所云。
|
|
256
|
-
if (typeof args.new_string !== "string") {
|
|
257
|
-
throw new Error(
|
|
258
|
-
`new_string must be a string${args.new_string === undefined ? " (missing)" : ` (got ${typeof args.new_string})`} — nothing written`,
|
|
259
|
-
)
|
|
260
|
-
}
|
|
261
|
-
const raw = await readFile(abs, "utf8")
|
|
262
|
-
const content = normalizeEOL(raw)
|
|
263
|
-
const occurrences = content.split(args.old_string).length - 1
|
|
264
|
-
if (occurrences === 0) {
|
|
265
|
-
// Give clues to help the model locate: first-line preview + common causes
|
|
266
|
-
const preview = args.old_string.slice(0, 100).split("\n")[0]
|
|
267
|
-
// Similarity candidates (LCS, line-level, top 3, score ≥ 0.5) — turns the
|
|
268
|
-
// "not found" black box into a pointer at the most likely intended line.
|
|
269
|
-
// Multi-line old_string: only its first line is scored (marked accordingly).
|
|
270
|
-
const cands = findCandidates(content.split("\n"), args.old_string)
|
|
271
|
-
let candText = ""
|
|
272
|
-
if (cands.length > 0) {
|
|
273
|
-
const header = args.old_string.includes("\n")
|
|
274
|
-
? ` similar lines (old_string line 1: "${args.old_string.split("\n")[0].slice(0, 80)}"):`
|
|
275
|
-
: " similar lines:"
|
|
276
|
-
candText = "\n" + header + "\n" + cands.map((c) => ` L${c.line}: ${c.preview} (${Math.round(c.score * 100)}%)`).join("\n")
|
|
277
|
-
}
|
|
278
|
-
throw new Error(
|
|
279
|
-
`old_string not found in ${args.path}\n` +
|
|
280
|
-
` searched: "${preview}${args.old_string.length > 100 ? "…" : ""}"\n` +
|
|
281
|
-
(lastWriteOf(abs)?.type === "write"
|
|
282
|
-
? ` hints: this file was modified since your last read (write 全文重写后内容全变) — re-read it to refresh your copy of the content, then retry\n`
|
|
283
|
-
: isDirty(abs)
|
|
284
|
-
? ` hints: this file was modified since your last read (a prior write marked it dirty) — re-read it to refresh your copy of the content, then retry\n`
|
|
285
|
-
: ` hints: whitespace mismatch? file already changed? try reading the file first\n`) +
|
|
286
|
-
candText
|
|
287
|
-
)
|
|
288
|
-
}
|
|
289
|
-
if (occurrences > 1 && !args.replace_all) {
|
|
290
|
-
throw new Error(`old_string matches ${occurrences} times in ${args.path}; provide more context or set replace_all`)
|
|
291
|
-
}
|
|
292
|
-
const updated = args.replace_all
|
|
293
|
-
? content.split(args.old_string).join(args.new_string)
|
|
294
|
-
// Functional replacement: avoid $-substitution patterns in new_string (match string / backreference) being expanded
|
|
295
|
-
: content.replace(args.old_string, () => args.new_string)
|
|
296
|
-
// Write back in the file's ORIGINAL EOL style (first-newline rule) — a CRLF
|
|
297
|
-
// file must not come back as LF (that rewrites every line in the diff).
|
|
298
|
-
// normalizeEOL first: new_string may carry \r\n (e.g. pasted from a raw CRLF
|
|
299
|
-
// read); without normalizing, split leaves stray \r and CRLF join makes \r\r\n.
|
|
300
|
-
await writeFile(abs, joinWithEol(normalizeEOL(updated).split("\n"), raw), "utf8")
|
|
301
|
-
// 2026-08-31 工具顺手度:记录受影响区(替换首行 + 行数差)——insert_after 精确判定
|
|
302
|
-
const matchIdx = content.indexOf(args.old_string)
|
|
303
|
-
const editStartLine = matchIdx >= 0 ? content.slice(0, matchIdx).split("\n").length : 1
|
|
304
|
-
const lineShift = args.new_string.split("\n").length - args.old_string.split("\n").length
|
|
305
|
-
recordWrite(abs, { type: "edit", startLine: editStartLine, shift: lineShift })
|
|
306
|
-
const diff = gitDiffOne(ctx.cwd, abs)
|
|
307
|
-
const baseResult = `Edited ${args.path}: replaced ${args.replace_all ? occurrences : 1} occurrence(s)${diff ? "\n" + diff : ""}${await autoSyntaxCheck(abs)}`
|
|
308
|
-
return await appendWriteContext(abs, editStartLine, baseResult)
|
|
261
|
+
// 单文件(现状路径)——执行体整段迁出至 edit-diff.mjs(EDIT.md §6——
|
|
262
|
+
// 行级 LCS 判定:零重叠→替换即删 / 一般 diff→LCS / 空 new→显式报错)
|
|
263
|
+
return runSingleEdit(args, ctx)
|
|
309
264
|
},
|
|
310
265
|
}
|
|
311
266
|
|
|
@@ -453,7 +408,7 @@ export const hashlineEditTool = {
|
|
|
453
408
|
const hashDump = fileHashes.slice(0, maxShow).map((h, i) => `${h} L${i + 1}: ${lines[i].slice(0, 80)}`).join("\n")
|
|
454
409
|
const preview = target.join(" ")
|
|
455
410
|
throw new Error(
|
|
456
|
-
`Hash sequence not found in ${args.path}: ${preview}\n` +
|
|
411
|
+
`Hash sequence not found in ${args.path}: ${preview} — for fresh hashes, re-read the file with hashes=true\n` +
|
|
457
412
|
`The file may have been modified since you last read it. Current hashes (first ${maxShow} lines):\n${hashDump}` +
|
|
458
413
|
(corrupted ? `\n${FFFD_WARNING}` : "")
|
|
459
414
|
)
|
package/src/tools/file_ops.md
CHANGED
|
@@ -13,4 +13,5 @@ Parameters:
|
|
|
13
13
|
Notes:
|
|
14
14
|
- Paths resolve relative to cwd — no directory restriction (same boundary as bash; the approval gate is the guard).
|
|
15
15
|
- `dest` is overwritten if it already exists. `copy` is recursive for directories.
|
|
16
|
-
- To create a directory, use `write` (creates parent dirs) or `bash mkdir`.
|
|
16
|
+
- To create a directory, use `write` (creates parent dirs) or `bash mkdir`.
|
|
17
|
+
- Returns `Moved|Copied|Renamed <source> -> <dest>` or `Error: ...` (same-path and missing-path errors).
|
|
@@ -3,4 +3,6 @@ Get the current date, time, weekday, and timezone.
|
|
|
3
3
|
**Route to get_current_time instead of bash:**
|
|
4
4
|
- `date` / `time` → get_current_time
|
|
5
5
|
|
|
6
|
-
Use it whenever a task depends on the current time or date (deadlines, freshness, timestamps) rather than shelling out.
|
|
6
|
+
Use it whenever a task depends on the current time or date (deadlines, freshness, timestamps) rather than shelling out.
|
|
7
|
+
|
|
8
|
+
Returns `Date` / `Timezone` / `Weekday` / `Local` lines (UTC + local timezone).
|
package/src/tools/git.mjs
CHANGED
|
@@ -42,7 +42,7 @@ export const gitTool = {
|
|
|
42
42
|
action: { type: "string", enum: ["diff", "status", "log", "show", "checkpoint", "add", "rm", "commit", "push", "tag", "branch", "checkout", "restore", "stash", "fetch", "pull", "reset", "revert", "merge", "cherry-pick", "ls-remote", "clone", "init", "rebase", "remote", "clean", "switch", "apply", "worktree", "archive", "blame", "mv"], description: "diff / status / log / show / checkpoint / add / rm / commit / push / tag / branch / checkout / restore / stash / fetch / pull / reset / revert / merge / cherry-pick / ls-remote / clone / init / rebase / remote / clean / switch / apply / worktree / archive / blame / mv — clean/rebase 操作前自动快照,checkpointAction=rewind 恢复" },
|
|
43
43
|
// diff/log params
|
|
44
44
|
staged: { type: "boolean", description: "(diff) Show staged changes instead of working tree" },
|
|
45
|
-
path: { type: "string", description: "(diff/log/add/commit/checkout/restore/checkpoint:cat/versions/rewind/rm/apply/archive/blame/mv/worktree) File or directory to scope to / stage / restore(checkout/restore 操作前自动快照,checkpointAction=rewind
|
|
45
|
+
path: { type: "string", description: "(diff/log/add/commit/checkout/restore/checkpoint:cat/versions/rewind/rm/apply/archive/blame/mv/worktree) File or directory to scope to / stage / restore(checkout/restore 操作前自动快照,checkpointAction=rewind 恢复)— add/rm/commit: space-separated for multiple(git add a b c——2026-09-05 发版痛点;含空格的文件名会被按多路径拆分——请用 bash git 处理)" },
|
|
46
46
|
ref: { type: "string", description: "(diff/show/checkout/reset/revert/merge/cherry-pick/tag:create/branch:create/rebase/worktree:add/archive) Commit/branch/ref; (push/pull/fetch) the branch or tag (space-separated for multiple)" },
|
|
47
47
|
count: { type: "number", description: "(log) Number of commits (default 10)" },
|
|
48
48
|
oneline: { type: "boolean", description: "(log) One-line-per-commit format" },
|
|
@@ -143,13 +143,16 @@ export const gitTool = {
|
|
|
143
143
|
}
|
|
144
144
|
case "rm": {
|
|
145
145
|
if (!args.path) return "Error: rm requires path (the file/directory to untrack, relative to repo root)"
|
|
146
|
-
const
|
|
147
|
-
|
|
146
|
+
const paths = args.path.split(/\s+/).filter(Boolean)
|
|
147
|
+
const r = runGitStrict(ctx.cwd, ["rm", "--cached", "-r", "--", ...paths])
|
|
148
|
+
return r.ok ? truncate(r.out || `Untracked ${paths.join(" ")} (kept on disk)`) : truncate(`git rm failed: ${r.err || r.out}`)
|
|
148
149
|
}
|
|
149
150
|
case "commit": {
|
|
150
151
|
if (!args.message) return "Error: commit requires message"
|
|
151
152
|
// Granular staging when path given (only stage these); otherwise stage all (add -A).
|
|
152
|
-
|
|
153
|
+
// 多路径:空格分隔(ref 先例——2026-09-05 发版痛点)
|
|
154
|
+
const staged = args.path ? args.path.split(/\s+/).filter(Boolean) : null
|
|
155
|
+
const add = runGitStrict(ctx.cwd, staged?.length ? ["add", "--", ...staged] : ["add", "-A"])
|
|
153
156
|
if (!add.ok) return truncate(`git add failed: ${add.err || add.out || "(no output)"}`)
|
|
154
157
|
const commit = runGitStrict(ctx.cwd, ["commit", "-m", args.message])
|
|
155
158
|
const parts = []
|
|
@@ -189,9 +192,11 @@ export const gitTool = {
|
|
|
189
192
|
}
|
|
190
193
|
case "add": {
|
|
191
194
|
// Granular staging: stage `path` when given, else all changes (add -A).
|
|
192
|
-
|
|
195
|
+
// 多路径:空格分隔(ref 先例 L175——2026-09-05 发版痛点——git add 单路径被迫 N 次调用)
|
|
196
|
+
const paths = args.path ? args.path.split(/\s+/).filter(Boolean) : null
|
|
197
|
+
const cmdArgs = paths?.length ? ["add", "--", ...paths] : ["add", "-A"]
|
|
193
198
|
const r = runGitStrict(ctx.cwd, cmdArgs)
|
|
194
|
-
return r.ok ? truncate(r.out || `Staged ${
|
|
199
|
+
return r.ok ? truncate(r.out || `Staged ${paths?.join(" ") || "all changes"}`) : truncate(`git add failed: ${r.err || r.out}`)
|
|
195
200
|
}
|
|
196
201
|
case "tag": {
|
|
197
202
|
const sub = args.tagAction
|
|
@@ -361,6 +366,9 @@ export const questionTool = {
|
|
|
361
366
|
readonly: true,
|
|
362
367
|
async execute(args, ctx) {
|
|
363
368
|
if (!ctx.onQuestion) throw new Error("question tool not supported in this context (no UI to ask)")
|
|
369
|
+
// §22 D-Q3 机械限制(2026-09-06 用户裁定:100 字符 / 4 条——超限拒绝回模型,不弹卡、不调 onQuestion)
|
|
370
|
+
if (args.question.length > 100) return "(error: question too long (>100 chars) — ask ONE short question; background belongs in your normal reply text)"
|
|
371
|
+
if (Array.isArray(args.options) && args.options.length > 4) return "(error: too many options (>4) — offer at most 4 plain-string options)"
|
|
364
372
|
return ctx.onQuestion(args.question, args.options ?? [])
|
|
365
373
|
},
|
|
366
374
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* glob-dialect.mjs — glob 方言(TOOLS.md §17 — 2026-09-06)单一权威模块(CLI)。
|
|
3
|
+
*
|
|
4
|
+
* `{a,b}` brace expansion is handled BEFORE the sentinel/escape flow (braces must
|
|
5
|
+
* not be literal-escaped — a bare "star-star slash star .{js,txt}" pattern
|
|
6
|
+
* previously escaped to a literal that silently matched nothing). Unsupported
|
|
7
|
+
* extglob dialects (`?(x)`/`@(a|b)`/`+(x)`/`*(x)`/`!(x)`) and malformed braces
|
|
8
|
+
* (empty `{}`, unclosed `{`, nested `{a,{b,c}}`) are EXPLICIT errors — never a
|
|
9
|
+
* silent no-match. Exclude prefixes (`!pattern`) are the CALLER's job: the
|
|
10
|
+
* glob/grep tools whitespace-split the pattern expression (splitGlobPatterns —
|
|
11
|
+
* never inside a brace group) and pass the parts to compileGlobMatchers, which
|
|
12
|
+
* intersects includes and excludes.
|
|
13
|
+
*
|
|
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
|
|
16
|
+
* importers (system.mjs / ls filter / tests) keep their import paths.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const BRACE_OPEN = "\u0003", BRACE_SEP = "\u0004", BRACE_CLOSE = "\u0005"
|
|
20
|
+
const EXT_GLOB_RE = /[@?+*!]\(/
|
|
21
|
+
|
|
22
|
+
export const GLOB_EXTGLOB_ERROR =
|
|
23
|
+
"glob syntax error: unsupported extglob syntax — ?(x)/@(a|b)/+(x)/*(x)/!(x) are not supported; use {a,b} brace expansion or multiple space-separated patterns instead"
|
|
24
|
+
export const GLOB_EMPTY_BRACE_ERROR =
|
|
25
|
+
'glob syntax error: empty brace group "{}" — put at least one alternative between the braces (e.g. {js,txt})'
|
|
26
|
+
export const GLOB_UNCLOSED_BRACE_ERROR =
|
|
27
|
+
'glob syntax error: unclosed brace group — add a matching "}" (e.g. {js,txt})'
|
|
28
|
+
export const GLOB_NESTED_BRACE_ERROR =
|
|
29
|
+
"glob syntax error: nested brace groups are not supported (e.g. {a,{b,c}}) — use a flat alternative list (e.g. {a,b,c})"
|
|
30
|
+
|
|
31
|
+
function assertNoExtglob(pattern) {
|
|
32
|
+
if (EXT_GLOB_RE.test(pattern)) throw new Error(GLOB_EXTGLOB_ERROR)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Replace every {a,b,c} group with placeholder alternation markers. Runs BEFORE
|
|
36
|
+
* the sentinel/escape flow so the inserted alternation survives literal escaping. */
|
|
37
|
+
function expandBraces(pattern) {
|
|
38
|
+
for (;;) {
|
|
39
|
+
const open = pattern.indexOf("{")
|
|
40
|
+
if (open === -1) return pattern
|
|
41
|
+
let close = -1
|
|
42
|
+
for (let j = open + 1; j < pattern.length; j++) {
|
|
43
|
+
if (pattern[j] === "{") throw new Error(GLOB_NESTED_BRACE_ERROR)
|
|
44
|
+
if (pattern[j] === "}") { close = j; break }
|
|
45
|
+
}
|
|
46
|
+
if (close === -1) throw new Error(GLOB_UNCLOSED_BRACE_ERROR)
|
|
47
|
+
const inner = pattern.slice(open + 1, close)
|
|
48
|
+
if (inner === "") throw new Error(GLOB_EMPTY_BRACE_ERROR)
|
|
49
|
+
const alts = inner.split(",").map((a) => a.trim())
|
|
50
|
+
pattern = pattern.slice(0, open) + BRACE_OPEN + alts.join(BRACE_SEP) + BRACE_CLOSE + pattern.slice(close + 1)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Convert a single glob pattern to a RegExp (anchored full match). Supports the
|
|
55
|
+
* recursive "star-star" forms, single-star, "?", "[..]" character classes and
|
|
56
|
+
* {a,b} brace expansion. Malformed braces and unsupported extglob syntax throw
|
|
57
|
+
* explicit errors (no silent mismatch). */
|
|
58
|
+
export function globToRegex(pattern) {
|
|
59
|
+
pattern = String(pattern)
|
|
60
|
+
assertNoExtglob(pattern)
|
|
61
|
+
pattern = expandBraces(pattern)
|
|
62
|
+
// Sentinel chars: \u0001/\u0002 never appear in real glob patterns (they
|
|
63
|
+
// come from model output or the filesystem) — safe as **/ and ** placeholders.
|
|
64
|
+
const DS = "\u0001", DP = "\u0002"
|
|
65
|
+
const escaped = pattern
|
|
66
|
+
.replace(/\*\*\//g, DS).replace(/\*\*/g, DP)
|
|
67
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
68
|
+
.replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")
|
|
69
|
+
.replace(new RegExp(DS, "g"), "(?:.+/)?")
|
|
70
|
+
.replace(new RegExp(DP, "g"), ".*")
|
|
71
|
+
.replace(new RegExp(BRACE_OPEN, "g"), "(?:")
|
|
72
|
+
.replace(new RegExp(BRACE_SEP, "g"), "|")
|
|
73
|
+
.replace(new RegExp(BRACE_CLOSE, "g"), ")")
|
|
74
|
+
return new RegExp(`^${escaped}$`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Whitespace-split a glob expression into parts WITHOUT splitting inside {a,b}
|
|
78
|
+
* brace groups and WITHOUT splitting literal spaces inside a single pattern
|
|
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
|
|
81
|
+
* "include !exclude" form). So a BARE pattern like "docs/my file/*.md" and an
|
|
82
|
+
* exclude like "!docs/my file/**" each stay ONE part; the standard
|
|
83
|
+
* "js-pattern 空格 !test-pattern" form still splits into its two parts. */
|
|
84
|
+
export function splitGlobPatterns(expr) {
|
|
85
|
+
const s = String(expr ?? "").trim()
|
|
86
|
+
if (!s) return []
|
|
87
|
+
const parts = []
|
|
88
|
+
let depth = 0
|
|
89
|
+
let cur = ""
|
|
90
|
+
for (let i = 0; i < s.length; i++) {
|
|
91
|
+
const ch = s[i]
|
|
92
|
+
if (ch === "{") depth++
|
|
93
|
+
else if (ch === "}") depth = Math.max(0, depth - 1)
|
|
94
|
+
if (/\s/.test(ch) && depth === 0) {
|
|
95
|
+
let j = i
|
|
96
|
+
while (j < s.length && /\s/.test(s[j])) j++
|
|
97
|
+
if (s[j] === "!") {
|
|
98
|
+
if (cur) { parts.push(cur); cur = "" }
|
|
99
|
+
i = j - 1 // next loop iteration continues at the "!"
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
// literal space inside a pattern (or trailing) — keep it in the current part
|
|
103
|
+
cur += ch
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
cur += ch
|
|
107
|
+
}
|
|
108
|
+
if (cur) parts.push(cur)
|
|
109
|
+
return parts
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Compile whitespace-split pattern parts (see splitGlobPatterns) into a matcher:
|
|
113
|
+
* non-"!" parts are includes, leading-"!" parts are excludes — a rel path matches
|
|
114
|
+
* when it matches ≥1 include (or there are no includes) and no exclude. Every
|
|
115
|
+
* part runs the full brace/error dialect through globToRegex (excludes too). */
|
|
116
|
+
export function compileGlobMatchers(parts) {
|
|
117
|
+
const includes = []
|
|
118
|
+
const excludes = []
|
|
119
|
+
for (const p of parts) {
|
|
120
|
+
const part = String(p)
|
|
121
|
+
if (part.startsWith("!")) excludes.push(part.slice(1))
|
|
122
|
+
else if (part !== "") includes.push(part)
|
|
123
|
+
}
|
|
124
|
+
const include = includes.map((p) => globToRegex(p))
|
|
125
|
+
const exclude = excludes.map((p) => globToRegex(p))
|
|
126
|
+
const test = (relPath) =>
|
|
127
|
+
(include.length === 0 || include.some((re) => re.test(relPath))) &&
|
|
128
|
+
!exclude.some((re) => re.test(relPath))
|
|
129
|
+
return { include, exclude, test }
|
|
130
|
+
}
|
package/src/tools/glob.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
Find files by glob pattern. Returns matching paths
|
|
1
|
+
Find files by glob pattern. Returns matching paths (relative to the search path), sorted, capped at 1000. Use this to discover file structure; use grep to search file contents.
|
|
2
2
|
|
|
3
3
|
Parameters:
|
|
4
|
-
- pattern (required): Glob pattern — supports
|
|
4
|
+
- pattern (required): Glob pattern — supports `**` (recursive), `**/`, `*` (within a segment), `?` (single char), `[..]` character classes, and `{a,b}` brace expansion (e.g. `**/*.{js,txt}` matches .js and .txt files at any depth). Space-separated multiple patterns with a leading `!` are EXCLUSIONS — `**/*.js !test/**` matches .js files except those under test/. Pattern matching is relative to `path`. A pattern containing a literal space is fine on its own (spaces only separate patterns when a `!` exclusion is present). An expression splits ONLY before `!`-exclusion tokens — to match several extensions in one include, use a `{a,b}` group (e.g. `**/*.{js,md}`); adjacent space-separated includes are not a supported form.
|
|
5
5
|
- path: Directory to search in (default cwd)
|
|
6
6
|
|
|
7
7
|
Notes:
|
|
8
|
+
- Invalid glob syntax is an EXPLICIT error — never a silent no-match. Unsupported extglob dialects (`?(x)`/`@(a|b)`/`+(x)`) and malformed braces (empty `{}`, unclosed `{`, nested `{a,{b,c}}`) return `glob error: ...` — use `{a,b}` or a space-separated `!exclude` pattern instead.
|
|
8
9
|
- Skips node_modules, .git, dist, build, .turbo, coverage
|
|
9
10
|
- Results capped at 1000 matches
|
|
10
|
-
- Use this to discover file structure; use grep to search file contents
|
|
11
11
|
- Prefer patterns with a literal anchor (extension or subdirectory) over bare wildcards
|
package/src/tools/grep.md
CHANGED
|
@@ -5,7 +5,7 @@ Search file contents with a regex. Returns matching lines as path:line: content.
|
|
|
5
5
|
Parameters:
|
|
6
6
|
- pattern (required): JavaScript regular expression, or a literal string when literal=true
|
|
7
7
|
- path: Directory or file to search (default cwd)
|
|
8
|
-
- glob: Only search files matching this glob (e.g. '*.mjs'
|
|
8
|
+
- glob: Only search files matching this glob — supports `**`, `*`, `?`, `[..]`, `{a,b}` braces and space-separated exclusion (`"**/*.js !test/**"` = .js files outside test/); e.g. '*.mjs'
|
|
9
9
|
- ignoreCase: Case-insensitive match (default false)
|
|
10
10
|
- literal: Literal string match — no regex interpretation (default false; use for strings with `. \` etc.)
|
|
11
11
|
- before: Lines of context to show before each match (grep -B). Default 0
|
|
@@ -10,3 +10,5 @@ Notes:
|
|
|
10
10
|
- Hashes are position-independent: they identify lines by content, not by line number (which changes after edits)
|
|
11
11
|
- If the hash sequence isn't found, the error will include the current file's hashes so you can retry with corrected values
|
|
12
12
|
- Prefer this over edit when: 1) the file may have mixed whitespace/encoding, 2) you want to edit a block of lines with a single call
|
|
13
|
+
- Replacement text replaces the lines identified by the hashes — content not present in new_content is deleted. For a new line after a known line, use insert_after. For a single simple string swap, use edit.
|
|
14
|
+
- use the most recent read of the file as the source of old_string / line numbers / hashes — re-read after the file changed
|
package/src/tools/index.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { checklistTool } from "./checklist.mjs";
|
|
|
10
10
|
import { lintTool } from "./linter.mjs";
|
|
11
11
|
import { lspTool } from "./lsp.mjs";
|
|
12
12
|
import { executeTool } from "./execute.mjs";
|
|
13
|
-
import { fileOpsTool, processTool, getCurrentTimeTool } from "./ops.mjs";
|
|
13
|
+
import { fileOpsTool, processTool, getCurrentTimeTool, waitForTool } from "./ops.mjs";
|
|
14
14
|
import { treeTool } from "./tree.mjs";
|
|
15
15
|
|
|
16
16
|
export const builtinTools = [
|
|
@@ -19,7 +19,7 @@ export const builtinTools = [
|
|
|
19
19
|
websearchTool, lsTool, fetchTool, deleteTool,
|
|
20
20
|
gitTool, questionTool,
|
|
21
21
|
checklistTool, lintTool, lspTool, executeTool,
|
|
22
|
-
fileOpsTool, processTool, getCurrentTimeTool,
|
|
22
|
+
fileOpsTool, processTool, getCurrentTimeTool, waitForTool,
|
|
23
23
|
treeTool,
|
|
24
24
|
];
|
|
25
25
|
|
|
@@ -29,6 +29,6 @@ export {
|
|
|
29
29
|
websearchTool, lsTool, fetchTool, deleteTool,
|
|
30
30
|
gitTool, questionTool,
|
|
31
31
|
checklistTool, lintTool, lspTool, executeTool,
|
|
32
|
-
fileOpsTool, processTool, getCurrentTimeTool,
|
|
32
|
+
fileOpsTool, processTool, getCurrentTimeTool, waitForTool,
|
|
33
33
|
treeTool,
|
|
34
34
|
};
|
|
@@ -8,7 +8,8 @@ Parameters:
|
|
|
8
8
|
|
|
9
9
|
Notes:
|
|
10
10
|
- Either after_line or after_regex is required; if both are given, after_line wins.
|
|
11
|
-
- Use this instead of
|
|
11
|
+
- Use this instead of edit when you're adding a new line — a checklist item, a doc heading, a line of prose, a function, an import, or a block — no need to fabricate surrounding context for exact matching.
|
|
12
12
|
- The inserted content becomes its own line; it's equivalent to `lines.splice(targetLine, 0, content)`.
|
|
13
13
|
- Returns a diff of the change.
|
|
14
14
|
- **Read-before-insert guard**: if the file was modified by any write tool (write/edit/insert_after/hashline_edit/apply_patch/delete) since your last `read`, this tool REFUSES with an error — line numbers may be stale. Read the file again, then retry. This prevents after_line from silently landing at a drifted position.
|
|
15
|
+
- use the most recent read of the file as the source of old_string / line numbers / hashes — re-read after the file changed
|
package/src/tools/lint.md
CHANGED
|
@@ -3,6 +3,8 @@ Without 'full', runs a fast node --check (JS/TS syntax only, catches parse error
|
|
|
3
3
|
With 'full', runs the language-aware cascade: tsc –noEmit (TS); ruff (Python); cargo check (Rust); go vet (Go). JS/JSX files fall back to node --check; TS uses tsc --noEmit (requires tsconfig.json).
|
|
4
4
|
Use the fast default after every write/edit; use 'full' before declaring a task complete.
|
|
5
5
|
|
|
6
|
+
Returns the check result: `Syntax OK: <path>` / `Syntax error in <path>: <message>` (or the language checker's output — `✓ no issues` or the failure text).
|
|
7
|
+
|
|
6
8
|
Parameters:
|
|
7
9
|
- path: File to check (default: most recently modified file)
|
|
8
10
|
- full: Run the full language-aware cascade instead of just node --check (default false)
|
package/src/tools/lsp.md
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
LSP code intelligence: go to definition, find references, hover info, document symbols, diagnostics. Use this to understand code structure without grep-guessing function locations or type shapes.
|
|
1
|
+
LSP code intelligence: go to definition, find references, hover info, document symbols, diagnostics. Use this to understand code structure without grep-guessing function locations or type shapes. Find files with glob / repo_outline — use lsp for definition / references / diagnostics
|
|
2
2
|
|
|
3
3
|
Parameters:
|
|
4
4
|
- subcommand (required): LSP operation — "definition" | "references" | "hover" | "symbols" | "diagnostics"
|
|
5
5
|
- uri (required): Target file path (relative to project root)
|
|
6
6
|
- line: 1-based line number (for definition/references/hover)
|
|
7
7
|
- character: 1-based character offset (for definition/references/hover)
|
|
8
|
+
|
|
9
|
+
Notes:
|
|
10
|
+
- Returns the requested LSP result: definition location, references, hover info, document symbols, or diagnostics — or an error message.
|