chenbhao commited on
Commit
5994b8c
·
1 Parent(s): 25861df

feat: /debug v2.0

Browse files

已将 VersperClaw 的 /debug 从被动日志阅读技能完全替换为 Openclaude /debug-mode 的主动插桩调试工作流。

改动文件

┌───────────────────────────────┬──────┬──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 文件 │ 操作 │ 说明 │
├───────────────────────────────┼──────┼──────────────────────────────────────────────────────────────────────────────────────────────────┤
│ src/tools/DebugSessionTool.ts │ 新建 │ 移植自 Openclaude,管理 .versperclaw-debug/ 目录(init/begin_run/begin_verify/read_log/cleanup) │
├───────────────────────────────┼──────┼──────────────────────────────────────────────────────────────────────────────────────────────────┤
│ src/tools.ts │ 修改 │ 导入并注册 DebugSessionTool 到工具列表 │
├───────────────────────────────┼──────┼──────────────────────────────────────────────────────────────────────────────────────────────────┤
│ src/skills/bundled/debug.ts │ 重写 │ 从被动日志分析替换为 8 步 CHECKPOINT 工作流 │
└───────────────────────────────┴──────┴──────────────────────────────────────────────────────────────────────────────────────────────────┘

新 /debug 工作流

1. Step 0 — 确认 bug 信息完整
2. Step 1 — 阅读代码,制定探针计划
3. Step 2 — 用 DebugSession 工具初始化调试目录
4. Step 3 — 在源码插入 DEBUG PROBE 探针(支持 JS/TS/Python/Go/Rust 等)
5. Step 4 — 复现 bug,收集日志(多 RUN 对比)
6. Step 5 — 基于日志证据定位根因
7. Step 6 — 修复并用 VERIFY run 验证
8. Step 7 — 自动清理所有探针代码

与之前对比

- 之前:只读 session debug log,被动分析
- 现在:插桩 + 复现 + 验证闭环,主动诊断

src/skills/bundled/debug.ts CHANGED
@@ -1,103 +1,204 @@
1
  import { open, stat } from 'fs/promises'
2
- import { VERSPER_CLAW_GUIDE_AGENT_TYPE } from 'src/tools/AgentTool/built-in/versperClawGuideAgent.js'
3
- import { getSettingsFilePathForSource } from 'src/utils/settings/settings.js'
4
- import { enableDebugLogging, getDebugLogPath } from '../../utils/debug.js'
5
- import { errorMessage, isENOENT } from '../../utils/errors.js'
6
- import { formatFileSize } from '../../utils/format.js'
 
 
 
7
  import { registerBundledSkill } from '../bundledSkills.js'
8
 
