thincoder 0.12.59 → 0.12.61

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 (192) hide show
  1. package/CHANGELOG.md +62 -4
  2. package/README.md +10 -8
  3. package/bin/thincoder.mjs +99 -133
  4. package/package.json +6 -4
  5. package/src/abort-provenance.mjs +116 -0
  6. package/src/acp/bridge.mjs +45 -21
  7. package/src/acp.mjs +6 -1
  8. package/src/advisor/citations.mjs +83 -21
  9. package/src/advisor/compaction.mjs +174 -0
  10. package/src/advisor/loop.mjs +293 -0
  11. package/src/advisor/messages.mjs +59 -137
  12. package/src/advisor/project-context.mjs +194 -0
  13. package/src/advisor/repos.mjs +17 -40
  14. package/src/advisor/run.mjs +156 -359
  15. package/src/advisor/truncate.mjs +57 -0
  16. package/src/advisor.mjs +27 -7
  17. package/src/agent/completion.mjs +17 -11
  18. package/src/agent/dispatch.mjs +145 -27
  19. package/src/agent/helpers.mjs +107 -13
  20. package/src/agent/record-results.mjs +55 -11
  21. package/src/agent/relay-prefix.mjs +39 -0
  22. package/src/agent/run-stages.mjs +242 -0
  23. package/src/agent/setup-reminders.mjs +69 -0
  24. package/src/agent/setup.mjs +107 -127
  25. package/src/agent/spawn-child.mjs +55 -13
  26. package/src/agent-tools/advisor-async.mjs +346 -0
  27. package/src/agent-tools/advisor-settle.mjs +231 -0
  28. package/src/agent-tools/advisor.mjs +167 -116
  29. package/src/agent-tools/async-settle.mjs +191 -0
  30. package/src/agent-tools/batch-segment.mjs +195 -0
  31. package/src/agent-tools/consult.mjs +139 -107
  32. package/src/agent-tools/design-token.mjs +117 -0
  33. package/src/agent-tools/digest-budget.mjs +76 -0
  34. package/src/agent-tools/eng.mjs +24 -29
  35. package/src/agent-tools/escalate-async.mjs +289 -0
  36. package/src/agent-tools/read-history.mjs +166 -32
  37. package/src/agent-tools/recent-changes.mjs +2 -1
  38. package/src/agent-tools/review-streak.mjs +93 -0
  39. package/src/agent-tools/settings.mjs +137 -34
  40. package/src/agent-tools/subagent-actions.mjs +180 -133
  41. package/src/agent-tools/subagent-async.mjs +184 -177
  42. package/src/agent-tools/subagent-panel.mjs +160 -0
  43. package/src/agent-tools/subagent-run.mjs +205 -0
  44. package/src/agent-tools/subagent-scheduler.mjs +100 -27
  45. package/src/agent-tools/subagent-spawn.mjs +453 -0
  46. package/src/agent-tools/subagent.mjs +256 -578
  47. package/src/agent-tools/verify.mjs +119 -292
  48. package/src/agent-tools.mjs +1 -0
  49. package/src/agent.mjs +89 -205
  50. package/src/cli/distill-command.mjs +12 -6
  51. package/src/cli/make-agent.mjs +26 -8
  52. package/src/cli/memory-command.mjs +4 -3
  53. package/src/cli/permission.mjs +2 -2
  54. package/src/cli/setup-wizard.mjs +42 -17
  55. package/src/completions.mjs +114 -0
  56. package/src/config-migrate.mjs +70 -0
  57. package/src/config.mjs +180 -63
  58. package/src/context.mjs +5 -147
  59. package/src/conventions.mjs +223 -0
  60. package/src/crash-reports.mjs +128 -0
  61. package/src/distill.mjs +11 -11
  62. package/src/expand-home.mjs +16 -0
  63. package/src/explore-distill.mjs +155 -0
  64. package/src/generate-title.mjs +1 -1
  65. package/src/hooks.mjs +7 -3
  66. package/src/memory/code-index.mjs +9 -3
  67. package/src/memory/code-sync.mjs +72 -32
  68. package/src/memory/core.mjs +6 -193
  69. package/src/memory/delete.mjs +236 -0
  70. package/src/memory/docs.mjs +68 -54
  71. package/src/memory/file-walk.mjs +109 -0
  72. package/src/memory/schema.mjs +15 -3
  73. package/src/memory.mjs +3 -1
  74. package/src/model-ref.mjs +66 -0
  75. package/src/model-specs.mjs +42 -8
  76. package/src/peer-domains.mjs +265 -0
  77. package/src/peer-instances.mjs +231 -0
  78. package/src/prompt-overlays.mjs +82 -0
  79. package/src/prompts/advisor-design.md +18 -75
  80. package/src/prompts/advisor-round1.md +14 -67
  81. package/src/prompts/advisor-round2.md +15 -51
  82. package/src/prompts/advisor-round3.md +15 -51
  83. package/src/prompts/common.md +115 -0
  84. package/src/prompts/consult-base.md +5 -23
  85. package/src/prompts/discipline-engineering.md +217 -0
  86. package/src/prompts/discipline-normal.md +179 -0
  87. package/src/prompts/persona-coder.md +21 -0
  88. package/src/prompts/persona-eng-coder.md +37 -0
  89. package/src/prompts/persona-eng-designer.md +55 -0
  90. package/src/prompts/persona-engineering.md +54 -0
  91. package/src/prompts/persona-explore.md +15 -0
  92. package/src/prompts/persona-normal.md +27 -0
  93. package/src/prompts/persona-plan.md +26 -0
  94. package/src/provider/anthropic.mjs +4 -4
  95. package/src/provider/core.mjs +18 -98
  96. package/src/provider/errors.mjs +101 -0
  97. package/src/provider/google.mjs +5 -6
  98. package/src/provider/index.mjs +2 -1
  99. package/src/provider/list-models.mjs +93 -0
  100. package/src/provider/rate.mjs +2 -1
  101. package/src/provider/responses.mjs +5 -3
  102. package/src/provider/retry.mjs +8 -45
  103. package/src/provider/sse.mjs +3 -4
  104. package/src/proxy.mjs +9 -14
  105. package/src/session-gc.mjs +214 -0
  106. package/src/session-guard.mjs +47 -0
  107. package/src/session-rename.mjs +38 -0
  108. package/src/session-slots.mjs +188 -60
  109. package/src/session.mjs +104 -124
  110. package/src/token-ttl.mjs +274 -0
  111. package/src/tools/{system.mjs → bash.mjs} +19 -221
  112. package/src/tools/checklist-sync.mjs +181 -0
  113. package/src/tools/checklist.mjs +52 -39
  114. package/src/tools/edit-batch.mjs +109 -10
  115. package/src/tools/edit-diff.mjs +110 -27
  116. package/src/tools/edit.md +17 -12
  117. package/src/tools/execute.mjs +31 -4
  118. package/src/tools/file.mjs +41 -16
  119. package/src/tools/git.md +1 -1
  120. package/src/tools/git.mjs +23 -34
  121. package/src/tools/glob-dialect.mjs +130 -0
  122. package/src/tools/glob.md +3 -3
  123. package/src/tools/grep.md +1 -1
  124. package/src/tools/index.mjs +9 -8
  125. package/src/tools/ops.mjs +188 -3
  126. package/src/tools/patch.mjs +3 -3
  127. package/src/tools/question.md +4 -0
  128. package/src/tools/question.mjs +26 -0
  129. package/src/tools/read.md +1 -2
  130. package/src/tools/read_image.md +1 -1
  131. package/src/tools/search.mjs +236 -0
  132. package/src/tools/shared.mjs +14 -13
  133. package/src/tools/wait_for.md +22 -0
  134. package/src/tui/agent-turn.mjs +36 -228
  135. package/src/tui/ansi.mjs +2 -0
  136. package/src/tui/clipboard.mjs +7 -1
  137. package/src/tui/cmd-advisor.mjs +3 -2
  138. package/src/tui/cmd-config.mjs +142 -30
  139. package/src/tui/cmd-eng.mjs +28 -40
  140. package/src/tui/cmd-exit.mjs +6 -8
  141. package/src/tui/cmd-mcp.mjs +8 -2
  142. package/src/tui/cmd-model.mjs +14 -12
  143. package/src/tui/cmd-new.mjs +3 -2
  144. package/src/tui/cmd-reindex.mjs +7 -0
  145. package/src/tui/cmd-session.mjs +19 -4
  146. package/src/tui/cmd-submodel.mjs +8 -5
  147. package/src/tui/cmd-think.mjs +10 -10
  148. package/src/tui/cmd-undo.mjs +4 -3
  149. package/src/tui/cmd-upgrade.mjs +19 -4
  150. package/src/tui/config-helpers.mjs +28 -16
  151. package/src/tui/distill-cmd.mjs +1 -1
  152. package/src/tui/index.mjs +40 -38
  153. package/src/tui/interaction.mjs +3 -3
  154. package/src/tui/key-handler.mjs +61 -17
  155. package/src/tui/key-modes.mjs +86 -8
  156. package/src/tui/layout.mjs +18 -10
  157. package/src/tui/model-catalog.mjs +89 -0
  158. package/src/tui/model-picker.mjs +498 -0
  159. package/src/tui/mouse.mjs +52 -9
  160. package/src/tui/pickers.mjs +28 -392
  161. package/src/tui/render-frame.mjs +32 -16
  162. package/src/tui/render-loop.mjs +2 -0
  163. package/src/tui/render-segments.mjs +12 -9
  164. package/src/tui/render.mjs +37 -5
  165. package/src/tui/slash-commands.mjs +2 -2
  166. package/src/tui/startup.mjs +4 -0
  167. package/src/tui/subagent-blocks.mjs +106 -295
  168. package/src/tui/subagent-children.mjs +162 -0
  169. package/src/tui/subagent-freeze.mjs +169 -0
  170. package/src/tui/subagent-panel.mjs +24 -31
  171. package/src/tui/suspension-drive.mjs +297 -0
  172. package/src/tui/tool-args.mjs +7 -5
  173. package/src/tui/tool-display.mjs +143 -0
  174. package/src/tui/tool-events.mjs +56 -185
  175. package/src/tui/tui-lifecycle.mjs +46 -4
  176. package/src/tui/update-notice.mjs +4 -0
  177. package/src/tui/wizard.mjs +61 -21
  178. package/src/tui/wrapped-spawn.mjs +38 -0
  179. package/src/prompts/coder.md +0 -56
  180. package/src/prompts/discipline.md +0 -102
  181. package/src/prompts/eng-coder.md +0 -44
  182. package/src/prompts/engineering-sub.md +0 -34
  183. package/src/prompts/engineering.md +0 -340
  184. package/src/prompts/explore.md +0 -21
  185. package/src/prompts/main.md +0 -56
  186. package/src/prompts/methodology-template.md +0 -58
  187. package/src/prompts/plan.md +0 -16
  188. package/src/prompts/system.md +0 -63
  189. package/src/tools/pdf-parse-text.mjs +0 -497
  190. package/src/tools/pdf-parse-xref.mjs +0 -499
  191. package/src/tools/pdf.mjs +0 -155
  192. 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 { isCodePath, isDocPath, loadConventions } from "../conventions.mjs"
