thincoder 0.12.59 → 0.12.60

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 (127) hide show
  1. package/CHANGELOG.md +38 -3
  2. package/README.md +2 -2
  3. package/bin/thincoder.mjs +80 -19
  4. package/package.json +4 -3
  5. package/src/acp/bridge.mjs +7 -4
  6. package/src/advisor/messages.mjs +24 -4
  7. package/src/advisor/run.mjs +35 -33
  8. package/src/advisor.mjs +25 -6
  9. package/src/agent/completion.mjs +17 -11
  10. package/src/agent/dispatch.mjs +102 -19
  11. package/src/agent/helpers.mjs +36 -0
  12. package/src/agent/record-results.mjs +46 -10
  13. package/src/agent/run-stages.mjs +227 -0
  14. package/src/agent/setup-reminders.mjs +62 -0
  15. package/src/agent/setup.mjs +18 -2
  16. package/src/agent/spawn-child.mjs +29 -4
  17. package/src/agent-tools/advisor-async.mjs +456 -0
  18. package/src/agent-tools/advisor.mjs +110 -108
  19. package/src/agent-tools/async-settle.mjs +191 -0
  20. package/src/agent-tools/consult.mjs +121 -102
  21. package/src/agent-tools/design-token.mjs +104 -0
  22. package/src/agent-tools/eng.mjs +24 -29
  23. package/src/agent-tools/escalate-async.mjs +286 -0
  24. package/src/agent-tools/read-history.mjs +155 -31
  25. package/src/agent-tools/recent-changes.mjs +2 -1
  26. package/src/agent-tools/settings.mjs +7 -17
  27. package/src/agent-tools/subagent-actions.mjs +168 -130
  28. package/src/agent-tools/subagent-async.mjs +129 -174
  29. package/src/agent-tools/subagent-panel.mjs +153 -0
  30. package/src/agent-tools/subagent-run.mjs +202 -0
  31. package/src/agent-tools/subagent-scheduler.mjs +45 -21
  32. package/src/agent-tools/subagent-spawn.mjs +406 -0
  33. package/src/agent-tools/subagent.mjs +107 -555
  34. package/src/agent-tools/verify.mjs +118 -270
  35. package/src/agent.mjs +57 -190
  36. package/src/cli/distill-command.mjs +10 -4
  37. package/src/cli/make-agent.mjs +3 -1
  38. package/src/cli/memory-command.mjs +2 -1
  39. package/src/cli/permission.mjs +2 -2
  40. package/src/cli/setup-wizard.mjs +17 -12
  41. package/src/config.mjs +56 -8
  42. package/src/context.mjs +5 -147
  43. package/src/crash-reports.mjs +123 -0
  44. package/src/distill.mjs +11 -11
  45. package/src/explore-distill.mjs +155 -0
  46. package/src/memory/code-sync.mjs +2 -1
  47. package/src/memory/core.mjs +6 -193
  48. package/src/memory/delete.mjs +234 -0
  49. package/src/memory/docs.mjs +58 -48
  50. package/src/memory.mjs +3 -1
  51. package/src/peer-domains.mjs +265 -0
  52. package/src/peer-instances.mjs +231 -0
  53. package/src/prompt-overlays.mjs +25 -0
  54. package/src/prompts/advisor-design.md +9 -76
  55. package/src/prompts/advisor-round1.md +9 -68
  56. package/src/prompts/advisor-round2.md +7 -54
  57. package/src/prompts/advisor-round3.md +7 -54
  58. package/src/prompts/coder.md +7 -50
  59. package/src/prompts/consult-base.md +4 -24
  60. package/src/prompts/discipline.md +26 -44
  61. package/src/prompts/eng-coder.md +7 -32
  62. package/src/prompts/engineering-sub.md +3 -23
  63. package/src/prompts/engineering.md +53 -306
  64. package/src/prompts/explore.md +3 -12
  65. package/src/prompts/main.md +10 -32
  66. package/src/prompts/methodology-template.md +28 -48
  67. package/src/prompts/plan.md +2 -9
  68. package/src/prompts/system.md +16 -35
  69. package/src/provider/core.mjs +6 -67
  70. package/src/provider/errors.mjs +76 -0
  71. package/src/provider/retry.mjs +8 -45
  72. package/src/session-gc.mjs +214 -0
  73. package/src/session-guard.mjs +47 -0
  74. package/src/session-rename.mjs +38 -0
  75. package/src/session-slots.mjs +181 -58
  76. package/src/session.mjs +48 -89
  77. package/src/token-ttl.mjs +273 -0
  78. package/src/tools/checklist-sync.mjs +181 -0
  79. package/src/tools/checklist.mjs +52 -39
  80. package/src/tools/edit-batch.mjs +109 -10
  81. package/src/tools/edit-diff.mjs +110 -27
  82. package/src/tools/edit.md +17 -12
  83. package/src/tools/execute.mjs +31 -4
  84. package/src/tools/file.mjs +11 -6
  85. package/src/tools/git.mjs +14 -6
  86. package/src/tools/glob-dialect.mjs +130 -0
  87. package/src/tools/glob.md +3 -3
  88. package/src/tools/grep.md +1 -1
  89. package/src/tools/index.mjs +5 -6
  90. package/src/tools/ops.mjs +175 -3
  91. package/src/tools/patch.mjs +3 -3
  92. package/src/tools/question.md +3 -0
  93. package/src/tools/read.md +0 -1
  94. package/src/tools/shared.mjs +14 -13
  95. package/src/tools/system.mjs +44 -9
  96. package/src/tools/wait_for.md +22 -0
  97. package/src/tui/agent-turn.mjs +17 -228
  98. package/src/tui/cmd-config.mjs +48 -7
  99. package/src/tui/cmd-eng.mjs +20 -16
  100. package/src/tui/cmd-mcp.mjs +8 -2
  101. package/src/tui/cmd-new.mjs +3 -2
  102. package/src/tui/cmd-session.mjs +19 -4
  103. package/src/tui/cmd-think.mjs +10 -10
  104. package/src/tui/cmd-upgrade.mjs +19 -4
  105. package/src/tui/config-helpers.mjs +28 -16
  106. package/src/tui/distill-cmd.mjs +1 -1
  107. package/src/tui/index.mjs +3 -2
  108. package/src/tui/interaction.mjs +3 -3
  109. package/src/tui/mouse.mjs +7 -1
  110. package/src/tui/pickers.mjs +40 -22
  111. package/src/tui/render-segments.mjs +27 -10
  112. package/src/tui/startup.mjs +4 -0
  113. package/src/tui/subagent-blocks.mjs +95 -263
  114. package/src/tui/subagent-children.mjs +176 -0
  115. package/src/tui/subagent-freeze.mjs +172 -0
  116. package/src/tui/subagent-panel.mjs +61 -23
  117. package/src/tui/suspension-drive.mjs +351 -0
  118. package/src/tui/tool-args.mjs +3 -3
  119. package/src/tui/tool-display.mjs +142 -0
  120. package/src/tui/tool-events.mjs +37 -173
  121. package/src/tui/tui-lifecycle.mjs +29 -0
  122. package/src/tui/update-notice.mjs +4 -0
  123. package/src/tui/wizard.mjs +12 -6
  124. package/src/tools/pdf-parse-text.mjs +0 -497
  125. package/src/tools/pdf-parse-xref.mjs +0 -499
  126. package/src/tools/pdf.mjs +0 -155
  127. package/src/tools/read_pdf.md +0 -21