9
- const DEFAULT_DEBUG_LINES_READ = 20
10
- const TAIL_READ_BYTES = 64 * 1024
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  export function registerDebugSkill(): void {
13
  registerBundledSkill({
14
  name: 'debug',
15
  description:
16
- process.env.USER_TYPE === 'ant'
17
- ? 'Debug your current VersperClaw session by reading the session debug log. Includes all event logging'
18
- : 'Enable debug logging for this session and help diagnose issues',
19
- allowedTools: ['Read', 'Grep', 'Glob'],
20
- argumentHint: '[issue description]',
21
- // disableModelInvocation so that the user has to explicitly request it in
22
- // interactive mode and so the description does not take up context.
23
  disableModelInvocation: true,
24
  userInvocable: true,
25
  async getPromptForCommand(args) {
26
- // Non-ants don't write debug logs by default — turn logging on now so
27
- // subsequent activity in this session is captured.
28
- const wasAlreadyLogging = enableDebugLogging()
29
- const debugLogPath = getDebugLogPath()
30
-
31
- let logInfo: string
32
- try {
33
- // Tail the log without reading the whole thing - debug logs grow
34
- // unbounded in long sessions and reading them in full spikes RSS.
35
- const stats = await stat(debugLogPath)
36
- const readSize = Math.min(stats.size, TAIL_READ_BYTES)
37
- const startOffset = stats.size - readSize
38
- const fd = await open(debugLogPath, 'r')
39
- try {
40
- const { buffer, bytesRead } = await fd.read({
41
- buffer: Buffer.alloc(readSize),
42
- position: startOffset,
43
- })
44
- const tail = buffer
45
- .toString('utf-8', 0, bytesRead)
46
- .split('\n')
47
- .slice(-DEFAULT_DEBUG_LINES_READ)
48
- .join('\n')
49
- logInfo = `Log size: ${formatFileSize(stats.size)}\n\n### Last ${DEFAULT_DEBUG_LINES_READ} lines\n\n\`\`\`\n${tail}\n\`\`\``
50
- } finally {
51
- await fd.close()
52
- }
53
- } catch (e) {
54
- logInfo = isENOENT(e)
55
- ? 'No debug log exists yet — logging was just enabled.'
56
- : `Failed to read last ${DEFAULT_DEBUG_LINES_READ} lines of debug log: ${errorMessage(e)}`
57
- }
58
-
59
- const justEnabledSection = wasAlreadyLogging
60
- ? ''
61
- : `
62
- ## Debug Logging Just Enabled
63
-
64
- Debug logging was OFF for this session until now. Nothing prior to this /debug invocation was captured.
65
-
66
- Tell the user that debug logging is now active at \`${debugLogPath}\`, ask them to reproduce the issue, then re-read the log. If they can't reproduce, they can also restart with \`claude --debug\` to capture logs from startup.
67
- `
68
-
69
- const prompt = `# Debug Skill
70
-
71
- Help the user debug an issue they're encountering in this current VersperClaw session.
72
- ${justEnabledSection}
73
- ## Session Debug Log
74
-
75
- The debug log for the current session is at: \`${debugLogPath}\`
76
-
77
- ${logInfo}
78
-
79
- For additional context, grep for [ERROR] and [WARN] lines across the full file.
80
-
81
- ## Issue Description
82
-
83
- ${args || 'The user did not describe a specific issue. Read the debug log and summarize any errors, warnings, or notable issues.'}
84
-
85
- ## Settings
86
-
87
- Remember that settings are in:
88
- * user - ${getSettingsFilePathForSource('userSettings')}
89
- * project - ${getSettingsFilePathForSource('projectSettings')}
90
- * local - ${getSettingsFilePathForSource('localSettings')}
91
-
92
- ## Instructions
93
-
94
- 1. Review the user's issue description
95
- 2. The last ${DEFAULT_DEBUG_LINES_READ} lines show the debug file format. Look for [ERROR] and [WARN] entries, stack traces, and failure patterns across the file
96
- 3. Consider launching the ${VERSPER_CLAW_GUIDE_AGENT_TYPE} subagent to understand the relevant Claude Code features
97
- 4. Explain what you found in plain language
98
- 5. Suggest concrete fixes or next steps
99
- `
100
- return [{ type: 'text', text: prompt }]
101
  },
102
  })
103
  }
 
1
  import { open, stat } from 'fs/promises'
2
+ import { DEBUG_SESSION_TOOL_NAME } from 'src/tools/DebugSessionTool.js'
3
+ import { AGENT_TOOL_NAME } from 'src/tools/AgentTool/constants.js'
4
+ import { BASH_TOOL_NAME } from 'src/tools/BashTool/toolName.js'
5
+ import { FILE_EDIT_TOOL_NAME } from 'src/tools/FileEditTool/constants.js'
6
+ import { FILE_READ_TOOL_NAME } from 'src/tools/FileReadTool/prompt.js'
7
+ import { FILE_WRITE_TOOL_NAME } from 'src/tools/FileWriteTool/prompt.js'
8
+ import { GLOB_TOOL_NAME } from 'src/tools/GlobTool/prompt.js'
9
+ import { GREP_TOOL_NAME } from 'src/tools/GrepTool/prompt.js'
10
  import { registerBundledSkill } from '../bundledSkills.js'
11
 
