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.
- package/CHANGELOG.md +78 -2
- package/README.md +3 -3
- package/bin/thincoder.mjs +88 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +135 -26
- package/src/advisor/messages.mjs +57 -4
- package/src/advisor/run.mjs +119 -79
- package/src/advisor.mjs +34 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +182 -22
- package/src/agent/helpers.mjs +71 -4
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +107 -20
- package/src/agent/spawn-child.mjs +54 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +133 -109
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +154 -104
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +26 -30
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +284 -0
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +152 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +470 -0
- package/src/agent-tools/subagent-async.mjs +382 -0
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +343 -0
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +203 -377
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +198 -238
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +145 -242
- package/src/auto-think.mjs +14 -0
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +4 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +61 -8
- package/src/context.mjs +81 -163
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +30 -12
- package/src/escape.mjs +6 -4
- package/src/explore-distill.mjs +155 -0
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +11 -72
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +206 -87
- package/src/memory.mjs +3 -1
- package/src/model-specs.mjs +15 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +18 -39
- package/src/prompts/advisor-round1.md +20 -32
- package/src/prompts/advisor-round2.md +16 -16
- package/src/prompts/advisor-round3.md +16 -16
- package/src/prompts/coder.md +7 -28
- package/src/prompts/consult-base.md +4 -11
- package/src/prompts/discipline.md +31 -44
- package/src/prompts/eng-coder.md +9 -34
- package/src/prompts/engineering-sub.md +10 -8
- package/src/prompts/engineering.md +61 -264
- package/src/prompts/explore.md +4 -14
- package/src/prompts/main.md +18 -35
- package/src/prompts/methodology-template.md +32 -38
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +18 -35
- package/src/provider/core.mjs +62 -69
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +131 -44
- package/src/tools/edit-diff.mjs +348 -0
- package/src/tools/edit.md +20 -13
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +55 -24
- package/src/tools/file.mjs +25 -70
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -3
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +84 -13
- package/src/tools/question.md +5 -1
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +18 -25
- package/src/tools/system.mjs +50 -30
- package/src/tools/tree.md +2 -1
- package/src/tools/wait_for.md +22 -0
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +179 -27
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +77 -16
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +17 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +11 -11
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +31 -96
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +46 -0
- package/src/tui/pickers.mjs +51 -25
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +182 -0
- package/src/tui/startup.mjs +40 -0
- package/src/tui/subagent-blocks.mjs +272 -262
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +125 -12
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +127 -231
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +76 -0
- package/src/tui/wizard.mjs +48 -12
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
package/src/agent-tools/task.mjs
CHANGED
|
@@ -31,9 +31,10 @@ export const taskTool = {
|
|
|
31
31
|
description:
|
|
32
32
|
"Plan and track a task list for complex multi-step work. Each call replaces the entire list. " +
|
|
33
33
|
"Keep exactly one item in_progress at a time; mark items done as you complete them; never mark done if tests fail or work is partial. " +
|
|
34
|
-
"Statuses: pending | in_progress | done. " +
|
|
35
|
-
"IMPORTANT:
|
|
36
|
-
"
|
|
34
|
+
"Statuses: pending | in_progress | done — synonyms (completed/finished/complete, todo/open/waiting, active/running/working, …) are accepted and normalized with a warning. " +
|
|
35
|
+
"IMPORTANT: title is required and must be a non-empty string — items with empty titles are silently dropped. " +
|
|
36
|
+
"For cross-session / project-level tracking, use checklist. " +
|
|
37
|
+
"Returns the updated task list (or the new item's ID on add).",
|
|
37
38
|
parameters: {
|
|
38
39
|
type: "object",
|
|
39
40
|
properties: {
|
|
@@ -11,25 +11,30 @@ export const timerTool = {
|
|
|
11
11
|
"Set a timer before you start analyzing code. When the timer fires, " +
|
|
12
12
|
"a system reminder will be injected suggesting you try running code or " +
|
|
13
13
|
"adding debug logs. Use this to enforce a thinking budget: you get " +
|
|
14
|
-
"N seconds to reason, then the timer reminds you to act."
|
|
14
|
+
"N seconds to reason, then the timer reminds you to act. " +
|
|
15
|
+
"Returns the set confirmation — the reminder fires at the deadline.",
|
|
15
16
|
parameters: {
|
|
16
17
|
type: "object",
|
|
17
18
|
properties: {
|
|
18
19
|
seconds: {
|
|
19
20
|
type: "number",
|
|
20
|
-
description: "Thinking budget in seconds (default
|
|
21
|
+
description: "Thinking budget in seconds (default 180). Longer for complex reasoning, shorter for simple tasks.",
|
|
21
22
|
},
|
|
22
23
|
message: {
|
|
23
24
|
type: "string",
|
|
24
25
|
description: "Custom reminder message to show when time is up. Default: a suggestion to add debug logs or run the code.",
|
|
25
26
|
},
|
|
26
27
|
},
|
|
27
|
-
required: [
|
|
28
|
+
required: [],
|
|
28
29
|
},
|
|
29
30
|
readonly: true,
|
|
30
31
|
sideEffectExempt: true,
|
|
31
32
|
execute(args, ctx) {
|
|
32
|
-
|
|
33
|
+
// D15.3#1:默认 180;非法值(非数字/非正数)显式报错——静默 NaN 定时器永不触发的坑
|
|
34
|
+
const seconds = Number(args.seconds ?? 180)
|
|
35
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
36
|
+
throw new Error(`timer seconds must be a positive number, got: ${args.seconds}`)
|
|
37
|
+
}
|
|
33
38
|
const expiresAt = Date.now() + seconds * 1000
|
|
34
39
|
const message = args.message || `⏰ Time's up (${seconds}s). Have you tried running the code, adding a console.log, or checking the output? Thinking more without data is guessing.`
|
|
35
40
|
|
|
@@ -1,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,
|
|
3
|
-
import {
|
|
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
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
*
|
|
31
|
-
*
|
|
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
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return
|
|
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
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
//
|
|
69
|
-
//
|
|
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.
|
|
76
|
-
let
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
94
|
-
// the task list/self-review checklist add nothing either.
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
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 code — excluded 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
|
-
//
|
|
108
|
-
|
|
109
|
-
const
|
|
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("
|
|
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",
|
|
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(`
|
|
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.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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("
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
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("
|
|
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(
|
|
156
|
-
|
|
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
|
|
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
|
-
|
package/src/agent-tools.mjs
CHANGED
|
@@ -13,3 +13,4 @@ export { recentChangesTool } from "./agent-tools/recent-changes.mjs"
|
|
|
13
13
|
export { timerTool } from "./agent-tools/timer.mjs"
|
|
14
14
|
export { advisorTool } from "./agent-tools/advisor.mjs"
|
|
15
15
|
export { engTool } from "./agent-tools/eng.mjs"
|
|
16
|
+
export { readHistoryTool } from "./agent-tools/read-history.mjs"
|