thincoder 0.12.60 → 0.12.62
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 -2
- package/README.md +8 -6
- package/bin/thincoder.mjs +38 -124
- package/package.json +3 -2
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +38 -17
- package/src/acp.mjs +19 -6
- package/src/advisor/citations.mjs +83 -21
- package/src/advisor/compaction.mjs +174 -0
- package/src/advisor/loop.mjs +293 -0
- package/src/advisor/messages.mjs +36 -134
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +124 -329
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +3 -2
- package/src/agent/completion.mjs +1 -1
- package/src/agent/dispatch.mjs +47 -12
- package/src/agent/helpers.mjs +71 -13
- package/src/agent/record-results.mjs +13 -5
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +24 -7
- package/src/agent/setup-reminders.mjs +16 -9
- package/src/agent/setup.mjs +92 -128
- package/src/agent/spawn-child.mjs +43 -11
- package/src/agent-tools/advisor-async.mjs +70 -180
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +69 -20
- package/src/agent-tools/async-settle.mjs +13 -0
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +28 -10
- package/src/agent-tools/design-token.mjs +14 -1
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +3 -3
- package/src/agent-tools/escalate-async.mjs +22 -13
- package/src/agent-tools/read-history.mjs +31 -6
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +130 -17
- package/src/agent-tools/subagent-actions.mjs +18 -6
- package/src/agent-tools/subagent-async.mjs +66 -14
- package/src/agent-tools/subagent-panel.mjs +22 -15
- package/src/agent-tools/subagent-run.mjs +10 -7
- package/src/agent-tools/subagent-scheduler.mjs +57 -8
- package/src/agent-tools/subagent-spawn.mjs +69 -16
- package/src/agent-tools/subagent.mjs +175 -49
- package/src/agent-tools/verify.mjs +13 -34
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +42 -21
- package/src/cli/distill-command.mjs +2 -2
- package/src/cli/make-agent.mjs +23 -7
- package/src/cli/memory-command.mjs +2 -2
- package/src/cli/setup-wizard.mjs +29 -9
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +132 -63
- package/src/context.mjs +12 -1
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +35 -8
- package/src/expand-home.mjs +16 -0
- package/src/generate-title.mjs +9 -4
- package/src/heap-watch.mjs +88 -0
- package/src/hooks.mjs +7 -3
- package/src/ledger.mjs +227 -0
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +77 -36
- package/src/memory/core.mjs +8 -9
- package/src/memory/delete.mjs +2 -0
- package/src/memory/docs.mjs +17 -11
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/scan.mjs +95 -0
- package/src/memory/schema.mjs +15 -3
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/prompt-overlays.mjs +73 -16
- package/src/prompts/advisor-design.md +19 -9
- package/src/prompts/advisor-round1.md +8 -2
- package/src/prompts/advisor-round2.md +14 -3
- package/src/prompts/advisor-round3.md +14 -3
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +2 -0
- package/src/prompts/discipline-engineering.md +258 -0
- package/src/prompts/discipline-normal.md +185 -0
- package/src/prompts/persona-coder.md +21 -0
- package/src/prompts/persona-eng-coder.md +37 -0
- package/src/prompts/persona-eng-designer.md +60 -0
- package/src/prompts/persona-engineering.md +55 -0
- package/src/prompts/persona-explore.md +15 -0
- package/src/prompts/persona-normal.md +27 -0
- package/src/prompts/persona-plan.md +26 -0
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +13 -32
- package/src/provider/errors.mjs +26 -1
- package/src/provider/google.mjs +5 -6
- package/src/provider/index.mjs +2 -1
- package/src/provider/list-models.mjs +93 -0
- package/src/provider/rate.mjs +2 -1
- package/src/provider/responses.mjs +5 -3
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-gc.mjs +9 -2
- package/src/session-guard.mjs +12 -0
- package/src/session-segments.mjs +100 -0
- package/src/session-slots.mjs +10 -2
- package/src/session-store.mjs +441 -0
- package/src/session.mjs +137 -99
- package/src/text-budget.mjs +46 -0
- package/src/token-ttl.mjs +2 -1
- package/src/tools/{system.mjs → bash.mjs} +6 -243
- package/src/tools/file.mjs +30 -10
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +15 -34
- package/src/tools/index.mjs +4 -2
- package/src/tools/ops.mjs +20 -7
- package/src/tools/question.md +1 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -1
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/traces/trace-store.mjs +195 -64
- package/src/tui/agent-turn.mjs +32 -13
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +7 -1
- package/src/tui/cmd-advisor.mjs +3 -2
- package/src/tui/cmd-clear.mjs +2 -0
- package/src/tui/cmd-config.mjs +108 -37
- package/src/tui/cmd-eng.mjs +11 -27
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-new.mjs +5 -1
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-session.mjs +8 -4
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/display-budget.mjs +184 -0
- package/src/tui/index.mjs +69 -40
- package/src/tui/key-handler-search.mjs +9 -1
- package/src/tui/key-handler.mjs +61 -17
- package/src/tui/key-modes.mjs +86 -8
- package/src/tui/layout.mjs +18 -10
- package/src/tui/ledger-surface.mjs +69 -0
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +47 -10
- package/src/tui/pickers.mjs +28 -410
- package/src/tui/render-frame.mjs +38 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +5 -19
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/startup.mjs +45 -13
- package/src/tui/subagent-blocks.mjs +70 -90
- package/src/tui/subagent-children.mjs +125 -67
- package/src/tui/subagent-freeze.mjs +48 -45
- package/src/tui/subagent-panel.mjs +21 -66
- package/src/tui/suspension-drive.mjs +31 -83
- package/src/tui/tool-args.mjs +9 -4
- package/src/tui/tool-display.mjs +20 -5
- package/src/tui/tool-events.mjs +76 -30
- package/src/tui/tui-lifecycle.mjs +27 -7
- package/src/tui/wizard.mjs +52 -18
- package/src/tui/wrapped-spawn.mjs +54 -0
- package/src/prompts/coder.md +0 -13
- package/src/prompts/discipline.md +0 -84
- package/src/prompts/eng-coder.md +0 -19
- package/src/prompts/engineering-sub.md +0 -14
- package/src/prompts/engineering.md +0 -87
- package/src/prompts/explore.md +0 -12
- package/src/prompts/main.md +0 -34
- package/src/prompts/methodology-template.md +0 -38
- package/src/prompts/plan.md +0 -9
- package/src/prompts/system.md +0 -44
|
@@ -4,25 +4,12 @@ import {
|
|
|
4
4
|
truncate,
|
|
5
5
|
makeDecoder,
|
|
6
6
|
BASH_TIMEOUT_MS,
|
|
7
|
-
IGNORED_DIRS,
|
|
8
|
-
resolveInCwd,
|
|
9
|
-
globToRegex,
|
|
10
|
-
splitGlobPatterns,
|
|
11
|
-
compileGlobMatchers,
|
|
12
|
-
normalizeEOL,
|
|
13
7
|
} from "./shared.mjs";
|
|
14
8
|
import { spawn, execFileSync } from "node:child_process";
|
|
15
|
-
import { readFile, readdir, stat, lstat } from "node:fs/promises";
|
|
16
|
-
import { join } from "node:path";
|
|
17
9
|
|
|
18
10
|
/** Maximum buffer size per stream (stdout / stderr) before truncation */
|
|
19
11
|
const MAX_STREAM_BUF = 2_000_000
|
|
20
12
|
|
|
21
|
-
/** Escape a string for literal regex matching (grep literal=true). */
|
|
22
|
-
function escapeRegExp(s) {
|
|
23
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
24
|
-
}
|
|
25
|
-
|
|
26
13
|
/** Keep only output lines matching a regex (bash filter, case-insensitive). */
|
|
27
14
|
function applyLineFilter(output, filter) {
|
|
28
15
|
let re
|
|
@@ -85,13 +72,6 @@ function killProcessTree(child) {
|
|
|
85
72
|
}
|
|
86
73
|
}
|
|
87
74
|
|
|
88
|
-
/**
|
|
89
|
-
* Run a bash command: spawn, stream stdout/stderr with buffering and decoding,
|
|
90
|
-
* enforce timeout and signal abort, return formatted result.
|
|
91
|
-
*
|
|
92
|
-
* Returns a promise that resolves to the formatted output string (stdout + stderr + status + truncation note).
|
|
93
|
-
*/
|
|
94
|
-
|
|
95
75
|
/**
|
|
96
76
|
* Detect destructive git commands and auto-snapshot BEFORE execution.
|
|
97
77
|
* The scenario: the model writes uncommitted code, then (after breaking things)
|
|
@@ -129,6 +109,12 @@ async function gitGuardSnapshot(command, cwd) {
|
|
|
129
109
|
}
|
|
130
110
|
}
|
|
131
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Run a bash command: spawn, stream stdout/stderr with buffering and decoding,
|
|
114
|
+
* enforce timeout and signal abort, return formatted result.
|
|
115
|
+
*
|
|
116
|
+
* Returns a promise that resolves to the formatted output string (stdout + stderr + status + truncation note).
|
|
117
|
+
*/
|
|
132
118
|
function runBash(command, cwd, { timeout, signal, onOutput, shell }) {
|
|
133
119
|
return new Promise((resolve) => {
|
|
134
120
|
// Windows + default cmd: force UTF-8 code page for this child process (each spawn
|
|
@@ -280,226 +266,3 @@ export const bashTool = {
|
|
|
280
266
|
return hint ? `${hint}\n${body}` : body
|
|
281
267
|
},
|
|
282
268
|
}
|
|
283
|
-
|
|
284
|
-
// ---------------------------------------------------------------- glob
|
|
285
|
-
|
|
286
|
-
export const globTool = {
|
|
287
|
-
name: "glob",
|
|
288
|
-
description: DESC("glob"),
|
|
289
|
-
parameters: {
|
|
290
|
-
type: "object",
|
|
291
|
-
properties: {
|
|
292
|
-
pattern: { type: "string", description: "Glob pattern — supports **, *, ?, [..], {a,b} braces, and space-separated exclusion (\"**/*.js !test/**\")" },
|
|
293
|
-
path: { type: "string", description: "Directory to search in (default cwd)" },
|
|
294
|
-
},
|
|
295
|
-
required: ["pattern"],
|
|
296
|
-
},
|
|
297
|
-
readonly: true,
|
|
298
|
-
async execute(args, ctx) {
|
|
299
|
-
const base = resolveInCwd(ctx, args.path ?? ".")
|
|
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
|
-
}
|
|
310
|
-
const results = []
|
|
311
|
-
for await (const relPath of walkFiles(base)) {
|
|
312
|
-
if (match(relPath)) {
|
|
313
|
-
results.push(relPath)
|
|
314
|
-
if (results.length >= 1000) break
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
if (results.length === 0) return "(no matches)"
|
|
318
|
-
return truncate(results.sort().join("\n"))
|
|
319
|
-
},
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/** Recursively traverse files, yield relative paths (skip IGNORED_DIRS) */
|
|
323
|
-
async function* walkFiles(dir, rel = "") {
|
|
324
|
-
let entries
|
|
325
|
-
try {
|
|
326
|
-
entries = await readdir(dir, { withFileTypes: true })
|
|
327
|
-
} catch {
|
|
328
|
-
return
|
|
329
|
-
}
|
|
330
|
-
for (const e of entries) {
|
|
331
|
-
// Skip ignored dirs AND symbolic links (symlinks to directories would cause infinite loops)
|
|
332
|
-
if (e.isSymbolicLink()) continue
|
|
333
|
-
if (e.isDirectory() && IGNORED_DIRS.has(e.name)) continue
|
|
334
|
-
const relPath = rel ? `${rel}/${e.name}` : e.name
|
|
335
|
-
if (e.isDirectory()) {
|
|
336
|
-
yield* walkFiles(join(dir, e.name), relPath)
|
|
337
|
-
} else {
|
|
338
|
-
yield relPath
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/** Glob to regex: **\/ matches zero or more directory levels, ** crosses directories, * within segment, ? single char within segment */
|
|
344
|
-
|
|
345
|
-
// ---------------------------------------------------------------- grep
|
|
346
|
-
|
|
347
|
-
export const grepTool = {
|
|
348
|
-
name: "grep",
|
|
349
|
-
description: DESC("grep"),
|
|
350
|
-
parameters: {
|
|
351
|
-
type: "object",
|
|
352
|
-
properties: {
|
|
353
|
-
pattern: { type: "string", description: "Regular expression, or a literal string when literal=true" },
|
|
354
|
-
path: { type: "string", description: "Directory or file to search (default cwd)" },
|
|
355
|
-
glob: { type: "string", description: "Only search files matching this glob — supports **, *, ?, [..], {a,b} braces and space-separated exclusion (\"**/*.js !test/**\")" },
|
|
356
|
-
ignoreCase: { type: "boolean", description: "Case-insensitive match (default false)" },
|
|
357
|
-
literal: { type: "boolean", description: "Literal string match — no regex interpretation (default false)" },
|
|
358
|
-
before: { type: "integer", description: "Lines of context to show before each match (grep -B). Default 0" },
|
|
359
|
-
after: { type: "integer", description: "Lines of context to show after each match (grep -A). Default 0" },
|
|
360
|
-
},
|
|
361
|
-
required: ["pattern"],
|
|
362
|
-
},
|
|
363
|
-
readonly: true,
|
|
364
|
-
async execute(args, ctx) {
|
|
365
|
-
const base = resolveInCwd(ctx, args.path ?? ".")
|
|
366
|
-
let regex
|
|
367
|
-
try {
|
|
368
|
-
const pat = args.literal ? escapeRegExp(String(args.pattern)) : args.pattern
|
|
369
|
-
regex = new RegExp(pat, args.ignoreCase ? "i" : "")
|
|
370
|
-
} catch (e) {
|
|
371
|
-
throw new Error(`grep pattern /${args.pattern}/ is not a valid regex: ${e.message}`, { cause: e })
|
|
372
|
-
}
|
|
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
|
-
}
|
|
383
|
-
const before = Math.max(0, Math.floor(args.before ?? 0))
|
|
384
|
-
const after = Math.max(0, Math.floor(args.after ?? 0))
|
|
385
|
-
const wantCtx = before > 0 || after > 0
|
|
386
|
-
const hits = [] // { file, line(1-based), text }
|
|
387
|
-
const fileLines = new Map() // file -> string[] (cached only when context lines requested)
|
|
388
|
-
|
|
389
|
-
async function search(file) {
|
|
390
|
-
let content
|
|
391
|
-
try {
|
|
392
|
-
// Large file guard: skip files over 10MB to prevent OOM
|
|
393
|
-
const fst = await stat(file)
|
|
394
|
-
if (fst.size > 10_000_000) return
|
|
395
|
-
content = normalizeEOL(await readFile(file, "utf8"))
|
|
396
|
-
} catch {
|
|
397
|
-
return // Skip unreadable files; binary files will be read as UTF-8 and searched (may produce garbled matches)
|
|
398
|
-
}
|
|
399
|
-
const lines = content.split("\n")
|
|
400
|
-
if (wantCtx) fileLines.set(file, lines)
|
|
401
|
-
for (let i = 0; i < lines.length; i++) {
|
|
402
|
-
if (regex.test(lines[i])) {
|
|
403
|
-
hits.push({ file, line: i + 1, text: lines[i] })
|
|
404
|
-
if (hits.length >= 200) return
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
async function walk(target, rel) {
|
|
410
|
-
if (hits.length >= 200) return
|
|
411
|
-
// Use lstat to avoid following symlinks — prevents ./evil → /etc from making grep scan the entire system
|
|
412
|
-
let s
|
|
413
|
-
try { s = await lstat(target) } catch { return }
|
|
414
|
-
if (!s.isDirectory()) {
|
|
415
|
-
// rel 为相对搜索基的路径(排除前缀如 !test/** 必须按目录路径作用——不能用裸文件名);
|
|
416
|
-
// 单文件目标(path 指向文件)时 rel 为空 → 退化为按文件名过滤(既有语义)。
|
|
417
|
-
if (!fileTest || fileTest(rel || target.split(/[\\/]/).pop())) await search(target)
|
|
418
|
-
return
|
|
419
|
-
}
|
|
420
|
-
let entries
|
|
421
|
-
try {
|
|
422
|
-
entries = await readdir(target, { withFileTypes: true })
|
|
423
|
-
} catch {
|
|
424
|
-
return
|
|
425
|
-
}
|
|
426
|
-
for (const e of entries) {
|
|
427
|
-
if (e.isDirectory() && IGNORED_DIRS.has(e.name)) continue
|
|
428
|
-
await walk(join(target, e.name), rel ? `${rel}/${e.name}` : e.name)
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
await walk(base, "")
|
|
433
|
-
if (hits.length === 0) return "(no matches)"
|
|
434
|
-
|
|
435
|
-
// No context: keep original path:line: content format
|
|
436
|
-
if (!wantCtx) {
|
|
437
|
-
return truncate(hits.map((h) => `${h.file}:${h.line}: ${h.text}`).join("\n"))
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// With context: matching lines use ':', context lines use '-' (like ripgrep); overlapping ranges in same file are merged and de-duplicated
|
|
441
|
-
const fileMatched = new Map() // file -> Set<line>
|
|
442
|
-
for (const h of hits) {
|
|
443
|
-
if (!fileMatched.has(h.file)) fileMatched.set(h.file, new Set())
|
|
444
|
-
fileMatched.get(h.file).add(h.line)
|
|
445
|
-
}
|
|
446
|
-
const out = []
|
|
447
|
-
for (const [file, matchedLines] of fileMatched) {
|
|
448
|
-
const lines = fileLines.get(file) ?? []
|
|
449
|
-
const lineSet = new Set()
|
|
450
|
-
for (const ml of matchedLines) {
|
|
451
|
-
for (let l = Math.max(1, ml - before); l <= Math.min(lines.length, ml + after); l++) lineSet.add(l)
|
|
452
|
-
}
|
|
453
|
-
for (const l of [...lineSet].sort((a, b) => a - b)) {
|
|
454
|
-
const sep = matchedLines.has(l) ? ":" : "-"
|
|
455
|
-
out.push(`${file}${sep}${l}${sep} ${lines[l - 1]}`)
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
return truncate(out.join("\n"))
|
|
459
|
-
},
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// ---------------------------------------------------------------- websearch
|
|
463
|
-
|
|
464
|
-
export const lsTool = {
|
|
465
|
-
name: "ls",
|
|
466
|
-
description: DESC("ls"),
|
|
467
|
-
parameters: {
|
|
468
|
-
type: "object",
|
|
469
|
-
properties: {
|
|
470
|
-
path: { type: "string", description: "Directory path (default cwd)" },
|
|
471
|
-
filter: { type: "string", description: "Only list entries matching this glob (e.g. '*.mjs', '*test*')" },
|
|
472
|
-
},
|
|
473
|
-
},
|
|
474
|
-
readonly: true,
|
|
475
|
-
async execute(args, ctx) {
|
|
476
|
-
const abs = resolveInCwd(ctx, args.path ?? ".")
|
|
477
|
-
const filterRe = args.filter ? globToRegex(args.filter) : null
|
|
478
|
-
let entries
|
|
479
|
-
try {
|
|
480
|
-
entries = await readdir(abs, { withFileTypes: true })
|
|
481
|
-
} catch (e) {
|
|
482
|
-
if (e.code === "ENOENT" || e.code === "ENOTDIR") throw new Error(`ls: ${args.path ?? "."} — ${e.code === "ENOTDIR" ? "not a directory" : "not found"}`, { cause: e })
|
|
483
|
-
throw e
|
|
484
|
-
}
|
|
485
|
-
const rows = await Promise.all(
|
|
486
|
-
entries
|
|
487
|
-
.filter((e) => !filterRe || filterRe.test(e.name))
|
|
488
|
-
.slice(0, 500)
|
|
489
|
-
.map(async (e) => {
|
|
490
|
-
const s = await stat(join(abs, e.name)).catch(() => null)
|
|
491
|
-
const isDir = e.isDirectory()
|
|
492
|
-
return {
|
|
493
|
-
dir: isDir,
|
|
494
|
-
name: e.name + (isDir ? "/" : ""),
|
|
495
|
-
size: s?.size ?? 0,
|
|
496
|
-
mtime: s ? s.mtime.toISOString().slice(0, 16).replace("T", " ") : "?",
|
|
497
|
-
}
|
|
498
|
-
}),
|
|
499
|
-
)
|
|
500
|
-
rows.sort((a, b) => (a.dir === b.dir ? a.name.localeCompare(b.name) : a.dir ? -1 : 1))
|
|
501
|
-
if (rows.length === 0) return "(no entries)"
|
|
502
|
-
const out = rows.map((r) => `${r.dir ? "d" : "-"} ${r.name.padEnd(40)} ${String(r.size).padStart(10)} ${r.mtime}`)
|
|
503
|
-
return truncate(out.join("\n"))
|
|
504
|
-
},
|
|
505
|
-
}
|
package/src/tools/file.mjs
CHANGED
|
@@ -21,6 +21,10 @@ import { join, relative, dirname } from "node:path";
|
|
|
21
21
|
|
|
22
22
|
const MAX_FILE_READ_BYTES = 10_000_000
|
|
23
23
|
const MAX_IMAGE_BYTES = 15_000_000
|
|
24
|
+
// DUAL-END-TRUNCATION (F-1, C 方案——2026-09-09):read 双端尾行数——窗口截断大文件时
|
|
25
|
+
// 返回 头 N 行(N = 请求窗口,默认 MAX_READ_LINES)+ 省略注 + 尾 M 行(本常量)。M 与
|
|
26
|
+
// 窗口合计(2500 行)为"整读上限"——K=0(尾区与窗口相接)时整印剩余、不产生假省略注。
|
|
27
|
+
export const READ_TAIL_LINES = 500
|
|
24
28
|
|
|
25
29
|
// ────────────────────────────────────────
|
|
26
30
|
// Dirty-file tracking (read-before-insert guard)
|
|
@@ -95,17 +99,30 @@ export const readTool = {
|
|
|
95
99
|
const lines = content.split("\n")
|
|
96
100
|
const offset = Math.max(1, args.offset ?? 1)
|
|
97
101
|
const limit = Math.min(args.limit ?? MAX_READ_LINES, MAX_READ_LINES)
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const ln =
|
|
101
|
-
if (args.hashes) {
|
|
102
|
-
const h = createHash("sha256").update(l).digest("hex").slice(0, 12)
|
|
103
|
-
return `${ln}\t[${h}] ${l}`
|
|
104
|
-
}
|
|
102
|
+
const windowEnd = offset - 1 + limit // index just past the requested window
|
|
103
|
+
const render = (slice, startLn) => slice.map((l, i) => {
|
|
104
|
+
const ln = startLn + i
|
|
105
|
+
if (args.hashes) return `${ln}\t[${hashLine(l)}] ${l}`
|
|
105
106
|
return `${ln}\t${l}`
|
|
106
107
|
}).join("\n")
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
// DUAL-END-TRUNCATION (F-1, C 方案——DUAL-END-TRUNCATION.md 2026-09-09):
|
|
109
|
+
// 大文件(> MAX_READ_LINES 行)窗口截断时返回 头(请求窗口)+ 中段省略注 +
|
|
110
|
+
// 文件真实尾部(末 READ_TAIL_LINES 行——现尾注升级为真实尾行内容——offload 产物
|
|
111
|
+
// 的尾端结论可见)。≤ 阈值文件任何窗口走旧头向路径(字节零变化);窗口覆盖全文件
|
|
112
|
+
// 也走旧路径。与 hashes 模式照常交互(头尾行 hash 照算)。
|
|
113
|
+
if (windowEnd >= lines.length || lines.length <= MAX_READ_LINES) {
|
|
114
|
+
const suffix = windowEnd < lines.length ? `\n... (${lines.length} lines total, use offset to continue)` : ""
|
|
115
|
+
return truncate(render(lines.slice(offset - 1, windowEnd), offset) + suffix)
|
|
116
|
+
}
|
|
117
|
+
const head = render(lines.slice(offset - 1, windowEnd), offset)
|
|
118
|
+
// 尾区 = 文件末 READ_TAIL_LINES 行;尾区起点落在窗口内(重叠)→ 尾区从窗口后开始
|
|
119
|
+
// —— 任何行不打印两次;剩余全被覆盖(K=0)→ 整印余段且无假省略注。
|
|
120
|
+
const tailStart = Math.max(windowEnd, lines.length - READ_TAIL_LINES)
|
|
121
|
+
const middle = tailStart - windowEnd
|
|
122
|
+
const tail = render(lines.slice(tailStart), tailStart + 1)
|
|
123
|
+
return middle > 0
|
|
124
|
+
? `${head}\n…(truncated: ${middle} lines in middle, use offset to continue)\n${tail}`
|
|
125
|
+
: `${head}\n${tail}`
|
|
109
126
|
},
|
|
110
127
|
}
|
|
111
128
|
|
|
@@ -145,11 +162,14 @@ export const readImageTool = {
|
|
|
145
162
|
|
|
146
163
|
// Vision capability gate: injecting an image into a text-only model's history poisons the whole
|
|
147
164
|
// conversation (every subsequent request 400s on the image part). Refuse before reading the file.
|
|
165
|
+
// F-3(IMAGE-DOWNGRADE-VISION——2026-09-09):软引导句逐字锚(设计档照抄——CLI 无 extension
|
|
166
|
+
// 式自动降级——模型收到错误后自选 spawn 视觉渠道子代理路径)。
|
|
148
167
|
const model = ctx.agent?.provider?.model
|
|
149
168
|
if (model && !specForModel(model).multimodal) {
|
|
150
169
|
throw new Error(
|
|
151
170
|
`Model "${model}" does not support image input — read_image is unavailable with this provider. ` +
|
|
152
|
-
`Verify visual output programmatically (file size, dimensions, pixel checks via code) or ask the user to switch to a vision-capable provider
|
|
171
|
+
`Verify visual output programmatically (file size, dimensions, pixel checks via code) or ask the user to switch to a vision-capable provider.\n` +
|
|
172
|
+
`模型不支持图像——可 spawn 一个视觉模型子代理(subagent model 参数指视觉渠道)用 read_image 读图`
|
|
153
173
|
)
|
|
154
174
|
}
|
|
155
175
|
const mime = IMAGE_EXTENSIONS[ext]
|
package/src/tools/git.md
CHANGED
|
@@ -7,7 +7,7 @@ Run a git command. Only works inside a git repository.
|
|
|
7
7
|
- action='log': recent commits. count (default 10), oneline=true compact, path=<file> for one file's history.
|
|
8
8
|
- action='show': a commit's details (--stat). ref=<ref> (default HEAD).
|
|
9
9
|
- action='add': stage files — path=<file> (granular) or all changes when path omitted.
|
|
10
|
-
- action='commit':
|
|
10
|
+
- action='commit': commit. message required; path=<file> → `git commit --only <paths>` — commits those files' working-tree content only (other staged batches are NOT mixed in — atomic); without path → add -A + full commit. New (untracked) files aren't committable by path alone — `git add` them first (staging extra is safe: --only still commits only the listed files).
|
|
11
11
|
- action='rm': untrack a file/dir (git rm --cached, kept on disk). path required.
|
|
12
12
|
- action='push'/'fetch'/'pull': sync with remote. remote=<origin>, ref=<branch or tag> (space-separated for multiple), tags=true for --tags.
|
|
13
13
|
- action='tag': manage tags. tagAction=list (optional filter) / create (name, optional ref) / delete (name; snapshots first).
|
package/src/tools/git.mjs
CHANGED
|
@@ -149,14 +149,22 @@ export const gitTool = {
|
|
|
149
149
|
}
|
|
150
150
|
case "commit": {
|
|
151
151
|
if (!args.message) return "Error: commit requires message"
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (!add.ok) return truncate(`git add failed: ${add.err || add.out || "(no output)"}`)
|
|
157
|
-
const commit = runGitStrict(ctx.cwd, ["commit", "-m", args.message])
|
|
152
|
+
// F-3 (QUICKFIX-BATCH-2):path 给定(含空串——空/空白 trim 后空一律明确错误,绝不
|
|
153
|
+
// 回落 add -A)→ git commit --only <paths>——从工作树取列文件提交(忽略索引他批——
|
|
154
|
+
// 原子——不再先 add)。空格分隔唯一形态(含空格文件名不支持)。无 path(缺省)→ add
|
|
155
|
+
// -A 全量(单代理语义保留)。
|
|
158
156
|
const parts = []
|
|
159
|
-
|
|
157
|
+
let commit
|
|
158
|
+
if (args.path !== undefined && args.path !== null) {
|
|
159
|
+
const trimmed = args.path.trim()
|
|
160
|
+
if (!trimmed) return "Error: commit path is empty/whitespace — give at least one file path (space-separated)"
|
|
161
|
+
commit = runGitStrict(ctx.cwd, ["commit", "--only", "-m", args.message, "--", ...trimmed.split(/\s+/)])
|
|
162
|
+
} else {
|
|
163
|
+
const add = runGitStrict(ctx.cwd, ["add", "-A"])
|
|
164
|
+
if (!add.ok) return truncate(`git add failed: ${add.err || add.out || "(no output)"}`)
|
|
165
|
+
if (add.out) parts.push(add.out)
|
|
166
|
+
commit = runGitStrict(ctx.cwd, ["commit", "-m", args.message])
|
|
167
|
+
}
|
|
160
168
|
if (commit.ok) {
|
|
161
169
|
if (commit.out) parts.push(commit.out)
|
|
162
170
|
// F6: commit = new safety baseline — clear this project's checkpoints
|
|
@@ -346,30 +354,3 @@ export const gitTool = {
|
|
|
346
354
|
},
|
|
347
355
|
}
|
|
348
356
|
|
|
349
|
-
// ---------------------------------------------------------------- question
|
|
350
|
-
|
|
351
|
-
export const questionTool = {
|
|
352
|
-
name: "question",
|
|
353
|
-
description: DESC("question"),
|
|
354
|
-
parameters: {
|
|
355
|
-
type: "object",
|
|
356
|
-
properties: {
|
|
357
|
-
question: { type: "string", description: "The question to ask the user" },
|
|
358
|
-
options: {
|
|
359
|
-
type: "array",
|
|
360
|
-
items: { type: "string" },
|
|
361
|
-
description: "Single-choice options for the user to pick from (optional)",
|
|
362
|
-
},
|
|
363
|
-
},
|
|
364
|
-
required: ["question"],
|
|
365
|
-
},
|
|
366
|
-
readonly: true,
|
|
367
|
-
async execute(args, ctx) {
|
|
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)"
|
|
372
|
-
return ctx.onQuestion(args.question, args.options ?? [])
|
|
373
|
-
},
|
|
374
|
-
}
|
|
375
|
-
|
package/src/tools/index.mjs
CHANGED
|
@@ -3,9 +3,11 @@ export { toOpenAISchema } from "./shared.mjs";
|
|
|
3
3
|
|
|
4
4
|
import { readTool, writeTool, editTool, insertAfterTool, readImageTool, hashlineEditTool } from "./file.mjs";
|
|
5
5
|
import { applyPatchTool, deleteTool } from "./patch.mjs";
|
|
6
|
-
import { bashTool
|
|
6
|
+
import { bashTool } from "./bash.mjs";
|
|
7
|
+
import { globTool, grepTool, lsTool } from "./search.mjs";
|
|
7
8
|
import { websearchTool, fetchTool } from "./web.mjs";
|
|
8
|
-
import { gitTool
|
|
9
|
+
import { gitTool } from "./git.mjs";
|
|
10
|
+
import { questionTool } from "./question.mjs";
|
|
9
11
|
import { checklistTool } from "./checklist.mjs";
|
|
10
12
|
import { lintTool } from "./linter.mjs";
|
|
11
13
|
import { lspTool } from "./lsp.mjs";
|
package/src/tools/ops.mjs
CHANGED
|
@@ -176,14 +176,27 @@ function asyncEntryDone(agent, id) {
|
|
|
176
176
|
return entry.done === true || entry.status === "done"
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
/**
|
|
180
|
-
function
|
|
181
|
-
|
|
179
|
+
/** 评审池双载体(第 10 批 §18.3 #2):agent ∪ history——未初始化的载体不进集合。 */
|
|
180
|
+
function advisorPools(agent) {
|
|
181
|
+
return [agent?._asyncAdvisors, agent?.history?._asyncAdvisors].filter((m) => m instanceof Map)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Any entry matching pred that is still running/queued (not done) — in the given pool. */
|
|
185
|
+
function hasRunningIn(pool, pred) {
|
|
186
|
+
for (const e of pool?.values() ?? []) {
|
|
182
187
|
if (pred(e) && !(e.done === true || e.status === "done")) return true
|
|
183
188
|
}
|
|
184
189
|
return false
|
|
185
190
|
}
|
|
186
191
|
|
|
192
|
+
/** "advisor settled" 真判据(第 10 批 §18.3 #2 修正):**评审池真实态**——双载体
|
|
193
|
+
* (agent._asyncAdvisors ∪ history._asyncAdvisors)无 running/queued 条目(与 §11.2 的
|
|
194
|
+
* 未决评审判定 advisorReviewPending 同源语义)。修前读**子代理池**的 role==="advisor"
|
|
195
|
+
* 条目——评审条目只在 _asyncAdvisors → 恒无命中 → 恒真 0ms 秒过(用户实证)。 */
|
|
196
|
+
function advisorSettled(agent) {
|
|
197
|
+
return !advisorPools(agent).some((pool) => hasRunningIn(pool, (e) => e.status === "running" || e.status === "queued"))
|
|
198
|
+
}
|
|
199
|
+
|
|
187
200
|
/** "consult done" — every consult session has drained (pending 0) or was
|
|
188
201
|
* explicitly stopped (its children were aborted). No sessions → true (vacuously). */
|
|
189
202
|
function consultAllDone(agent) {
|
|
@@ -221,10 +234,10 @@ export async function evaluateWaitForCondition(condition, ctx) {
|
|
|
221
234
|
const agent = ctx?.agent
|
|
222
235
|
switch (parsed.kind) {
|
|
223
236
|
case "advisor":
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
return
|
|
237
|
+
// 第 10 批修正(§18.3 #2——修前恒真 0ms 秒过):判据 = 评审池真实态(双载体)——
|
|
238
|
+
// 不再读子代理池的 role==="advisor" 条目(该池永无此类条目);语义与 §11.2 未决
|
|
239
|
+
// 评审判定同源(advisorReviewPending)。
|
|
240
|
+
return advisorSettled(agent)
|
|
228
241
|
case "subagent":
|
|
229
242
|
return asyncEntryDone(agent, parsed.arg)
|
|
230
243
|
case "consult":
|
package/src/tools/question.md
CHANGED
|
@@ -8,6 +8,7 @@ Notes:
|
|
|
8
8
|
- The agent loop pauses until the user answers
|
|
9
9
|
- The answer is injected as the next user message
|
|
10
10
|
- Returns the user's answer — the chosen option or free text — as the next message; the loop resumes when it arrives.
|
|
11
|
+
- Availability: this tool needs an interactive UI — in contexts without one (headless runs, subagent children) it returns an error instead of asking; put the question in your reply text instead.
|
|
11
12
|
- Use sparingly — prefer making reasonable decisions when possible
|
|
12
13
|
- Ask ONE question per call — never bundle multiple sub-questions into one question string; ask the next one after the answer arrives.
|
|
13
14
|
- Keep the question text short — one or two sentences. Background, context, and analysis belong in your normal reply text, NOT in the question.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DESC } from "./shared.mjs";
|
|
2
|
+
|
|
3
|
+
export const questionTool = {
|
|
4
|
+
name: "question",
|
|
5
|
+
description: DESC("question"),
|
|
6
|
+
parameters: {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
question: { type: "string", description: "The question to ask the user" },
|
|
10
|
+
options: {
|
|
11
|
+
type: "array",
|
|
12
|
+
items: { type: "string" },
|
|
13
|
+
description: "Single-choice options for the user to pick from (optional)",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
required: ["question"],
|
|
17
|
+
},
|
|
18
|
+
readonly: true,
|
|
19
|
+
async execute(args, ctx) {
|
|
20
|
+
if (!ctx.onQuestion) throw new Error("question tool not supported in this context (no UI to ask)")
|
|
21
|
+
// §22 D-Q3 机械限制(2026-09-06 用户裁定:100 字符 / 4 条——超限拒绝回模型,不弹卡、不调 onQuestion)
|
|
22
|
+
if (args.question.length > 100) return "(error: question too long (>100 chars) — ask ONE short question; background belongs in your normal reply text)"
|
|
23
|
+
if (Array.isArray(args.options) && args.options.length > 4) return "(error: too many options (>4) — offer at most 4 plain-string options)"
|
|
24
|
+
return ctx.onQuestion(args.question, args.options ?? [])
|
|
25
|
+
},
|
|
26
|
+
}
|
package/src/tools/read.md
CHANGED
|
@@ -11,7 +11,7 @@ Read a text file. Returns numbered lines. Use offset/limit to page large files.
|
|
|
11
11
|
Parameters:
|
|
12
12
|
- path (required): File path, relative to cwd or absolute (alias: filePath)
|
|
13
13
|
- offset: 1-based line number to start reading from
|
|
14
|
-
- limit: Max lines to return (default 2000)
|
|
14
|
+
- limit: Max lines to return (default 2000) — windows over files beyond that return head + `…(truncated: K lines in middle, use offset to continue)` + the file's real tail lines, so the file end is never hidden
|
|
15
15
|
- hashes: Include SHA256 content hashes per line (for hashline_edit). Set true before using hashline_edit.
|
|
16
16
|
|
|
17
17
|
Notes:
|
package/src/tools/read_image.md
CHANGED
|
@@ -5,4 +5,4 @@ Parameters:
|
|
|
5
5
|
|
|
6
6
|
Notes:
|
|
7
7
|
- Raster formats only (png/jpg/gif/webp): no mainstream vision API (Kimi, Anthropic, OpenAI, Gemini) accepts svg or bmp, and an unsupported image part in history makes every subsequent request fail with 400. svg files are returned as text source instead (readable by any model); bmp is rejected — convert to PNG first.
|
|
8
|
-
- This tool only works with models that support vision/image input (
|
|
8
|
+
- This tool only works with models that support vision/image input (capability is spec-driven — the model spec decides, not a hardcoded list). Models without vision support will receive an error — except svg, which needs no vision support since it is read as text.
|