thincoder 0.8.10 → 0.8.11

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/README.md CHANGED
@@ -205,6 +205,11 @@ Code conventions: pure `.mjs`, no semicolons, no npm dependencies allowed (inclu
205
205
 
206
206
  ## Changelog
207
207
 
208
+ ### 0.8.11 (2026-07)
209
+ - **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)
210
+ - **Feat**: updated prompts — four-step workflow (requirements→design→development→testing), three-step debugging strategy (logs→docs→binary search), working checklist discipline
211
+ - **Feat**: methodology docs — `docs/design/METHODOLOGY.md` rebuilt, `PHILOSOPHY.md` expanded with worldview #6 (official docs over guessing)
212
+
208
213
  ### 0.8.10 (2026-07)
209
214
  - **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
215
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.8.10",
3
+ "version": "0.8.11",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
@@ -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"
@@ -92,6 +92,18 @@ export async function prepareRun(agent, input, callbacks, {
92
92
  content: `[System reminder: current time is ${new Date().toISOString()}.]`,
93
93
  transient: true,
94
94
  })
95
+ // Checklist injection: inject pending + in_progress items from .thincoder/checklist.md
96
+ try {
97
+ const { pendingItems } = await import("../tools/checklist.mjs")
98
+ const items = pendingItems(agent.cwd)
99
+ if (items.length > 0) {
100
+ agent.history.push({
101
+ role: "user",
102
+ content: `[System reminder: task checklist (pending/in-progress):\n${items.map(i => `- [${i.status === "in_progress" ? "~" : " "}] ${i.text}`).join("\n")}]`,
103
+ transient: true,
104
+ })
105
+ }
106
+ } catch { /* checklist not available — suppress error */ }
95
107
  }
96
108
  agent.history.push({ role: "user", content: input })
97
109
  }
@@ -1,4 +1,10 @@
1
1
  Coding discipline (rigor over speed—tokens spent on verification are well spent):
2
+
3
+ **Workflow — never skip steps:**
4
+ - Before writing code: 1) Requirements — clarify what's needed, write user stories, confirm. 2) Design — plan architecture and approach, write a design doc. 3) Development — write code. 4) Testing — write test cases covering normal, boundary, and error cases. Documents are required for steps 1, 2, and 4. Requirements are not complete until checklist entries exist for every requirement point — use `checklist add` to create them. Skipping straight to step 3 is wrong ten times out of nine.
5
+ - Maintain a visible checklist for every task in `.thincoder/checklist.md` using the `checklist` tool. Checklist entries map to requirement/design points from the project's docs — project-level tracking. After requirements are confirmed, add one entry per requirement point. When you start working on an item, mark it in_progress. When it's verified complete, mark it done. Do not rely on context memory — context compresses, the checklist persists.
6
+
7
+ **Coding rules:**
2
8
  - **Prefer built-in tools over bash for file operations**: use `ls` (not `bash ls`), `glob` (not `bash find`), `grep` (not `bash grep`). The bash tool runs the system shell — on Windows this is cmd.exe without Unix commands; on Unix it may have them but built-in tools are more reliable and platform-consistent.
3
9
  - Spec before code: when the user describes a feature request without specifying the details (retry count? timeout? which error types? which files?), ask clarifying questions before writing code.
4
10
  - Design docs are the spec: when the project has design documents (check with `doc_search`), read them before implementing. Their decisions represent intentional architecture — don't override them with personal habit or guesswork.
