thincoder 0.12.58 → 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 (158) hide show
  1. package/CHANGELOG.md +78 -2
  2. package/README.md +3 -3
  3. package/bin/thincoder.mjs +88 -19
  4. package/package.json +4 -3
  5. package/src/acp/bridge.mjs +135 -26
  6. package/src/advisor/messages.mjs +57 -4
  7. package/src/advisor/run.mjs +119 -79
  8. package/src/advisor.mjs +34 -7
  9. package/src/agent/completion.mjs +17 -11
  10. package/src/agent/dispatch.mjs +182 -22
  11. package/src/agent/helpers.mjs +71 -4
  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 +107 -20
  16. package/src/agent/spawn-child.mjs +54 -4
  17. package/src/agent-tools/advisor-async.mjs +456 -0
  18. package/src/agent-tools/advisor.mjs +133 -109
  19. package/src/agent-tools/async-settle.mjs +191 -0
  20. package/src/agent-tools/consult.mjs +154 -104
  21. package/src/agent-tools/design-token.mjs +104 -0
  22. package/src/agent-tools/eng.mjs +26 -30
  23. package/src/agent-tools/escalate-async.mjs +286 -0
  24. package/src/agent-tools/goal.mjs +11 -1
  25. package/src/agent-tools/read-history.mjs +284 -0
  26. package/src/agent-tools/recent-changes.mjs +2 -1
  27. package/src/agent-tools/settings.mjs +152 -0
  28. package/src/agent-tools/skill.mjs +2 -1
  29. package/src/agent-tools/subagent-actions.mjs +470 -0
  30. package/src/agent-tools/subagent-async.mjs +382 -0
  31. package/src/agent-tools/subagent-panel.mjs +153 -0
  32. package/src/agent-tools/subagent-run.mjs +202 -0
  33. package/src/agent-tools/subagent-scheduler.mjs +343 -0
  34. package/src/agent-tools/subagent-spawn.mjs +406 -0
  35. package/src/agent-tools/subagent.mjs +203 -377
  36. package/src/agent-tools/task.mjs +4 -3
  37. package/src/agent-tools/timer.mjs +9 -4
  38. package/src/agent-tools/verify.mjs +198 -238
  39. package/src/agent-tools.mjs +1 -0
  40. package/src/agent.mjs +145 -242
  41. package/src/auto-think.mjs +14 -0
  42. package/src/cli/distill-command.mjs +10 -4
  43. package/src/cli/make-agent.mjs +4 -1
  44. package/src/cli/memory-command.mjs +2 -1
  45. package/src/cli/permission.mjs +8 -1
  46. package/src/cli/setup-wizard.mjs +17 -12
  47. package/src/config.mjs +61 -8
  48. package/src/context.mjs +81 -163
  49. package/src/crash-reports.mjs +123 -0
  50. package/src/distill.mjs +30 -12
  51. package/src/escape.mjs +6 -4
  52. package/src/explore-distill.mjs +155 -0
  53. package/src/log.mjs +195 -0
  54. package/src/memory/code-sync.mjs +2 -1
  55. package/src/memory/core.mjs +11 -72
  56. package/src/memory/delete.mjs +234 -0
  57. package/src/memory/docs.mjs +206 -87
  58. package/src/memory.mjs +3 -1
  59. package/src/model-specs.mjs +15 -1
  60. package/src/peer-domains.mjs +265 -0
  61. package/src/peer-instances.mjs +231 -0
  62. package/src/prompt-overlays.mjs +25 -0
  63. package/src/prompts/advisor-design.md +18 -39
  64. package/src/prompts/advisor-round1.md +20 -32
  65. package/src/prompts/advisor-round2.md +16 -16
  66. package/src/prompts/advisor-round3.md +16 -16
  67. package/src/prompts/coder.md +7 -28
  68. package/src/prompts/consult-base.md +4 -11
  69. package/src/prompts/discipline.md +31 -44
  70. package/src/prompts/eng-coder.md +9 -34
  71. package/src/prompts/engineering-sub.md +10 -8
  72. package/src/prompts/engineering.md +61 -264
  73. package/src/prompts/explore.md +4 -14
  74. package/src/prompts/main.md +18 -35
  75. package/src/prompts/methodology-template.md +32 -38
  76. package/src/prompts/plan.md +2 -9
  77. package/src/prompts/system.md +18 -35
  78. package/src/provider/core.mjs +62 -69
  79. package/src/provider/errors.mjs +76 -0
  80. package/src/provider/retry.mjs +8 -45
  81. package/src/session-gc.mjs +214 -0
  82. package/src/session-guard.mjs +47 -0
  83. package/src/session-rename.mjs +38 -0
  84. package/src/session-slots.mjs +181 -58
  85. package/src/session.mjs +48 -89
  86. package/src/token-ttl.mjs +273 -0
  87. package/src/tools/apply_patch.md +3 -1
  88. package/src/tools/bash.md +1 -1
  89. package/src/tools/checklist-sync.mjs +181 -0
  90. package/src/tools/checklist.mjs +52 -39
  91. package/src/tools/delete.md +1 -0
  92. package/src/tools/edit-batch.mjs +131 -44
  93. package/src/tools/edit-diff.mjs +348 -0
  94. package/src/tools/edit.md +20 -13
  95. package/src/tools/execute.md +7 -7
  96. package/src/tools/execute.mjs +55 -24
  97. package/src/tools/file.mjs +25 -70
  98. package/src/tools/file_ops.md +2 -1
  99. package/src/tools/get_current_time.md +3 -1
  100. package/src/tools/git.mjs +14 -6
  101. package/src/tools/glob-dialect.mjs +130 -0
  102. package/src/tools/glob.md +3 -3
  103. package/src/tools/grep.md +1 -1
  104. package/src/tools/hashline_edit.md +2 -0
  105. package/src/tools/index.mjs +3 -3
  106. package/src/tools/insert_after.md +2 -1
  107. package/src/tools/lint.md +2 -0
  108. package/src/tools/lsp.md +4 -1
  109. package/src/tools/ops.mjs +175 -3
  110. package/src/tools/patch.mjs +84 -13
  111. package/src/tools/question.md +5 -1
  112. package/src/tools/repomap.mjs +1 -1
  113. package/src/tools/shared.mjs +18 -25
  114. package/src/tools/system.mjs +50 -30
  115. package/src/tools/tree.md +2 -1
  116. package/src/tools/wait_for.md +22 -0
  117. package/src/tools/web.mjs +5 -3
  118. package/src/tools/websearch.md +2 -1
  119. package/src/tools/write.md +2 -0
  120. package/src/traces/trace-store.mjs +224 -0
  121. package/src/tui/agent-turn.mjs +179 -27
  122. package/src/tui/clipboard.mjs +15 -4
  123. package/src/tui/cmd-config.mjs +77 -16
  124. package/src/tui/cmd-eng.mjs +20 -16
  125. package/src/tui/cmd-extract.mjs +1 -1
  126. package/src/tui/cmd-mcp.mjs +17 -2
  127. package/src/tui/cmd-new.mjs +3 -2
  128. package/src/tui/cmd-session.mjs +19 -4
  129. package/src/tui/cmd-think.mjs +11 -11
  130. package/src/tui/cmd-upgrade.mjs +19 -4
  131. package/src/tui/config-helpers.mjs +28 -16
  132. package/src/tui/distill-cmd.mjs +1 -1
  133. package/src/tui/index.mjs +31 -96
  134. package/src/tui/interaction.mjs +13 -2
  135. package/src/tui/key-handler.mjs +105 -155
  136. package/src/tui/key-modes.mjs +215 -0
  137. package/src/tui/layout.mjs +22 -1
  138. package/src/tui/mouse.mjs +46 -0
  139. package/src/tui/pickers.mjs +51 -25
  140. package/src/tui/render-conversation.mjs +13 -161
  141. package/src/tui/render-frame.mjs +27 -10
  142. package/src/tui/render-loop.mjs +4 -1
  143. package/src/tui/render-segments.mjs +182 -0
  144. package/src/tui/startup.mjs +40 -0
  145. package/src/tui/subagent-blocks.mjs +272 -262
  146. package/src/tui/subagent-children.mjs +176 -0
  147. package/src/tui/subagent-freeze.mjs +172 -0
  148. package/src/tui/subagent-panel.mjs +125 -12
  149. package/src/tui/suspension-drive.mjs +351 -0
  150. package/src/tui/tool-args.mjs +10 -2
  151. package/src/tui/tool-display.mjs +142 -0
  152. package/src/tui/tool-events.mjs +127 -231
  153. package/src/tui/tui-lifecycle.mjs +29 -0
  154. package/src/tui/update-notice.mjs +76 -0
  155. package/src/tui/wizard.mjs +48 -12
  156. package/src/agent-tools/escalate.mjs +0 -179
  157. package/src/agent-tools/subagent-check.mjs +0 -107
  158. 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,218 +1,261 @@
