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.
- package/README.md +18 -5
- package/package.json +1 -1
- 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 +119 -0
- package/src/agent/dispatch.mjs +54 -7
- package/src/agent/post-turn.mjs +70 -0
- package/src/agent/setup.mjs +93 -5
- 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 +110 -150
- package/src/cli/make-agent.mjs +1 -0
- package/src/cli/setup-wizard.mjs +1 -0
- package/src/config.mjs +22 -4
- 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/session.mjs +270 -89
- package/src/skills.mjs +48 -15
- package/src/tools/apply_patch.md +1 -1
- package/src/tools/codemode.mjs +10 -4
- 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 +169 -66
- package/src/tui/cmd-config.mjs +12 -0
- 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 +7 -6
- package/src/tui/key-handler.mjs +132 -4
- package/src/tui/layout.mjs +5 -5
- package/src/tui/pickers.mjs +184 -44
- package/src/tui/render-conversation.mjs +49 -11
- package/src/tui/render-frame.mjs +38 -12
- package/src/tui/render-loop.mjs +2 -1
- 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
|
@@ -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"
|