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.
Files changed (85) hide show
  1. package/README.md +29 -6
  2. package/package.json +3 -3
  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 +134 -0
  9. package/src/agent/dispatch.mjs +54 -7
  10. package/src/agent/post-turn.mjs +70 -0
  11. package/src/agent/setup.mjs +95 -6
  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 +152 -161
  19. package/src/cli/make-agent.mjs +1 -0
  20. package/src/cli/setup-wizard.mjs +1 -0
  21. package/src/config.mjs +34 -4
  22. package/src/context.mjs +47 -13
  23. package/src/generate-title.mjs +44 -0
  24. package/src/prompts/advisor-design.md +43 -0
  25. package/src/prompts/advisor-round1.md +11 -4
  26. package/src/prompts/advisor-round2.md +12 -7
  27. package/src/prompts/advisor-round3.md +11 -6
  28. package/src/prompts/coder.md +9 -3
  29. package/src/prompts/discipline.md +12 -96
  30. package/src/prompts/eng-coder.md +34 -0
  31. package/src/prompts/engineering-sub.md +12 -0
  32. package/src/prompts/engineering.md +96 -0
  33. package/src/prompts/main.md +1 -1
  34. package/src/prompts/methodology-template.md +39 -0
  35. package/src/prompts/plan.md +2 -2
  36. package/src/prompts/system.md +43 -61
  37. package/src/provider/core.mjs +58 -2
  38. package/src/session.mjs +291 -94
  39. package/src/skills.mjs +48 -15
  40. package/src/tools/apply_patch.md +1 -1
  41. package/src/tools/checklist.mjs +4 -3
  42. package/src/tools/codemode.mjs +23 -11
  43. package/src/tools/delete.md +1 -0
  44. package/src/tools/edit.md +1 -1
  45. package/src/tools/execute.md +5 -0
  46. package/src/tools/file.mjs +4 -0
  47. package/src/tools/git.md +15 -0
  48. package/src/tools/git.mjs +1 -6
  49. package/src/tools/lint.md +8 -0
  50. package/src/tools/linter.mjs +1 -5
  51. package/src/tools/lsp.md +7 -0
  52. package/src/tools/lsp.mjs +8 -9
  53. package/src/tools/patch.mjs +1 -29
  54. package/src/tools/read_image.md +5 -1
  55. package/src/tools/system.mjs +1 -1
  56. package/src/tools/web.mjs +3 -3
  57. package/src/tui/agent-turn.mjs +184 -66
  58. package/src/tui/ansi.mjs +4 -0
  59. package/src/tui/clipboard.mjs +9 -0
  60. package/src/tui/cmd-config.mjs +14 -26
  61. package/src/tui/cmd-eng.mjs +44 -0
  62. package/src/tui/cmd-exit.mjs +1 -1
  63. package/src/tui/cmd-fold.mjs +3 -4
  64. package/src/tui/cmd-model.mjs +11 -6
  65. package/src/tui/cmd-new.mjs +5 -5
  66. package/src/tui/cmd-session.mjs +21 -11
  67. package/src/tui/cmd-think.mjs +1 -0
  68. package/src/tui/index.mjs +20 -9
  69. package/src/tui/key-handler.mjs +177 -9
  70. package/src/tui/layout.mjs +5 -5
  71. package/src/tui/markdown.mjs +52 -0
  72. package/src/tui/pickers.mjs +190 -45
  73. package/src/tui/render-conversation.mjs +54 -13
  74. package/src/tui/render-frame.mjs +39 -12
  75. package/src/tui/render-loop.mjs +2 -1
  76. package/src/tui/render.mjs +13 -7
  77. package/src/tui/slash-commands.mjs +11 -7
  78. package/src/tui/startup.mjs +4 -3
  79. package/src/tui/wizard.mjs +3 -0
  80. package/src/tools/checkpoint.md +0 -15
  81. package/src/tools/git_diff.md +0 -11
  82. package/src/tools/git_log.md +0 -10
  83. package/src/tools/git_status.md +0 -8
  84. package/src/tools/linter.md +0 -13
  85. package/src/tools/syntax_check.md +0 -10
@@ -1,36 +1,183 @@
1
1
  /**
2
2
  * agent-tools/advisor.mjs — advisor tool wrapper.
3
- * The agent calls this explicitly at the end of a coding task to get a code review.
3
+ * The agent calls this explicitly to get an independent review.
4
+ * type="design" for design doc review, type="code" for code review (default).
4
5
  */
