thincoder 0.11.1 → 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.
package/README.md CHANGED
@@ -205,6 +205,14 @@ Code conventions: pure `.mjs`, no semicolons, no npm dependencies allowed (inclu
205
205
 
206
206
  ## Changelog
207
207
 
208
+ ### 0.12.0 (2026-07)
209
+ - **Interactive slash command UX** — `/advisor`, `/think`, `/config`, `/mcp` now use persistent menu loops with live state feedback. Toggle, change settings, and see results without re-entering the command. Cursor position is remembered across menu cycles. `/plan` and `/auto` now show immediate local feedback (`❯ Plan: ON/OFF`).
210
+ - **User-level AGENTS.md** — `~/.thincoder/AGENTS.md` is now loaded alongside the project-level `AGENTS.md`. User-level preferences (language, style, format) apply across all projects; project-level rules take priority.
211
+ - **Skill subdirectory format** — `.thincoder/skills/` now supports the standard `skill-name/SKILL.md` subdirectory convention (Claude Code / Cursor compatible). Flat `.md` files remain fully backward-compatible. Subdirectories take priority when both formats exist with the same name.
212
+ - **MCP stdio `env` field** — `env` key in MCP stdio server config is now merged into the child process environment. Enables MCP servers requiring custom environment variables (e.g. `deveco-mcp`).
213
+ - **Project-level `.mcp.json`** — `.mcp.json` in the project root is auto-loaded at startup (standard MCP client convention). Servers defined here are merged with `config.json` servers — `config.json` takes priority for same-named entries.
214
+ - **Cleaner conversations** — Removed redundant `[System reminder: ...]` injections from `/advisor`, `/plan`, `/auto`, and `/think` toggles. All feedback is now local TUI output, not conversation noise.
215
+
208
216
  ### 0.10.0 (2026-07)
209
217
  - **LSP tool** — `lsp` tool provides code intelligence via Language Server Protocol: go-to-definition, find-references, hover info, document symbols, diagnostics. Zero-dependency JSON-RPC 2.0 over stdio client. Lazy-starts language servers on first call. Configurable via `lsp.servers` in config.json (defaults: `typescript-language-server` for JS/TS, `pyright-langserver` for Python).
210
218
  - **Smart context: compaction checkpoint** — `compressIfNeeded` now auto-creates a git checkpoint before compaction. A checkpoint reference is injected after compaction so the model can reconstruct context from git diff + recent messages + task progress. Prevents information loss during long sessions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
package/src/advisor.mjs CHANGED
@@ -18,6 +18,15 @@
18
18
  * Round 3+: strict convergence — only checks the prior issue table.
19
19
  * No hard round cap — the convergence protocol naturally limits divergence.
20
20
  *
21
+ * Session memory (agent._advisorSession):
22
+ * All advisor calls within one run share a single conversation — round 2+
23
+ * just appends a follow-up (agent response + refreshed diff + round rules),
24
+ * so the advisor keeps its exploration context instead of re-discovering
25
+ * everything. The session is discarded when the run ends (runAgent resets it);
26
+ * the next task starts a fresh advisor session. After an app restart the
27
+ * in-memory session is gone — falls back to a fresh session seeded from the
28
+ * issue/response tables in the main history.
29
+ *
21
30
  *
22
31
  * Project customisation: .thincoder/advisor.md in the project root.
23
32
  */
@@ -32,7 +41,6 @@ import { toOpenAISchema } from "./tools/index.mjs"
32
41
  const __dirname = dirname(fileURLToPath(import.meta.url))
33
42
 
34
43
  const ADVISOR_MD_PATH = ".thincoder/advisor.md"
35
- const MAX_TASK_SUMMARY = 500
36
44
  const GIT_TIMEOUT = 5_000
37
45
 
38
46
  const DEFAULT_CRITERIA = `Review the code changes, focusing on:
@@ -180,6 +188,39 @@ function findReviewRepos(agent) {
180
188
 
181
189
  // ────────────────────────────────────────
182
190
 
191
+ /** Cap per-repo embedded diff — generous (large-context models); advisor can fetch the rest via its git tool */
192
+ const MAX_EMBEDDED_DIFF = 50_000
193
+
194
+ /**
195
+ * Collect git status + diff for each repo, embedded into the review context so
196
+ * the advisor doesn't need to spend its first tool calls discovering changes.
197
+ */
198
+ function collectRepoSnapshots(repos, cwd) {
199
+ const targets = repos.length > 0 ? repos : [cwd]
200
+ const parts = []
201
+ for (const repo of targets) {
202
+ let status = "", diff = ""
203
+ try {
204
+ status = execFileSync("git", ["status", "--porcelain"], {
205
+ cwd: repo, encoding: "utf8", timeout: GIT_TIMEOUT, stdio: ["ignore", "pipe", "pipe"],
206
+ }).trim()
207
+ diff = execFileSync("git", ["diff", "HEAD"], {
208
+ cwd: repo, encoding: "utf8", timeout: GIT_TIMEOUT, stdio: ["ignore", "pipe", "pipe"],
209
+ maxBuffer: 8 * 1024 * 1024,
210
+ })
211
+ } catch { continue /* not a git repo or git failed */ }
212
+ if (!status && !diff.trim()) continue
213
+ parts.push(`### ${repo}`)
214
+ if (status) parts.push("```", status, "```")
215
+ if (diff.trim()) {
216
+ const truncated = diff.length > MAX_EMBEDDED_DIFF
217
+ parts.push("```diff", truncated ? diff.slice(0, MAX_EMBEDDED_DIFF) : diff.trimEnd(), "```")
218
+ if (truncated) parts.push(`(diff truncated at ${MAX_EMBEDDED_DIFF} chars — use the git tool to see the rest)`)
219
+ }
220
+ }
221
+ return parts
222
+ }
223
+
183
224
  export function loadAdvisorMd(cwd) {
184
225
  const path = join(cwd, ADVISOR_MD_PATH)
185
226
  if (!existsSync(path)) return DEFAULT_CRITERIA
@@ -191,16 +232,46 @@ export function loadAdvisorMd(cwd) {
191
232
  }
192
233
  }
