thincoder 0.12.2 → 0.12.4
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 +29 -6
- package/package.json +3 -3
- package/src/advisor/history.mjs +112 -0
- package/src/advisor/messages.mjs +182 -0
- package/src/advisor/repos.mjs +133 -0
- package/src/advisor/run.mjs +346 -0
- package/src/advisor.mjs +109 -509
- package/src/agent/completion.mjs +134 -0
- package/src/agent/dispatch.mjs +54 -7
- package/src/agent/post-turn.mjs +70 -0
- package/src/agent/setup.mjs +95 -6
- package/src/agent-tools/advisor.mjs +159 -12
- package/src/agent-tools/eng.mjs +64 -0
- package/src/agent-tools/subagent.mjs +73 -3
- package/src/agent-tools/task.mjs +45 -6
- package/src/agent-tools/verify.mjs +18 -0
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +152 -161
- package/src/cli/make-agent.mjs +1 -0
- package/src/cli/setup-wizard.mjs +1 -0
- package/src/config.mjs +34 -4
- package/src/context.mjs +47 -13
- package/src/generate-title.mjs +44 -0
- package/src/prompts/advisor-design.md +43 -0
- package/src/prompts/advisor-round1.md +11 -4
- package/src/prompts/advisor-round2.md +12 -7
- package/src/prompts/advisor-round3.md +11 -6
- package/src/prompts/coder.md +9 -3
- package/src/prompts/discipline.md +12 -96
- package/src/prompts/eng-coder.md +34 -0
- package/src/prompts/engineering-sub.md +12 -0
- package/src/prompts/engineering.md +96 -0
- package/src/prompts/main.md +1 -1
- package/src/prompts/methodology-template.md +39 -0
- package/src/prompts/plan.md +2 -2
- package/src/prompts/system.md +43 -61
- package/src/provider/core.mjs +58 -2
- package/src/session.mjs +291 -94
- package/src/skills.mjs +48 -15
- package/src/tools/apply_patch.md +1 -1
- package/src/tools/checklist.mjs +4 -3
- package/src/tools/codemode.mjs +23 -11
- package/src/tools/delete.md +1 -0
- package/src/tools/edit.md +1 -1
- package/src/tools/execute.md +5 -0
- package/src/tools/file.mjs +4 -0
- package/src/tools/git.md +15 -0
- package/src/tools/git.mjs +1 -6
- package/src/tools/lint.md +8 -0
- package/src/tools/linter.mjs +1 -5
- package/src/tools/lsp.md +7 -0
- package/src/tools/lsp.mjs +8 -9
- package/src/tools/patch.mjs +1 -29
- package/src/tools/read_image.md +5 -1
- package/src/tools/system.mjs +1 -1
- package/src/tools/web.mjs +3 -3
- package/src/tui/agent-turn.mjs +184 -66
- package/src/tui/ansi.mjs +4 -0
- package/src/tui/clipboard.mjs +9 -0
- package/src/tui/cmd-config.mjs +14 -26
- package/src/tui/cmd-eng.mjs +44 -0
- package/src/tui/cmd-exit.mjs +1 -1
- package/src/tui/cmd-fold.mjs +3 -4
- package/src/tui/cmd-model.mjs +11 -6
- package/src/tui/cmd-new.mjs +5 -5
- package/src/tui/cmd-session.mjs +21 -11
- package/src/tui/cmd-think.mjs +1 -0
- package/src/tui/index.mjs +20 -9
- package/src/tui/key-handler.mjs +177 -9
- package/src/tui/layout.mjs +5 -5
- package/src/tui/markdown.mjs +52 -0
- package/src/tui/pickers.mjs +190 -45
- package/src/tui/render-conversation.mjs +54 -13
- package/src/tui/render-frame.mjs +39 -12
- package/src/tui/render-loop.mjs +2 -1
- package/src/tui/render.mjs +13 -7
- package/src/tui/slash-commands.mjs +11 -7
- package/src/tui/startup.mjs +4 -3
- package/src/tui/wizard.mjs +3 -0
- package/src/tools/checkpoint.md +0 -15
- package/src/tools/git_diff.md +0 -11
- package/src/tools/git_log.md +0 -10
- package/src/tools/git_status.md +0 -8
- package/src/tools/linter.md +0 -13
- package/src/tools/syntax_check.md +0 -10
package/README.md
CHANGED
|
@@ -15,17 +15,17 @@ Design philosophy (the entire meaning of the name): if the Node standard library
|
|
|
15
15
|
- **Fix-verify loop**: file changes without `verify` get pushed back — syntax check + tests must pass before the agent can claim completion (auto-repair up to 3 rounds)
|
|
16
16
|
- **Checkpoint system**: auto-snapshot before every user task, `list`/`create`/`rewind` tools for the model, single-file restore — rewinding itself is reversible (pre-rewind state auto-saved)
|
|
17
17
|
- **Codebase understanding** ⭐0.5.0: `repo_outline` (dependency outline, auto-injected at startup), `code_search` (source FTS5 + vectors + JSDoc extraction), `doc_search` (docs chunked by ## headings) — background indexing, auto-incremental updates on file writes, three tools guided by "structure → intent → details"
|
|
18
|
-
- **Model adaptation** ⭐: top-tier only, latest only. Built-in flagship models from
|
|
18
|
+
- **Model adaptation** ⭐: top-tier only, latest only. Built-in flagship models from twelve providers — DeepSeek / Kimi / GLM / Qwen / MiniMax / OpenAI / Claude / Gemini / Grok / Mistral / Volcengine Ark (豆包) / Hunyuan (腾讯混元) / SiliconFlow (硅基流动) / OpenRouter / Groq. No legacy model compatibility, no local model support. Auto-matched context windows, truncation-resume protocols (prefix/partial), thinking-mode APIs (thinking.type / reasoning_effort), reasoning_content echo strategies (reasoningEcho), output limits, temperature range clamping — all deeply adapted.
|
|
19
19
|
- **Toolset**: `read` / `write` / `edit` / `bash` / `glob` (supports `**`) / `grep` / `websearch` / `ls` / `fetch` + `read_image` (image/video paste) + three retrieval tools + MCP — all zero-dependency, file tools confined to the working directory
|
|
20
20
|
- **Memory system**: three layers (personal/project/team), FTS5 + vector RRF hybrid retrieval, git-friendly markdown format
|
|
21
21
|
- **Two-phase tool scheduling**: permission prompts serialized, read-only tools parallelized, side-effect tools serialized
|
|
22
|
-
- **Session persistence** ⭐0.5.0: up to 5 archive slots, `/session` to switch anytime, tool results visible after restore
|
|
22
|
+
- **Session persistence** ⭐0.5.0: up to 5 archive slots, `/session` to switch anytime, tool results visible after restore. Process-level isolation — multiple instances in the same directory each get their own session slot
|
|
23
23
|
- **Concurrent subagents**: three roles — `explore`/`plan`/`coder` — dispatched in parallel, streaming output visible, reports land in the conversation
|
|
24
24
|
- **Plan Mode**: read-only exploration + design, implement after user approval
|
|
25
25
|
- **AUTO mode**: `/auto` full authorization, no confirmations on long tasks
|
|
26
26
|
- **Task tracking**: `task` tool breaks down multi-step work, status bar ✓n/m live progress, auto-filters completed items
|
|
27
27
|
- **Goal/Verify/Skills**: long-goal tracking, completion verification, reusable skills
|
|
28
|
-
- **Streaming TUI**: bare ANSI, permission preview right above the input box, write/edit auto-shows diffs, paste shortcut hint in the input box corner for multimodal models (Win: Alt+V / Mac/Linux: Ctrl+V)
|
|
28
|
+
- **Streaming TUI**: bare ANSI, permission preview right above the input box, write/edit auto-shows diffs, paste shortcut hint in the input box corner for multimodal models (Win: Alt+V / Mac/Linux: Ctrl+V). Two-level model picker (providers → models), search/filter support, Shift+Enter for multiline input
|
|
29
29
|
|
|
30
30
|
## Memory: What One Learns, the Whole Team Knows
|
|
31
31
|
|
|
@@ -85,10 +85,12 @@ thincoder upgrade
|
|
|
85
85
|
|
|
86
86
|
Running from source: replace `thincoder` above with `node bin/thincoder.mjs`.
|
|
87
87
|
|
|
88
|
-
Slash commands in the TUI: `/help`, `/model` (
|
|
88
|
+
Slash commands in the TUI: `/help`, `/model` (two-level picker: first select provider, then model; `/model <provider>:<name>` switches directly), `/provider` (add/remove providers, set keys, custom endpoints), `/think` (thinking mode toggle and reasoning effort), `/config` (view config, `/config embedkey` for the embedding key, `/config set` for parameters), `/session` (list/switch archived sessions), `/reindex` (rebuild the index), `/extract` (extract knowledge from the current session), `/restore` (restore checkpoint), `/clear`, `/exit`. High-frequency commands support abbreviations: `/h` `/x` `/m` `/p` `/t` `/c` `/n`. Typing `/` shows live matching hints in the status bar. Model picker supports search/filter — type to narrow down results.
|
|
89
89
|
|
|
90
90
|
Environment variables: `THINCODER_API_KEY` (or `DEEPSEEK_API_KEY` / `OPENAI_API_KEY`), `THINCODER_BASE_URL`, `THINCODER_MODEL`, `SILICONFLOW_API_KEY`.
|
|
91
91
|
|
|
92
|
+
> **Kimi note**: Kimi has **two separate platforms with non-interchangeable API keys** — Moonshot (`https://api.moonshot.cn/v1`, keys `sk-...`, platform.moonshot.cn) and **Kimi For Coding** (`https://api.kimi.com/coding/v1`, keys `sk-kimi-...`, platform.kimi.com, model ID `k3`). Use the `kimi` preset for Moonshot and `kimi-code` for Kimi For Coding — putting one platform's key on the other's endpoint fails with 401 (a hint is appended when the key/baseURL look mismatched).
|
|
93
|
+
|
|
92
94
|
## Configuration
|
|
93
95
|
|
|
94
96
|
`~/.thincoder/config.json`:
|
|
@@ -164,14 +166,15 @@ src/
|
|
|
164
166
|
memory/ three-layer memory — schema.mjs (DDL/constants), core.mjs (CRUD + retrieval),
|
|
165
167
|
code-index.mjs + code-sync.mjs (code_chunks), docs.mjs (doc_chunks)
|
|
166
168
|
memory.mjs re-export shim → src/memory/*
|
|
167
|
-
session.mjs session persistence (up to 5 archive slots, isolated by project cwd)
|
|
169
|
+
session.mjs session persistence (up to 5 archive slots, isolated by project cwd, process-level isolation via sessionId + slotSessions)
|
|
168
170
|
skills.mjs skill discovery/loading (.thincoder/skills/*.md)
|
|
169
171
|
markdown.mjs entry format (frontmatter parse/serialize)
|
|
170
172
|
git/ checkpoint.mjs (git patch snapshots / rewind), gitmem.mjs (Team layer git sync)
|
|
171
173
|
distill.mjs session knowledge extraction (candidates + human confirmation)
|
|
172
174
|
config.mjs config loading
|
|
173
175
|
tui/ bare-ANSI terminal UI — index.mjs (startTUI), render.mjs (drawing primitives),
|
|
174
|
-
render-frame.mjs (frame layout),
|
|
176
|
+
render-frame.mjs (frame layout), render-conversation.mjs (conversation panel),
|
|
177
|
+
markdown.mjs (lightweight inline markdown → ANSI), ansi.mjs
|
|
175
178
|
tui.mjs re-export shim → src/tui/index.mjs
|
|
176
179
|
tui-render.mjs re-export shim → src/tui/render.mjs
|
|
177
180
|
prompts/ prompt texts — system.md (core), discipline.md (coding/testing rules),
|
|
@@ -205,6 +208,26 @@ Code conventions: pure `.mjs`, no semicolons, no npm dependencies allowed (inclu
|
|
|
205
208
|
|
|
206
209
|
## Changelog
|
|
207
210
|
|
|
211
|
+
### 0.12.4 (2026-08)
|
|
212
|
+
- **Context compaction unified spec** — CLI/VS Code now share one compaction semantics (`docs/design/CONTEXT-COMPACTION.md`): window-adaptive tail size (`max(10, ctx/100K×30)`, ≤40% of history), measured prompt-token baseline preferred, pure-estimation path includes system+tools overhead, head/tail tool-call pairing protection on both sides, 3-tier fallback (LLM summary → deterministic truncation after 3 failures → per-message shrink), compaction summaries are silent to the frontend (no streaming into the conversation), task re-injection deduplicated. Unknown model names now warn once instead of silently degrading to the 128K default
|
|
213
|
+
- **Kimi For Coding support** — new `kimi-code` preset (`api.kimi.com/coding/v1`, model `k3`, platform.kimi.com, `sk-kimi-` keys — NOT interchangeable with Moonshot); `MODEL_SPECS` gains the `k3` alias (1M context / multimodal / partialMode / reasoningEcho); 401 errors hint at the two-platform key mismatch; README documents the split
|
|
214
|
+
- **Ctrl+C double-confirm** — idle-state first Ctrl+C only warns (3s window), second press exits; picker-cancel and in-flight abort semantics unchanged
|
|
215
|
+
- **Empty-response auto-retry** — a transient empty LLM response (reasoning exhausted / truncated output) injects a retry reminder instead of aborting the whole turn; after 2 consecutive empties the original error surfaces (with the `/think` lowering hint)
|
|
216
|
+
- **Lightweight inline markdown display** — model replies render `**bold**`, `` `code` `` (reverse video), `~~strike~~`, and `# headings` (marker-stripped + bold) via ANSI instead of showing literal markers; code spans are not re-interpreted; streaming-safe on unclosed markers; copied text comes out clean
|
|
217
|
+
|
|
218
|
+
### 0.12.3 (2026-08)
|
|
219
|
+
- **Fix: user-level skills loading** — skills in `~/.thincoder/skills/` are now properly discovered and loaded alongside project-level skills. Project-level skills with the same name take priority. Both `skill list` and `skill load` support both directories.
|
|
220
|
+
|
|
221
|
+
### 0.12.2 (2026-08)
|
|
222
|
+
- **Session isolation for multiple processes** — each process gets a unique session ID (`pid-timestamp-random`), manifest tracks slot ownership via `slotSessions` mapping. Concurrent sessions in the same directory automatically get separate slots. Dead process slots are intelligently reclaimed via `isProcessAlive()` check
|
|
223
|
+
- **Two-level model picker** — `/model` now shows providers first, then models for selected provider. Reduces visual clutter when many providers are configured. Direct switching still works: `/model qwen:qwen-max`
|
|
224
|
+
- **Picker improvements** — auto-scroll keeps selected item visible, "type to filter" hint shown, search/filter support added
|
|
225
|
+
- **Shift+Enter multiline input** — TUI now supports Shift+Enter for multiline input (regular Enter still submits)
|
|
226
|
+
- **New provider presets** — added Volcengine Ark (豆包), Hunyuan (腾讯混元), SiliconFlow (硅基流动), OpenRouter, Groq (5 new providers, total now 12)
|
|
227
|
+
- **Execute tool security hardening** — blocks dynamic `import()` calls to prevent sandbox escape. Pre-execution regex check + sandbox blocks `require()` and `process` access
|
|
228
|
+
- **Advisor system overhaul** — convergence hardening with round-aware prompts, evidence rules, round cap (5 max). Design-review token gate. Simplified prompts, removed git dependency. Unified streaming block
|
|
229
|
+
- **Verify guard improvements** — file path validation, error context enrichment, test coverage expanded
|
|
230
|
+
|
|
208
231
|
### 0.12.1 (2026-07)
|
|
209
232
|
- **Fix: `/exit` screen artifacts** — `/exit` now uses synchronous `process.exit(0)` instead of the deferred cleanup callback, preventing the post-handler `render()` from redrawing the TUI over the cleaned terminal. Ctrl+C and `/exit` now produce identical clean exits.
|
|
210
233
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thincoder",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.4",
|
|
4
4
|
"description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"url": "https://gitee.com/shanghai-xinbo/thincoder.git"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
|
-
"test": "node --test",
|
|
34
|
-
"prepublishOnly": "node --test"
|
|
33
|
+
"test": "node --test \"test/*.mjs\"",
|
|
34
|
+
"prepublishOnly": "node --test \"test/*.mjs\""
|
|
35
35
|
},
|
|
36
36
|
"author": "liwei <liwei@51marine.com> (上海新舶)",
|
|
37
37
|
"license": "MIT"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* advisor/history.mjs — advisor history extraction: issue/response tables and conversation background.
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync } from "node:fs"
|
|
5
|
+
import { join } from "node:path"
|
|
6
|
+
|
|
7
|
+
export const ADVISOR_MD_PATH = ".thincoder/advisor.md"
|
|
8
|
+
export const ADVISOR_TABLE_HEADER = "| # | File | Severity | Issue | Suggestion |"
|
|
9
|
+
export const CONVERGENCE_TABLE_HEADER = "| # | Orig# | File | Severity | Status | Notes |"
|
|
10
|
+
export const AGENT_RESPONSE_HEADER = "| # | Action | Detail |"
|
|
11
|
+
export const LEGACY_ADVISOR_HEADER = "| # | 文件 | 严重程度 | 问题描述 | 建议修复 |"
|
|
12
|
+
|
|
13
|
+
const DEFAULT_CRITERIA = `Review the code changes, focusing on:
|
|
14
|
+
1. Correctness: logic errors, edge cases, off-by-one, incomplete modifications
|
|
15
|
+
2. Security: unhandled exceptions, null references, resource leaks, race conditions
|
|
16
|
+
3. Consistency: alignment with existing project patterns and conventions
|
|
17
|
+
4. Completeness: missing callers, imports, or follow-up changes
|
|
18
|
+
5. Maintainability: vague naming, missing comments, overly complex logic`
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extract the most recent advisor review table from history.
|
|
22
|
+
* Returns { text, sinceIdx } where sinceIdx is the index of the advisor call's
|
|
23
|
+
* own history entry — extractAgentResponseTable skips it (role is "tool", not
|
|
24
|
+
* "assistant") and scans forward for the agent's response table.
|
|
25
|
+
* Returns null when: no advisor call, empty output, or the last review is
|
|
26
|
+
* all-clear (nothing to follow up on).
|
|
27
|
+
*/
|
|
28
|
+
export function extractPriorIssueTable(history) {
|
|
29
|
+
// allClear: exact phrases the prompts instruct the advisor to use on a clean review.
|
|
30
|
+
// NOTE: "已修复" (Fixed) is NOT here — it's a per-row Status value in convergence tables,
|
|
31
|
+
// and a mixed table must continue convergence even if some rows are Fixed.
|
|
32
|
+
const allClear = ["no 🔴", "all clear", "全部通过", "review passed", "no issues found", "no new issues"]
|
|
33
|
+
// Negative signals: a table listing SOME items as unfixed is NOT all-clear.
|
|
34
|
+
// Applied when the message carries a Status column (English or Chinese convergence
|
|
35
|
+
// format) — "failed"/"❌" in a round-1 Issue description must NOT trigger it.
|
|
36
|
+
const partiallyFixedRe = /\bunfixed\b|未修复|❌|\bfailed\b/i
|
|
37
|
+
const entries = Array.isArray(history) ? history : []
|
|
38
|
+
|
|
39
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
40
|
+
const m = entries[i]
|
|
41
|
+
if (m.role !== "tool" || typeof m.content !== "string") continue
|
|
42
|
+
// Only review outputs carry one of these table headers. Matched at LINE START:
|
|
43
|
+
// an `includes()` match would also fire on advisor output that quotes the
|
|
44
|
+
// header constants' own source code (e.g. history.mjs), producing a phantom
|
|
45
|
+
// "prior issue table" and re-opening convergence rounds against stale data.
|
|
46
|
+
if (!lineHasHeader(m.content, ADVISOR_TABLE_HEADER)
|
|
47
|
+
&& !lineHasHeader(m.content, CONVERGENCE_TABLE_HEADER)
|
|
48
|
+
&& !lineHasHeader(m.content, LEGACY_ADVISOR_HEADER)) continue
|
|
49
|
+
const text = m.content
|
|
50
|
+
const lower = text.toLowerCase()
|
|
51
|
+
// Has a Status column? (English convergence format or any table with a Status-like
|
|
52
|
+
// column) — Chinese legacy tables lack it, but they are issue tables, not convergence.
|
|
53
|
+
const hasStatusColumn = lineHasHeader(text, CONVERGENCE_TABLE_HEADER)
|
|
54
|
+
|| /Status|状态/.test(text.slice(0, text.indexOf("\n") + 1))
|
|
55
|
+
if (hasStatusColumn && partiallyFixedRe.test(lower)) return { text, sinceIdx: i }
|
|
56
|
+
if (allClear.some((s) => lower.includes(s))) return null
|
|
57
|
+
// sinceIdx = the advisor call's own index; extractAgentResponseTable skips it (role !== assistant)
|
|
58
|
+
return { text, sinceIdx: i }
|
|
59
|
+
}
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** True when some line of `text` starts with `header` — table headers always sit at line start. */
|
|
64
|
+
function lineHasHeader(text, header) {
|
|
65
|
+
return text.split("\n").some((l) => l.trimStart().startsWith(header))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Extract the agent's response table (| # | Action | Detail |) that follows
|
|
70
|
+
* the advisor review. Returns null when missing or no advisor review precedes.
|
|
71
|
+
*/
|
|
72
|
+
export function extractAgentResponseTable(history, sinceIdx) {
|
|
73
|
+
const entries = Array.isArray(history) ? history : []
|
|
74
|
+
for (let i = sinceIdx ?? 0; i < entries.length; i++) {
|
|
75
|
+
const m = entries[i]
|
|
76
|
+
if (m.role !== "assistant" || typeof m.content !== "string") continue
|
|
77
|
+
if (m.content.includes(AGENT_RESPONSE_HEADER)) return m.content
|
|
78
|
+
}
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Load review criteria: project .thincoder/advisor.md if present,
|
|
84
|
+
* otherwise the built-in defaults.
|
|
85
|
+
*/
|
|
86
|
+
export function loadAdvisorMd(cwd) {
|
|
87
|
+
try {
|
|
88
|
+
return readFileSync(join(cwd, ADVISOR_MD_PATH), "utf8")
|
|
89
|
+
} catch {
|
|
90
|
+
return DEFAULT_CRITERIA
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Extract recent user↔assistant exchanges (up to maxTurns) for intent context. */
|
|
95
|
+
export function extractConversationBackground(history, maxTurns = 3) {
|
|
96
|
+
const entries = Array.isArray(history) ? history : []
|
|
97
|
+
const lines = []
|
|
98
|
+
let turns = 0
|
|
99
|
+
for (let i = entries.length - 1; i >= 0 && turns < maxTurns; i--) {
|
|
100
|
+
const m = entries[i]
|
|
101
|
+
if (m.role === "tool" || m.role === "system") continue
|
|
102
|
+
if (typeof m.content !== "string") continue
|
|
103
|
+
if (m.content.startsWith("[System reminder:") || m.content.startsWith("[System mode:") || m.content.startsWith("[Relevant memories")) continue
|
|
104
|
+
// ^ [System mode:] is not currently generated anywhere — kept as forward-looking
|
|
105
|
+
// defensive filtering in case convergence messages ever adopt a different prefix.
|
|
106
|
+
if (m.role === "user" || m.role === "assistant") {
|
|
107
|
+
lines.unshift(`${m.role === "user" ? "User" : "Assistant"}: ${m.content.slice(0, 400)}`)
|
|
108
|
+
if (m.role === "user") turns++
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return lines.length > 0 ? lines.join("\n\n") : null
|
|
112
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* advisor/messages.mjs — advisor user-message building (buildAdvisorUserMessage).
|
|
3
|
+
* Split out of advisor.mjs to keep it under the 300-line advisory threshold
|
|
4
|
+
* (.thincoder/advisor.md). System prompts live in advisor.mjs / prompts/.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from "node:fs"
|
|
7
|
+
import { resolve } from "node:path"
|
|
8
|
+
import { findReviewRepos, collectRepoSnapshots, collectChangedFiles } from "./repos.mjs"
|
|
9
|
+
import { loadAdvisorMd, extractConversationBackground, extractAgentResponseTable, extractPriorIssueTable } from "./history.mjs"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build the user message for an advisor review session.
|
|
13
|
+
* @param {Object} agent — the parent agent
|
|
14
|
+
* @param {Object|null} [_prior] — prior issue table
|
|
15
|
+
* @param {string} [reviewType] — "design" or "code" (default)
|
|
16
|
+
* @param {string|null} [designToken] — token injected into the design-review prompt; the advisor echoes it only on approval
|
|
17
|
+
* @param {string[]|null} [documents] — design review only: explicit list of doc paths to review (requirements + design + referenced docs).
|
|
18
|
+
* When set, the review input is built from this list ONLY — no git-diff change-set collection.
|
|
19
|
+
* When absent, the legacy git-diff-based scope is kept (backward compatible).
|
|
20
|
+
* @returns {string} the user message
|
|
21
|
+
*/
|
|
22
|
+
export function buildAdvisorUserMessage(agent, _prior, reviewType, designToken = null, documents = null, paths = null) {
|
|
23
|
+
const prior = _prior ?? extractPriorIssueTable(agent.history)
|
|
24
|
+
|
|
25
|
+
const parts = []
|
|
26
|
+
const docList = Array.isArray(documents) ? documents.filter((d) => typeof d === "string" && d.trim()) : []
|
|
27
|
+
const pathList = Array.isArray(paths) ? paths.filter((p) => typeof p === "string" && p.trim()) : []
|
|
28
|
+
|
|
29
|
+
// Design review: simplified message — focus on the design doc, not code
|
|
30
|
+
if (reviewType === "design") {
|
|
31
|
+
const repos = findReviewRepos(agent)
|
|
32
|
+
parts.push("## Design Review")
|
|
33
|
+
if (docList.length > 0) {
|
|
34
|
+
// Explicit review scope (engineering mode, FR2): the caller hands over the
|
|
35
|
+
// doc list — the advisor reviews ONLY these. No git-diff change-set
|
|
36
|
+
// collection: diff-based discovery reviewed unrelated files, and untracked
|
|
37
|
+
// design docs were invisible to git diff anyway (ENGINEERING-MODE.md §2.4).
|
|
38
|
+
parts.push("The documents below are the review scope. Review ONLY these files — do not scan git diff or read any other files.")
|
|
39
|
+
parts.push("")
|
|
40
|
+
parts.push("## Documents to Review")
|
|
41
|
+
parts.push(docList.map((d) => `- ${d} — Read this file in full`).join("\n"))
|
|
42
|
+
parts.push("")
|
|
43
|
+
} else {
|
|
44
|
+
// Backward-compatible fallback (no documents): discover docs via git status/diff.
|
|
45
|
+
parts.push("The following changes are a design document. Review it against the project's methodology.")
|
|
46
|
+
parts.push("")
|
|
47
|
+
|
|
48
|
+
// List changed file paths explicitly — new design docs are untracked,
|
|
49
|
+
// so git diff HEAD won't show their content; the advisor must read the file itself
|
|
50
|
+
const changedFiles = collectChangedFiles(repos, agent.cwd)
|
|
51
|
+
if (changedFiles.length > 0) {
|
|
52
|
+
parts.push("## Changed Files")
|
|
53
|
+
parts.push(changedFiles.map((f) => `- ${f}`).join("\n"))
|
|
54
|
+
parts.push("")
|
|
55
|
+
parts.push("Read each changed file in full — untracked files are not shown in the diff below.")
|
|
56
|
+
parts.push("")
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Pre-collected changes — the design doc diff.
|
|
60
|
+
// _advisorLastSnapshot is only consumed by code-review convergence — skip the write here.
|
|
61
|
+
const snapshots = collectRepoSnapshots(repos, agent.cwd)
|
|
62
|
+
if (snapshots.length > 0) {
|
|
63
|
+
parts.push("## Design Document (git diff)")
|
|
64
|
+
parts.push(...snapshots)
|
|
65
|
+
parts.push("")
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Engineering mode: inject project methodology
|
|
70
|
+
if (agent.config?.agent?.engineering) {
|
|
71
|
+
try {
|
|
72
|
+
const mpath = resolve(agent.cwd, "METHODOLOGY.md")
|
|
73
|
+
const methodology = readFileSync(mpath, "utf8")
|
|
74
|
+
parts.push("## Project Methodology")
|
|
75
|
+
parts.push("Evaluate the design against this methodology:")
|
|
76
|
+
parts.push(methodology)
|
|
77
|
+
parts.push("")
|
|
78
|
+
} catch { /* file doesn't exist — skip */ }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
parts.push("## Instructions")
|
|
82
|
+
if (docList.length > 0) {
|
|
83
|
+
parts.push("1. Read every document in the Documents to Review list in full — review ONLY those files. Read METHODOLOGY.md to understand the project's standards.")
|
|
84
|
+
} else {
|
|
85
|
+
parts.push("1. Read the design document fully. Read METHODOLOGY.md to understand the project's standards.")
|
|
86
|
+
}
|
|
87
|
+
parts.push("2. Review against: completeness (all requirements covered?), feasibility (can this be built?), clarity (specific enough?), acceptance criteria (verifiable?), scope (appropriate?).")
|
|
88
|
+
parts.push("3. Do NOT run git diff or look for code changes — there are none at this stage.")
|
|
89
|
+
parts.push("4. If you find issues, produce your review table with the format: | # | Category | Severity | Issue | Suggestion |. If the design passes, no table is needed.")
|
|
90
|
+
if (designToken) {
|
|
91
|
+
parts.push("")
|
|
92
|
+
parts.push("## Approval Signal")
|
|
93
|
+
parts.push(`If — and ONLY if — your review finds NO 🔴 (Critical) issues, end your reply with this exact token: [DESIGN-TOKEN:${designToken}]`)
|
|
94
|
+
parts.push("🟡 (Advisory) and 🔵 (Note) findings do NOT block approval — list them if present, but still include the token. If there are any 🔴 issues, do NOT include the token.")
|
|
95
|
+
}
|
|
96
|
+
return parts.join("\n")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Convergence data (round 2+)
|
|
100
|
+
if (prior && (agent._advisorRound || 0) > 0) {
|
|
101
|
+
const response = extractAgentResponseTable(agent.history, prior.sinceIdx)
|
|
102
|
+
|| "(Agent did not provide a response table — re-evaluate each issue)"
|
|
103
|
+
const round = (agent._advisorRound || 0) + 1
|
|
104
|
+
const label = round === 2 ? "Verify Prior Table + Flag New Issues" : "Strict Verification"
|
|
105
|
+
parts.push(`## Round ${round} — ${label}`)
|
|
106
|
+
parts.push("")
|
|
107
|
+
parts.push("## Prior Issue Table")
|
|
108
|
+
parts.push(prior.text)
|
|
109
|
+
parts.push("")
|
|
110
|
+
parts.push("## Agent Response")
|
|
111
|
+
parts.push(response)
|
|
112
|
+
parts.push("")
|
|
113
|
+
parts.push("---")
|
|
114
|
+
parts.push("")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
parts.push("## Review Scope")
|
|
118
|
+
if (pathList.length > 0) {
|
|
119
|
+
parts.push("Review these code files/directories — read them in full for context:")
|
|
120
|
+
parts.push("")
|
|
121
|
+
parts.push(pathList.map((p) => `- ${p}`).join("\n"))
|
|
122
|
+
parts.push("")
|
|
123
|
+
}
|
|
124
|
+
if (docList.length > 0) {
|
|
125
|
+
if (reviewType === "design") {
|
|
126
|
+
parts.push("The documents below are the review scope. Review ONLY these files — do NOT scan git diff or read any other files.")
|
|
127
|
+
} else {
|
|
128
|
+
parts.push("The documents below define acceptance criteria and review context. Read them for context, then read the code files specified in the review scope. Judge the implementation against these documents.")
|
|
129
|
+
}
|
|
130
|
+
parts.push("")
|
|
131
|
+
parts.push("## Documents to Review")
|
|
132
|
+
parts.push(docList.map((d) => `- ${d} — Read this file in full`).join("\n"))
|
|
133
|
+
parts.push("")
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Conversation background — recent user↔assistant exchanges for intent context
|
|
137
|
+
const background = extractConversationBackground(agent.history)
|
|
138
|
+
if (background) {
|
|
139
|
+
parts.push("## Conversation Background (recent turns)")
|
|
140
|
+
parts.push(background)
|
|
141
|
+
parts.push("")
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Review criteria
|
|
145
|
+
const criteria = loadAdvisorMd(agent.cwd)
|
|
146
|
+
parts.push("## Review Criteria")
|
|
147
|
+
parts.push(criteria)
|
|
148
|
+
parts.push("")
|
|
149
|
+
|
|
150
|
+
// Engineering mode: inject project methodology so advisor knows the rules
|
|
151
|
+
if (agent.config?.agent?.engineering) {
|
|
152
|
+
try {
|
|
153
|
+
const mpath = resolve(agent.cwd, "METHODOLOGY.md")
|
|
154
|
+
const methodology = readFileSync(mpath, "utf8")
|
|
155
|
+
parts.push("## Project Methodology (Engineering Mode)")
|
|
156
|
+
parts.push("The project follows this methodology. Evaluate the changes against it:")
|
|
157
|
+
parts.push(methodology)
|
|
158
|
+
parts.push("")
|
|
159
|
+
} catch { /* file doesn't exist — skip */ }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Instructions — round-aware: re-reviews skip convention discovery entirely
|
|
163
|
+
const isReReview = prior && (agent._advisorRound || 0) > 0
|
|
164
|
+
parts.push("## Instructions")
|
|
165
|
+
parts.push("1. IMPORTANT: in the diff, `-` lines are REMOVED content (no longer in the file), `+` lines are ADDED. The prior issue table (if any) is HISTORY — always verify current file state with `read` before judging an item.")
|
|
166
|
+
if (isReReview) {
|
|
167
|
+
parts.push("2. STALE-CONTEXT WARNING: any diff embedded in earlier messages is a historical snapshot — treat it as expired. Only the \"Current Changes\" section above and fresh `read` results describe the current state. Never quote a `-` line from any diff as if it were live code.")
|
|
168
|
+
parts.push("3. 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.")
|
|
169
|
+
parts.push("4. `read` only the files touched by the fixes. Batch independent reads/greps in a single reply.")
|
|
170
|
+
parts.push("5. Produce your verification table. Do not re-read content you already have.")
|
|
171
|
+
} else {
|
|
172
|
+
parts.push("2. Read `AGENTS.md` / design docs only if they exist (check once; do not re-probe with multiple patterns).")
|
|
173
|
+
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.")
|
|
174
|
+
parts.push("4. Use `grep` or `lsp` to trace callers, imports, and dependencies — only where the diff leaves genuine doubt.")
|
|
175
|
+
parts.push("5. Produce your review table based on the review criteria above. Do not re-read content you already have.")
|
|
176
|
+
parts.push("6. You may also flag other issues: crashes, data loss, logic errors — anything obvious. This is the convergence protocol: round 1 is the full review, later rounds only re-verify.")
|
|
177
|
+
}
|
|
178
|
+
parts.push("")
|
|
179
|
+
parts.push("Return your review as a markdown table (or a clear statement that everything is fine).")
|
|
180
|
+
|
|
181
|
+
return parts.join("\n")
|
|
182
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* advisor/repos.mjs — git repository discovery and change collection for advisor reviews.
|
|
3
|
+
* Shared by message building (advisor.mjs) and the review runner (advisor/run.mjs).
|
|
4
|
+
*/
|
|
5
|
+
import { execFileSync } from "node:child_process"
|
|
6
|
+
import { dirname, basename, resolve } from "node:path"
|
|
7
|
+
|
|
8
|
+
export const GIT_TIMEOUT = 5_000
|
|
9
|
+
export const MAX_EMBEDDED_DIFF = 50_000
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Find the git repository roots that contain the agent's touched files.
|
|
13
|
+
* Falls back to cwd if no repos found.
|
|
14
|
+
*/
|
|
15
|
+
export function findReviewRepos(agent, paths = null) {
|
|
16
|
+
const touched = agent._touchedFiles ?? []
|
|
17
|
+
const sources = paths ? [...touched, ...paths.map((p) => resolve(agent.cwd, p))] : touched
|
|
18
|
+
const repos = []
|
|
19
|
+
|
|
20
|
+
for (const abs of sources) {
|
|
21
|
+
try {
|
|
22
|
+
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
23
|
+
cwd: dirname(abs), encoding: "utf8", timeout: GIT_TIMEOUT,
|
|
24
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
25
|
+
}).trim()
|
|
26
|
+
if (root && !repos.includes(root)) repos.push(root)
|
|
27
|
+
} catch { /* not a git repo */ }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (repos.length > 0) return repos
|
|
31
|
+
|
|
32
|
+
// Fallback: cwd itself
|
|
33
|
+
try {
|
|
34
|
+
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
35
|
+
cwd: agent.cwd, encoding: "utf8", timeout: GIT_TIMEOUT,
|
|
36
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
37
|
+
}).trim()
|
|
38
|
+
if (root) return [root]
|
|
39
|
+
} catch { /* not a git repo */ }
|
|
40
|
+
|
|
41
|
+
return []
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Collect git status + diff for each repo, embedded into the review context so
|
|
46
|
+
* the advisor doesn't need to spend its first tool calls discovering changes.
|
|
47
|
+
*/
|
|
48
|
+
export function collectRepoSnapshots(repos, cwd) {
|
|
49
|
+
const targets = repos.length > 0 ? repos : [cwd]
|
|
50
|
+
const parts = []
|
|
51
|
+
for (const repo of targets) {
|
|
52
|
+
let status = "", diff = ""
|
|
53
|
+
try {
|
|
54
|
+
status = execFileSync("git", ["status", "--porcelain"], {
|
|
55
|
+
cwd: repo, encoding: "utf8", timeout: GIT_TIMEOUT, stdio: ["ignore", "pipe", "pipe"],
|
|
56
|
+
}).trim()
|
|
57
|
+
diff = execFileSync("git", ["diff", "HEAD"], {
|
|
58
|
+
cwd: repo, encoding: "utf8", timeout: GIT_TIMEOUT, stdio: ["ignore", "pipe", "pipe"],
|
|
59
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
60
|
+
})
|
|
61
|
+
} catch { continue /* not a git repo or git failed */ }
|
|
62
|
+
if (!status && !diff.trim()) continue
|
|
63
|
+
parts.push(`### ${repo}`)
|
|
64
|
+
if (status) parts.push("```", status, "```")
|
|
65
|
+
if (diff.trim()) {
|
|
66
|
+
const truncated = diff.length > MAX_EMBEDDED_DIFF
|
|
67
|
+
// Prepend a blockquote explaining diff notation to the LLM so it doesn't
|
|
68
|
+
// treat deleted lines (-) as still-present content (phantom-issue fix).
|
|
69
|
+
parts.push("**⚠️ IMPORTANT:** In the diff below, `-` lines are **REMOVED** (no longer in the file); `+` lines are **ADDED**. Always `read` the actual file for its current state — never treat a `-` line as still-present content.")
|
|
70
|
+
parts.push("```diff", truncated ? diff.slice(0, MAX_EMBEDDED_DIFF) : diff.trimEnd(), "```")
|
|
71
|
+
if (truncated) parts.push(`(diff truncated at ${MAX_EMBEDDED_DIFF} chars — use the git tool to see the rest)`)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return parts
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** List changed file paths (including untracked) across repos — used by design review
|
|
78
|
+
* so the advisor knows which files to read even when git diff HEAD can't show them.
|
|
79
|
+
* Multi-repo: each path is annotated with its repo basename so the advisor can resolve it. */
|
|
80
|
+
export function collectChangedFiles(repos, cwd) {
|
|
81
|
+
const targets = repos.length > 0 ? repos : [cwd]
|
|
82
|
+
const files = []
|
|
83
|
+
for (const repo of targets) {
|
|
84
|
+
try {
|
|
85
|
+
const status = execFileSync("git", ["status", "--porcelain"], {
|
|
86
|
+
cwd: repo, encoding: "utf8", timeout: GIT_TIMEOUT, stdio: ["ignore", "pipe", "pipe"],
|
|
87
|
+
}).trim()
|
|
88
|
+
const repoLabel = targets.length > 1 ? `[${basename(repo)}] ` : ""
|
|
89
|
+
for (const line of status.split("\n")) {
|
|
90
|
+
// "XY path" or "XY old -> new" (rename) — take the final path, strip quotes
|
|
91
|
+
const pathParts = line.slice(3).split(" -> ")
|
|
92
|
+
const p = pathParts[pathParts.length - 1].trim().replace(/^"|"$/g, "")
|
|
93
|
+
if (p) files.push(repoLabel + p)
|
|
94
|
+
}
|
|
95
|
+
} catch { /* not a git repo — skip */ }
|
|
96
|
+
}
|
|
97
|
+
return files
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const DOC_FILE = /(?:^|[/\\])(?:LICENSE|NOTICE|CHANGELOG|AUTHORS)(?:\.\w+)?$|\.(?:md|markdown|mdx|txt|rst|adoc)$/i
|
|
101
|
+
|
|
102
|
+
/** True when a path matches the doc/license pattern by extension or name.
|
|
103
|
+
* NOTE: this is extension-based only — it does NOT exclude src/ paths.
|
|
104
|
+
* Callers must separately check the src/ prefix for product-code semantics
|
|
105
|
+
* (e.g. src/prompts/*.md IS product code despite matching DOC_FILE).
|
|
106
|
+
* See isDocOnlyChange for the combined check. */
|
|
107
|
+
export function isDocFile(p) {
|
|
108
|
+
return DOC_FILE.test(p ?? "")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** True when all changed files across repos are documentation (md/txt/LICENSE etc.).
|
|
112
|
+
* Anything under src/ (incl. src/prompts/*.md) counts as product code —
|
|
113
|
+
* isProductCode semantics, consistent with the design gate. */
|
|
114
|
+
export function isDocOnlyChange(repos, cwd) {
|
|
115
|
+
const targets = repos.length > 0 ? repos : [cwd]
|
|
116
|
+
let sawChanges = false
|
|
117
|
+
for (const repo of targets) {
|
|
118
|
+
let status = ""
|
|
119
|
+
try {
|
|
120
|
+
status = execFileSync("git", ["status", "--porcelain"], {
|
|
121
|
+
cwd: repo, encoding: "utf8", timeout: GIT_TIMEOUT, stdio: ["ignore", "pipe", "pipe"],
|
|
122
|
+
}).trim()
|
|
123
|
+
} catch { continue /* repo inaccessible — check the rest */ }
|
|
124
|
+
if (!status) continue
|
|
125
|
+
sawChanges = true
|
|
126
|
+
for (const line of status.split("\n")) {
|
|
127
|
+
// porcelain: "XY path" or "XY old -> new" (rename)
|
|
128
|
+
const filePath = line.slice(3).split(" -> ").pop().replace(/^"|"$/g, "")
|
|
129
|
+
if (/^src[\\/]/.test(filePath) || !DOC_FILE.test(filePath)) return false
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return sawChanges
|
|
133
|
+
}
|