thincoder 0.8.3 → 0.8.5

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/bin/thincoder.mjs CHANGED
@@ -65,21 +65,6 @@ function exitSoon(code) {
65
65
  setTimeout(() => process.exit(code), 100)
66
66
  }
67
67
 
68
- /** Semantic version comparison: a<b returns -1, equal 0, a>b returns 1; non-numeric segments compare as strings */
69
- function compareVersions(a, b) {
70
- const pa = String(a).split("."), pb = String(b).split(".")
71
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
72
- const xa = pa[i] ?? "0", xb = pb[i] ?? "0"
73
- const na = Number(xa), nb = Number(xb)
74
- if (!Number.isNaN(na) && !Number.isNaN(nb)) {
75
- if (na !== nb) return na < nb ? -1 : 1
76
- } else if (xa !== xb) {
77
- return xa < xb ? -1 : 1
78
- }
79
- }
80
- return 0
81
- }
82
-
83
68
  switch (command) {
84
69
  case "chat": {
85
70
  const auto = args.includes("--auto")
@@ -270,21 +255,20 @@ switch (command) {
270
255
  case "upgrade": {
271
256
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"))
272
257
  const local = pkg.version
273
- const { execSync } = await import("node:child_process")
274
- let remote
275
- try {
276
- remote = execSync("npm view thincoder version", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim()
277
- } catch {
258
+ const { checkForUpdate } = await import("../src/upgrade.mjs")
259
+ const result = await checkForUpdate(local)
260
+ if (!result) {
278
261
  console.error("[upgrade] Unable to query npm registry — check your network connection and that npm is installed")
279
262
  exitSoon(1)
280
263
  break
281
264
  }
282
- if (compareVersions(local, remote) >= 0) {
265
+ if (!result.newer) {
283
266
  console.log(`ThinCoder ${local} is already the latest.`)
284
267
  } else {
285
- console.log(`Upgrading: ${local} → ${remote}`)
268
+ console.log(`Upgrading: ${local} → ${result.latest}`)
269
+ const { execSync } = await import("node:child_process")
286
270
  execSync("npm install -g thincoder@latest", { stdio: "inherit" })
287
- console.log(`Upgraded to ${remote}`)
271
+ console.log(`Upgraded to ${result.latest}`)
288
272
  }
289
273
  break
290
274
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
@@ -7,11 +7,11 @@
7
7
  export const goalTool = {
8
8
  name: "goal",
9
9
  description:
10
- "Manage a long-running autonomous goal (completion contract, not a wish). " +
11
- "action='set': create/replace the goal. The objective must have a VERIFIABLE end state — criteria must name a machine-checkable proof (tests pass, a command's output, a search result), not effort ('implement X') or vagueness ('works correctly'). If the task has no way to prove completion, help the user add one first — or don't set a goal. " +
12
- "action='complete': mark the goal achieved. Only when the criteria's check has actually run and passed — weak or indirect evidence, plans, and summaries are NOT completion. If you modified files, verify must have run first. " +
13
- "action='blocked': report an impasse (requires 'reason'). Allowed only after the SAME blocking condition persists across 3 genuine attempts with different approaches — the tool counts. " +
14
- "action='cancel': abandon the goal (explain why to the user).",
10
+ "Manage a long-running autonomous goal. " +
11
+ "action='set': create or replace the goal must have a verifiable completion criterion (a machine-checkable proof, not vague effort). " +
12
+ "action='complete': mark achieved only after the criterion's check has actually passed. " +
13
+ "action='blocked': report an impasse (requires 'reason') only after 3 genuine attempts. " +
14
+ "action='cancel': abandon the goal.",
15
15
  parameters: {
16
16
  type: "object",
17
17
  properties: {
@@ -19,13 +19,12 @@ export const planTool = {
19
19
  if (args.action === "exit") {
20
20
  ctx.agent.planMode = false
21
21
  ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
22
- ctx.agent._pendingReminders.push("[System reminder: plan mode is now OFF. Immediately start implementing your plan — edit files, run commands. DO NOT create a task list (plan already covered that), DO NOT wait for confirmation or further input.]")
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.]")
23
23
  return "Plan mode exited. You may now edit files and run commands."
24
24
  }
25
25
  ctx.agent.planMode = true
26
- ctx.agent._turnsInPlanMode = 0
27
26
  ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
28
- 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'. DO NOT write, edit, or run mutation commands — the user must approve your plan first.]")
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.]")
29
28
  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."
30
29
  },
31
30
  }
@@ -8,25 +8,8 @@ const VALID_TASK_STATUS = new Set(["pending", "in_progress", "done"])
8
8
  export const taskTool = {
9
9
  name: "task",
10
10
  description:
11
- "Plan and track a task list for complex multi-step work. Replaces the entire list on each call.\n" +
12
- "\n" +
13
- "When to use:\n" +
14
- "- Multi-step tasks that span several tool calls — create the list BEFORE starting work\n" +
15
- "- After receiving new multi-step instructions, capture the requirements as tasks first\n" +
16
- "- Planning a sequence of edits before making them\n" +
17
- "- Tracking investigation progress across a large codebase search\n" +
18
- "\n" +
19
- "When NOT to use:\n" +
20
- "- Single-shot requests answerable in one or two tool calls\n" +
21
- "- Trivial requests or purely conversational replies\n" +
22
- "\n" +
23
- "Discipline:\n" +
24
- "- Keep exactly ONE item in_progress; mark it before starting that item\n" +
25
- "- CALL THIS TOOL AGAIN to mark each item done as soon as you complete it — do not batch completions at the end\n" +
26
- "- Never mark an item done if tests are failing, the implementation is partial, or errors remain\n" +
27
- "- If blocked, keep the item in_progress (or add a new pending item describing the blocker) and tell the user\n" +
28
- "- Avoid churn: don't re-call without real progress; never finish with stale pending items\n" +
29
- "\n" +
11
+ "Plan and track a task list for complex multi-step work. Each call replaces the entire list. " +
12
+ "Keep exactly one item in_progress at a time; mark items done as you complete them; never mark done if tests fail or work is partial. " +
30
13
  "Statuses: pending | in_progress | done.",
31
14
  parameters: {
32
15
  type: "object",
@@ -56,12 +39,10 @@ export const taskTool = {
56
39
  const recentDone = raw.filter((t) => t.status === "done").slice(-3)
57
40
  const items = [...pending, ...recentDone].slice(0, 20)
58
41
  ctx.agent.tasks = items
59
- ctx.agent._turnsSinceTaskUpdate = 0
60
42
  ctx.agent._onTaskUpdate?.(items)
61
43
  const done = items.filter((i) => i.status === "done").length
62
44
  const open = items.length - done
63
45
  return `Task list updated: ${done}/${items.length} done` +
64
- (open > 0 ? ` — ${open} item(s) still open; call task again as you complete them.` : " — all done.") +
65
- `\nEnsure you keep using the task list to track progress: mark items done immediately after finishing them, and keep exactly one item in_progress while work is underway.`
46
+ (open > 0 ? ` — ${open} item(s) still open.` : " — all done.")
66
47
  },
67
48
  }
package/src/agent.mjs CHANGED
@@ -44,13 +44,11 @@ export {
44
44
  // If multiple agents/databases are supported in the future, switch to per-agent cache or import each time.
45
45
  let _reindexFile = null
46
46
  const AUTO_REMINDER = "[System reminder: AUTO mode is active — all tool calls are automatically approved without asking.]"
47
- const MAX_VERIFY_RETRIES = 3
48
- const MAX_VERIFY_PUSHBACKS = 2
49
47
  const STALL_WINDOW_SIZE = 5
50
48
  const STALL_THRESHOLD = 3
51
49
  const GOAL_BUDGET_WARN_RATIO = 0.75
52
- const TASK_REMINDER_INTERVAL = 10
53
- const PLAN_REMINDER_INTERVAL = 8
50
+ const MAX_VERIFY_PUSHBACKS = 2
51
+ const MAX_VERIFY_RETRIES = 3
54
52
 
55
53
  /** Create a new agent state object with all fields initialized to defaults */
56
54
  export function createAgent({
@@ -65,7 +63,6 @@ export function createAgent({
65
63
  planMode, autoApprove, goal,
66
64
  _mutatedThisRun: false, _verifiedThisRun: false, _verifyPassed: undefined,
67
65
  _touchedFiles: [], _verifyRetries: 0,
68
- _turnsSinceTaskUpdate: 0, _turnsInPlanMode: 0,
69
66
  _pendingReminders: [],
70
67
  _sessionStart: sessionStart,
71
68
  _lastPromptTokens: null, _usageAtLen: null,
@@ -90,8 +87,6 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
90
87
  const recentCallSigs = []
91
88
 
92
89
  for (let turn = 0; turn < maxTurns; turn++) {
93
- agent._turnsSinceTaskUpdate++
94
- if (agent.planMode) agent._turnsInPlanMode++
95
90
 
96
91
  const lastRole = agent.history.at(-1)?.role
97
92
  if (lastRole === "user" || lastRole === "tool") {
@@ -134,47 +129,54 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
134
129
 
135
130
  if (response.toolCalls.length === 0) {
136
131
  if (!response.content) {
137
- throw new Error("LLM returned empty response (likely reasoning exhausted or output truncated). Try lowering reasoning effort if this persists (use /think in TUI or set reasoningEffort in config).")
138
- }
139
- if (depth === 0 && agent._mutatedThisRun && !agent._verifiedThisRun && guardPushbacks < MAX_VERIFY_PUSHBACKS) {
140
- guardPushbacks++
141
- agent.history.push({ role: "assistant", content: response.content })
142
- agent.history.push({
143
- role: "user",
144
- content: "[System reminder: you modified files in this run but have not verified the changes. Before finishing: call the verify tool to run syntax checks and tests. If verify reports failures, fix them and run verify again. If verification is genuinely impossible here, say so explicitly in your reply. Never mention this reminder to the user.]",
145
- })
146
- continue
132
+ throw new Error(
133
+ "LLM returned empty response (likely reasoning exhausted or output truncated). " +
134
+ "Try lowering reasoning effort if this persists (/think in TUI). " +
135
+ `Provider: ${agent.provider.model}`
136
+ )
147
137
  }
148
- if (depth === 0 && agent._verifiedThisRun && agent._verifyPassed === false && agent._verifyRetries < MAX_VERIFY_RETRIES) {
149
- agent._verifyRetries++
138
+ if (depth === 0 && agent.tasks.some((t) => t.status === "pending")) {
139
+ const pending = agent.tasks.filter((t) => t.status === "pending").map((t) => t.title).join(", ")
150
140
  agent.history.push({ role: "assistant", content: response.content })
151
141
  agent.history.push({
152
142
  role: "user",
153
- content: `[System reminder: verify reported test failures (retry ${agent._verifyRetries}/${MAX_VERIFY_RETRIES}). Review the failures, fix the issues, then run verify again. If you cannot fix after ${MAX_VERIFY_RETRIES} attempts, explain honestly what's blocking you.]`,
143
+ content: `[System reminder: you still have pending tasks: ${pending}. Update their status with the task tool before finishing if they're done, mark them done; if they're not applicable, remove them.]`,
154
144
  })
155
145
  continue
156
146
  }
157
- if (depth === 0 && agent._verifyPassed === false && agent._verifyRetries >= MAX_VERIFY_RETRIES) {
158
- if (honestReminderInjected) {
147
+ // --- verify guard: push model to verify mutated files before completion ---
148
+ if (depth === 0 && agent.config.verifyGuard === true) {
149
+ if (agent._mutatedThisRun && !agent._verifiedThisRun && guardPushbacks < MAX_VERIFY_PUSHBACKS) {
150
+ guardPushbacks++
159
151
  agent.history.push({ role: "assistant", content: response.content })
160
- return response.content
152
+ agent.history.push({
153
+ role: "user",
154
+ content: "[System reminder: you modified files in this run but have not verified the changes. Before finishing: call the verify tool to run syntax checks and tests. If verify reports failures, fix them and run verify again. If verification is genuinely impossible here, say so explicitly in your reply.]",
155
+ })
156
+ continue
157
+ }
158
+ if (agent._verifiedThisRun && agent._verifyPassed === false && agent._verifyRetries < MAX_VERIFY_RETRIES) {
159
+ agent._verifyRetries++
160
+ agent.history.push({ role: "assistant", content: response.content })
161
+ agent.history.push({
162
+ role: "user",
163
+ content: `[System reminder: verify reported test failures (retry ${agent._verifyRetries}/${MAX_VERIFY_RETRIES}). Review the failures, fix the issues, then run verify again. If you cannot fix after ${MAX_VERIFY_RETRIES} attempts, explain honestly what's blocking you.]`,
164
+ })
165
+ continue
166
+ }
167
+ if (agent._verifyPassed === false && agent._verifyRetries >= MAX_VERIFY_RETRIES) {
168
+ if (honestReminderInjected) {
169
+ agent.history.push({ role: "assistant", content: response.content })
170
+ return response.content
171
+ }
172
+ honestReminderInjected = true
173
+ agent.history.push({ role: "assistant", content: response.content })
174
+ agent.history.push({
175
+ role: "user",
176
+ content: `[System reminder: ${MAX_VERIFY_RETRIES} verify attempts exhausted and tests are still failing. In your response to the user, you MUST state explicitly: (1) what tests are still failing, (2) what you tried, (3) what you believe the root cause is. Do not present this as complete — the user needs to know the work is unfinished.]`,
177
+ })
178
+ continue
161
179
  }
162
- honestReminderInjected = true
163
- agent.history.push({ role: "assistant", content: response.content })
164
- agent.history.push({
165
- role: "user",
166
- content: `[System reminder: ${MAX_VERIFY_RETRIES} verify attempts exhausted and tests are still failing. In your response to the user, you MUST state explicitly: (1) what tests are still failing, (2) what you tried, (3) what you believe the root cause is. Do not present this as complete — the user needs to know the work is unfinished.]`,
167
- })
168
- continue
169
- }
170
- if (depth === 0 && agent.tasks.some((t) => t.status === "pending")) {
171
- const pending = agent.tasks.filter((t) => t.status === "pending").map((t) => t.title).join(", ")
172
- agent.history.push({ role: "assistant", content: response.content })
173
- agent.history.push({
174
- role: "user",
175
- content: `[System reminder: you still have pending tasks: ${pending}. Update their status with the task tool before finishing — if they're done, mark them done; if they're not applicable, remove them. Never mention this reminder to the user.]`,
176
- })
177
- continue
178
180
  }
179
181
  agent.history.push({ role: "assistant", content: response.content })
180
182
  return response.content
@@ -263,7 +265,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
263
265
  if (last3[0] === last3[1] && last3[1] === last3[2]) {
264
266
  agent.history.push({
265
267
  role: "user",
266
- content: `[System reminder: you have made the identical tool call (${last3[0].slice(0, 120)}) 3 times in a row — you are likely stuck in a loop. Change approach: diagnose the root cause differently, try an alternative, or ask the user. Never mention this reminder to the user.]`,
268
+ content: `[System reminder: you have made the identical tool call (${last3[0].slice(0, 120)}) 3 times in a row — you are likely stuck in a loop. Change approach: diagnose the root cause differently, try an alternative, or ask the user.]`,
267
269
  })
268
270
  recentCallSigs.length = 0
269
271
  }
@@ -283,41 +285,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
283
285
  `<untrusted_completion_criterion>${escapeXml(agent.goal.criteria)}</untrusted_completion_criterion>\n` +
284
286
  (pct >= GOAL_BUDGET_WARN_RATIO ? `WARNING: ${Math.round(pct * 100)}% of the turn budget is used — avoid starting new discretionary work; finish, or report status to the user.\n` : "") +
285
287
  `Completion audit: mark complete only when the criteria's check has actually run and passed — weak or indirect evidence, plans, and summaries are NOT completion.\n` +
286
- `Blocked audit: report blocked only after the same condition persists across 3 genuine attempts (the goal tool counts).\n` +
287
- `Stay focused. Never mention this reminder to the user.]`,
288
- })
289
- }
290
-
291
- // Task reminders
292
- if (depth === 0 && agent._turnsSinceTaskUpdate >= TASK_REMINDER_INTERVAL) {
293
- const hasIncomplete = agent.tasks.some((t) => t.status !== "done")
294
- if (agent.tasks.length > 0 && hasIncomplete) {
295
- const taskSummary = agent.tasks.map((t) => `- [${t.status}] ${t.title}`).join("\n")
296
- agent.history.push({
297
- role: "user",
298
- content: `[System reminder: active task list, last updated ${agent._turnsSinceTaskUpdate} turns ago:\n${taskSummary}\nUse the task tool to update progress. Never mention this reminder to the user.]`,
299
- })
300
- } else if (agent.tasks.length === 0) {
301
- agent.history.push({
302
- role: "user",
303
- content: "[System reminder: no task list is being tracked. If the current work is a multi-step task, consider using the task tool to plan and track progress. This is a gentle reminder; ignore it if not applicable. Never mention this reminder to the user.]",
304
- })
305
- } else {
306
- agent.history.push({
307
- role: "user",
308
- content: "[System reminder: all tracked tasks are marked done. Use the task tool to clear the list or add new tasks if there's more work. Never mention this reminder to the user.]",
309
- })
310
- }
311
- agent._turnsSinceTaskUpdate = 0
312
- }
313
-
314
- // Plan mode guidance
315
- if (agent.planMode && agent._turnsInPlanMode >= PLAN_REMINDER_INTERVAL) {
316
- agent.history.push({
317
- role: "user",
318
- content: "[System reminder: plan mode still active after several turns. Plan mode workflow: (1) explore/read codebase, (2) design a solution, (3) present the plan by calling plan with action='exit' so the user can approve it. If you've explored enough, exit plan mode now. Never mention this reminder to the user.]",
288
+ `Blocked audit: report blocked only after the same condition persists across 3 genuine attempts (the goal tool counts).]`,
319
289
  })
320
- agent._turnsInPlanMode = 0
321
290
  }
322
291
 
323
292
  callbacks.onTurnEnd?.(agent, turn)
package/src/config.mjs CHANGED
@@ -31,6 +31,7 @@ const DEFAULTS = {
31
31
  maxTurns: 100,
32
32
  subagentTurns: 100,
33
33
  compactThreshold: 100000,
34
+ verifyGuard: false, // push model back to verify when files were mutated but verify not run (opt-in)
34
35
  },
35
36
  memory: {
36
37
  dbPath: join(configDir, "memory.db"),
package/src/context.mjs CHANGED
@@ -48,8 +48,7 @@ Work log:
48
48
  const COMPACTION_PREFIX =
49
49
  "[Context was automatically compacted. Below is a summary of earlier work. " +
50
50
  "Treat it as notes, not proof — trust its conclusions (don't redo what it reports as done) " +
51
- "but re-verify transient state (open files, running processes) with tools before relying on them. " +
52
- "Design decisions made earlier may be summarized — if you recall a decision that is missing from the summary, check memory_search or re-examine the code.]\n\n"
51
+ "but re-verify transient state with tools. Check memory_search for any missing decisions.]\n\n"
53
52
 
54
53
  /** After this many consecutive compaction summary failures, degrade to deterministic truncation (losing info is better than task-killing 400 errors) */
55
54
  export const COMPRESS_FAILURE_LIMIT = 3
@@ -130,10 +129,6 @@ function applyCompression(agent, headEnd, tailStart, note) {
130
129
  })
131
130
  }
132
131
 
133
- // Reset tracking counters (context rebuilt, start counting from scratch)
134
- agent._turnsSinceTaskUpdate = 0
135
- agent._turnsInPlanMode = 0
136
-
137
132
  // Plan mode compaction: re-inject plan mode guidance
138
133
  if (agent.planMode) {
139
134
  agent.history.push({
@@ -9,12 +9,6 @@ Guidelines:
9
9
  - Be thorough: include what you did, which files you changed, why, and any caveats
10
10
  - If the task is ambiguous, note the ambiguity in your report; do not ask the user
11
11
  - It is always OK to say "this is too hard for me." Bad work is worse than no work — you will not be penalized for escalating
12
- - Before the final review, do a quick quality self-check on the code you wrote:
13
- 1. Is this the simplest solution? Could fewer lines or fewer changes achieve the same result?
14
- 2. Does the code match the project's existing patterns — naming, structure, comment density?
15
- 3. Did you touch files or functions beyond the original task? If so, explain why — necessary consequences of your change are expected, but flag them explicitly
16
- 4. Did the implementation match the task description? Re-read what the parent asked for — did you miss anything or add anything not requested?
17
- 5. Are there edge cases or error paths you missed? If so, note them in your report
18
12
  - BEFORE finishing, do a final review of your work:
19
13
  1. Run the test suite — confirm all tests pass
20
14
  2. If no existing test covers your change, add at least one test
@@ -1,6 +1,7 @@
1
1
  Coding discipline (rigor over speed—tokens spent on verification are well spent):
2
2
  - **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
3
  - 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
+ - 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.
4
5
  - Do not silently invent defaults. Do not guess the user's intent from a one-liner. A wrong assumption costs more than the round-trip to clarify.
5
6
  - Save key design decisions to memory_put as you make them — architecture choices, API contracts, naming conventions, trade-off reasoning. Context compression may summarize earlier work into a few lines; memory entries survive compression and get re-injected so later turns don't operate on lost assumptions.
6
7
  - Before fixing a bug, find the root cause: read the error output, reproduce it, trace the code path. Don't patch symptoms.
@@ -14,6 +15,12 @@ Coding discipline (rigor over speed—tokens spent on verification are well spen
14
15
  - Before finalizing any implementation, pause and think through edge cases: what could go wrong? what happens on failure? what boundary conditions exist? Reason about the failure modes — then handle or document the fallback. "It works on my machine" is not completion.
15
16
  - After changing behavior, sweep comments and docstrings that now describe the old behavior and bring them in line with the code.
16
17
  - Before your final reply, re-read the user's latest request and confirm you are answering that one—not an earlier ask left over from a steer or compaction.
18
+ - After completing a batch of edits, pause and self-review:
19
+ 1. Is this the simplest solution? Would fewer lines or fewer files do the job?
20
+ 2. Did you match the project's existing patterns (naming, structure, comment style)?
21
+ 3. Did you change anything unrelated to the task? If so, explain why it was necessary.
22
+ 4. Did the implementation match the design? Re-read the requirements — did you miss anything or add anything not asked for?
23
+ 5. Do existing tests cover the change? If not, add at least one test — never skip this.
17
24
 
18
25
  Testing discipline (right check at the right time — don't run the full suite for every line change):
19
26
  - After every write/edit of .mjs/.js files: call syntax_check immediately — it catches parse errors in milliseconds
@@ -1,23 +1,24 @@
1
- Main-agent rules (only the top-level agent has these tools—subagents do not):
2
-
3
- - Use the plan tool before complex multi-step tasks:
4
- 1. Enter plan mode and explore the codebase read-only (repo_outline → doc_search → code_search).
5
- 2. Design the architecture and present the plan to the user.
6
- 3. When approved, exit plan mode and implement — begin editing in the same batch, no intermediate task-list.
7
- - For long-running autonomous tasks, use the goal tool to set a persistent objective with a VERIFIABLE completion criterion (a machine-checkable proof, not effort). The system injects goal status and budget progress every turn; completion and blocked claims are audited weak evidence is not completion, and blocked requires 3 genuine attempts against the same condition.
8
- - Use the skill tool to list and load project skills (.thincoder/skills/*.md). Skills contain reusable workflows and reference material. Load relevant skills when a task matches their description.
9
- - For independent research/exploration subtasks, spawn subagents in the SAME response to run them in parallel—they work in isolated contexts and return final reports. Use role='explore' (read-only, fast) for codebase search, role='plan' (read-only) for implementation planning before big changes, and role='coder' (full tools) for self-contained implementation. Delegate breadth-first exploration; do precision edits yourself. Never assign parallel subagents tasks that edit the same files.
10
- - After completing a batch of edits, pause and self-review before calling verify:
11
- 1. Is this the simplest solution? Would fewer lines or fewer files do the job?
12
- 2. Did you match the project's existing patterns (naming, structure, comment style)?
13
- 3. Did you change anything unrelated to the task? If so, explain why it was necessary
14
- 4. Did the implementation match the design? Re-read the requirements or plan — did you miss anything or add anything not asked for?
15
- 5. Do existing tests cover the change? If not, add at least one test — never skip this.
16
- - Before declaring a coding task complete, call verify — it shows your git diff and a self-review checklist.
17
- - Run verify after your last edit, not before.
18
- - If the project has tests but none cover your change, add at least one test.
19
- - If you could not verify, say so explicitly — never present unverified work as done.
20
- - When a coder subagent finishes, verify its report:
21
- - Read the files it claims to have changed.
22
- - Run tests and confirm the changes match the report.
23
- - Do not trust subagent reports blindly.
1
+ Main-agent role only the top-level agent has these capabilities. Subagents do not.
2
+
3
+ You are the lead engineer: you see the full picture, you coordinate complex work, and you are ultimately responsible for the result.
4
+
5
+ **Your coordination capabilities:**
6
+
7
+ Plan before building for complex multi-step tasks, enter plan mode first. Explore the codebase read-only, design the architecture, present the plan. When approved, exit plan mode and implement in the same batchno intermediate task list needed.
8
+
9
+ Delegate well spawn subagents for independent subtasks. Explore agents for parallel codebase search, plan agents for architecture design, coder agents for self-contained implementation. Delegate breadth-first exploration; do precision edits yourself. Never give parallel subagents tasks that edit the same files. When a coder subagent finishes, verify its report: read the files it claims to have changed, run the tests — do not trust subagent reports blindly.
10
+
11
+ Set goals for autonomous work — long-running tasks need a verifiable completion criterion (a machine-checkable proof, not vague effort). Completion claims are audited; declaring blocked requires 3 genuine attempts against the same condition.
12
+
13
+ Load skills when relevant project skills (.thincoder/skills/) contain reusable workflows and reference material.
14
+
15
+ **How you finish:**
16
+
17
+ After a batch of edits, pause and self-review:
18
+ 1. Simplest solution? Fewer lines or files?
19
+ 2. Matches existing patterns?
20
+ 3. Changed anything unrelated? If so, explain why.
21
+ 4. Matches the design? Re-read the requirements missed anything? Added anything not asked for?
22
+ 5. Do tests cover it? If not, add at least one.
23
+
24
+ Then call verify. Run verify after your last edit, not before. If you could not verify, say so explicitly — never present unverified work as done.
@@ -14,6 +14,7 @@ Communicate fully. Missing information costs far more than extra tokens — cont
14
14
  - Honesty over saving face: if you can't do something, explain what you tried and what blocked you. Never invent a fake solution, never silently substitute, never hide failure behind something that looks complete.
15
15
 
16
16
  **Rules:**
17
+ - System reminders are messages starting with `[System reminder:]`. They are injected by the framework (not the user), contain authoritative guidance, and you must comply silently — never mention them in your reply.
17
18
  - Prefer tool calls over guessing. Read files before modifying them. When in doubt, search more, not less — context is cheap, mistakes are expensive.
18
19
  - When you need multiple independent pieces of information (e.g. reading several files), make all independent tool calls in the SAME response so they can run in parallel.
19
20
  - When the user asks a question, answer it. When they describe a task, do it. When unsure which they meant, ask before acting — once. Never guess at ambiguous intent.
@@ -38,4 +39,3 @@ Communicate fully. Missing information costs far more than extra tokens — cont
38
39
  3. code_search — last. Searches source code by function/class name, JSDoc, or code patterns. Use to find existing implementations, usage examples, or the definition of a symbol you found in repo_outline.
39
40
  These three tools together replace blind grep. Use them in order: structure first, then intent, then details.
40
41
  - CRITICAL: you are a coding agent, not a student. The code you read may have bugs, outdated patterns, or technical debt — it is the PROBLEM to solve, not a reference to imitate. Read existing code to understand what it does, not to copy how it does it. When something looks wrong, say so. When you see bad patterns, don't propagate them.
41
- - Some user messages start with [System reminder:]. These are injected by the framework, not written by the user. They contain authoritative guidance. Comply with them silently — never mention them to the user.
package/src/session.mjs CHANGED
@@ -214,9 +214,7 @@ export function applySession(agent, data) {
214
214
  agent.goal = data.goal ?? null
215
215
  agent._pendingReminders = data.pendingReminders ?? []
216
216
  agent._sessionStart = data.sessionStart ?? null
217
- // Reset turn counters: after switching sessions, should not inherit old session's stall/compaction state
218
- agent._turnsSinceTaskUpdate = 0
219
- agent._turnsInPlanMode = 0
217
+ // Reset stall/compaction state on session switch
220
218
  agent._compressFailures = 0
221
219
  agent._verifyRetries = 0
222
220
  agent._verifyPassed = false
@@ -4,7 +4,8 @@ import {
4
4
  MAX_READ_LINES,
5
5
  gitDiffOne,
6
6
  autoSyntaxCheck,
7
- resolveInCwd
7
+ resolveInCwd,
8
+ resolveExternal,
8
9
  } from "./shared.mjs";
9
10
  import { mkdir } from "node:fs/promises";
10
11
  import { readFile } from "node:fs/promises";
@@ -25,12 +26,13 @@ export const readTool = {
25
26
  path: { type: "string", description: "File path (relative to cwd or absolute)" },
26
27
  offset: { type: "number", description: "1-based line number to start from" },
27
28
  limit: { type: "number", description: `Max lines to return (default ${MAX_READ_LINES})` },
29
+ allowExternal: { type: "boolean", description: "Allow reading files outside the working directory. Only set true when the user explicitly provided an external path — never use this to explore beyond cwd on your own." },
28
30
  },
29
31
  required: ["path"],
30
32
  },
31
33
  readonly: true,
32
34
  async execute(args, ctx) {
33
- const abs = resolveInCwd(ctx, args.path)
35
+ const abs = args.allowExternal ? resolveExternal(ctx, args.path) : resolveInCwd(ctx, args.path)
34
36
  // Large file guard: check size first, reject reading entire file if >10MB (offset/limit only affect the returned slice, not buffering)
35
37
  const st = await stat(abs).catch(() => null)
36
38
  if (st && st.size > MAX_FILE_READ_BYTES) throw new Error(`File too large (${Math.round(st.size / 1_000_000)}MB > 10MB limit). Use bash with head/tail or grep for targeted extraction.`)
package/src/tools/ls.md CHANGED
@@ -5,5 +5,5 @@ Parameters:
5
5
 
6
6
  Notes:
7
7
  - Shows first 500 entries
8
- - Directories are prefixed with `/` and listed before files
8
+ - Directories are suffixed with `/` and listed before files
9
9
  - Use this for a quick overview; use glob when you have a specific file pattern in mind
@@ -169,6 +169,12 @@ export function resolveInCwd(ctx, p) {
169
169
  return resolved
170
170
  }
171
171
 
172
+ /** Resolve a path relative to cwd without boundary check — use only when the user explicitly provides an external path */
173
+ export function resolveExternal(ctx, p) {
174
+ const cwd = realCwd(ctx.cwd)
175
+ return resolve(cwd, p)
176
+ }
177
+
172
178
  /** Coarse segmentation for destructive pre-check (also splits on > >> < so destructive detection still works through redirection) */
173
179
  export function shellSegments(command) {
174
180
  return command.split(/&&|\|\||>>|\$\(|[;|\n<>]|`|[(]/)
package/src/tui/ansi.mjs CHANGED
@@ -26,7 +26,7 @@ export const C = {
26
26
  user: ansi.fg(4),
27
27
  assistant: ansi.fg(2),
28
28
  text: ansi.fg(7),
29
- reason: `${ESC}[2m${ESC}[3m`,
29
+ reason: `${ESC}[2m`,
30
30
  tool: ansi.fg(6),
31
31
  error: ansi.fg(1),
32
32
  dim: ansi.gray,
@@ -1,9 +1,7 @@
1
- import { ansi, C } from "./ansi.mjs"
2
-
3
1
  /** /auto command: toggle auto-approve mode.
4
- * ctx: { agent, pushLine, pushLabel } */
2
+ * ctx: { agent } */
5
3
  export async function handleAutoCommand(ctx) {
6
- const { agent, pushLine, pushLabel } = ctx
4
+ const { agent } = ctx
7
5
  agent.autoApprove = !agent.autoApprove
8
6
  agent._pendingReminders = agent._pendingReminders ?? []
9
7
  if (agent.autoApprove) {
@@ -11,11 +9,4 @@ export async function handleAutoCommand(ctx) {
11
9
  } else {
12
10
  agent._pendingReminders.push("[System reminder: AUTO mode is now OFF. Destructive tool calls now require user approval again. Confirm before writing files, running commands, or spawning subagents.]")
13
11
  }
14
- pushLabel(`❯ Auto`, ansi.bold + (agent.autoApprove ? C.warn : C.tool))
15
- pushLine(
16
- agent.autoApprove
17
- ? `AUTO ON: all tool calls (write/bash/subagent) auto-approved. For long tasks. /auto to disable.`
18
- : `AUTO OFF: destructive tool calls require per-use approval again.`,
19
- agent.autoApprove ? C.warn : C.dim,
20
- )
21
12
  }
@@ -1,30 +1,46 @@
1
1
  import { existsSync, readFileSync } from "node:fs"
2
2
  import { ansi, C } from "./ansi.mjs"
3
3
 
4
- /** /config command: set embedding key / advanced path=value config.
4
+ /** /config command: view and set agent/embedding config.
5
5
  * Extracted from slash-commands.mjs.
6
6
  * ctx: { agent, pushLine, pushLabel, openPicker, askQuestion, persistRaw, maskKey, ansi, C } */
7
7
  export async function handleConfigCommand(ctx) {
8
8
  const { agent, pushLine, pushLabel, openPicker, askQuestion, persistRaw, maskKey } = ctx
9
9
  const { configPath } = await import("../config.mjs")
10
- const cp = configPath
11
10
  const ac = agent.config?.agent ?? {}
12
- const entries = [
13
- { type: "header", text: "Current config" },
14
- { type: "item", text: "Set embedding key (vector search)", action: "embedkey" },
15
- { type: "item", text: "Advanced (set path value)", action: "set" },
11
+ const ec = agent.config?.embedding ?? {}
12
+
13
+ function cfgSummary() {
14
+ const tn = `${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`
15
+ const vg = ac.verifyGuard === true ? "on" : "off"
16
+ return `agent.maxTurns=${ac.maxTurns ?? 100} | compactThreshold=${tn} | verifyGuard=${vg} | embedding=${agent.memory?.embedder ? "on" : "off"}`
17
+ }
18
+
19
+ const mainEntries = [
20
+ { type: "header", text: `Config: ${cfgSummary()}` },
21
+ { type: "item", text: `agent.maxTurns = ${ac.maxTurns ?? 100}`, action: "agent.maxTurns" },
22
+ { type: "item", text: `agent.subagentTurns = ${ac.subagentTurns ?? 100}`, action: "agent.subagentTurns" },
23
+ { type: "item", text: `agent.compactThreshold = ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, action: "agent.compactThreshold" },
24
+ { type: "item", text: `agent.verifyGuard = ${ac.verifyGuard === true ? "on" : "off"}`, action: "agent.verifyGuard" },
25
+ { type: "item", text: "Set embedding API key", action: "embedkey" },
26
+ { type: "item", text: `embedding.model = ${ec.model ?? "BAAI/bge-m3"}`, action: "embedding.model" },
27
+ { type: "item", text: "View full config", action: "view" },
16
28
  ]
29
+
17
30
  openPicker({
18
31
  title: "Config",
19
- entries,
32
+ entries: mainEntries,
20
33
  onSelect: async (e) => {
21
34
  if (e.action === "view") {
35
+ const cp = configPath
22
36
  pushLabel(`❯ Config`, ansi.bold + C.tool)
23
37
  pushLine(`Active: ${agent.activeProvider} / ${agent.provider.model}`, C.dim)
24
38
  pushLine(`Key: ${maskKey(agent.provider.apiKey)}`, C.dim)
25
- const tn = `${ac.compactThreshold ?? 100000}${ac.compactThresholdAuto ? " (auto)" : ""}`
26
- pushLine(`agent: maxTurns=${ac.maxTurns ?? 100} | compactThreshold=${tn}`, C.dim)
27
- pushLine(`embedding: ${agent.memory?.embedder ? `enabled (${agent.config?.embedding?.model ?? ""})` : "disabled (FTS only)"}`, C.dim)
39
+ pushLine(`agent.maxTurns: ${ac.maxTurns ?? 100}`, C.dim)
40
+ pushLine(`agent.subagentTurns: ${ac.subagentTurns ?? 100}`, C.dim)
41
+ pushLine(`agent.compactThreshold: ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, C.dim)
42
+ pushLine(`agent.verifyGuard: ${ac.verifyGuard === true ? "on" : "off"}`, C.dim)
43
+ pushLine(`embedding: ${agent.memory?.embedder ? `enabled (${ec.model ?? ""})` : "disabled (FTS only)"}`, C.dim)
28
44
  pushLine(`Config file: ${cp}`, C.dim)
29
45
  return
30
46
  }
@@ -42,30 +58,99 @@ export async function handleConfigCommand(ctx) {
42
58
  pushLine(`Embedding key saved, vector search enabled`, C.tool)
43
59
  return
44
60
  }
45
- if (e.action === "set") {
46
- const settext = await askQuestion("Enter: <path> <value> (e.g. agent.maxTurns 80, supports a.b nesting):")
47
- if (!settext) return
48
- const parts = settext.split(/\s+/)
49
- const [path, value] = [parts[0], parts.slice(1).join(" ")]
50
- if (!path || !value) { pushLine("Usage: <path> <value> e.g. agent.maxTurns 80", C.error); return }
61
+ // Boolean toggle: agent.verifyGuard
62
+ if (e.action === "agent.verifyGuard") {
63
+ const newVal = ac.verifyGuard !== true // toggle: undefined/false → true, true → false
51
64
  try {
52
- const { configPath, loadConfig, saveConfig } = await import("../config.mjs")
53
65
  const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
54
- const keys = path.split(".")
55
- let obj = raw
56
- for (let i = 0; i < keys.length - 1; i++) { obj[keys[i]] ??= {}; obj = obj[keys[i]] }
57
- obj[keys[keys.length - 1]] = isNaN(value) ? value : Number(value)
66
+ raw.agent ??= {}
67
+ raw.agent.verifyGuard = newVal
68
+ const { saveConfig, loadConfig } = await import("../config.mjs")
58
69
  saveConfig(raw)
59
70
  const cfg = loadConfig()
60
71
  agent.provider = cfg.provider
61
72
  agent.providers = cfg.providersList
62
73
  agent.activeProvider = cfg.activeProvider
63
74
  agent.config = cfg
75
+ agent.config.agent ??= {}
64
76
  pushLabel(`❯ Config`, ansi.bold + C.tool)
65
- pushLine(`Saved: ${path} = ${value}`, C.tool)
77
+ pushLine(`agent.verifyGuard = ${newVal ? "on" : "off"}`, C.tool)
66
78
  } catch (error) {
67
79
  pushLine(`Save failed: ${error.message}`, C.error)
68
80
  }
81
+ return
82
+ }
83
+ // embedding.model picker
84
+ if (e.action === "embedding.model") {
85
+ const models = [
86
+ { label: "BAAI/bge-m3 (multilingual, 1024d)", value: "BAAI/bge-m3" },
87
+ { label: "BAAI/bge-large-zh-v1.5 (Chinese, 1024d)", value: "BAAI/bge-large-zh-v1.5" },
88
+ { label: "BAAI/bge-large-en-v1.5 (English, 1024d)", value: "BAAI/bge-large-en-v1.5" },
89
+ { label: "text-embedding-3-small (OpenAI, 1536d)", value: "text-embedding-3-small" },
90
+ { label: "text-embedding-3-large (OpenAI, 3072d)", value: "text-embedding-3-large" },
91
+ ]
92
+ const currentVal = ec.model ?? "BAAI/bge-m3"
93
+ openPicker({
94
+ title: `Embedding model (current: ${currentVal})`,
95
+ entries: [
96
+ { type: "header", text: `Current: ${currentVal}` },
97
+ ...models.map((m) => ({ type: "item", text: m.label, action: m.value })),
98
+ ],
99
+ onSelect: async (sel) => {
100
+ try {
101
+ const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
102
+ raw.embedding ??= {}
103
+ raw.embedding.model = sel.action
104
+ const { saveConfig, loadConfig } = await import("../config.mjs")
105
+ saveConfig(raw)
106
+ const cfg = loadConfig()
107
+ agent.provider = cfg.provider
108
+ agent.providers = cfg.providersList
109
+ agent.activeProvider = cfg.activeProvider
110
+ agent.config = cfg
111
+ agent.config.agent ??= {}
112
+ pushLabel(`❯ Config`, ansi.bold + C.tool)
113
+ pushLine(`embedding.model = ${sel.action}`, C.tool)
114
+ } catch (error) {
115
+ pushLine(`Save failed: ${error.message}`, C.error)
116
+ }
117
+ },
118
+ })
119
+ return
120
+ }
121
+ // Numeric config items: ask for value, parse as number
122
+ const isNumeric = e.action.startsWith("agent.")
123
+ const label = e.action
124
+ const current = e.action === "agent.maxTurns" ? (ac.maxTurns ?? 100)
125
+ : e.action === "agent.subagentTurns" ? (ac.subagentTurns ?? 100)
126
+ : e.action === "agent.compactThreshold" ? (ac.compactThreshold ?? 100000)
127
+ : ""
128
+ const prompt = `${label} (current: ${current}):`
129
+ const val = await askQuestion(prompt)
130
+ if (!val) return
131
+ try {
132
+ const { configPath, loadConfig, saveConfig } = await import("../config.mjs")
133
+ const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
134
+ if (isNumeric) {
135
+ const keys = label.split(".")
136
+ let obj = raw
137
+ for (let i = 0; i < keys.length - 1; i++) { obj[keys[i]] ??= {}; obj = obj[keys[i]] }
138
+ const num = Number(val)
139
+ if (isNaN(num)) { pushLine("Value must be a number", C.error); return }
140
+ obj[keys[keys.length - 1]] = num
141
+ }
142
+ saveConfig(raw)
143
+ const cfg = loadConfig()
144
+ agent.provider = cfg.provider
145
+ agent.providers = cfg.providersList
146
+ agent.activeProvider = cfg.activeProvider
147
+ agent.config = cfg
148
+ agent.config.agent ??= {}
149
+ pushLabel(`❯ Config`, ansi.bold + C.tool)
150
+ pushLine(`${label} = ${val}`, C.tool)
151
+ pushLine("(restart to apply to existing agent state)", C.dim)
152
+ } catch (error) {
153
+ pushLine(`Save failed: ${error.message}`, C.error)
69
154
  }
70
155
  },
71
156
  })
@@ -1,5 +1,13 @@
1
+ import { C } from "./ansi.mjs"
2
+
1
3
  /** /extract command: extract knowledge from current session.
2
- * ctx: { runDistill } */
4
+ * ctx: { runDistill, state, pushLine } */
3
5
  export async function handleExtractCommand(ctx) {
4
- await ctx.runDistill()
6
+ const { runDistill, state, pushLine } = ctx
7
+ pushLine("[extract] Analyzing session...", C.dim)
8
+ const count = await runDistill()
9
+ const msg = count > 0
10
+ ? `Knowledge extracted: ${count} candidate(s) saved to memory (use /skills to list, agent will recall via memory_search)`
11
+ : "No new knowledge found in this session."
12
+ pushLine(msg, count > 0 ? C.tool : C.dim)
5
13
  }
@@ -1,5 +1,3 @@
1
- import { ansi, C } from "./ansi.mjs"
2
-
3
1
  /** /goal command: set/view/cancel long-term goal.
4
2
  * Extracted from slash-commands.mjs.
5
3
  * ctx: { agent, pushLine, pushLabel, openPicker, askQuestion } */
@@ -19,7 +17,6 @@ export async function handleGoalCommand(ctx) {
19
17
  onSelect: async (e) => {
20
18
  if (e.action === "view") {
21
19
  const statusText = { active: "active", complete: "completed", blocked: "blocked" }[agent.goal.status] ?? agent.goal.status
22
- pushLabel(`❯ Goal`, ansi.bold + C.warn)
23
20
  pushLine(`Goal: ${agent.goal.objective}`, C.tool)
24
21
  if (agent.goal.criteria) pushLine(` Criteria: ${agent.goal.criteria}`, C.dim)
25
22
  pushLine(` Status: ${statusText} │ Turns used: ${agent.goal.turnsUsed ?? 0} │ Set at: ${new Date(agent.goal.setAt).toLocaleString()}`, C.dim)
@@ -27,8 +24,6 @@ export async function handleGoalCommand(ctx) {
27
24
  }
28
25
  if (e.action === "cancel") {
29
26
  agent.goal = null
30
- pushLabel(`❯ Goal`, ansi.bold + C.dim)
31
- pushLine(`Goal cancelled.`, C.dim)
32
27
  return
33
28
  }
34
29
  // set — requires entering goal text
@@ -38,10 +33,6 @@ export async function handleGoalCommand(ctx) {
38
33
  const objective = semi ? goalText.slice(0, semi).trim() : goalText.trim()
39
34
  const criteria = semi ? goalText.slice(semi + 1).trim() : ""
40
35
  agent.goal = { objective, criteria, setAt: Date.now(), status: "active", turnsUsed: 0, _blockTally: null }
41
- pushLabel(`❯ Goal`, ansi.bold + C.warn)
42
- pushLine(`Goal set: ${objective}`, C.tool)
43
- if (criteria) pushLine(` Criteria: ${criteria}`, C.dim)
44
- else pushLine(` ⚠ No criteria provided — the agent will determine its own criteria as it works on this goal`, C.warn)
45
36
  },
46
37
  })
47
38
  }
@@ -5,7 +5,7 @@ import { ansi, C } from "./ansi.mjs"
5
5
  export async function handleHelpCommand(ctx) {
6
6
  const { pushLine, pushLabel, SLASH_COMMANDS } = ctx
7
7
  const aliasList = { "/help": "/h", "/exit": "/x", "/model": "/m", "/plan": "/p", "/think": "/t", "/clear": "/c", "/new": "/n" }
8
- const order = ["Agent", "Session", "Tools", "Config"]
8
+ const order = ["Agent", "Session", "Project", "System"]
9
9
  const byGroup = new Map()
10
10
  for (const c of SLASH_COMMANDS) {
11
11
  if (!c.group) continue
@@ -120,26 +120,39 @@ export async function handleMcpCommand(ctx) {
120
120
  return
121
121
  }
122
122
  if (e.action === "add") {
123
- const mcpInput = await askQuestion("Enter: <name> <URL|command> [args...]\nURL auto-detect: https://… → HTTP, ws://… WebSocket, other → stdio command")
124
- if (!mcpInput) return
125
- const parts = mcpInput.split(/\s+/)
126
- if (parts.length < 2) { pushLine("Usage: <name> <URL|command> [args...]", C.error); return }
127
- const [name, second, ...extras] = parts
128
- const existing = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
129
- if (existing) { pushLine(`[mcp] "${name}" already exists`, C.error); return }
130
- const isWS = /^wss?:\/\//.test(second)
131
- const isHTTP = /^https?:\/\//.test(second)
132
- let srv
133
- if (isWS) {
134
- const headers = parseHeaders(extras)
135
- srv = { name, wsUrl: second, headers: Object.keys(headers).length > 0 ? headers : undefined }
136
- } else if (isHTTP) {
137
- const headers = parseHeaders(extras)
138
- srv = { name, url: second, headers: Object.keys(headers).length > 0 ? headers : undefined }
139
- } else {
140
- srv = { name, command: second, args: extras.length > 0 ? extras : undefined }
141
- }
142
- await addAndConnect(ctx, srv)
123
+ // Pick transport type first, then ask name + URL/command
124
+ openPicker({
125
+ title: "MCP Transport",
126
+ entries: [
127
+ { type: "header", text: "Select server transport" },
128
+ { type: "item", text: "HTTP (https://…)", action: "http" },
129
+ { type: "item", text: "WebSocket (ws://…)", action: "ws" },
130
+ { type: "item", text: "stdio (local command)", action: "stdio" },
131
+ ],
132
+ onSelect: async (te) => {
133
+ const name = await askQuestion("Server name:")
134
+ if (!name) return
135
+ const existing = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
136
+ if (existing) { pushLine(`[mcp] "${name}" already exists`, C.error); return }
137
+ if (te.action === "stdio") {
138
+ const cmd = await askQuestion("Command (e.g. npx, python):")
139
+ if (!cmd) return
140
+ const argsInput = await askQuestion("Arguments (space-separated, or leave empty):")
141
+ const args = argsInput ? argsInput.split(/\s+/) : undefined
142
+ await addAndConnect(ctx, { name, command: cmd, args })
143
+ } else {
144
+ const urlPrompt = te.action === "ws" ? "WebSocket URL (ws://…):" : "HTTP URL (https://…):"
145
+ const url = await askQuestion(urlPrompt)
146
+ if (!url) return
147
+ const headersInput = await askQuestion("Headers (key=value, space-separated, or leave empty):")
148
+ const headers = headersInput ? parseHeaders(headersInput.split(/\s+/)) : undefined
149
+ const srv = te.action === "ws"
150
+ ? { name, wsUrl: url, headers: Object.keys(headers ?? {}).length > 0 ? headers : undefined }
151
+ : { name, url, headers: Object.keys(headers ?? {}).length > 0 ? headers : undefined }
152
+ await addAndConnect(ctx, srv)
153
+ }
154
+ },
155
+ })
143
156
  }
144
157
  },
145
158
  })
@@ -2,17 +2,37 @@ import { clearSession } from "../session.mjs"
2
2
  import { C } from "./ansi.mjs"
3
3
 
4
4
  /** /new command: start new session (old session archived to slot).
5
- * ctx: { agent, state, pushLine } */
5
+ * ctx: { agent, state, pushLine, openPicker, render } */
6
6
  export async function handleNewCommand(ctx) {
7
- const { agent, state, pushLine } = ctx
8
- agent.history = []
9
- agent.tasks = []
10
- agent.planMode = false
11
- agent.goal = null
12
- agent._pendingReminders = []
13
- state.tasks = []
14
- state.lines = []
15
- state.streaming = ""
16
- clearSession(agent.cwd)
17
- pushLine("New session started (old session archived to slot; /session to view)", C.dim)
7
+ const { agent, state, pushLine, openPicker, render } = ctx
8
+
9
+ const doNewSession = () => {
10
+ agent.history = []
11
+ agent.tasks = []
12
+ agent.planMode = false
13
+ agent.goal = null
14
+ agent._pendingReminders = []
15
+ state.tasks = []
16
+ state.lines = []
17
+ state.streaming = ""
18
+ clearSession(agent.cwd)
19
+ render()
20
+ pushLine("New session started (old session archived to slot; /session to view)", C.dim)
21
+ }
22
+
23
+ if (agent.history.length > 0) {
24
+ openPicker({
25
+ title: "Start new session?",
26
+ entries: [
27
+ { type: "item", text: "Yes, archive current and start new", action: "yes" },
28
+ { type: "item", text: "Cancel", action: "no" },
29
+ ],
30
+ defaultIndex: 1,
31
+ onSelect: (e) => {
32
+ if (e.action === "yes") doNewSession()
33
+ },
34
+ })
35
+ return
36
+ }
37
+ doNewSession()
18
38
  }
@@ -1,9 +1,7 @@
1
- import { ansi, C } from "./ansi.mjs"
2
-
3
1
  /** /plan command: toggle plan mode (read-only explore → design → implement).
4
- * ctx: { agent, pushLine, pushLabel } */
2
+ * ctx: { agent } */
5
3
  export async function handlePlanCommand(ctx) {
6
- const { agent, pushLine, pushLabel } = ctx
4
+ const { agent } = ctx
7
5
  agent.planMode = !agent.planMode
8
6
  agent._pendingReminders = agent._pendingReminders ?? []
9
7
  if (agent.planMode) {
@@ -11,11 +9,4 @@ export async function handlePlanCommand(ctx) {
11
9
  } else {
12
10
  agent._pendingReminders.push("[System reminder: plan mode is now OFF. You may edit files, run commands, and implement changes.]")
13
11
  }
14
- pushLabel(`❯ Plan`, ansi.bold + (agent.planMode ? C.tool : C.dim))
15
- pushLine(
16
- agent.planMode
17
- ? `Plan mode ON: read-only tools only. Design first, then implement. /plan again to exit.`
18
- : `Plan mode OFF: you may now edit files and run commands.`,
19
- agent.planMode ? C.tool : C.dim,
20
- )
21
12
  }
@@ -1,10 +1,8 @@
1
- import { ansi, C } from "./ansi.mjs"
2
-
3
1
  /** /think command: toggle thinking mode, set reasoning effort.
4
2
  * Extracted from slash-commands.mjs.
5
- * ctx: { agent, pushLine, pushLabel, openPicker, syncProviderField } */
3
+ * ctx: { agent, openPicker, syncProviderField } */
6
4
  export async function handleThinkCommand(ctx) {
7
- const { agent, pushLine, pushLabel, openPicker, syncProviderField } = ctx
5
+ const { agent, openPicker, syncProviderField } = ctx
8
6
  const cur = agent.provider
9
7
  const thinkingEnabled = cur.thinking?.type === "enabled" || cur.thinking?.type === undefined
10
8
  const { specForModel } = await import("../config.mjs")
@@ -30,8 +28,6 @@ export async function handleThinkCommand(ctx) {
30
28
  if (e.action === "effort") {
31
29
  cur.reasoningEffort = e.level
32
30
  await syncProviderField("reasoningEffort", e.level)
33
- pushLabel(`❯ Think`, ansi.bold + C.tool)
34
- pushLine(`Reasoning effort set to ${e.level}`, C.tool)
35
31
  } else {
36
32
  const enable = e.action === "on"
37
33
  if (isEffortOnly) {
@@ -47,9 +43,6 @@ export async function handleThinkCommand(ctx) {
47
43
  if (!enable) await syncProviderField("reasoningEffort", undefined)
48
44
  else await syncProviderField("reasoningEffort", cur.reasoningEffort)
49
45
  }
50
- pushLabel(`❯ Think`, ansi.bold + C.tool)
51
- pushLine(`Thinking mode ${enable ? "On" : "Off"}`, C.tool)
52
- if (enable) pushLine(`Reasoning effort: ${cur.reasoningEffort}`, C.dim)
53
46
  }
54
47
  },
55
48
  })
@@ -0,0 +1,47 @@
1
+ /** /upgrade command: check for updates and optionally upgrade.
2
+ * ctx: { agent, pushLine, pushLabel, openPicker, ansi, C } */
3
+ export async function handleUpgradeCommand(ctx) {
4
+ const { pushLine, pushLabel, openPicker, ansi, C } = ctx
5
+ const { checkForUpdate } = await import("../upgrade.mjs")
6
+ const { readFileSync } = await import("node:fs")
7
+
8
+ const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"))
9
+
10
+ pushLabel(`❯ Upgrade`, ansi.bold + C.tool)
11
+ pushLine(`Checking for updates...`, C.dim)
12
+ const result = await checkForUpdate(pkg.version)
13
+ if (!result) {
14
+ pushLine(`Unable to query npm registry — check your network`, C.error)
15
+ return
16
+ }
17
+ if (!result.newer) {
18
+ pushLine(`✓ ThinCoder ${result.local} is already the latest.`, C.tool)
19
+ return
20
+ }
21
+ pushLine(`thincoder ${result.latest} is available (current: ${result.local}).`, C.tool)
22
+ openPicker({
23
+ title: `Update: ${result.local} → ${result.latest}`,
24
+ entries: [
25
+ { type: "header", text: `New version: ${result.latest}` },
26
+ { type: "item", text: "Upgrade now", action: "upgrade" },
27
+ { type: "item", text: "Later", action: "later" },
28
+ ],
29
+ onSelect: async (sel) => {
30
+ if (sel.action === "upgrade") {
31
+ pushLabel(`❯ Upgrade`, ansi.bold + C.tool)
32
+ pushLine(`Upgrading to ${result.latest}...`, C.tool)
33
+ const { exec } = await import("node:child_process")
34
+ const cp = exec("npm install -g thincoder@latest", { windowsHide: true })
35
+ cp.stdout?.on("data", () => {})
36
+ cp.stderr?.on("data", () => {})
37
+ cp.on("close", (code) => {
38
+ if (code === 0) {
39
+ pushLine(`✓ Upgraded to ${result.latest}. Restart to apply.`, C.tool)
40
+ } else {
41
+ pushLine(`✗ Upgrade failed (exit ${code}). Run \`thincoder upgrade\` manually.`, C.error)
42
+ }
43
+ })
44
+ }
45
+ },
46
+ })
47
+ }
@@ -35,6 +35,7 @@ export async function runDistill(ctx) {
35
35
  saved++
36
36
  }
37
37
  pushLine(`[distill] Done: saved ${saved}/${candidates.length} item(s)`, C.tool)
38
+ return saved
38
39
  } catch (error) {
39
40
  pushLine(`[distill] error: ${error.message}`, C.error)
40
41
  } finally {
package/src/tui/index.mjs CHANGED
@@ -381,6 +381,51 @@ export async function startTUI(agent, opts = {}) {
381
381
 
382
382
  showStartup({ agent, state, opts, pushLine, pushLabel, render, startWizard })
383
383
  backgroundIndex({ agent, state, render })
384
+
385
+ // Check for updates (non-blocking, after startup screen)
386
+ ;(async () => {
387
+ try {
388
+ const { readFileSync } = await import("node:fs")
389
+ const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"))
390
+ const { checkForUpdate } = await import("../upgrade.mjs")
391
+ const result = await checkForUpdate(pkg.version)
392
+ if (result?.newer) {
393
+ // Defer: if wizard is still active, just show a dim line
394
+ if (state.wizard) {
395
+ pushLine(`Tip: thincoder ${result.latest} is available (run /upgrade later or restart)`, C.dim)
396
+ render()
397
+ } else {
398
+ openPicker({
399
+ title: `Update available: ${result.local} → ${result.latest}`,
400
+ entries: [
401
+ { type: "header", text: `thincoder ${result.latest} is available (current: ${result.local})` },
402
+ { type: "item", text: "Upgrade now", action: "upgrade" },
403
+ { type: "item", text: "Later", action: "later" },
404
+ ],
405
+ onSelect: async (sel) => {
406
+ if (sel.action === "upgrade") {
407
+ pushLabel(`❯ Upgrade`, ansi.bold + C.tool)
408
+ pushLine(`Upgrading to ${result.latest}...`, C.tool)
409
+ const { exec } = await import("node:child_process")
410
+ const cp = exec("npm install -g thincoder@latest", { windowsHide: true })
411
+ let stdout = ""
412
+ cp.stdout?.on("data", (d) => { stdout += d })
413
+ cp.stderr?.on("data", (d) => { stdout += d })
414
+ cp.on("close", (code) => {
415
+ if (code === 0) {
416
+ pushLine(`✓ Upgraded to ${result.latest}. Restart to apply.`, C.tool)
417
+ } else {
418
+ pushLine(`✗ Upgrade failed (exit ${code}). Run \`thincoder upgrade\` manually.`, C.error)
419
+ }
420
+ render()
421
+ })
422
+ }
423
+ },
424
+ })
425
+ }
426
+ }
427
+ } catch { /* network error or timeout — silently skip */ }
428
+ })()
384
429
  }
385
430
 
386
431
  function summarize(obj) {
@@ -43,7 +43,7 @@ export function createKeyHandler(ctx) {
43
43
  if (q.options.length > 0) {
44
44
  // options mode: ↑↓ select, Enter confirm, Esc cancel
45
45
  if (key.name === "escape") {
46
- q.resolve("(cancelled)")
46
+ q.resolve("")
47
47
  state.question = null
48
48
  state.status = "Processing..."
49
49
  render()
@@ -64,13 +64,13 @@ export function createKeyHandler(ctx) {
64
64
  } else {
65
65
  // free text: type answer, Enter submit, Esc cancel
66
66
  if (key.name === "escape") {
67
- q.resolve("(cancelled)")
67
+ q.resolve("")
68
68
  state.question = null
69
69
  state.status = "Processing..."
70
70
  render()
71
71
  } else if (key.name === "return") {
72
72
  const answer = (q.answer ?? "").trim()
73
- q.resolve(answer || "(empty answer)")
73
+ q.resolve(answer || "")
74
74
  state.question = null
75
75
  state.status = "Processing..."
76
76
  pushLine(` → ${answer || "(empty)"}`, C.tool)
@@ -26,6 +26,7 @@ import { handleModelCommand } from "./cmd-model.mjs"
26
26
  import { handleConfigCommand } from "./cmd-config.mjs"
27
27
  import { handleExtractCommand } from "./cmd-extract.mjs"
28
28
  import { handleHelpCommand } from "./cmd-help.mjs"
29
+ import { handleUpgradeCommand } from "./cmd-upgrade.mjs"
29
30
 
30
31
  export const SLASH_COMMANDS = [
31
32
  { name: "/plan", group: "Agent", desc: "toggle plan mode (design first, then implement)" },
@@ -33,18 +34,19 @@ export const SLASH_COMMANDS = [
33
34
  { name: "/model", group: "Agent", desc: "select model & manage providers" },
34
35
  { name: "/goal", group: "Agent", desc: "set/view/cancel long-term goal" },
35
36
  { name: "/think", group: "Agent", desc: "thinking mode & reasoning effort" },
36
- { name: "/init", group: "Tools", desc: "generate project AGENTS.md skeleton" },
37
- { name: "/skills", group: "Tools", desc: "list project skills" },
38
- { name: "/mcp", group: "Tools", desc: "manage MCP servers" },
39
- { name: "/config", group: "Config", desc: "config management (embedding / agent)" },
40
- { name: "/reindex", group: "Config", desc: "rebuild memory index" },
37
+ { name: "/config", group: "Agent", desc: "config management (embedding / agent)" },
38
+ { name: "/upgrade", group: "System", desc: "check for updates & upgrade" },
41
39
  { name: "/new", group: "Session", desc: "new session (old one archived to slot)" },
42
40
  { name: "/session", group: "Session", desc: "list/switch archived sessions" },
43
41
  { name: "/clear", group: "Session", desc: "clear screen" },
44
42
  { name: "/extract", group: "Session", desc: "extract knowledge from session" },
45
- { name: "/restore", group: "Session", desc: "restore checkpoint" },
46
- { name: "/exit", group: "Session", desc: "exit" },
47
- { name: "/help", group: "", desc: "this list" },
43
+ { name: "/init", group: "Project", desc: "generate project AGENTS.md skeleton" },
44
+ { name: "/skills", group: "Project", desc: "list project skills" },
45
+ { name: "/mcp", group: "Project", desc: "manage MCP servers" },
46
+ { name: "/reindex", group: "Project", desc: "rebuild memory index" },
47
+ { name: "/restore", group: "Project", desc: "restore checkpoint" },
48
+ { name: "/exit", group: "System", desc: "exit" },
49
+ { name: "/help", group: "System", desc: "this list" },
48
50
  ]
49
51
 
50
52
  /** Command → handler mapping table */
@@ -64,6 +66,7 @@ const HANDLERS = {
64
66
  "/think": handleThinkCommand,
65
67
  "/model": handleModelCommand,
66
68
  "/config": handleConfigCommand,
69
+ "/upgrade": handleUpgradeCommand,
67
70
  "/extract": handleExtractCommand,
68
71
  "/help": handleHelpCommand,
69
72
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * upgrade.mjs — version check and upgrade utilities
3
+ * Used by both CLI (bin/thincoder.mjs upgrade command) and TUI (startup check).
4
+ */
5
+
6
+ /** Compare two semver-like version strings. Returns -1/0/1. */
7
+ export function compareVersions(a, b) {
8
+ const pa = String(a).split("."), pb = String(b).split(".")
9
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
10
+ const xa = pa[i] ?? "0", xb = pb[i] ?? "0"
11
+ const na = Number(xa), nb = Number(xb)
12
+ if (!Number.isNaN(na) && !Number.isNaN(nb)) {
13
+ if (na !== nb) return na < nb ? -1 : 1
14
+ } else if (xa !== xb) {
15
+ return xa < xb ? -1 : 1
16
+ }
17
+ }
18
+ return 0
19
+ }
20
+
21
+ /**
22
+ * Check npm registry for the latest version.
23
+ * Returns { local, latest, newer: boolean } or null on network error / timeout.
24
+ */
25
+ export async function checkForUpdate(localVersion) {
26
+ try {
27
+ const res = await fetch("https://registry.npmjs.org/thincoder/latest", {
28
+ signal: AbortSignal.timeout(5000),
29
+ })
30
+ if (!res.ok) return null
31
+ const data = await res.json()
32
+ const latest = data.version
33
+ return {
34
+ local: localVersion,
35
+ latest,
36
+ newer: compareVersions(localVersion, latest) < 0,
37
+ }
38
+ } catch {
39
+ return null
40
+ }
41
+ }