@@ -1,64 +1,26 @@
1
- import { isDocFile } from "../advisor/repos.mjs"
2
- import { execSync, spawn, spawnSync } from "node:child_process"
3
- import { readFileSync, existsSync } from "node:fs"
4
- import { dirname, join, resolve } from "node:path"
5
-
6
1
  /**
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.
2
+ * verify.mjs verify tool: generic pre-completion verification gate (VERIFY-REDESIGN.md).
3
+ * Verify is language/framework/project agnostic: it does NOT hardcode any project's
4
+ * module→test mapping, does NOT auto-run any test/verification command, and does NOT
5
+ * require a test for every change. The model DECLARES its verification status via the
6
+ * `verification` argument (whether it already ran the project's verification and the
7
+ * result); verify makes a mechanical gate decision on that declaration. Which
8
+ * verification to run is described in natural language in each project's AGENTS.md —
9
+ * verify never parses or executes it. Retained mechanical checks: doc-only fast path,
10
+ * an advisory (non-gating) node --check hint, git diff report, and — on the code
11
+ * path (doc-only changes return early) — the task list and self-review checklist.
10
12
  */
11
- const MODULE_TO_TEST = {
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"],
39
- mcp: null,
40
- }
41
13
 
42
- /**
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"
47
- */
48
- function moduleName(srcPath) {
49
- const norm = srcPath.replace(/\\/g, "/")
50
- const srcIdx = norm.lastIndexOf("/src/")
51
- const rel = srcIdx === -1 ? norm.replace(/^src\//, "") : norm.slice(srcIdx + 5)
52
- const firstSlash = rel.search(/[/\\]/)
53
- if (firstSlash === -1) return rel.replace(/\.mjs$/, "")
54
- return rel.slice(0, firstSlash)
55
- }
14
+ import { isDocFile } from "../advisor/repos.mjs"
15
+ import { execSync, spawnSync } from "node:child_process"
16
+ import { existsSync } from "node:fs"
17
+ import { dirname, join, resolve } from "node:path"
56
18
 
57
19
  /**
58
20
  * §18.12 D-VR1 path normalization — mirrors the §20.5 file-domain handling:
59
21
  * resolved to an absolute path with forward slashes; dedup uses the win32
60
22
  * lowercase comparison key (changedFileKey) so "D:/x/src/a.mjs" and
61
- * "d:\x\SRC\a.mjs" count as the same file.
23
+ * "d:\\x\SRC\a.mjs" count as the same file.
62
24
  */
63
25
  function normalizeChangedPath(p, base) {
64
26
  return resolve(base, p).replace(/\\/g, "/")
@@ -71,9 +33,7 @@ function changedFileKey(p) {
71
33
 
72
34
  /**
73
35
  * 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.
36
+ * project/repo root. Walk stops at the filesystem root; returns null when no anchor exists.
77
37
  */
78
38
  function findProjectRoot(absPath) {
79
39
  let dir = dirname(resolve(absPath))
@@ -98,36 +58,51 @@ function isUnderSrc(absPath) {
98
58
  }
99
59
 
100
60
  /**
101
- * verify tool: pre-completion self-check. When called:
102
- * 0. Doc-only fast path all changed files are docs (docs/, *.md, LICENSE…):
103
- * short report, no syntax checks, no tests.
104
- * 1. git diff --stat — changed file list
105
- * 2. node --check — syntax check all changed .mjs/.js files
106
- * 3. Related tests — run test files that cover the changed modules (default)
107
- * 4. npm test — run ALL project tests (only when full=true)
108
- * 5. task list + self-review checklist
109
- * Default runs syntax checks + related tests; full=true runs the entire test suite.
110
- * Agent must not say "done" before verify passes. Fix-verify loop at most MAX_VERIFY_RETRIES rounds.
61
+ * D-V3 block guidance a block message must not just say "not verified": it lists
62
+ * the changed code files and points to the project's AGENTS.md natural-language
63
+ * description of how to verify, so the model knows what to run/add.
64
+ */
65
+ function appendBlockGuidance(lines, codeFiles) {
66
+ lines.push("")
67
+ lines.push("Changed code file(s):")
68
+ for (const f of codeFiles) lines.push(` ${f}`)
69
+ lines.push("")
70
+ lines.push("verify does not run commands for you decide and run the project's verification yourself.")
71
+ lines.push("Reference the verification approach declared in the project's AGENTS.md (natural language, e.g. \"tests run with npm test\" / \"no automated tests — rely on manual review\") to decide what to run, then call verify again declaring the outcome via verification.status.")
72
+ }
73
+
74
+ /**
75
+ * verify tool: generic pre-completion verification gate. Flow:
76
+ * 0. Doc-only fast path — all changed files are docs: short report, no verification needed.
77
+ * 1. git diff --stat — changed file list (_touchedFiles ∪ git diff)
78
+ * 2. Advisory syntax hint (D-V5) — node --check changed .js/.mjs when node exists; SOFT, not gating.
79
+ * 3. Verification gate (D-V1/D-V2) — requires the model to DECLARE verification.status:
80
+ * passed → allowed; failed → blocked; skipped → allowed but needs a summary reason.
81
+ * 4. Task list + self-review checklist.
82
+ * Agent must not say "done" before verify passes.
111
83
  */
112
84
  export const verifyTool = {
113
85
  name: "verify",
114
86
  description:
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.",
87
+ "Generic pre-completion verification gate — language/framework/project agnostic. verify does NOT run any test/verification command for you and does NOT require a test per change. You DECLARE your verification status via the verification argument: { status: 'passed' | 'failed' | 'skipped', command?, summary? }. 'passed' → allowed; 'failed' → blocked (cannot claim done); 'skipped' → allowed but requires a summary reason. Whether/how to verify is described in natural language in the project's AGENTS.md and is decided + executed by you verify only mechanically gates on your declaration. Also reports changed files (git diff), an advisory node --check hint on changed .js/.mjs (not gating), the task list, and a self-review checklist. Call this BEFORE declaring a coding task complete — do not say 'done' until verify passes.",
117
88
  parameters: {
118
89
  type: "object",
119
90
  properties: {
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." },
91
+ verification: {
92
+ type: "object",
93
+ description: "The model's declaration of whether it already ran the project's verification and its outcome. verify does not execute the command — it is self-reported evidence.",
94
+ properties: {
95
+ status: { type: "string", enum: ["passed", "failed", "skipped"], description: "The verification outcome you declare: 'passed' → allowed; 'failed' → blocked; 'skipped' → allowed but requires a summary reason." },
96
+ command: { type: "string", description: "Optional: the verification command you ran (self-reported — verify does not execute it)." },
97
+ summary: { type: "string", description: "Optional: summary/result of the verification. Required when status='skipped' — a concrete reason (e.g. \"project has no automated tests — verified by manual review\")." },
98
+ },
99
+ required: ["status"],
100
+ },
121
101
  workdir: { type: "string", description: "Optional: run verify in this subdirectory (relative to cwd or absolute) — for monorepos" },
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." },
123
102
  },
124
103
  },
125
104
  readonly: true,
126
- outputPanel: true, // stream test output to a panel instead of inline
127
105
  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
- }
131
106
  const cwd = ctx.agent.cwd
132
107
  // Changed-file resolution (§18.12 D-VR3): _touchedFiles (per-run bookkeeping,
133
108
  // absolute paths) ∪ git diff fallback — git is tried at testCwd
@@ -183,14 +158,31 @@ export const verifyTool = {
183
158
  return true
184
159
  })
185
160
 
161
+ // D-V11 G10 (doc-only/empty change set must not swallow an explicit failed):
162
+ // an explicit verification.status='failed' is ALWAYS respected — never
163
+ // overridden to pass by the doc-only fast path or the no-code-changed path
164
+ // below (D-V5 exception, review #6). Aligns CLI with VS Code (T-V8). Lists
165
+ // the changed files + points at AGENTS.md (D-V3 guidance).
166
+ if (args.verification?.status === "failed") {
167
+ lines.push("")
168
+ lines.push("VERIFY BLOCKED: you declared verification failed (verification.status = 'failed').")
169
+ lines.push("Do not say 'done' while verification fails — fix the issue, re-run the project's verification, then call verify again declaring the outcome.")
170
+ lines.push("")
171
+ lines.push("Changed file(s):")
172
+ for (const f of changedFiles) lines.push(` ${f}`)
173
+ lines.push("")
174
+ lines.push("verify does not run commands for you — decide and run the project's verification yourself.")
175
+ lines.push("Reference the verification approach declared in the project's AGENTS.md (natural language, e.g. \"tests run with npm test\" / \"no automated tests — rely on manual review\") to decide what to run, then call verify again declaring the outcome via verification.status.")
176
+ ctx.agent._verifyPassed = false
177
+ return lines.join("\n")
178
+ }
179
+
186
180
  // 1b. Doc-only fast path: every changed file is documentation (docs/, *.md,
187
- // LICENSE…) — syntax checks and tests are meaningless for doc changes, and
188
- // the task list/self-review checklist add nothing either. Mirrors the
189
- // advisor's doc-only review skip ("No issues found documentation-only
190
- // changes, code review skipped."). src/** (incl. prompts/*.md) is product
191
- // code excluded from the fast path, consistent with isProductCode.
192
- // Empty list (no changes / git unavailable) intentionally falls through
193
- // to the normal path below.
181
+ // LICENSE…) — syntax checks and a verification declaration are meaningless
182
+ // for doc changes, and the task list/self-review checklist add nothing either.
183
+ // src/** (incl. prompts/*.md) is product codeexcluded from the fast path,
184
+ // consistent with isProductCode. Empty list (no changes / git unavailable)
185
+ // intentionally falls through to the normal path below.
194
186
  if (changedFiles.length > 0 && changedFiles.every((f) => !isUnderSrc(f) && isDocFile(f))) {
195
187
  lines.push("")
196
188
  lines.push("Documentation-only changes — skipping syntax checks and tests.")
@@ -198,133 +190,72 @@ export const verifyTool = {
198
190
  return lines.join("\n")
199
191
  }
200
192
 
201
- // 2. Syntax check: run node --check on all changed .mjs/.js files (skip deleted files)
202
- let syntaxFailed = false
203
- const jsFiles = changedFiles.filter((f) => /\.(m?js)$/i.test(f))
193
+ // Code files needing verification: anything under src/ (incl. src/prompts/*.md)
194
+ // or any non-doc file (D-V1 — these require a verification declaration).
195
+ const codeFiles = changedFiles.filter((f) => isUnderSrc(f) || !isDocFile(f))
196
+
197
+ // 2. Advisory syntax hint (D-V5) — node --check on changed .js/.mjs when node
198
+ // exists. SOFT hint only — it does NOT gate done (no language-specific
199
+ // enforcement; a non-JS project or a missing node simply skips silently).
200
+ const jsFiles = codeFiles.filter((f) => /\.(m?js)$/i.test(f) && existsSync(f))
204
201
  if (jsFiles.length > 0) {
205
202
  lines.push("")
206
- lines.push("Syntax check (node --check):")
203
+ lines.push("Advisory syntax hint (node --check — does not gate):")
207
204
  for (const f of jsFiles) {
208
- const abs = resolve(cwd, f)
209
- if (!existsSync(abs)) continue // skip deleted files
210
205
  try {
211
- const result = spawnSync("node", ["--check", abs], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 10000 })
206
+ const result = spawnSync("node", ["--check", f], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 10000 })
212
207
  if (result.status !== 0) throw result
213
208
  lines.push(` ✓ ${f}`)
214
209
  } catch (e) {
215
- syntaxFailed = true
216
210
  const errOutput = (e.stderr || e.stdout || e.message || "").toString()
217
211
  const errMsg = errOutput.split("\n").slice(0, 3).join("\n")
218
- lines.push(` ${f} — syntax error`)
212
+ lines.push(` ${f} — node --check flagged it`)
219
213
  lines.push(` ${errMsg.replace(/\n/g, "\n ")}`)
220
214
  }
221
215
  }
222
- if (!syntaxFailed) lines.push(" All syntax checks passed.")
223
216
  }
224
217
 
225
- // 3. Identify related test files for changed source modules
226
- const srcFiles = changedFiles.filter((f) => /\.mjs$/i.test(f) && isUnderSrc(f) && existsSync(f))
227
- const modules = [...new Set(srcFiles.map(moduleName))]
228
- const relatedTests = [...new Set(modules.flatMap((m) => MODULE_TO_TEST[m] ?? []).filter(Boolean))]
229
-
230
- // 4. Run tests
231
- const pkgPath = join(testCwd, "package.json")
232
- const hasTestScript = existsSync(pkgPath) && (() => { try { return !!JSON.parse(readFileSync(pkgPath, "utf8")).scripts?.test } catch { return false } })()
233
-
234
- if (args.full) {
235
- // Full mode: run the entire test suite
236
- if (hasTestScript) {
218
+ // 3. Verification gate (D-V1/D-V2): the model declares its verification status.
219
+ if (codeFiles.length === 0) {
220
+ // No code changed (empty change set / only temp/non-code files) — nothing to verify.
221
+ lines.push("")
222
+ lines.push("No code files changed — nothing to verify.")
223
+ ctx.agent._verifyPassed = true
224
+ } else {
225
+ const v = args.verification ?? {}
226
+ const status = v?.status
227
+ if (!status) {
228
+ lines.push("")
229
+ lines.push("VERIFY BLOCKED: no verification status was declared.")
230
+ lines.push("These code changes must be verified before you say 'done' — declare whether you ran the project's verification and its result.")
231
+ appendBlockGuidance(lines, codeFiles)
232
+ ctx.agent._verifyPassed = false
233
+ } else if (status === "passed") {
237
234
  lines.push("")
238
- lines.push("Tests (full suite):")
239
- const result = await runTestSuite(testCwd, ctx, args.testNamePattern)
240
- if (result.passed) {
241
- lines.push("✓ All tests passed.")
242
- ctx.agent._verifyPassed = !syntaxFailed
235
+ lines.push("Verification declared passed.")
236
+ if (v.command) lines.push(` command: ${v.command}`)
237
+ if (v.summary) lines.push(` summary: ${v.summary}`)
238
+ ctx.agent._verifyPassed = true
239
+ } else if (status === "skipped") {
240
+ const reason = (v.summary ?? "").trim()
241
+ if (reason) {
242
+ lines.push("")
243
+ lines.push(`Verification skipped with reason: ${reason}`)
244
+ ctx.agent._verifyPassed = true
243
245
  } else {
244
- lines.push("✗ Tests FAILED. Review the output above, fix the issues, then run verify again.")
246
+ lines.push("")
247
+ lines.push("VERIFY BLOCKED: verification skipped with no summary reason — an empty skip is not allowed.")
248
+ lines.push("Give a concrete reason in verification.summary (e.g. \"project has no automated tests — verified by manual review\"), or run the project's verification and declare its result.")
249
+ appendBlockGuidance(lines, codeFiles)
245
250
  ctx.agent._verifyPassed = false
246
251
  }
247
252
  } else {
253
+ // Unknown status value — defensive (schema enum normally rejects it first).
248
254
  lines.push("")
249
- lines.push("Tests: no test script in package.json — skipped.")
250
- ctx.agent._verifyPassed = !syntaxFailed
251
- }
252
- } else if (relatedTests.length > 0) {
253
- // Default mode: run only related test files
254
- lines.push("")
255
- lines.push(`Related tests (${relatedTests.length} file(s) for modules: ${modules.join(", ")}):`)
256
- let anyTestFailed = false
257
- let anyTestMissing = false
258
- for (const testFile of relatedTests) {
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`)
275
- continue
276
- }
277
- try {
278
- const result = await runTestFile(testRunCwd, testAbs, ctx, args.testNamePattern)
279
- if (result.passed) {
280
- lines.push(` ✓ ${testFile}`)
281
- } else {
282
- anyTestFailed = true
283
- lines.push(` ✗ ${testFile} — FAILED`)
284
- lines.push(` ${result.tail.replace(/\n/g, "\n ")}`)
285
- }
286
- } catch (e) {
287
- anyTestFailed = true
288
- lines.push(` ✗ ${testFile} — error: ${e.message}`)
289
- }
290
- }
291
- if (anyTestFailed) {
292
- lines.push("")
293
- lines.push("✗ Related tests FAILED. Review the output above, fix the issues, then run verify again.")
255
+ lines.push(`VERIFY BLOCKED: unknown verification.status ${JSON.stringify(status)}use 'passed' | 'failed' | 'skipped'.`)
256
+ appendBlockGuidance(lines, codeFiles)
294
257
  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
299
- } else {
300
- lines.push(" All related tests passed.")
301
- ctx.agent._verifyPassed = !syntaxFailed
302
258
  }
303
- } else {
304
- // No related tests found for the changed modules
305
- const uncovered = modules.filter((m) => !MODULE_TO_TEST[m] || (Array.isArray(MODULE_TO_TEST[m]) && MODULE_TO_TEST[m].length === 0))
306
- const untested = modules.filter((m) => !(m in MODULE_TO_TEST))
307
- lines.push("")
308
- if (uncovered.length > 0) {
309
- lines.push(`Related tests: NONE for module(s) ${uncovered.join(", ")} — these modules have no dedicated test file.`)
310
- lines.push("ACTION REQUIRED: write a test that covers the change you just made.")
311
- lines.push("Do NOT proceed to 'done' — a test file is required before this change is complete.")
312
- } else if (untested.length > 0) {
313
- lines.push(`Related tests: NONE for module(s) ${untested.join(", ")} — unknown module, no test mapping exists.`)
314
- lines.push("ACTION REQUIRED: determine which test file covers this code and add it to MODULE_TO_TEST, or write a new test.")
315
- } else if (srcFiles.length === 0) {
316
- lines.push("Related tests: no source .mjs files changed — nothing to test.")
317
- ctx.agent._verifyPassed = !syntaxFailed
318
- } else {
319
- lines.push("Related tests: none matched. Run verify with full=true to run the full suite.")
320
- ctx.agent._verifyPassed = !syntaxFailed
321
- }
322
- }
323
-
324
- // Show full-suite hint when not run
325
- if (!args.full && hasTestScript) {
326
- lines.push("")
327
- lines.push("Note: full test suite not run. Use verify full=true to run ALL tests before committing.")
328
259
  }
329
260
 
330
261
  // 4. Task list
@@ -349,7 +280,7 @@ export const verifyTool = {
349
280
  // 5. Checklist
350
281
  lines.push("")
351
282
  lines.push("Self-review checklist:")
352
- lines.push("- [ ] Did I run the project's tests and do they pass?")
283
+ lines.push("- [ ] Did I run the project's verification (per its AGENTS.md) and does it pass, or did I declare a concrete skip reason?")
353
284
  lines.push("- [ ] Did I read every file I changed to catch leftover debug code or stale comments?")
354
285
  lines.push("- [ ] Do comments and docstrings match what the code actually does?")
355
286
  lines.push("- [ ] Did I remove placeholder code, TODO stubs, or commented-out experiment blocks?")
@@ -359,86 +290,3 @@ export const verifyTool = {
359
290
  return lines.join("\n")
360
291
  },
361
292
  }
362
-
363
- /**
364
- * Run a single test file with node --test, no maxBuffer limit.
365
- * Returns { passed: boolean, tail: string } — the last few lines of output.
366
- */
367
- function runTestFile(cwd, testPath, ctx, filter) {
368
- return new Promise((resolve, reject) => {
369
- const child = spawn("node", filter ? ["--test", "--test-name-pattern", filter, testPath] : ["--test", testPath], {
370
- cwd, stdio: ["ignore", "pipe", "pipe"],
371
- env: { ...process.env, FORCE_COLOR: "0" },
372
- })
373
- let stdout = ""
374
- let stderr = ""
375
- child.stdout.on("data", (d) => {
376
- const s = d.toString()
377
- stdout += s
378
- ctx.callbacks?.onToolOutput?.("verify", s)
379
- })
380
- child.stderr.on("data", (d) => {
381
- const s = d.toString()
382
- stderr += s
383
- ctx.callbacks?.onToolOutput?.("verify", s)
384
- })
385
- const timer = setTimeout(() => {
386
- child.kill("SIGKILL")
387
- reject(new Error(`Test ${testPath} timed out after 120s`))
388
- }, 120000)
389
- child.on("error", (e) => {
390
- clearTimeout(timer)
391
- reject(e)
392
- })
393
- child.on("close", (code) => {
394
- clearTimeout(timer)
395
- const output = (stdout + stderr).trim()
396
- const tail = output.split("\n").slice(-8).join("\n")
397
- resolve({ passed: code === 0, tail })
398
- })
399
- })
400
- }
401
-
402
- /**
403
- * Run npm test via spawn, no maxBuffer limit.
404
- * Test output is streamed through ctx.callbacks.onToolOutput (TUI can display progress in real time).
405
- * Returns { passed: boolean, tail: string }.
406
- */
407
- function runTestSuite(cwd, ctx, filter) {
408
- return new Promise((resolve, reject) => {
409
- const child = spawn("npm", filter ? ["test", "--", `--test-name-pattern=${filter}`] : ["test"], {
410
- cwd, shell: true, stdio: ["ignore", "pipe", "pipe"],
411
- env: { ...process.env, FORCE_COLOR: "0" },
412
- })
413
- let stdout = ""
414
- let stderr = ""
415
- child.stdout.on("data", (d) => {
416
- const s = d.toString()
417
- stdout += s
418
- ctx.callbacks?.onToolOutput?.("verify", s)
419
- })
420
- child.stderr.on("data", (d) => {
421
- const s = d.toString()
422
- stderr += s
423
- ctx.callbacks?.onToolOutput?.("verify", s)
424
- })
425
- const timer = setTimeout(() => {
426
- child.kill("SIGKILL")
427
- const err = new Error("Tests timed out after 120s")
428
- err.stdout = stdout
429
- err.stderr = stderr
430
- reject(err)
431
- }, 120000)
432
- child.on("error", (e) => {
433
- clearTimeout(timer)
434
- reject(e)
435
- })
436
- child.on("close", (code) => {
437
- clearTimeout(timer)
438
- const output = (stdout + stderr).trim()
439
- const tail = output.split("\n").slice(-8).join("\n")
440
- resolve({ passed: code === 0, tail })
441
- })
442
- })
443
- }
444
-