thincoder 0.12.54 → 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.
Files changed (129) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +1 -1
  3. package/bin/thincoder.mjs +25 -3
  4. package/package.json +3 -7
  5. package/src/acp/bridge.mjs +132 -26
  6. package/src/advisor/messages.mjs +38 -3
  7. package/src/advisor/run.mjs +91 -53
  8. package/src/advisor.mjs +15 -7
  9. package/src/agent/dispatch.mjs +156 -39
  10. package/src/agent/helpers.mjs +46 -4
  11. package/src/agent/setup.mjs +102 -19
  12. package/src/agent/spawn-child.mjs +28 -1
  13. package/src/agent-tools/advisor.mjs +43 -11
  14. package/src/agent-tools/consult.mjs +37 -6
  15. package/src/agent-tools/eng.mjs +4 -1
  16. package/src/agent-tools/goal.mjs +11 -1
  17. package/src/agent-tools/read-history.mjs +160 -0
  18. package/src/agent-tools/settings.mjs +162 -0
  19. package/src/agent-tools/skill.mjs +2 -1
  20. package/src/agent-tools/subagent-actions.mjs +432 -0
  21. package/src/agent-tools/subagent-async.mjs +427 -0
  22. package/src/agent-tools/subagent-scheduler.mjs +319 -0
  23. package/src/agent-tools/subagent.mjs +565 -128
  24. package/src/agent-tools/task.mjs +4 -3
  25. package/src/agent-tools/timer.mjs +9 -4
  26. package/src/agent-tools/verify.mjs +161 -49
  27. package/src/agent-tools.mjs +1 -0
  28. package/src/agent.mjs +182 -81
  29. package/src/auto-think.mjs +14 -0
  30. package/src/cli/make-agent.mjs +27 -1
  31. package/src/cli/memory-command.mjs +28 -7
  32. package/src/cli/permission.mjs +8 -1
  33. package/src/config.mjs +125 -8
  34. package/src/context.mjs +115 -34
  35. package/src/distill.mjs +19 -1
  36. package/src/escape.mjs +82 -27
  37. package/src/log.mjs +195 -0
  38. package/src/mcp/transport-http.mjs +13 -1
  39. package/src/mcp.mjs +52 -7
  40. package/src/memory/code-sync.mjs +1 -1
  41. package/src/memory/core.mjs +204 -10
  42. package/src/memory/docs.mjs +197 -62
  43. package/src/memory.mjs +1 -1
  44. package/src/model-specs.mjs +38 -1
  45. package/src/prompts/advisor-design.md +46 -0
  46. package/src/prompts/advisor-round1.md +49 -2
  47. package/src/prompts/advisor-round2.md +47 -0
  48. package/src/prompts/advisor-round3.md +47 -0
  49. package/src/prompts/coder.md +22 -0
  50. package/src/prompts/consult-base.md +13 -0
  51. package/src/prompts/discipline.md +25 -6
  52. package/src/prompts/eng-coder.md +2 -2
  53. package/src/prompts/engineering-sub.md +23 -1
  54. package/src/prompts/engineering.md +157 -50
  55. package/src/prompts/explore.md +1 -2
  56. package/src/prompts/main.md +11 -5
  57. package/src/prompts/methodology-template.md +14 -0
  58. package/src/prompts/system.md +5 -2
  59. package/src/provider/anthropic.mjs +7 -5
  60. package/src/provider/core.mjs +104 -28
  61. package/src/provider/google.mjs +57 -24
  62. package/src/provider/normalize.mjs +1 -1
  63. package/src/provider/rate.mjs +0 -2
  64. package/src/provider/responses.mjs +8 -13
  65. package/src/provider/sse.mjs +20 -0
  66. package/src/session.mjs +15 -0
  67. package/src/tools/apply_patch.md +5 -1
  68. package/src/tools/bash.md +3 -3
  69. package/src/tools/delete.md +1 -0
  70. package/src/tools/edit-batch.mjs +92 -0
  71. package/src/tools/edit-diff.mjs +265 -0
  72. package/src/tools/edit.md +11 -6
  73. package/src/tools/execute.md +8 -8
  74. package/src/tools/execute.mjs +31 -35
  75. package/src/tools/file.mjs +26 -114
  76. package/src/tools/file_ops.md +3 -2
  77. package/src/tools/get_current_time.md +3 -1
  78. package/src/tools/git.md +1 -1
  79. package/src/tools/git.mjs +8 -16
  80. package/src/tools/hashline_edit.md +2 -0
  81. package/src/tools/index.mjs +3 -2
  82. package/src/tools/insert_after.md +2 -1
  83. package/src/tools/lint.md +3 -1
  84. package/src/tools/linter.mjs +9 -37
  85. package/src/tools/lsp.md +4 -1
  86. package/src/tools/patch.mjs +84 -13
  87. package/src/tools/pdf-parse-text.mjs +497 -0
  88. package/src/tools/pdf-parse-xref.mjs +499 -0
  89. package/src/tools/pdf.mjs +155 -0
  90. package/src/tools/question.md +2 -1
  91. package/src/tools/read.md +1 -0
  92. package/src/tools/read_pdf.md +21 -0
  93. package/src/tools/repomap.mjs +1 -1
  94. package/src/tools/shared.mjs +11 -32
  95. package/src/tools/system.mjs +6 -21
  96. package/src/tools/tree.md +2 -1
  97. package/src/tools/web.mjs +5 -3
  98. package/src/tools/websearch.md +2 -1
  99. package/src/tools/write.md +2 -0
  100. package/src/traces/trace-store.mjs +224 -0
  101. package/src/tui/agent-turn.mjs +387 -24
  102. package/src/tui/clipboard.mjs +17 -6
  103. package/src/tui/cmd-config.mjs +29 -9
  104. package/src/tui/cmd-eng.mjs +1 -0
  105. package/src/tui/cmd-extract.mjs +1 -1
  106. package/src/tui/cmd-mcp-form.mjs +197 -0
  107. package/src/tui/cmd-mcp.mjs +264 -114
  108. package/src/tui/cmd-think.mjs +1 -1
  109. package/src/tui/index.mjs +49 -95
  110. package/src/tui/interaction.mjs +41 -3
  111. package/src/tui/key-handler.mjs +105 -143
  112. package/src/tui/key-modes.mjs +215 -0
  113. package/src/tui/layout.mjs +22 -1
  114. package/src/tui/mouse.mjs +41 -1
  115. package/src/tui/pickers.mjs +73 -7
  116. package/src/tui/render-conversation.mjs +13 -161
  117. package/src/tui/render-frame.mjs +45 -20
  118. package/src/tui/render-loop.mjs +4 -1
  119. package/src/tui/render-segments.mjs +165 -0
  120. package/src/tui/render.mjs +4 -4
  121. package/src/tui/startup.mjs +40 -2
  122. package/src/tui/subagent-blocks.mjs +404 -111
  123. package/src/tui/subagent-panel.mjs +88 -13
  124. package/src/tui/tool-args.mjs +10 -2
  125. package/src/tui/tool-events.mjs +172 -95
  126. package/src/tui/update-notice.mjs +72 -0
  127. package/src/tui/wizard.mjs +36 -6
  128. package/src/agent-tools/escalate.mjs +0 -179
  129. package/src/tools/exec-prelude.mjs +0 -84