12
+ const ALLOWED_TOOLS = [
13
+ FILE_READ_TOOL_NAME,
14
+ GLOB_TOOL_NAME,
15
+ GREP_TOOL_NAME,
16
+ FILE_EDIT_TOOL_NAME,
17
+ FILE_WRITE_TOOL_NAME,
18
+ BASH_TOOL_NAME,
19
+ AGENT_TOOL_NAME,
20
+ DEBUG_SESSION_TOOL_NAME,
21
+ ]
22
+
23
+ const DEBUG_SESSION_DIR = '.versperclaw-debug'
24
+
25
+ function buildDebugPrompt(args: string): string {
26
+ const issue = args.trim() || 'The user did not provide a bug description yet.'
27
+
28
+ return `# /debug - Runtime Debugging
29
+
30
+ You are in VersperClaw debug mode. Debug mode is evidence-driven: insert temporary probes, collect runtime data, identify the root cause from logs, fix, verify, and clean up.
31
+
32
+ Debug artifact directory: \`${DEBUG_SESSION_DIR}/\`
33
+ Runtime log file: \`${DEBUG_SESSION_DIR}/debug.log\`
34
+ Use the \`${DEBUG_SESSION_TOOL_NAME}\` tool to initialize the session, add run separators, read logs, add verify separators, and clean up the debug directory.
35
+
36
+ ## User Issue
37
+
38
+ ${issue}
39
+
40
+ ## Mandatory Checkpoints
41
+
42
+ Print each checkpoint marker exactly before moving past that step:
43
+
44
+ - \`CHECKPOINT 0: Context gathered\`
45
+ - \`CHECKPOINT 1: Probe plan created - N probes planned\`
46
+ - \`CHECKPOINT 2: Log collector initialized\`
47
+ - \`CHECKPOINT 3: N probes inserted into source files\`
48
+ - \`CHECKPOINT 4: N log entries collected\`
49
+ - \`CHECKPOINT 5: Root cause identified with log evidence\`
50
+ - \`CHECKPOINT 6: Fix applied and verified with probes\`
51
+ - \`CHECKPOINT 7: All probes removed, cleanup complete\`
52
+
53
+ Hard rules:
54
+
55
+ 1. Do not print CHECKPOINT 5 until checkpoints 1-4 are complete.
56
+ 2. Do not propose or apply a fix until you can cite specific collected log entries, except for asking the user for missing reproduction details.
57
+ 3. Probe insertion must use the Edit tool to physically add probe blocks to source files.
58
+ 4. Probe cleanup must use the Edit tool to remove every probe block.
59
+ 5. Before final response, grep for \`DEBUG PROBE\`; no probe code may remain.
60
+ 6. Use at most 3 instrumentation iterations. If still unresolved, summarize evidence and ask for the next reproduction clue.
61
+
62
+ ## Workflow
63
+
64
+ ### Step 0: Triage
65
+
66
+ If the user did not provide expected behavior, actual behavior, reproduction steps, consistency, and relevant errors, ask only for the missing details. If enough context is already present, continue.
67
+
68
+ After this, print:
69
+ \`CHECKPOINT 0: Context gathered\`
70
+
71
+ ### Step 1: Understand and Plan Probes
72
+
73
+ Read relevant code. Form 2-3 hypotheses. Plan minimal probes on critical paths.
74
+
75
+ Output a compact table:
76
+
77
+ | # | File:Line | Label | Variables | Hypothesis |
78
+ |---|-----------|-------|-----------|------------|
79
+
80
+ Choose logging strategy:
81
+
82
+ - CLI, server, worker, script: file-based logging to \`${DEBUG_SESSION_DIR}/debug.log\`.
83
+ - Browser or frontend-only: \`console.log\` probes with \`[DEBUG PROBE N]\`; ask the user to paste console output if it cannot be captured directly.
84
+
85
+ Then print:
86
+ \`CHECKPOINT 1: Probe plan created - N probes planned\`
87
+
88
+ ### Step 2: Initialize Debug Session
89
+
90
+ Call \`${DEBUG_SESSION_TOOL_NAME}\` with \`{"action":"init"}\` for file-based or hybrid logging.
91
+
92
+ Then print:
93
+ \`CHECKPOINT 2: Log collector initialized\`
94
+
95
+ ### Step 3: Insert Probes
96
+
97
+ Every probe block must include START and END markers:
98
+
99
+ \`\`\`ts
100
+ // DEBUG PROBE [1] label
101
+ try {
102
+ require('fs').appendFileSync('${DEBUG_SESSION_DIR}/debug.log', \`[\${new Date().toISOString()}] [js] file.ts:42 | label | value=\${JSON.stringify(value)}\\n\`)
103
+ } catch {}
104
+ // DEBUG PROBE END [1]
105
+ \`\`\`
106
+
107
+ Python probe example:
108
+
109
+ \`\`\`py
110
+ # DEBUG PROBE [1] label
111
+ try:
112
+ import datetime
113
+ open("${DEBUG_SESSION_DIR}/debug.log", "a").write(f"[{datetime.datetime.now().isoformat()}] [python] file.py:42 | label | value={value}\\n")
114
+ except Exception:
115
+ pass
116
+ # DEBUG PROBE END [1]
117
+ \`\`\`
118
+
119
+ Go probe example:
120
+
121
+ \`\`\`go
122
+ // DEBUG PROBE [1] label
123
+ import "os"
124
+ f, _ := os.OpenFile("${DEBUG_SESSION_DIR}/debug.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
125
+ if f != nil {
126
+ fmt.Fprintf(f, "[%s] [go] file.go:42 | label | value=%v\\n", time.Now().Format(time.RFC3339), value)
127
+ f.Close()
128
+ }
129
+ // DEBUG PROBE END [1]
130
+ \`\`\`
131
+
132
+ Rust probe example:
133
+
134
+ \`\`\`rust
135
+ // DEBUG PROBE [1] label
136
+ {
137
+ use std::fs::OpenOptions;
138
+ use std::io::Write;
139
+ if let Ok(mut f) = OpenOptions::new().create(true).append(true).open("${DEBUG_SESSION_DIR}/debug.log") {
140
+ writeln!(f, "[{}] [rust] file.rs:42 | label | value={:?}", chrono::Utc::now().to_rfc3339(), value).ok();
141
+ }
142
+ }
143
+ // DEBUG PROBE END [1]
144
+ \`\`\`
145
+
146
+ For other languages, adapt the probe to that language's normal append-to-file API. Keep the same \`DEBUG PROBE [N]\` / \`DEBUG PROBE END [N]\` markers, timestamp, file:line, label, and observed variables.
147
+
148
+ Probe code must observe only; it must not alter program behavior.
149
+ If a probe requires a temporary import, include, or helper function, wrap that temporary addition in its own \`DEBUG PROBE [N]\` / \`DEBUG PROBE END [N]\` markers too.
150
+
151
+ Then print:
152
+ \`CHECKPOINT 3: N probes inserted into source files\`
153
+
154
+ ### Step 4: Reproduce and Collect Logs
155
+
156
+ For file-based runs, call \`${DEBUG_SESSION_TOOL_NAME}\` with \`{"action":"begin_run","label":"short label"}\` before each reproduction command. Run the reproduction with Bash. For intermittent issues, collect 2-3 runs.
157
+
158
+ Read logs with \`${DEBUG_SESSION_TOOL_NAME}\` using \`{"action":"read_log","tailLines":200}\`.
159
+
160
+ Then print:
161
+ \`CHECKPOINT 4: N log entries collected\`
162
+
163
+ ### Step 5: Analyze Evidence
164
+
165
+ Analyze ordering, missing probes, unexpected values, duplicates, timing gaps, and before/after differences. Cite exact log lines.
166
+
167
+ Then print:
168
+ \`CHECKPOINT 5: Root cause identified with log evidence\`
169
+
170
+ ### Step 6: Fix and Verify
171
+
172
+ Apply the smallest targeted fix. Keep probes in place. Call \`${DEBUG_SESSION_TOOL_NAME}\` with \`{"action":"begin_verify","label":"fix verification"}\`, rerun reproduction, then read logs again.
173
+
174
+ Compare failing runs and VERIFY logs. The problematic value, ordering, or missing path must be corrected.
175
+
176
+ Then print:
177
+ \`CHECKPOINT 6: Fix applied and verified with probes\`
178
+
179
+ ### Step 7: Cleanup
180
+
181
+ Remove all probe blocks from source files using Edit. Search with Grep for \`DEBUG PROBE\` and continue cleanup until there are zero matches. Then call \`${DEBUG_SESSION_TOOL_NAME}\` with \`{"action":"cleanup"}\`.
182
+
183
+ Then print:
184
+ \`CHECKPOINT 7: All probes removed, cleanup complete\`
185
+
186
+ ## Final Response
187
+
188
+ Summarize the root cause, the log evidence, the fix, the verification result, and cleanup status.`
189
+ }
190
 