15
+ import { execSync, spawnSync } from "node:child_process"
16
+ import { existsSync } from "node:fs"
17
+ import { 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, "/")
@@ -70,64 +32,51 @@ function changedFileKey(p) {
70
32
  }
71
33
 
72
34
  /**
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.
35
+ * D-V3 block guidance a block message must not just say "not verified": it lists
36
+ * the changed code files and points to the project's AGENTS.md natural-language
37
+ * description of how to verify, so the model knows what to run/add.
92
38
  */
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) + "/")
39
+ function appendBlockGuidance(lines, codeFiles) {
40
+ lines.push("")
41
+ lines.push("Changed code file(s):")
42
+ for (const f of codeFiles) lines.push(` ${f}`)
43
+ lines.push("")
44
+ lines.push("verify does not run commands for you — decide and run the project's verification yourself.")
45
+ 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.")
98
46
  }
99
47
 
100
48
  /**
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 --checksyntax 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.
49
+ * verify tool: generic pre-completion verification gate. Flow:
50
+ * 0. Doc-only fast path — all changed files are docs: short report, no verification needed.
51
+ * 1. git diff --stat changed file list (_touchedFiles ∪ git diff)
52
+ * 2. Advisory syntax hint (D-V5) node --check changed .js/.mjs when node exists; SOFT, not gating.
53
+ * 3. Verification gate (D-V1/D-V2) requires the model to DECLARE verification.status:
54
+ * passed allowed; failed blocked; skipped allowed but needs a summary reason.
55
+ * 4. Task list + self-review checklist.
56
+ * Agent must not say "done" before verify passes.
111
57
  */
