thincoder 0.11.0 → 0.12.0

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 (51) hide show
  1. package/README.md +8 -0
  2. package/package.json +1 -1
  3. package/src/advisor.mjs +535 -72
  4. package/src/agent/helpers.mjs +18 -5
  5. package/src/agent/setup.mjs +2 -2
  6. package/src/agent-tools/advisor.mjs +36 -0
  7. package/src/agent-tools/plan.mjs +53 -2
  8. package/src/agent-tools/subagent.mjs +7 -1
  9. package/src/agent-tools/timer.mjs +1 -1
  10. package/src/agent-tools/verify.mjs +1 -0
  11. package/src/agent-tools.mjs +1 -0
  12. package/src/agent.mjs +80 -21
  13. package/src/auto-think.mjs +23 -5
  14. package/src/cli/make-agent.mjs +20 -0
  15. package/src/config.mjs +1 -1
  16. package/src/mcp/transport-stdio.mjs +4 -3
  17. package/src/mcp.mjs +1 -1
  18. package/src/prompts/advisor-round1.md +23 -0
  19. package/src/prompts/advisor-round2.md +26 -0
  20. package/src/prompts/advisor-round3.md +24 -0
  21. package/src/prompts/coder.md +1 -0
  22. package/src/prompts/discipline.md +15 -1
  23. package/src/prompts/explore.md +2 -0
  24. package/src/prompts/plan.md +2 -0
  25. package/src/prompts/system.md +5 -1
  26. package/src/provider/anthropic.mjs +4 -4
  27. package/src/provider/core.mjs +6 -126
  28. package/src/provider/google.mjs +4 -2
  29. package/src/provider/sse.mjs +112 -0
  30. package/src/skills.mjs +67 -31
  31. package/src/tools/bash.md +8 -0
  32. package/src/tools/codemode.mjs +5 -16
  33. package/src/tools/edit.md +8 -0
  34. package/src/tools/git.mjs +9 -6
  35. package/src/tools/read.md +7 -0
  36. package/src/tools/shared.mjs +43 -2
  37. package/src/tools/system.mjs +14 -10
  38. package/src/tools/web.mjs +21 -16
  39. package/src/tui/agent-turn.mjs +130 -73
  40. package/src/tui/cmd-advisor.mjs +237 -41
  41. package/src/tui/cmd-auto.mjs +6 -8
  42. package/src/tui/cmd-mcp.mjs +4 -2
  43. package/src/tui/cmd-plan.mjs +6 -8
  44. package/src/tui/cmd-think.mjs +93 -70
  45. package/src/tui/index.mjs +9 -188
  46. package/src/tui/interaction.mjs +2 -1
  47. package/src/tui/key-handler.mjs +8 -4
  48. package/src/tui/layout.mjs +3 -2
  49. package/src/tui/render-conversation.mjs +92 -0
  50. package/src/tui/render-frame.mjs +94 -168
  51. package/src/tui/render-loop.mjs +110 -0
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { configDir } from "../config.mjs"
5
5
  import { readFileSync, readdirSync, existsSync } from "node:fs"
6
+ import { homedir } from "node:os"
6
7
  import { writeFile, mkdir } from "node:fs/promises"
7
8
  import { join } from "node:path"
8
9
  import { execSync } from "node:child_process"
@@ -12,8 +13,12 @@ export const DEFAULT_SUBAGENT_TURNS = 100
12
13
  export const DEFAULT_GOAL_TURNS = 200
13
14
  export const MIN_REPORT_CHARS = 200
14
15
  export const REPORT_CONTINUATION =
15
- "Your report is too brief to be a complete handoff — the parent agent sees nothing else from your run. " +
16
- "Expand it: what you did and why, the path of every file you touched, how you verified (commands/tests run, with results), and anything left undone."
16
+ "Your report was sent back: too brief to be a complete handoff — the parent agent sees nothing else from your run. " +
17
+ "Rewrite your final message as a checklist:\n" +
18
+ "1. What you changed and why\n" +
19
+ "2. The path of every file you touched\n" +
20
+ "3. How you verified (tests run, commands executed, with results)\n" +
21
+ "4. Anything left undone or worth follow-up"
17
22
 
18
23
  const TOOL_RESULT_OFFLOAD_LIMIT = 16_000
19
24
  const TOOL_RESULT_PREVIEW = 2_000
@@ -22,7 +27,7 @@ const GIT_TIMEOUT_MS = 5000
22
27
  const MAX_GIT_CHANGES_DISPLAY = 20
23
28
 
24
29
  export const OUTLINE_INJECT_PREFIX = "[System reminder: project dependency outline:"
25
- export const FILE_MUTATORS = new Set(["write", "edit", "insert_after", "apply_patch", "delete"])
30
+ export const FILE_MUTATORS = new Set(["write", "edit", "insert_after", "apply_patch", "delete", "hashline_edit"])
26
31
 
