thincoder 0.12.54 → 0.12.59
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 +98 -0
- package/README.md +1 -1
- package/bin/thincoder.mjs +25 -3
- package/package.json +3 -7
- package/src/acp/bridge.mjs +132 -26
- package/src/advisor/messages.mjs +38 -3
- package/src/advisor/run.mjs +91 -53
- package/src/advisor.mjs +15 -7
- package/src/agent/dispatch.mjs +156 -39
- package/src/agent/helpers.mjs +46 -4
- package/src/agent/setup.mjs +102 -19
- package/src/agent/spawn-child.mjs +28 -1
- package/src/agent-tools/advisor.mjs +43 -11
- package/src/agent-tools/consult.mjs +37 -6
- package/src/agent-tools/eng.mjs +4 -1
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +160 -0
- package/src/agent-tools/settings.mjs +162 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +432 -0
- package/src/agent-tools/subagent-async.mjs +427 -0
- package/src/agent-tools/subagent-scheduler.mjs +319 -0
- package/src/agent-tools/subagent.mjs +565 -128
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +161 -49
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +182 -81
- package/src/auto-think.mjs +14 -0
- package/src/cli/make-agent.mjs +27 -1
- package/src/cli/memory-command.mjs +28 -7
- package/src/cli/permission.mjs +8 -1
- package/src/config.mjs +125 -8
- package/src/context.mjs +115 -34
- package/src/distill.mjs +19 -1
- package/src/escape.mjs +82 -27
- package/src/log.mjs +195 -0
- package/src/mcp/transport-http.mjs +13 -1
- package/src/mcp.mjs +52 -7
- package/src/memory/code-sync.mjs +1 -1
- package/src/memory/core.mjs +204 -10
- package/src/memory/docs.mjs +197 -62
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +38 -1
- package/src/prompts/advisor-design.md +46 -0
- package/src/prompts/advisor-round1.md +49 -2
- package/src/prompts/advisor-round2.md +47 -0
- package/src/prompts/advisor-round3.md +47 -0
- package/src/prompts/coder.md +22 -0
- package/src/prompts/consult-base.md +13 -0
- package/src/prompts/discipline.md +25 -6
- package/src/prompts/eng-coder.md +2 -2
- package/src/prompts/engineering-sub.md +23 -1
- package/src/prompts/engineering.md +157 -50
- package/src/prompts/explore.md +1 -2
- package/src/prompts/main.md +11 -5
- package/src/prompts/methodology-template.md +14 -0
- package/src/prompts/system.md +5 -2
- package/src/provider/anthropic.mjs +7 -5
- package/src/provider/core.mjs +104 -28
- package/src/provider/google.mjs +57 -24
- package/src/provider/normalize.mjs +1 -1
- package/src/provider/rate.mjs +0 -2
- package/src/provider/responses.mjs +8 -13
- package/src/provider/sse.mjs +20 -0
- package/src/session.mjs +15 -0
- package/src/tools/apply_patch.md +5 -1
- package/src/tools/bash.md +3 -3
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +92 -0
- package/src/tools/edit-diff.mjs +265 -0
- package/src/tools/edit.md +11 -6
- package/src/tools/execute.md +8 -8
- package/src/tools/execute.mjs +31 -35
- package/src/tools/file.mjs +26 -114
- package/src/tools/file_ops.md +3 -2
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +8 -16
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -2
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +3 -1
- package/src/tools/linter.mjs +9 -37
- package/src/tools/lsp.md +4 -1
- package/src/tools/patch.mjs +84 -13
- package/src/tools/pdf-parse-text.mjs +497 -0
- package/src/tools/pdf-parse-xref.mjs +499 -0
- package/src/tools/pdf.mjs +155 -0
- package/src/tools/question.md +2 -1
- package/src/tools/read.md +1 -0
- package/src/tools/read_pdf.md +21 -0
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +11 -32
- package/src/tools/system.mjs +6 -21
- package/src/tools/tree.md +2 -1
- 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 +387 -24
- package/src/tui/clipboard.mjs +17 -6
- package/src/tui/cmd-config.mjs +29 -9
- package/src/tui/cmd-eng.mjs +1 -0
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp-form.mjs +197 -0
- package/src/tui/cmd-mcp.mjs +264 -114
- package/src/tui/cmd-think.mjs +1 -1
- package/src/tui/index.mjs +49 -95
- package/src/tui/interaction.mjs +41 -3
- package/src/tui/key-handler.mjs +105 -143
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +41 -1
- package/src/tui/pickers.mjs +73 -7
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +45 -20
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +165 -0
- package/src/tui/render.mjs +4 -4
- package/src/tui/startup.mjs +40 -2
- package/src/tui/subagent-blocks.mjs +404 -111
- package/src/tui/subagent-panel.mjs +88 -13
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-events.mjs +172 -95
- package/src/tui/update-notice.mjs +72 -0
- package/src/tui/wizard.mjs +36 -6
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/tools/exec-prelude.mjs +0 -84
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* update-notice.mjs — 后台升级提示(2026-09-03 D-S1c 自 index.mjs 拆出):
|
|
3
|
+
* upgradeFailureText / pendingNoticeReady(纯函数,index.mjs re-export——tui.test
|
|
4
|
+
* 动态 import 面零改动)+ createUpdateNotice 装配(升级提示 picker + 启动检查)。
|
|
5
|
+
* index.mjs 只保留装配调用(showPicker 闭包在此经 ctx 注入)。
|
|
6
|
+
*/
|
|
7
|
+
import { ansi, C } from "./ansi.mjs"
|
|
8
|
+
|
|
9
|
+
/** 升级失败提示文案:附 npm 输出尾部(最多 3 行),方便定位失败原因。 */
|
|
10
|
+
export function upgradeFailureText(code, output) {
|
|
11
|
+
const tail = (output ?? "").trimEnd().split("\n").slice(-3).join("\n")
|
|
12
|
+
return `✗ Upgrade failed (exit ${code}). Run \`thincoder upgrade\` manually.${tail ? `\n${tail}` : ""}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 后台更新提示可弹出的条件:无任何交互弹层(picker/permission/question)激活。 */
|
|
16
|
+
export function pendingNoticeReady(state) {
|
|
17
|
+
return Boolean(state.pendingNotice && !state.picker && !state.permission && !state.question)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 升级提示 + 启动检查装配。ctx: { state, showPicker, pushLine, pushLabel, render }
|
|
22
|
+
* 返回 { showUpdateNotice, checkUpdates }——checkUpdates 非阻塞(网络错误静默跳过);
|
|
23
|
+
* 有 picker 打开时提示挂到 state.pendingNotice,picker 全部关闭后由 doRender 弹出。
|
|
24
|
+
*/
|
|
25
|
+
export function createUpdateNotice(ctx) {
|
|
26
|
+
const { state, showPicker, pushLine, pushLabel, render } = ctx
|
|
27
|
+
|
|
28
|
+
const showUpdateNotice = async (result) => {
|
|
29
|
+
const sel = await showPicker(`Update available: ${result.local} → ${result.latest}`, [
|
|
30
|
+
{ type: "header", text: `thincoder ${result.latest} is available (current: ${result.local})` },
|
|
31
|
+
{ type: "item", text: "Upgrade now", action: "upgrade" },
|
|
32
|
+
{ type: "item", text: "Later", action: "later" },
|
|
33
|
+
])
|
|
34
|
+
if (sel?.action !== "upgrade") return
|
|
35
|
+
pushLabel(`❯ Upgrade`, ansi.bold + C.tool)
|
|
36
|
+
pushLine(`Upgrading to ${result.latest}...`, C.tool)
|
|
37
|
+
const { exec } = await import("node:child_process")
|
|
38
|
+
const cp = exec("npm install -g thincoder@latest", { windowsHide: true })
|
|
39
|
+
let stdout = ""
|
|
40
|
+
cp.stdout?.on("data", (d) => { stdout += d })
|
|
41
|
+
cp.stderr?.on("data", (d) => { stdout += d })
|
|
42
|
+
cp.on("close", (code) => {
|
|
43
|
+
if (code === 0) {
|
|
44
|
+
pushLine(`✓ Upgraded to ${result.latest}. Restart to apply.`, C.tool)
|
|
45
|
+
} else {
|
|
46
|
+
pushLine(upgradeFailureText(code, stdout), C.error)
|
|
47
|
+
}
|
|
48
|
+
render()
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const checkUpdates = async () => {
|
|
53
|
+
try {
|
|
54
|
+
const { readFileSync } = await import("node:fs")
|
|
55
|
+
const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"))
|
|
56
|
+
const { checkForUpdate } = await import("../upgrade.mjs")
|
|
57
|
+
const result = await checkForUpdate(pkg.version)
|
|
58
|
+
if (result?.newer) {
|
|
59
|
+
// Defer: if wizard is still active, just show a dim line
|
|
60
|
+
if (state.wizard) {
|
|
61
|
+
pushLine(`Tip: thincoder ${result.latest} is available (run /upgrade later or restart)`, C.dim)
|
|
62
|
+
render()
|
|
63
|
+
} else {
|
|
64
|
+
state.pendingNotice = result
|
|
65
|
+
render()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch { /* network error or timeout — silently skip */ }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { showUpdateNotice, checkUpdates }
|
|
72
|
+
}
|
package/src/tui/wizard.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* wizard.mjs — first-launch config wizard
|
|
3
|
-
* Extracted from index.mjs: provider select → step-by-step input (name/baseURL/model/key/embedkey)
|
|
3
|
+
* Extracted from index.mjs: provider select → step-by-step input (name/baseURL/model/format/key/embedkey)
|
|
4
|
+
* → persist → then model picker. Custom 分支含 API format 步(D-C2,TUI.md §10.6D)。
|
|
4
5
|
* Accesses shared state and UI functions from the startTUI closure via ctx object.
|
|
5
6
|
* ctx: { agent, state, pushLine, pushLabel, render, persistRaw, openModelPicker }
|
|
6
7
|
*/
|
|
@@ -23,7 +24,13 @@ export function createWizard(ctx) {
|
|
|
23
24
|
}
|
|
24
25
|
for (const [name, p] of Object.entries(PRESETS)) {
|
|
25
26
|
if (!agent.providers.some((x) => x.name === name)) {
|
|
26
|
-
items.push({
|
|
27
|
+
items.push({
|
|
28
|
+
kind: "preset", name, baseURL: p.baseURL, model: p.model, label: `${name} (${p.desc})`,
|
|
29
|
+
// 预设自身声明的扩展字段随 preset 直达落盘(code review 🟡——与 pickers preset 路径同构;
|
|
30
|
+
// claude/gemini 缺 format、deepseek/glm 缺 thinking/maxTokens 会静默错配);不新增提问步。
|
|
31
|
+
format: p.format, thinking: p.thinking, reasoningEffort: p.reasoningEffort,
|
|
32
|
+
maxTokens: p.maxTokens, chatPath: p.chatPath,
|
|
33
|
+
})
|
|
27
34
|
}
|
|
28
35
|
}
|
|
29
36
|
items.push({ kind: "custom", name: null, label: "Custom endpoint…" })
|
|
@@ -45,6 +52,14 @@ export function createWizard(ctx) {
|
|
|
45
52
|
prompt: "Enter model name (e.g. gpt-4o)",
|
|
46
53
|
validate: (v) => v.length > 0 || "Model name required",
|
|
47
54
|
},
|
|
55
|
+
// D-C2(TUI.md §10.6D):Custom 分支的 API format 步(endpoint 后 key 前——与 Add Provider
|
|
56
|
+
// picker 路径两入口一致;默认 openai)。空输入 = openai 直过(Enter 即默认——同 D-C1 index=0);
|
|
57
|
+
// Esc 在该步沿用 wizard 既有“Esc 随时跳过”语义(取消整个向导——无半配置落盘)。
|
|
58
|
+
format: {
|
|
59
|
+
prompt: "API format [openai/anthropic/google] (Enter = openai default)",
|
|
60
|
+
// 大小写不敏感(旧手输口径——输入统一 toLowerCase 后落盘);空输入 = openai 默认
|
|
61
|
+
validate: (v) => !v || /^(openai|anthropic|google)$/i.test(v) || "API format must be one of: openai, anthropic, google",
|
|
62
|
+
},
|
|
48
63
|
key: {
|
|
49
64
|
prompt: "Enter API key",
|
|
50
65
|
validate: (v) => v.length > 0 || "Key must not be empty",
|
|
@@ -54,7 +69,7 @@ export function createWizard(ctx) {
|
|
|
54
69
|
validate: () => true, // skippable
|
|
55
70
|
},
|
|
56
71
|
}
|
|
57
|
-
const WIZARD_NEXT = { name: "baseURL", baseURL: "model", model: "key", key: "embedkey", embedkey: null }
|
|
72
|
+
const WIZARD_NEXT = { name: "baseURL", baseURL: "model", model: "format", format: "key", key: "embedkey", embedkey: null }
|
|
58
73
|
|
|
59
74
|
function startWizard() {
|
|
60
75
|
state.wizard = { step: "provider", index: 0, scroll: 0, selectedLine: 0, fields: {}, error: null, lines: [] }
|
|
@@ -94,6 +109,11 @@ export function createWizard(ctx) {
|
|
|
94
109
|
w.step = "name"
|
|
95
110
|
} else {
|
|
96
111
|
w.fields = { name: item.name, baseURL: item.baseURL, model: item.model }
|
|
112
|
+
// preset 直达:预设声明的扩展字段(format/thinking/maxTokens/chatPath…)直接带进 fields——
|
|
113
|
+
// 无 format 提问步(T-C4)但落盘不丢字段(code review 🟡——picker preset 路径同款复制)。
|
|
114
|
+
for (const k of ["format", "thinking", "reasoningEffort", "maxTokens", "chatPath"]) {
|
|
115
|
+
if (item[k]) w.fields[k] = item[k]
|
|
116
|
+
}
|
|
97
117
|
w.step = "key"
|
|
98
118
|
}
|
|
99
119
|
renderWizard()
|
|
@@ -111,7 +131,9 @@ export function createWizard(ctx) {
|
|
|
111
131
|
w.error = null
|
|
112
132
|
state.input = []
|
|
113
133
|
state.cursor = 0
|
|
114
|
-
w.fields[w.step === "key" ? "key" : w.step] = w.step === "baseURL" ? value.replace(/\/+$/, "")
|
|
134
|
+
w.fields[w.step === "key" ? "key" : w.step] = w.step === "baseURL" ? value.replace(/\/+$/, "")
|
|
135
|
+
: w.step === "format" ? (value.toLowerCase() || "openai") // 空输入 = openai 默认(D-C2)
|
|
136
|
+
: value
|
|
115
137
|
const next = WIZARD_NEXT[w.step]
|
|
116
138
|
if (next) {
|
|
117
139
|
w.step = next
|
|
@@ -131,9 +153,17 @@ export function createWizard(ctx) {
|
|
|
131
153
|
async function finishWizard() {
|
|
132
154
|
const f = state.wizard.fields
|
|
133
155
|
state.wizard = null
|
|
156
|
+
// D-C2:format 非默认(anthropic/google)时落盘;openai = 默认省略(与 D-C1 picker 路径同构)
|
|
157
|
+
const providerRec = { name: f.name, baseURL: f.baseURL, model: f.model, apiKey: f.key }
|
|
158
|
+
if (f.format && f.format !== "openai") providerRec.format = f.format
|
|
159
|
+
// code review 🟡:preset 直达带来的扩展字段一并落盘(truthy 语义与 pickers preset 分支一致——
|
|
160
|
+
// thinking: null 不落;Custom 路径无这些字段不受影响)
|
|
161
|
+
for (const k of ["thinking", "reasoningEffort", "maxTokens", "chatPath"]) {
|
|
162
|
+
if (f[k]) providerRec[k] = f[k]
|
|
163
|
+
}
|
|
134
164
|
const existing = agent.providers.find((p) => p.name === f.name)
|
|
135
|
-
if (existing) Object.assign(existing,
|
|
136
|
-
else agent.providers.push(
|
|
165
|
+
if (existing) Object.assign(existing, providerRec)
|
|
166
|
+
else agent.providers.push(providerRec)
|
|
137
167
|
agent.activeProvider = f.name
|
|
138
168
|
agent.activeModel = null
|
|
139
169
|
agent.provider = { ...agent.providers.find((p) => p.name === f.name) }
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* escalate.mjs — 飞刀 (the "flying knife", docs/design/ESCALATE.md). CLI port.
|
|
3
|
-
*
|
|
4
|
-
* Hand an implementation task to a STRONGER model — like a hospital flying in an
|
|
5
|
-
* outside expert (飞刀): the expert arrives, operates personally (WRITE access),
|
|
6
|
-
* hands back the post-op report, leaves. Complementary to consult (parallel
|
|
7
|
-
* READ-ONLY opinions for judgment calls).
|
|
8
|
-
*
|
|
9
|
-
* Candidate pool = all consultModels rows. The tool is only registered when the
|
|
10
|
-
* pool is non-empty (setup.mjs).
|
|
11
|
-
*
|
|
12
|
-
* CLI adaptation (vs the VS Code plugin): the child runner is CLI's runAgent
|
|
13
|
-
* (runAgent(child, input, callbacks, opts) — an agent object, not provider+cwd);
|
|
14
|
-
* the child is createAgent({ role: "coder", CODER_OVERLAY }); activity streams to
|
|
15
|
-
* the parent TUI via the relay prefix `escalate#<id>/`; mutations merge via the
|
|
16
|
-
* CLI mergeChildMutations(parent, child) (agent object, not a state sink).
|
|
17
|
-
*/
|
|
18
|
-
import { isAbsolute, relative } from "node:path"
|
|
19
|
-
import { createAgent, runAgent, CODER_OVERLAY, DEFAULT_SUBAGENT_TURNS } from "../agent.mjs"
|
|
20
|
-
import { resolveChildProvider, mergeChildMutations } from "./subagent.mjs"
|
|
21
|
-
import { makeRelay, wrapChildCallbacks, runWithContinue, ensureChildApiKey, clampEffort, TURN_CAP_MARK } from "../agent/spawn-child.mjs"
|
|
22
|
-
|
|
23
|
-
const label = (m) => `${m.provider}:${m.model}`
|
|
24
|
-
|
|
25
|
-
export const escalateTool = {
|
|
26
|
-
name: "escalate",
|
|
27
|
-
sideEffectExempt: true, // the child's mutations are tracked and reviewed, like subagent
|
|
28
|
-
description:
|
|
29
|
-
"TERMINOLOGY (one word for one thing): 'escalate' is the ONLY name — the tool, and the " +
|
|
30
|
-
"role of the expert sub-agent it spawns, are both called 'escalate'; 飞刀 is the Chinese " +
|
|
31
|
-
"alias. When the user says 飞刀 / escalate / 'fly in <model>', call THIS tool directly — " +
|
|
32
|
-
"never via a script importing this module. " +
|
|
33
|
-
"Hand an implementation task to a stronger model (飞刀 — a flown-in expert). " +
|
|
34
|
-
"It gets WRITE access and does the work itself — reads, edits, runs tests — then returns " +
|
|
35
|
-
"a post-op report (what changed, why, verification). You review the report and report to " +
|
|
36
|
-
"the user. Use it when YOU judge the task calls for stronger hands (complex multi-file " +
|
|
37
|
-
"refactoring, an intractable bug, intricate algorithm work — or work beyond your " +
|
|
38
|
-
"comfortable ability). Early or late, your judgment; the cost is one expert run, " +
|
|
39
|
-
"comparable to doing it yourself. For parallel READ-ONLY opinions use consult_start instead. " +
|
|
40
|
-
"Not available in engineering mode (implementation goes through eng-coder subagents there).\n" +
|
|
41
|
-
"Parameters:\n" +
|
|
42
|
-
"- task (required): the task description — goal, constraints, entry files, acceptance criteria\n" +
|
|
43
|
-
"- model (optional): pick a specific consultant as 'provider:model'; default = the first consult model",
|
|
44
|
-
parameters: {
|
|
45
|
-
type: "object",
|
|
46
|
-
properties: {
|
|
47
|
-
task: { type: "string", description: "Task description with acceptance criteria" },
|
|
48
|
-
model: { type: "string", description: "Candidate 'provider:model' from the consult models (optional)" },
|
|
49
|
-
},
|
|
50
|
-
required: ["task"],
|
|
51
|
-
},
|
|
52
|
-
async execute({ task, model }, ctx) {
|
|
53
|
-
const parent = ctx.agent
|
|
54
|
-
if ((ctx.depth ?? 0) > 0) return "Error: escalate is only available at depth 0 (an escalate's work cannot be delegated again)"
|
|
55
|
-
if (parent?.config?.agent?.engineering) {
|
|
56
|
-
return "Error: engineering mode is ON — escalate is unavailable (it spawns a coder sub-agent, which engineering mode forbids). Use subagent with role='eng-coder' and a designToken from advisor(type='design') instead."
|
|
57
|
-
}
|
|
58
|
-
const pool = parent?.config?.agent?.consultModels ?? []
|
|
59
|
-
if (pool.length === 0) return "Error: no escalate candidates — configure at least one consult model (agent.consultModels)"
|
|
60
|
-
|
|
61
|
-
const wanted = typeof model === "string" ? model.replace(/\s+\([^)]*\)\s*$/, "").trim() : model
|
|
62
|
-
const pick = wanted ? pool.find((m) => label(m) === wanted) : pool[0]
|
|
63
|
-
if (!pick) {
|
|
64
|
-
return `Error: "${model}" is not a consult candidate. Available: ${pool.map(label).join(", ")}`
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
let provider
|
|
68
|
-
try {
|
|
69
|
-
provider = resolveChildProvider(parent, `${pick.provider}:${pick.model}`)
|
|
70
|
-
} catch (e) {
|
|
71
|
-
return `Error: ${e.message}`
|
|
72
|
-
}
|
|
73
|
-
if (!ensureChildApiKey(provider)) {
|
|
74
|
-
return `Error: provider "${pick.provider}" has no API key — set it in config.json before flying it in`
|
|
75
|
-
}
|
|
76
|
-
let effortNote = ""
|
|
77
|
-
if (pick.effort && !clampEffort(provider, pick.model, pick.effort)) {
|
|
78
|
-
// Out-of-enum effort dropped (see clampEffort): the provider preset default may ALSO be
|
|
79
|
-
// out-of-enum for this override model — e.g. qwenplan preset default "high" is
|
|
80
|
-
// invalid for qwen3.8-max, enum xhigh/medium/low.
|
|
81
|
-
effortNote = ` (effort "${pick.effort}" unsupported by ${pick.model}, dropped)`
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const tag = label(pick)
|
|
85
|
-
const relayPrefix = makeRelay(parent, "escalate", ctx.callbacks?.onToken, provider.model ?? tag)
|
|
86
|
-
// Report the escalated model to the display layer (it may differ from the parent's)
|
|
87
|
-
// — makeRelay already emitted the `[model]` metadata token above.
|
|
88
|
-
|
|
89
|
-
// No wall-clock watchdog — turn cap only, exactly like subagent (the verified write
|
|
90
|
-
// path). Rationale (2026-08-16): a fixed wall-clock aborts NORMAL-but-slow surgery —
|
|
91
|
-
// two max-effort consultants hit a 10min wall just READING files. Hang protection is
|
|
92
|
-
// already covered by FETCH_TIMEOUT_MS (per LLM call) and the user's Stop (parent
|
|
93
|
-
// signal propagates directly below). maxTurns is the cost budget; hitting it asks
|
|
94
|
-
// the user whether to continue (main-agent parity), falling back to partial work.
|
|
95
|
-
|
|
96
|
-
// No custom onToken here (consult P2, 2026-08-30): wrapChildCallbacks already
|
|
97
|
-
// applies the prefixed relay + D7 sentinel strip for the display path, and
|
|
98
|
-
// runWithContinue owns the capture (stripEventTokensForCapture) for the
|
|
99
|
-
// partial-output return — a hand-rolled duplicate ran the strip twice and
|
|
100
|
-
// maintained a second output buffer.
|
|
101
|
-
const childCallbacks = wrapChildCallbacks(relayPrefix, ctx.callbacks ?? {})
|
|
102
|
-
|
|
103
|
-
// Declared outside try so the catch can merge mutations even on a partial failure.
|
|
104
|
-
let child = null
|
|
105
|
-
try {
|
|
106
|
-
// Full write path (role "coder"): permission gate via the parent's onPermissionRequest,
|
|
107
|
-
// recent-changes tracking, mutations merge into the parent below.
|
|
108
|
-
child = createAgent({
|
|
109
|
-
provider,
|
|
110
|
-
tools: parent.tools,
|
|
111
|
-
config: parent.config,
|
|
112
|
-
cwd: parent.cwd,
|
|
113
|
-
memory: parent.memory,
|
|
114
|
-
overlay: CODER_OVERLAY,
|
|
115
|
-
role: "coder",
|
|
116
|
-
})
|
|
117
|
-
const runner = ctx.runAgent ?? runAgent
|
|
118
|
-
const runOpts = {
|
|
119
|
-
depth: 1,
|
|
120
|
-
maxTurns: parent.config?.agent?.subagentTurns ?? DEFAULT_SUBAGENT_TURNS, // review #7: constant, not literal (single source with subagent.mjs)
|
|
121
|
-
signal: ctx.signal ?? null,
|
|
122
|
-
}
|
|
123
|
-
// Turn-cap continue via runWithContinue (§7.2 D3), main-agent parity (tui/agent-turn.mjs):
|
|
124
|
-
// when the child hits ContinueError, ask the user through the SAME channel as child
|
|
125
|
-
// write approval (ctx.onPermissionRequest). The name "continue" renders the TUI's
|
|
126
|
-
// dedicated y/n Continue panel. The resumed run passes resume:true, so runAgent does
|
|
127
|
-
// NOT re-inject the task text (setup.mjs skips input on resume) and keeps the child's
|
|
128
|
-
// history + mutation bookkeeping, with a fresh maxTurns budget per run. No permission
|
|
129
|
-
// handler (headless) or a declined prompt falls through to the partial-work return.
|
|
130
|
-
// Continues are UNLIMITED — the user can decline at any prompt.
|
|
131
|
-
const report = await runWithContinue(
|
|
132
|
-
async (childAgent, input, cbs, opts) => {
|
|
133
|
-
// Merge mid-run mutations even when the run throws — the outer catch keeps
|
|
134
|
-
// handling createAgent failures; AbortError still propagates (user Stop).
|
|
135
|
-
try {
|
|
136
|
-
return await runner(childAgent, input, cbs, opts)
|
|
137
|
-
} catch (e) {
|
|
138
|
-
mergeChildMutations(parent, childAgent)
|
|
139
|
-
throw e
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
child, task, { ...childCallbacks, onPermissionRequest: parent.autoApprove ? async () => true : (ctx.onPermissionRequest ?? null) },
|
|
143
|
-
runOpts,
|
|
144
|
-
{
|
|
145
|
-
// escalate has NO permQueue: prompts go straight to the user (T-L spec).
|
|
146
|
-
askContinue: (e) => (ctx.onPermissionRequest
|
|
147
|
-
? ctx.onPermissionRequest("continue", { turns: e.turn, agent: tag })
|
|
148
|
-
: Promise.resolve(false)),
|
|
149
|
-
onDeclined: (e, output) => `escalate (${tag}) ${TURN_CAP_MARK} (${e.turn} turns) — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${output.slice(0, 2000)}`,
|
|
150
|
-
},
|
|
151
|
-
).catch((e) => {
|
|
152
|
-
// Generic run failure (not ContinueError): match the original loop's return shape —
|
|
153
|
-
// error text + partial output, mutations already merged in the runner wrapper.
|
|
154
|
-
if (ctx.signal?.aborted || e?.name === "AbortError") throw e
|
|
155
|
-
return `escalate (${tag}) error: ${e?.message ?? String(e)}\nPartial output: ${(child._capturedOutput ?? "").slice(0, 2000)}`
|
|
156
|
-
})
|
|
157
|
-
// Escalate mutations are the parent's mutations: verify/advisor guards must see them
|
|
158
|
-
mergeChildMutations(parent, child)
|
|
159
|
-
return `escalate (${tag})${effortNote} post-op report:\n${report || (child._capturedOutput ?? "").slice(0, 4000)}${touchedFilesNote(child, parent.cwd)}`
|
|
160
|
-
} catch (e) {
|
|
161
|
-
// Reached only when createAgent itself fails or the continue prompt throws —
|
|
162
|
-
// run failures are handled above (mutations merge inside the runner wrapper).
|
|
163
|
-
if (child) mergeChildMutations(parent, child)
|
|
164
|
-
if (ctx.signal?.aborted || e?.name === "AbortError") throw e
|
|
165
|
-
return `escalate (${tag}) error: ${e?.message ?? String(e)}`
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Relative touched-file list appended to every return (child paths are absolute). */
|
|
171
|
-
function touchedFilesNote(child, cwd) {
|
|
172
|
-
const touched = child?._touchedFiles ?? []
|
|
173
|
-
if (touched.length === 0) return ""
|
|
174
|
-
const shown = touched.map((f) => {
|
|
175
|
-
const r = relative(cwd ?? process.cwd(), f)
|
|
176
|
-
return r && !r.startsWith("..") && !isAbsolute(r) ? r : f
|
|
177
|
-
})
|
|
178
|
-
return `\nTouched files: ${shown.join(", ")}`
|
|
179
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* exec-prelude.mjs — sandbox API injected into `execute`'s child process.
|
|
3
|
-
*
|
|
4
|
-
* The `execute` tool spawns `node --input-type=module --eval` and `import()`-s
|
|
5
|
-
* this file first, so the user's code gets readFile/writeFile/glob/grep/log/require
|
|
6
|
-
* without hand-writing fs boilerplate. Confines file paths to the workspace root
|
|
7
|
-
* (THINCODER_EXEC_ROOT, default cwd) — an orthopedic guard, NOT a sandbox:
|
|
8
|
-
* require()/process/import()/fetch() are full Node, the same boundary as bash
|
|
9
|
-
* (project philosophy: no fake sandbox; transparency + audit).
|
|
10
|
-
*/
|
|
11
|
-
import { createRequire } from "node:module"
|
|
12
|
-
import { readFileSync, writeFileSync, existsSync, statSync, mkdirSync, readdirSync } from "node:fs"
|
|
13
|
-
import { resolve, relative, dirname, join, isAbsolute, sep } from "node:path"
|
|
14
|
-
|
|
15
|
-
const require = createRequire(join(process.cwd(), "__exec__.js"))
|
|
16
|
-
const root = process.env.THINCODER_EXEC_ROOT || process.cwd()
|
|
17
|
-
|
|
18
|
-
/** Resolve a path against the working dir, asserting it stays within the workspace root. */
|
|
19
|
-
function safe(p) {
|
|
20
|
-
if (typeof p !== "string") throw new Error(`Path must be a string, got ${typeof p}`)
|
|
21
|
-
const abs = resolve(process.cwd(), p)
|
|
22
|
-
const rel = relative(root, abs)
|
|
23
|
-
// isAbsolute(rel) covers cross-drive (relative() returns an absolute path then)
|
|
24
|
-
if (isAbsolute(rel) || rel === ".." || rel.startsWith(".." + sep)) {
|
|
25
|
-
throw new Error(`Path traversal denied: ${p}`)
|
|
26
|
-
}
|
|
27
|
-
return abs
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function globToRegex(pattern) {
|
|
31
|
-
const DS = "\u0001", DP = "\u0002"
|
|
32
|
-
const escaped = pattern
|
|
33
|
-
.replace(/\*\*\//g, DS).replace(/\*\*/g, DP)
|
|
34
|
-
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
35
|
-
.replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")
|
|
36
|
-
.replace(new RegExp(DS, "g"), "(?:.+/)?").replace(new RegExp(DP, "g"), ".*")
|
|
37
|
-
return new RegExp(`^${escaped}$`)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
globalThis.require = require
|
|
41
|
-
globalThis.readFile = (p) => {
|
|
42
|
-
const abs = safe(p)
|
|
43
|
-
if (!existsSync(abs)) throw new Error(`File not found: ${p}`)
|
|
44
|
-
if (statSync(abs).size > 5_000_000) throw new Error(`File too large: ${p}`)
|
|
45
|
-
return readFileSync(abs, "utf8").replace(/\r\n/g, "\n")
|
|
46
|
-
}
|
|
47
|
-
globalThis.writeFile = (p, content) => {
|
|
48
|
-
const abs = safe(p)
|
|
49
|
-
mkdirSync(dirname(abs), { recursive: true })
|
|
50
|
-
writeFileSync(abs, String(content), "utf8")
|
|
51
|
-
}
|
|
52
|
-
globalThis.glob = (pattern) => {
|
|
53
|
-
if (typeof pattern !== "string") throw new Error("glob pattern must be a string")
|
|
54
|
-
const re = globToRegex(pattern)
|
|
55
|
-
const out = []
|
|
56
|
-
function walk(dir, rel) {
|
|
57
|
-
let entries
|
|
58
|
-
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
59
|
-
for (const e of entries) {
|
|
60
|
-
if (e.name.startsWith(".") || e.name === "node_modules") continue
|
|
61
|
-
const rp = rel ? `${rel}/${e.name}` : e.name
|
|
62
|
-
if (e.isDirectory()) walk(join(dir, e.name), rp)
|
|
63
|
-
else if (re.test(rp)) out.push(rp)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
walk(process.cwd(), "")
|
|
67
|
-
const capped = out.slice(0, 200)
|
|
68
|
-
if (out.length > 200) capped.push(`... (${out.length - 200} more)`)
|
|
69
|
-
return capped
|
|
70
|
-
}
|
|
71
|
-
globalThis.grep = (pattern, file) => {
|
|
72
|
-
if (typeof pattern !== "string") throw new Error("grep pattern must be a string")
|
|
73
|
-
if (typeof file !== "string") throw new Error("grep file must be a string")
|
|
74
|
-
const abs = safe(file)
|
|
75
|
-
if (!existsSync(abs)) throw new Error(`File not found: ${file}`)
|
|
76
|
-
const re = new RegExp(pattern)
|
|
77
|
-
const lines = readFileSync(abs, "utf8").replace(/\r\n/g, "\n").split("\n")
|
|
78
|
-
const m = []
|
|
79
|
-
for (let i = 0; i < lines.length; i++) if (re.test(lines[i])) m.push(`${i + 1}: ${lines[i].slice(0, 200)}`)
|
|
80
|
-
const capped = m.slice(0, 100)
|
|
81
|
-
if (m.length > 100) capped.push(`... (${m.length - 100} more)`)
|
|
82
|
-
return capped
|
|
83
|
-
}
|
|
84
|
-
globalThis.log = (...a) => console.log(a.map((x) => (x && typeof x === "object" ? JSON.stringify(x) : String(x))).join(" "))
|