dsh-painfinder-plugin
Verifieddsh-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 tohelp show <operation>. With no study bound it simply omits the stage.- A read names its entity with the
idparameter, 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 listwith{"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(stageselect_study, orinitializefor 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 draftstudy initwithtitle,research_question, and the consultation summary indescription. A draft missing any of those, naming an unknown field (for examplequestion/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 nextreportsdirectuntil a substantive acquisition plan has been proposed (exploreactions withobjective,source_scope, andconstraints), holds atawait_selectionwhile the proposal is unresolved, and only reaches acquisition after a selection — an empty proposal does not unlock the evidence chain. Astudy listentry the installed schema cannot read is reported withneeds_migrationinstead 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.