191
  export function registerDebugSkill(): void {
192
  registerBundledSkill({
193
  name: 'debug',
194
  description:
195
+ 'Runtime debug mode: insert temporary probes, collect logs, identify root cause, fix, verify, and clean up.',
196
+ allowedTools: ALLOWED_TOOLS,
197
+ argumentHint: '<bug description>',
 
 
 
 
198
  disableModelInvocation: true,
199
  userInvocable: true,
200
  async getPromptForCommand(args) {
201
+ return [{ type: 'text', text: buildDebugPrompt(args) }]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  },
203
  })
204
  }
src/tools.ts CHANGED
@@ -78,6 +78,7 @@ import { LSPTool } from './tools/LSPTool/LSPTool.js'
78
  import { ListMcpResourcesTool } from './tools/ListMcpResourcesTool/ListMcpResourcesTool.js'
79
  import { ReadMcpResourceTool } from './tools/ReadMcpResourceTool/ReadMcpResourceTool.js'
80
  import { ToolSearchTool } from './tools/ToolSearchTool/ToolSearchTool.js'
 
81
  import { FriendEmotionTool } from './tools/FriendEmotionTool.js'
82
  import { FriendScreenObserveTool } from './tools/FriendScreenObserveTool.js'
83
  import { EnterPlanModeTool } from './tools/EnterPlanModeTool/EnterPlanModeTool.js'
