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 +190 -89
- src/tools.ts +3 -0
- src/tools/DebugSessionTool.ts +315 -0
|
@@ -1,103 +1,204 @@
|
|
| 1 |
import { open, stat } from 'fs/promises'
|
| 2 |
-
import {
|
| 3 |
-
import {
|
| 4 |
-
import {
|
| 5 |
-
import {
|
| 6 |
-
import {
|
|
|
|
|
|
|
|
|
|
| 7 |
import { registerBundledSkill } from '../bundledSkills.js'
|
| 8 |
|
| 9 |
-
const
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
export function registerDebugSkill(): void {
|
| 13 |
registerBundledSkill({
|
| 14 |
name: 'debug',
|
| 15 |
description:
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 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 |
-
|
| 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 |
}
|
|
@@ -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,
|
|
@@ -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>)
|