27
32
  /** Escape XML special characters in a string for safe embedding in XML/HTML */
28
33
  export function escapeXml(s) {
@@ -174,14 +179,22 @@ export function readonlyToolNames(tools) {
174
179
 
175
180
  const MAX_INSTRUCTION_CHARS = 32_000
176
181
 
177
- /** Load AGENTS.md / project_rules.md from the project root, return as project instructions */
182
+ /** Load AGENTS.md / project_rules.md from user home and project root.
183
+ * User-level (~/.thincoder/AGENTS.md) loaded first (lower priority).
184
+ * Project-level overrides take precedence. */
178
185
  export async function loadProjectInstructions(cwd) {
179
186
  const parts = []
187
+ // 1. User-level: global preferences across all projects
188
+ try {
189
+ const userPath = join(homedir(), ".thincoder", "AGENTS.md")
190
+ const content = readFileSync(userPath, "utf8").trim()
191
+ if (content) parts.push(`<!-- From: ${userPath} -->\n${content}`)
192
+ } catch { /* file does not exist */ }
193
+ // 2. Project-level: project-specific conventions
180
194
  for (const name of ["AGENTS.md", "project_rules.md"]) {
181
195
  try {
182
196
  const content = readFileSync(join(cwd, name), "utf8").trim()
183
197
  if (!content) continue
184
- const key = name.toLowerCase()
185
198
  parts.push(`<!-- From: ${join(cwd, name)} -->\n${content}`)
186
199
  } catch { /* file does not exist */ }
187
200
  }
@@ -117,8 +117,8 @@ export async function prepareRun(agent, input, callbacks, {
117
117
  }
118
118
 
119
119
  // task/plan tools are injected with the main loop; subagent/skill/goal/verify only at top level
120
- const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool } = await import("../agent-tools.mjs")
121
- const tools = [...agent.tools, taskTool, planTool, timerTool, ...(depth === 0 ? [subagentTool, skillTool, goalTool, verifyTool, recentChangesTool] : [])]
120
+ const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool } = await import("../agent-tools.mjs")
121
+ const tools = [...agent.tools, taskTool, planTool, timerTool, ...(depth === 0 ? [subagentTool, skillTool, goalTool, verifyTool, recentChangesTool, advisorTool] : [])]
122
122
  const toolSchemas = tools.map(toOpenAISchema)
123
123
  const toolByName = new Map(tools.map((t) => [t.name, t]))
124
124
  agent._onTaskUpdate = callbacks.onTaskUpdate
@@ -0,0 +1,36 @@
1
+ /**
2
+ * agent-tools/advisor.mjs — advisor tool wrapper.
3
+ * The agent calls this explicitly at the end of a coding task to get a code review.
4
+ */
5
+ import { runAdvisorReview } from "../advisor.mjs"
6
+
7
+ export const advisorTool = {
8
+ name: "advisor",
9
+ description:
10
+ "Run a code review on your changes (convergence protocol). " +
11
+ "Call this when you have finished coding and want an independent review before finalising. " +
12
+ "The advisor is an independent read-only sub-agent that explores the codebase, runs git diff, " +
13
+ "reads files, and traces callers via grep/lsp. " +
14
+ "Review criteria come from .thincoder/advisor.md (if present) or sensible defaults. " +
15
+ "Round 1 does a full review and produces a numbered issue table. " +
16
+ "After the review, you MUST produce a response table (see discipline rules for format). " +
17
+ "Round 2 verifies the table + can flag obvious new issues. " +
18
+ "Round 3+ strictly checks only the prior table — convergence, not divergence. " +
19
+ "If issues are found, fix them, update your response table, then re-run advisor. " +
20
+ "If advisor says all clear, call verify.",
21
+ parameters: {
22
+ type: "object",
23
+ properties: {},
24
+ },
25
+ readonly: true,
26
+ sideEffectExempt: true,
27
+ outputPanel: true,
28
+ async execute(_args, ctx) {
29
+ const agent = ctx.agent
30
+
31
+ const result = await runAdvisorReview(agent, ctx.onOutput, ctx.signal)
32
+ if (!result) return "Advisor: review is disabled or no changes to review."
33
+
34
+ return result
35
+ },
36
+ }
@@ -2,7 +2,56 @@
2
2
  * plan tool: enter/exit plan mode.
3
3
  * In plan mode only read-only tools are allowed — explore code, design solutions, no code writing.
4
4
  * After the user approves the plan, exit plan mode and start implementing.
5
+ *
6
+ * Reminder cadence (kimi-code style): while plan mode is active the agent loop
7
+ * re-injects reminders — sparse every 2 turns, full every 5 turns or when the
8
+ * user sends a new message — so the constraint never fades from context.
5
9
  */
10
+
11
+ export const PLAN_FULL_REMINDER =
12
+ "[System reminder: plan mode is ON. Workflow: (1) explore/read codebase with read-only tools, " +
13
+ "(2) design a solution considering trade-offs, (3) present your plan by calling plan with action='exit' " +
14
+ "so the user can approve it. Only read-only tools are allowed — do not write, edit, or run mutation commands. " +
15
+ "Your turn must end with either a clarifying question to the user or a call to plan with action='exit'.]"
16
+
17
+ export const PLAN_SPARSE_REMINDER =
18
+ "[System reminder: plan mode still active — read-only tools only (the current plan file exempt). " +
19
+ "Design the solution, then call plan with action='exit' for user approval.]"
20
+
21
+ export const PLAN_EXIT_REMINDER =
22
+ "[System reminder: plan mode is now OFF. Start implementing your plan — edit files, run commands. " +
23
+ "No need for a task list (plan already covered that) or further confirmation.]"
24
+
25
+ /** Turns between reminder re-injections while plan mode is active */
26
+ const SPARSE_INTERVAL = 2
27
+ const FULL_INTERVAL = 5
28
+
29
+ /**
30
+ * Decide which plan-mode reminder (if any) to inject this turn.
31
+ * @param {object} agent — the agent object (mutated: tracks reminder state)
32
+ * @param {boolean} userMessageSince — whether a user message arrived since the last reminder
33
+ * @returns {string|null} reminder text or null
34
+ */
35
+ export function planReminderForTurn(agent, userMessageSince) {
36
+ if (!agent.planMode) {
37
+ agent._planTurnsSinceReminder = 0
38
+ agent._planTurnsSinceSparse = 0
39
+ return null
40
+ }
41
+ agent._planTurnsSinceReminder = (agent._planTurnsSinceReminder ?? 0) + 1
42
+ agent._planTurnsSinceSparse = (agent._planTurnsSinceSparse ?? 0) + 1
43
+ if (userMessageSince || agent._planTurnsSinceReminder >= FULL_INTERVAL) {
44
+ agent._planTurnsSinceReminder = 0
45
+ agent._planTurnsSinceSparse = 0
46
+ return PLAN_FULL_REMINDER
47
+ }
48
+ if (agent._planTurnsSinceSparse >= SPARSE_INTERVAL) {
49
+ agent._planTurnsSinceSparse = 0
50
+ return PLAN_SPARSE_REMINDER
51
+ }
52
+ return null
53
+ }
54
+
6
55
  export const planTool = {
7
56
  name: "plan",
8
57
  description:
@@ -18,13 +67,15 @@ export const planTool = {
18
67
  async execute(args, ctx) {
19
68
  if (args.action === "exit") {
20
69
  ctx.agent.planMode = false
70
+ ctx.agent._planTurnsSinceReminder = 0
21
71
  ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
22
- ctx.agent._pendingReminders.push("[System reminder: plan mode is now OFF. Start implementing your plan — edit files, run commands. No need for a task list (plan already covered that) or further confirmation.]")
72
+ ctx.agent._pendingReminders.push(PLAN_EXIT_REMINDER)
23
73
  return "Plan mode exited. You may now edit files and run commands."
24
74
  }
25
75
  ctx.agent.planMode = true
76
+ ctx.agent._planTurnsSinceReminder = 0
26
77
  ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
27
- ctx.agent._pendingReminders.push("[System reminder: plan mode is now ON. Workflow: (1) explore/read codebase with read-only tools, (2) design a solution considering trade-offs, (3) present your plan by calling plan with action='exit' so the user can approve it. Only read-only tools are allowed — do not write, edit, or run mutation commands.]")
78
+ ctx.agent._pendingReminders.push(PLAN_FULL_REMINDER)
28
79
  return "Plan mode activated. You are now restricted to READ-ONLY tools. Explore the codebase, understand the architecture, design a solution. Present your plan to the user for approval before writing any code."
29
80
  },
30
81
  }
@@ -16,7 +16,13 @@ import {
16
16
  export const subagentTool = {
17
17
  name: "subagent",
18
18
  description:
19
- "Spawn a sub-agent to handle an independent subtask in an isolated context. The sub-agent returns only its final report. Spawn MULTIPLE subagents in the SAME response for parallel work—they run concurrently. Use role='explore' for codebase search/analysis (read-only, fast), role='plan' for read-only implementation planning (returns a step-by-step plan, never edits), role='coder' for self-contained implementation tasks. Do not give parallel subagents tasks that edit the same files.",
19
+ "Spawn a sub-agent to handle an independent subtask in an isolated context. The sub-agent returns only its final report. Spawn MULTIPLE subagents in the SAME response for parallel work—they run concurrently.\n" +
20
+ "Use role='explore' for codebase search/analysis (read-only, fast), role='plan' for read-only implementation planning (returns a step-by-step plan, never edits), role='coder' for self-contained implementation tasks. Do not give parallel subagents tasks that edit the same files.\n\n" +
21
+ "Writing the prompt:\n" +
22
+ "- The sub-agent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.\n" +
23
+ "- Put exact paths and commands in the prompt when you know them. The sub-agent should not search for things you already know.\n" +
24
+ "- Do not delegate understanding: if the task hinges on a file path or line number, find it yourself first and write it into the prompt.\n" +
25
+ "- Once a sub-agent is running, leave that scope to it: don't redo its searches in parallel, and don't abandon it midway to finish manually.",
20
26
  parameters: {
21
27
  type: "object",
22
28
  properties: {
@@ -29,7 +29,7 @@ export const timerTool = {
29
29
  readonly: true,
30
30
  sideEffectExempt: true,
31
31
  execute(args, ctx) {
32
- const seconds = args.seconds ?? 30
32
+ const seconds = args.seconds ?? 180
33
33
  const expiresAt = Date.now() + seconds * 1000
34
34
  const message = args.message || `⏰ Time's up (${seconds}s). Have you tried running the code, adding a console.log, or checking the output? Thinking more without data is guessing.`
35
35
 
@@ -19,6 +19,7 @@ const MODULE_TO_TEST = {
19
19
  skills: "test/tools.test.mjs",
20
20
  distill: "test/tools.test.mjs",
21
21
  markdown: "test/agent.test.mjs",
22
+ advisor: "test/advisor.test.mjs",
22
23
  mcp: null,
23
24
  prompts: null,
24
25
  context: null,
@@ -11,3 +11,4 @@ export { goalTool } from "./agent-tools/goal.mjs"
11
11
  export { verifyTool } from "./agent-tools/verify.mjs"
12
12
  export { recentChangesTool } from "./agent-tools/recent-changes.mjs"
13
13
  export { timerTool } from "./agent-tools/timer.mjs"
14
+ export { advisorTool } from "./agent-tools/advisor.mjs"
package/src/agent.mjs CHANGED
@@ -31,7 +31,7 @@ export const EXPLORE_OVERLAY = _EXPLORE
31
31
  export const CODER_OVERLAY = _CODER
32
32
  export const PLAN_OVERLAY = _PLAN
33
33
 
34
- // Re-exported for consumption by agent-tools.mjs
34
+ // exported for consumption by agent-tools.mjs
35
35
  export {
36
36
  ContinueError,
37
37
  repairHistory, listWorkDir, loadProjectInstructions,
@@ -49,6 +49,7 @@ const STALL_THRESHOLD = 3
49
49
  const GOAL_BUDGET_WARN_RATIO = 0.75
50
50
  const MAX_VERIFY_PUSHBACKS = 2
51
51
  const MAX_VERIFY_RETRIES = 3
52
+ const MAX_ADVISOR_PUSHBACKS = 3
52
53
 
53
54
  /** Create a new agent state object with all fields initialized to defaults */
54
55
  export function createAgent({
@@ -61,8 +62,8 @@ export function createAgent({
61
62
  provider, tools, config, cwd, memory, _role: role,
62
63
  overlay, tasks, history,
63
64
  planMode, autoApprove, goal,
64
- _mutatedThisRun: false, _verifiedThisRun: false, _verifyPassed: undefined,
65
- _touchedFiles: [], _verifyRetries: 0,
65
+ _mutatedThisRun: false, _verifiedThisRun: false, _verifyPassed: undefined, _calledAdvisorThisRun: false,
66
+ _touchedFiles: [], _verifyRetries: 0, _advisorRound: 0, _advisorSession: null,
66
67
  _pendingReminders: [],
67
68
  _pendingTimers: [],
68
69
  _sessionStart: sessionStart,
@@ -81,11 +82,18 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
81
82
  agent._mutatedThisRun = false
82
83
  agent._verifiedThisRun = false
83
84
  agent._verifyPassed = undefined
85
+ agent._calledAdvisorThisRun = false
84
86
  agent._touchedFiles = []
85
87
  agent._verifyRetries = 0
88
+ agent._advisorRound = 0
89
+ agent._advisorSession = null // advisor session is per-run: discard when the task ends, next task starts fresh
86
90
  let guardPushbacks = 0
91
+ let advisorPushbacks = 0
87
92
  let honestReminderInjected = false
88
93
  const recentCallSigs = []
94
+ // repeat: "once" stream rules fire at most once per runAgent call (user turn):
95
+ // this set survives across chat() calls (rule abort-retry, tool loop) within the turn.
96
+ const streamRuleFired = new Set()
89
97
 
90
98
  for (let turn = 0; turn < maxTurns; turn++) {
91
99
 
@@ -94,6 +102,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
94
102
  try {
95
103
  if (await compressIfNeeded(agent, threshold, callbacks)) {
96
104
  agent._compressFailures = 0
105
+ agent._planReminderAtLen = 0 // After compression history shrinks, reset cadence so reminders resume
97
106
  recentCallSigs.length = 0 // After compression history is rebuilt, reset stall detection counter
98
107
  callbacks.onCompress?.()
99
108
  if (agent.autoApprove && !agent.history.some((m) => m.content === AUTO_REMINDER)) {
@@ -111,6 +120,24 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
111
120
  }
112
121
  }
113
122
 
123
+ // Plan-mode reminder cadence: re-inject constraint reminders while plan mode is active
124
+ // (sparse every 2 turns, full every 5 turns or when the user sends a new message),
125
+ // so the read-only restriction never fades from context.
126
+ if (agent.planMode) {
127
+ const lastMsg = agent.history.at(-1)
128
+ const realUserMsg = lastMsg?.role === "user"
129
+ && typeof lastMsg.content === "string"
130
+ && !lastMsg.content.startsWith("[System reminder:")
131
+ && !lastMsg.content.startsWith("[User interrupt:")
132
+ const newUserSince = realUserMsg && agent.history.length > (agent._planReminderAtLen ?? 0)
133
+ const { planReminderForTurn } = await import("./agent-tools/plan.mjs")
134
+ const reminder = planReminderForTurn(agent, newUserSince)
135
+ if (reminder) {
136
+ agent._planReminderAtLen = agent.history.length + 1
137
+ agent.history.push({ role: "user", content: reminder, transient: true })
138
+ }
139
+ }
140
+
114
141
  const messages = [{ role: "system", content: systemPrompt }, ...agent.history]
115
142
  let response
116
143
 
@@ -129,15 +156,18 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
129
156
  onWait: callbacks.onWait,
130
157
  signal,
131
158
  streamRules: agent.config.agent?.streamRules ?? [],
159
+ firedPatterns: streamRuleFired,
132
160
  })
133
161
  } catch (e) {
134
162
  // User interrupt (Ctrl+I): controller.abort({ interrupt: true, message: "…" }).
135
163
  // Inject the message into history and let the outer loop recreate the controller.
136
164
  if (e.name === "AbortError" && signal?.reason?.interrupt) {
137
- agent.history.push({
138
- role: "user",
139
- content: `[User interrupt: ${signal.reason.message}]`,
140
- })
165
+ const msg = `[User interrupt: ${signal.reason.message}]`
166
+ // Dedup: if the interrupt was already handled during tool execution (L302-310),
167
+ // don't push a duplicate — the outer loop will still recreate the controller.
168
+ if (agent.history.at(-1)?.content !== msg) {
169
+ agent.history.push({ role: "user", content: msg })
170
+ }
141
171
  }
142
172
  throw e
143
173
  }
@@ -219,6 +249,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
219
249
  role: "user",
220
250
  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.]`,
221
251
  })
252
+ callbacks.onTurnEnd?.(agent, turn)
222
253
  continue
223
254
  }
224
255
  // --- verify guard: push model to verify mutated files before completion ---
@@ -230,15 +261,15 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
230
261
  role: "user",
231
262
  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.]",
232
263
  })
264
+ callbacks.onTurnEnd?.(agent, turn)
233
265
  continue
234
- }
235
- if (agent._verifiedThisRun && agent._verifyPassed === false && agent._verifyRetries < MAX_VERIFY_RETRIES) {
236
266
  agent._verifyRetries++
237
267
  agent.history.push({ role: "assistant", content: response.content })
238
268
  agent.history.push({
239
269
  role: "user",
240
270
  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.]`,
241
271
  })
272
+ callbacks.onTurnEnd?.(agent, turn)
242
273
  continue
243
274
  }
244
275
  if (agent._verifyPassed === false && agent._verifyRetries >= MAX_VERIFY_RETRIES) {
@@ -252,6 +283,25 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
252
283
  role: "user",
253
284
  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.]`,
254
285
  })
286
+ callbacks.onTurnEnd?.(agent, turn)
287
+ continue
288
+ }
289
+ }
290
+ // --- advisor guard: review of mutated files is mandatory before completion ---
291
+ // When advisor is enabled and guard is not explicitly disabled, the model MUST
292
+ // call advisor after mutating files — skipping is not offered as an option
293
+ // (offering it caused a skip → pushback loop). Pushbacks are capped as a
294
+ // backstop against pathological loops; convergence is unbounded by design.
295
+ if (depth === 0 && agent.config?.advisor?.enabled && agent.config?.advisor?.guard !== false) {
296
+ if (agent._mutatedThisRun && !agent._calledAdvisorThisRun && (agent._touchedFiles ?? []).length > 0
297
+ && advisorPushbacks < MAX_ADVISOR_PUSHBACKS) {
298
+ advisorPushbacks++
299
+ agent.history.push({ role: "assistant", content: response.content })
300
+ agent.history.push({
301
+ role: "user",
302
+ content: `[System reminder: you changed code in this run and MUST get an advisor review before finishing (round ${agent._advisorRound + 1}). Call the \`advisor\` tool now. This is required, not optional — do not skip it even if you believe the changes are trivial; a small diff just makes the review fast. After the review, produce a response table for every issue found (see discipline rules for format).]`,
303
+ })
304
+ callbacks.onTurnEnd?.(agent, turn)
255
305
  continue
256
306
  }
257
307
  }
@@ -276,8 +326,20 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
276
326
 
277
327
  const results = await executeToolCalls(agent, toolByName, response.toolCalls, callbacks, depth, signal)
278
328
 
329
+ // Ctrl+I interrupt during tool execution: skip committing partial results —
330
+ // the tool failure messages would mislead the model. Inject the interrupt and retry.
331
+ if (signal?.reason?.interrupt) {
332
+ agent.history.push({
333
+ role: "user",
334
+ content: `[User interrupt: ${signal.reason.message}]`,
335
+ })
336
+ callbacks.onTurnEnd?.(agent, turn)
337
+ continue
338
+ }
339
+
279
340
  // Model is executing tools → doing real work, reset guard pushback counter
280
341
  guardPushbacks = 0
342
+ advisorPushbacks = 0
281
343
 
282
344
  for (const { toolCall, result, ok } of results) {
283
345
  const tool = toolByName.get(toolCall.name)
@@ -308,8 +370,16 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
308
370
  }
309
371
  agent.history.push({ role: "tool", tool_call_id: toolCall.id, content: result })
310
372
  if (tool && ok) {
311
- if (!tool.readonly && !tool.sideEffectExempt) agent._mutatedThisRun = true
373
+ if (!tool.readonly && !tool.sideEffectExempt) {
374
+ agent._mutatedThisRun = true
375
+ // Any mutation after advisor invalidates the review — need re-review
376
+ if (agent._calledAdvisorThisRun) agent._calledAdvisorThisRun = false
377
+ }
312
378
  if (toolCall.name === "verify") agent._verifiedThisRun = true
379
+ if (toolCall.name === "advisor") {
380
+ agent._calledAdvisorThisRun = true
381
+ agent._advisorRound++ // advance convergence round
382
+ }
313
383
  if (FILE_MUTATORS.has(toolCall.name)) {
314
384
  const args = JSON.parse(toolCall.arguments)
315
385
  const paths = tool.touchedPaths ? tool.touchedPaths(args) : [args.path]
@@ -386,17 +456,6 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
386
456
  }
387
457
 
388
458
  callbacks.onTurnEnd?.(agent, turn)
389
-
390
- // Advisor: automated code review after each tool-execution turn.
391
- // Runs asynchronously — failure is silent, main loop continues regardless.
392
- if (agent.config?.advisor?.enabled) {
393
- const { runAdvisor } = await import("./advisor.mjs")
394
- const note = await runAdvisor(agent)
395
- if (note) {
396
- agent.history.push({ role: "user", content: note })
397
- callbacks.onAdvisor?.(note)
398
- }
399
- }
400
459
  }
401
460
 
402
461
  throw new ContinueError(maxTurns)
@@ -31,6 +31,26 @@ const EFFORT_MAP = {
31
31
  high: ["max", "max", "xhigh", "max"],
32
32
  }
33
33
 
34
+ /**
35
+ * Build classifier input from history: the latest real user message (reminders and
36
+ * interrupt injections excluded), plus the previous user message as context when the
37
+ * latest is too short to classify on its own (e.g. "继续" / "还有几个问题").
38
+ * Exported for tests.
39
+ */
40
+ export function buildClassifierInput(history) {
41
+ const isRealUser = (m) =>
42
+ m.role === "user" && typeof m.content === "string"
43
+ && !m.content.startsWith("[System reminder:") && !m.content.startsWith("[User interrupt:")
44
+ const users = history.filter(isRealUser)
45
+ const last = users.at(-1)
46
+ if (!last) return null
47
+ let prompt = last.content
48
+ if (prompt.length < 200 && users.length > 1) {
49
+ prompt = `Previous request (context):\n${users.at(-2).content.slice(0, 1200)}\n\nLatest message:\n${prompt}`
50
+ }
51
+ return prompt.slice(0, 2000)
52
+ }
53
+
34
54
  /**
35
55
  * Classify the difficulty of the user's prompt and adjust reasoning effort.
36
56
  * Only runs on the first turn (turn === 0) of a user message.
@@ -47,10 +67,8 @@ export async function classifyAndApply(agent, turn) {
47
67
  const validEfforts = spec.reasoningEffortEnum
48
68
  if (!validEfforts) return null // Model doesn't support reasoning effort
49
69
 
50
- // Get the last user message (should be the most recent history entry or the input)
51
- const lastUser = [...agent.history].reverse().find(m => m.role === "user")
52
- if (!lastUser) return null
53
- const prompt = typeof lastUser.content === "string" ? lastUser.content : ""
70
+ const prompt = buildClassifierInput(agent.history)
71
+ if (prompt == null) return null
54
72
 
55
73
  // Classification call: use same provider, minimal tokens, no tools, no streaming
56
74
  let level
@@ -59,7 +77,7 @@ export async function classifyAndApply(agent, turn) {
59
77
  const response = await chat(classifierProvider, {
60
78
  messages: [
61
79
  { role: "system", content: CLASSIFY_PROMPT },
62
- { role: "user", content: prompt.slice(0, 2000) },
80
+ { role: "user", content: prompt },
63
81
  ],
64
82
  tools: [],
65
83
  signal: AbortSignal.timeout(5_000),
@@ -52,6 +52,26 @@ export async function assembleAgent() {
52
52
 
53
53
  // MCP servers: connect in parallel (a dead server won't block startup), collect failures as warnings (stderr invisible in TUI, passed via agent object)
54
54
  const mcpServers = config.mcp?.servers ?? []
55
+ // Read project-level .mcp.json (standard MCP client convention) — merge into mcpServers
56
+ // config.json servers take priority over .mcp.json entries with the same name
57
+ try {
58
+ const { existsSync, readFileSync } = await import("node:fs")
59
+ const mcpJsonPath = join(cwd, ".mcp.json")
60
+ if (existsSync(mcpJsonPath)) {
61
+ const mcpJson = JSON.parse(readFileSync(mcpJsonPath, "utf8"))
62
+ if (mcpJson.mcpServers && typeof mcpJson.mcpServers === "object") {
63
+ const configNames = new Set(mcpServers.map((s) => s.name))
64
+ for (const [name, server] of Object.entries(mcpJson.mcpServers)) {
65
+ if (configNames.has(name)) continue // config.json takes priority
66
+ if (!server || typeof server !== "object") continue
67
+ mcpServers.push({ name, ...server })
68
+ }
69
+ }
70
+ }
71
+ } catch (e) {
72
+ // .mcp.json parse failure — non-fatal, log and continue
73
+ console.error(`[mcp] Failed to read .mcp.json: ${e.message}`)
74
+ }
55
75
  let mcpTools = []
56
76
  const mcpWarnings = []
57
77
  if (mcpServers.length) {
package/src/config.mjs CHANGED
@@ -39,7 +39,7 @@ const DEFAULTS = {
39
39
  compactThreshold: 100000,
40
40
  verifyGuard: false, // push model back to verify when files were mutated but verify not run (opt-in)
41
41
  streamRules: [], // time-traveling stream rules: [{ pattern: "regex", message: "reminder", action: "abort"|"warn", repeat: "always"|"once" }]
42
- advisor: { enabled: false }, // automated code review after each tool-execution turn; optionally: { enabled: true, provider: "deepseek", model: "deepseek-chat" }
42
+ advisor: { enabled: false }, // code review; { enabled: true, provider: "deepseek", model: "deepseek-chat", thinking: { type: "enabled" }, reasoningEffort: "max", guard: true }
43
43
  autoThink: false, // auto-classify task difficulty and set reasoning effort per-turn
44
44
  },
45
45
  memory: {
@@ -4,9 +4,10 @@
4
4
  import { spawn } from "node:child_process"
5
5
  import { rpcId, CALL_TIMEOUT_MS, withTimeout, quoteArg } from "./helpers.mjs"
6
6
 
7
- /** Create an MCP stdio transport over a spawned child process */
8
- export function stdioTransport(command, args) {
9
- const spawnOptions = { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, env: { ...process.env } }
7
+ /** Create an MCP stdio transport over a spawned child process.
8
+ * @param {Object} [env] — extra environment variables merged on top of process.env */
9
+ export function stdioTransport(command, args, env) {
10
+ const spawnOptions = { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, env: { ...process.env, ...env } }
10
11
  const child =
11
12
  process.platform === "win32" && !/\.exe$/i.test(command)
12
13
  ? spawn("cmd.exe", ["/d", "/s", "/c", [command, ...(args ?? [])].map(quoteArg).join(" ")], {
package/src/mcp.mjs CHANGED
@@ -77,7 +77,7 @@ export async function connectMcpServer(config) {
77
77
  }
78
78
 
79
79
  if (config.command) {
80
- const transport = stdioTransport(config.command, config.args ?? [])
80
+ const transport = stdioTransport(config.command, config.args ?? [], config.env)
81
81
  try {
82
82
  const mcpTools = await doInitialize(transport, config.name ?? config.command)
83
83
  return buildTools(mcpTools, transport, config)
@@ -0,0 +1,23 @@
1
+ You are a code review advisor.
2
+ Perform a full-scope review of the code changes.
3
+ You have read-only tools to explore the codebase.
4
+
5
+ Review workflow:
6
+ 1. The uncommitted changes (git status + diff) are already provided in the review context — do not re-run them unless marked truncated.
7
+ 2. Read AGENTS.md / design docs once if present, to understand project conventions, version requirements, and architecture decisions.
8
+ 3. Read changed files for full context beyond the diff. Batch independent tool calls in one reply.
9
+ 4. Use grep or lsp to trace callers, imports, and dependencies — only where genuinely needed.
10
+ 5. Produce your review table.
11
+
12
+ Rules:
13
+ - Reply in the same language as the conversation background.
14
+ - Respect the project's stated platform requirements — do not flag features as errors if they are valid under the project's target environment.
15
+ - Output a Markdown table. This table becomes the sole basis for convergence in later rounds — be thorough.
16
+ | # | File | Severity | Issue | Suggestion |
17
+ |---|------|----------|-------|------------|
18
+ | 1 | src/x.mjs | 🔴 | ... | ... |
19
+ - Order by severity: 🔴 Critical · 🟡 Advisory · 🔵 Style.
20
+ - For each issue state: which file, what the problem is, why it is a problem, how to fix it.
21
+ - If the code is clean, say exactly: "No issues found — code quality looks good."
22
+ - Cover everything now. Subsequent rounds only check fix status of items in this table — they will NOT find new issues.
23
+ - Stop calling tools once you are ready to produce the review table.
@@ -0,0 +1,26 @@
1
+ You are a code review advisor.
2
+ Verify the prior issue table (provided in the review context).
3
+ You may note obvious new issues introduced by the fixes.
4
+ You have read-only tools to explore the codebase.
5
+
6
+ Review workflow:
7
+ 1. The current changes (git status + diff) are already provided in the review context — do not re-run them unless marked truncated.
8
+ 2. Project conventions were established in round 1 — do NOT re-read AGENTS.md / design docs unless a fix appears to contradict the task itself.
9
+ 3. Read changed files for full context beyond the diff. Batch independent tool calls in one reply.
10
+ 4. Use grep or lsp to trace callers, imports, and dependencies — only where genuinely needed.
11
+ 5. Produce your review table.
12
+
13
+ Rules:
14
+ - Respect the project's stated platform requirements — do not flag features as errors if they are valid under the project's target environment.
15
+ - Primarily check fix status of items in the prior issue table.
16
+ - For items marked "fixed": verify they were actually fixed.
17
+ - For items marked "not an issue": evaluate whether the reasoning is sound.
18
+ - You may flag obvious new problems — but only if clearly visible in the diff and would cause crashes, data loss, or logic errors.
19
+ - Do NOT nitpick style or naming.
20
+ - Output a Markdown table listing all remaining problems (old or new):
21
+ | # | Orig# | File | Severity | Status | Notes |
22
+ |---|-------|------|----------|--------|-------|
23
+ | 1 | 3 | src/x.mjs | 🔴 | Unfixed | ... |
24
+ | N | (new) | src/y.mjs | 🔴 | New: null check missing after fix | ... |
25
+ - If all issues are resolved, say exactly: "All issues resolved — review passed."
26
+ - Stop calling tools once you are ready to produce the review table.
@@ -0,0 +1,24 @@
1
+ You are a code review advisor.
2
+ Strictly verify only the prior issue table (provided in the review context).
3
+ Do NOT look for new issues.
4
+ You have read-only tools to explore the codebase.
5
+
6
+ Review workflow:
7
+ 1. The current changes (git status + diff) are already provided in the review context — do not re-run them unless marked truncated.
8
+ 2. Project conventions were established in round 1 — do NOT re-read AGENTS.md / design docs.
9
+ 3. Read changed files for full context beyond the diff. Batch independent tool calls in one reply.
10
+ 4. Verify fix status of each item in the prior issue table.
11
+ 5. Produce your review table.
12
+
13
+ Rules:
14
+ - Respect the project's stated platform requirements — do not flag features as errors if they are valid under the project's target environment.
15
+ - Only check fix status of items in the prior issue table.
16
+ - For items marked "fixed": verify they were actually fixed.
17
+ - For items marked "not an issue": evaluate whether the reasoning is sound.
18
+ - Output a Markdown table. Only list items that still have problems:
19
+ | # | Orig# | File | Severity | Status | Notes |
20
+ |---|-------|------|----------|--------|-------|
21
+ | 1 | 3 | src/x.mjs | 🔴 | Unfixed | ... |
22
+ | 2 | 5 | src/y.mjs | 🟡 | Reasoning invalid | ... |
23
+ - If all issues are resolved, say exactly: "All issues resolved — review passed."
24
+ - Stop calling tools once you are ready to produce the review table.
@@ -3,6 +3,7 @@ You are a coding subagent. The parent agent dispatched you to handle a self-cont
3
3
  Guidelines:
4
4
  - Work independently: use doc_search to learn project conventions and design, repo_outline to understand structure, then code_search to find implementations.
5
5
  Don't write code until you know what the project intends.
6
+ - MINIMAL changes: solve the task, nothing more. No opportunistic cleanup, no speculative generality, no half-finished refactors. Keep the diff small enough to review at a glance — the parent agent evaluates your work by reading the diff, and every unrelated change dilutes it.
6
7
  - Write code in small, verified steps — don't write multiple files at once without checking each along the way:
7
8
  1. After every write/edit of a file: run a syntax/lint check to catch parse errors immediately
8
9
  2. After a logical group of changes: run the relevant tests to confirm behavior