thincoder 0.8.10 → 0.8.12

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 (52) hide show
  1. package/README.md +16 -0
  2. package/bin/thincoder.mjs +115 -0
  3. package/package.json +1 -1
  4. package/src/advisor.mjs +105 -0
  5. package/src/agent/dispatch.mjs +35 -0
  6. package/src/agent/helpers.mjs +1 -1
  7. package/src/agent/setup.mjs +21 -7
  8. package/src/agent-tools/subagent.mjs +1 -1
  9. package/src/agent-tools/timer.mjs +41 -0
  10. package/src/agent-tools/verify.mjs +165 -56
  11. package/src/agent-tools.mjs +1 -0
  12. package/src/agent.mjs +128 -20
  13. package/src/auto-think.mjs +83 -0
  14. package/src/cli/make-agent.mjs +9 -0
  15. package/src/config.mjs +18 -18
  16. package/src/git/checkpoint.mjs +2 -1
  17. package/src/git/gitmem.mjs +8 -2
  18. package/src/markdown.mjs +1 -1
  19. package/src/mcp/transport-http.mjs +8 -2
  20. package/src/memory/code-index.mjs +2 -2
  21. package/src/memory/code-sync.mjs +92 -35
  22. package/src/memory/core.mjs +10 -1
  23. package/src/memory/docs.mjs +24 -26
  24. package/src/memory/schema.mjs +16 -3
  25. package/src/prompts/coder.md +7 -4
  26. package/src/prompts/discipline.md +55 -16
  27. package/src/prompts/main.md +15 -11
  28. package/src/prompts/system.md +33 -7
  29. package/src/provider/core.mjs +186 -20
  30. package/src/provider/index.mjs +1 -1
  31. package/src/rules.mjs +53 -0
  32. package/src/session.mjs +1 -1
  33. package/src/tools/checklist.md +7 -0
  34. package/src/tools/checklist.mjs +114 -0
  35. package/src/tools/file.mjs +82 -1
  36. package/src/tools/hashline_edit.md +12 -0
  37. package/src/tools/index.mjs +7 -3
  38. package/src/tools/linter.md +13 -0
  39. package/src/tools/linter.mjs +146 -0
  40. package/src/tools/read.md +3 -2
  41. package/src/tools/repomap.mjs +14 -9
  42. package/src/tui/agent-turn.mjs +9 -2
  43. package/src/tui/ansi.mjs +1 -0
  44. package/src/tui/cmd-advisor.mjs +68 -0
  45. package/src/tui/cmd-think.mjs +36 -10
  46. package/src/tui/index.mjs +2 -1
  47. package/src/tui/key-handler.mjs +36 -1
  48. package/src/tui/layout.mjs +3 -1
  49. package/src/tui/pickers.mjs +15 -15
  50. package/src/tui/render-frame.mjs +17 -8
  51. package/src/tui/slash-commands.mjs +3 -0
  52. package/src/tools/repomap-parse.mjs +0 -168
