thincoder 0.12.58 → 0.12.59
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/CHANGELOG.md +42 -1
- package/README.md +1 -1
- package/bin/thincoder.mjs +8 -0
- package/package.json +1 -1
- package/src/acp/bridge.mjs +132 -26
- package/src/advisor/messages.mjs +34 -1
- package/src/advisor/run.mjs +89 -51
- package/src/advisor.mjs +15 -7
- package/src/agent/dispatch.mjs +91 -14
- package/src/agent/helpers.mjs +35 -4
- package/src/agent/setup.mjs +90 -19
- package/src/agent/spawn-child.mjs +25 -0
- package/src/agent-tools/advisor.mjs +24 -2
- package/src/agent-tools/consult.mjs +37 -6
- package/src/agent-tools/eng.mjs +2 -1
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +160 -0
- package/src/agent-tools/settings.mjs +162 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +432 -0
- package/src/agent-tools/subagent-async.mjs +427 -0
- package/src/agent-tools/subagent-scheduler.mjs +319 -0
- package/src/agent-tools/subagent.mjs +467 -193
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +161 -49
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +161 -125
- package/src/auto-think.mjs +14 -0
- package/src/cli/make-agent.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/config.mjs +5 -0
- package/src/context.mjs +87 -27
- package/src/distill.mjs +19 -1
- package/src/escape.mjs +6 -4
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +1 -1
- package/src/memory/core.mjs +126 -0
- package/src/memory/docs.mjs +196 -87
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +15 -1
- package/src/prompts/advisor-design.md +46 -0
- package/src/prompts/advisor-round1.md +49 -2
- package/src/prompts/advisor-round2.md +47 -0
- package/src/prompts/advisor-round3.md +47 -0
- package/src/prompts/coder.md +22 -0
- package/src/prompts/consult-base.md +13 -0
- package/src/prompts/discipline.md +10 -5
- package/src/prompts/eng-coder.md +2 -2
- package/src/prompts/engineering-sub.md +23 -1
- package/src/prompts/engineering.md +106 -56
- package/src/prompts/explore.md +1 -2
- package/src/prompts/main.md +11 -6
- package/src/prompts/methodology-template.md +14 -0
- package/src/prompts/system.md +4 -2
- package/src/provider/core.mjs +56 -2
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +31 -43
- package/src/tools/edit-diff.mjs +265 -0
- package/src/tools/edit.md +10 -8
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +24 -20
- package/src/tools/file.mjs +18 -68
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -2
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/patch.mjs +84 -13
- package/src/tools/pdf-parse-text.mjs +497 -0
- package/src/tools/pdf-parse-xref.mjs +499 -0
- package/src/tools/pdf.mjs +155 -0
- package/src/tools/question.md +2 -1
- package/src/tools/read.md +1 -0
- package/src/tools/read_pdf.md +21 -0
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +4 -12
- package/src/tools/system.mjs +6 -21
- package/src/tools/tree.md +2 -1
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +385 -22
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +29 -9
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +9 -0
- package/src/tui/cmd-think.mjs +1 -1
- package/src/tui/index.mjs +29 -95
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +40 -0
- package/src/tui/pickers.mjs +11 -3
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +165 -0
- package/src/tui/startup.mjs +36 -0
- package/src/tui/subagent-blocks.mjs +322 -144
- package/src/tui/subagent-panel.mjs +88 -13
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-events.mjs +132 -100
- package/src/tui/update-notice.mjs +72 -0
- package/src/tui/wizard.mjs +36 -6
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
package/src/agent-tools/task.mjs
CHANGED
|
@@ -31,9 +31,10 @@ export const taskTool = {
|
|
|
31
31
|
description:
|
|
32
32
|
"Plan and track a task list for complex multi-step work. Each call replaces the entire list. " +
|
|
33
33
|
"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. " +
|
|
34
|
-
"Statuses: pending | in_progress | done. " +
|
|
35
|
-
"IMPORTANT:
|
|
36
|
-
"
|
|
34
|
+
"Statuses: pending | in_progress | done — synonyms (completed/finished/complete, todo/open/waiting, active/running/working, …) are accepted and normalized with a warning. " +
|
|
35
|
+
"IMPORTANT: title is required and must be a non-empty string — items with empty titles are silently dropped. " +
|
|
36
|
+
"For cross-session / project-level tracking, use checklist. " +
|
|
37
|
+
"Returns the updated task list (or the new item's ID on add).",
|
|
37
38
|
parameters: {
|
|
38
39
|
type: "object",
|
|
39
40
|
properties: {
|
|
@@ -11,25 +11,30 @@ export const timerTool = {
|
|
|
11
11
|
"Set a timer before you start analyzing code. When the timer fires, " +
|
|
12
12
|
"a system reminder will be injected suggesting you try running code or " +
|
|
13
13
|
"adding debug logs. Use this to enforce a thinking budget: you get " +
|
|
14
|
-
"N seconds to reason, then the timer reminds you to act."
|
|
14
|
+
"N seconds to reason, then the timer reminds you to act. " +
|
|
15
|
+
"Returns the set confirmation — the reminder fires at the deadline.",
|
|
15
16
|
parameters: {
|
|
16
17
|
type: "object",
|
|
17
18
|
properties: {
|
|
18
19
|
seconds: {
|
|
19
20
|
type: "number",
|
|
20
|
-
description: "Thinking budget in seconds (default
|
|
21
|
+
description: "Thinking budget in seconds (default 180). Longer for complex reasoning, shorter for simple tasks.",
|
|
21
22
|
},
|
|
22
23
|
message: {
|
|
23
24
|
type: "string",
|
|
24
25
|
description: "Custom reminder message to show when time is up. Default: a suggestion to add debug logs or run the code.",
|
|
25
26
|
},
|
|
26
27
|
},
|
|
27
|
-
required: [
|
|
28
|
+
required: [],
|
|
28
29
|
},
|
|
29
30
|
readonly: true,
|
|
30
31
|
sideEffectExempt: true,
|
|
31
32
|
execute(args, ctx) {
|
|
32
|
-
|
|
33
|
+
// D15.3#1:默认 180;非法值(非数字/非正数)显式报错——静默 NaN 定时器永不触发的坑
|
|
34
|
+
const seconds = Number(args.seconds ?? 180)
|
|
35
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
36
|
+
throw new Error(`timer seconds must be a positive number, got: ${args.seconds}`)
|
|
37
|
+
}
|
|
33
38
|
const expiresAt = Date.now() + seconds * 1000
|
|
34
39
|
const message = args.message || `⏰ Time's up (${seconds}s). Have you tried running the code, adding a console.log, or checking the output? Thinking more without data is guessing.`
|
|
35
40
|
|
|
@@ -1,42 +1,102 @@
|
|
|
1
1
|
import { isDocFile } from "../advisor/repos.mjs"
|
|
2
2
|
import { execSync, spawn, spawnSync } from "node:child_process"
|
|
3
3
|
import { readFileSync, existsSync } from "node:fs"
|
|
4
|
-
import { join, resolve } from "node:path"
|
|
4
|
+
import { dirname, join, resolve } from "node:path"
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Source module → test file mapping.
|
|
8
|
-
*
|
|
9
|
-
* Modules without dedicated tests map to null.
|
|
7
|
+
* Source module → test file mapping (AGENT-LOOP §18.14: test files are split by domain —
|
|
8
|
+
* one module maps to the list of domain files that cover it). Heuristic: the FIRST
|
|
9
|
+
* path component after src/ determines the module. Modules without dedicated tests map to null.
|
|
10
10
|
*/
|
|
11
11
|
const MODULE_TO_TEST = {
|
|
12
|
-
tools:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
tools: [
|
|
13
|
+
"test/file-tools.test.mjs", "test/edit-tools.test.mjs", "test/git.test.mjs",
|
|
14
|
+
"test/checkpoint.test.mjs", "test/bash.test.mjs", "test/ops-scope.test.mjs",
|
|
15
|
+
"test/execute.test.mjs", "test/checklist.test.mjs", "test/skills-distill.test.mjs",
|
|
16
|
+
"test/verify-domain.test.mjs", "test/pdf-parse.test.mjs",
|
|
17
|
+
],
|
|
18
|
+
"agent-tools": [
|
|
19
|
+
"test/subagent-core.test.mjs", "test/subagent-async.test.mjs", "test/subagent-tool.test.mjs",
|
|
20
|
+
"test/subagent-panel.test.mjs", "test/subagent-scheduler.test.mjs", "test/verify-domain.test.mjs",
|
|
21
|
+
],
|
|
22
|
+
agent: [
|
|
23
|
+
"test/agent-core.test.mjs", "test/agent-context.test.mjs", "test/guards.test.mjs",
|
|
24
|
+
"test/eng-delivery.test.mjs", "test/eng-reminders.test.mjs", "test/provider-stream.test.mjs",
|
|
25
|
+
"test/stream-rules.test.mjs", "test/session-compaction.test.mjs",
|
|
26
|
+
"test/skills-distill.test.mjs", "test/verify-domain.test.mjs", "test/dispatch.test.mjs",
|
|
27
|
+
],
|
|
28
|
+
memory: ["test/memory.test.mjs"],
|
|
29
|
+
tui: ["test/tui-render.test.mjs", "test/tui-input.test.mjs", "test/tui-panel.test.mjs", "test/tui-stream.test.mjs", "test/tui-picker.test.mjs", "test/subagent-blocks.test.mjs"],
|
|
30
|
+
provider: ["test/integration-provider.mjs"],
|
|
31
|
+
config: ["test/integration-provider.mjs"],
|
|
32
|
+
skills: ["test/skills-distill.test.mjs"],
|
|
33
|
+
distill: ["test/skills-distill.test.mjs"],
|
|
34
|
+
markdown: ["test/memory.test.mjs"],
|
|
35
|
+
advisor: ["test/advisor-message.test.mjs", "test/advisor-review.test.mjs", "test/advisor-eng.test.mjs", "test/advisor-parallel.test.mjs"],
|
|
36
|
+
prompts: ["test/prompts.test.mjs"],
|
|
37
|
+
context: ["test/agent-context.test.mjs", "test/session-compaction.test.mjs"],
|
|
38
|
+
session: ["test/session.test.mjs", "test/session-compaction.test.mjs", "test/session-eng-advisor.test.mjs", "test/session-migration.test.mjs", "test/session-safety.test.mjs", "test/acp.test.mjs"],
|
|
23
39
|
mcp: null,
|
|
24
|
-
prompts: null,
|
|
25
|
-
context: null,
|
|
26
|
-
session: null,
|
|
27
40
|
}
|
|
28
41
|
|
|
29
42
|
/**
|
|
30
|
-
* Extract module name from a source path.
|
|
31
|
-
*
|
|
43
|
+
* Extract module name from a source path (src-relative or absolute — §18.12:
|
|
44
|
+
* changed files are normalized to absolute paths before this runs).
|
|
45
|
+
* "src/tools/bash.mjs" → "tools", "src/agent.mjs" → "agent",
|
|
46
|
+
* "D:/proj/src/agent/helpers.mjs" → "agent"
|
|
32
47
|
*/
|
|
33
48
|
function moduleName(srcPath) {
|
|
34
|
-
const
|
|
49
|
+
const norm = srcPath.replace(/\\/g, "/")
|
|
50
|
+
const srcIdx = norm.lastIndexOf("/src/")
|
|
51
|
+
const rel = srcIdx === -1 ? norm.replace(/^src\//, "") : norm.slice(srcIdx + 5)
|
|
35
52
|
const firstSlash = rel.search(/[/\\]/)
|
|
36
53
|
if (firstSlash === -1) return rel.replace(/\.mjs$/, "")
|
|
37
54
|
return rel.slice(0, firstSlash)
|
|
38
55
|
}
|
|
39
56
|
|
|
57
|
+
/**
|
|
58
|
+
* §18.12 D-VR1 path normalization — mirrors the §20.5 file-domain handling:
|
|
59
|
+
* resolved to an absolute path with forward slashes; dedup uses the win32
|
|
60
|
+
* lowercase comparison key (changedFileKey) so "D:/x/src/a.mjs" and
|
|
61
|
+
* "d:\x\SRC\a.mjs" count as the same file.
|
|
62
|
+
*/
|
|
63
|
+
function normalizeChangedPath(p, base) {
|
|
64
|
+
return resolve(base, p).replace(/\\/g, "/")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Win32 comparison key: case-insensitive drives/folders (same file). */
|
|
68
|
+
function changedFileKey(p) {
|
|
69
|
+
return process.platform === "win32" ? p.toLowerCase() : p
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Nearest ancestor of an absolute path holding package.json or .git — the
|
|
74
|
+
* project/repo root (§18.12 F-VR1: the agent cwd may be a workspace root that
|
|
75
|
+
* is NOT the repo root — related test files must be located against it).
|
|
76
|
+
* Walk stops at the filesystem root; returns null when no anchor exists.
|
|
77
|
+
*/
|
|
78
|
+
function findProjectRoot(absPath) {
|
|
79
|
+
let dir = dirname(resolve(absPath))
|
|
80
|
+
for (;;) {
|
|
81
|
+
if (existsSync(join(dir, "package.json")) || existsSync(join(dir, ".git"))) return dir
|
|
82
|
+
const parent = dirname(dir)
|
|
83
|
+
if (parent === dir) return null
|
|
84
|
+
dir = parent
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* True when the path is under <projectRoot>/src/ (anchored via findProjectRoot —
|
|
90
|
+
* a parent-path /src/ segment outside the project must not classify a file as
|
|
91
|
+
* product code). Falls back to the loose /src/ pattern when no anchor exists.
|
|
92
|
+
*/
|
|
93
|
+
function isUnderSrc(absPath) {
|
|
94
|
+
const root = findProjectRoot(absPath)
|
|
95
|
+
if (!root) return /(?:^|[\\/])src[\\/]/.test(absPath)
|
|
96
|
+
const norm = normalizeChangedPath(absPath, root)
|
|
97
|
+
return norm.startsWith(normalizeChangedPath("src", root) + "/")
|
|
98
|
+
}
|
|
99
|
+
|
|
40
100
|
/**
|
|
41
101
|
* verify tool: pre-completion self-check. When called:
|
|
42
102
|
* 0. Doc-only fast path — all changed files are docs (docs/, *.md, LICENSE…):
|
|
@@ -52,42 +112,76 @@ function moduleName(srcPath) {
|
|
|
52
112
|
export const verifyTool = {
|
|
53
113
|
name: "verify",
|
|
54
114
|
description:
|
|
55
|
-
"Run a pre-completion self-check. By default runs syntax checks on changed files AND any test files related to the changed modules, shows git diff and task list, and displays a self-review checklist. Set full=true to run the project's full test suite (npm test) instead of just related tests. Call this BEFORE declaring any coding task complete — do not say 'done' until verify passes."
|
|
115
|
+
"Run a pre-completion self-check. By default runs syntax checks on changed files AND any test files related to the changed modules, shows git diff and task list, and displays a self-review checklist. Set full=true to run the project's full test suite (npm test) instead of just related tests. testNamePattern limits the run to matching test names (renamed from filter — the old name is rejected with an error). Call this BEFORE declaring any coding task complete — do not say 'done' until verify passes. " +
|
|
116
|
+
"Returns the check report — syntax results, related-test outcomes, task list; verify failure blocks the completion claim.",
|
|
56
117
|
parameters: {
|
|
57
118
|
type: "object",
|
|
58
119
|
properties: {
|
|
59
120
|
full: { type: "boolean", description: "Run the full test suite (npm test) instead of just related tests. Default false — use sparingly, per the testing discipline rules." },
|
|
60
121
|
workdir: { type: "string", description: "Optional: run verify in this subdirectory (relative to cwd or absolute) — for monorepos" },
|
|
61
|
-
|
|
122
|
+
testNamePattern: { type: "string", description: "Optional: limit the test run to matching test names (node --test-name-pattern / npm test -- --test-name-pattern). Renamed from filter — the old name is rejected with an error." },
|
|
62
123
|
},
|
|
63
124
|
},
|
|
64
125
|
readonly: true,
|
|
65
126
|
outputPanel: true, // stream test output to a panel instead of inline
|
|
66
127
|
async execute(args, ctx) {
|
|
128
|
+
if ("filter" in args) {
|
|
129
|
+
throw new Error("filter was renamed to testNamePattern — use testNamePattern; the old name is rejected")
|
|
130
|
+
}
|
|
67
131
|
const cwd = ctx.agent.cwd
|
|
68
|
-
//
|
|
69
|
-
//
|
|
132
|
+
// Changed-file resolution (§18.12 D-VR3): _touchedFiles (per-run bookkeeping,
|
|
133
|
+
// absolute paths) ∪ git diff fallback — git is tried at testCwd
|
|
134
|
+
// (workdir-resolved) first, then at ctx.agent.cwd; first success wins.
|
|
135
|
+
// git-diff paths are repo-root-relative — resolved against the discovered
|
|
136
|
+
// git root, then normalized (absolute, forward slashes, win32 key).
|
|
70
137
|
const testCwd = args.workdir ? resolve(cwd, args.workdir) : cwd
|
|
71
138
|
const lines = []
|
|
72
139
|
lines.push("=== VERIFICATION REPORT ===")
|
|
73
140
|
lines.push("")
|
|
74
141
|
|
|
75
|
-
// 1.
|
|
76
|
-
let
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
142
|
+
// 1. Changed files — _touchedFiles ∪ git diff (§18.12 D-VR1)
|
|
143
|
+
let gitOk = false
|
|
144
|
+
let gitStat = ""
|
|
145
|
+
let gitFiles = []
|
|
146
|
+
for (const gitCwd of new Set([testCwd, cwd])) {
|
|
147
|
+
try {
|
|
148
|
+
// Anchor the repo root FIRST (also proves git works here) — diff paths
|
|
149
|
+
// are repo-root-relative, so both diff calls run at the root.
|
|
150
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", { cwd: gitCwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 }).trim() || gitCwd
|
|
151
|
+
const diff = execSync("git diff --stat", { cwd: gitRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 })
|
|
152
|
+
const nameOnly = execSync("git diff --name-only", { cwd: gitRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 })
|
|
153
|
+
gitOk = true
|
|
154
|
+
gitStat = diff.trim()
|
|
155
|
+
gitFiles = nameOnly.trim().split("\n").filter(Boolean).map((p) => normalizeChangedPath(p, gitRoot))
|
|
156
|
+
break
|
|
157
|
+
} catch { /* git unavailable here — try the next cwd in the chain */ }
|
|
158
|
+
}
|
|
159
|
+
if (gitOk && gitStat) {
|
|
160
|
+
lines.push("Changed files (git diff --stat):")
|
|
161
|
+
lines.push(gitStat)
|
|
162
|
+
} else if (gitOk) {
|
|
163
|
+
lines.push("Changed files: (none — no uncommitted changes)")
|
|
164
|
+
} else {
|
|
89
165
|
lines.push("Changed files: (not a git repo or git unavailable)")
|
|
90
166
|
}
|
|
167
|
+
// _touchedFiles: files written by the tools this run — captured regardless
|
|
168
|
+
// of git state (§18.12 F-VR1: subagent cwd ≠ git root must still locate).
|
|
169
|
+
const touchedFiles = (ctx.agent._touchedFiles ?? []).map((p) => normalizeChangedPath(p, cwd))
|
|
170
|
+
const gitKeys = new Set(gitFiles.map(changedFileKey))
|
|
171
|
+
const touchedOnly = touchedFiles.filter((p) => !gitKeys.has(changedFileKey(p)))
|
|
172
|
+
if (touchedOnly.length > 0) {
|
|
173
|
+
lines.push("")
|
|
174
|
+
lines.push("Changed files (this run — _touchedFiles):")
|
|
175
|
+
for (const f of touchedOnly) lines.push(` ${f}`)
|
|
176
|
+
}
|
|
177
|
+
// Union, deduped by comparison key (win32 case-insensitive).
|
|
178
|
+
const changedKeys = new Set()
|
|
179
|
+
const changedFiles = [...touchedFiles, ...gitFiles].filter((p) => {
|
|
180
|
+
const k = changedFileKey(p)
|
|
181
|
+
if (changedKeys.has(k)) return false
|
|
182
|
+
changedKeys.add(k)
|
|
183
|
+
return true
|
|
184
|
+
})
|
|
91
185
|
|
|
92
186
|
// 1b. Doc-only fast path: every changed file is documentation (docs/, *.md,
|
|
93
187
|
// LICENSE…) — syntax checks and tests are meaningless for doc changes, and
|
|
@@ -97,7 +191,7 @@ export const verifyTool = {
|
|
|
97
191
|
// code — excluded from the fast path, consistent with isProductCode.
|
|
98
192
|
// Empty list (no changes / git unavailable) intentionally falls through
|
|
99
193
|
// to the normal path below.
|
|
100
|
-
if (changedFiles.length > 0 && changedFiles.every((f) =>
|
|
194
|
+
if (changedFiles.length > 0 && changedFiles.every((f) => !isUnderSrc(f) && isDocFile(f))) {
|
|
101
195
|
lines.push("")
|
|
102
196
|
lines.push("Documentation-only changes — skipping syntax checks and tests.")
|
|
103
197
|
ctx.agent._verifyPassed = true
|
|
@@ -111,7 +205,7 @@ export const verifyTool = {
|
|
|
111
205
|
lines.push("")
|
|
112
206
|
lines.push("Syntax check (node --check):")
|
|
113
207
|
for (const f of jsFiles) {
|
|
114
|
-
const abs =
|
|
208
|
+
const abs = resolve(cwd, f)
|
|
115
209
|
if (!existsSync(abs)) continue // skip deleted files
|
|
116
210
|
try {
|
|
117
211
|
const result = spawnSync("node", ["--check", abs], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 10000 })
|
|
@@ -129,9 +223,9 @@ export const verifyTool = {
|
|
|
129
223
|
}
|
|
130
224
|
|
|
131
225
|
// 3. Identify related test files for changed source modules
|
|
132
|
-
const srcFiles = changedFiles.filter((f) =>
|
|
226
|
+
const srcFiles = changedFiles.filter((f) => /\.mjs$/i.test(f) && isUnderSrc(f) && existsSync(f))
|
|
133
227
|
const modules = [...new Set(srcFiles.map(moduleName))]
|
|
134
|
-
const relatedTests = [...new Set(modules.
|
|
228
|
+
const relatedTests = [...new Set(modules.flatMap((m) => MODULE_TO_TEST[m] ?? []).filter(Boolean))]
|
|
135
229
|
|
|
136
230
|
// 4. Run tests
|
|
137
231
|
const pkgPath = join(testCwd, "package.json")
|
|
@@ -142,7 +236,7 @@ export const verifyTool = {
|
|
|
142
236
|
if (hasTestScript) {
|
|
143
237
|
lines.push("")
|
|
144
238
|
lines.push("Tests (full suite):")
|
|
145
|
-
const result = await runTestSuite(testCwd, ctx, args.
|
|
239
|
+
const result = await runTestSuite(testCwd, ctx, args.testNamePattern)
|
|
146
240
|
if (result.passed) {
|
|
147
241
|
lines.push("✓ All tests passed.")
|
|
148
242
|
ctx.agent._verifyPassed = !syntaxFailed
|
|
@@ -160,14 +254,28 @@ export const verifyTool = {
|
|
|
160
254
|
lines.push("")
|
|
161
255
|
lines.push(`Related tests (${relatedTests.length} file(s) for modules: ${modules.join(", ")}):`)
|
|
162
256
|
let anyTestFailed = false
|
|
257
|
+
let anyTestMissing = false
|
|
163
258
|
for (const testFile of relatedTests) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
259
|
+
// Lookup: cwd first (existing behavior), then the changed files' project
|
|
260
|
+
// root (§18.12 F-VR1 — subagent cwd may be a workspace root, not the repo
|
|
261
|
+
// root; the mapped test file lives at <projectRoot>/test/...).
|
|
262
|
+
let testAbs = join(cwd, testFile)
|
|
263
|
+
let testRunCwd = cwd
|
|
264
|
+
if (!existsSync(testAbs)) {
|
|
265
|
+
const roots = [...new Set(srcFiles.map(findProjectRoot).filter(Boolean))]
|
|
266
|
+
const inRoot = roots.map((r) => ({ root: r, p: join(r, testFile) })).find((x) => existsSync(x.p))
|
|
267
|
+
if (inRoot) {
|
|
268
|
+
testAbs = inRoot.p
|
|
269
|
+
testRunCwd = inRoot.root
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (!existsSync(testAbs)) {
|
|
273
|
+
anyTestMissing = true
|
|
274
|
+
lines.push(` ? ${testFile} — file not found (cwd + changed files' project root), verify did not run it`)
|
|
167
275
|
continue
|
|
168
276
|
}
|
|
169
277
|
try {
|
|
170
|
-
const result = await runTestFile(
|
|
278
|
+
const result = await runTestFile(testRunCwd, testAbs, ctx, args.testNamePattern)
|
|
171
279
|
if (result.passed) {
|
|
172
280
|
lines.push(` ✓ ${testFile}`)
|
|
173
281
|
} else {
|
|
@@ -184,13 +292,17 @@ export const verifyTool = {
|
|
|
184
292
|
lines.push("")
|
|
185
293
|
lines.push("✗ Related tests FAILED. Review the output above, fix the issues, then run verify again.")
|
|
186
294
|
ctx.agent._verifyPassed = false
|
|
295
|
+
} else if (anyTestMissing) {
|
|
296
|
+
lines.push("")
|
|
297
|
+
lines.push("✗ Related test file(s) MISSING — verify did NOT certify this change. Locate the test file or fix MODULE_TO_TEST.")
|
|
298
|
+
ctx.agent._verifyPassed = false
|
|
187
299
|
} else {
|
|
188
300
|
lines.push(" All related tests passed.")
|
|
189
301
|
ctx.agent._verifyPassed = !syntaxFailed
|
|
190
302
|
}
|
|
191
303
|
} else {
|
|
192
304
|
// No related tests found for the changed modules
|
|
193
|
-
const uncovered = modules.filter((m) => MODULE_TO_TEST[m] ===
|
|
305
|
+
const uncovered = modules.filter((m) => !MODULE_TO_TEST[m] || (Array.isArray(MODULE_TO_TEST[m]) && MODULE_TO_TEST[m].length === 0))
|
|
194
306
|
const untested = modules.filter((m) => !(m in MODULE_TO_TEST))
|
|
195
307
|
lines.push("")
|
|
196
308
|
if (uncovered.length > 0) {
|
package/src/agent-tools.mjs
CHANGED
|
@@ -13,3 +13,4 @@ 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
15
|
export { engTool } from "./agent-tools/eng.mjs"
|
|
16
|
+
export { readHistoryTool } from "./agent-tools/read-history.mjs"
|