thincoder 0.11.0 → 0.11.1

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.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/src/advisor.mjs +360 -72
  3. package/src/agent/helpers.mjs +7 -3
  4. package/src/agent/setup.mjs +2 -2
  5. package/src/agent-tools/advisor.mjs +36 -0
  6. package/src/agent-tools/plan.mjs +53 -2
  7. package/src/agent-tools/subagent.mjs +7 -1
  8. package/src/agent-tools/timer.mjs +1 -1
  9. package/src/agent-tools/verify.mjs +1 -0
  10. package/src/agent-tools.mjs +1 -0
  11. package/src/agent.mjs +73 -21
  12. package/src/auto-think.mjs +23 -5
  13. package/src/config.mjs +1 -1
  14. package/src/prompts/advisor-round1.md +23 -0
  15. package/src/prompts/advisor-round2.md +26 -0
  16. package/src/prompts/advisor-round3.md +24 -0
  17. package/src/prompts/coder.md +1 -0
  18. package/src/prompts/discipline.md +15 -1
  19. package/src/prompts/explore.md +2 -0
  20. package/src/prompts/plan.md +2 -0
  21. package/src/prompts/system.md +5 -1
  22. package/src/provider/anthropic.mjs +4 -4
  23. package/src/provider/core.mjs +6 -126
  24. package/src/provider/google.mjs +4 -2
  25. package/src/provider/sse.mjs +112 -0
  26. package/src/tools/bash.md +8 -0
  27. package/src/tools/codemode.mjs +5 -16
  28. package/src/tools/edit.md +8 -0
  29. package/src/tools/git.mjs +9 -6
  30. package/src/tools/read.md +7 -0
  31. package/src/tools/shared.mjs +16 -0
  32. package/src/tools/system.mjs +14 -10
  33. package/src/tools/web.mjs +21 -16
  34. package/src/tui/agent-turn.mjs +76 -64
  35. package/src/tui/cmd-advisor.mjs +119 -18
  36. package/src/tui/index.mjs +7 -187
  37. package/src/tui/key-handler.mjs +8 -2
  38. package/src/tui/layout.mjs +1 -1
  39. package/src/tui/render-conversation.mjs +92 -0
  40. package/src/tui/render-frame.mjs +27 -103
  41. package/src/tui/render-loop.mjs +181 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
package/src/advisor.mjs CHANGED
@@ -1,105 +1,393 @@
1
1
  /**
2
- * advisor.mjs — automated code review after each turn.
2
+ * advisor.mjs — Code review engine.
3
+ * Called by the advisor tool (agent-tools/advisor.mjs) when the agent
4
+ * explicitly requests a review at the end of a coding task.
3
5
  *
4
- * After every agent turn with tool execution, a second chat completion reviews
5
- * the turn's transcript and injects observations as a system reminder.
6
+ * The advisor runs as a read-only exploration sub-agent with tools
7
+ * (read, glob, grep, ls, git, lsp, code_search). It discovers changes
8
+ * via git diff, reads files for context, and traces callers via grep/lsp.
6
9
  *
7
10
  * Config:
8
11
  * { advisor: { enabled: true, provider: "deepseek", model: "deepseek-chat" } }
9
12
  * provider + model are optional — defaults to the main agent's provider/model.
13
+ *
14
+ * Convergence protocol:
15
+ * Round 1: full review → produces a numbered issue table.
16
+ * Agent responds with a response table per issue.
17
+ * Round 2: semi-convergence — verifies table + can flag obvious new issues.
18
+ * Round 3+: strict convergence — only checks the prior issue table.
19
+ * No hard round cap — the convergence protocol naturally limits divergence.
20
+ *
21
+ *
22
+ * Project customisation: .thincoder/advisor.md in the project root.
10
23
  */
11
24
  import { chat } from "./provider/core.mjs"
12
25
  import { findProvider } from "./config.mjs"
26
+ import { existsSync, readFileSync } from "node:fs"
27
+ import { join, dirname } from "node:path"
28
+ import { fileURLToPath } from "node:url"
29
+ import { execFileSync } from "node:child_process"
30
+ import { toOpenAISchema } from "./tools/index.mjs"
13
31
 