1
+ /**
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.
12
+ */
13
+
1
14
  import { isDocFile } from "../advisor/repos.mjs"
2
- import { execSync, spawn, spawnSync } from "node:child_process"
3
- import { readFileSync, existsSync } from "node:fs"
4
- import { join, resolve } from "node:path"
15
+ import { execSync, spawnSync } from "node:child_process"
16
+ import { existsSync } from "node:fs"
17
+ import { dirname, join, resolve } from "node:path"
18
+
19
+ /**
20
+ * §18.12 D-VR1 path normalization — mirrors the §20.5 file-domain handling:
21
+ * resolved to an absolute path with forward slashes; dedup uses the win32
22
+ * lowercase comparison key (changedFileKey) so "D:/x/src/a.mjs" and
23
+ * "d:\\x\SRC\a.mjs" count as the same file.
24
+ */
25
+ function normalizeChangedPath(p, base) {
26
+ return resolve(base, p).replace(/\\/g, "/")
27
+ }
28
+
29
+ /** Win32 comparison key: case-insensitive drives/folders (same file). */
30
+ function changedFileKey(p) {
31
+ return process.platform === "win32" ? p.toLowerCase() : p
32
+ }
5
33
 
6
34
  /**
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.
35
+ * Nearest ancestor of an absolute path holding package.json or .git the
36
+ * project/repo root. Walk stops at the filesystem root; returns null when no anchor exists.
10
37
  */
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",
23
- mcp: null,
24
- prompts: null,
25
- context: null,
26
- session: null,
38
+ function findProjectRoot(absPath) {
39
+ let dir = dirname(resolve(absPath))
40
+ for (;;) {
41
+ if (existsSync(join(dir, "package.json")) || existsSync(join(dir, ".git"))) return dir
42
+ const parent = dirname(dir)
43
+ if (parent === dir) return null
44
+ dir = parent
45
+ }
27
46
  }