package/README.md CHANGED
@@ -205,6 +205,22 @@ Code conventions: pure `.mjs`, no semicolons, no npm dependencies allowed (inclu
205
205
 
206
206
  ## Changelog
207
207
 
208
+ ### 0.8.12 (2026-07)
209
+ - **Indexing: git-repo-only** — `codeSync` and `docSync` now only index inside git worktrees (via `git ls-files`), respecting `.gitignore`. Non-git directories get empty indexes. Prevents 2.9GB memory.db from accidentally indexing entire user profiles (AppData, browser extensions, Office add-ins, Program Files)
210
+ - **Indexing: file-size caps** — code files >1MB and doc files >512KB are skipped during bulk indexing (minified bundles, test fixtures, generated code)
211
+ - **Compaction: earlier trigger** — `COMPACT_RATIO` lowered from 0.8 to 0.6 (with 40K floor), so injected context (directory tree, git context, outline, memory/doc search results) doesn't starve the model for headroom before compaction fires
212
+ - **Bugfix: event-loop blocking** — `buildOutline`/`buildSummary` in `repomap.mjs` now awaited (was sync calling async), `setup.mjs` outline injection fixed, `codeSync` walk uses `readFile` instead of `readFileSync`
213
+ - **Feat: SKIP_DIRS expanded** — Windows profile directories (`AppData`, `Desktop`, `Documents`, `Downloads`, `Music`, `Pictures`, `Videos`, etc.) added to the skip list
214
+ - **Feat: `listProjectFiles` shared helper** — extracted from `codeSync`/`docSync`, used by both; git-only with fallback removed
215
+ - **Refactor: `embed-config.mjs`** — simplified to only read API key via `resolveEmbedKey()`, base URL and model are fixed constants; VSCode SecretStorage dependency removed
216
+
217
+ ### 0.8.11 (2026-07)
218
+ - **Feat**: `checklist` tool — persistent project task tracking in `.thincoder/checklist.md`. Add/mark/list items, auto-archive done items to `checklist-done.md`. Injected at session start (pending + in_progress only)
219
+ - **Feat**: updated prompts — four-step workflow (requirements→design→development→testing), three-step debugging strategy (logs→docs→binary search), working checklist discipline
220
+ - **Feat**: methodology docs — `docs/design/METHODOLOGY.md` rebuilt, `PHILOSOPHY.md` expanded with worldview #6 (official docs over guessing)
221
+ - **Bugfix**: vision guard — `read_image` now refuses non-vision models before reading the file, the agent loop injects a system reminder instead of image parts for text-only models, and `stripImagesForTextModel` sanitizes image parts at send time (history untouched, restored when switching back to a vision model). Previously a single image in history made text-only APIs (e.g. DeepSeek) reject EVERY subsequent request with 400, bricking the conversation
222
+ - **Improvement**: silent `catch {}` blocks now log to stderr (checkpoint file copies, MCP notify/close, session incremental save, clipboard paste, provider picker, team-memory rebase abort, tool dispatch) — failures are visible for debugging instead of disappearing
223
+
208
224
  ### 0.8.10 (2026-07)
209
225
  - **Bugfix**: pasted text now lands in the active TUI text target — the API key prompt when adding a provider via `/model` (and any free-text `askQuestion`) now accepts paste correctly. Previously, bracketed-paste injection in the terminal was always written to the main input box, so pasting into a question prompt appeared as "nothing happened" and orphaned the text into the input box after the question closed. Both bracketed-paste (Windows Terminal / most modern terminals) and Ctrl+V-as-key-event (legacy conhost) now route through a single `insertPastedText` helper that targets the question answer, options-list (ignored), or main input box as appropriate
210
226
 
package/bin/thincoder.mjs CHANGED
@@ -6,6 +6,7 @@
6
6
  * thincoder chat "..." One-shot agent run (tools enabled, streamed)
7
7
  * thincoder memory <sub> Memory management: list / search / put / remove
8
8
  * thincoder upgrade Update to the latest version from npm
9
+ * thincoder completion <sh> Shell completion: bash / zsh / fish
9
10
  * thincoder -v Print version
10
11
  * thincoder --help Print help
11
12
  */
@@ -49,6 +50,7 @@ Usage:
49
50
  Extract knowledge candidates from a session
50
51
  transcript file; confirm each before saving
51
52
  thincoder upgrade Update to the latest version from npm
53
+ thincoder completion <sh> Generate shell completion script (bash / zsh / fish)
52
54
  thincoder -v, --version Print version
53
55
 
54
56
  Config: ~/.thincoder/config.json (providers[] + activeProvider; manage via /provider, /model in TUI)
@@ -252,6 +254,119 @@ switch (command) {
252
254
  break
253
255
  }
254
256
 
257
+ case "completion": {
258
+ const shell = args[0]
259
+ if (!["bash", "zsh", "fish"].includes(shell)) {
260
+ console.error(`Usage: thincoder completion <bash|zsh|fish>`)
261
+ exitSoon(1)
262
+ break
263
+ }
264
+ if (shell === "bash") {
265
+ process.stdout.write(`_thincoder() {
266
+ local cur prev words cword
267
+ _init_completion 2>/dev/null || { COMPREPLY=(); return; }
268
+ cur="\${COMP_WORDS[COMP_CWORD]}"
269
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
270
+ case "\${COMP_WORDS[1]}" in
271
+ chat) COMPREPLY=( \\$(compgen -W "--auto" -- "\\$cur") ) ;;
272
+ memory)
273
+ case "\\$prev" in
274
+ memory) COMPREPLY=( \\$(compgen -W "list search put remove" -- "\\$cur") ) ;;
275
+ list) COMPREPLY=( \\$(compgen -W "--type=rule --type=knowledge --type=decision --type=pattern" -- "\\$cur") ) ;;
276
+ put) COMPREPLY=( \\$(compgen -W "--type= --title= --content= --tags=" -- "\\$cur") ) ;;
277
+ esac ;;
278
+ distill) COMPREPLY=( \\$(compgen -W "--yes --scope=" -- "\\$cur") ) ;;
279
+ completion) COMPREPLY=( \\$(compgen -W "bash zsh fish" -- "\\$cur") ) ;;
280
+ *)
281
+ COMPREPLY=( \\$(compgen -W "chat memory sync reindex distill upgrade completion -v --version -h --help" -- "\\$cur") ) ;;
282
+ esac
283
+ }
284
+ complete -F _thincoder thincoder
285
+ `)
286
+ } else if (shell === "zsh") {
287
+ process.stdout.write(`#compdef thincoder
288
+
289
+ _thincoder() {
290
+ local context state state_descr line
291
+ typeset -A opt_args
292
+ _arguments -C \\
293
+ '1: :->cmd' \\
294
+ '*:: :->args'
295
+
296
+ case "\\$state" in
297
+ cmd)
298
+ _values 'command' \\
299
+ 'chat[One-shot agent run with tools]' \\
300
+ 'memory[Manage long-term memory]' \\
301
+ 'sync[Sync team memory repo]' \\
302
+ 'reindex[Rebuild local index from markdown]' \\
303
+ 'distill[Extract knowledge from session transcript]' \\
304
+ 'upgrade[Update to latest version from npm]' \\
305
+ 'completion[Generate shell completion script]'
306
+ ;;
307
+ args)
308
+ case "\\$words[1]" in
309
+ chat) _arguments '--auto[Auto-approve all tool calls]' ;;
310
+ memory)
311
+ case "\\$words[2]" in
312
+ list) _arguments '--type=[Filter by type]' ;;
313
+ put) _arguments '--type=[Entry type]' '--title=[Title]' '--content=[Content]' '--tags=[Space-separated tags]' ;;
314
+ esac ;;
315
+ distill) _arguments '--yes[Skip confirmation]' '--scope=[Scope filter]' ;;
316
+ completion) _values 'shell' 'bash' 'zsh' 'fish' ;;
317
+ esac ;;
318
+ esac
319
+ }
320
+ _thincoder
321
+ `)
322
+ } else if (shell === "fish") {
323
+ process.stdout.write(`# thincoder completions for fish shell
324
+ complete -c thincoder -f
325
+
326
+ # Subcommands
327
+ complete -c thincoder -a chat -d 'One-shot agent run with tools'
328
+ complete -c thincoder -a memory -d 'Manage long-term memory'
329
+ complete -c thincoder -a sync -d 'Sync team memory repo'
330
+ complete -c thincoder -a reindex -d 'Rebuild local index from markdown'
331
+ complete -c thincoder -a distill -d 'Extract knowledge from session'
332
+ complete -c thincoder -a upgrade -d 'Update to latest version'
333
+ complete -c thincoder -a completion -d 'Shell completion'
334
+
335
+ # Flags
336
+ complete -c thincoder -s v -l version -d 'Print version'
337
+ complete -c thincoder -s h -l help -d 'Print help'
338
+
339
+ # chat flags
340
+ complete -c thincoder -n '__fish_seen_subcommand_from chat' -l auto -d 'Auto-approve tool calls'
341
+
342
+ # memory subcommands
343
+ complete -c thincoder -n '__fish_seen_subcommand_from memory' -a list -d 'List entries'
344
+ complete -c thincoder -n '__fish_seen_subcommand_from memory' -a search -d 'Search memory'
345
+ complete -c thincoder -n '__fish_seen_subcommand_from memory' -a put -d 'Add entry'
346
+ complete -c thincoder -n '__fish_seen_subcommand_from memory' -a remove -d 'Remove entry'
347
+
348
+ # memory list flags
349
+ complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from list' -l type -d 'Filter by type' -xa 'rule knowledge decision pattern'
350
+
351
+ # memory put flags
352
+ complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l type -d 'Entry type'
353
+ complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l title -d 'Title'
354
+ complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l content -d 'Content'
355
+ complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l tags -d 'Space-separated tags'
356
+
357
+ # distill flags
358
+ complete -c thincoder -n '__fish_seen_subcommand_from distill' -l yes -d 'Skip confirmation'
359
+ complete -c thincoder -n '__fish_seen_subcommand_from distill' -l scope -d 'Scope filter'
360
+
361
+ # completion shells
362
+ complete -c thincoder -n '__fish_seen_subcommand_from completion' -a bash -d 'Bash completions'
363
+ complete -c thincoder -n '__fish_seen_subcommand_from completion' -a zsh -d 'Zsh completions'
364
+ complete -c thincoder -n '__fish_seen_subcommand_from completion' -a fish -d 'Fish completions'
365
+ `)
366
+ }
367
+ break
368
+ }
369
+
255
370
  case "upgrade": {
256
371
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"))
257
372
  const local = pkg.version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.8.10",
3
+ "version": "0.8.12",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
@@ -0,0 +1,105 @@
1
+ /**
2
+ * advisor.mjs — automated code review after each turn.
3
+ *
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
+ *
7
+ * Config:
8
+ * { advisor: { enabled: true, provider: "deepseek", model: "deepseek-chat" } }
9
+ * provider + model are optional — defaults to the main agent's provider/model.
10
+ */
11
+ import { chat } from "./provider/core.mjs"
12
+ import { findProvider } from "./config.mjs"
13
+
14
+ const ADVISOR_PROMPT = `你是代码审查顾问。审查编程助手最近一轮操作并提供简短的观察。
15
+
16
+ 规则:
17
+ - 用中文回答,2-4 行,分条目
18
+ - 关注:遗漏的边界情况、错误的假设、低效的模式、安全隐患
19
+ - 如果一切正常,说"未发现问题"
20
+ - 不要建议或调用工具 — 只做只读审查
21
+ - 不要直接对用户说话 — 用第三人称评价助手的行为`
22
+
23
+ /**
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.
27
+ */
28
+ export async function runAdvisor(agent) {
29
+ const cfg = agent.config?.advisor
30
+ if (!cfg?.enabled) return null
31
+
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
50
+ }
51
+
52
+ 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
62
+
63
+ return `[Advisor 审查 — 自动观察,非用户指令。请批判性参考:\n${response.content.trim()}]`
64
+ } catch {
65
+ // Advisor failure is non-fatal — main loop continues
66
+ return null
67
+ }
68
+ }
69
+
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
74
+ }
75
+ return -1
76
+ }
77
+
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")
85
+
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)}`)
95
+ }
96
+ }
97
+
98
+ return lines.join("\n")
99
+ }
100
+
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) + "…"
105
+ }
@@ -2,6 +2,37 @@
2
2
  * agent/dispatch.mjs — two-phase tool call execution
3
3
  */
4
4
  import { offloadToolResult } from "./helpers.mjs"
5
+ import { writeFileSync, mkdirSync, existsSync } from "node:fs"
6
+ import { join } from "node:path"
7
+ import { homedir } from "node:os"
8
+
9
+ const ERRORS_DIR = join(homedir(), ".thincoder", "tool-errors")
10
+
11
+ /**
12
+ * Persist a tool error to ~/.thincoder/tool-errors/YYYY-MM-DD/HHmmss-toolName.log
13
+ * Only called for actual execution failures and malformed invocations.
14
+ * Skipped for intentional denials (plan mode, user reject).
15
+ */
16
+ function logToolError(toolName, args, error) {
17
+ try {
18
+ const now = new Date()
19
+ const ymd = now.toISOString().slice(0, 10)
20
+ const ts = now.toISOString().replace(/:/g, "").replace(/\..+/, "").replace("T", "-")
21
+ const dir = join(ERRORS_DIR, ymd)
22
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
23
+ const file = join(dir, `${ts}-${toolName.replace(/[/\\]/g, "_")}.log`)
24
+ const entry = [
25
+ `time: ${now.toISOString()}`,
26
+ `tool: ${toolName}`,
27
+ `args: ${JSON.stringify(args, null, 2)}`,
28
+ `error: ${error?.message ?? String(error)}`,
29
+ error?.stack ? `stack:\n${error.stack}` : "",
30
+ ].filter(Boolean).join("\n") + "\n"
31
+ writeFileSync(file, entry, "utf8")
32
+ } catch {
33
+ // Log failure itself must not crash the agent
34
+ }
35
+ }
5
36
 
6
37
  /**
7
38
  * Two-phase execution:
@@ -19,11 +50,13 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
19
50
  try {
20
51
  args = JSON.parse(toolCall.arguments || "{}")
21
52
  } catch {
53
+ logToolError(toolCall.name, { arguments: toolCall.arguments }, new Error("Invalid JSON arguments"))
22
54
  prepared.push({ toolCall, tool: null, error: `Invalid tool arguments JSON: ${toolCall.arguments}` })
23
55
  continue
24
56
  }
25
57
 
26
58
  if (!tool) {
59
+ logToolError(toolCall.name, {}, new Error(`Unknown tool: ${toolCall.name}`))
27
60
  prepared.push({ toolCall, tool: null, error: `Unknown tool: ${toolCall.name}` })
28
61
  continue
29
62
  }
@@ -79,6 +112,8 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
79
112
  callbacks.onToolResult?.(item.toolCall.name, result)
80
113
  return { ...item, result, ok: true }
81
114
  } catch (error) {
115
+ // Persist to ~/.thincoder/tool-errors/ for post-mortem; only pass message to the model (stack traces confuse LLMs and may leak paths)
116
+ logToolError(item.toolCall.name, item.args, error)
82
117
  return { ...item, result: `Error: ${error.message}`, ok: false }
83
118
  }
84
119
  }
@@ -2,7 +2,7 @@
2
2
  * agent/helpers.mjs — Agent utility functions and constants
3
3
  */
4
4
  import { configDir } from "../config.mjs"
5
- import { readFileSync, readdirSync } from "node:fs"
5
+ import { readFileSync, readdirSync, existsSync } from "node:fs"
6
6
  import { writeFile, mkdir } from "node:fs/promises"
7
7
  import { join } from "node:path"
8
8
  import { execSync } from "node:child_process"
@@ -47,13 +47,17 @@ export async function prepareRun(agent, input, callbacks, {
47
47
  }
48
48
  if (depth === 0) {
49
49
  const tree = listWorkDir(agent.cwd)
50
+ const platform = { win32: 'Windows', darwin: 'macOS', linux: 'Linux' }[process.platform] ?? process.platform
51
+ agent._sessionStart ??= new Date().toISOString()
50
52
  if (tree) {
51
- agent.history.push({ role: "user", content: `[System reminder: working directory snapshot (${new Date().toISOString()}):\n<untrusted_cwd_listing>\n${escapeXml(tree)}\n</untrusted_cwd_listing>]`, transient: true })
53
+ agent.history.push({ role: "user", content: `[System reminder: OS: ${platform}. Working directory: ${agent.cwd}. Session start: ${agent._sessionStart}. Working directory snapshot:\n<untrusted_cwd_listing>\n${escapeXml(tree)}\n</untrusted_cwd_listing>]`, transient: true })
54
+ } else {
55
+ agent.history.push({ role: "user", content: `[System reminder: OS: ${platform}. Working directory: ${agent.cwd}. Session start: ${agent._sessionStart}.]`, transient: true })
52
56
  }
53
57
  if (agent.memory && !agent.history.some((m) => typeof m.content === "string" && m.content.startsWith(OUTLINE_INJECT_PREFIX))) {
54
58
  try {
55
59
  const { buildSummary } = await import("../tools/repomap.mjs")
56
- const summary = buildSummary(agent.memory.db, agent.cwd)
60
+ const summary = await buildSummary(agent.memory.db, agent.cwd)
57
61
  if (summary && !summary.startsWith("(no indexed")) {
58
62
  agent.history.push({ role: "user", content: `${OUTLINE_INJECT_PREFIX}\n${summary}]`, transient: true })
59
63
  }
@@ -92,6 +96,18 @@ export async function prepareRun(agent, input, callbacks, {
92
96
  content: `[System reminder: current time is ${new Date().toISOString()}.]`,
93
97
  transient: true,
94
98
  })
99
+ // Checklist injection: inject pending + in_progress items from .thincoder/checklist.md
100
+ try {
101
+ const { pendingItems } = await import("../tools/checklist.mjs")
102
+ const items = pendingItems(agent.cwd)
103
+ if (items.length > 0) {
104
+ agent.history.push({
105
+ role: "user",
106
+ content: `[System reminder: task checklist (pending/in-progress):\n${items.map(i => `- [${i.status === "in_progress" ? "~" : " "}] ${i.text}`).join("\n")}]`,
107
+ transient: true,
108
+ })
109
+ }
110
+ } catch { /* checklist not available — suppress error */ }
95
111
  }
96
112
  agent.history.push({ role: "user", content: input })
97
113
  }
@@ -104,8 +120,8 @@ export async function prepareRun(agent, input, callbacks, {
104
120
  }
105
121
 
106
122
  // task/plan tools are injected with the main loop; subagent/skill/goal/verify only at top level
107
- const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool } = await import("../agent-tools.mjs")
108
- const tools = [...agent.tools, taskTool, planTool, ...(depth === 0 ? [subagentTool, skillTool, goalTool, verifyTool, recentChangesTool] : [])]
123
+ const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool } = await import("../agent-tools.mjs")
124
+ const tools = [...agent.tools, taskTool, planTool, timerTool, ...(depth === 0 ? [subagentTool, skillTool, goalTool, verifyTool, recentChangesTool] : [])]
109
125
  const toolSchemas = tools.map(toOpenAISchema)
110
126
  const toolByName = new Map(tools.map((t) => [t.name, t]))
111
127
  agent._onTaskUpdate = callbacks.onTaskUpdate
@@ -118,9 +134,7 @@ export async function prepareRun(agent, input, callbacks, {
118
134
  : depth === 0
119
135
  ? `${base}\n\n${mainOverlay}`
120
136
  : base
121
- const platform = { win32: 'Windows', darwin: 'macOS', linux: 'Linux' }[process.platform] ?? process.platform
122
- agent._sessionStart ??= new Date().toISOString()
123
- systemPrompt += `\n\nOS: ${platform}. Working directory: ${agent.cwd}. Session start: ${agent._sessionStart}.`
137
+
124
138
  const projectRules = await loadProjectInstructions(agent.cwd)
125
139
  if (projectRules) {
126
140
  systemPrompt += `\n\nProject instructions (follow these as project conventions):\n<untrusted_project_instructions>\n${escapeXml(projectRules)}\n</untrusted_project_instructions>`
@@ -100,7 +100,7 @@ export const subagentTool = {
100
100
  ? (name, args) => ctx.callbacks.onToolCall(`${relayPrefix}${name}`, args)
101
101
  : null,
102
102
  }
103
- const childRunOpts = { depth: (ctx.depth ?? 0) + 1, maxTurns: DEFAULT_SUBAGENT_TURNS }
103
+ const childRunOpts = { depth: (ctx.depth ?? 0) + 1, maxTurns: ctx.agent?.config?.agent?.subagentTurns ?? DEFAULT_SUBAGENT_TURNS }
104
104
  let report = await runAgent(child, input, childOpts, childRunOpts)
105
105
 
106
106
  // Report too short = incomplete handoff: send back for expansion once (inspired by kimi-code's summaryPolicy: min 200 chars, retry 1 time).
@@ -0,0 +1,41 @@
1
+ /**
2
+ * timer tool: set a time budget for thinking before the agent insists on action.
3
+ * Call this when starting to analyze code or debug — it gives a bounded
4
+ * thinking window. When the timer fires, a system reminder is injected
5
+ * suggesting the model try running code, adding logs, or otherwise acting
6
+ * instead of continuing to think.
7
+ */
8
+ export const timerTool = {
9
+ name: "timer",
10
+ description:
11
+ "Set a timer before you start analyzing code. When the timer fires, " +
12
+ "a system reminder will be injected suggesting you try running code or " +
13
+ "adding debug logs. Use this to enforce a thinking budget: you get " +
14
+ "N seconds to reason, then the timer reminds you to act.",
15
+ parameters: {
16
+ type: "object",
17
+ properties: {
18
+ seconds: {
19
+ type: "number",
20
+ description: "Thinking budget in seconds (default 30). Longer for complex reasoning, shorter for simple tasks.",
21
+ },
22
+ message: {
23
+ type: "string",
24
+ description: "Custom reminder message to show when time is up. Default: a suggestion to add debug logs or run the code.",
25
+ },
26
+ },
27
+ required: ["seconds"],
28
+ },
29
+ readonly: true,
30
+ sideEffectExempt: true,
31
+ execute(args, ctx) {
32
+ const seconds = args.seconds ?? 30
33
+ const expiresAt = Date.now() + seconds * 1000
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
+
36
+ ctx.agent._pendingTimers = ctx.agent._pendingTimers ?? []
37
+ ctx.agent._pendingTimers.push({ id: Date.now(), expiresAt, message })
38
+
39
+ return `Timer set for ${seconds} seconds. A reminder will appear when time is up.`
40
+ },
41
+ }