14
- const ADVISOR_PROMPT = `你是代码审查顾问。审查编程助手最近一轮操作并提供简短的观察。
32
+ const __dirname = dirname(fileURLToPath(import.meta.url))
15
33
 
16
- 规则:
17
- - 用中文回答,2-4 行,分条目
18
- - 关注:遗漏的边界情况、错误的假设、低效的模式、安全隐患
19
- - 如果一切正常,说"未发现问题"
20
- - 不要建议或调用工具 只做只读审查
21
- - 不要直接对用户说话 — 用第三人称评价助手的行为`
34
+ const ADVISOR_MD_PATH = ".thincoder/advisor.md"
35
+ const MAX_TASK_SUMMARY = 500
36
+ const GIT_TIMEOUT = 5_000
37
+
38
+ const DEFAULT_CRITERIA = `Review the code changes, focusing on:
39
+ 1. Correctness: logic errors, edge cases, off-by-one, incomplete modifications
40
+ 2. Security: unhandled exceptions, null references, resource leaks, race conditions
41
+ 3. Consistency: alignment with existing project patterns and conventions
42
+ 4. Completeness: missing callers, imports, or follow-up changes
43
+ 5. Maintainability: vague naming, missing comments, overly complex logic`
44
+
45
+ // ────────────────────────────────────────
46
+ // Advisor's read-only tool set
47
+ // ────────────────────────────────────────
22
48
 
23
49
  /**
24
- * Run advisor review on the most recent turn.
25
- * Only fires when the agent executed tool calls (did real work).
26
- * Returns the advisor's message, or null if advisor is disabled or there's nothing to review.
50
+ * Restricted git tool: diff / status / log only.
51
+ * Checkpoint create/rewind are blocked the advisor must not mutate state.
27
52
  */
