thincoder 0.12.2 → 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 +1 -1
  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
@@ -0,0 +1,64 @@
1
+ /**
2
+ * eng tool: enter/exit engineering mode.
3
+ * In engineering mode the agent follows design-before-code methodology.
4
+ * Toggled here at session level; persisted by /eng.
5
+ */
6
+ import { ENG_ON_REMINDER } from "../agent.mjs"
7
+
8
+ export const engTool = {
9
+ name: "eng",
10
+ description:
11
+ "Enter or exit engineering mode. In engineering mode, follow design-before-code: write a design document, run advisor design review, get user approval, then implement via eng-coder subagents.",
12
+ parameters: {
13
+ type: "object",
14
+ properties: {
15
+ action: { type: "string", enum: ["enter", "exit"], description: "Enter or exit engineering mode" },
16
+ },
17
+ required: ["action"],
18
+ },
19
+ readonly: true,
20
+ async execute(args, ctx) {
21
+ ctx.agent.config.agent ??= {}
22
+ if (args.action === "exit") {
23
+ ctx.agent.config.agent.engineering = false
24
+ ctx.agent._engDesignToken = null // stale token from prior design review invalidated
25
+ ctx.agent._engDesignReviewed = false // reset gate state
26
+ ctx.agent._advisorRound = 0 // reset convergence budget
27
+ ctx.agent._touchedFiles = [] // clear mutation tracking
28
+ ctx.agent._lastEngState = false
29
+ ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
30
+ ctx.agent._pendingReminders.push(
31
+ "[System reminder: engineering mode is now OFF — standard discipline applies. Changes go through the normal workflow.]")
32
+ // 持久化工程模式状态到会话
33
+ if (ctx.persistState) {
34
+ await ctx.persistState({
35
+ engineering: false,
36
+ engDesignToken: null,
37
+ engDesignReviewed: false,
38
+ advisorRound: 0,
39
+ touchedFiles: []
40
+ })
41
+ }
42
+ return "Engineering mode exited. Standard discipline now applies. You may edit files directly."
43
+ }
44
+ if (args.action === "enter") {
45
+ ctx.agent.config.agent.engineering = true
46
+ ctx.agent._engDesignToken = null // re-entering requires a fresh design review
47
+ ctx.agent._lastEngState = true
48
+ ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
49
+ ctx.agent._pendingReminders.push(ENG_ON_REMINDER)
50
+ // 持久化工程模式状态到会话
51
+ if (ctx.persistState) {
52
+ await ctx.persistState({
53
+ engineering: true,
54
+ engDesignToken: null,
55
+ engDesignReviewed: false,
56
+ advisorRound: 0,
57
+ touchedFiles: []
58
+ })
59
+ }
60
+ return "Engineering mode activated. Design-before-code enforced: write a design document in docs/, run advisor with type='design', get user approval, then implement via eng-coder subagents."
61
+ }
62
+ return "Invalid action: expected 'enter' or 'exit'"
63
+ },
64
+ }
@@ -1,9 +1,10 @@
1
1
  import {
2
2
  createAgent, runAgent, ContinueError,
3
3
  readonlyToolNames, collectGitContext, escapeXml,
4
- EXPLORE_OVERLAY, CODER_OVERLAY, PLAN_OVERLAY,
4
+ EXPLORE_OVERLAY, CODER_OVERLAY, PLAN_OVERLAY, ENG_CODER_OVERLAY,
5
5
  MIN_REPORT_CHARS, REPORT_CONTINUATION, DEFAULT_SUBAGENT_TURNS,
6
6
  } from "../agent.mjs"
7
+ import { validateDesignToken } from "./advisor.mjs"
7
8
 