28
47
 
29
48
  /**
30
- * Extract module name from a source path.
31
- * "src/tools/bash.mjs" "tools", "src/agent.mjs" "agent", "src/agent/helpers.mjs" "agent"
49
+ * True when the path is under <projectRoot>/src/ (anchored via findProjectRoot —
50
+ * a parent-path /src/ segment outside the project must not classify a file as
51
+ * product code). Falls back to the loose /src/ pattern when no anchor exists.
32
52
  */
33
- function moduleName(srcPath) {
34
- const rel = srcPath.replace(/^src[/\\]/, "")
35
- const firstSlash = rel.search(/[/\\]/)
36
- if (firstSlash === -1) return rel.replace(/\.mjs$/, "")
37
- return rel.slice(0, firstSlash)
53
+ function isUnderSrc(absPath) {
54
+ const root = findProjectRoot(absPath)
55
+ if (!root) return /(?:^|[\\/])src[\\/]/.test(absPath)
56
+ const norm = normalizeChangedPath(absPath, root)
57
+ return norm.startsWith(normalizeChangedPath("src", root) + "/")
38
58
  }
39
59
 
40
60
  /**
41
- * verify tool: pre-completion self-check. When called:
42
- * 0. Doc-only fast path all changed files are docs (docs/, *.md, LICENSE…):
43
- * short report, no syntax checks, no tests.
44
- * 1. git diff --stat — changed file list
45
- * 2. node --check — syntax check all changed .mjs/.js files
46
- * 3. Related tests — run test files that cover the changed modules (default)
47
- * 4. npm test — run ALL project tests (only when full=true)
48
- * 5. task list + self-review checklist
49
- * Default runs syntax checks + related tests; full=true runs the entire test suite.
50
- * 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.
51
83
  */
52
84
  export const verifyTool = {
53
85
  name: "verify",
54
86
  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.",
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.",
56
88
  parameters: {
57
89
  type: "object",
58
90
  properties: {
59
- 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
+ },
60
101
  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)" },
62
102
  },
63
103
  },
64
104
  readonly: true,
