thincoder 0.12.1 → 0.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +18 -5
  2. package/package.json +1 -1
  3. package/src/advisor/history.mjs +112 -0
  4. package/src/advisor/messages.mjs +182 -0
  5. package/src/advisor/repos.mjs +133 -0
  6. package/src/advisor/run.mjs +346 -0
  7. package/src/advisor.mjs +109 -509
  8. package/src/agent/completion.mjs +119 -0
  9. package/src/agent/dispatch.mjs +54 -7
  10. package/src/agent/post-turn.mjs +70 -0
  11. package/src/agent/setup.mjs +93 -5
  12. package/src/agent-tools/advisor.mjs +159 -12
  13. package/src/agent-tools/eng.mjs +64 -0
  14. package/src/agent-tools/subagent.mjs +73 -3
  15. package/src/agent-tools/task.mjs +45 -6
  16. package/src/agent-tools/verify.mjs +18 -0
  17. package/src/agent-tools.mjs +1 -0
  18. package/src/agent.mjs +110 -150
  19. package/src/cli/make-agent.mjs +1 -0
  20. package/src/cli/setup-wizard.mjs +1 -0
  21. package/src/config.mjs +22 -4
  22. package/src/prompts/advisor-design.md +43 -0
  23. package/src/prompts/advisor-round1.md +11 -4
  24. package/src/prompts/advisor-round2.md +12 -7
  25. package/src/prompts/advisor-round3.md +11 -6
  26. package/src/prompts/coder.md +9 -3
  27. package/src/prompts/discipline.md +12 -96
  28. package/src/prompts/eng-coder.md +34 -0
  29. package/src/prompts/engineering-sub.md +12 -0
  30. package/src/prompts/engineering.md +96 -0
  31. package/src/prompts/main.md +1 -1
  32. package/src/prompts/methodology-template.md +39 -0
  33. package/src/prompts/plan.md +2 -2
  34. package/src/prompts/system.md +43 -61
  35. package/src/session.mjs +270 -89
  36. package/src/skills.mjs +48 -15
  37. package/src/tools/apply_patch.md +1 -1
  38. package/src/tools/codemode.mjs +10 -4
  39. package/src/tools/delete.md +1 -0
  40. package/src/tools/edit.md +1 -1
  41. package/src/tools/execute.md +5 -0
  42. package/src/tools/file.mjs +4 -0
  43. package/src/tools/git.md +15 -0
  44. package/src/tools/git.mjs +1 -6
  45. package/src/tools/lint.md +8 -0
  46. package/src/tools/linter.mjs +1 -5
  47. package/src/tools/lsp.md +7 -0
  48. package/src/tools/lsp.mjs +8 -9
  49. package/src/tools/patch.mjs +1 -29
  50. package/src/tools/read_image.md +5 -1
  51. package/src/tools/system.mjs +1 -1
  52. package/src/tools/web.mjs +3 -3
  53. package/src/tui/agent-turn.mjs +169 -66
  54. package/src/tui/cmd-config.mjs +12 -0
  55. package/src/tui/cmd-eng.mjs +44 -0
  56. package/src/tui/cmd-exit.mjs +6 -16
  57. package/src/tui/cmd-fold.mjs +3 -4
  58. package/src/tui/cmd-model.mjs +11 -6
  59. package/src/tui/cmd-new.mjs +5 -5
  60. package/src/tui/cmd-session.mjs +21 -11
  61. package/src/tui/cmd-think.mjs +1 -0
  62. package/src/tui/index.mjs +7 -6
  63. package/src/tui/key-handler.mjs +132 -4
  64. package/src/tui/layout.mjs +5 -5
  65. package/src/tui/pickers.mjs +184 -44
  66. package/src/tui/render-conversation.mjs +49 -11
  67. package/src/tui/render-frame.mjs +38 -12
  68. package/src/tui/render-loop.mjs +2 -1
  69. package/src/tui/slash-commands.mjs +11 -7
  70. package/src/tui/startup.mjs +4 -3
  71. package/src/tui/wizard.mjs +3 -0
  72. package/src/tools/checkpoint.md +0 -15
  73. package/src/tools/git_diff.md +0 -11
  74. package/src/tools/git_log.md +0 -10
  75. package/src/tools/git_status.md +0 -8
  76. package/src/tools/linter.md +0 -13
  77. 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 five leading Chinese vendors — DeepSeek / Kimi / GLM / Qwen / MiniMax. 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 five deeply adapted.
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,7 +85,7 @@ 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` (arrow-key picker across all models of all providers; `/model <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.
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
 
@@ -164,7 +164,7 @@ src/
164
164
  memory/ three-layer memory — schema.mjs (DDL/constants), core.mjs (CRUD + retrieval),
165
165
  code-index.mjs + code-sync.mjs (code_chunks), docs.mjs (doc_chunks)
166
166
  memory.mjs re-export shim → src/memory/*
167
- session.mjs session persistence (up to 5 archive slots, isolated by project cwd)
167
+ session.mjs session persistence (up to 5 archive slots, isolated by project cwd, process-level isolation via sessionId + slotSessions)
168
168
  skills.mjs skill discovery/loading (.thincoder/skills/*.md)
169
169
  markdown.mjs entry format (frontmatter parse/serialize)
170
170
  git/ checkpoint.mjs (git patch snapshots / rewind), gitmem.mjs (Team layer git sync)
@@ -205,6 +205,19 @@ Code conventions: pure `.mjs`, no semicolons, no npm dependencies allowed (inclu
205
205
 
206
206
  ## Changelog
207
207
 
208
+ ### 0.12.3 (2026-08)
209
+ - **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.
210
+
211
+ ### 0.12.2 (2026-08)
212
+ - **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
213
+ - **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`
214
+ - **Picker improvements** — auto-scroll keeps selected item visible, "type to filter" hint shown, search/filter support added
215
+ - **Shift+Enter multiline input** — TUI now supports Shift+Enter for multiline input (regular Enter still submits)
216
+ - **New provider presets** — added Volcengine Ark (豆包), Hunyuan (腾讯混元), SiliconFlow (硅基流动), OpenRouter, Groq (5 new providers, total now 12)
217
+ - **Execute tool security hardening** — blocks dynamic `import()` calls to prevent sandbox escape. Pre-execution regex check + sandbox blocks `require()` and `process` access
218
+ - **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
219
+ - **Verify guard improvements** — file path validation, error context enrichment, test coverage expanded
220
+
208
221
  ### 0.12.1 (2026-07)
209
222
  - **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
223
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.12.1",
3
+ "version": "0.12.3",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
@@ -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
+ }