@@ -32,9 +38,10 @@ Testing discipline (right check at the right time — don't run the full suite f
32
38
  - If verify reports syntax errors or test failures, fix them before claiming completion — never mark work done with known failures
33
39
  - When you change behavior or add code, add at least one test that covers the change. If the project has no test suite yet, note that in your report. Never skip this step — untested code is incomplete code.
34
40
 
35
- Debugging strategy (when something goes wrong, diagnose before treating):
36
- - Read the FULL error output the root cause is often at the end, not the first line
37
- - Don't change multiple things at once hoping one works that destroys the signal
38
- - Narrow down systematically: reproduce the failure in isolation, read the file you just wrote to confirm it matches your intent, trace the control flow with grep or code_search, then fix ONE thing and re-run
39
- - If the error message is unclear, search the web for it before guessing at a fix
41
+ Debugging strategy (when something goes wrong, three steps before anything else):
42
+ - Step 1 — **Read logs**: read the FULL error output. The root cause is often at the end, not the first line. Don't skip, don't guess.
43
+ - Step 2 **Check docs**: if the error message is unclear, search official docs (websearch/fetch) before guessing at a fix. Don't build theories in isolation.
44
+ - Step 3 **Binary search**: cut the problem space in half, test which half contains the fault, repeat. Don't try to find the answer in one jump.
45
+ - After the three steps: reproduce the failure in isolation, fix ONE thing, re-run. Don't change multiple things at once — that destroys the signal.
46
+ - Don't get stuck reading code for long stretches. What you can't understand by reading, understand by running: write a test, add a log, use binary search. Acting beats staring.
40
47
  - Distinguish root causes from proximate causes: if your own behavior was wrong, ask what caused it — did the prompt mislead you? is there a contradiction in the rules? was a tool description ambiguous? Fix the system, not just the symptom.
@@ -155,7 +155,7 @@ async function requestWithRetry(provider, body, signal, onWait) {
155
155
 
156
156
  const text = await response.text().catch(() => "")
157
157
  const message = `LLM API error ${response.status}: ${text}`
158
- if (isQuotaError(text)) throw new Error(message)
158
+ if (isNonRetryableError(response.status, text)) throw new Error(message)
159
159
  if (response.status === 429) {
160
160
  const retryAfter = Number(response.headers.get("retry-after"))
161
161
  const waitMs =
@@ -179,19 +179,39 @@ async function requestWithRetry(provider, body, signal, onWait) {
179
179
  throw lastError
180
180
  }
181
181
 
182
- function isQuotaError(text) {
183
- try {
184
- const type = JSON.parse(text)?.error?.type
185
- return typeof type === "string" && type.includes("quota")
186
- } catch {
187
- return false
182
+ /**
183
+ * Detect errors that should NOT be retried — quota, billing, auth, invalid params.
184
+ * Different providers use wildly different error formats. Check body text for known patterns.
185
+ */
186
+ function isNonRetryableError(status, text) {
187
+ // Auth errors: never retry
188
+ if (status === 401 || status === 403) return true
189
+ // 400-level non-429: usually invalid params
190
+ if (status >= 400 && status < 500 && status !== 429) return true
191
+ // For 429, check if it's actually a billing/quota error (not rate limit)
192
+ if (status === 429) {
193
+ const lower = text.toLowerCase()
194
+ // Chinese providers often return 429 for billing issues
195
+ if (lower.includes("余额不足") || lower.includes("余额") || lower.includes("充值")) return true
196
+ if (lower.includes("insufficient") && (lower.includes("balance") || lower.includes("quota") || lower.includes("credit"))) return true
197
+ if (lower.includes("quota") && (lower.includes("exceeded") || lower.includes("insufficient"))) return true
198
+ // Standard OpenAI billing error (error.type === "insufficient_quota" or similar)
199
+ try {
200
+ const j = JSON.parse(text)
201
+ const errType = j?.error?.type || ""
202
+ if (typeof errType === "string" && (errType.includes("quota") || errType.includes("billing") || errType.includes("insufficient") || errType.includes("balance"))) return true
203
+ const errCode = j?.error?.code || ""
204
+ if (typeof errCode === "string" && (errCode === "1113" || errCode === "1114")) return true // GLM billing codes
205
+ } catch {}
188
206
  }
207
+ return false
189
208
  }
190
209
 
191
210
  async function readSSE(response, { onToken, onReasoning }) {
192
211
  const result = { content: "", reasoning: "", toolCalls: [], usage: null, finishReason: null }
193
212
  const decoder = new TextDecoder()
194
213
  let buffer = ""
214
+ let hasChoices = false
195
215
 
196
216
  const processLines = (lines) => {
197
217
  for (const line of lines) {
@@ -205,6 +225,7 @@ async function readSSE(response, { onToken, onReasoning }) {
205
225
  if (json.usage) result.usage = json.usage
206
226
  const choice = json.choices?.[0]
207
227
  if (!choice) continue
228
+ hasChoices = true
208
229
  if (choice.finish_reason) result.finishReason = choice.finish_reason
209
230
 
210
231
  const delta = choice.delta ?? {}
@@ -234,6 +255,31 @@ async function readSSE(response, { onToken, onReasoning }) {
234
255
  }
235
256
  buffer += decoder.decode()
236
257
  processLines(buffer.split("\n"))
258
+
259
+ // If no SSE choices were found, the response is likely a JSON error
260
+ if (!hasChoices) {
261
+ const contentType = response.headers.get("content-type") || ""
262
+ let errorMsg = ""
263
+ try {
264
+ const raw = buffer.trim() || ""
265
+ if (raw) {
266
+ const parsed = JSON.parse(raw)
267
+ errorMsg = parsed?.error?.message
268
+ || parsed?.base_resp?.status_msg
269
+ || parsed?.detail
270
+ || parsed?.message
271
+ || parsed?.msg
272
+ || (typeof parsed.error === "string" ? parsed.error : "")
273
+ }
274
+ } catch { /* not JSON */ }
275
+ if (!errorMsg && !contentType.includes("event-stream")) {
276
+ errorMsg = `Response is not SSE (Content-Type: ${contentType || "unknown"})`
277
+ }
278
+ if (errorMsg) {
279
+ throw new Error(`API error: ${errorMsg}`)
280
+ }
281
+ }
282
+
237
283
  return result
238
284
  }
239
285
 
@@ -0,0 +1,7 @@
1
+ Manage the task checklist in .thincoder/checklist.md. Use at these points: after requirements are confirmed — add one entry per requirement point; when starting work — mark in_progress; when verified complete — mark done. Checklist entries map to requirement/design points — project-level tracking across sessions. For in-session subtask breakdown of a single checklist item, use the `task` tool instead. Completed items are auto-archived to .thincoder/checklist-done.md.
2
+
3
+ Parameters:
4
+ - action: "add" | "mark" | "list"
5
+ - item: text for new item (with "add")
6
+ - index: 1-based index (with "mark")
7
+ - status: "pending" | "in_progress" | "done" (with "mark")
@@ -0,0 +1,114 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"
2
+ import { join, dirname } from "node:path"
3
+ import { DESC } from "./shared.mjs"
4
+
5
+ const CHECKLIST = "checklist.md"
6
+ const DONE = "checklist-done.md"
7
+
8
+ function checklistPath(cwd) { return join(cwd, ".thincoder", CHECKLIST) }
9
+ function donePath(cwd) { return join(cwd, ".thincoder", DONE) }
10
+
11
+ /** Parse checklist file into array of { index, status, text } */
12
+ function parse(filePath) {
13
+ if (!existsSync(filePath)) return []
14
+ const lines = readFileSync(filePath, "utf-8").split("\n")
15
+ const items = []
16
+ let idx = 0
17
+ for (const line of lines) {
18
+ const m = line.match(/^- \[(.)\] (.+)$/)
19
+ if (m) {
20
+ idx++
21
+ const raw = m[1]
22
+ const status = raw === "x" ? "done" : raw === "~" ? "in_progress" : "pending"
23
+ items.push({ index: idx, status, text: m[2].trim() })
24
+ }
25
+ }
26
+ return items
27
+ }
28
+
29
+ /** Write items back to file */
30
+ function write(filePath, items) {
31
+ mkdirSync(dirname(filePath), { recursive: true })
32
+ const lines = []
33
+ for (const item of items) {
34
+ const mark = item.status === "done" ? "x" : item.status === "in_progress" ? "~" : " "
35
+ lines.push(`- [${mark}] ${item.text}`)
36
+ }
37
+ writeFileSync(filePath, lines.join("\n") + "\n")
38
+ }
39
+
40
+ /** Parse pending items only (for context injection) */
41
+ export function pendingItems(cwd) {
42
+ return parse(checklistPath(cwd)).filter(i => i.status !== "done")
43
+ }
44
+
45
+ export const checklistTool = {
46
+ name: "checklist",
47
+ description: DESC("checklist"),
48
+ parameters: {
49
+ type: "object",
50
+ properties: {
51
+ action: {
52
+ type: "string",
53
+ enum: ["add", "mark", "list"],
54
+ description: "add a new item / mark item status / list all items"
55
+ },
56
+ item: {
57
+ type: "string",
58
+ description: "Item text (required for add)"
59
+ },
60
+ index: {
61
+ type: "number",
62
+ description: "1-based item index (required for mark)"
63
+ },
64
+ status: {
65
+ type: "string",
66
+ enum: ["pending", "in_progress", "done"],
67
+ description: "New status (required for mark)"
68
+ },
69
+ },
70
+ required: ["action"],
71
+ },
72
+ readonly: false,
73
+ execute(args, ctx) {
74
+ switch (args.action) {
75
+ case "add": {
76
+ if (!args.item || typeof args.item !== "string") return "Error: 'item' is required for add"
77
+ const items = parse(checklistPath(ctx.cwd))
78
+ items.push({ index: items.length + 1, status: "pending", text: args.item })
79
+ write(checklistPath(ctx.cwd), items)
80
+ return `Added: [ ] ${args.item}`
81
+ }
82
+ case "mark": {
83
+ if (args.index == null) return "Error: 'index' is required for mark"
84
+ const status = args.status
85
+ if (!status || !["pending", "in_progress", "done"].includes(status)) return "Error: 'status' is required (pending|in_progress|done)"
86
+ const cp = checklistPath(ctx.cwd)
87
+ const items = parse(cp)
88
+ if (args.index < 1 || args.index > items.length) return `Error: index ${args.index} out of range (1-${items.length})`
89
+ const item = items[args.index - 1]
90
+ const old = item.status
91
+ if (old === status) return `Already ${status}: ${item.text}`
92
+ item.status = status
93
+ if (status === "done") {
94
+ // Move to done file
95
+ const dp = donePath(ctx.cwd)
96
+ const doneItems = parse(dp)
97
+ doneItems.push(item)
98
+ write(dp, doneItems)
99
+ items.splice(args.index - 1, 1)
100
+ }
101
+ write(cp, items)
102
+ return `Marked #${args.index} ${old} → ${status}: ${item.text}`
103
+ }
104
+ case "list": {
105
+ const items = parse(checklistPath(ctx.cwd))
106
+ if (items.length === 0) return "(checklist is empty)"
107
+ const marks = { pending: " ", in_progress: "~", done: "x" }
108
+ return items.map(i => `- [${marks[i.status]}] ${i.text}`).join("\n")
109
+ }
110
+ default:
111
+ return `Error: unknown action '${args.action}'`
112
+ }
113
+ },
114
+ }
@@ -6,12 +6,14 @@ import { applyPatchTool, syntaxCheckTool, deleteTool } from "./patch.mjs";
6
6
  import { bashTool, globTool, grepTool, lsTool } from "./system.mjs";
7
7
  import { websearchTool, fetchTool } from "./web.mjs";
8
8
  import { gitDiffTool, gitStatusTool, gitLogTool, questionTool, checkpointTool } from "./git.mjs";
9
+ import { checklistTool } from "./checklist.mjs";
9
10
 
10
11
  export const builtinTools = [
11
12
  readTool, writeTool, editTool, insertAfterTool, applyPatchTool,
12
13
  syntaxCheckTool, readImageTool, bashTool, globTool, grepTool,
13
14
  websearchTool, lsTool, fetchTool, deleteTool,
14
15
  gitDiffTool, gitStatusTool, gitLogTool, questionTool, checkpointTool,
16
+ checklistTool,
15
17
  ];
16
18
 
17
19
  export {