@@ -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: status must be exactly one of these three stringsno synonyms (e.g. 'completed', 'finished', 'open' are INVALID). " +
36
- "IMPORTANT: title is required and must be a non-empty string items with empty titles are silently dropped.",
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 stringitems 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 30). Longer for complex reasoning, shorter for simple tasks.",
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: ["seconds"],
28
+ required: [],
28
29
  },
29
30
  readonly: true,
30
31
  sideEffectExempt: true,
31
32
  execute(args, ctx) {
32
- const seconds = args.seconds ?? 180
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. Heuristic: the FIRST path component
8
- * after src/ determines the module. Map it to the test file that imports from it.
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: "test/tools.test.mjs",
13
- "agent-tools": "test/tools.test.mjs",
14
- agent: "test/agent.test.mjs",
15
- memory: "test/memory.test.mjs",
16
- tui: "test/tui.test.mjs",
17
- provider: "test/integration-provider.mjs",
18
- config: "test/integration-provider.mjs",
19
- skills: "test/tools.test.mjs",
20
- distill: "test/tools.test.mjs",
21
- markdown: "test/agent.test.mjs",
22
- advisor: "test/advisor.test.mjs",
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
- * "src/tools/bash.mjs" "tools", "src/agent.mjs" "agent", "src/agent/helpers.mjs" "agent"
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 rel = srcPath.replace(/^src[/\\]/, "")
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
- filter: { type: "string", description: "Optional: limit the test run to matching test names (node --test-name-pattern / npm test -- --test-name-pattern)" },
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
- // workdir only relocates WHERE tests (and package.json) live changed-file
69
- // resolution (git diff) stays anchored to the project root.
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. Git difffind changed files
76
- let changedFiles = []
77
- try {
78
- const diff = execSync("git diff --stat", { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 })
79
- if (diff.trim()) {
80
- lines.push("Changed files (git diff --stat):")
81
- lines.push(diff.trim())
82
- // extract changed file paths
83
- const nameOnly = execSync("git diff --name-only", { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 })
84
- changedFiles = nameOnly.trim().split("\n").filter(Boolean)
85
- } else {
86
- lines.push("Changed files: (none — no uncommitted changes)")
87
- }
88
- } catch {
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) => !/^src[\\/]/.test(f) && isDocFile(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 = join(cwd, f)
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) => /^src[/\\].+\.mjs$/i.test(f) && existsSync(join(cwd, 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.map((m) => MODULE_TO_TEST[m]).filter(Boolean))]
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.filter)
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
- const abs = join(cwd, testFile)
165
- if (!existsSync(abs)) {
166
- lines.push(` ? ${testFile} file not found, skipping`)
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(cwd, testFile, ctx, args.filter)
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] === null)
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) {
@@ -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"