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 +8 -0
- package/package.json +1 -1
- package/src/advisor.mjs +206 -31
- package/src/agent/helpers.mjs +11 -2
- package/src/agent.mjs +14 -7
- package/src/cli/make-agent.mjs +20 -0
- package/src/mcp/transport-stdio.mjs +4 -3
- package/src/mcp.mjs +1 -1
- package/src/prompts/advisor-round1.md +5 -5
- package/src/prompts/advisor-round2.md +4 -4
- package/src/prompts/advisor-round3.md +3 -3
- package/src/prompts/discipline.md +1 -1
- package/src/skills.mjs +67 -31
- package/src/tools/shared.mjs +27 -2
- package/src/tui/agent-turn.mjs +57 -12
- package/src/tui/cmd-advisor.mjs +190 -95
- package/src/tui/cmd-auto.mjs +6 -8
- package/src/tui/cmd-mcp.mjs +4 -2
- package/src/tui/cmd-plan.mjs +6 -8
- package/src/tui/cmd-think.mjs +93 -70
- package/src/tui/index.mjs +2 -1
- package/src/tui/interaction.mjs +2 -1
- package/src/tui/key-handler.mjs +0 -2
- package/src/tui/layout.mjs +3 -2
- package/src/tui/render-frame.mjs +67 -65
- package/src/tui/render-loop.mjs +43 -114
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
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
|
-
|
|
195
|
-
|
|
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
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
355
|
+
parts.push("1. The uncommitted changes are already provided above — do 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:
|
|
298
|
-
onReasoning:
|
|
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
|
-
|
|
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.`
|
package/src/agent/helpers.mjs
CHANGED
|
@@ -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
|
|
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:
|
|
288
|
-
// When advisor is enabled and guard is not explicitly disabled,
|
|
289
|
-
//
|
|
290
|
-
// (
|
|
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
|
|
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)
|
package/src/cli/make-agent.mjs
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
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.
|
|
7
|
-
2.
|
|
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
|
|
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.
|
|
8
|
-
2.
|
|
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.
|
|
8
|
-
2.
|
|
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
|
-
-
|
|
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.
|