@@ -247,6 +248,8 @@ export function getAllBaseTools(): Tools {
247
  ...(getPowerShellTool() ? [getPowerShellTool()] : []),
248
  ...(SnipTool ? [SnipTool] : []),
249
  ...(process.env.NODE_ENV === 'test' ? [TestingPermissionTool] : []),
 
 
250
  // Friend VRM desktop pet tools — enabled when the plugin is active
251
  FriendEmotionTool,
252
  FriendScreenObserveTool,
 
78
  import { ListMcpResourcesTool } from './tools/ListMcpResourcesTool/ListMcpResourcesTool.js'
79
  import { ReadMcpResourceTool } from './tools/ReadMcpResourceTool/ReadMcpResourceTool.js'
80
  import { ToolSearchTool } from './tools/ToolSearchTool/ToolSearchTool.js'
81
+ import { DebugSessionTool } from './tools/DebugSessionTool.js'
82
  import { FriendEmotionTool } from './tools/FriendEmotionTool.js'
83
  import { FriendScreenObserveTool } from './tools/FriendScreenObserveTool.js'
84
  import { EnterPlanModeTool } from './tools/EnterPlanModeTool/EnterPlanModeTool.js'
 
248
  ...(getPowerShellTool() ? [getPowerShellTool()] : []),
249
  ...(SnipTool ? [SnipTool] : []),
250
  ...(process.env.NODE_ENV === 'test' ? [TestingPermissionTool] : []),
251
+ // Debug session tool — used by /debug for runtime probe debugging
252
+ DebugSessionTool,
253
  // Friend VRM desktop pet tools — enabled when the plugin is active
254
  FriendEmotionTool,
255
  FriendScreenObserveTool,
src/tools/DebugSessionTool.ts ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * DebugSessionTool — manage the .versperclaw-debug runtime debugging directory.
3
+ *
4
+ * Ported from Openclaude's DebugSessionTool.
5
+ * Used by /debug to manage log sessions, separators, tail-reading, and cleanup.
6
+ */
7
+ import { appendFile, mkdir, open, readFile, rm, stat, writeFile } from 'fs/promises'
8
+ import { join } from 'path'
9
+ import { z } from 'zod/v4'
10
+ import { buildTool, type ToolDef } from '../Tool.js'
11
+ import { getCwd } from '../utils/cwd.js'
12
+ import { lazySchema } from '../utils/lazySchema.js'
13
+ import { jsonStringify } from '../utils/slowOperations.js'
14
+
15
+ export const DEBUG_SESSION_TOOL_NAME = 'DebugSession'
16
+ const DEBUG_SESSION_DIR = '.versperclaw-debug'
17
+ const DEBUG_SESSION_LOG_FILE = 'debug.log'
18
+ const DEBUG_SESSION_STATE_FILE = 'state'
19
+
20
+ const DEFAULT_TAIL_LINES = 200
21
+ const MAX_TAIL_LINES = 1000
22
+ const TAIL_READ_BYTES = 512 * 1024
23
+
24
+ type DebugState = {
25
+ runCount: number
26
+ }
27
+
28
+ const inputSchema = lazySchema(() =>
29
+ z.strictObject({
30
+ action: z
31
+ .enum(['init', 'begin_run', 'begin_verify', 'read_log', 'cleanup'])
32
+ .describe('Debug session action to perform.'),
33
+ label: z
34
+ .string()
35
+ .max(120)
36
+ .optional()
37
+ .describe('Optional label for run or verification separators.'),
38
+ tailLines: z
39
+ .number()
40
+ .int()
41
+ .min(1)
42
+ .max(MAX_TAIL_LINES)
43
+ .optional()
44
+ .describe('Number of log lines to read for read_log. Default: 200.'),
45
+ }),
46
+ )
47
+ type InputSchema = ReturnType<typeof inputSchema>
48
+ type Input = z.infer<InputSchema>
49
+
50
+ const outputSchema = lazySchema(() =>
51
+ z.object({
52
+ success: z.boolean(),
53
+ action: z.enum(['init', 'begin_run', 'begin_verify', 'read_log', 'cleanup']),
54
+ debugDir: z.string(),
55
+ logFile: z.string(),
56
+ message: z.string(),
57
+ runNumber: z.number().optional(),
58
+ log: z.string().optional(),
59
+ }),
60
+ )
61
+ type OutputSchema = ReturnType<typeof outputSchema>
62
+
63
+ function getPaths() {
64
+ const cwd = getCwd()
65
+ const debugDir = join(cwd, DEBUG_SESSION_DIR)
66
+ return {
67
+ cwd,
68
+ debugDir,
69
+ logFile: join(debugDir, DEBUG_SESSION_LOG_FILE),
70
+ stateFile: join(debugDir, DEBUG_SESSION_STATE_FILE),
71
+ }
72
+ }
73
+
74
+ function formatLabel(label: string | undefined): string {
75
+ const trimmed = label?.trim()
76
+ return trimmed ? ` | ${trimmed.replace(/\s+/g, ' ')}` : ''
77
+ }
78
+
79
+ function hasErrorCode(error: unknown, code: string): boolean {
80
+ return (error as { code?: string } | null)?.code === code
81
+ }
82
+
83
+ function isENOENT(error: unknown): boolean {
84
+ return hasErrorCode(error, 'ENOENT')
85
+ }
86
+
87
+ async function ensureDebugDir(): Promise<ReturnType<typeof getPaths>> {
88
+ const paths = getPaths()
89
+ await mkdir(paths.debugDir, { recursive: true })
90
+ return paths
91
+ }
92
+
93
+ async function ensureStateFile(stateFile: string): Promise<void> {
94
+ const fh = await open(stateFile, 'a')
95
+ await fh.close()
96
+ }
97
+
98
+ function parseDebugState(content: string): DebugState {
99
+ const trimmed = content.trim()
100
+ if (!trimmed) return { runCount: 0 }
101
+
102
+ try {
103
+ const parsed = JSON.parse(trimmed) as { runCount?: unknown }
104
+ if (
105
+ typeof parsed.runCount === 'number' &&
106
+ Number.isInteger(parsed.runCount) &&
107
+ parsed.runCount >= 0
108
+ ) {
109
+ return { runCount: parsed.runCount }
110
+ }
111
+ } catch {
112
+ // Fall back to the legacy run_count=N format below.
113
+ }
114
+
115
+ const legacyMatch = trimmed.match(/run_count=(\d+)/)
116
+ if (legacyMatch) {
117
+ return { runCount: Number.parseInt(legacyMatch[1]!, 10) }
118
+ }
119
+
120
+ return { runCount: 0 }
121
+ }
122
+
123
+ async function readDebugState(stateFile: string): Promise<DebugState> {
124
+ try {
125
+ const content = await readFile(stateFile, 'utf8')
126
+ return parseDebugState(content)
127
+ } catch (error) {
128
+ if (isENOENT(error)) return { runCount: 0 }
129
+ throw error
130
+ }
131
+ }
132
+
133
+ async function writeDebugState(
134
+ stateFile: string,
135
+ state: DebugState,
136
+ ): Promise<void> {
137
+ await writeFile(stateFile, `${JSON.stringify(state, null, 2)}\n`, 'utf8')
138
+ }
139
+
140
+ async function readLogTail(logFile: string, tailLines: number): Promise<string> {
141
+ let stats
142
+ try {
143
+ stats = await stat(logFile)
144
+ } catch (error) {
145
+ if (isENOENT(error)) return ''
146
+ throw error
147
+ }
148
+
149
+ const readSize = Math.min(stats.size, TAIL_READ_BYTES)
150
+ if (readSize === 0) return ''
151
+
152
+ const fd = await open(logFile, 'r')
153
+ try {
154
+ const { buffer, bytesRead } = await fd.read({
155
+ buffer: Buffer.alloc(readSize),
156
+ position: stats.size - readSize,
157
+ })
158
+ return buffer
159
+ .toString('utf8', 0, bytesRead)
160
+ .split('\n')
161
+ .slice(-tailLines)
162
+ .join('\n')
163
+ } finally {
164
+ await fd.close()
165
+ }
166
+ }
167
+
168
+ export const DebugSessionTool = buildTool({
169
+ name: DEBUG_SESSION_TOOL_NAME,
170
+ searchHint: 'manage runtime debug log sessions',
171
+ maxResultSizeChars: 200_000,
172
+ strict: true,
173
+ async description() {
174
+ return 'Manage the fixed .versperclaw-debug runtime debugging directory, log separators, log reading, and cleanup.'
175
+ },
176
+ async prompt() {
177
+ return `Use ${DEBUG_SESSION_TOOL_NAME} only during /debug runtime debugging.
178
+
179
+ Actions:
180
+ - init: create .versperclaw-debug/, reset debug.log, and reset the run counter.
181
+ - begin_run: append a RUN #N separator before reproducing the bug.
182
+ - begin_verify: append a VERIFY separator before validating a fix.
183
+ - read_log: read the tail of debug.log.
184
+ - cleanup: delete .versperclaw-debug/ after all DEBUG PROBE blocks have been removed from source files.`
185
+ },
186
+ get inputSchema(): InputSchema {
187
+ return inputSchema()
188
+ },
189
+ get outputSchema(): OutputSchema {
190
+ return outputSchema()
191
+ },
192
+ isReadOnly(input: Input) {
193
+ return input.action === 'read_log'
194
+ },
195
+ toAutoClassifierInput(input: Input) {
196
+ return input.action
197
+ },
198
+ userFacingName() {
199
+ return 'DebugSession'
200
+ },
201
+ getActivityDescription(input) {
202
+ const action = input?.action ?? 'debug'
203
+ return `Managing debug session: ${action}`
204
+ },
205
+ renderToolUseMessage(input) {
206
+ return `Debug session ${input.action ?? 'operation'}`
207
+ },
208
+ async checkPermissions(input, context) {
209
+ if (input.action === 'read_log') {
210
+ return { behavior: 'allow' as const, updatedInput: input }
211
+ }
212
+ // For non-read actions, check if the tool is in the always-allow list
213
+ const rules = context.getAppState().toolPermissionContext.alwaysAllowRules.command
214
+ if (rules?.some(rule => rule === '*' || rule === DEBUG_SESSION_TOOL_NAME)) {
215
+ return { behavior: 'allow' as const, updatedInput: input }
216
+ }
217
+ return {
218
+ behavior: 'ask' as const,
219
+ message: `Run debug session action: ${input.action}`,
220
+ }
221
+ },
222
+ async call(input: Input) {
223
+ const tailLines = input.tailLines ?? DEFAULT_TAIL_LINES
224
+
225
+ if (input.action === 'cleanup') {
226
+ const paths = getPaths()
227
+ await rm(paths.debugDir, { recursive: true, force: true })
228
+ return {
229
+ data: {
230
+ success: true,
231
+ action: input.action,
232
+ debugDir: paths.debugDir,
233
+ logFile: paths.logFile,
234
+ message: `Removed ${DEBUG_SESSION_DIR}/`,
235
+ },
236
+ }
237
+ }
238
+
239
+ if (input.action === 'read_log') {
240
+ const paths = getPaths()
241
+ const log = await readLogTail(paths.logFile, tailLines)
242
+ return {
243
+ data: {
244
+ success: true,
245
+ action: input.action,
246
+ debugDir: paths.debugDir,
247
+ logFile: paths.logFile,
248
+ log,
249
+ message: log
250
+ ? `Read last ${tailLines} lines from ${DEBUG_SESSION_LOG_FILE}`
251
+ : `${DEBUG_SESSION_LOG_FILE} is empty or has not been created yet`,
252
+ },
253
+ }
254
+ }
255
+
256
+ const paths = await ensureDebugDir()
257
+
258
+ if (input.action === 'init') {
259
+ await writeFile(paths.logFile, '', 'utf8')
260
+ await writeDebugState(paths.stateFile, { runCount: 0 })
261
+ return {
262
+ data: {
263
+ success: true,
264
+ action: input.action,
265
+ debugDir: paths.debugDir,
266
+ logFile: paths.logFile,
267
+ message: `Initialized ${DEBUG_SESSION_DIR}/`,
268
+ },
269
+ }
270
+ }
271
+
272
+ if (input.action === 'begin_run') {
273
+ await ensureStateFile(paths.stateFile)
274
+ const state = await readDebugState(paths.stateFile)
275
+ const runNumber = state.runCount + 1
276
+ await writeDebugState(paths.stateFile, { runCount: runNumber })
277
+ const separator = `\n========== RUN #${runNumber}${formatLabel(input.label)} | ${new Date().toISOString()} ==========\n`
278
+ await appendFile(paths.logFile, separator, 'utf8')
279
+ return {
280
+ data: {
281
+ success: true,
282
+ action: input.action,
283
+ debugDir: paths.debugDir,
284
+ logFile: paths.logFile,
285
+ runNumber,
286
+ message: `Started RUN #${runNumber}`,
287
+ },
288
+ }
289
+ }
290
+
291
+ if (input.action === 'begin_verify') {
292
+ const separator = `\n========== VERIFY${formatLabel(input.label)} | ${new Date().toISOString()} ==========\n`
293
+ await appendFile(paths.logFile, separator, 'utf8')
294
+ return {
295
+ data: {
296
+ success: true,
297
+ action: input.action,
298
+ debugDir: paths.debugDir,
299
+ logFile: paths.logFile,
300
+ message: 'Started VERIFY run',
301
+ },
302
+ }
303
+ }
304
+
305
+ const _exhaustive: never = input.action
306
+ return _exhaustive
307
+ },
308
+ mapToolResultToToolResultBlockParam(output, toolUseID) {
309
+ return {
310
+ tool_use_id: toolUseID,
311
+ type: 'tool_result',
312
+ content: jsonStringify(output),
313
+ }
314
+ },
315
+ } satisfies ToolDef<InputSchema, OutputSchema>)