112
58
  export const verifyTool = {
113
59
  name: "verify",
114
60
  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.",
61
+ "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
62
  parameters: {
118
63
  type: "object",
119
64
  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." },
65
+ verification: {
66
+ type: "object",
67
+ 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.",
68
+ properties: {
69
+ status: { type: "string", enum: ["passed", "failed", "skipped"], description: "The verification outcome you declare: 'passed' → allowed; 'failed' → blocked; 'skipped' → allowed but requires a summary reason." },
70
+ command: { type: "string", description: "Optional: the verification command you ran (self-reported — verify does not execute it)." },
71
+ 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\")." },
72
+ },
73
+ required: ["status"],
74
+ },
121
75
  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
76
  },
124
77
  },
125
78
  readonly: true,
126
- outputPanel: true, // stream test output to a panel instead of inline
127
79
  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
80
  const cwd = ctx.agent.cwd
132
81
  // Changed-file resolution (§18.12 D-VR3): _touchedFiles (per-run bookkeeping,
133
82
  // absolute paths) ∪ git diff fallback — git is tried at testCwd
@@ -183,150 +132,111 @@ export const verifyTool = {
183
132
  return true
184
133
  })
185
134
 
135
+ // D-V11 G10 (doc-only/empty change set must not swallow an explicit failed):
136
+ // an explicit verification.status='failed' is ALWAYS respected — never
137
+ // overridden to pass by the doc-only fast path or the no-code-changed path
138
+ // below (D-V5 exception, review #6). Aligns CLI with VS Code (T-V8). Lists
139
+ // the changed files + points at AGENTS.md (D-V3 guidance).
140
+ if (args.verification?.status === "failed") {
141
+ lines.push("")
142
+ lines.push("VERIFY BLOCKED: you declared verification failed (verification.status = 'failed').")
143
+ 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.")
144
+ lines.push("")
145
+ lines.push("Changed file(s):")
146
+ for (const f of changedFiles) lines.push(` ${f}`)
147
+ lines.push("")
148
+ lines.push("verify does not run commands for you — decide and run the project's verification yourself.")
149
+ 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.")
150
+ ctx.agent._verifyPassed = false
151
+ return lines.join("\n")
152
+ }
153
+
186
154
  // 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.