8
9
  /**
9
10
  * subagent tool: spawn a child agent to handle an independent subtask (isolated context, only the report is returned).
@@ -28,7 +29,8 @@ export const subagentTool = {
28
29
  properties: {
29
30
  task: { type: "string", description: "Self-contained task description for the sub-agent" },
30
31
  context: { type: "string", description: "Optional background the sub-agent needs (it cannot see this conversation)" },
31
- role: { type: "string", enum: ["explore", "plan", "coder"], description: "Sub-agent role: 'explore' (read-only search/analysis), 'plan' (read-only implementation planning), or 'coder' (full implementation). Default: same tools as parent." },
32
+ role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder"], description: "Sub-agent role: 'explore' (read-only search/analysis), 'plan' (read-only implementation planning), 'coder' (full implementation), 'eng-coder' (engineering-mode coder — strict methodology, design-driven). ENUM IS OVERRIDDEN IN setup.mjs PER ENGINEERING MODE." },
33
+ designToken: { type: "string", description: "Required when role='eng-coder': the token returned by advisor(type='design') after the design review passed. Without a valid token, eng-coder cannot modify files." },
32
34
  },
33
35
  required: ["task"],
34
36
  },
@@ -39,6 +41,23 @@ export const subagentTool = {
39
41
  const parent = ctx.agent
40
42
  const role = args.role
41
43
 
44
+ // Role is mutually exclusive per mode: normal mode → "coder", engineering mode → "eng-coder"
45
+ if (parent.config?.agent?.engineering && role === "coder") {
46
+ throw new Error("Engineering mode: use role='eng-coder' for implementation tasks.")
47
+ }
48
+ if (!parent.config?.agent?.engineering && role === "eng-coder") {
49
+ throw new Error("Engineering mode is not active — use role='coder' for implementation tasks.")
50
+ }
51
+
52
+ // eng-coder token gate: the design review must have passed and the caller must
53
+ // present the exact token advisor issued — otherwise the child is not authorized to code.
54
+ if (role === "eng-coder") {
55
+ const issued = parent._engDesignToken
56
+ if (!issued || args.designToken !== issued || !validateDesignToken(args.designToken)) {
57
+ throw new Error("Invalid or missing design token — run advisor with type='design' first and pass the returned token as designToken.")
58
+ }
59
+ }
60
+
42
61
  // Filter tool set by role: explore/plan are read-only (plan is a planning agent, its deliverable is the plan itself)
43
62
  let tools
44
63
  if (role === "explore" || role === "plan") {
@@ -53,6 +72,7 @@ export const subagentTool = {
53
72
  if (role === "explore") overlay = EXPLORE_OVERLAY
54
73
  else if (role === "coder") overlay = CODER_OVERLAY
55
74
  else if (role === "plan") overlay = PLAN_OVERLAY
75
+ else if (role === "eng-coder") overlay = ENG_CODER_OVERLAY
56
76
 
57
77
  // explore/plan: force read-only permission; coder/default: AUTO passes through directly,
58
78
  // manual mode queues permission requests for the parent agent's approval UI (human in the loop, child agent is no longer silently rejected)
@@ -71,16 +91,24 @@ export const subagentTool = {
71
91
  }
72
92
  }
73
93
 
94
+ // eng-coder: force engineering=true on child config so setup.mjs applies engineering prompt
95
+ const childConfig = role === "eng-coder"
96
+ ? { ...parent.config, agent: { ...parent.config.agent, engineering: true } }
97
+ : parent.config
98
+
74
99
  const child = createAgent({
75
100
  provider: parent.provider,
76
101
  tools,
77
- config: parent.config,
102
+ config: childConfig,
78
103
  cwd: parent.cwd,
79
104
  memory: parent.memory,
80
105
  overlay,
81
106
  role,
82
107
  })
83
108
 
109
+ // Token-verified design review → child is authorized to modify files without re-reviewing
110
+ if (role === "eng-coder") child._engDesignReviewed = true
111
+
84
112
  // explore/plan: inject git context (branch/recent commits/working tree state) — exploration and planning both relate to current repo state (inspired by kimi-code's promptPrefix)
85
113
  let input = args.context ? `Context:\n${args.context}\n\nTask:\n${args.task}` : args.task
86
114
  if (role === "explore" || role === "plan") {
@@ -115,6 +143,48 @@ export const subagentTool = {
115
143
  report = await runAgent(child, REPORT_CONTINUATION, childOpts, childRunOpts)
116
144
  }
117
145
 
146
+ // Engineering mode mechanical code gate: delegated file changes must not
147
+ // bypass the parent's advisor/verify guards. Merge the child's mutations
148
+ // into the parent so "advisor mandatory at both gates" is enforced, not just
149
+ // promised in the engineering prompt.
150
+ // CRITICAL: Only merge if child actually mutated files (defense-in-depth against
151
+ // runAgent throwing before any writes occurred).
152
+ if (role === "eng-coder" && child._mutatedThisRun) {
153
+ mergeChildMutations(parent, child)
154
+ }
155
+
118
156
  return report
119
157
  },
120
158
  }
159
+
160
+ /**
161
+ * Merge an eng-coder child's mutations into the parent agent's bookkeeping.
162
+ * The parent must stay aware of delegated file changes: `_touchedFiles` enables
163
+ * the advisor guard (completion.mjs) to detect that code was modified and
164
+ * pushback for review. Prior verify/advisor state is invalidated because it
165
+ * judged an older state.
166
+ *
167
+ * `_advisorRound` is reset to 0: merged code is new code that deserves a fresh
168
+ * convergence budget. Mirrors the design-review reset semantics.
169
+ *
170
+ * Returns true when mutations were merged (kept for future caller checks).
171
+ */
172
+ export function mergeChildMutations(parent, child) {
173
+ if (!child._mutatedThisRun) return false
174
+ parent._mutatedThisRun = true
175
+ for (const abs of child._touchedFiles ?? []) {
176
+ if (!parent._touchedFiles.includes(abs)) parent._touchedFiles.push(abs)
177
+ }
178
+ if (parent._calledAdvisorThisRun) parent._calledAdvisorThisRun = false
179
+ if (parent._verifiedThisRun) {
180
+ parent._verifiedThisRun = false
181
+ parent._verifyPassed = undefined
182
+ }
183
+ // Fresh code → fresh convergence budget + stale session/diff cleanup.
184
+ // _advisorRound reset ensures new code gets a full round-1 review;
185
+ // _advisorSession + _advisorLastSnapshotHash prevent cross-contamination.
186
+ parent._advisorRound = 0
187
+ parent._advisorSession = null
188
+ parent._advisorLastSnapshotHash = null
189
+ return true
190
+ }
@@ -1,5 +1,28 @@
1
1
  const VALID_TASK_STATUS = new Set(["pending", "in_progress", "done"])