28
- export async function runAdvisor(agent) {
29
- const cfg = agent.config?.advisor
30
- if (!cfg?.enabled) return null
53
+ const { gitTool, readTool, globTool, grepTool, lsTool } = await import("./tools/index.mjs")
54
+ const { lspTool } = await import("./tools/lsp.mjs")
55
+ const { codeModeTool: codeSearchTool } = await import("./tools/codemode.mjs")
56
+
57
+ const advisorGitTool = {
58
+ ...gitTool,
59
+ readonly: true,
60
+ async execute(args, ctx) {
61
+ // Block checkpoint create/rewind — advisor is read-only
62
+ if (args.action === "checkpoint") {
63
+ if (args.checkpointAction === "create" || args.checkpointAction === "rewind") {
64
+ return "Error: checkpoint create/rewind is disabled in advisor mode. Use diff/status/log only."
65
+ }
66
+ }
67
+ return gitTool.execute(args, ctx)
68
+ },
69
+ }
70
+
71
+ const ADVISOR_TOOLS = [readTool, globTool, grepTool, lsTool, advisorGitTool, lspTool, codeSearchTool]
72
+ const ADVISOR_TOOL_SCHEMAS = ADVISOR_TOOLS.map(toOpenAISchema)
73
+ const ADVISOR_TOOL_BY_NAME = new Map(ADVISOR_TOOLS.map((t) => [t.name, t]))
31
74
 
32
- // Only review turns where the agent did real work (tool calls executed)
33
- const lastAssistant = findLast(agent.history, (m) => m.role === "assistant")
34
- const lastTool = findLast(agent.history, (m) => m.role === "tool")
35
- if (!lastAssistant || !lastTool) return null
36
-
37
- // Build a clean transcript: last user message + assistant + tool results
38
- const lastUser = findLast(agent.history, (m) => m.role === "user")
39
- const transcript = buildTranscript(agent.history, lastUser)
40
-
41
- // Resolve provider: explicit provider name → look up from providers list;
42
- // only model → reuse main provider with different model; neither → main provider as-is.
43
- let provider
44
- if (cfg.provider) {
45
- provider = findProvider(agent.providers ?? [agent.provider], cfg.provider)
46
- if (cfg.model) provider = { ...provider, model: cfg.model }
47
- } else {
48
- provider = { ...agent.provider }
49
- if (cfg.model) provider.model = cfg.model
75
+ // ────────────────────────────────────────
76
+ // Prompt files loaded at module init
77
+ // ────────────────────────────────────────
78
+
79
+ const ADVISOR_ROUND1 = readFileSync(join(__dirname, "prompts", "advisor-round1.md"), "utf8")
80
+ const ADVISOR_ROUND2 = readFileSync(join(__dirname, "prompts", "advisor-round2.md"), "utf8")
81
+ const ADVISOR_ROUND3 = readFileSync(join(__dirname, "prompts", "advisor-round3.md"), "utf8")
82
+
83
+ // ────────────────────────────────────────
84
+ // History extraction issue/response tables
85
+ // ────────────────────────────────────────
86
+
87
+ const ADVISOR_TABLE_HEADER = "| # | File | Severity | Issue | Suggestion |"
88
+ const CONVERGENCE_TABLE_HEADER = "| # | Orig# | File | Severity | Status | Notes |"
89
+ const AGENT_RESPONSE_HEADER = "| # | Action | Detail |"
90
+ const LEGACY_ADVISOR_HEADER = "| # | 文件 | 严重程度 | 问题描述 | 建议修复 |"
91
+ const LEGACY_CONVERGENCE_HEADER = "| # | 原# | 文件 | 严重程度 | 当前状态 | 说明 |"
92
+ const LEGACY_RESPONSE_HEADER = "| # | 处理 | 详情 |"
93
+ const ALL_CLEAR_PHRASES = [
94
+ "No issues found",
95
+ "All issues resolved",
96
+ "review passed",
97
+ "未发现问题",
98
+ "所有问题已解决",
99
+ "审查通过",
100
+ ]
101
+
102
+ export function extractPriorIssueTable(history) {
103
+ for (let i = history.length - 1; i >= 0; i--) {
104
+ const m = history[i]
105
+ if (m.role !== "tool") continue
106
+ const content = typeof m.content === "string" ? m.content : ""
107
+ if (ALL_CLEAR_PHRASES.some((p) => content.includes(p))) return null
108
+ if (content.includes(ADVISOR_TABLE_HEADER) || content.includes(CONVERGENCE_TABLE_HEADER) ||
109
+ content.includes(LEGACY_ADVISOR_HEADER) || content.includes(LEGACY_CONVERGENCE_HEADER)) {
110
+ const table = extractTableBlock(content)
111
+ if (table) return { text: table, sinceIdx: i }
112
+ return null
113
+ }
50
114
  }
115
+ return null
116
+ }
51
117
 
118
+ export function extractAgentResponseTable(history, sinceIdx) {
119
+ for (let i = sinceIdx + 1; i < history.length; i++) {
120
+ const m = history[i]
121
+ if (m.role !== "assistant") continue
122
+ const content = typeof m.content === "string" ? m.content : ""
123
+ if (content.includes(AGENT_RESPONSE_HEADER) || content.includes(LEGACY_RESPONSE_HEADER)) {
124
+ return extractTableBlock(content)
125
+ }
126
+ }
127
+ return null
128
+ }
129
+
130
+ function extractTableBlock(text) {
131
+ const lines = text.split("\n")
132
+ let start = -1
133
+ for (let i = 0; i < lines.length; i++) {
134
+ if (lines[i].startsWith("|")) { start = i; break }
135
+ }
136
+ if (start < 0) return null
137
+ let end = start
138
+ for (let i = start + 1; i < lines.length; i++) {
139
+ if (lines[i].startsWith("|")) end = i
140
+ else break
141
+ }
142
+ return lines.slice(start, end + 1).join("\n")
143
+ }
144
+
145
+ // ────────────────────────────────────────
146
+ // Review scope — repos to review
147
+ // ────────────────────────────────────────
148
+
149
+ /**
150
+ * Find the git repository roots that contain the agent's touched files.
151
+ * Falls back to cwd if no repos found.
152
+ */
153
+ function findReviewRepos(agent) {
154
+ const touched = agent._touchedFiles ?? []
155
+ const repos = []
156
+
157
+ for (const abs of touched) {
158
+ try {
159
+ const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
160
+ cwd: dirname(abs), encoding: "utf8", timeout: GIT_TIMEOUT,
161
+ stdio: ["ignore", "pipe", "pipe"],
162
+ }).trim()
163
+ if (root && !repos.includes(root)) repos.push(root)
164
+ } catch { /* not a git repo */ }
165
+ }
166
+
167
+ if (repos.length > 0) return repos
168
+
169
+ // Fallback: cwd itself
52
170
  try {
53
- const response = await chat(provider, {
54
- messages: [
55
- { role: "system", content: ADVISOR_PROMPT },
56
- { role: "user", content: transcript },
57
- ],
58
- tools: [],
59
- signal: new AbortController().signal,
60
- })
61
- if (!response.content?.trim()) return null
171
+ const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
172
+ cwd: agent.cwd, encoding: "utf8", timeout: GIT_TIMEOUT,
173
+ stdio: ["ignore", "pipe", "pipe"],
174
+ }).trim()
175
+ if (root) return [root]
176
+ } catch { /* not a git repo */ }
62
177
 
