dsh-prompt-seed
DSH composer plugin: turn a one-line seed into an executable prompt behind a semantic fidelity gate; signals infer, directives stay in degree, conversations stay yours.
- Stars
- 1
- Language
- JavaScript
- Created
- Oct 5, 2026
- Updated
- Oct 7, 2026
Introduction
dsh-prompt-seed
English | 简体中文
A DeepSeek Harness plugin. Type a rough draft, click ✦, and it is rewritten in place — the plugin picks the operation: a true seed is unfolded into a prompt an agent can act on, a complete-but-messy draft is clarified at about the same length, an already-precise instruction passes through near-verbatim. A semantic fidelity gate makes sure the meaning survives every path. Click ↺ to get your original back.
make an image compression feature
│ click ✦
▼
Make an image compression feature: accept jpg, png and webp uploads; let me set
a quality level or a target size; show a before/after comparison of file size and
sharpness; compress several images in one batch and download them as one archive.
Keep the page responsive on large images, keep transparent pngs transparent, say
so when a format is unsupported, and keep the original when a compression fails.
│ written in place
│ (credential: fidelity ✓ · +178 chars · filled in: formats, quality, edge cases)
▼
↺ original · ✦ regenerate
│ after generating another version
▼
↺ original · ✦ regenerate · ‹ previous version
Why this one
The "rewrite my draft" slot in the DSH ecosystem is crowded — there are 26+ plugins in it. Most of them polish: strip filler, tighten wording, cap the length, and refuse to add anything the user did not write. This plugin does the opposite thing on purpose: a one-line seed is usually an insufficient prompt, and the value is in the detail the user could not write.
| typical prompt-optimizer plugin | dsh-prompt-seed | |
|---|---|---|
| vague one-liner | returned nearly unchanged, or padded with (TBD: …) placeholders | unfolded into a concrete, executable request (measured 10–25×) |
| already-precise instruction | reworded, sometimes with extra procedure appended | left essentially as-is (measured 1.0–1.1×), via a separate precise-input contract |
| fidelity check | deterministic string check: did every path / identifier / number survive? | semantic audit by a second call, classifying the failure: DISTORTED / SCOPE_ADDED / CONTRADICTED / TONE_SHIFTED / PADDED |
| on a violation | accept, or reject outright | targeted repair by violation class, re-audited; only reject if it still distorts |
| what you see | a length ratio | a gate credential: fidelity ✓ · +150 chars · filled in: edge cases, failure handling |
| not happy with the result | revert and click again | ✦ regenerate (keeps up to 3 versions) and ‹ step back |
| complete but tangled draft | reworded, sometimes padded | clarified at about the same length — same intent, references resolved, nothing added (hard 1.5x cap) |
PADDED (padding a request that was already complete) and TONE_SHIFTED (a request rewritten into
a different register) have no counterpart in any of the plugins surveyed — a string-equality check
cannot catch "nothing was lost, but the meaning changed".
Being explicit about the trade-off: this plugin adds detail the user did not write. That is the product, and it is also the risk. The gate exists to make it safe — anything that changes what was asked for, narrows it, contradicts it, or pads it is caught and repaired. If you want a plugin that never adds anything, one of the conservative ones will suit you better.
Compatibility
| Host | dsh >=0.2.0-rc.1 <0.3.0-0 (declared as engines.dsh); tested on 0.2.0-rc.2 |
| Node | >=22.19 (matches the host's own requirement) |
| Runtime dependencies | none — the package ships six host modules plus the entry and one browser bundle, no build toolchain needed to install |
DSH is in developer preview and its README warns of breaking changes, so this plugin declares a
range and disables itself instead of breaking the host's boot when a seam is missing: if the
webServer or slots service is absent, it logs one readable line naming the missing seam and the
host version it wants, then stays out of the way. Host boot is all-or-nothing — one plugin that
throws stops the whole process — so self-disabling is the only acceptable failure mode here.
During 0.x, minor version bumps may contain breaking changes (SemVer §4); patch bumps will not.
Install
Install the released tarball (works today, pinned by version):
dsh plugin --profile <name> add \
https://github.com/Zian-anson/dsh-prompt-seed/releases/download/v0.9.4/dsh-prompt-seed-0.9.4.tgz
If that fails with
ERR_PNPM_MISSING_TARBALL_INTEGRITY, download the asset and add the file instead — the tarball is the same bytes, and a local path always resolves cleanly:curl -LO https://github.com/Zian-anson/dsh-prompt-seed/releases/download/v0.9.4/dsh-prompt-seed-0.9.4.tgz dsh plugin --profile <name> add ./dsh-prompt-seed-0.9.3.tgzThe cause is upstream, not in this package: pnpm writes a lockfile entry for an
httpstarball without anintegrityfield and then rejects its own entry on the verification pass. The release asset itself is verified byte-for-byte against the repository bynode tools/verify-release.mjs v0.9.4.
The npm channel lights up once the package is published there:
dsh plugin --profile <name> add dsh-prompt-seed # npm, pending publication
Verify the bundle and row landed before booting:
dsh --profile <name> --dump-config # must show "# == dsh-prompt-seed"
dsh --profile <name>
The ✦ button appears in the composer tool row, immediately left of the model selector. Remove it
with dsh plugin --profile <name> remove dsh-prompt-seed.
Quick start
- Type a rough draft — one line is fine, typos are fine.
- Click ✦ and wait 1–6 s. The draft is replaced in place; nothing pops up.
- Hover the ↺ button to read the credential: gate verdict, size change, and what was filled in.
- Not quite right? ✦ next to it generates another version; ‹ steps back to the previous one.
- ↺ restores your original. Editing the draft yourself also clears the undo — the plugin never overwrites what you have since typed.
Signals and short directives (0.9.0)
Two input shapes that the elaboration contract was never designed for get dedicated semantics:
A bare signal — a number like 42, or a go-ahead like "continue" or "ok" — carries no task content. Its only meaning is: continue with what the conversation left pending. So it is resolved against an anchor (the latest assistant turn plus the latest user turn) in a fixed priority order, and refused honestly when nothing matches:
| Anchor found in context | Inference | Example |
|---|---|---|
| an option list the number matches | choice | 1 + "Continue? 1. keep organizing 2. stop for now" → keep organizing |
| a question the assistant left pending | answer | 15 + "When does it ship this month?" → ships on the 15th |
| the user's own unanswered question / a continuation offer | continue | "continue" → proceed with the offered action |
| nothing matches | cannot_infer, zero model calls | 42 against options 1/2 is a deterministic refusal — never a guess |
A short directive — "fix it" / "wrong" / "change it" — has a clear verb and a missing object. Expansion runs inside a degree contract: the referent must come from context; divergence is allowed only inside the natural sub-parts of the user's own verb and its implied follow-ups; new goals, tools, numbers, or scope are forbidden; the verb must survive verbatim; the output is hard-capped at 180 characters, with one tightened retry before rejection. An unresolvable referent is reported, not invented (an unresolvable referent maps to cannot_infer).
Measured against the live model: a short directive in a button-color context expands to a single sentence under 40 characters with the user's verb preserved and nothing invented, while a mismatched number refuses in 0 model calls.
How it works
Two halves, one package:
┌─ Browser ────────────────────────────────────────────────┐
│ lib/client.js __ModuleLoader__ bundle │
│ slots.register('conversation.input.right', …) │
│ 5 states: ✦ idle / ⟳ busy / ↺ revert / 🛡 declined / ⚠ error │
│ undo backup + draftRev CAS + divergence detection │
└──────────────────────┬───────────────────────────────────┘
│ POST /api/prompt-seed/optimize { text }
┌──────────────────────▼───────────────────────────────────┐
│ lib/index.js Cordis plugin (host) │
│ ctx.webServer.register({ kind: 'exact', path, handler })│
│ loopback-only (peer address AND Host header) │
│ │
│ CONTRACT: adaptive — one call, three operations │
│ ① rewrite call ctx.get('llm').stream(...) │
│ │ model declares MODE first, then does it: │
│ │ seed → unfold the implied detail │
│ │ clarify → same meaning, tidied, ≤1.5x cap │
│ │ precise → near-verbatim pass-through │
│ ② budget gates deterministic, not model judgement: │
│ │ clarify >1.5x → one clarify-only retry → │
│ │ still over → rejected; anchors lost → rejected │
│ │ seed + unchanged → retry at temp .4/.7 │
│ ③ distortion audit 2nd cheap call, semantic rule: │
│ │ OK | DISTORTED | SCOPE_ADDED | │
│ │ CONTRADICTED | TONE_SHIFTED | THIN│
│ ├─ OK, not THIN → accept (tier full) │
│ ├─ THIN + seed input → ④ elaborate once → │
│ │ re-audit → tier │
│ │ elaborated | thin │
│ ├─ unparsable / call failed → accept (fail-open) │
│ └─ distortion → ④ targeted repair (keep the added │
│ │ detail, drop only the violation)│
│ └─ ⑤ re-audit → tier repaired | rejected │
└──────────────────────┬───────────────────────────────────┘
▼
current default model
tier in the response body says where the accepted text came from: full (first draft
passed), elaborated (too thin → elaborated once), repaired (distorted → repaired),
thin (elaboration introduced distortion, so the thin but faithful draft was kept).
The browser half talks to the host half over one loopback-only HTTP route rather than a generated Remote service: the payload is a single request/response pair, and a route avoids binding this package's build to Typert's schema code generation.
| Service | Used by | If missing |
|---|---|---|
webServer | host half (inject) | plugin does not load — a hard dependency |
llm | host half (optional read) | llm_unavailable |
agentDefaultModel | host half (optional read) | model_unavailable |
slots | browser half (inject) | button does not mount |
Configuration
The row accepts a few optional keys; defaults are correct for almost everyone.
- insert:
- id: prompt-seed
name: dsh-prompt-seed
config:
route: /api/prompt-seed/optimize # route path (this is the default)
# provider: zai-coding-cn # pin the model route (provider + model
# model: glm-5.3-flash # work as a pair; defaults to agentDefaultModel)
# context: false # disable session-context injection (on by default)
Changing route requires editing src/client-plugin.js's ROUTE and rebuilding — the browser
half is a compiled bundle, so the two must be changed together. A test asserts they match.
| Key | Default | Meaning |
|---|---|---|
route | /api/prompt-seed/optimize | route path; changing it requires editing ROUTE in src/client-plugin.js and rebuilding |
provider + model | host default | pin the rewrite/audit model (both keys together) |
context | true | session-context injection; now read on demand (short draft or anaphora only) |
samples | $DSH_HOME/prompt-seed/samples.jsonl | event log path; false disables logging entirely |
templates | true | allow $DSH_HOME/prompt-seed/prompts/*.md to override the built-in prompts |
Depth is a client-side setting (right-click the ✦ button): auto (from local feedback counts),
light, standard, deep. It shapes how much a seed is unfolded — clarify and precise
ignore it. It is not a row config key because it is per-user, not per-profile.
Prompt overrides: drop system.md, user.md, audit.md, signal.md, deictic.md or
conversational.md into
$DSH_HOME/prompt-seed/prompts/ to replace the built-in contract. They are re-read on every
request, so an edit takes effect on the next click — no app restart.
Context injection sends the session's two most recent user turns (each truncated to
300 characters) to the rewrite and audit prompts as a reference-resolution-only block. The
last assistant turn is read only to anchor bare-signal inference ("42" → which option?) and is
never sent to the rewrite or audit prompts.
It fails open at every step — no session id, no sessionQuery service, corrupt reads, or
context: false all degrade silently to context-free optimization.
Model routing: rewriting rewards instruction-following over raw generation, but the
floor is higher than "any flash will do". Measured across providers (v0.4.6): glm-5.2
passes every case class cleanly — including the two gray zones where both flash-tier
models fail (procedural insertions into already-precise requests, and conversation
narration leaking into reference resolution). Flash models are fine for the simple
classes and cheaper, but the retry chain they trigger erases the latency win. The
tested-good route is one config line:
provider: zai-coding-cn, model: glm-5.2.
Error codes
The route always answers 200 for business outcomes and puts the outcome in the body.
code | Cause | User-facing message (the UI ships Chinese strings today; English meaning below) |
|---|---|---|
empty_input | draft is blank | Type something first. |
input_too_long | over 8000 characters | Too long — trim it and try again (a neutral hint, not a failure). |
llm_unavailable | llm service not mounted | Model service unavailable. |
model_unavailable | no default model | No usable default model. |
llm_error | call threw, or finish was error/aborted | Model call failed. |
empty_result | empty after normalization | The model returned nothing usable. |
truncated | finish.kind === 'max-tokens' | Result was truncated — shorten the input and retry. |
nothing_to_optimize | input is a bare greeting / punctuation only | Too short — nothing to optimize (a neutral hint, not a failure). |
cannot_infer | a bare signal with no anchor in the conversation | Cannot tell what this refers to — write out what you mean (zero model calls; a neutral refusal, never a guess). |
fidelity_rejected | rewrite and targeted repair both distort the request | The rewrite would change your meaning; your text was kept (the tooltip appends the added list: what the audit flagged). |
forbidden | non-loopback peer or Host | — (403) |
Privacy and data flow
The plugin holds no API key and stores no credentials: it calls the host's own llm service, so
model access and credential handling stay exactly where they already are.
- The only outbound request is the model call the host would make anyway. No telemetry, no update check, no third-party endpoint.
- One local HTTP route (
POST /api/prompt-seed/optimize), reachable only from loopback: the handler verifies both the peer socket address and theHostheader, so a DNS-rebinding page cannot reach it from a browser. - Session context is read on demand, not always: only when the draft is short (≤12 chars) or contains an anaphor, and never for a precise instruction that already names its files. It is used to resolve references, never as content to rewrite.
- The local event log (
$DSH_HOME/prompt-seed/samples.jsonl) records the outcome of each run — code, tier, depth, gate verdict, violation classes, character counts, latency — plus the first 400 characters of the input, so that "is the depth right / is the gate misfiring" can be answered with data instead of opinion. Setsamples: falsein the row config to disable it entirely. - Implicit feedback is four local counters (
applied/reverted/retried/submitted) kept inlocalStorage; they tune the depth setting on your machine and are never uploaded.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| No ✦ button | host older than 0.2.0-rc.1, or the slots service is missing | open the browser console — the plugin logs which seam is missing and the version it wants |
| Click does nothing | the route is not registered | check the host log for the [prompt-seed] loaded banner; confirm --dump-config shows the row |
| "Model service unavailable" / "No usable default model" | host llm or agentDefaultModel not mounted | configure a session model, or set provider + model in the row config |
| Result is longer/shorter than you want | depth setting | right-click the ✦ button to cycle auto / light / standard / deep; the choice is remembered |
| The rewrite was not applied (shield 🛡) | the fidelity gate judged the rewrite would change your meaning — nothing is written back, your text is untouched | hover the shield — the tooltip names the violation class; the eye button lets you view the rejected draft anyway |
| Output keeps getting rejected | the model is weak at instruction-following | point the row at a stronger model (provider + model) |
| You edited the prompts but nothing changed | override files must be at $DSH_HOME/prompt-seed/prompts/ — system.md, user.md, audit.md for the three main contracts, signal.md, deictic.md, conversational.md for the 0.9.x branches | files are re-read on every request, so a correct path takes effect on the next click |
| You upgraded the plugin but behaviour is unchanged | the host half is an ES module, and Node's module cache is keyed by URL — reinstalling the same package name lands on the same path, so the running process keeps executing the module it already loaded | restart the app. Then confirm with curl -s -X POST 'http://127.0.0.1:19387/api/prompt-seed/optimize?debug=1' -H 'content-type: application/json' --data '{"text":"hi"}' and check _debug.codeVersion |
That last row is worth internalising, and it was measured rather than assumed:
| what changed | install path | host half picks it up |
|---|---|---|
| reinstall the same package name with new content | unchanged | no — the process keeps running the cached module until restart |
| install under a different path (renamed package) | changed | yes — the cache misses and the new module loads |
Both cases were observed directly on a running app, and the frozen-at-load codeVersion is what
makes them distinguishable: after a same-name upgrade it kept reporting the previous version while
the new one sat on disk, and after the rename it reported the new one immediately. disable →
enable only re-runs apply against the cached module, so it does not help either. Restart the
app after a normal upgrade, and check _debug.codeVersion instead of trusting the tarball you
just installed.
Development
npm test # builds lib/ then runs 171 tests
npm run build # tools/build.mjs → lib/
Source layout:
| Path | Role |
|---|---|
src/prompt-templates.js | the meta-prompt asset, output normalization, input validation |
src/host-core.js | route resolution, llm.stream consumption, error normalization, adaptive/branch routing |
src/signal-inference.js | deterministic classification, anchor inference, degree checks for signals and short directives |
src/session-context.js | on-demand extraction of recent user turns from the session surface |
src/sample-log.js | the local event log (samples.jsonl) |
src/host-plugin.js | Cordis host plugin → lib/index.js |
src/client-plugin.js | browser factory body → wrapped into lib/client.js |
tools/build.mjs | copies the host half, wraps the browser half |
src/ is the tested source of truth; lib/ is generated and should not be edited by hand.
Test a local checkout without publishing:
npm pack --pack-destination /tmp
dsh plugin --profile plugin-lab add /tmp/dsh-prompt-seed-$(node -p "require('./package.json').version").tgz
dsh --profile plugin-lab --dump-config
Publishing
npm run build
npm publish --access public
Distribution options, from least to most friction for your users:
| Form | Consumer runs | Notes |
|---|---|---|
| npm package | dsh plugin add <name> | prebuilt lib/ ships in the tarball; no build permission needed |
| tarball | dsh plugin add ./x.tgz | immutable, checksummable, good for review before publishing |
| Git dependency | dsh plugin add github:you/repo | fetches source; needs a self-contained prepare and explicit allowBuilds approval from the user, which executes your code on their host outside the agent sandbox |
Prefer npm or a tarball. Only add the Git path if you also ship a prepare that builds from a
clean clone.
Verification status
| Layer | How | Status |
|---|---|---|
| Prompt asset, normalization, validation, error codes | unit tests | ✅ |
| Contract guard (both historical pathologies absent, counter-example triplets present) | template-content tests | ✅ |
| Rewrite pipeline: elaboration gate, semantic audit, targeted repair, call budget | scripted multi-call llm stubs | ✅ |
| Adaptive contract: MODE declaration, clarify budget + anchor rules, question routing, override continuity | unit + pipeline tests + live-model probes (glm-5.2: seed 10.2x / question 1.0x / precise 1.17x) | ✅ |
Gate credential: DETAIL parsing, gate.verdict / rechecked / repairs | unit tests | ✅ |
| Depth: three suffixes, request wiring, invalid-value fallback | unit tests | ✅ |
| On-demand context: anaphora / short-draft / precise-input rules | unit + route tests | ✅ |
| Signals & short directives: classification, anchor priority order, refusal paths, degree cap | unit tests + live-model probes (six cases: two refusals at 0 model calls, four grounded expansions) | ✅ |
| Conversational branch: classifier on the real regression inputs, non-swallowing guards, cap with original-text fallback | unit tests + live-model probes (three regression inputs came back at 1.00×, seeds still 17×) | ✅ |
| Signal / deictic / conversational template overrides | unit tests + route test with a temp $DSH_HOME | ✅ |
Template overrides: read per request, live edit, fallback, templates: false | route test with a temp $DSH_HOME | ✅ |
Self-disable: missing webServer / effect / slots never throws | unit tests | ✅ |
| Host route: loopback guard, method/body rejection, optional services | real req/res stubs | ✅ |
Browser bundle: __ModuleLoader__ shape, slot registration, rendered button props | test materializes the real bundle and renders it | ✅ |
| Cross-artifact route consistency | test compares lib/client.js against the host default | ✅ |
codeVersion truthfulness | test rewrites package.json after load and asserts the reported version is unchanged | ✅ |
| Bundle manifest, patch, tarball contents | npm pack + install into a throwaway profile | ✅ |
| Published release equals the repository | tools/verify-release.mjs, run by CI on every published release | ✅ v0.9.3 byte-identical |
| Real-model behaviour (seed unfolded, precise preserved, multilingual, depth) | live-route and direct-model probes against the installed bundle | ✅ |
| Full click-through in a live browser session | not automated — fetch → route → refill is exercised in pieces, not end to end | ⛔ |
Known limitations
- Reference chips degrade to plain text.
@file//commandchips project toU+FFFC, and bothsetDraftandinsertTextstrip that placeholder, so a rewrite cannot keep a chip a chip — there is no API to rebuild chip nodes from text. The button stays enabled, the tooltip says so, and a conservation guard refuses the write-back if any chip label would be lost. The reference itself is never dropped silently; its structure is. - UI strings are Chinese-only; the
localeservice is not wired. - Non-streaming, and the call budget depends on which path the input takes:
1 for a bare signal (anchored, not judged), a conversational message, a question-shaped
short message, or a declared
preciserewrite kept near-verbatim (audit skipped); 1–2 for a short directive (one draft, plus one tightened retry); 2–3 forclarify(draft + audit, plus one clarify-only retry if over budget); 2–4 forseed(draft + audit, unfold retry, or a repair that is always re-audited). Measured 0.9 s (near-verbatim pass-through) to ~20 s (worst case, slow provider). TONE_SHIFTEDandPADDEDare judgement calls by the audit model, not deterministic checks — a misjudgement inside the tolerance band is the residual fidelity risk.- The audit judge and the rewriter share the routed model by default; routing the audit to a cheaper model would need another config knob.
- Depth adaptation needs a few runs before it moves (deliberately: two signals minimum, so two clicks cannot swing it).
License
MIT — see LICENSE.
Not affiliated with, endorsed by, or sponsored by DeepSeek. "DeepSeek Harness" and "DSH" refer to the open-source host application this plugin targets; this is an independent third-party plugin, and no license in this repository grants any trademark rights.