194
- if (changedFiles.length > 0 && changedFiles.every((f) => !isUnderSrc(f) && isDocFile(f))) {
155
+ // LICENSE…) — syntax checks and a verification declaration are meaningless
156
+ // for doc changes, and the task list/self-review checklist add nothing either.
157
+ // Paths inside a declared code segment (default: srcincl. prompts/*.md) are
158
+ // product code excluded from the fast path, consistent with the design gate.
159
+ // The project's own layout is declarable (.thincoder/conventions.json).
160
+ // Empty list (no changes / git unavailable)
161
+ // intentionally falls through to the normal path below.
162
+ const conv = loadConventions(cwd)
163
+ if (changedFiles.length > 0 && changedFiles.every((f) => isDocPath(f, conv))) {
195
164
  lines.push("")
196
165
  lines.push("Documentation-only changes — skipping syntax checks and tests.")
197
166
  ctx.agent._verifyPassed = true
198
167
  return lines.join("\n")
199
168
  }
200
169
 
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))
170
+ // Code files needing verification: anything the shared classifier calls product
171
+ // code (D-V1 — these require a verification declaration). Temp scratch files are
172
+ // neither code nor docs and fall out of this list (a throwaway diagnostic script
173
+ // is not a code change — no declaration is demanded for it).
174
+ const codeFiles = changedFiles.filter((f) => isCodePath(f, conv))
175
+
176
+ // 2. Advisory syntax hint (D-V5) — node --check on changed .js/.mjs when node
177
+ // exists. SOFT hint only — it does NOT gate done (no language-specific
178
+ // enforcement; a non-JS project or a missing node simply skips silently).
179
+ const jsFiles = codeFiles.filter((f) => /\.(m?js)$/i.test(f) && existsSync(f))
204
180
  if (jsFiles.length > 0) {
205
181
  lines.push("")
206
- lines.push("Syntax check (node --check):")
182
+ lines.push("Advisory syntax hint (node --check — does not gate):")
207
183
  for (const f of jsFiles) {
208
- const abs = resolve(cwd, f)
209
- if (!existsSync(abs)) continue // skip deleted files
210
184
  try {
211
- const result = spawnSync("node", ["--check", abs], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 10000 })
185
+ const result = spawnSync("node", ["--check", f], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 10000 })
212
186
  if (result.status !== 0) throw result
213
187
  lines.push(` ✓ ${f}`)
214
188
  } catch (e) {
215
- syntaxFailed = true
216
189
  const errOutput = (e.stderr || e.stdout || e.message || "").toString()
217
190
  const errMsg = errOutput.split("\n").slice(0, 3).join("\n")
218
- lines.push(` ${f} — syntax error`)
191
+ lines.push(` ${f} — node --check flagged it`)
219
192
  lines.push(` ${errMsg.replace(/\n/g, "\n ")}`)
220
193
  }