63
- return `[Advisor 审查 — 自动观察,非用户指令。请批判性参考:\n${response.content.trim()}]`
178
+ return []
179
+ }
180
+
181
+ // ────────────────────────────────────────
182
+
183
+ export function loadAdvisorMd(cwd) {
184
+ const path = join(cwd, ADVISOR_MD_PATH)
185
+ if (!existsSync(path)) return DEFAULT_CRITERIA
186
+ try {
187
+ const content = readFileSync(path, "utf8").trim()
188
+ return content || DEFAULT_CRITERIA
64
189
  } catch {
65
- // Advisor failure is non-fatal — main loop continues
66
- return null
190
+ return DEFAULT_CRITERIA
67
191
  }
68
192
  }
69
193
 
70
- /** Find the last history entry matching a predicate. */
71
- function findLast(arr, fn) {
72
- for (let i = arr.length - 1; i >= 0; i--) {
73
- if (fn(arr[i])) return i
194
+ function extractTaskSummary(history) {
195
+ for (let i = history.length - 1; i >= 0; i--) {
196
+ const m = history[i]
197
+ if (m.role !== "user") continue
198
+ const content = typeof m.content === "string" ? m.content : ""
199
+ if (content.startsWith("[System reminder:") || content.startsWith("[User interrupt:")) continue
200
+ const firstPara = content.split("\n\n")[0]
201
+ return firstPara.length > MAX_TASK_SUMMARY ? firstPara.slice(0, MAX_TASK_SUMMARY) + "…" : firstPara
74
202
  }
75
- return -1
203
+ return null
76
204
  }
77
205
 
78
- /** Build a compact transcript of the last turn for the advisor. */
79
- function buildTranscript(history, lastUserIdx) {
80
- const lines = []
81
- lines.push("## Last user message")
82
- lines.push(truncate(String(history[lastUserIdx]?.content ?? "")))
83
- lines.push("")
84
- lines.push("## Assistant response and tool calls")
206
+ // ────────────────────────────────────────
207
+ // System prompt routing
208
+ // ────────────────────────────────────────
85
209
 
86
- for (let i = lastUserIdx + 1; i < history.length; i++) {
87
- const m = history[i]
88
- if (m.role === "assistant") {
89
- const content = typeof m.content === "string" ? m.content : ""
90
- const tools = m.tool_calls?.map((tc) => tc.function?.name).filter(Boolean) ?? []
91
- const label = tools.length ? ` (called: ${tools.join(", ")})` : ""
92
- lines.push(`[assistant${label}] ${truncate(content, 500)}`)
93
- } else if (m.role === "tool") {
94
- lines.push(`[tool ${m.tool_call_id}] ${truncate(String(m.content ?? ""), 300)}`)
210
+ export function buildAdvisorSystemPrompt(agent, _prior) {
211
+ const prior = _prior ?? extractPriorIssueTable(agent.history)
212
+ if (!prior || (agent._advisorRound || 0) === 0) return ADVISOR_ROUND1
213
+ const round = (agent._advisorRound || 0) + 1
214
+ if (round === 2) return ADVISOR_ROUND2
215
+ return ADVISOR_ROUND3
216
+ }
217
+
218
+ // ────────────────────────────────────────
219
+ // User message building
220
+ // ────────────────────────────────────────
221
+
222
+ export function buildAdvisorUserMessage(agent, _prior) {
223
+ const prior = _prior ?? extractPriorIssueTable(agent.history)
224
+
225
+ // Repos to review
226
+ const repos = findReviewRepos(agent)
227
+ const repoList = repos.length > 0
228
+ ? repos.map((r, i) => `${i + 1}. ${r}`).join("\n")
229
+ : `(no git repository — working directory: ${agent.cwd})`
230
+
231
+ const parts = []
232
+
233
+ // Convergence data (round 2+)
234
+ if (prior && (agent._advisorRound || 0) > 0) {
235
+ const response = extractAgentResponseTable(agent.history, prior.sinceIdx)
236
+ || "(Agent did not provide a response table — re-evaluate each issue)"
237
+ const round = (agent._advisorRound || 0) + 1
238
+ const label = round === 2 ? "Verify Prior Table + Flag New Issues" : "Strict Verification"
239
+ parts.push(`## Round ${round} — ${label}`)
240
+ parts.push("")
241
+ parts.push("## Prior Issue Table")
242
+ parts.push(prior.text)
243
+ parts.push("")
244
+ parts.push("## Agent Response")
245
+ parts.push(response)
246
+ parts.push("")
247
+ parts.push("---")
248
+ parts.push("")
249
+ }
250
+
251
+ // Review scope
252
+ parts.push("## Review Scope")
253
+ parts.push(`Review the following git repositor${repos.length === 1 ? "y" : "ies"}:`)
254
+ parts.push(repoList)
255
+ parts.push("")
256
+
257
+ // Task summary
258
+ const taskSummary = extractTaskSummary(agent.history)
259
+ if (taskSummary) {
260
+ parts.push("## Task")
261
+ parts.push(taskSummary)
262
+ parts.push("")
263
+ }
264
+
265
+ // Review criteria
266
+ const criteria = loadAdvisorMd(agent.cwd)
267
+ parts.push("## Review Criteria")
268
+ parts.push(criteria)
269
+ parts.push("")
270
+
271
+ // Instructions
272
+ parts.push("## Instructions")
273
+ parts.push("1. Read `AGENTS.md` and any design documents first — understand project conventions, version requirements, and architecture decisions before flagging issues.")
274
+ parts.push("2. Run `git diff HEAD` in each repo to discover uncommitted changes.")
275
+ parts.push("3. `read` changed files for full context beyond the diff.")
276
+ parts.push("4. Use `grep` or `lsp` to trace callers, imports, and dependencies.")
277
+ parts.push("5. Produce your review table based on the review criteria above.")
278
+ parts.push("Do NOT flag features that are valid under the project's stated platform requirements.")
279
+
280
+ return parts.join("\n")
281
+ }
282
+
283
+ // ────────────────────────────────────────
284
+ // Advisor tool loop
285
+ // ────────────────────────────────────────
286
+
287
+ /**
288
+ * Run the advisor's tool loop: chat → execute tools → repeat.
289
+ * Stops when the model produces text without tool calls.
290
+ */
291
+ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, cwd) {
292
+ while (true) {
293
+ const response = await chat(provider, {
294
+ messages,
295
+ tools: ADVISOR_TOOL_SCHEMAS,
296
+ signal: (signal && !signal.aborted) ? signal : new AbortController().signal,
297
+ onToken: onOutput,
298
+ onReasoning: onOutput,
299
+ })
300
+
301
+ // No tool calls — this is the final review text
302
+ if (!response.toolCalls?.length) {
303
+ if (!response.content?.trim()) return "Advisor: (empty response — review was inconclusive)"
304
+ return response.content.trim()
305
+ }
306
+
307
+ // Push assistant message with tool calls
308
+ messages.push({
309
+ role: "assistant",
310
+ content: response.content || null,
311
+ tool_calls: response.toolCalls.map((tc) => ({
312
+ id: tc.id, type: "function",
313
+ function: { name: tc.name, arguments: tc.arguments },
314
+ })),
315
+ })
316
+
317
+ // Execute each tool call
318
+ for (const tc of response.toolCalls) {
319
+ const tool = ADVISOR_TOOL_BY_NAME.get(tc.name)
320
+ let result
321
+ if (!tool) {
322
+ result = `Error: unknown tool "${tc.name}". Available: ${[...ADVISOR_TOOL_BY_NAME.keys()].join(", ")}`
323
+ } else {
324
+ try {
325
+ const args = JSON.parse(tc.arguments || "{}")
326
+ result = await tool.execute(args, {
327
+ cwd,
328
+ agent,
329
+ onOutput,
330
+ signal,
331
+ })
332
+ } catch (e) {
333
+ result = `Error: ${e.message}`
334
+ }
335
+ }
336
+ messages.push({ role: "tool", tool_call_id: tc.id, content: String(result) })
95
337
  }
96
338
  }
339
+ }
340
+
341
+ // ────────────────────────────────────────
342
+ // Main entry point
343
+ // ────────────────────────────────────────
97
344
 
98
- return lines.join("\n")
345
+ export function resolveAdvisorProvider(agent) {
346
+ const cfg = agent.config?.advisor
347
+ if (cfg?.provider) {
348
+ try {
349
+ const provider = findProvider(agent.providers ?? [agent.provider], cfg.provider)
350
+ const result = cfg.model ? { ...provider, model: cfg.model } : { ...provider }
351
+ if (cfg.thinking === null) result.thinking = undefined // explicitly off
352
+ else if (cfg.thinking !== undefined) result.thinking = cfg.thinking
353
+ if (cfg.reasoningEffort !== undefined) result.reasoningEffort = cfg.reasoningEffort
354
+ return result
355
+ } catch {
356
+ // Provider not found — fall back to main provider
357
+ }
358
+ }
359
+ const provider = { ...agent.provider }
360
+ if (cfg?.model) provider.model = cfg.model
361
+ if (cfg?.thinking === null) provider.thinking = undefined // explicitly off
362
+ else if (cfg?.thinking !== undefined) provider.thinking = cfg.thinking
363
+ if (cfg?.reasoningEffort !== undefined) provider.reasoningEffort = cfg.reasoningEffort
364
+ return provider
99
365
  }
100
366
 
101
- /** Truncate text to maxLen characters, adding "…" if truncated. */
102
- function truncate(text, maxLen = 1000) {
103
- if (text.length <= maxLen) return text
104
- return text.slice(0, maxLen) + "…"
367
+ export async function runAdvisorReview(agent, onOutput, signal) {
368
+ const cfg = agent.config?.advisor
369
+ if (!cfg?.enabled) return null
370
+
371
+ const repos = findReviewRepos(agent)
372
+ if ((agent._touchedFiles ?? []).length === 0) return null
373
+
374
+ const provider = resolveAdvisorProvider(agent)
375
+ const priorIssue = extractPriorIssueTable(agent.history)
376
+ const systemPrompt = buildAdvisorSystemPrompt(agent, priorIssue)
377
+ const userMessage = buildAdvisorUserMessage(agent, priorIssue)
378
+
379
+ // Set the advisor's cwd to the first repo (for tool context)
380
+ const advisorCwd = repos.length > 0 ? repos[0] : agent.cwd
381
+
382
+ const messages = [
383
+ { role: "system", content: systemPrompt },
384
+ { role: "user", content: userMessage },
385
+ ]
386
+
387
+ try {
388
+ return await runAdvisorToolLoop(provider, messages, onOutput, signal, agent, advisorCwd)
389
+ } catch (e) {
390
+ if (e.name === "AbortError" && signal?.reason?.interrupt) throw e
391
+ return `Advisor: review failed — ${e.message || "unknown error"}. You may retry or proceed to verify.`
392
+ }
105
393
  }
@@ -12,8 +12,12 @@ export const DEFAULT_SUBAGENT_TURNS = 100
12
12
  export const DEFAULT_GOAL_TURNS = 200
13
13
  export const MIN_REPORT_CHARS = 200
14
14
  export const REPORT_CONTINUATION =
15
- "Your report is too brief to be a complete handoff — the parent agent sees nothing else from your run. " +
16
- "Expand it: what you did and why, the path of every file you touched, how you verified (commands/tests run, with results), and anything left undone."
15
+ "Your report was sent back: too brief to be a complete handoff — the parent agent sees nothing else from your run. " +
16
+ "Rewrite your final message as a checklist:\n" +
17
+ "1. What you changed and why\n" +
18
+ "2. The path of every file you touched\n" +
19
+ "3. How you verified (tests run, commands executed, with results)\n" +
20
+ "4. Anything left undone or worth follow-up"
17
21
 
18
22
  const TOOL_RESULT_OFFLOAD_LIMIT = 16_000
19
23
  const TOOL_RESULT_PREVIEW = 2_000
@@ -22,7 +26,7 @@ const GIT_TIMEOUT_MS = 5000
22
26
  const MAX_GIT_CHANGES_DISPLAY = 20
23
27
 
24
28
  export const OUTLINE_INJECT_PREFIX = "[System reminder: project dependency outline:"
25
- export const FILE_MUTATORS = new Set(["write", "edit", "insert_after", "apply_patch", "delete"])
29
+ export const FILE_MUTATORS = new Set(["write", "edit", "insert_after", "apply_patch", "delete", "hashline_edit"])
26
30
 
27
31
  /** Escape XML special characters in a string for safe embedding in XML/HTML */
28
32
  export function escapeXml(s) {
@@ -117,8 +117,8 @@ export async function prepareRun(agent, input, callbacks, {
117
117
  }
118
118
 
119
119
  // task/plan tools are injected with the main loop; subagent/skill/goal/verify only at top level
120
- const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool } = await import("../agent-tools.mjs")
121
- const tools = [...agent.tools, taskTool, planTool, timerTool, ...(depth === 0 ? [subagentTool, skillTool, goalTool, verifyTool, recentChangesTool] : [])]
120
+ const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool } = await import("../agent-tools.mjs")
121
+ const tools = [...agent.tools, taskTool, planTool, timerTool, ...(depth === 0 ? [subagentTool, skillTool, goalTool, verifyTool, recentChangesTool, advisorTool] : [])]
122
122
  const toolSchemas = tools.map(toOpenAISchema)
123
123
  const toolByName = new Map(tools.map((t) => [t.name, t]))
124
124
  agent._onTaskUpdate = callbacks.onTaskUpdate
@@ -0,0 +1,36 @@
1
+ /**
2
+ * agent-tools/advisor.mjs — advisor tool wrapper.
3
+ * The agent calls this explicitly at the end of a coding task to get a code review.
4
+ */
5
+ import { runAdvisorReview } from "../advisor.mjs"
6
+
7
+ export const advisorTool = {
8
+ name: "advisor",
9
+ description:
10
+ "Run a code review on your changes (convergence protocol). " +
11
+ "Call this when you have finished coding and want an independent review before finalising. " +
12
+ "The advisor is an independent read-only sub-agent that explores the codebase, runs git diff, " +
13
+ "reads files, and traces callers via grep/lsp. " +
14
+ "Review criteria come from .thincoder/advisor.md (if present) or sensible defaults. " +
15
+ "Round 1 does a full review and produces a numbered issue table. " +
16
+ "After the review, you MUST produce a response table (see discipline rules for format). " +
17
+ "Round 2 verifies the table + can flag obvious new issues. " +
18
+ "Round 3+ strictly checks only the prior table — convergence, not divergence. " +
19
+ "If issues are found, fix them, update your response table, then re-run advisor. " +
20
+ "If advisor says all clear, call verify.",
21
+ parameters: {
22
+ type: "object",
23
+ properties: {},
24
+ },
25
+ readonly: true,
26
+ sideEffectExempt: true,
27
+ outputPanel: true,
28
+ async execute(_args, ctx) {
29
+ const agent = ctx.agent
30
+
31
+ const result = await runAdvisorReview(agent, ctx.onOutput, ctx.signal)
32
+ if (!result) return "Advisor: review is disabled or no changes to review."
33
+
34
+ return result
35
+ },
36
+ }
@@ -2,7 +2,56 @@
2
2
  * plan tool: enter/exit plan mode.
3
3
  * In plan mode only read-only tools are allowed — explore code, design solutions, no code writing.
4
4
  * After the user approves the plan, exit plan mode and start implementing.
5
+ *
6
+ * Reminder cadence (kimi-code style): while plan mode is active the agent loop
7
+ * re-injects reminders — sparse every 2 turns, full every 5 turns or when the
8
+ * user sends a new message — so the constraint never fades from context.
5
9
  */
10
+
11
+ export const PLAN_FULL_REMINDER =
12
+ "[System reminder: plan mode is ON. Workflow: (1) explore/read codebase with read-only tools, " +
13
+ "(2) design a solution considering trade-offs, (3) present your plan by calling plan with action='exit' " +
14
+ "so the user can approve it. Only read-only tools are allowed — do not write, edit, or run mutation commands. " +
15
+ "Your turn must end with either a clarifying question to the user or a call to plan with action='exit'.]"
16
+
17
+ export const PLAN_SPARSE_REMINDER =
18
+ "[System reminder: plan mode still active — read-only tools only (the current plan file exempt). " +
19
+ "Design the solution, then call plan with action='exit' for user approval.]"
20
+
21
+ export const PLAN_EXIT_REMINDER =
22
+ "[System reminder: plan mode is now OFF. Start implementing your plan — edit files, run commands. " +
23
+ "No need for a task list (plan already covered that) or further confirmation.]"
24
+
25
+ /** Turns between reminder re-injections while plan mode is active */
26
+ const SPARSE_INTERVAL = 2
27
+ const FULL_INTERVAL = 5
28
+
29
+ /**
30
+ * Decide which plan-mode reminder (if any) to inject this turn.
31
+ * @param {object} agent — the agent object (mutated: tracks reminder state)
32
+ * @param {boolean} userMessageSince — whether a user message arrived since the last reminder
33
+ * @returns {string|null} reminder text or null
34
+ */
35
+ export function planReminderForTurn(agent, userMessageSince) {
36
+ if (!agent.planMode) {
37
+ agent._planTurnsSinceReminder = 0
38
+ agent._planTurnsSinceSparse = 0
39
+ return null
40
+ }
41
+ agent._planTurnsSinceReminder = (agent._planTurnsSinceReminder ?? 0) + 1
42
+ agent._planTurnsSinceSparse = (agent._planTurnsSinceSparse ?? 0) + 1
43
+ if (userMessageSince || agent._planTurnsSinceReminder >= FULL_INTERVAL) {
44
+ agent._planTurnsSinceReminder = 0
45
+ agent._planTurnsSinceSparse = 0
46
+ return PLAN_FULL_REMINDER
47
+ }
48
+ if (agent._planTurnsSinceSparse >= SPARSE_INTERVAL) {
49
+ agent._planTurnsSinceSparse = 0
50
+ return PLAN_SPARSE_REMINDER
51
+ }
52
+ return null
53
+ }
54
+
6
55
  export const planTool = {
7
56
  name: "plan",
8
57
  description:
@@ -18,13 +67,15 @@ export const planTool = {
18
67
  async execute(args, ctx) {
19
68
  if (args.action === "exit") {
20
69
  ctx.agent.planMode = false
70
+ ctx.agent._planTurnsSinceReminder = 0
21
71
  ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
22
- ctx.agent._pendingReminders.push("[System reminder: plan mode is now OFF. Start implementing your plan — edit files, run commands. No need for a task list (plan already covered that) or further confirmation.]")
72
+ ctx.agent._pendingReminders.push(PLAN_EXIT_REMINDER)
23
73
  return "Plan mode exited. You may now edit files and run commands."
24
74
  }
25
75
  ctx.agent.planMode = true
76
+ ctx.agent._planTurnsSinceReminder = 0
26
77
  ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
27
- ctx.agent._pendingReminders.push("[System reminder: plan mode is now ON. Workflow: (1) explore/read codebase with read-only tools, (2) design a solution considering trade-offs, (3) present your plan by calling plan with action='exit' so the user can approve it. Only read-only tools are allowed — do not write, edit, or run mutation commands.]")
78
+ ctx.agent._pendingReminders.push(PLAN_FULL_REMINDER)
28
79
  return "Plan mode activated. You are now restricted to READ-ONLY tools. Explore the codebase, understand the architecture, design a solution. Present your plan to the user for approval before writing any code."
29
80
  },
30
81
  }
@@ -16,7 +16,13 @@ import {
16
16
  export const subagentTool = {
17
17
  name: "subagent",
18
18
  description:
19
- "Spawn a sub-agent to handle an independent subtask in an isolated context. The sub-agent returns only its final report. Spawn MULTIPLE subagents in the SAME response for parallel work—they run concurrently. Use role='explore' for codebase search/analysis (read-only, fast), role='plan' for read-only implementation planning (returns a step-by-step plan, never edits), role='coder' for self-contained implementation tasks. Do not give parallel subagents tasks that edit the same files.",
19
+ "Spawn a sub-agent to handle an independent subtask in an isolated context. The sub-agent returns only its final report. Spawn MULTIPLE subagents in the SAME response for parallel work—they run concurrently.\n" +
20
+ "Use role='explore' for codebase search/analysis (read-only, fast), role='plan' for read-only implementation planning (returns a step-by-step plan, never edits), role='coder' for self-contained implementation tasks. Do not give parallel subagents tasks that edit the same files.\n\n" +
21
+ "Writing the prompt:\n" +
22
+ "- The sub-agent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.\n" +
23
+ "- Put exact paths and commands in the prompt when you know them. The sub-agent should not search for things you already know.\n" +
24
+ "- Do not delegate understanding: if the task hinges on a file path or line number, find it yourself first and write it into the prompt.\n" +
25
+ "- Once a sub-agent is running, leave that scope to it: don't redo its searches in parallel, and don't abandon it midway to finish manually.",
20
26
  parameters: {
21
27
  type: "object",
22
28
  properties: {
@@ -29,7 +29,7 @@ export const timerTool = {
29
29
  readonly: true,
30
30
  sideEffectExempt: true,
31
31
  execute(args, ctx) {
32
- const seconds = args.seconds ?? 30
32
+ const seconds = args.seconds ?? 180
33
33
  const expiresAt = Date.now() + seconds * 1000
34
34
  const message = args.message || `⏰ Time's up (${seconds}s). Have you tried running the code, adding a console.log, or checking the output? Thinking more without data is guessing.`
35
35