5
- import { runAdvisorReview } from "../advisor.mjs"
6
+ import { randomUUID, createHmac } from "node:crypto"
7
+ import { runAdvisorReview } from "../advisor/run.mjs"
8
+
9
+ const TOKEN_EXPIRY_MS = 3600000 // 1 hour
10
+ const TOKEN_SECRET = process.env.THINCODER_TOKEN_SECRET || "thincoder-default-secret"
11
+
12
+ /** Generate a signed design token with expiration */
13
+ function generateDesignToken() {
14
+ const uuid = randomUUID()
15
+ const expiresAt = Date.now() + TOKEN_EXPIRY_MS
16
+ const payload = `${uuid}:${expiresAt}`
17
+ const signature = createHmac("sha256", TOKEN_SECRET).update(payload).digest("hex").slice(0, 16)
18
+ return `${payload}:${signature}`
19
+ }
20
+
21
+ /** Validate design token: check format, expiration, and signature
22
+ * For backward compatibility, tokens that don't match the new format are accepted as-is
23
+ */
24
+ export function validateDesignToken(token) {
25
+ if (!token || typeof token !== "string") return false
26
+
27
+ // New format: uuid:expiresAt:signature (3 parts separated by ':')
28
+ const parts = token.split(":")
29
+ if (parts.length !== 3) {
30
+ // Old format or simple token - accept for backward compatibility
31
+ return true
32
+ }
33
+
34
+ const [uuid, expiresAt, signature] = parts
35
+ const expTime = parseInt(expiresAt, 10)
36
+
37
+ // If it looks like a new format token, validate it properly
38
+ if (isNaN(expTime)) {
39
+ // Not a valid new format, treat as old format
40
+ return true
41
+ }
42
+
43
+ // Check expiration
44
+ if (Date.now() > expTime) return false
45
+
46
+ // Check signature
47
+ const payload = `${uuid}:${expiresAt}`
48
+ const expectedSig = createHmac("sha256", TOKEN_SECRET).update(payload).digest("hex").slice(0, 16)
49
+ return signature === expectedSig
50
+ }
51
+
52
+ /** Extract UUID from signed token for regex matching */
53
+ export function extractTokenUUID(token) {
54
+ const parts = token.split(":")
55
+ return parts.length >= 1 ? parts[0] : token
56
+ }
57
+
58
+ /** Build a [DESIGN-TOKEN:...] regex; escapes special chars as a safety net even though UUIDs contain only hex/hyphens.
59
+ * Flexible matching: allows token to be on its own line, in a code block, or surrounded by whitespace.
60
+ * Rejects partial matches by requiring word boundaries or brackets around the token. */
61
+ const makeDesignTokenRegex = (token, flags = "") => {
62
+ // Extract UUID from signed token (format: uuid:expiresAt:signature)
63
+ const uuid = extractTokenUUID(token)
64
+ const escaped = uuid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
65
+ // Match [DESIGN-TOKEN: <uuid>] with flexible surrounding context:
66
+ // - Allow leading/trailing whitespace and newlines
67
+ // - Allow being inside code blocks (```...```)
68
+ // - Require complete token (not truncated)
69
+ return new RegExp(
70
+ `(?:^|\\s|\`|\\*)\\[DESIGN-TOKEN:\\s*${escaped}\\s*\\](?:\\s|$|\`|\\*)`,
71
+ flags + "ms"
72
+ )
73
+ }
6
74
 
