dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Keeps a long agent turn answerable to the user's actual request: a standing maintainer-document reminder, two pre-execute gates, an objective anchor quoting your latest instruction, and a nudge stage that corrects before it blocks.
A DeepSeek Harness (dsh) bundle that keeps long-term project memory outside the context window — and keeps a long turn answerable to the request that started it.
A small-parameter model that "thinks hard" builds a long context; the harness compacts it; the compacted summary silently drops the project's conventions, its plan, its stack decisions and its working state — and the model drifts.
The proven fix (measured on DeepSeek V4 Flash) is to externalise that memory into maintainer documents and make the model read them every turn, so memory is re-hydrated from files instead of recalled from a lossy summary:
.dsh-maintainer-doc-guard/
<session-id>/ # one isolated set per conversation
plan.md conventions.md stack.md state.md maintainer/README.md
The five documents live in a per-session subfolder under .dsh-maintainer-doc-guard/
(override the parent with the docsDir knob). Each conversation gets its own set, so two
sessions in the same workspace never share or mix the same file. The plugin creates the
session folder and a blank file for each document on first open of the panel, so there is
nothing to set up by hand and reads are instant.
It has five parts, plus five advisory notices added in 0.5.3 (see The advisory notices below).
1. A standing reminder (a system-prompt section). It registers one
system-prompt section (maintainer-doc-guard) that lists the maintainer
documents present in the workspace and instructs the model to read the relevant
one before every operation or thinking round.
Why a prompt section and not a pre-step message: a pre-step message is
persisted to the session log (that is how instruction-hint dedupes its hint), so
injecting one per step would flood the history. A section is re-evaluated on every
prompt assembly — every turn — and never accumulates. That is exactly the
"standing, per-turn instruction" this needs.
2. A pre-write precedent gate (a tools/pre-execute guard). The harness
already makes a model read a file before overwriting it
(dsh-fs-observation-policy → FS_NOT_OBSERVED). That covers the file it
replaces, but not the contract it must obey — so authoring a brand-new
infrastructure file (a plugin, a preset, a skill) from memory is how invented
APIs reach a running harness.
The gate closes that half. A write/edit whose target lives in a guarded
infrastructure area is denied until the session has read at least one
same-basename precedent that already works. The denial is model-visible and
lists concrete candidate files to read:
Blocked by maintainer-doc-guard: you are about to write "<target>", which lives
in a guarded infrastructure area, but this session has not read any working
"index.js" precedent that shows the real contract.
Read one of these first, then retry:
- /…/node_modules/@deepseek-ai/pkg-alpha/lib/index.js
Why: infrastructure written from memory is how invented APIs reach a running
harness. If the file already exists, read it; if you are creating it, read a
working sibling — the analogue you copy is what keeps the new file consistent
with the real API.
Design rules it follows:
gate.maxDeniesPerTarget denials for one target it lets the
call through and logs — a stubborn model cannot deadlock its own turn.agent.session identity and never persist, so a recovered session re-reads its
precedent — the same semantics dsh-fs-observation-policy uses.Candidates are found by walking up the directory chain and asking, at each level,
whether sibling packages carry the same relative path — for
…/node_modules/<pkg>/lib/index.js the first hit is
…/node_modules/<other>/lib/index.js, which is exactly the file a careful author
reads first.
3. An intent gate (a second tools/pre-execute guard). The precedent gate
polices how a change is written; it says nothing about whether the change was
worth making. The intent gate polices that, and it is the cheaper half: a turn
that has not said what its step is for gets its first side-effecting call denied
once. One round trip buys an explicit statement of purpose — and, when the
purpose cannot be stated, the honest answer is to stop and ask rather than to
run the command and find out.
Its design rules differ from the precedent gate's in one place: it is turn- scoped, not target-scoped, because what it measures is a property of a turn's reasoning, not of a file. It is also the only guard here that stands down on a missing sensor — see the note on fail-open under The intent gate.
4. An objective anchor (a second system-prompt section). Both gates above police how a step is taken. Neither notices that the objective was replaced — which is the actual mechanism behind "it burned a whole turn on something nobody asked for". A long turn drifts because the model's own last paragraph becomes its new premise: the user's instruction recedes into a compacted history while a self-discovered lead takes its place, and every later step is locally reasonable.
The anchor keeps the user's latest instruction quoted verbatim in the system prompt, with an explicit precedence rule:
## Current objective — the user's latest instruction
> 修改一下估算的,这是任务1,任务2是……
This is the instruction the current work is answerable to. It is quoted from the
user's last message, so it outranks anything you inferred since.
Precedence when these disagree:
1. the user's latest instruction, quoted above
2. the plan you stated in your own previous turn
3. a lead, idea, or side-quest you found by yourself
Work the first item does not cover is your own excursion, not the task. Say so in
one line before you spend another step on it, and ask before it costs more than
that.
Two design choices worth naming:
anchor.order, default 10150),
not text appended to the document reminder. An unchanged prompt keeps prefix
(KV) reuse, and a changed one loses it from the first changed token — and this
is the one section that changes whenever the user speaks. Sitting after the Web
surface (10100) and before the deployment persona suffix (10200) keeps the
damage to the smallest tail in the assembly.user/message events
too; source.kind === 'user' is the discriminator the loop itself uses, so the
anchor can never end up quoting the guard back at itself.The anchor is off in subagents by default, and the reason is not performance — it
is correctness. A delegated child (dsh's subagent tool) is started by a
delegation message, not by something a human typed, so the parent's "latest user
instruction" is not the child's job. Rendering it there would invert the very
precedence rule the anchor exists to enforce, and invite the child to chase the
original request instead of the task it was handed.
Set anchor.inSubagents: true when you do want it, and the section switches
identity for that reader:
## Your delegated task — what you were sent here to do
> 把解析器改成允许尾逗号,周五前。
This is the task you were delegated. It is quoted from the message that started
your session. It outranks anything you inferred since, and it does not widen by
being interesting.
Precedence when these disagree:
1. the delegated task, quoted above
2. the plan you stated in your own previous turn
3. a lead, idea, or side-quest you found by yourself
Two mechanics make that work:
source.kind
cannot tell it apart from a human's. Instead a child adopts the first
non-empty user-role message it sees and keeps it; a later real send does not
quietly replace the task it was given. At the top level nothing changes — the
latest user message still wins, and plugin/tool notices are still ignored.inSubagents / anchor.inSubagents) because they
want opposite answers here: the documents are inherited through the parent's
cwd and worth reading; the parent's objective is not the child's task. true
opts a contribution into children and grandchildren; the default, false, or
anything unrecognised keeps it at the top level, so a typo fails silent.Both switches are composition-only — see cordis.patch.yml, not the settings
card, which holds exactly ten fields.
The anchor renders to an empty string — and therefore disappears, costing no tokens — until the session has seen a user message. Because the loop appends the accepted user batch during the step that claimed it, the next assembly already carries the new objective; the turn's own first step is the one place where the user's message is still the last thing in the request, so there is nothing for an anchor to disambiguate yet.
5. A nudge stage (correct before you block). An interception that only fires after a whole turn has burned is a receipt, not a correction. So the intent judgement is staged, and the cheap stage comes first:
| # | what happens | cost |
|---|---|---|
| 1st unexplained action | a reminder is injected (agent.inject) and the call runs | 0 round trips |
| 2nd | denied, with the request to say what the step is for | 1 round trip |
| 3rd | denied again, until intent.maxBlocksPerTurn is spent | 1 per denial |
The reminder reaches the model at its next step boundary, so the correction lands inside the turn that needs it instead of in the next one. Two reminders exist:
nudge.afterSteps steps
without the user speaking again — the one drift signal that needs no semantic
judgement about what the model is doing, because the step count is a fact and
the fact is what "burned a turn on a side-quest" looks like from outside. It
fires even for a turn that narrated itself perfectly, because a well-narrated
turn can drift just as far.The channel is deliberately agent.inject rather than the post-execute
additionalContexts field: additionalContexts exists only on the
POST-dispatch decision, while the pre-dispatch PreToolDecision is
allow/deny/ask and cannot carry context — so a reminder that must arrive before
an action has to travel through the agent. steer is the waking half of the same
pair and is not used, because a guard has no business starting a turn on an idle
driver. Delivery failure degrades to a plain allow and counts nudgeFailed; the
staged path can never turn into a blocking path by accident.
Set nudge.grace: 0 to restore the pre-0.5 behaviour (deny the first offence),
nudge.enabled: false to drop the stage entirely, or nudge.afterSteps: 0 to
keep the nudges but drop the drift check.
The model does not write the operator's documents. A write, edit or doc_write
whose target is plan.md or conventions.md — or a shell command that WRITES one —
is denied outright, deliberately WITHOUT the precedent escape the infrastructure
gate offers: reading another plan.md does not license rewriting this one. The
denial redirects the model to lessons.md, the one document it owns, and records
that gate.operatorDocsReadOnly: false lifts the rule, and the right-sidebar switch
(操作者文档只读) toggles it at runtime — process-level, lost on restart.
The same rule decides what a seed can do: CONVENTIONS_SEED now carries ten rules (8 workspace notes that stay short and classified, 9 notes live in write/ and other people''s documents are off limits, 10 delegation to subagents is allowed), but the seed only applies where the file is CREATED. An existing workspace keeps its own conventions.md — the plugin never rewrites it.
The sidebar tab is one tab with three views, because one button per function in a
single row stopped being readable: documents (reload / save / strict mode),
settings (the six process-level switches, open folder, sweep orphans), and
templates (save the current conventions.md as a named template, then apply any
template to the current workspace). Templates live in a SHARED store —
$DSH_HOME/maintainer-doc-guard-templates/ — precisely so a set of iron rules can be
carried into a workspace whose conventions.md already exists, which the seed can
never do. Both directions go through the panel (the operator's channel), never
through the model's gate.
Five notices added on top of the five parts. All five are ON by default, all five observe only and never block, and all five can be switched from the right-sidebar tab (process-level, like the strict-mode switch) — they are deliberately NOT new settings-card fields, because that card has a hard ceiling of ten fields.
On-demand ledger hits. lessons.md is listed alongside the operator's
documents but is NOT force-injected: a line is delivered only when its 触发
field matches the live call — same tool, plus at least one path/command fragment
present in this call (substring match; * splits a fragment into AND-ed pieces).
One hit per line per session. Cross-conversation reuse comes from the
workspace-level <cwd>/.dsh-maintainer-doc-guard/lessons.md, not from re-stating
it every step.
Landing check for deleted numbers. For edit, a net decrease in number runs
between old_string and new_string is the objective trace of the observed
failure — deleting the sentence that carried a number or a conclusion. The notice
asks for the landing-place check before that deletion stands.
Completion standard, once per artifact. After a successful write to a code
artifact (.mjs/.cjs/.js/.jsx/.ts/.tsx/.ps1/.sh/.py/.json/
.yml), the next call is reminded once to state how it was verified and the
actual reading — not to mark it done.
Auto-induction. A failed call on a managed tool, followed by a successful call on the SAME target, appends one line to the ledger: the failure's own error text as the cause, and which argument keys changed as the fix. Only observed facts; shell calls are excluded (a command string has no stable "same target"); capped at three lines per session; deduped by full line.
Ledger hits and auto-induction are OFF inside a delegated child by default:
isChildSession(session) && !subagentOptIn(config.inSubagents) returns "no
notice / no booking", the same policy as the document reminder (opt a child in
with inSubagents). The number-landing check and the completion reminder have no
child check at all, so they DO run in children — and because all four share one
process-level switch group, a parent cannot quiet its children independently.
What it buys. The model stops losing the thread: it stays on topic, and it stops "solving" a long task with a grand detour that goes nowhere. In practice that is a noticeably more comfortable long-run experience than 4.0.
What it costs — the honest list.
edit + index.js fires for any path containing index.js, including edits to
unrelated files.cordis.patch.yml.A delegated child's report is input, not a conclusion. After a subagent or
subagent_fork result lands, the parent is reminded once to re-run the key
readings before adopting a number, a bound or a counterexample; to check the work
tree (git status --short), because a child told to be read-only can still create
files — that is an instruction, not a sandbox; and to specify the output path
positively next time instead of only forbidding writes. Armed on the result and
delivered on the next tool call, like the completion reminder.
Row config (all optional):
| key | default | meaning |
|---|---|---|
enabled | true | turn the section off without removing the row |
docs | ["plan.md","conventions.md","stack.md","state.md","maintainer/README.md"] | document names to probe |
docsDir | ".dsh-maintainer-doc-guard" | folder (relative to each workspace) that holds the documents; created on first access |
onlyWhenPresent | false | when true, stay silent unless at least one document exists |
inSubagents | false | also inject the document reminder into delegated children (true opts in) |
order | 100 | section sort order (after the persona prefix at 0) |
walkUp | 6 | ancestor levels to walk looking for the project root |
projectMarkers | [".git"] | root markers for the walk-up |
title / intro | built-in | override the reminder's heading / body text |
Gate config lives under gate: and is also all optional:
| key | default | meaning |
|---|---|---|
gate.enabled | true | mount the tools/pre-execute guard at all |
gate.dryRun | false | observe only: count what it would deny, never block |
gate.tools | ["write","edit"] | tool names the gate examines |
gate.guardedGlobs | [".dsh/profiles/", ".dsh/skills/", ".dsh/.agent-presets/", "node_modules/@deepseek-ai/"] | POSIX substrings of the resolved target path that make it guarded |
gate.exemptGlobs | [] | POSIX substrings that exempt a target even if guarded |
gate.maxDeniesPerTarget | 2 | denials for one target before the gate gives up and lets it through |
gate.maxCandidates | 3 | precedents listed in one denial |
gate.scanLevels | 4 | ancestor levels scanned for sibling precedents |
gate.entriesPerLevel | 400 | directory entries examined per level (caps node_modules scans) |
Example — observe before enforcing:
- id: maintainer-doc-guard
config:
gate:
dryRun: true
The gate is off for anything outside guardedGlobs, so ordinary project files
(plan.md, source, notes, scratch scripts) are never touched by it.
Each document is probed in the session working directory first, then in the project root, so a document kept at the repo root is found even from a nested cwd.
Everything this plugin copies from outside itself — the quoted user instruction, document names,
title/introoverrides — is defused for the harness's{{variable}}interpolation before it reaches the prompt (the two-character opener and closer are split apart), because rendering throws on an unregistered reference. A user who types{{x}}therefore cannot break prompt assembly for the rest of the session.
Anchor config lives under anchor: and is also all optional:
| key | default | meaning |
|---|---|---|
anchor.enabled | true | render the standing objective section at all |
anchor.inSubagents | false | also render inside delegated children, quoting the child's own delegation under child-specific wording (true opts in) |
anchor.order | 10150 | section sort order — late on purpose, so the section that changes on every user message invalidates as little prefix as possible |
anchor.maxChars | 800 | clip the quoted instruction, marking the cut with … (truncated) |
anchor.title / anchor.intro | built-in | override the heading / the framing sentence |
anchor.priorities | the three-step rule | the precedence list, rendered as a numbered list after the quote |
anchor.childTitle / anchor.childIntro / anchor.childPriorities | built-in | override all three for the child reading — each defaults to wording that says "your delegated task" rather than "the user's latest instruction" |
Nudge config lives under nudge: and is also all optional:
| key | default | meaning |
|---|---|---|
nudge.enabled | true | stage the intent judgement at all; false denies the first offence, as 0.4 did |
nudge.dryRun | false | observe only: count what it would inject, inject nothing |
nudge.grace | 1 | unexplained actions allowed through with a reminder before any denial |
nudge.maxPerTurn | 3 | reminders per turn before it stops injecting (the deny budget is separate) |
nudge.afterSteps | 12 | step count that triggers the once-per-turn drift check; 0 disables it |
Both gates' most-touched switches are also editable at runtime, in the harness's own settings UI under 设置 → 插件 → 插件配置 ("Plugin configuration"), with no restart:
| settings field | maps to | default |
|---|---|---|
gateEnabled | gate.enabled | true |
gateDryRun | gate.dryRun | false |
gateMaxDeniesPerTarget | gate.maxDeniesPerTarget | 2 |
intentEnabled | intent.enabled | true |
intentDryRun | intent.dryRun | false |
intentMaxBlocksPerTurn | intent.maxBlocksPerTurn | 1 |
intentMinChars | intent.minChars | 24 |
anchorEnabled | anchor.enabled | true |
nudgeGrace | nudge.grace | 1 |
nudgeAfterSteps | nudge.afterSteps | 12 |
The card lists the ten fields in guard order — precedent gate, intent gate, objective anchor, then drift correction — each with its own label and hint, so it is never ambiguous which guard a switch belongs to.
How it works, and what it deliberately does not do:
maintainer-doc-guard) through
ctx.settings.installSection(...) — guarded by ctx.inject(['settings'], …),
so a deployment without a settings provider simply keeps using the composition
config instead. Nothing here is required for the gate to work.$DSH_HOME/settings.yaml),
which is hot-reloaded; installSection falls ba