193
234
 
194
- function extractTaskSummary(history) {
195
- for (let i = history.length - 1; i >= 0; i--) {
235
+ const MAX_BACKGROUND_CHARS = 20_000
236
+ const MAX_BG_USER_CHARS = 2000
237
+ const MAX_BG_ASSISTANT_CHARS = 1500
238
+
239
+ /**
240
+ * Recent conversation context for the advisor: the last few user↔assistant
241
+ * exchanges (default 3 user turns). The last user message alone often lacks
242
+ * context ("把那个问题改一下" means nothing without the preceding turns) —
243
+ * the advisor needs the background to judge whether the changes match intent.
244
+ * Tool messages are skipped (noise); texts are truncated with generous caps
245
+ * (models have large context windows — completeness beats frugality).
246
+ */
247
+ export function extractConversationBackground(history, maxTurns = 3) {
248
+ const isNoise = (c) => c.startsWith("[System reminder:") || c.startsWith("[User interrupt:")
249
+ const picked = []
250
+ let userCount = 0
251
+ for (let i = history.length - 1; i >= 0 && userCount < maxTurns; i--) {
196
252
  const m = history[i]
197
- if (m.role !== "user") continue
198
- const content = typeof m.content === "string" ? m.content : ""
199
- if (content.startsWith("[System reminder:") || content.startsWith("[User interrupt:")) continue
200
- const firstPara = content.split("\n\n")[0]
201
- return firstPara.length > MAX_TASK_SUMMARY ? firstPara.slice(0, MAX_TASK_SUMMARY) + "" : firstPara
253
+ if (m.role !== "user" && m.role !== "assistant") continue
254
+ const content = typeof m.content === "string" ? m.content.trim() : ""
255
+ if (!content || isNoise(content)) continue
256
+ picked.unshift({ role: m.role === "user" ? "User" : "Assistant", text: content })
257
+ if (m.role === "user") userCount++
202
258
  }
203
- return null
259
+ if (picked.length === 0) return null
260
+
261
+ const lines = picked.map((e) => {
262
+ const cap = e.role === "User" ? MAX_BG_USER_CHARS : MAX_BG_ASSISTANT_CHARS
263
+ const text = e.text.length > cap ? e.text.slice(0, cap) + "…" : e.text
264
+ return `${e.role}: ${text}`
265
+ })
266
+ // Keep the most recent lines within the total budget
267
+ const out = []
268
+ let total = 0
269
+ for (let i = lines.length - 1; i >= 0; i--) {
270
+ if (out.length > 0 && total + lines[i].length > MAX_BACKGROUND_CHARS) break
271
+ total += lines[i].length
272
+ out.unshift(lines[i])
273
+ }
274
+ return out.join("\n")
204
275
  }
205
276
 
206
277
  // ────────────────────────────────────────
@@ -254,11 +325,21 @@ export function buildAdvisorUserMessage(agent, _prior) {
254
325
  parts.push(repoList)
255
326
  parts.push("")
256
327
 
257
- // Task summary
258
- const taskSummary = extractTaskSummary(agent.history)
259
- if (taskSummary) {
260
- parts.push("## Task")
261
- parts.push(taskSummary)
328
+ // Pre-collected changes — saves the advisor from spending its first tool
329
+ // calls on discovery (git status / git diff) every single round.
330
+ const snapshots = collectRepoSnapshots(repos, agent.cwd)
331
+ agent._advisorLastSnapshot = snapshots.join("\n") // dedup baseline for follow-up rounds
332
+ if (snapshots.length > 0) {
333
+ parts.push("## Current Changes (git status + git diff HEAD, pre-collected)")
334
+ parts.push(...snapshots)
335
+ parts.push("")
336
+ }
337
+
338
+ // Conversation background — recent user↔assistant exchanges for intent context
339
+ const background = extractConversationBackground(agent.history)
340
+ if (background) {
341
+ parts.push("## Conversation Background (recent turns)")
342
+ parts.push(background)
262
343
  parts.push("")
263
344
  }
264
345
 
@@ -268,34 +349,128 @@ export function buildAdvisorUserMessage(agent, _prior) {
268
349
  parts.push(criteria)
269
350
  parts.push("")
270
351
 
271
- // Instructions
352
+ // Instructions — round-aware: re-reviews skip convention discovery entirely
353
+ const isReReview = prior && (agent._advisorRound || 0) > 0
272
354
  parts.push("## Instructions")
273
- parts.push("1. Read `AGENTS.md` and any design documents firstunderstand project conventions, version requirements, and architecture decisions before flagging issues.")
274
- parts.push("2. Run `git diff HEAD` in each repo to discover uncommitted changes.")
275
- parts.push("3. `read` changed files for full context beyond the diff.")
276
- parts.push("4. Use `grep` or `lsp` to trace callers, imports, and dependencies.")
277
- parts.push("5. Produce your review table based on the review criteria above.")
355
+ parts.push("1. The uncommitted changes are already provided abovedo NOT re-run `git status` / `git diff` unless the embedded diff is marked truncated.")
356
+ if (isReReview) {
357
+ parts.push("2. Do NOT re-read AGENTS.md / design docs conventions were established in round 1. Focus on verifying the prior issue table against the current diff.")
358
+ parts.push("3. `read` only the files touched by the fixes. Batch independent reads/greps in a single reply.")
359
+ parts.push("4. Produce your verification table. Do not re-read content you already have.")
360
+ } else {
361
+ parts.push("2. Read `AGENTS.md` / design docs only if they exist (check once; do not re-probe with multiple patterns).")
362
+ parts.push("3. `read` changed files for full context beyond the diff. Batch independent reads/greps in a single reply instead of one call per round-trip.")
363
+ parts.push("4. Use `grep` or `lsp` to trace callers, imports, and dependencies — only where the diff leaves genuine doubt.")
364
+ parts.push("5. Produce your review table based on the review criteria above. Do not re-read content you already have.")
365
+ }
278
366
  parts.push("Do NOT flag features that are valid under the project's stated platform requirements.")
279
367
 
280
368
  return parts.join("\n")
281
369
  }
282
370
 
371
+ // ────────────────────────────────────────
372
+ // Session continuity — one advisor conversation per run
373
+ // ────────────────────────────────────────
374
+
375
+ /**
376
+ * Follow-up message for round 2+ in a continued advisor session.
377
+ * The advisor already has full context (its exploration, its issue table) in
378
+ * the conversation — the follow-up only carries what changed: the agent's
379
+ * response table, the fresh diff snapshot, and this round's rules.
380
+ */
381
+ export function buildAdvisorFollowUp(agent, _prior) {
382
+ const prior = _prior ?? extractPriorIssueTable(agent.history)
383
+ const round = (agent._advisorRound || 0) + 1
384
+ const response = (prior ? extractAgentResponseTable(agent.history, prior.sinceIdx) : null)
385
+ || "(Agent did not provide a response table — re-evaluate each issue)"
386
+ const rules = round === 2
387
+ ? "Verify each item in your prior issue table against the current changes. " +
388
+ "You may flag obvious NEW issues introduced by the fixes — but only crashes, data loss, or logic errors clearly visible in the diff. Do not nitpick style."
389
+ : "Strictly verify only your prior issue table against the current changes. Do NOT look for new issues."
390
+
391
+ const parts = [
392
+ `## Round ${round} — ${round === 2 ? "Verify Prior Table + Flag New Issues" : "Strict Verification"}`,
393
+ "",
394
+ rules,
395
+ "",
396
+ 'If every prior issue is resolved, say exactly: "All issues resolved — review passed."',
397
+ "",
398
+ "Do NOT re-read AGENTS.md / design docs or re-run git status/diff (current changes are below) — you already have full context from previous rounds.",
399
+ "",
400
+ "## Agent Response to Your Review",
401
+ response,
402
+ "",
403
+ ]
404
+ const snapshots = collectRepoSnapshots(findReviewRepos(agent), agent.cwd)
405
+ const snapshotText = snapshots.join("\n")
406
+ // Skip re-pushing an identical diff (e.g. advisor re-run without any file changes) —
407
+ // the previous snapshot is already in the conversation, duplicating it wastes tokens.
408
+ if (snapshotText && snapshotText === agent._advisorLastSnapshot) {
409
+ parts.push("## Current Changes", "(No changes since your previous review.)")
410
+ } else if (snapshots.length > 0) {
411
+ parts.push("## Current Changes (git status + git diff HEAD, refreshed)", ...snapshots)
412
+ }
413
+ agent._advisorLastSnapshot = snapshotText
414
+ return parts.join("\n")
415
+ }
416
+
417
+ /**
418
+ * Build or continue the advisor conversation for this run.
419
+ * First call in a run: fresh [system, user] session. Later calls: append a
420
+ * follow-up to the existing session so the advisor keeps its context.
421
+ * After an app restart (session lost), falls back to a fresh session whose
422
+ * system prompt is picked from history tables (round 2/3 style).
423
+ */
424
+ export function prepareAdvisorMessages(agent) {
425
+ const prior = extractPriorIssueTable(agent.history)
426
+ let session = agent._advisorSession
427
+ if (session) {
428
+ session.push({ role: "user", content: buildAdvisorFollowUp(agent, prior) })
429
+ return session
430
+ }
431
+ session = [
432
+ { role: "system", content: buildAdvisorSystemPrompt(agent, prior) },
433
+ { role: "user", content: buildAdvisorUserMessage(agent, prior) },
434
+ ]
435
+ return session
436
+ }
437
+
283
438
  // ────────────────────────────────────────
284
439
  // Advisor tool loop
285
440
  // ────────────────────────────────────────
286
441
 
442
+ /**
443
+ * Compact one-line summary of tool args for panel progress lines.
444
+ * Picks the most identifying field; falls back to truncated JSON.
445
+ */
446
+ function summarizeToolArgs(args) {
447
+ // e.g. "git diff HEAD", "read src/x.mjs" — action first when present
448
+ const parts = [args.action, args.path ?? args.pattern ?? args.command].filter((v) => v != null)
449
+ let s = parts.length > 0 ? parts.map(String).join(" ") : JSON.stringify(args)
450
+ s = s.replace(/\s+/g, " ").trim()
451
+ return s.length > 80 ? s.slice(0, 79) + "…" : s
452
+ }
453
+
287
454
  /**
288
455
  * Run the advisor's tool loop: chat → execute tools → repeat.
289
456
  * Stops when the model produces text without tool calls.
457
+ *
458
+ * Progress lines (→ tool args) are emitted via onOutput between model bursts so
459
+ * the panel keeps moving while the advisor explores — otherwise the panel sits
460
+ * frozen through every tool-call phase and the review appears to have stalled.
290
461
  */
291
462
  async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, cwd) {
463
+ // Kind-tagged wrappers: the TUI panel colors reasoning / answer / tool progress differently.
464
+ const emit = (kind) => (onOutput ? (text) => onOutput({ kind, text }) : undefined)
465
+ const onThink = emit("think")
466
+ const onText = emit("text")
292
467
  while (true) {
293
468
  const response = await chat(provider, {
294
469
  messages,
295
470
  tools: ADVISOR_TOOL_SCHEMAS,
296
471
  signal: (signal && !signal.aborted) ? signal : new AbortController().signal,
297
- onToken: onOutput,
298
- onReasoning: onOutput,
472
+ onToken: onText,
473
+ onReasoning: onThink,
299
474
  })
300
475
 
301
476
  // No tool calls — this is the final review text
@@ -317,12 +492,14 @@ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, c
317
492
  // Execute each tool call
318
493
  for (const tc of response.toolCalls) {
319
494
  const tool = ADVISOR_TOOL_BY_NAME.get(tc.name)
495
+ let args = {}
496
+ try { args = JSON.parse(tc.arguments || "{}") } catch { /* summarized as raw JSON below */ }
497
+ onOutput?.({ kind: "tool", text: `\n→ ${tc.name} ${summarizeToolArgs(args)}\n` })
320
498
  let result
321
499
  if (!tool) {
322
500
  result = `Error: unknown tool "${tc.name}". Available: ${[...ADVISOR_TOOL_BY_NAME.keys()].join(", ")}`
323
501
  } else {
324
502
  try {
325
- const args = JSON.parse(tc.arguments || "{}")
326
503
  result = await tool.execute(args, {
327
504
  cwd,
328
505
  agent,
@@ -372,20 +549,18 @@ export async function runAdvisorReview(agent, onOutput, signal) {
372
549
  if ((agent._touchedFiles ?? []).length === 0) return null
373
550
 
374
551
  const provider = resolveAdvisorProvider(agent)
375
- const priorIssue = extractPriorIssueTable(agent.history)
376
- const systemPrompt = buildAdvisorSystemPrompt(agent, priorIssue)
377
- const userMessage = buildAdvisorUserMessage(agent, priorIssue)
378
552
 
379
553
  // Set the advisor's cwd to the first repo (for tool context)
380
554
  const advisorCwd = repos.length > 0 ? repos[0] : agent.cwd
381
555
 
382
- const messages = [
383
- { role: "system", content: systemPrompt },
384
- { role: "user", content: userMessage },
385
- ]
556
+ const messages = prepareAdvisorMessages(agent)
386
557
 
387
558
  try {
388
- return await runAdvisorToolLoop(provider, messages, onOutput, signal, agent, advisorCwd)
559
+ const result = await runAdvisorToolLoop(provider, messages, onOutput, signal, agent, advisorCwd)
560
+ // Persist the conversation: the next advisor call in this run continues here
561
+ // (reset by runAgent when the run ends — each task gets a fresh advisor session)
562
+ agent._advisorSession = messages
563
+ return result
389
564
  } catch (e) {
390
565
  if (e.name === "AbortError" && signal?.reason?.interrupt) throw e
391
566
  return `Advisor: review failed — ${e.message || "unknown error"}. You may retry or proceed to verify.`
@@ -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"
@@ -178,14 +179,22 @@ export function readonlyToolNames(tools) {
178
179
 
179
180
  const MAX_INSTRUCTION_CHARS = 32_000
180
181
 
181
- /** 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. */
182
185
  export async function loadProjectInstructions(cwd) {
183
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
184
194
  for (const name of ["AGENTS.md", "project_rules.md"]) {
185
195
  try {
186
196
  const content = readFileSync(join(cwd, name), "utf8").trim()
187
197
  if (!content) continue
188
- const key = name.toLowerCase()
189
198
  parts.push(`<!-- From: ${join(cwd, name)} -->\n${content}`)
190
199
  } catch { /* file does not exist */ }
191
200
  }
package/src/agent.mjs CHANGED
@@ -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({
@@ -62,7 +63,7 @@ export function createAgent({
62
63
  overlay, tasks, history,
63
64
  planMode, autoApprove, goal,
64
65
  _mutatedThisRun: false, _verifiedThisRun: false, _verifyPassed: undefined, _calledAdvisorThisRun: false,
65
- _touchedFiles: [], _verifyRetries: 0, _advisorRound: 0,
66
+ _touchedFiles: [], _verifyRetries: 0, _advisorRound: 0, _advisorSession: null,
66
67
  _pendingReminders: [],
67
68
  _pendingTimers: [],
68
69
  _sessionStart: sessionStart,
@@ -85,7 +86,9 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
85
86
  agent._touchedFiles = []
86
87
  agent._verifyRetries = 0
87
88
  agent._advisorRound = 0
89
+ agent._advisorSession = null // advisor session is per-run: discard when the task ends, next task starts fresh
88
90
  let guardPushbacks = 0
91
+ let advisorPushbacks = 0
89
92
  let honestReminderInjected = false
90
93
  const recentCallSigs = []
91
94
  // repeat: "once" stream rules fire at most once per runAgent call (user turn):
@@ -284,16 +287,19 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
284
287
  continue
285
288
  }
286
289
  }
287
- // --- advisor guard: push model to review changes before completion ---
288
- // When advisor is enabled and guard is not explicitly disabled, mutated files
289
- // must be reviewed before the turn ends. No hard round cap convergence protocol
290
- // (round 3+ strict verification) naturally limits divergence.
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.
291
295
  if (depth === 0 && agent.config?.advisor?.enabled && agent.config?.advisor?.guard !== false) {
292
- if (agent._mutatedThisRun && !agent._calledAdvisorThisRun && (agent._touchedFiles ?? []).length > 0) {
296
+ if (agent._mutatedThisRun && !agent._calledAdvisorThisRun && (agent._touchedFiles ?? []).length > 0
297
+ && advisorPushbacks < MAX_ADVISOR_PUSHBACKS) {
298
+ advisorPushbacks++
293
299
  agent.history.push({ role: "assistant", content: response.content })
294
300
  agent.history.push({
295
301
  role: "user",
296
- content: `[System reminder: you changed code in this run but haven't reviewed with advisor (round ${agent._advisorRound + 1}). Call the \`advisor\` tool to get an independent code review. After the review, produce a response table for every issue found (see discipline rules for format). If the changes are trivial (typo, one-liner, formatting only), you may skip and explain why in your reply.]`,
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).]`,
297
303
  })
298
304
  callbacks.onTurnEnd?.(agent, turn)
299
305
  continue
@@ -333,6 +339,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
333
339
 
334
340
  // Model is executing tools → doing real work, reset guard pushback counter
335
341
  guardPushbacks = 0
342
+ advisorPushbacks = 0
336
343
 
337
344
  for (const { toolCall, result, ok } of results) {
338
345
  const tool = toolByName.get(toolCall.name)
@@ -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) {
@@ -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)
@@ -3,14 +3,14 @@ Perform a full-scope review of the code changes.
3
3
  You have read-only tools to explore the codebase.
4
4
 
5
5
  Review workflow:
6
- 1. Read AGENTS.md and any design documents to understand project conventions, version requirements, and architecture decisions.
7
- 2. Run git diff HEAD to discover uncommitted changes.
8
- 3. Read changed files for full context.
9
- 4. Use grep or lsp to trace callers, imports, and dependencies.
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
10
  5. Produce your review table.
11
11
 
12
12
  Rules:
13
- - Reply in the same language as the task summary.
13
+ - Reply in the same language as the conversation background.
14
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
15
  - Output a Markdown table. This table becomes the sole basis for convergence in later rounds — be thorough.
16
16
  | # | File | Severity | Issue | Suggestion |
@@ -4,10 +4,10 @@ You may note obvious new issues introduced by the fixes.
4
4
  You have read-only tools to explore the codebase.
5
5
 
6
6
  Review workflow:
7
- 1. Read AGENTS.md and any design documents to understand project conventions, version requirements, and architecture decisions.
8
- 2. Run git diff HEAD to see what changed since the last review.
9
- 3. Read changed files for full context.
10
- 4. Use grep or lsp to trace callers, imports, and dependencies.
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
11
  5. Produce your review table.
12
12
 
13
13
  Rules:
@@ -4,9 +4,9 @@ Do NOT look for new issues.
4
4
  You have read-only tools to explore the codebase.
5
5
 
6
6
  Review workflow:
7
- 1. Read AGENTS.md and any design documents to understand project conventions, version requirements, and architecture decisions.
8
- 2. Run git diff HEAD to see what changed since the last review.
9
- 3. Read changed files for full context.
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
10
  4. Verify fix status of each item in the prior issue table.
11
11
  5. Produce your review table.
12
12
 
@@ -81,7 +81,7 @@ Testing discipline (right check at the right time):
81
81
  - If advisor says "all clear": proceed to verify.
82
82
  - If issues persist: fix them, update your response table, re-run advisor.
83
83
  - No hard round cap — the convergence protocol naturally limits divergence.
84
- - The advisor is optional if you only made trivial changes (typo, one-liner).
84
+ - **Calling advisor is mandatory when it is enabled and you changed code** — it is not your call to skip, even for trivial changes (a trivial diff makes the review fast, not optional). The run cannot finish until advisor has reviewed the changes.
85
85
 
86
86
  Debugging strategy (when something goes wrong, three steps before anything else):
87
87
  - **Step 0 — Set a timer before you start reasoning**: immediately call `timer(180, "试试加个日志?")` to give yourself a bounded thinking window.