2
2
 
3
+ /** Common synonyms LLMs tend to use — normalize to canonical values */
4
+ const STATUS_ALIASES = {
5
+ completed: "done",
6
+ finished: "done",
7
+ complete: "done",
8
+ done: "done",
9
+ pending: "pending",
10
+ todo: "pending",
11
+ open: "pending",
12
+ waiting: "pending",
13
+ in_progress: "in_progress",
14
+ inprogress: "in_progress",
15
+ active: "in_progress",
16
+ running: "in_progress",
17
+ working: "in_progress",
18
+ }
19
+
20
+ function normalizeStatus(raw) {
21
+ if (!raw) return "pending"
22
+ const key = String(raw).toLowerCase().replace(/[\s_-]+/g, "")
23
+ return STATUS_ALIASES[key] ?? STATUS_ALIASES[raw] ?? null
24
+ }
25
+
3
26
  /**
4
27
  * task tool: multi-step task planning and progress tracking (Claude Code's todo mode).
5
28
  * Each call replaces the entire list; only modifies agent internal state (no external world), so readonly.
@@ -10,7 +33,9 @@ export const taskTool = {
10
33
  description:
11
34
  "Plan and track a task list for complex multi-step work. Each call replaces the entire list. " +
12
35
  "Keep exactly one item in_progress at a time; mark items done as you complete them; never mark done if tests fail or work is partial. " +
13
- "Statuses: pending | in_progress | done.",
36
+ "Statuses: pending | in_progress | done. " +
37
+ "IMPORTANT: status must be exactly one of these three strings — no synonyms (e.g. 'completed', 'finished', 'open' are INVALID). " +
38
+ "IMPORTANT: title is required and must be a non-empty string — items with empty titles are silently dropped.",
14
39
  parameters: {
15
40
  type: "object",
16
41
  properties: {
@@ -31,10 +56,23 @@ export const taskTool = {
31
56
  readonly: true,
32
57
  async execute(args, ctx) {
33
58
  // Keep only non-done items + the 3 most recently completed (for context reference), max 20 to prevent accumulation
34
- const raw = (args.items ?? []).map((it) => ({
35
- title: String(it.title ?? "").slice(0, 200),
36
- status: VALID_TASK_STATUS.has(it.status) ? it.status : "pending",
37
- }))
59
+ const warnings = []
60
+ const raw = (args.items ?? []).map((it) => {
61
+ const normalized = normalizeStatus(it.status)
62
+ if (normalized && normalized !== it.status) {
63
+ warnings.push(`status "${it.status}" normalized to "${normalized}"`)
64
+ } else if (!normalized) {
65
+ warnings.push(`"${it.status}" is not valid (use: pending | in_progress | done)`)
66
+ }
67
+ const title = String(it.title ?? "").trim()
68
+ if (!title) {
69
+ warnings.push(`empty title skipped (item was: ${JSON.stringify(it).slice(0, 100)})`)
70
+ }
71
+ return {
72
+ title,
73
+ status: normalized ?? "pending",
74
+ }
75
+ }).filter((t) => t.title.length > 0)
38
76
  const pending = raw.filter((t) => t.status !== "done")
39
77
  const recentDone = raw.filter((t) => t.status === "done").slice(-3)
40
78
  const items = [...pending, ...recentDone].slice(0, 20)
@@ -42,7 +80,8 @@ export const taskTool = {
42
80
  ctx.agent._onTaskUpdate?.(items)
43
81
  const done = items.filter((i) => i.status === "done").length
44
82
  const open = items.length - done
83
+ const warningText = warnings.length > 0 ? ` ⚠️ ${warnings.join("; ")}` : ""
45
84
  return `Task list updated: ${done}/${items.length} done` +
46
- (open > 0 ? ` — ${open} item(s) still open.` : " — all done.")
85
+ (open > 0 ? ` — ${open} item(s) still open.` : " — all done.") + warningText
47
86
  },
48
87
  }
@@ -1,4 +1,5 @@
1
1
  import { repairHistory, listWorkDir } from "../agent.mjs"
2
+ import { isDocFile } from "../advisor/repos.mjs"
2
3
  import { execSync, spawn, spawnSync } from "node:child_process"
3
4
  import { readFileSync, existsSync } from "node:fs"
4
5
  import { join } from "node:path"
@@ -39,6 +40,8 @@ function moduleName(srcPath) {
39
40
 
40
41
  /**
41
42
  * verify tool: pre-completion self-check. When called:
43
+ * 0. Doc-only fast path — all changed files are docs (docs/, *.md, LICENSE…):
44
+ * short report, no syntax checks, no tests.
42
45
  * 1. git diff --stat — changed file list
43
46
  * 2. node --check — syntax check all changed .mjs/.js files
44
47
  * 3. Related tests — run test files that cover the changed modules (default)
@@ -82,6 +85,21 @@ export const verifyTool = {
82
85
  lines.push("Changed files: (not a git repo or git unavailable)")
83
86
  }
84
87
 
88
+ // 1b. Doc-only fast path: every changed file is documentation (docs/, *.md,
89
+ // LICENSE…) — syntax checks and tests are meaningless for doc changes, and
90
+ // the task list/self-review checklist add nothing either. Mirrors the
91
+ // advisor's doc-only review skip ("No issues found — documentation-only
92
+ // changes, code review skipped."). src/** (incl. prompts/*.md) is product
93
+ // code — excluded from the fast path, consistent with isProductCode.
94
+ // Empty list (no changes / git unavailable) intentionally falls through
95
+ // to the normal path below.
96
+ if (changedFiles.length > 0 && changedFiles.every((f) => !/^src[\\/]/.test(f) && isDocFile(f))) {
97
+ lines.push("")
98
+ lines.push("Documentation-only changes — skipping syntax checks and tests.")
99
+ ctx.agent._verifyPassed = true
100
+ return lines.join("\n")
101
+ }
102
+
85
103
  // 2. Syntax check: run node --check on all changed .mjs/.js files (skip deleted files)
86
104
  let syntaxFailed = false
87
105
  const jsFiles = changedFiles.filter((f) => /\.(m?js)$/i.test(f))
@@ -12,3 +12,4 @@ export { verifyTool } from "./agent-tools/verify.mjs"
12
12
  export { recentChangesTool } from "./agent-tools/recent-changes.mjs"
13
13
  export { timerTool } from "./agent-tools/timer.mjs"
14
14
  export { advisorTool } from "./agent-tools/advisor.mjs"
15
+ export { engTool } from "./agent-tools/eng.mjs"