DeepSeek Harness Plugin

hyzyn/dsh-plugin-kit#codegraph

Stars ★ 48 Downloads (30d) 5,421 Category Tools & Capabilities Added 2026-08-30 npm @hyzyn/dsh-codegraph

Code-graph integration for the DSH Web GUI: a "Codegraph" card in Settings → Plugins with index status, symbol search, callers / callees / impact views, and one-click sync / index; hosts the codegraph MCP server and injects a CodeGraph usage guideline into the system prompt. Requires the codegraph CLI.

Install

# from npm (prebuilt)

dsh plugin --profile web add @hyzyn/dsh-codegraph

# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)

dsh plugin --profile web add github:hyzyn/dsh-plugin-kit#path:/packages/codegraph

Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).

README

中文 | English

The “Codegraph” card under DSH Settings → Plugins: it brings code-graph status, symbol search, and call chains into the GUI, and aligns the project directory of the codegraph MCP server for you.

Features

  • status --json fields laid out per section: initialized / version / projectPath / fileCount / nodeCount / edgeCount / lastIndexed / pendingChanges / languages / dbSizeBytes render as cells with the raw JSON behind a <details>; a symbol drill-down returns its line-numbered verbatim source plus callers / callees / impact lists. The CLI's staleness signals (reindexRecommended / builtWithVersion / extractor version gap / worktreeMismatch) surface as a warning line with "rebuild index" flagged as the suggested action.
  • Two index paths, two timeout buckets: sync -- <path> incremental and index [--force] -- <path> full rebuild run on indexTimeoutMs (600 s, 0 = unlimited); status / query / callers / callees / impact / node run on cliTimeoutMs (60 s, 0 = unlimited). Timeout and cancellation both follow a "SIGTERM → 3 s grace → SIGKILL" process-group escalation on every platform; index-class operations get a Cancel button on the card, and closing the page aborts the CLI too.
  • Managed MCP server row: dsh-mcp-client declares no MCP roots, so codegraph serve --mcp resolves .codegraph/ upward from process.cwd() only; the plugin maintains the @deepseek-ai/dsh-mcp-client row in ~/.dsh/cordis.patch.yml with config.cwd, and the rewrite hot-loads through watchUserPatches to rebuild the MCP connection. One server mounts one project at a time; other projects are queried with projectPath.
  • Index detection reads .codegraph/*.db and walks upward like the CLI: from the target directory up (stopping at the git root) it looks for the first .codegraph/ holding an index database; the hit root is the project root, so sessions in monorepo subdirectories are no longer misread as "unindexed". Directory existence alone mistakes the CLI's own install dir ~/.codegraph for a project index, dropping the managed row on an unindexed cwd — measured there, codegraph_explore's required grows from ["query"] to ["query","projectPath"]. Anything short of a real index leaves the existing cwd untouched, and the card reports indexState plus the reason.
  • Write discipline: every write to ~/.dsh/cordis.patch.yml is re-verified against an mtime+size stamp (the same CAS as dsh-mcp, at most 3 re-reads); when reusing the dsh-mcp block only the codegraph row's own cwd: line is changed in place, leaving the block's comments, its other server rows and the loader's legitimate override entries byte-identical; if the block is corrupt it refuses to append a second one (a serverName collision would break codegraph MCP loading wholesale) and reports the reason on the card.
  • The managed row's cwd follows the active session: followSession (on by default) aligns the row when a session switches to a project with a valid index, otherwise falls back to the pinned path; "Set as default project" writes that path into the codegraph settings namespace and sets followSession false (an explicit pin beats session following). A failed session report retries with backoff instead of being silently lost forever.
  • Optional per-agent MCP isolation (mcpScope: 'per-agent', default 'managed'): in the default mode one MCP server time-shares across all projects (via projectPath and session following). With per-agent enabled, each agent mounts its own dsh-mcp-client inside its own Cordis scope, with cwd pinned to the index root of that agent's session directory — parallel multi-project work no longer shares a single global cwd, and the whole "write patch → hot reload → rebuild connection" chain disappears; the global managed row is suspended (disabled: true, restored automatically when you switch back to managed). The cost is one child process per agent (measured ~40MB idle Node baseline). An agent whose session directory has no usable index is not mounted at all (rather than falling back to the default project) — otherwise it would inherit another project's context, the exact class of error that gets claimed-but-not-implemented. Mechanism measurements and tradeoffs: docs/p0-plan.md.
  • One-click initialisation: an uninitialised directory gets an "Initialise index" button on the card that runs codegraph init (two-step confirm) — index / sync both require the project to be initialised first, and this was the last step that still forced users back to a terminal.
  • Two systemPrompt sections, switches and gating independent: plugin:dsh-codegraph (order 150) and plugin:dsh-codegraph:usage (order 151), both behind a <command> --version probe; announceToAgent / usageGuidance add or remove the sections live through the settings namespace.

Why the MCP working directory is managed

DSH's MCP client does not declare the MCP roots capability, so once codegraph serve --mcp starts it can only search upward for .codegraph/ from its process working directory. And dsh web is often started from the home directory — in that case model calls to mcp__codegraph__* always return:

No CodeGraph project is loaded for this session.
Searched for a .codegraph/ directory starting from: /Users/you

This plugin solves that: it manages one codegraph MCP server row in ~/.dsh/cordis.patch.yml (codegraph serve --mcp, cwd = the default project path). When the default project path changes (the card's “Set as default project” or a config edit), that row is rewritten, and DSH's watchUserPatches hot reload mounts the MCP server onto the new project automatically.

Behavior details:

  • It prefers reusing an existing codegraph row in the @hyzyn/dsh-mcp managed block (only the cwd is filled in; other fields, including the disabled state, are left untouched); only when there is none does it write this plugin's own block, avoiding a serverName collision.
  • Manual rows outside the block are only detected, never touched (to avoid conflicts).
  • A target path is only managed when it holds a real index: a .db index file must exist inside .codegraph/. A bare .codegraph/ directory is not enough — the codegraph CLI keeps its own install data in ~/.codegraph (current -> versions/<v>, bundles/, codegraph.lock, no index database), so the home directory used to be mistaken for an indexed project: the plugin pinned the MCP cwd to it and reported everything as fine, while codegraph status --json -- ~ actually returns initialized:false and the tools keep answering "No CodeGraph project is loaded". When that happens the plugin leaves the existing cwd alone, creates no row, and the card shows "default project X is not a valid index" plus the one-click fix.
  • Multiple projects: one codegraph MCP server mounts one default project at a time; other indexed projects can be queried by passing projectPath on the tool call, or by switching with one click in the card.
  • Follow semantics: the managed cwd is the session directory when following is on and that directory holds a valid index, otherwise the pinned path. A valid index means an index database inside .codegraph/ (not merely the directory existing), so the CLI's own ~/.codegraph install dir cannot drag the cwd off; after codegraph uninit the cwd falls back automatically instead of going stale.
  • Following is reported by the browser half (it subscribes to the active session at page load, whether or not the settings panel is open): the host has no "current session" signal of its own, so following works while a GUI page is open and uses the pinned path otherwise.
  • How to turn it off: the plugin config mcpIntegration: false (this reverts the managed row written by this plugin).

CLI surface

The card used to cover only status/query/callers/callees/impact/node/sync/index/init; every other subcommand meant going to a terminal. The flagship and everyday ones are now covered too:

  • explore: the same output as the MCP codegraph_explore tool (relevant symbol source plus call paths). The model reached this through MCP while a human on the card could not get at the same capability — a bit absurd, and now fixed.
  • context: assembles context for a task (as opposed to explore, which targets an area).
  • files: the file structure inside the index (language / symbol count / size).
  • affected: maps changed files back to the tests they affect (measured in this repo: touching src/index.ts → 5 test files). Useful precisely for "I changed this, which tests should I run". The plugin deliberately does not guess the change list for you (it does not read git status and does not guess editor state): where the list of files to test comes from is an upstream concern, and the card only hands the list you give it to the CLI.
  • uninit: deletes .codegraph/ — the inverse of init, and the card's second destructive action, likewise behind a two-step confirm. Measured: -f is mandatory — without it the CLI asks Continue? (y/N), and the runner has no TTY, so it reads EOF and aborts without deleting (safe but useless); the card therefore owns the confirmation.
  • telemetry: a read-only relay of the upstream anonymous-usage telemetry status. Deliberately no toggle — that is the user's global preference (~/.codegraph/telemetry.json, owned by the CLI's own telemetry on|off), and flipping it for them would mean overreaching into someone else's global setting.
  • Query-parameter panel: query -k/--kind (a kind filter) and callers|callees -l/--limit (the CLI defaults to 20, which truncates large symbols and the card had no way to know).

kind is free text rather than a dropdown enumeration: when upstream adds a new kind the plugin does not have to change along with it, and a wrong value simply gets an empty result back from the CLI.

Project list (one-click switch)

One codegraph MCP server mounts one project at a time, so switching projects is a frequent action — and the card used to require typing an absolute path by hand. GET /projects plus a row of pill buttons on the card solves it.

Candidates use in-scope data sources only: the active sessions' header.cwd from the host's sessions.list(), plus paths the plugin observed itself (/follow reports, directories whose /status was queried, the current default project). It deliberately does not read ~/.dsh/sessions/ — that does hold historical project paths, but in DSH's internal storage format (session bucket names are path-encoded, logs are multi-frame zstd), so parsing it would silently break the day DSH changes that format; measured, decoding 51 directories from bucket names got not a single one right. Better a few projects short of the user's memory than a list that is right only sometimes.

Every candidate recomputes locateIndex live: only a real index qualifies as a switch target; unindexed ones are still listed but disabled (a user wants to know "this project is not indexed yet") and clicking them does nothing. The list is sorted by most recently seen, capped at 50 entries, evicting the least recently seen. Switching reuses the "Set as default project" chain (persist defaultPath → turn following off → hot-switch the MCP).

API

Route Method Description
/api/dsh-codegraph/status?path= GET Index status (JSON)
/api/dsh-codegraph/query?q=&path=&limit= GET Search symbols
/api/dsh-codegraph/callers?symbol=&path= GET Look up callers
/api/dsh-codegraph/callees?symbol=&path= GET Look up callees
/api/dsh-codegraph/impact?symbol=&path=&depth= GET Look up impact
/api/dsh-codegraph/node?name=&path= GET Look up symbol / file details
/api/dsh-codegraph/sync POST Incremental sync { path }
/api/dsh-codegraph/index POST Full rebuild { path } (requires the project to be already initialised, see below)
/api/dsh-codegraph/init POST Runs codegraph init in a not yet initialised directory (creates .codegraph/ plus the initial index) and then recomputes the managed MCP row; returns 409 for an already-indexed directory
/api/dsh-codegraph/default-path GET Pinned defaultPath + effectivePath + sessionPath + followSession + prompt toggles + cliAvailable / cliProbeError / cliProbeAt + MCP managed state (indexState describes the effective path)
/api/dsh-codegraph/follow POST Report the active session directory { path } (empty = no active session); the host aligns the managed cwd and falls back for unindexed directories
/api/dsh-codegraph/settings POST Write toggles { announceToAgent?, usageGuidance?, mcpIntegration?, followSession? } (booleans) and { mcpScope? } ('managed' / 'per-agent', 400 otherwise), effective immediately
/api/dsh-codegraph/agents GET Per-agent MCP mount ledger { mode, requested, effective, reason, fallback, mounted, live, agents } — which agents mounted, on which index root, and why not (read-only)
/api/dsh-codegraph/default-path POST Set as default project { path } (requires an index database inside .codegraph/), hot-switches the MCP at the same time
/api/dsh-codegraph/reprobe POST Re-runs the <command> --version probe, returns { cliAvailable, cliProbeError, cliProbeAt } and refreshes the systemPrompt gate
/api/dsh-codegraph/files GET File structure { path, files, raw } (codegraph files --json); knobs filter / pattern / maxDepth
/api/dsh-codegraph/affected GET Affected tests { path, affected, raw } (codegraph affected --json -- <files…>); files may repeat
/api/dsh-codegraph/explore GET Exploration { path, query, output } (codegraph explore, same output as the MCP codegraph_explore tool, markdown); knob maxFiles
/api/dsh-codegraph/context GET Task context { path, task, output } (codegraph context, markdown); knob maxNodes
/api/dsh-codegraph/uninit POST Deletes .codegraph/ { path } (codegraph uninit -f); 409 for an unindexed directory; undoes the index root
/api/dsh-codegraph/telemetry GET Read-only relay of upstream anonymous-usage telemetry status { enabled, output }
/api/dsh-codegraph/projects GET Known-projects list { projects, indexedCount, effectivePath } — candidates come from live sessions plus paths the plugin observed (/follow reports, directories whose /status was queried, the default project), each re-checked for index state; monorepo subdirectories collapse to the index root
/api/dsh-codegraph/unlock POST Clears stale lock files blocking indexing { path } (codegraph unlock; idempotent — exits 0 when there is no lock)
/api/dsh-codegraph/cancel POST Cancels in-flight CLI calls { path? } (omit = cancel all); a disconnecting page also aborts its call
/api/dsh-codegraph/diagnose GET Collects a diagnostics bundle { path?, report }: report is plain text covering versions/platform, the raw CLI probe failure, index state, the managed row plus the redacted patch blocks, the ~/.codegraph daemon registrations and log tail, the most recent CLI failure, and the adoption rate
/api/dsh-codegraph/metrics GET Adoption rate { path? }: without path returns every project as { summaries, since }; with path returns { project, summary, text, since } for that project

All routes are loopback-only, to prevent remote access. diagnose is GET-only (read-only), but it may run a probe, so it is never reachable as a side effect of a write path.

Redaction policy for the diagnostics bundle: ~/.dsh/cordis.patch.yml is shared by every MCP server, so other entries may carry credentials under headers / env. The report therefore excerpts only the two codegraph-related blocks (this plugin's block and the dsh-mcp card block) and redacts by key name: id / name / serverName / transport / command / args / cwd / disabled keep their values (exactly what you need for diagnosis), every other key becomes <redacted>, and a sensitive key such as headers: / env: taints its whole subtree (the authorization: value is wiped while its name stays). Newly added fields are redacted by default (fail-closed).

Adoption metrics

The adoption row on the card answers the question that comes after configuration: once it is set up, does the model actually use it?

  • Source: the host's existing session/event stream (counting tool/call only — the same public subscription surface dsh-agent-instructions and dsh-acp use). tool/result is deliberately not counted: adoption asks whether the model wants to use it, and a failed call still counts as wanting to (whether it worked is a different question, answered by the diagnostics bundle).
  • Numerator/denominator (two denominators; the narrow one is primary): codegraph matches mcp__codegraph__*; the narrow denominator is discovery calls (grep / glob / search / find / list_dir) and the wide denominator additionally includes read / view / open. Both are reported because in real history read accounts for 1956 calls against only 100 for grep, and read is mostly "open the file I already know I need to change" — codegraph replaces finding things, so counting read in the primary denominator would pin the number in the single digits forever (measured: 2.2% wide vs 28.8% narrow).
  • bash / media / network tools are excluded from the denominator: most of what a model does with bash (running tests, installing deps, git) has nothing to do with code exploration, and read_image / read_pdf / web_search are the same (this bullet was added after measuring real history — see below).
  • Measured numbers: see docs/adoption-audit.md: traced back over 177 real historical sessions — 23% session-level, 28.8% narrow, 47% over the last three days — plus a quality sample of the query calls codegraph received (34/47 carried a concrete symbol name).
  • A zero denominator reads "no exploratory calls yet", not 0%: "never explored" and "explored but grepped everything" are different facts.
  • The project key is the index root (resolveIndexedRoot) — the same criterion as the managed row's cwd and the injection gate, so several sessions in one repository (new session, subagent, host restart) merge into one row instead of scattering into samples too small to mean anything. Unindexed projects are recorded too, but the wording states that the number says nothing about prompt effectiveness, and /metrics additionally reports grouped.indexed / grouped.unindexed subtotals — the model should not be using codegraph in an unindexed project at all, and mixing those into the overall figure would produce a meaningless number.
  • In-memory only; a host restart resets it, and since reports the starting point honestly. Not persisting is deliberate: this is an observation for deciding whether to tune the prompt, not an audit log, and persisting would leave tool names and project paths on disk indefinitely.
  • The same data also goes into the diagnostics bundle (/diagnose), because the first thing to separate when someone reports "codegraph doesn't seem to help" is "the model never used it" from "it used it and the results were wrong".

Request semantics (since v0.4.2):

  • POST routes answer 400 for malformed / oversized / non-object bodies — an unreadable body is never treated as "no path given" and silently executed against the default project.
  • Paths must exist and be directories: status / query / callers / callees / impact / node return 400 for a nonexistent path (the CLI exits 0 with empty results there, which reads like "no matches"); --limit / --depth accept positive integers only (limit is capped at 10000; depth's upper bound is clamped by the CLI itself to 10).
  • Ancestor semantics: POST /default-path binds the repository root that holds the index when invoked on a monorepo subdirectory; /init on such a subdirectory answers 409 (the ancestor already has an index, avoiding a nested one); /follow rejects reports of nonexistent directories.

Compatibility (DSH / codegraph CLI)

  • DSH: the host version matrix (every tier states how it was verified — do not dress a regression run up as a compatibility claim):
    • 0.1.7-rc.1 (the previous baseline, measured 2026-09-23): node scripts/verify-codegraph-host-contract.mjs --profile test --port 3087 --dsh-bin <separately installed rc.1 dsh> --runtime-store <that install's .pnpm/node_modules/@deepseek-ai> — 42/42 passed: all 25 routes present (including /projects, /metrics, /diagnose, /unlock, /files, /affected, /explore, /context, /uninit, /telemetry, /agents), the POST and loopback gates hold, /diagnose output is complete in every section, the browser-half artifact is servable and carries the latest UI, the managed MCP row is written into the home patch according to the real index, and the mcpScope switch ("per-agent → the global row is suspended → back to managed restores it") is fully reversible with the real ~/.dsh/cordis.patch.yml byte-identical. Two new checks specifically cover the 0.1.7 settings migration: switching mcpScope back to the managed baseline through the plugin's own write path after startup, and switching the default project to a temporary one — both must answer HTTP 200 (under the old API these two steps only produced 500s).

      • How to verify: if the machine's global dsh is not rc.1, point --dsh-bin at a separately installed rc.1 and use --runtime-store to re-point the copied temporary profile's node_modules/@deepseek-ai/* at that install's runtime — otherwise those links still point at the global old install, and the new cohort's compatibility preflight disables the whole old-runtime row as incompatible (the host then never starts at all, which has nothing to do with this plugin).
      • Another piece of live evidence already collected: starting the test profile with an isolated DSH_HOME (with the same runtime re-pointing) → all 10 plugins mounted, 0 lines of disabling profile plugin row, and POST /api/dsh-codegraph/settings answering 200 in both directions between per-agent and managed.
    • 0.1.7-rc.2 (the current baseline, measured 2026-09-25): the cohort moved from rc.1 to rc.2, raising the peerDependencies and dsh.engines.dsh lower bounds together. node scripts/verify-codegraph-host-contract.mjs --profile test --port 3087 (this machine's global dsh already is rc.2, so no --dsh-bin / --runtime-store needed) — 42/42 passed: all 25 routes, the POST / loopback gates, /diagnose sections, the browser-half artifact being servable, the managed MCP row written into the isolated home patch, and mcpScope reversible between per-agent and managed with the real ~/.dsh/cordis.patch.yml byte-identical. Two more: a real host loading the test profile mounts all 10 plugins with 0 lines of disabling profile plugin row; and pnpm -r typecheck green across the workspace, vitest 960/960, pnpm -r build green. This round changed no plugin runtime behavior (only compatibility declarations, docs and the lockfile). Per-file comparison of the lib artifacts from rc.1 → rc.2: dsh-tools only adds an optional PreToolDecision.displayReason (this repo constructs no such decision), dsh-session-query only gained JSDoc, dsh-client-ui-theme only added design tokens, and dsh-mcp-client / dsh-host-webserver / dsh-subprocess-local / dsh-credentials / dsh-base / dsh-settings / dsh-web-app / dsh-headless are byte-identical; dsh-app-boot removed skippedProfileBundles and changed generateConfigSchema's signature (dropping the leading binName), but that is an internal surface between DSH's own CLI and app-boot that this repo never references.

      ⚠️ Note (left as-is) — the two test totals are not from the same run: this entry records the count at the rc.2 verification point (960/960); after this round of project-level documentation work the current value is 56 files / 965 tests repo-wide (packages/codegraph itself: 14 files / 317 tests). Neither number is changed — the current value is the ROADMAP.md “2026-09-25 re-measurement” table.

      The same goes for the “25 routes” figure here: compute it with the command (see the ROADMAP re-measurement table); do not treat it as a contract.

    • 0.2.0-rc.1 (the second supported baseline, added in the same round; measured 2026-09-28): no runtime code changed — only the compatibility range became two segments (see the last two bullets). Evidence: ① this repo's 13 installable packages judged one by one with 0.2.0-rc.1's own evaluator (evaluatePluginCompatibility in dsh-app-boot): the single segment ^0.1.7-rc.2 gets 12/12 refused (^0.1.7-rc.2 desugars to >=0.1.7-rc.2 <0.2.0-0, and 0.2.0-rc.1 sits exactly on that -0; includePrerelease: true does not rescue it), while the two-segment range admits every cohort. ② With an isolated DSH_HOME on a real 0.2.0-rc.1 host: dsh plugin add installs all 10 plugins without any exemption, all 10 mount at startup with no skipping profile bundle, verify-client-ui is 5/5 PASS and verify-rss-opml is 9/9 PASS (real download + round-trip). ③ The same packages regress green on a 0.1.7-rc.2 host (client-ui 5/5, rss-opml 9/9). ④ Per-file comparison of the two cohorts' plugin API surface: the .d.ts of dsh-tools / dsh-client-ui-slots / dsh-client-locale / dsh-host-webserver / dsh-subprocess-local / dsh-client-modules / dsh-package-manifest are byte-identical, the plugins.row.config slot contract is identical, and cordis 4.0.4 plus schemastery 3.18.4 are the same versions.

    • 0.2.0-rc.2 (the third supported baseline, appended in the same round; measured 2026-09-29): no runtime code changed — the compatibility range only gained a third segment (see the last two bullets). This tier differs from the previous two: 0.2.0-rc.2 was already covered by ^0.2.0-rc.1 (>=0.2.0-rc.1 <0.3.0-0; semver.satisfies('0.2.0-rc.2', …) is true with includePrerelease: true), so the new segment is an explicit declaration rather than a boundary fix — but it still went through every step of "each added cohort must be really tested". Evidence: ① the evaluator from each of the three cohorts (evaluatePluginCompatibility in dsh-app-boot; the 0.1.7-rc.2 and 0.2.0-rc.1 copies installed from the registry with their dependencies) judging this repo's 13 installable packages one by one × cohort — 3 evaluators × 3 cohorts, every combination admitted (node scripts/check-dsh-peers.mjs --app-boot <that cohort's dsh-app-boot dir>). ② With an isolated DSH_HOME on a real 0.2.0-rc.2 host: dsh plugin add installs all 10 plugins without any exemption; --dump-config writes 0 lines to stderr and all 10 @hyzyn bundles appear in the composed tree; at startup all 10/10 mount with no skipping profile bundle. ③ Three layers of live acceptance on that same host: verify-client-ui 6/6 PASS (all 10 @hyzyn client bundles really loaded in Chrome, 0 uncaught exceptions, 0 error-level console output, 0 failed sub-requests), verify-rss-opml 9/9 PASS (real download + import round-trip + cleanup), and verify-codegraph-host-contract 42/42 PASS (all 25 routes, the POST / loopback gates, /diagnose sections, the browser-half artifact being servable, the managed MCP row written into the isolated home patch, and mcpScope reversible between per-agent and managed with the real home's cordis.patch.yml byte-identical). ④ Per-file comparison of the plugin API surface from rc.1 → rc.2: of all 166 dependency packages of the cli + web-app only 40 have real code changes, and the host packages this repo imports (dsh-base / dsh-tools / dsh-client-ui-slots / dsh-web-app / dsh-plugin-manager / dsh-mcp-client / dsh-settings / dsh-system-prompt / dsh-credentials / dsh-scope / dsh-session-query / dsh-launch-environment / dsh-agent / dsh-hmr and more) are byte-for-byte unchanged; the only plugin-facing change is dsh-cordis-client-runner's slot catalog, 97 → 98 entries, additions only (a new sidebar.right.tab.files.actions), while the 4 slots this repo registers (plugins.row.config / settings.kit.item / settings.plugin.item / sidebar.right.pane.tab) are byte-identical. ⑤ Workspace regression: pnpm -r build / pnpm -r typecheck green, pnpm test 92 files / 1484 tests all passing; this round touched only compatibility declarations and docs, changing no plugin runtime behavior.

    • 0.2.1-alpha.1 (the fourth supported baseline; measured 2026-10-03): no runtime code changed — the compatibility range only gained a fourth segment (see the last two bullets). Like the third tier, this is an explicit declaration rather than a boundary fix: 0.2.1-alpha.1 was already covered by ^0.2.0-rc.2 (>=0.2.0-rc.2 <0.3.0-0; semver.satisfies('0.2.1-alpha.1', …) is true with includePrerelease: true), yet it still went through every step of "each added cohort must be really tested". Evidence: ① this repo's 13 entries (12 installable packages plus templates/hello) judged with 0.2.1-alpha.1's own evaluator (the dsh-app-boot inside the local install, whose getDshRuntimeVersion() reports 0.2.1-alpha.1): 13/13 admitted, and node scripts/check-dsh-peers.mjs --app-boot <that install's @deepseek-ai/dsh-app-boot dir> admits all four cohorts x 13 entries. ② A real host started with an isolated DSH_HOME (the local test2 profile copied in, --patch pinning the port): 10/10 @hyzyn plugins mounted, 0 lines of disabling profile plugin row, 0 lines of skipping profile bundle; on the real local test2 profile the install-time judgement was exercised too: dsh plugin --profile test2 add link:<each package> (10 of them) returned Already up to date for every one, with no incompatible-version and no exemption needed (judged once before install and once at startup). ③ Four layers of live acceptance on that host: verify-client-ui --mode boot 5 PASS / 0 WARN / 0 FAIL and --mode full 17 PASS / 1 WARN / 0 FAIL (all 10 @hyzyn client bundles really loaded in Chrome, 0 uncaught exceptions, 0 error-level console output, 0 failed sub-requests; --mode full additionally proves that all 8 plugins' inline config cards render (plugins.bundle.config plus the plugins.row.config row marks) and that the settings page lists 8 kit cards under its plugin-config row; the single WARN is the sandbox refusing a PTY with posix_openpt failed, not a regression), verify-mcp-http 7 PASS / 0 FAIL (all three streamable-http response modes plus a readable failure when the URL is unreachable), verify-rss-opml 9 PASS / 0 FAIL (real download + import round-trip + cleanup), and verify-codegraph-host-contract 42/42 PASS (all 25 routes, the POST / loopback gates, /diagnose sections, the browser-half artifact being servable, the managed MCP row written into the isolated home patch, and mcpScope reversible between per-agent and managed, with the real ~/.dsh/cordis.patch.yml byte-identical). ④ Per-file comparison of the plugin API surface from 0.2.0-rc.2 to 0.2.1-alpha.1: of the CLI package's 20 files only package.json changed; the host packages this repo imports (dsh-base / dsh-host-webserver / dsh-subprocess-local / dsh-client-locale / dsh-client-ui-slots / dsh-settings / dsh-session-query / dsh-mcp-client / dsh-system-prompt / dsh-launch-environment / dsh-client-ui-theme / dsh-headless and more) are byte-for-byte unchanged; dsh-tools only reorders the run_code parameters and rewrites their wording, and drops the ./invariant subpath export (unused here; index.d.ts is byte-identical), while evaluatePluginCompatibility is semantically equivalent; dsh-cordis-client-runner's slot catalog went 98 to 100 entries, additions only (plugins.add.actions / shell.bottom), and every slot this repo registers is still present. ⑤ Runtime dependency drift: this tier's host carries cordis 4.0.5-alpha.1 and schemastery 3.18.5-alpha.1 (the first three tiers all had 4.0.4 / 3.18.4), but both tarballs are byte-identical apart from package.json, and app-boot detects native schemas with Symbol.for('schemastery') rather than instanceof; this repo's pnpm -r typecheck was run after re-linking to those two new versions and is green. ⑥ Workspace regression: pnpm -r build / pnpm -r typecheck green, pnpm test 106 files / 1644 tests all passing, pnpm aggregate with no diff; this round touched only compatibility declarations, the guard and docs, changing no plugin runtime behavior. Not run: pnpm live-smoke (it copies one-off profiles into ~/.dsh/profiles/live-smoke-*, and this round's execution environment only permits writes inside the workspace, so ~/.dsh writes were denied; run it from a normal local terminal when needed).

    • 0.2.1-alpha.2 (the fifth supported baseline; measured 2026-10-09): no runtime code changed — the compatibility range only gained a fifth segment (see the last two bullets). Like the third and fourth tiers, this is an explicit declaration rather than a boundary fix: 0.2.1-alpha.2 was already covered by ^0.2.1-alpha.1 (the same 0.2.1 version tuple; prereleases do not rank against each other), yet it still went through every step of "each added cohort must be really tested". Evidence: ① this repo's 13 entries (12 installable packages plus templates/hello) judged with 0.2.1-alpha.2's own evaluator (the dsh-app-boot inside the local install, whose getDshRuntimeVersion() reports 0.2.1-alpha.2): 13/13 admitted, and node scripts/check-dsh-peers.mjs --with-app-boot (which locates that evaluator on its own) admits all five cohorts x 13 entries. ② The install-time judgement was exercised on the real local test profile: dsh plugin --profile test add link:<each package> (10 of them) returned Already up to date for every one, with no incompatible-version and no exemption needed; a real host then started from that profile (dsh --profile test --patch <port overlay>, runtime 0.2.1-alpha.2): 10/10 @hyzyn plugins mounted, 0 lines of disabling profile plugin row, 0 lines of skipping profile bundle. ③ Acceptance on that host: verify-client-ui --mode full 18 PASS / 0 WARN / 0 FAIL (all 10 @hyzyn client bundles really loaded in Chrome; all 8 plugins' inline config cards render (plugins.bundle.config plus the plugins.row.config row marks); the settings page lists 8 kit cards under its plugin-config row; the global-search command palette really returns hits; the terminal pane's xterm initializes; 0 uncaught exceptions, 0 error-level console output, 0 failed sub-requests — this round refused no PTY, so the fourth tier's single WARN is gone as well), and pnpm live-smoke (= live-host-smoke --bootstrap --strict) 18/18 PASS (two host instances: the authorization ladder, route gating, tool inventory, probe wording, and the client.js the host actually serves) — which closes the "not run" gap the fourth tier recorded. ④ File-by-file API-surface comparison, 0.2.1-alpha.1 → 0.2.1-alpha.2: among the host packages this repo imports, dsh-mcp-client (the MCP managed row) / dsh-settings / dsh-credentials / dsh-launch-environment / dsh-sandbox / dsh-scope / dsh-user-approval / dsh-agent / dsh-client-locale / dsh-subprocess are byte-identical except for package.json; in dsh-tools the defineTool export line this repo uses is byte-identical, and the only changes are ToolPresentationMode losing 'both' (zero use here) and the internal PTC bridge RunCodeBridgeOptions gaining a required resolveWorkingDirectory (this repo implements no such bridge); the client slot surface only grows (adds conversation.chat.flow / conversation.chat.reasoning.body; zero removals, zero renames, and no slot this repo registers was taken away); the dsh-host-webserver and dsh-web-app changes all sit on the host configuration plane (the listen address now takes loopback or one concrete NIC only, new TLS config, webRuntime → webStartup), names this repo never references. ⑤ Runtime deps: this tier still ships cordis 4.0.5-alpha.1 and schemastery 3.18.5-alpha.1 (the same pair as the fourth tier). ⑥ Workspace regression: pnpm -r build / pnpm -r typecheck green — and that typecheck ran after node scripts/link-dsh-runtime.mjs had pointed this repo's node_modules/@deepseek-ai/* at the alpha.2 set (so the type surface really was alpha.2's dsh-tools, not the older devDeps pin), pnpm test 139 files / 2420 tests all green, pnpm aggregate with no diff. ⑦ The same round re-checked the three facts and the picker criteria in docs/os-consent-plan.md §2.1 through the host's upgrade tripwire: this tier really did change those criteria (the loopback test moved to isLoopbackHost() from dsh-host-webserver, and allowsRemoteAuthorities was added), so the tripwire went red by design and the re-check is recorded at the end of that document's §2.1 and in §4 item 2.

    • 0.1.6-alpha.2 / 0.1.5-rc.2 / 0.1.0-rc.7 (historical baselines, no longer supported): those tiers still had the old register(ns, schema) API for the settings service, whereas the 0.1.7 line replaced it with SettingsForms (describe/update plus a volatile exported Config); this package's peerDependencies floor ^0.1.7-rc.2 also makes a new host refuse to load it onto an old one, both before install and at startup. The evidence collected for those tiers at the time (0.1.6-alpha.2 40/40, 0.1.5-rc.2 no script, 0.1.0-rc.7 audit baseline) stays in git history for reference only.

    • package.json declares peerDependencies: { "@deepseek-ai/dsh": "^0.1.7-rc.2 || ^0.2.0-rc.1 || ^0.2.0-rc.2 || ^0.2.1-alpha.1 || ^0.2.1-alpha.2" } (with optional in peerDependenciesMeta, purely to quiet pnpm's unmet-peer noise). From DSH 0.1.7-rc.1 on, compatibility is judged from this before install and at startup: an incompatible pair throws incompatible-version on install, and at startup the whole row is marked disabled (an incompatible bundle is skipped outright). dsh.engines.dsh: ">=0.1.7-rc.2" is kept as well, as the market display slot (the host does not read it — see the next bullet; the market resolver only accepts one segment, so it carries the lowest cohort), and scripts/check-dsh-peers.mjs asserts the peer floor agrees with it.

    • Why peer rather than engines.dsh: dsh-app-boot walks only peerDependencies entries named @deepseek-ai/dsh or starting with @deepseek-ai/dsh-, judging them with semver.satisfies(runtime, range, { includePrerelease: true }) (prereleases take part in range matching). engines.dsh is still declared, though — the host does not read it, but the plugin market / community entries display compatibility from it, so it is kept in the standard form >=0.1.7-rc.2 (the market resolver only accepts >=X.Y.Z[-prerelease]; an || range is judged "unverifiable" and fails closed) and made to agree with the peer floor. Evidence: evaluatePluginCompatibility in dsh-app-boot/lib/index.js, and the app-boot README's own wording that these checks use the peer declaration rather than engines.dsh.

    • Why the range is five segments, ^0.1.7-rc.2 || ^0.2.0-rc.1 || ^0.2.0-rc.2 || ^0.2.1-alpha.1 || ^0.2.1-alpha.2: with prereleases taking part in matching, ^0.1.7-rc.2 = >=0.1.7-rc.2 <0.2.0-0 — a single segment kills the whole 0.2 line, which is exactly why this repo's plugins showed as "abnormal / contains no components" in the desktop app once 0.2.0-rc.1 shipped. The second segment admits the 0.2.0-rc.1 tier and, with it, the whole 0.2.0 rc line and 0.2.x finals; the third, ^0.2.0-rc.2, the fourth, ^0.2.1-alpha.1, and the fifth, ^0.2.1-alpha.2, all explicitly declare the current newest tiers (zero behavioral difference for a host, since the second segment already covers them; the fourth and fifth tiers each have their own measurements and API-surface comparison, see the previous bullet) — they are kept because this repo's rule is "declare every cohort you adapt to and leave the measured evidence", not "a range that is just barely sufficient". Matching any branch admits the host, and all five tiers' API surfaces have been compared file by file (previous bullet). 0.1.7 final and later 0.1.7-rc.N / 0.1.8 still ride the first segment, while 0.1.7-rc.1, 0.1.7-alpha.2 and 0.1.6-* still do not pass. To temporarily admit another version combination, write the exemption into the profile's own compatibility.json (dsh plugin --profile <p> allow-version <pkg@ver> --dsh-version <ver> --accept-risk) rather than adding segments ad hoc — every added segment must be a cohort that was really tested and whose API surface was compared.

  • Codegraph CLI: version matrix — the full chain has been verified on 1.5.0 (macOS) and 1.6.0 (Windows / macOS); the subcommands used are status / query / callers / callees / impact / node / sync / index, with every flag checked one by one. codegraph serve --mcp still works (it is not listed in the top-level help, but codegraph serve --help has it), so the managed row needs no change. Re-verify after upgrading the CLI: flags such as -y are version-dependent (this plugin deliberately omits it, see below).
  • URL shape of the browser half: DSH currently goes through client-modules' combo route, so the single-package direct link /plugins/@hyzyn/dsh-codegraph/client.js is no longer directly usable; the browser only uses /plugins/??<id>/client.js&rev=… handed down by the boot graph (window.__DSH_BOOT__), and the plugin side needs no change.
  • Operating systems: Windows / macOS / Linux run the same code path, and CI is a three-platform matrix (pnpm -r build + typecheck + test).
    • Windows: the CLI goes through %COMSPEC% /d /s /c with cmd escaping (a global npm / pnpm install only ships a .cmd shim, which execFile cannot start directly — ENOENT); a timeout kills the whole tree with taskkill /pid <pid> /T /F, including the grandchild CLI inside the shim (killing only cmd.exe lets a large index run to completion); rewriting the patch file keeps the file's own line endings (a CRLF file is not turned into mixed endings, and a rewrite does not change the line count).
    • index will not initialise for you: codegraph index --help says "same result as a fresh init", but that describes the result of a full rebuild being equivalent to a fresh init — not that index performs initialisation. On a directory without .codegraph/, both index and sync fail with CodeGraph not initialized in <path> plus Run "codegraph init" first. The card's "Initialise index" button therefore uses init.
      • Arguments are fixed to init -- <path>: -y is not passed — that flag only exists in CLI 1.6.0 and 1.5.0 fails with error: unknown option '-y'; omitting it does not hang either (the runner has no TTY, and both 1.5.0 and 1.6.0 were verified to take the defaults and finish). -f is deliberately not passed (the CLI uses it to guard against accidentally initialising your home directory or a filesystem root, and the plugin should not bypass that for the user).
      • This is the plugin's only action that writes into the user's project: it creates codegraph.db and a self-ignoring .gitignore (containing * and !.gitignore) under .codegraph/. It touches no source file and does not modify the project's root .gitignore; codegraph uninit removes it entirely. The card uses a two-step confirm and it is never triggered automatically.
      • init / index trigger the codegraph CLI's own anonymous usage statistics (upstream prints a notice; disable with codegraph telemetry off or CODEGRAPH_TELEMETRY=0). The plugin does not change that switch — it is the user's preference.
    • PATH: both the probe and the calls use the plugin's command config (default codegraph, resolved through PATH). When the host is started from an entry point that does not inherit the shell environment (Dock / Start menu), the CLI may be missing from PATH — neither prompt block is injected and the card cannot resolve a working command.
      • PATH is read once, when the host process starts, so refreshing the page or reopening the card cannot change it, and the host will not pick it up on its own. Two fixes: (1) point command at the CLI's absolute path (editing the profile patch triggers a hot reload and re-probes); (2) restart the host from a freshly opened terminal so the new environment block applies.
      • Afterwards, click re-probe on the card (POST /reprobe) to confirm in place — no host restart needed. The card shows the actual probe failure (spawn codegraph ENOENT, a non-zero exit's stderr, a timeout), which distinguishes "not installed" from "installed but off PATH".
      • On Chinese (and other non-UTF-8 code-page) Windows, the CLI and cmd.exe write stderr in the console code page; the plugin decodes it with the @hyzyn/dsh-kit tolerant decoder (UTF-8 first, falling back to the code page on invalid bytes), so the card shows the original Chinese instead of ���.
    • MCP row: dsh-mcp-client uses the official SDK's StdioClientTransport (the SDK depends on cross-spawn), which resolves .cmd shims on Windows itself, so the managed row needs no platform branch.
    • The upstream CLI ships official builds for all three platforms × x64/arm64 (bundled Node runtime); the plugin's index detection only looks for a *.db under .codegraph/ rather than a fixed filename, so an upstream rename does not affect it.

Development

pnpm --filter @hyzyn/dsh-codegraph build
pnpm --filter @hyzyn/dsh-codegraph typecheck
pnpm test                                   # repository-wide vitest (or: vitest run packages/codegraph)
pnpm --filter @hyzyn/dsh-codegraph test     # this package only (managed-row matrix + CLI knobs + route regressions)
node packages/codegraph/scripts/preview-card.mjs        # render the card preview HTML into .preview/ (--png needs a normal terminal: Chrome cannot start under a restricted sandbox)
                                                        # also --per-agent / --fallback: view the per-agent "active" and "fell back to managed" states offline
node scripts/verify-codegraph-indexforce.mjs --profile test --port 3086   # live end-to-end: does indexForce really spawn with --force (requires a local DSH)
node scripts/verify-codegraph-host-contract.mjs --profile test --port 3087  # live end-to-end: host contract (25 routes + gates + served artifact + managed row + per-agent mode switch); needs a local DSH

per-agent: three layers of acceptance

Each layer proves one thing; missing a layer leaves a real gap uncovered — gap-closing, not padding:

Script Layer What it proves
verify-codegraph-agent-scope.mjs mechanism Minimal Cordis root: scopes can mount mcp-client, the same serverName does not collide across scopes, the global layer stays clean, disposal reclaims only its own
verify-codegraph-host-contract.mjs routes / switch Real host: 25 routes, mcpScope switching, the global managed row getting suspended and restored, real patch byte-identical
verify-codegraph-agent-integration.mjs integration Mounts the real plugin, drives a real agent/created through the real AgentRegistry, and asserts the tool lands on that agent, that an unindexed agent is not mounted, and that agent/disposed reclaims it

The third layer was added because the first two being green still could not prove "MCP actually mounts when a user opens a session": the mechanism script uses fake agents, and the host script's host never creates an agent (every /agents round reported mounted=0). That path is the entire value of per-agent.

All three build their own temp projects, recl

…

Content from the project README on GitHub ↗

Links

More in this category

View the whole category →

Community comments

Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.