Skip to content

dsh-painfinder-plugin

Verified

dsh-painfinder-plugin · v0.6.0

This ESM plugin runs on DSH **`>=0.2.1-rc.2`** — the official desktop app's current runtime — and refuses older runtimes through `engines.dsh` (the harness compatibility gate lets an own `engines.dsh` range decide alone). DSH 0.2.1 changed the delegation

Install

dsh plugin add dsh-painfinder-plugin

Confirm the layer applied with dsh --profile default --dump-config — see the install guide.

Source

Published to npm without a public repository. Inspect the package contents before installing.

Readme

DeepSeek Harness bridge

This ESM plugin runs on DSH >=0.2.1-rc.2 — the official desktop app's current runtime — and refuses older runtimes through engines.dsh (the harness compatibility gate lets an own engines.dsh range decide alone). DSH 0.2.1 changed the delegation contract the preset encodes: delegation-tool config no longer carries persona/toolFilter (a config carrying either fails at load), every delegation is asynchronous, and a continuable child reports with send_message. The bridge therefore fills a delegated child's persona as a per-agent prompt override in the child's own scope, and the preset mounts the continuation tools (ADR-005). The two native surfaces the bridge calls directly (the validateJsonSchemaValue argument walker in dsh-tools and foldConsumedWork in dsh-agent) are unchanged in 0.2.1. Version 0.6.0 ships the agent preset as a bundle patch (presets/pain-discovery.patch.yml, declared by dsh.bundle.patch), which is the form a desktop installation reads; the presets/pain-discovery/{preset.yml,agent.cordis.yml} directory pair remains as the host-root composition the SDK runner overlay generates from. It keeps the reliance on continuable-subagent and session/event turn-end contracts. It provides two native tools: painctl and pain_skill. Skills and reference role contracts (agents/, while the runtime persona text lives in personas/) are bundled here. Python remains the only canonical writer.

The package is self-contained. npm pack copies the repository's pyproject.toml, uv.lock, and src/pain_discovery into python/; on first use the bridge installs uv with its official installer if the machine has none, syncs ~/.dsh/painfinder/venv from the shipped lock (uv sync --frozen --no-dev), smoke-tests painctl, and records the state in ~/.dsh/painfinder/setup.json. The same work is available as an explicit, idempotent command: painfinder-setup (or node <package>/scripts/setup.mjs). No launch environment is required; optional overrides go in ~/.dsh/.painfinder-env (read by the plugin itself): PAINFINDER_PROJECT for a development checkout, PAINFINDER_STUDIES for a pinned studies root.

Follow the installation guide for packaged installation, preset registration, service PATH, and compatibility checks. The desktop template is presets/pain-discovery.patch.yml; the legacy directory template is presets/pain-discovery/agent.cordis.yml; either needs trusted deployment configuration. This repository does not change your host configuration or provision model credentials. Configure native tool presentation: under ADR-002 (amended by ADR-005) every research agent is a coding agent, so it also receives bash, the file tools, skill, web_fetch, web_search, and the continuation tools send_message/list_agents/interrupt_agent for reaching and processing external sources and for reporting/steering delegations, while run_code and unlisted tools stay outside the sanctioned path. Loading it in host-root scope is rejected.

Trusted plugin configuration:

Field Meaning
projectRoot Optional override: a Python checkout to use instead of the bundled python/ (development/SDK). The shipped preset does not set it.
studyRoot Single-study mode: one pinned, existing canonical study path (SDK runners). Wins over studiesRoot.
studiesRoot Optional override for the parent folder holding one folder per study. Omitted (the shipped preset's setting): each session uses its own workspace folder — the DSH working directory the person opened — resolved once per session tree from the live workingDirectory service (its projection initializes from the session header's cwd), canonicalized with realpath, and refused when it is a filesystem root. study init creates an auto-named directory per study under it.
role scout, interpreter, adversary, validator, or director
policyVersion Version of the role's research policy
modelPolicy Free-text label for the host's model/provider/prompt identity, saved on new Runs. The bridge routes nothing: model selection is Harness session configuration.
executionRun Optional existing Run shared by workers executing compiled Tasks
producingTask Optional runner-owned Task ID; attached to mutation provenance and validated against the execution Run

