Skip to content
dsh.fish
Bundle

dsh-tool-diagnose

A DeepSeek Harness bundle plugin exposing a diagnose tool over an extensible ctx.diagnostics check registry.

Source
xu-kai-quan
stars
2 stars
License
MIT
Updated
Updated 10 days ago

Readme

# dsh-tool-diagnose

[![npm](https://img.shields.io/npm/v/dsh-tool-diagnose.svg)](https://www.npmjs.com/package/dsh-tool-diagnose)
[![license](https://img.shields.io/npm/l/dsh-tool-diagnose.svg)](LICENSE)

A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) **Cordis plugin** (not a
Skill — see [Plugin vs Skill](#plugin-vs-skill)) that exposes a model-facing `diagnose` tool backed
by an extensible check registry, `ctx.diagnostics`. Where a symptom-table Skill tells the agent
which shell command to run and how to read its output, this plugin runs the introspection itself,
inside the process, against live Cordis state — and lets other plugins contribute more checks the
same way this package's own `checks.ts` does.

## Install

**If you use the DSH web UI:**

```sh
dsh plugin --profile web add dsh-tool-diagnose
```

**If you run one-off tasks from the terminal instead** (`dsh --profile headless "some task"`):

```sh
dsh plugin --profile headless add dsh-tool-diagnose
```

`web` and `headless` are the two names DSH ships out of the box, matching the two commands above —
whichever one you already use day to day, use that same word in place of `--profile web` /
`--profile headless`. You don't need to set anything up first; both initialize themselves
automatically the first time you use them.

<details>
<summary>Using a custom profile name instead of web/headless?</summary>

A "profile" is just a folder under `$DSH_HOME/profiles/<name>/` (`$DSH_HOME` defaults to
`~/.dsh`). If you've set up your own named profile, use that folder's name after `--profile`. See
what you already have:

```sh
ls "$DSH_HOME/profiles"          # macOS/Linux
dir $env:DSH_HOME\profiles       # PowerShell
```

</details>

Then, in a session: "Use the diagnose tool to check why the bash_run tool isn't showing up." See
[Status](#status) for the GitHub-source install alternative, and [Try it locally](#try-it-locally)
for hacking on a checkout without installing it into a profile.

## Layout

```
src/
  registry.ts    # ctx.diagnostics: DiagnosticRegistry Service — registerCheck/list/run
  checks.ts      # create*Check FACTORIES (2 always-on) + optional checks (4, service-gated)
  index.ts       # the plugin: mounts the registry, calls each factory, registers the `diagnose` tool
tests/
  registry.spec.ts              # registry mechanics + the closure-vs-passed-ctx regression test
  checks.spec.ts                # credential-format / approval-policy branches + the index.ts wiring regression test
  checks-integration.spec.ts    # all 6 checks against real Loader/ToolRuntime/SessionStore/etc.
cordis.patch.yml    # the bundle layer a profile applies when it lists this package
package.json        # declares dsh.bundle, pointing at cordis.patch.yml
```

## What the checks actually do

Every one reads a real, already-shipped service — no invented APIs. Two are always registered
(they only need `ctx.tools`, which this plugin already requires, or nothing beyond `ctx.diagnostics`
itself); four more register only when their service is actually mounted in this deployment (see
[Optional checks](#optional-checks-service-gated)).

- **`plugin-fiber-state`** reads `ctx.loader.entries()` — the same primitive
  `@deepseek-ai/dsh-host-plugin-inventory` projects to the web UI — and reports each matching
  entry's `enabled`/`disabled` state and fiber phase (`pending`/`loading`/`active`/`failed`/
  `unloading`/none). This is the live version of manually running `dsh --profile <name>
  --dump-config` and reading it by eye. (It's gated on `ctx.loader` like the other optional checks
  below, even though the description here groups it with the "always on" pair for token-cost
  purposes — see `index.ts`.)
- **`tool-visibility`** calls `ctx.tools.get(name)` to tell you whether a tool is registered and
  visible in the global scope, or absent (never registered, owning plugin failed to load, or
  scope-restricted away).

Call the tool with an optional `target` (check-specific: a plugin module-name substring, a tool
name, a credential-reference name, or a session id); omit it to get every check's unfiltered view.

### Optional checks (service-gated)

Each depends on a service `dsh-base` normally provides but an unusual deployment might not mount;
`index.ts` registers each one inside its own `ctx.inject([...])`, so a deployment missing one
service still gets every other check instead of the whole plugin failing to load.

- **`credential-resolution`** (needs `ctx.credentials`) — resolves a credential reference (e.g.
  `DEEPSEEK_API_KEY`) via `ctx.credentials.describe()`: configured/source/writable, **never the
  value itself**, per that seam's own doctrine.
- **`approval-policy`** (needs `ctx.approval`, `ctx.sessions`) — reports the deployment default
  approval policy (`"ask"`/`"never"`), or a specific session's override when given a session-id
  target.
- **`token-pressure`** (needs `ctx.tokenMeter`, `ctx.sessions`) — measures a live session's current
  `totalTokens` (request/response pressure) vs. `surfaceTokens` (fixed-heuristic total), useful for
  "why hasn't compaction run yet" symptoms.
- **`subagent-tree`** (needs `ctx.subagents`) — flattens a session's descendant subagent tree,
  reporting count, max depth, and any descendant the registry couldn't interpret
  (`corrupt`/`unavailable`).

## Extending it: a third-party check

Any other plugin can depend on this one and register more checks. **A check is a factory that
closes over its own already-scoped `ctx`** — construct it inside the `ctx.inject` callback for
whatever services it needs, not as a standalone object built ahead of time (see
[Audit notes](#audit-notes) for exactly why that distinction is load-bearing):

```ts
import type { Context } from '@deepseek-ai/cordis'

export const name = 'my-extra-diagnostics'

export function apply(ctx: Context) {
  ctx.inject(['diagnostics' /* , 'whateverServiceThisCheckNeeds' */], (ctx) => {
    ctx.diagnostics.registerCheck({
      id: 'my-check',
      description: 'What this check looks at and what target means for it.',
      run(target) {
        // read ctx (closed over above) here — never accept ctx as a run() parameter
        return []
      },
    })
  })
}
```

Disposing that plugin's fiber automatically unregisters its check — `registerCheck` returns the
disposer but you rarely need to call it directly; Cordis calls it for you on unload.

## Try it locally

For hacking on a checkout rather than installing the published package (see [Install](#install)
above for that):

```sh
pnpm dsh plugin --profile demo add /path/to/dsh-tool-diagnose
dsh --profile demo --dump-config   # confirm the "diagnose-tool" row is present
dsh --profile demo
```

On Windows, if you instead point a `--patch` overlay directly at `src/index.ts` (the
tutorial-style local-file pattern) for a quick source-launch test, the module `name` must be a
`file:///` URL, not a bare `D:/...` path — Node's ESM loader rejects the latter with
`ERR_UNSUPPORTED_ESM_URL_SCHEME`.

## Plugin vs Skill

If you want a portable, no-build, copy-into-`~/.agents/skills/` symptom table any DSH session can
read instead, that's a Skill, not a plugin — see the original share in
[Discussion #1739](https://github.com/deepseek-ai/deepseek-harness/discussions/1739), which is what
this plugin took its methodology cues from (especially version-anchoring knowledge to a specific
dsh release). The two compose well: a Skill can tell the agent "call the `diagnose` tool first,
then read its findings against this table" instead of asking it to run shell commands and parse
the output itself.

## Status

- [x] Published on GitHub, public, tagged with the [`dsh-plugin`](https://github.com/topics/dsh-plugin) topic.
- [x] `LICENSE` filled in with a real copyright holder.
- [x] Published on npm as [`dsh-tool-diagnose`](https://www.npmjs.com/package/dsh-tool-diagnose) —
      see [Install](#install) above. Installing straight from GitHub source instead
      (`dsh plugin --profile <name> add github:xu-kai-quan/dsh-tool-diagnose`, see the build-script
      caveat below) still works too, and tracks `master` rather than the last tagged npm release.

If you fork or extend this:

1. Confirm dependency versions against what you actually build/test against — this skeleton pins
   the versions available at scaffold time: `@deepseek-ai/cordis@4.0.1`,
   `@deepseek-ai/cordis-plugin-loader@1.0.2`, `@deepseek-ai/schemastery@3.18.1`, and
   `@deepseek-ai/dsh-tools`/`dsh-credentials`/`dsh-user-approval`/`dsh-token-meter`/`dsh-session`/
   `dsh-subagent` all at `0.1.0-rc.5` (the dsh family is versioned in lockstep).
2. Ship prebuilt output (`npm publish`, or `pnpm pack` + tarball) rather than relying on installers
   allowing a git-install `prepare` script, unless you want users to opt into that.
3. `npm install && npm test && npm run build` before tagging a release — and, per the audit notes
   below, actually boot it inside a real `dsh` process at least once. This package's own bugs were
   invisible to its test suite until that step.
4. Write real checks — the six shipped here are a useful starting set, not the ceiling; port
   whatever mechanism chains you'd otherwise write as Skill symptom rows into `DiagnosticCheck`s
   that actually read the live state instead of describing how to. Grep the target package's
   README for its `ctx.<key>` service surface first, the way this package's checks were built.

## Audit notes

### Two real bugs a full test suite (22 passing tests) did not catch

Both were found only by actually running this exact plugin inside a real `dsh --profile headless`
process and asking the agent to call the `diagnose` tool for real — every unit and integration
test up to that point passed cleanly while the tool was either unregistered or broken.

1. **Wrong `output.schema` DSL for `defineTool`.** `dsh-tools`' `ValueSchemaSpec` marks a property
   optional/required with `required: true` **on the property itself**, mirroring `parameters` —
   NOT a sibling plain-JSON-Schema `required: [...]` array on the object node. The wrong form
   compiled fine under `tsc` and looked identical to the (correct) `parameters` block a few lines
   above it, but threw `JsonSchemaError: schema.items.required is not supported by the value
   schema DSL` the instant `defineTool()` ran — which happens at plugin-load time, so the entire
   `diagnose` tool silently failed to register with no crash and no log line, in every profile.
2. **A ctx reference does not survive being stored and handed back through another service's
   `this.ctx`.** The original registry design captured `ctx` (or `this.ctx`) once and passed it to
   each check's `run(ctx, target)` later. Verified against the real `@deepseek-ai/cordis` 4.0.1
   runtime with isolated repro scripts: a scope's injected-service access (`ctx.approval`,
   `ctx.tools`, even a **statically** `inject`-declared dependency) is granted per exact context
   reference and does *not* propagate when that reference is stored as a plain value inside
   another `Service` instance and read back via `this.ctx` in a later method call — it throws
   `cannot get property "X" without inject`, regardless of whether the original grant was static
   or from a nested `ctx.inject()`. The fix: `DiagnosticCheck.run` takes no `ctx` parameter at
   all; every check is a **factory** (`create*Check(ctx)`) that closes over its scope's `ctx`
   directly at construction time, and the registry only ever calls `check.run(target)`.

`tests/registry.spec.ts` and `tests/checks.spec.ts` both now carry regression tests for bug #2
(a check registered from a deeper scope than `run()` is later called from); nothing in the suite
directly regression-tests bug #1 (a wrong-but-type-checking `output.schema` shape) — if you touch
that schema, boot the plugin for real again rather than trusting `npm test` alone.

### What was verified this way, end-to-end, with a live model call

Against a real `dsh --profile headless` boot (no mocks, no fakes, real npm-installed
dependencies): the `diagnose` tool appears in the model's tool list; called with no target, it
returns real findings from `plugin-fiber-state` (4 real disabled-plugin rows: `cordis-plugin-hmr`,
`dsh-bash-sandbox`, `dsh-tool-bash`, `dsh-skill-badge`) and `approval-policy` (`"ask"`, the real
deployment default); called with `target: 'DEEPSEEK_API_KEY'`, `credential-resolution` correctly
reported it configured (source: file, writable) **without leaking the value**; called with
`target: 'write'`, `tool-visibility` correctly reported it registered and visible. `credentials`,
`approval`, `sessions`, `tokenMeter`, and `subagents` all turned out to be mounted in the
`headless` profile — only `loader` gating was ever a real portability concern, and it's now
handled the same way as the other four.

### Per-check unit-test technique, in case you extend these further

- `plugin-fiber-state`: `ctx.plugin(Loader)` + `ctx.loader.builtins.<name> = fn` +
  `ctx.loader.create({ name })` — the same harness `dsh-host-plugin-inventory`'s own test suite
  uses, avoiding any real file/module resolution.
- `tool-visibility`: direct `new ToolRuntime(ctx)` — but its constructor reads `ctx.systemPrompt`
  synchronously, so `await ctx.plugin(SystemPrompt, {})` has to happen first, or construction
  throws `Cannot read properties of undefined (reading 'tools')`. `dsh-tools`' own
  `code-mode.spec.ts` has the same prerequisite.
- `token-pressure` / `approval-policy`: direct `new SessionStore(ctx)` + `new TokenMeter(ctx)` /
  `new ApprovalService(ctx, config)`, then `ctx.sessions.create()` for a live session — no
  persistence or agent loop needed since `measure()` and `overrideOf()` take the session object
  directly rather than reading it back through a service.
- `subagent-tree`: direct `new SessionStore(ctx)` + `new SessionProjectionRegistry(ctx)` +
  `new SubagentRuntime(ctx)`. `listDescendants()` throws `SubagentError` before any candidate
  scan if `sessionProjections` isn't mounted — even for a session with zero children — so that
  service is not skippable even for the trivial empty-tree case tested here.

### A real dependency-declaration bug, separately caught and fixed

`@deepseek-ai/dsh-credentials` was briefly marked `optional` in `peerDependenciesMeta`, but
`checks.ts` does a hard value-import of `credentialRef` from it — so the module import would fail
at load time regardless of whether the credential check is ever registered, contradicting
"optional". `dsh-user-approval`, `dsh-token-meter`, and `dsh-subagent` are genuinely optional:
they're referenced only via `import type {}` side-effect imports, which TypeScript erases, leaving
no runtime import to fail if the package is absent.

### Still not covered

The non-trivial branches of `subagent-tree` (an actual descendant, or a `corrupt`/`unavailable`
diagnostic entry) aren't tested — reaching them needs a real subagent provider and a started child
(see `packages/subagent/subagent/tests/list-children.spec.ts` in the harness repo for the full
weight of that harness: JSONL persistence, a mock LLM adapter, a real spawn/fork provider). The
trivial empty-tree path is covered, and was independently confirmed by the live boot test above.

Install

dsh plugin --profile web add github:xu-kai-quan/dsh-tool-diagnose

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source