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
|
@@ -1,36 +1,183 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* agent-tools/advisor.mjs — advisor tool wrapper.
|
|
3
|
-
* The agent calls this explicitly
|
|
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 {
|
|
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
|
|
11
|
-
"
|
|
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(
|
|
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
|
-
|
|
32
|
-
|
|
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),
|
|
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:
|
|
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
|
+
}
|
package/src/agent-tools/task.mjs
CHANGED
|
@@ -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
|
|
35
|
-
|
|
36
|
-
|
|
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))
|
package/src/agent-tools.mjs
CHANGED
|
@@ -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"
|