7
75
  export const advisorTool = {
8
76
  name: "advisor",
9
77
  description:
10
- "Run a code review on your changes (convergence protocol). " +
11
- "Call this when you have finished coding and want an independent review before finalising. " +
78
+ "Run an independent review on your work. " +
79
+ "Use type='design' to review design documents before implementation pass documents=[...] with the explicit list of doc paths to review; use documents in code review too (the task's Docs involved list). " +
80
+ "Use type='code' (default) to review code changes after implementation — pass paths=[...] to specify which files or directories to review, or documents=[...] for acceptance criteria context. " +
12
81
  "The advisor is an independent read-only sub-agent that explores the codebase, runs git diff, " +
13
82
  "reads files, and traces callers via grep/lsp. " +
83
+ "For code review: round 1 does a full review, round 2 verifies the prior table, " +
84
+ "round 3+ strictly checks only the prior table — convergence, not divergence. " +
85
+ "For design review: single-pass review against methodology and requirements. " +
14
86
  "Review criteria come from .thincoder/advisor.md (if present) or sensible defaults. " +
15
- "Round 1 does a full review and produces a numbered issue table. " +
16
87
  "After the review, you MUST produce a response table (see discipline rules for format). " +
17
- "Round 2 verifies the table + can flag obvious new issues. " +
18
- "Round 3+ strictly checks only the prior table — convergence, not divergence. " +
19
- "If issues are found, fix them, update your response table, then re-run advisor. " +
20
88
  "If advisor says all clear, call verify.",
21
89
  parameters: {
22
90
  type: "object",
23
- properties: {},
91
+ properties: {
92
+ type: { type: "string", enum: ["code", "design"], description: "Review type: 'design' for design doc review, 'code' for code review (default)" },
93
+ paths: {
94
+ type: "array",
95
+ items: { type: "string" },
96
+ description: "Code files or directories to review (for code review). Required unless documents is provided. The advisor reviews git diff filtered to these paths.",
97
+ },
98
+ documents: {
99
+ type: "array",
100
+ items: { type: "string" },
101
+ description: "Explicit list of doc paths to review (design docs, requirements docs, referenced docs). The advisor reviews ONLY these — it does NOT scan git diff. Use for both design review and code review to pass the task's Docs involved list.",
102
+ },
103
+ },
24
104
  },
25
105
  readonly: true,
26
106
  sideEffectExempt: true,
27
107
  outputPanel: true,
28
- async execute(_args, ctx) {
108
+ async execute(args, ctx) {
29
109
  const agent = ctx.agent
110
+ const reviewType = args.type || "code"
111
+ const documents = args.documents || null
112
+ const paths = args.paths || null
113
+
114
+ // Code review must have a scope — no implicit fallback.
115
+ if (reviewType !== "design" && !paths && !documents) {
116
+ return "Advisor: no review scope specified. Provide paths (files/directories to review) or documents (acceptance criteria — code diff is still used)."
117
+ }
118
+
119
+ // Design review: validate that documents are in docs/ or are recognized doc files
120
+ if (reviewType === "design" && documents) {
121
+ const { isDocFile } = await import("../advisor/repos.mjs")
122
+ const invalidDocs = documents.filter((doc) => {
123
+ // Allow docs/ directory and recognized doc files (METHODOLOGY.md, README.md, etc.)
124
+ if (doc.startsWith("docs/") || doc.startsWith("docs\\")) return false
125
+ return !isDocFile(doc)
126
+ })
127
+ if (invalidDocs.length > 0) {
128
+ return `Advisor: design review documents must be in docs/ directory or be recognized doc files. Invalid: ${invalidDocs.join(", ")}`
129
+ }
130
+ }
131
+
132
+ // Design review: always starts from round 1 (no convergence)
133
+ if (reviewType === "design") {
134
+ agent._advisorRound = 0
135
+ agent._advisorSession = null
136
+ agent._advisorLastSnapshotHash = null // stale diff dedup baseline must not leak into the next code review
137
+ }
30
138
 
31
- const result = await runAdvisorReview(agent, ctx.onOutput, ctx.signal)
32
- if (!result) return "Advisor: review is disabled or no changes to review."
139
+ // Generate the design token BEFORE the review and inject it into the advisor's prompt.
140
+ // The advisor (LLM) decides pass/fail itself and echoes the token only on approval —
141
+ // the gate is a mechanical string match, not fragile semantics parsing.
142
+ const designToken = reviewType === "design" ? generateDesignToken() : null
143
+ const result = await runAdvisorReview(agent, reviewType, {
144
+ onOutput: ctx.onOutput,
145
+ signal: ctx.signal,
146
+ }, designToken, documents, paths)
33
147
 
148
+ if (reviewType === "design") {
149
+ // Whitespace-tolerant match (LLM may add spaces or wrap in fences).
150
+ // The token IS the verdict — the advisor echoes it only on approval (prompt-enforced);
151
+ // no findings-table heuristics: a design with issues never carries the token.
152
+ const tokenPattern = makeDesignTokenRegex(designToken)
153
+ if (designToken && result && tokenPattern.test(result)) {
154
+ // Advisor echoed the token → review passed. Issue it to the parent for eng-coder.
155
+ // (session cleanup for design reviews is owned by runAdvisorReview)
156
+ agent._engDesignToken = designToken
157
+ // Unlock the dispatch design gate (dispatch.mjs) for eng-coder SELF-review:
158
+ // an eng-coder whose own design review passed may write files without the
159
+ // parent spawn-time authorization. NOTE: unreachable today — eng-coder.md
160
+ // tells the child not to re-run the design review, and spawn already sets
161
+ // _engDesignReviewed (subagent.mjs). Kept as defense-in-depth for a future
162
+ // eng-coder autonomous design-revision entry. Parent agents (role undefined)
163
+ // don't use this flag — their runAgent resets it anyway; they are trusted
164
+ // via the engineering prompt.
165
+ if (agent._role === "eng-coder") agent._engDesignReviewed = true
166
+ // Strip the bracketed token so only ONE unambiguous format (plain UUID) reaches the main agent
167
+ const cleanResult = result.replace(makeDesignTokenRegex(designToken, "g"), "").trim()
168
+ const tokenUUID = extractTokenUUID(designToken)
169
+ return `${cleanResult}\n\nApproved. Pass this exact token to eng-coder (designToken parameter): ${designToken}`
170
+ }
171
+ // Review failed (or advisor chose not to pass) → invalidate any previously-issued token.
172
+ // Guard: result === null means the review was skipped (advisor disabled / not engineering
173
+ // mode) — a skipped review must not revoke an already-issued token.
174
+ if (result !== null) agent._engDesignToken = null
175
+ // Strip every dead token occurrence from the raw output so the main agent can't grab an invalid one
176
+ if (result) {
177
+ const stripped = result.replace(makeDesignTokenRegex(designToken, "g"), "").trim()
178
+ return stripped || "Advisor: design review did not pass."
179
+ }
180
+ }
34
181
  return result
35
182
  },
36
183
  }
@@ -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"