65
- outputPanel: true, // stream test output to a panel instead of inline
66
105
  async execute(args, ctx) {
67
106
  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.
107
+ // Changed-file resolution (§18.12 D-VR3): _touchedFiles (per-run bookkeeping,
108
+ // absolute paths) ∪ git diff fallback git is tried at testCwd
109
+ // (workdir-resolved) first, then at ctx.agent.cwd; first success wins.
110
+ // git-diff paths are repo-root-relative — resolved against the discovered
111
+ // git root, then normalized (absolute, forward slashes, win32 key).
70
112
  const testCwd = args.workdir ? resolve(cwd, args.workdir) : cwd
71
113
  const lines = []
72
114
  lines.push("=== VERIFICATION REPORT ===")
73
115
  lines.push("")
74
116
 
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 {
117
+ // 1. Changed files_touchedFiles git diff (§18.12 D-VR1)
118
+ let gitOk = false
119
+ let gitStat = ""
120
+ let gitFiles = []
121
+ for (const gitCwd of new Set([testCwd, cwd])) {
122
+ try {
123
+ // Anchor the repo root FIRST (also proves git works here) — diff paths
124
+ // are repo-root-relative, so both diff calls run at the root.
125
+ const gitRoot = execSync("git rev-parse --show-toplevel", { cwd: gitCwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 }).trim() || gitCwd
126
+ const diff = execSync("git diff --stat", { cwd: gitRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 })
127
+ const nameOnly = execSync("git diff --name-only", { cwd: gitRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 })
128
+ gitOk = true
129
+ gitStat = diff.trim()
130
+ gitFiles = nameOnly.trim().split("\n").filter(Boolean).map((p) => normalizeChangedPath(p, gitRoot))
131
+ break
132
+ } catch { /* git unavailable here — try the next cwd in the chain */ }
133
+ }
134
+ if (gitOk && gitStat) {
135
+ lines.push("Changed files (git diff --stat):")
136
+ lines.push(gitStat)
137
+ } else if (gitOk) {
138
+ lines.push("Changed files: (none — no uncommitted changes)")
139
+ } else {
89
140
  lines.push("Changed files: (not a git repo or git unavailable)")
90
141
  }
142
+ // _touchedFiles: files written by the tools this run — captured regardless
143
+ // of git state (§18.12 F-VR1: subagent cwd ≠ git root must still locate).
144
+ const touchedFiles = (ctx.agent._touchedFiles ?? []).map((p) => normalizeChangedPath(p, cwd))
145
+ const gitKeys = new Set(gitFiles.map(changedFileKey))
146
+ const touchedOnly = touchedFiles.filter((p) => !gitKeys.has(changedFileKey(p)))
147
+ if (touchedOnly.length > 0) {
148
+ lines.push("")
149
+ lines.push("Changed files (this run — _touchedFiles):")
150
+ for (const f of touchedOnly) lines.push(` ${f}`)
151
+ }
152
+ // Union, deduped by comparison key (win32 case-insensitive).
153
+ const changedKeys = new Set()
154
+ const changedFiles = [...touchedFiles, ...gitFiles].filter((p) => {
155
+ const k = changedFileKey(p)
156
+ if (changedKeys.has(k)) return false
157
+ changedKeys.add(k)
158
+ return true
159
+ })
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
+ }
91
179
 
92
180
  // 1b. Doc-only fast path: every changed file is documentation (docs/, *.md,
93
- // LICENSE…) — syntax checks and tests are meaningless for doc changes, and
94
- // the task list/self-review checklist add nothing either. Mirrors the
95
- // advisor's doc-only review skip ("No issues found documentation-only
96
- // changes, code review skipped."). src/** (incl. prompts/*.md) is product
97
- // code excluded from the fast path, consistent with isProductCode.
98
- // Empty list (no changes / git unavailable) intentionally falls through
99
- // to the normal path below.
100
- if (changedFiles.length > 0 && changedFiles.every((f) => !/^src[\\/]/.test(f) && isDocFile(f))) {
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.
186
+ if (changedFiles.length > 0 && changedFiles.every((f) => !isUnderSrc(f) && isDocFile(f))) {
101
187
  lines.push("")
102
188
  lines.push("Documentation-only changes — skipping syntax checks and tests.")
103
189
  ctx.agent._verifyPassed = true
104
190
  return lines.join("\n")
105
191
  }
106
192
 
107
- // 2. Syntax check: run node --check on all changed .mjs/.js files (skip deleted files)
108
- let syntaxFailed = false
109
- 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))
110
201
  if (jsFiles.length > 0) {
111
202
  lines.push("")
112
- lines.push("Syntax check (node --check):")
203
+ lines.push("Advisory syntax hint (node --check — does not gate):")
113
204
  for (const f of jsFiles) {
114
- const abs = join(cwd, f)
115
- if (!existsSync(abs)) continue // skip deleted files
116
205
  try {
117
- 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 })
118
207
  if (result.status !== 0) throw result
119
208
  lines.push(` ✓ ${f}`)
120
209
  } catch (e) {
121
- syntaxFailed = true
122
210
  const errOutput = (e.stderr || e.stdout || e.message || "").toString()
123
211
  const errMsg = errOutput.split("\n").slice(0, 3).join("\n")
124
- lines.push(` ${f} — syntax error`)
212
+ lines.push(` ${f} — node --check flagged it`)
125
213
  lines.push(` ${errMsg.replace(/\n/g, "\n ")}`)
126
214
  }