Without producingTask or executionRun (a live web session), the bridge acts as the worker: on a scope's first commit it registers, leases, and starts one stage Task (<stage>-<role>-<session suffix>) through Python and stamps it on every record and into search record / acquisition import data. Settlement is explicit and truthful: the agent itself settles its Task — task complete with the committed record ids as semantic_outputs, or task fail with an error_summary; the bridge fills the generated task id when the call omits it, so an agent never has to learn it. Disposal settles the Task only after a turn that ended cleanly (turn/end reason completed: completes with committed work, cancels with none). A DSH stop is a pause, not an outcome: an interrupted, errored, or never-observed turn records no transition, so a continuable child resumes with its Task still leased, and a genuinely dead Run goes through ADR-004's terminal-owner takeover. Compiled Tasks are never claimed this way; they need their execution Run. New Runs also record the plugin's package.json version as preset_version (the ontology's Harness preset/plugin version field), so a study can name the bridge generation that wrote each record instead of inferring it from Run naming.

In DSH 0.2.1 every delegation is asynchronous: a delegate_* call returns the child's id (started subagent …) and does not wait for its work. The child's closing message arrives later as a completion notice, and the child can report earlier or be steered while paused with send_message (list_agents shows the ids). The bundled preset therefore mounts tool-subagent-control and tool-subagent-control/list-agents — the current web bundle leaves those host rows disabled — and the bridge surfaces send_message/list_agents/ interrupt_agent to every role. A delegated child is a continuable child: an interruption keeps its durable session and pending inbox, so a stopped stage is resumed by steering the same child, never by re-delegating over it and orphaning the producing Task it holds.

A child joins the director's preset, so it inherits that composition; DSH 0.2.1 no longer lets the delegation tool configure a persona. The bridge registers dsh-persona's persona slot in the child's own scope (the documented per-agent override) and fills it with the role persona from personas/<role>.md once pain_skill binds the role; binding also narrows the child's visible tools to its role. The director keeps the preset's persona.

The director also mirrors the compiled plan onto the harness's native team task board (one team_task_create per compiled Task, team_task_update as each completes): the board is a live coordination and visibility surface only. It exists when the optional agent-team bundle is enabled; the board tools are registered in the agent's own scope, so the bridge's guard (director only) is their gate and no restriction names them. The study database stays the system of record — painctl workflow next is truth, and a board entry is never read back as research state. Python records no inter-task dependency edges today, so the mirrored entries carry no blocked_by edges. On a team-enabled profile the team bundle registers send_message, list_agents, and interrupt_agent in each member's own scope; those shadow the continuation tools for the team Lead, so a director can message its teammates but cannot steer its own delegate children (the children can still report to it). The bridge guard does not sanction spawn_teammate or wait_agent, so a painfinder session cannot create or await teammates itself.

One execution Run per session tree. The director compiles the approved plan into Tasks under a Run the bridge derives for the tree the session belongs to (harness:<root session id>:execution, created once, recording that root session); a delegated child shares it, so the child can lease and execute the compiled Tasks it was handed and every record it writes satisfies the ownership check (docs/control-usage.md: workers sharing compiled tasks must configure that Run in the bridge). Human decisions are the exception: study init, action select, action cancel, and ontology-change decide run under their own harness:<session>:<role>:decision Run and carry no Task. An external runner may pin its own Run with executionRun (the SDK workflow runner does), in which case the bridge verifies that Run instead of creating one.

The supplied preset configures neither the Python checkout nor the studies root: the package ships the substrate (overridable through ~/.dsh/.painfinder-env) and each session's workspace folder is the studies root. Set role, modelPolicy, and policyVersion in trusted composition. Route models using Harness session configuration: scout handles acquisition/extraction, interpreter handles interpretation/ontology/claims, adversary seeks counterevidence, validator evaluates frozen packets, and director proposes actions. agents/ holds the reference role contracts; the bridge registers personas/<role>.md as the runtime prompt. Separate sessions allow independent adversarial review. No automatic multi-model scheduler or production model choices are bundled.

Painfinder owns the painfinder-session-model label it records on new Runs; the label is provenance, not a router. The preset configures no model selector and defaults to no vendor or model — the host or the runner supplies the route, and the domain package records only the identity it is handed. There is no RigorQuant runtime dependency.

