dsh-jvm-doctor
离线、确定性的 JVM 生产报告诊断器 — 一个 DeepSeek Harness 插件,向模型注册 5 个工具:粘贴线程 dump / GC 日志 / 依赖树 / 异常栈 / Spring 配置文本,换回结构化诊断与诚实的解析覆盖率回执。
MIT License · DSH 0.2.0-rc.2 验证通过 · Node ≥ 20.10(运行)/ ≥ 22.6(测试)
永不接触运行中的进程 · 无
jcmd/jstack/jmap/jstat/ attach / JMX · 无网络 · 无文件读取 · 无时钟、无随机 · 每次调用都带字节回执
快速开始
dsh plugin --profile web add dsh-jvm-doctor
装完重启应用。想一次装齐四个插件(含 deterministic-tools / py-doctor / win-native)用 @ax2zicode/dsh-dev-toolbox;别和单独安装混用。
家族总览:zicode.com/software 是四个插件、合装包与桌面软件的统一入口。
15 秒看效果 —— 把异常栈贴进去,模型调用 java_stack_trace,拿回(实测输出):
root_cause: java.lang.IllegalStateException: inner
primary: java.lang.RuntimeException: boom
frames: com.acme.Main.main(Main.java:7) coverage: 100% (3/3, parser 1)
线程 dump 同理:java_thread_dump 给出 Java 级死锁环(waiting_for / holding 链),而不是让你在 500 个线程里自己找。
为什么不让模型自己写 pwsh? 死锁环、GC 停顿 p50/p90/p99、依赖冲突消解、... N more 折叠还原——写成脚本都是几十行,每次还得重新写对;工具是固定实现 + 47 个测试,并且只出结构化事实、不做因果推断。
相邻插件:dsh-deterministic-tools · dsh-py-doctor · dsh-win-native · dsh-dev-toolbox
为什么需要它
JVM 生产排查的证据几乎全部长这样:一段 jstack 输出、一坨 -Xlog:gc 日志、一棵 dependency:tree、一条 Caused by: 链、一份 application.yml。这些文本运维手里已经有了,但模型拿到之后只能:
- 自己起进程去采 —— 生产环境通常不允许:容器里没有
jcmd、权限与审计不让 attach、事故期间更不该再给 JVM 加负担; - 或者肉眼心算 —— 95 行 jstack 里哪几个线程互相持有锁、
Found 1 deadlock之外还有没有第二个环、omitted for conflict with到底谁赢了,全靠猜,而且不可复现。
dsh-jvm-doctor 只做一件事:把已到手的文本变成结构化诊断。它刻意不做采集 —— 没有 jcmd、没有 jstack、没有 attach、没有 JMX、没有网络、没有文件系统读取。每个工具都是纯函数:同样的输入永远给同样的输出。
顺带解决一个更隐蔽的问题:「解析失败」和「没有异常」长得一模一样。所有工具都返回 unknown_lines / unknown_samples / coverage / confidence,并且把预算导致的截断逐条报数,所以「没发现问题」和「没看懂输入」永远不会被混为一谈。
设计契约:每个结果都是回执
规范返回值固定为:
{
"result": { "…工具自己的载荷…": true },
"meta": {
"bytesIn": 5396,
"bytesOut": 7716,
"truncated": false,
"notes": []
}
}
模型看到的就是 result(对象给格式化 JSON),只有当发生截断或需要解释时才追加页脚:
[bounded] input=2307B output=5615B
[note] 7 frame(s) hidden behind `... N more` could not be resolved because the enclosing trace was truncated or absent
干净的调用不花额外 token;有问题的调用一定说清楚。
边界行为(五种,绝不混淆)
| 情况 | 行为 |
|---|---|
输入超 maxInputBytes |
抛错拒绝:报出实际字节、上限,并明确「没有任何截断或部分处理」 |
渲染文本超 maxOutputBytes |
按 code point 边界截断 + truncated: true + note(meta.bytesOut 仍报真实载荷大小) |
| 结构化列表超预算(线程数 / GC cause 数 / 冲突数 / 每条冲突的边数 / 异常数 / 每异常的帧数 / 差异条数) | 截断并逐条报数:threads_omitted、causes_omitted、conflicts_omitted、edges_omitted、exceptions_omitted、frames_omitted、changes_omitted |
| 不认识的语法 | 绝不静默丢弃:计入 unknown_lines,unknown_samples 里给出原文,并按覆盖率下调 confidence |
| 无法给出结论 | 返回诚实标记(confidence、findings、errors[]),而不是编一个诊断 |
安装
npm(推荐):
dsh plugin --profile web add dsh-jvm-doctor
预构建 tarball(与 npm 上是同一份产物,无需构建步骤,也避开 pnpm 的 allowBuilds 授权):
dsh plugin --profile web add https://github.com/ax2/dsh-jvm-doctor/releases/latest/download/dsh-jvm-doctor.tgz
要固定版本就把 latest/download/dsh-jvm-doctor.tgz 换成 download/v<version>/dsh-jvm-doctor-<version>.tgz。
本地开发:不安装,直接挂载构建产物。
pnpm install && pnpm build
dsh --profile web --patch ./cordis.local-overlay.yml --dump-config # 先验证层合成
dsh --profile web --patch ./cordis.local-overlay.yml # 再启动
配置
apply(ctx, config) 接受以下配置(全部可选,含默认值):
| 字段 | 默认 | 作用 |
|---|---|---|
maxInputBytes |
2097152 |
单份粘贴文本的硬上限(UTF-8 字节),超过即拒绝 |
maxOutputBytes |
262144 |
渲染文本的软上限,超过即按 code point 截断并标记 |
maxThreads |
200 |
java_thread_dump 序列化的线程条目上限 |
maxStatusLines |
40 |
java_thread_dump 汇总的状态行上限 |
maxContention |
20 |
java_thread_dump 报告的锁竞争队列上限 |
maxGcEvents |
500 |
java_gc_log 保留的 GC cause 种类上限 |
maxPauseSamples |
5000 |
计算百分位所用的停顿样本上限 |
maxConflicts |
100 |
java_dependency_conflicts 报告的冲突 artifact 上限 |
maxEdgesPerConflict |
20 |
每条冲突保留的边上限 |
maxExceptions |
25 |
java_stack_trace 保留的异常链条目上限 |
maxFramesPerException |
60 |
每个异常保留的帧上限 |
maxChanges |
500 |
spring_config_diff 报告的差异条数上限 |
maxKeys |
5000 |
每份配置文档展平后的键上限 |
sampleLimit |
5 |
unknown_samples 保留的原文行数上限 |
toolPrefix |
'' |
工具名前缀,例如 jvm → jvm__java_thread_dump |
only |
[] |
只注册指定工具(空 = 全部) |
非法值(0、负数、NaN、Infinity)静默回落到默认值,并向下取整 —— 一个坏配置无法把预算关掉。
工具清单(5 个)
| 工具 | 说明 |
|---|---|
java_thread_dump |
解析 jstack / jcmd <pid> Thread.print:线程普查、归一化与原文状态直方图、daemon 数、每线程 top 帧与锁、Java 级死锁环(含 waiting_for/holding 链)、Object.wait/park 计数、锁竞争队列 |
java_gc_log |
解析 JEP 158 统一日志与 JDK 9 之前的旧式 GC 行:事件/停顿计数、总/均/最大停顿、p50/p90/p99 百分位、Young/Full 拆分、堆前后区间、cause 直方图、promotion failure,并在有时间戳时估算分配速率 |
java_dependency_conflicts |
解析 Maven dependency:tree -Dverbose 与 Gradle dependencies / dependencyInsight:同一 artifact 的多版本、omitted for conflict with X、omitted for duplicate、每条边所在深度、逐 artifact 结论摘要 |
java_stack_trace |
解析异常链:类型与消息、class.method(file:line) 帧、Caused by: 链、Suppressed: 块、... N more 折叠按外层 trace 还原、最深的业务帧、最多 6 条可能原因提示 |
spring_config_diff |
把两份 application.yml / .properties 展平成点号键后逐键对比(add / remove / change);敏感值在进入载荷之前就替换为 *** |
java_thread_dump — 死锁环与竞争队列
输入(节选;真实示例是完整 jstack -l):
Full thread dump OpenJDK 64-Bit Server VM (21.0.4+7-LTS-103 mixed mode, sharing):
"deadlock-A" #40 ... nid=0x2b00 waiting for monitor entry
java.lang.Thread.State: BLOCKED (on object monitor)
at com.acme.order.Inventory.reserve(Inventory.java:61)
- waiting to lock <0x0000000712d00020> (a com.acme.order.Inventory)
- locked <0x0000000712c00010> (a com.acme.order.OrderDao)
...
Found one Java-level deadlock:
"deadlock-A":
waiting to lock monitor 0x... (object 0x0000000712d00020, a com.acme.order.Inventory),
which is held by "deadlock-B"
"Thead-B": ... which is held by "deadlock-A"
Found 1 deadlock.
输出(节选,… 表示省略):
{
"source": "jstack",
"jvm_version": "OpenJDK 64-Bit Server VM",
"captured_at": "2026-01-02 03:04:05",
"thread_count": 9,
"daemon_threads": 2,
"state_histogram": { "RUNNABLE": 1, "WAITING": 1, "BLOCKED": 5, "TIMED_WAITING": 2 },
"raw_state_histogram": {
"RUNNABLE": 1, "WAITING (parking)": 1, "BLOCKED (on object monitor)": 5,
"TIMED_WAITING (sleeping)": 1, "TIMED_WAITING (on object monitor)": 1
},
"deadlock_marker": true,
"reported_deadlock_count": 1,
"deadlock_cycles": [{
"threads": ["deadlock-A", "deadlock-B"],
"locks": ["0x0000000712d00020", "0x0000000712c00010"],
"chain": [
{ "thread": "deadlock-A", "waiting_for": "0x0000000712d00020", "holding": "0x0000000712c00010" },
{ "thread": "deadlock-B", "waiting_for": "0x0000000712c00010", "holding": "0x0000000712d00020" }
]
}],
"lock_contention_candidates": [{
"method": "com.acme.order.OrderDao.save",
"thread_count": 4,
"threads": ["worker-1", "worker-2", "worker-3", "deadlock-B"],
"states": ["BLOCKED"]
}],
"total_lines": 95, "parsed_lines": 95, "unknown_lines": 0,
"coverage": 1, "confidence": "high"
}
state_histogram 是归一化枚举(BLOCKED),raw_state_histogram 保留 JVM 原话(BLOCKED (on object monitor))—— 归一化便于统计,原文便于核对,两者都不丢。
明确拒绝:文本超 maxInputBytes 抛错;只认得 dump 语法的行,任何其它行都进 unknown_lines 并降 confidence,不会当成「线程正常」。
java_gc_log — 停顿统计与分配速率
[2026-01-02T03:04:05.678+0800][info][gc] Using G1
[2026-01-02T03:04:05.678+0800][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 128M->45M(512M) 12.345ms
[2026-01-02T03:04:07.100+0800][info][gc] GC(2) Pause Young (Concurrent Start) (Metadata GC Threshold) 480M->120M(512M) 30.500ms
[2026-01-02T03:04:08.100+0800][info][gc] GC(4) Pause Full (G1 Compaction Pause) 500M->90M(512M) 200.000ms
{
"gc_event_count": 6, "pause_count": 5, "non_gc_lines": 1,
"total_pause_ms": 268.095, "mean_pause_ms": 53.619, "max_pause_ms": 200,
"pause_percentiles": { "p50_ms": 20, "p90_ms": 132.2, "p99_ms": 193.22, "max_ms": 200 },
"young_pause_count": 4, "full_pause_count": 1, "concurrent_cycle_count": 1,
"heap_before_max_bytes": 524288000, "heap_after_min_bytes": 41943040, "heap_total_max_bytes": 536870912,
"unique_causes": 4, "causes_omitted": 0,
"cause_histogram": [
{ "reason": "Pause Young (Normal) (G1 Evacuation Pause)", "count": 3, "mean_pause_ms": 12.532, "max_pause_ms": 20 },
{ "reason": "Pause Full (G1 Compaction Pause)", "count": 1, "mean_pause_ms": 200, "max_pause_ms": 200 }
],
"estimated_allocation_rate": { "bytes_per_second": 31182870, "bytes_per_hour": 112258331344, "window_seconds": 2.522, "events_with_timestamp": 6 },
"findings": [
"1 pause(s) triggered by Metadata GC Threshold: class-loader or metaspace pressure is forcing collections",
"1 full-collection pause(s) present; check old-generation occupancy and humongous allocations",
"heap occupancy reached 97.7% before a collection: collections are running close to exhaustion"
],
"coverage": 1, "confidence": "high"
}
堆大小一律换算成字节(128M → 134217728),百分位用最近秩法在保留的样本上计算;样本被 maxPauseSamples 截断时 pause_percentiles_sampled: true 并附 note。同时认得旧式 [GC (Allocation Failure) [PSYoungGen: ...] 1234K->567K(2048K), 0.0123456 secs] 与 [Times: user=... real=... secs],并在 pause_source 里说明停顿秒数取自内联字段、尾随 secs] 还是 [Times: real=]。
java_dependency_conflicts — 谁被裁掉了
[INFO] com.acme:order-service:jar:1.0.0
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.7.18:compile
[INFO] | \- org.springframework:spring-web:jar:5.3.31:compile
[INFO] +- com.acme:widget-core:jar:1.2.3:compile
[INFO] | \- com.google.guava:guava:jar:31.1-jre:compile
[INFO] +- com.acme:widget-extras:jar:2.0.0:compile
[INFO] | \- com.google.guava:guava:jar:32.1.3-jre:compile (omitted for conflict with 31.1-jre)
{
"source": "maven", "edge_count": 9, "artifact_count": 8,
"omitted_edge_count": 2, "duplicate_markers": 1,
"conflicts": [{
"artifact": "com.google.guava:guava",
"kind": "version_conflict",
"resolved_versions": ["31.1-jre"],
"omitted_versions": ["32.1.3-jre"],
"depths": [1], "min_depth": 1, "max_depth": 1,
"edges_omitted": 0,
"summary": "com.google.guava:guava is requested at 31.1-jre; the build resolved 31.1-jre; 1 edge(s) pruned at depth 1..1"
}],
"coverage": 1, "confidence": "high"
}
Maven 的 +-/\- 与 Gradle 的 +---/\--- 各自成树,-> 记入 requested_version:请求版本与被解析版本分开存,因为「构建实际用哪个」和「谁想要哪个」是两个问题。treeDepthOf 让 Maven 与 Gradle 的深度定义一致(顶层直接依赖 = 深度 0)。
java_stack_trace — ... N more 不是黑盒
org.springframework.web.util.NestedServletException: Request processing failed; …
at com.acme.web.CatalogController.list(CatalogController.java:64)
Caused by: java.lang.NullPointerException: Cannot invoke "…Product.getSku()" because "product" is null
at com.acme.catalog.CatalogService.toSku(CatalogService.java:112)
... 15 more
{
"exception_count": 2,
"root_cause": "java.lang.NullPointerException",
"chain": [
{ "type": "org.springframework.web.util.NestedServletException", "relation": "primary", "frame_count": 3,
"deepest_application_frame": "com.acme.web.CatalogController.list",
"hints": ["NestedServletException surfaced through com.acme.web.CatalogController.list; …"] },
{ "type": "java.lang.NullPointerException", "relation": "caused_by", "frame_count": 16, "folded_frames": 15,
"folded_resolved": 3, "deepest_application_frame": "com.acme.catalog.CatalogService.toSku",
"hints": ["NullPointerException dereferences a null receiver or return value at com.acme.catalog.CatalogService.toSku; …"] }
],
"total_frames": 7, "folded_frames_resolved": 3, "folded_frames_unresolved": 12,
"exceptions_omitted": 0, "frames_omitted": 0,
"confidence": "medium"
}
折叠帧只从最近的外层异常里还原(Java 的语义如此),还原不出来就计入 folded_frames_unresolved 并降 confidence —— 不编造不存在的帧。application_frames、library_prefixes、test_failure 用来区分业务边界与框架管道。
spring_config_diff — 密钥不落地
{
"left_dialect": "yaml", "right_dialect": "yaml",
"left_keys": 8, "right_keys": 8,
"added": 1, "removed": 1, "changed": 4, "unchanged": 3,
"masked_values": 2,
"changes": [
{ "key": "spring.datasource.hikari.connection-timeout", "op": "add", "left": null, "right": "3000", "masked": false },
{ "key": "server.shutdown", "op": "remove", "left": "immediate", "right": null, "masked": false },
{ "key": "logging.level.root", "op": "change", "left": "INFO", "right": "WARN", "masked": false },
{ "key": "server.port", "op": "change", "left": "8080", "right": "9090", "masked": false },
{ "key": "spring.datasource.hikari.maximum-pool-size", "op": "change", "left": "10", "right": "32", "masked": false },
{ "key": "spring.datasource.url", "op": "change", "left": "jdbc:postgresql://db:5432/orders", "right": "jdbc:postgresql://db-primary:5432/orders", "masked": false }
],
"errors": [],
"findings": ["2 secret-looking value(s) were replaced with *** before comparison"]
}
键名命中 password|secret|token|key|credential 时,左右两侧的值在进入载荷之前就替换为 ***(masked: true 仍然说明这里有一处变化),所以密码既不会被比较、也不会被回显。.properties 的转义(\: \= \\ \ )与行内注释照常解析;解析不了的行进 errors[] 并带上行号,调用本身仍然是成功结果。
诚实性设计:coverage 与 confidence
所有解析器共用同一套记账(src/core/lines.ts):
total_lines按 CR/LF 归一化后计数,单个结尾换行不额外造一行;- 空行算结构、不算语法:把空行计入
unknown_lines会让覆盖率取决于报告排版多松散; coverage = (total - unknown) / total;confidence:coverage ≥ 0.98且没有可疑信号 →high;≥ 0.85→medium;否则low;- 「可疑信号」指文档解析成功但没有任何主体(0 个线程 / 0 条依赖边),这种情况永远拿不到
high; - 完全空的输入是
low,不是「100% 覆盖率」的假象; unknown_samples只保留前sampleLimit行原文,且永远不丢计数。
离线与安全边界
- 不采集:没有
jcmd/jstack/jmap/jstat/jinfo、没有 attach API、没有 JMX、没有 socket、没有子进程; - 不读盘:报告文本只能从工具入参进来;
- 不联网:没有任何出站请求,包括遥测;
- 不猜密码:
spring_config_diff在比较之前就把敏感值掩码; - 可复现:无时钟、无随机、无环境变量读取、无 locale 依赖;同一份文本永远给同一份结果(含
bytesIn/bytesOut)。
开发
pnpm install
pnpm typecheck # tsc --noEmit,0 错误
pnpm test # 47 个用例(5 个工具,用例数 ≥ 工具数)
pnpm build # 产出 lib/(ESM + .d.ts,导入已改写为 .js)
测试分三类:
- 解析器金值测试(
test/core.test.ts,27 例):内联真实的jstack/ 两种 GC 日志 / Maven 与 Gradle 依赖树 / 多层异常链 / YAML 与 properties fixture,断言精确的结构化数字(线程数、直方图、死锁环的waiting_for/holding、百分位、堆字节数、冲突深度),而不是「非空」; - 组装路径测试(
test/tools.test.ts,20 例):definition.execute(args, exec)→definition.output.render(),断言result/meta/ 页脚,以及每个工具的 schema 形状与「离线 + 拒绝」描述;only/toolPrefix过滤、resolveLimits非法值回落、maxOutputBytes截断路径; - 诚实性测试:故意塞入被截断/被人类改过的 fixture,断言覆盖率、
unknown_samples、confidence如实地变差(例如 100 行里 4 行不认识 →coverage 0.96、confidence medium),并断言每个预算截断都报出精确的丢弃条数(exceptions_omitted、causes_omitted、edges_omitted…)。
兼容性与风险
- 官方插件 API 自称 pre-stable,无冻结承诺;本插件只依赖最稳定的两个面:
ctx.tools.register(defineTool(...))与apply(ctx, config)。 - 验证到什么程度(不夸大):
tsc --noEmit0 错误;47 个用例在真实@deepseek-ai/dsh-tools上跑通组装路径;lib/产物由research/plugin-smoke.mjs(stub registry)加载并验证 5 个工具全部注册、schema 齐全、无重名、无 default export、dsh.bundle指向存在且 id 正确的 patch;Node 22 / 24 的 CI 矩阵;npm 上的版本用dsh plugin --profile <scratch> add装进一个临时 profile 后,真实dsh启动把本包 5 个工具 schema 发给了模型(会话日志核对)。真实对话也跑通了:同一个临时 profile 接上本机 desktop profile 用的那条 LLM 路由后,模型在真实回合里调用了java_stack_trace(3 行异常链),正确报出root_cause、帧位置与 100% 解析覆盖率(会话session-b5681b93)。尚未做:跨多个真实会话的调用统计(这属于 M3)。 - 语法是启发式的:不同 JDK 版本的 dump/GC 文案、各构建工具的输出变体都可能落在已知语法之外 —— 这正是
unknown_lines存在的理由;覆盖率下降就是「这份报告我没完全看懂」的信号,请以它为准。 - 不做因果推断:工具给的是结构化事实与明确的 finding(例如「97.7% 堆占用触发回收」),不是「你的服务就是这样挂的」。
- GC 分配速率是线性估算(首尾时间戳窗口内的正堆增量),不含分代细节,仅作量级参考。
Roadmap
- JDK 版本感知:为 JDK 8 / 11 / 17 / 21 的 dump 与 GC 文案分别固定 golden 语料,把版本差异变成测试而不是猜测。
- 更多报告:
jcmd GC.heap_info文本、JFR 摘要、线程 dump 的差集对比(两次 dump 之间谁新增/谁消失)。 - 建议引擎:把
findings从「事实陈述」升级为可核对的检查项(例如「持有锁超过 N 个采样」),并保持每个结论可回溯到具体行号。 output.presentCall/presentResult卡片化,让 Web UI 直接渲染线程表与死锁环图,而不是纯文本 JSON。
License
MIT © ax2
English
Offline, deterministic JVM production-report diagnostics for DeepSeek Harness — 5 model-visible tools that turn pasted jstack / GC logs / dependency trees / exception chains / Spring configuration into structured diagnostics with an honest parse-coverage receipt.
- Never touches a running process. No
jcmd,jstack,jmap,jstat, no attach API, no JMX, no sockets, no subprocesses, no filesystem reads. Every tool is a pure function of the text you paste: no clock, no randomness, no environment reads, so the same input always yields the same output. - Every call returns a receipt:
{ result, meta: { bytesIn, bytesOut, truncated, notes } }. Model-facing text is exactlyresult; a footer appears only when something was clipped or needs explaining. - Explicit failure, never silent degradation. Over-budget input is refused with both sizes and an assurance that nothing was partially processed. Rendered text is clipped at code-point boundaries and flagged. Every budget that drops list entries reports the exact count (
threads_omitted,causes_omitted,conflicts_omitted,edges_omitted,exceptions_omitted,frames_omitted,changes_omitted). - Unrecognized syntax is never dropped: it is counted in
unknown_lines, sampled verbatim inunknown_samples, and lowersconfidence. An entirely empty paste islowconfidence, not a fake 100% coverage. - Secrets stay out of the payload:
spring_config_diffreplaces values whose key matchespassword|secret|token|key|credentialwith***before comparison.
Tools:
| Tool | What it answers |
|---|---|
java_thread_dump |
Thread census, normalized and raw state histograms, daemon count, per-thread top frames and locks, Java-level deadlock cycles with the waiting_for/holding chain, Object.wait/park counts, lock-contention cohorts |
java_gc_log |
JEP 158 unified and legacy GC grammar: event/pause counts, total/mean/max pause, p50/p90/p99 percentiles, Young vs Full split, heap before/after ranges in bytes, cause histogram, promotion failures, allocation-rate estimate |
java_dependency_conflicts |
Maven dependency:tree -Dverbose and Gradle dependencies/dependencyInsight: same artifact at several versions, omitted for conflict with X, omitted for duplicate, per-edge depth, per-artifact summary |
java_stack_trace |
Exception chain: type and message, class.method(file:line) frames, Caused by: chain, Suppressed: blocks, ... N more folds resolved against the enclosing trace, deepest application frame, up to six likely-cause hints |
spring_config_diff |
Flatten two application.yml / .properties documents to dotted keys and classify every difference as add, remove or change — with sensitive values masked before they reach the payload |
- Install:
dsh plugin --profile web add dsh-jvm-doctor, or mount the built entry locally withdsh --profile web --patch ./cordis.local-overlay.yml. - Develop:
pnpm install && pnpm typecheck && pnpm test && pnpm build— 47 tests (parser golden values + assembled tool path + honesty/degradation cases) against the real@deepseek-ai/dsh-tools. - Verified against DSH 0.2.0-rc.2 on Node 22 and 24; only
ctx.tools.register(defineTool(...))andapply(ctx, config)are used, because the official plugin API is explicitly pre-stable. - Honest about grammar: dump and GC wording varies across JDK versions and build tools. Coverage below
1is the signal that part of the report was not understood — treatunknown_linesandconfidenceas part of the answer, not as noise.
No comments yet. Be the first to write one.