127
215
  }
128
- if (!syntaxFailed) lines.push(" All syntax checks passed.")
129
216
  }
130
217
 
131
- // 3. Identify related test files for changed source modules
132
- const srcFiles = changedFiles.filter((f) => /^src[/\\].+\.mjs$/i.test(f) && existsSync(join(cwd, f)))
133
- const modules = [...new Set(srcFiles.map(moduleName))]
134
- const relatedTests = [...new Set(modules.map((m) => MODULE_TO_TEST[m]).filter(Boolean))]
135
-
136
- // 4. Run tests
137
- const pkgPath = join(testCwd, "package.json")
138
- const hasTestScript = existsSync(pkgPath) && (() => { try { return !!JSON.parse(readFileSync(pkgPath, "utf8")).scripts?.test } catch { return false } })()
139
-
140
- if (args.full) {
141
- // Full mode: run the entire test suite
142
- 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") {
143
234
  lines.push("")
144
- lines.push("Tests (full suite):")
145
- const result = await runTestSuite(testCwd, ctx, args.filter)
146
- if (result.passed) {
147
- lines.push("✓ All tests passed.")
148
- 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
149
245
  } else {
150
- 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)
151
250
  ctx.agent._verifyPassed = false
152
251
  }
153
252
  } else {
253
+ // Unknown status value — defensive (schema enum normally rejects it first).
154
254
  lines.push("")
155
- lines.push("Tests: no test script in package.json — skipped.")
156
- ctx.agent._verifyPassed = !syntaxFailed
157
- }
158
- } else if (relatedTests.length > 0) {
159
- // Default mode: run only related test files
160
- lines.push("")
161
- lines.push(`Related tests (${relatedTests.length} file(s) for modules: ${modules.join(", ")}):`)
162
- let anyTestFailed = false
163
- for (const testFile of relatedTests) {
164
- const abs = join(cwd, testFile)
165
- if (!existsSync(abs)) {
166
- lines.push(` ? ${testFile} — file not found, skipping`)
167
- continue
168
- }
169
- try {
170
- const result = await runTestFile(cwd, testFile, ctx, args.filter)
171
- if (result.passed) {
172
- lines.push(` ✓ ${testFile}`)
173
- } else {
174
- anyTestFailed = true
175
- lines.push(` ✗ ${testFile} — FAILED`)
176
- lines.push(` ${result.tail.replace(/\n/g, "\n ")}`)
177
- }
178
- } catch (e) {
179
- anyTestFailed = true
180
- lines.push(` ✗ ${testFile} — error: ${e.message}`)
181
- }
182
- }
183
- if (anyTestFailed) {
184
- lines.push("")
185
- 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)
186
257
  ctx.agent._verifyPassed = false
187
- } else {
188
- lines.push(" All related tests passed.")
189
- ctx.agent._verifyPassed = !syntaxFailed
190
258
  }
191
- } else {
192
- // No related tests found for the changed modules
193
- const uncovered = modules.filter((m) => MODULE_TO_TEST[m] === null)
194
- const untested = modules.filter((m) => !(m in MODULE_TO_TEST))
195
- lines.push("")
196
- if (uncovered.length > 0) {
197
- lines.push(`Related tests: NONE for module(s) ${uncovered.join(", ")} — these modules have no dedicated test file.`)
198
- lines.push("ACTION REQUIRED: write a test that covers the change you just made.")
199
- lines.push("Do NOT proceed to 'done' — a test file is required before this change is complete.")
200
- } else if (untested.length > 0) {
201
- lines.push(`Related tests: NONE for module(s) ${untested.join(", ")} — unknown module, no test mapping exists.`)
202
- lines.push("ACTION REQUIRED: determine which test file covers this code and add it to MODULE_TO_TEST, or write a new test.")
203
- } else if (srcFiles.length === 0) {
204
- lines.push("Related tests: no source .mjs files changed — nothing to test.")
205
- ctx.agent._verifyPassed = !syntaxFailed
206
- } else {
207
- lines.push("Related tests: none matched. Run verify with full=true to run the full suite.")
208
- ctx.agent._verifyPassed = !syntaxFailed
209
- }
210
- }
211
-
212
- // Show full-suite hint when not run
213
- if (!args.full && hasTestScript) {
214
- lines.push("")
215
- lines.push("Note: full test suite not run. Use verify full=true to run ALL tests before committing.")
216
259
  }