221
194
  }
222
- if (!syntaxFailed) lines.push(" All syntax checks passed.")
223
195
  }
224
196
 
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) {
197
+ // 3. Verification gate (D-V1/D-V2): the model declares its verification status.
198
+ if (codeFiles.length === 0) {
199
+ // No code changed (empty change set / only temp/non-code files) — nothing to verify.
200
+ lines.push("")
201
+ lines.push("No code files changed — nothing to verify.")
202
+ ctx.agent._verifyPassed = true
203
+ } else {
204
+ const v = args.verification ?? {}
205
+ const status = v?.status
206
+ if (!status) {
207
+ lines.push("")
208
+ lines.push("VERIFY BLOCKED: no verification status was declared.")
209
+ lines.push("These code changes must be verified before you say 'done' — declare whether you ran the project's verification and its result.")
210
+ appendBlockGuidance(lines, codeFiles)
211
+ ctx.agent._verifyPassed = false
212
+ } else if (status === "passed") {
237
213
  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
214
+ lines.push("Verification declared passed.")
215
+ if (v.command) lines.push(` command: ${v.command}`)
216
+ if (v.summary) lines.push(` summary: ${v.summary}`)
217
+ ctx.agent._verifyPassed = true
218
+ } else if (status === "skipped") {
219
+ const reason = (v.summary ?? "").trim()
220
+ if (reason) {
221
+ lines.push("")
222
+ lines.push(`Verification skipped with reason: ${reason}`)
223
+ ctx.agent._verifyPassed = true
243
224
  } else {
244
- lines.push("✗ Tests FAILED. Review the output above, fix the issues, then run verify again.")
225
+ lines.push("")
226
+ lines.push("VERIFY BLOCKED: verification skipped with no summary reason — an empty skip is not allowed.")
227
+ 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.")
228
+ appendBlockGuidance(lines, codeFiles)
245
229
  ctx.agent._verifyPassed = false
246
230
  }
247
231
  } else {
232
+ // Unknown status value — defensive (schema enum normally rejects it first).
248
233
  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.")
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.")
234
+ lines.push(`VERIFY BLOCKED: unknown verification.status ${JSON.stringify(status)}use 'passed' | 'failed' | 'skipped'.`)
235
+ appendBlockGuidance(lines, codeFiles)
298
236
  ctx.agent._verifyPassed = false
299
- } else {
300
- lines.push(" All related tests passed.")
301
- ctx.agent._verifyPassed = !syntaxFailed
302
- }
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
237
  }
322
238
  }
323
239
 
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
- }
329
-
330
240
  // 4. Task list
331
241
  lines.push("")
332
242
  if (ctx.agent.tasks.length === 0) {
@@ -349,7 +259,7 @@ export const verifyTool = {
349
259
  // 5. Checklist
350
260
  lines.push("")
351
261
  lines.push("Self-review checklist:")
352
- lines.push("- [ ] Did I run the project's tests and do they pass?")
262
+ 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
263
  lines.push("- [ ] Did I read every file I changed to catch leftover debug code or stale comments?")
354
264
  lines.push("- [ ] Do comments and docstrings match what the code actually does?")
355
265
  lines.push("- [ ] Did I remove placeholder code, TODO stubs, or commented-out experiment blocks?")
@@ -359,86 +269,3 @@ export const verifyTool = {
359
269
  return lines.join("\n")
360
270
  },
361
271
  }
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
-
@@ -14,3 +14,4 @@ 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
16
  export { readHistoryTool } from "./agent-tools/read-history.mjs"
17
+ export { batchSegmentTool } from "./agent-tools/batch-segment.mjs"