Without executionRun, the bridge creates a Run per session/role. With executionRun, it verifies that existing Run and binds mutations to it, while the audit actor still records the individual session/role. Set the shared Run's model_policy to the routing policy and use model-specific policyVersion values for the participating roles. Tasks and their evidence-producing mutations must reference the same execution Run.

Call pain_skill with a bundled skill name. For mutations call painctl with a two-word operation, a JSON data object in input_json, and idempotency_key. The bridge supplies provenance, study path, and argv itself, invokes uv run --frozen without a shell, and checks integrity before and after writes. study init, action select, action cancel, and ontology-change decide are human decisions: the bridge raises an approval ask, and a denied or unavailable approval fails closed so the operation never runs. Guards enforce the role's operation allowlist; they do not sandbox arbitrary host plugins or a privileged operator.

Every approval card says what it is approving and how to disagree. The card headline is the only text this host plugin controls, so the bridge composes a short context block per gate — the new study's title, question and intake plan; the selected actions read back from the proposal with their objectives and venues; the action and why-note for a cancellation; the decision and change id for an ontology decision — and ends every one with "if this does not match what you asked for, deny it and tell me what to change". When the proposal cannot be read the card falls back to the id list rather than failing.

painctl help is per command and progressive, so an agent never has to guess a shape or read a wall of names:

  • help list (the default) answers with a small block scoped to the caller: current stage/role/skill, the operations that role may commit, the read surface, the task lifecycle, the human gates, and one pointer to help show <operation>. With no study bound it simply omits the stage.
  • A read names its entity with the id parameter, never inside the operation string: {"operation": "observation show", "id": "<id>"}. Appending it (snapshot show <id>) is refused with the exact call to write instead.
  • help show <operation> returns that one command's accepted fields, required subset, enum values, a copyable example, the covering trap, the roles allowed to call it, and whether it is a human gate.
  • help list with {"all": true} returns the full callable index (generated by Python from the schema, filtered by the bridge to names the guard permits), for the rare case where the caller wants to browse.

Help reads no study state, so it also works before a study exists, and every validation error points back at it.

A new topic starts with an intake consultation rather than a bare approval card, and the bridge supports both halves of it:

  • workflow next (stage select_study, or initialize for a pinned root) tells the director to post an intake note in chat first — its understanding of the ask, the scope choices it had to infer, its questions with defaults, and the proposed venues — and then to draft study init with title, research_question, and the consultation summary in description. A draft missing any of those, naming an unknown field (for example question/topic), or arriving as unparseable JSON is denied before any approval is raised, so no human decision is ever spent on a call Python will reject.
  • Once a study exists, workflow next reports direct until a substantive acquisition plan has been proposed (explore actions with objective, source_scope, and constraints), holds at await_selection while the proposal is unresolved, and only reaches acquisition after a selection — an empty proposal does not unlock the evidence chain. A study list entry the installed schema cannot read is reported with needs_migration instead of being silently hidden, so a session migrates or reports it rather than initialising the same topic again.

Every research agent gets the capability surface of ADR-002 as amended by ADR-005 (shell, file tools, skill, web_fetch, web_search, and send_message/list_agents/interrupt_agent) for reaching, processing, and reporting on external sources; only a director (or a scope that has not bound a role yet) also gets delegate_*, so a child never delegates, and only a director may call the team-board tools when the session exposes them. acquisition import commits captured text; painctl itself never retrieves. The bridge tests cover guard behavior, provenance, the approval gate, the per-agent child persona override, and the native defineTool parameter DSL, against a stubbed CLI runner; one integration test mounts the bridge with the real invoke runner against a uv-synchronized checkout and verifies its one committed record in the SQLite study, and the Python side of the call chain is covered by the SDK runner tests. The SDK adapter mounts the same bridge on each SDK-created agent scope and hides every inherited global tool, so a scope sees exactly painctl, pain_skill, and the capability tools the host provides; the bridge also maps recurring near-miss spellings (assertion, packet, controller-spec, research-action, controller input, id on evidence freeze) onto the canonical contract before validation. The repo runners install this directory into the study-local profile (<study>/.dsh/profiles/sdk) whenever the installed version differs from package.json, so bump the version after a change that a running study must pick up.