217
260
 
218
261
  // 4. Task list
@@ -237,7 +280,7 @@ export const verifyTool = {
237
280
  // 5. Checklist
238
281
  lines.push("")
239
282
  lines.push("Self-review checklist:")
240
- 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?")
241
284
  lines.push("- [ ] Did I read every file I changed to catch leftover debug code or stale comments?")
242
285
  lines.push("- [ ] Do comments and docstrings match what the code actually does?")
243
286
  lines.push("- [ ] Did I remove placeholder code, TODO stubs, or commented-out experiment blocks?")
@@ -247,86 +290,3 @@ export const verifyTool = {
247
290
  return lines.join("\n")
248
291
  },
249
292
  }
250
-
251
- /**
252
- * Run a single test file with node --test, no maxBuffer limit.
253
- * Returns { passed: boolean, tail: string } — the last few lines of output.
254
- */
255
- function runTestFile(cwd, testPath, ctx, filter) {
256
- return new Promise((resolve, reject) => {
257
- const child = spawn("node", filter ? ["--test", "--test-name-pattern", filter, testPath] : ["--test", testPath], {
258
- cwd, stdio: ["ignore", "pipe", "pipe"],
259
- env: { ...process.env, FORCE_COLOR: "0" },
260
- })
261
- let stdout = ""
262
- let stderr = ""
263
- child.stdout.on("data", (d) => {
264
- const s = d.toString()
265
- stdout += s
266
- ctx.callbacks?.onToolOutput?.("verify", s)
267
- })
268
- child.stderr.on("data", (d) => {
269
- const s = d.toString()
270
- stderr += s
271
- ctx.callbacks?.onToolOutput?.("verify", s)
272
- })
273
- const timer = setTimeout(() => {
274
- child.kill("SIGKILL")
275
- reject(new Error(`Test ${testPath} timed out after 120s`))
276
- }, 120000)
277
- child.on("error", (e) => {
278
- clearTimeout(timer)
279
- reject(e)
280
- })
281
- child.on("close", (code) => {
282
- clearTimeout(timer)
283
- const output = (stdout + stderr).trim()
284
- const tail = output.split("\n").slice(-8).join("\n")
285
- resolve({ passed: code === 0, tail })
286
- })
287
- })
288
- }
289
-
290
- /**
291
- * Run npm test via spawn, no maxBuffer limit.
292
- * Test output is streamed through ctx.callbacks.onToolOutput (TUI can display progress in real time).
293
- * Returns { passed: boolean, tail: string }.
294
- */
295
- function runTestSuite(cwd, ctx, filter) {
296
- return new Promise((resolve, reject) => {
297
- const child = spawn("npm", filter ? ["test", "--", `--test-name-pattern=${filter}`] : ["test"], {
298
- cwd, shell: true, stdio: ["ignore", "pipe", "pipe"],
299
- env: { ...process.env, FORCE_COLOR: "0" },
300
- })
301
- let stdout = ""
302
- let stderr = ""
303
- child.stdout.on("data", (d) => {
304
- const s = d.toString()
305
- stdout += s
306
- ctx.callbacks?.onToolOutput?.("verify", s)
307
- })
308
- child.stderr.on("data", (d) => {
309
- const s = d.toString()
310
- stderr += s
311
- ctx.callbacks?.onToolOutput?.("verify", s)
312
- })
313
- const timer = setTimeout(() => {
314
- child.kill("SIGKILL")
315
- const err = new Error("Tests timed out after 120s")
316
- err.stdout = stdout
317
- err.stderr = stderr
318
- reject(err)
319
- }, 120000)
320
- child.on("error", (e) => {
321
- clearTimeout(timer)
322
- reject(e)
323
- })
324
- child.on("close", (code) => {
325
- clearTimeout(timer)
326
- const output = (stdout + stderr).trim()
327
- const tail = output.split("\n").slice(-8).join("\n")
328
- resolve({ passed: code === 0, tail })
329
- })
330
- })
331
- }
332
-
@@ -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"