DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Maintainer Doc Guard — DeepSeek Harness 插件(DSH Plugin)
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins
M

dsh-maintainer-doc-guard

Maintainer Doc Guard

让长时间运行的代理始终围绕用户的实际请求作答:通过持续性的维护者文档提醒、两个执行前检查(编辑基础设施前先读取同名基准;说明副作用步骤的用途)、引用用户最新指令并包含明确优先级规则的目标锚点,以及在阻止操作前先进行纠正的提醒阶段。

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add github:huxin7735-collab/dsh-maintainer-doc-guard#04c125705c716375cb19a6aebd6749c00ea39523
README兼容性版本

说明

让长时间运行的代理始终围绕用户的实际请求作答:通过持续性的维护者文档提醒、两个执行前检查(编辑基础设施前先读取同名基准;说明副作用步骤的用途)、引用用户最新指令并包含明确优先级规则的目标锚点,以及在阻止操作前先进行纠正的提醒阶段。具体包括:(1) 一个持续生效的系统提示词部分,要求模型在每次操作前读取工作区维护者文档(plan.md / conventions.md / stack.md / state.md / maintainer/README.md),避免小模型的上下文压缩丢失长期项目记忆。(2) 一个 `tools/pre-execute` 基准检查:在受保护的基础设施区域内进行写入或编辑前,必须先读取一个可用的同名基准,否则拒绝操作。(3) 一个 `tools/pre-execute` 意图检查:当模型未说明副作用调用用途时,对该轮中的副作用调用进行约束。(4) 一个目标锚点:将用户的最新指令原样引用到第二个提示词部分,并明确优先级规则(用户的最新指令 > 你自己上次陈述的计划 > 你自行发现的线索),防止长时间运行的回合悄然重新定义任务。(5) 一个提醒阶段,在阻止操作前先进行纠正——第一次未解释的操作只会注入提醒,供模型在下一步读取;一次长回合中只触发一次偏离提醒;只有重复违规才会被拒绝。此外,还提供一个右侧边栏标签页,用于查看和编辑这些文档,并在运行时设置页面(设置 → 插件 → 插件配置)中公开十个运行时参数。

兼容性与来源证明

Maintainer Doc Guard 以 dsh-maintainer-doc-guard 发布,当前版本为 0.5.2。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
web
发布来源
github
Registry 更新时间
2026/9/20

版本

0.5.2stable
2026/9/20
0.5.1stable
2026/9/20

相关插件

正在加载相关插件…

最新版
0.5.2
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
未提供
文件数
未提供
Surface
web
许可证
MIT
发布源
github
GitHub
★ 0
周下载
0
最近提交
2026/9/20
查看源码 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

相关插件

继续浏览 productivity-workflow 分类下经过校验的插件。

Client Ui Task Board@linxin666/dsh-client-ui-task-board面向 DSH Web GUI 的主机权威任务面板,支持实际会话执行、主机 cron 调度以及可选的跨平台空闲睡眠保护;以挂载方式提供,无需修改 DSH 源代码。Web All@linxin666/dsh-web-allDSH Web UI 全家桶聚合插件:一键安装全部功能插件(task-board / git-graph / pet / remote-web-ui / web-ui-settings / skin-center / community-plugins / compat shim)。compat 桥接层已并入本包(src/client),无需独立 compat npm 包。Rewind Plugindsh-rewind-plugin同窗口内对话回退并恢复工作区文件Acp App@deepseek-ai/dsh-acp-appdsh ACP 配置文件包:基于 dsh-base 的仅限自动化的 JSON-RPC stdio 和进程生命周期管理

README

dsh-maintainer-doc-guard

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.

Why

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.

What this plugin does

It has five parts.

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:

  • Monotonic. The listener delegates downstream first and only then applies its own deny, so it can never force an allow another guard refused.
  • Bounded. After gate.maxDeniesPerTarget denials for one target it lets the call through and logs — a stubborn model cannot deadlock its own turn.
  • Never fatal. Every path is wrapped; an internal failure degrades to "no decision" and warns. A guard bug must not break tool dispatch, exactly as a section bug must not break prompt assembly.
  • Per-session, in-memory. Observed reads are keyed by the opaque agent.session identity and never persist, so a recovered session re-reads its precedent — the same semantics dsh-fs-observation-policy uses.
  • Coarse by design. "Same basename" is a proxy for "a working analogue of this kind of file", not a proof. That keeps false denials rare, which is the right trade for a speed bump whose value is forcing one conscious look.

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:

  • It is a separate section at a late order (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.
  • Only a message the user actually authored becomes the anchor. The guard's own notices and the loop's runtime-context snapshot are 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.

Inside a delegated child

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:

  • The delegation is adopted positionally, per lineage. A child's opening message is delivered through the ordinary user-message path, so 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.
  • Reach is one switch per contribution. The document reminder and the anchor are separately switchable (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 happenscost
1st unexplained actiona reminder is injected (agent.inject) and the call runs0 round trips
2nddenied, with the request to say what the step is for1 round trip
3rddenied again, until intent.maxBlocksPerTurn is spent1 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:

  • the step-purpose nudge, injected instead of the first denial;
  • the drift check, injected once when a turn has run 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.

Configuration

Row config (all optional):

keydefaultmeaning
enabledtrueturn 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
onlyWhenPresentfalsewhen true, stay silent unless at least one document exists
inSubagentsfalsealso inject the document reminder into delegated children (true opts in)
order100section sort order (after the persona prefix at 0)
walkUp6ancestor levels to walk looking for the project root
projectMarkers[".git"]root markers for the walk-up
title / introbuilt-inoverride the reminder's heading / body text

Gate config lives under gate: and is also all optional:

keydefaultmeaning
gate.enabledtruemount the tools/pre-execute guard at all
gate.dryRunfalseobserve 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.maxDeniesPerTarget2denials for one target before the gate gives up and lets it through
gate.maxCandidates3precedents listed in one denial
gate.scanLevels4ancestor levels scanned for sibling precedents
gate.entriesPerLevel400directory 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 / intro overrides — 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:

keydefaultmeaning
anchor.enabledtruerender the standing objective section at all
anchor.inSubagentsfalsealso render inside delegated children, quoting the child's own delegation under child-specific wording (true opts in)
anchor.order10150section sort order — late on purpose, so the section that changes on every user message invalidates as little prefix as possible
anchor.maxChars800clip the quoted instruction, marking the cut with … (truncated)
anchor.title / anchor.introbuilt-inoverride the heading / the framing sentence
anchor.prioritiesthe three-step rulethe precedence list, rendered as a numbered list after the quote
anchor.childTitle / anchor.childIntro / anchor.childPrioritiesbuilt-inoverride 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:

keydefaultmeaning
nudge.enabledtruestage the intent judgement at all; false denies the first offence, as 0.4 did
nudge.dryRunfalseobserve only: count what it would inject, inject nothing
nudge.grace1unexplained actions allowed through with a reminder before any denial
nudge.maxPerTurn3reminders per turn before it stops injecting (the deny budget is separate)
nudge.afterSteps12step count that triggers the once-per-turn drift check; 0 disables it

Settings page — the ten live knobs

Both gates' most-touched switches are also editable at runtime, in the harness's own settings UI under 设置 → 插件 → 插件配置 ("Plugin configuration"), with no restart:

settings fieldmaps todefault
gateEnabledgate.enabledtrue
gateDryRungate.dryRunfalse
gateMaxDeniesPerTargetgate.maxDeniesPerTarget2
intentEnabledintent.enabledtrue
intentDryRunintent.dryRunfalse
intentMaxBlocksPerTurnintent.maxBlocksPerTurn1
intentMinCharsintent.minChars24
anchorEnabledanchor.enabledtrue
nudgeGracenudge.grace1
nudgeAfterStepsnudge.afterSteps12

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:

  • The host half registers a settings namespace (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.
  • Values persist to the harness settings document ($DSH_HOME/settings.yaml), which is hot-reloaded; installSection falls back to the composition entry if the provider detaches, so the overlay can never go stale.
  • The gate reads its config per tool call, never at mount time — that is what makes a flipped switch take effect immediately. Mounting the listener only when gate.enabled was true would have frozen the switch at load. The anchor re-reads its config per assembly for the same reason.
  • Only these ten knobs are exposed. gate.tools, gate.guardedGlobs, gate.exemptGlobs, gate.scanLevels, intent.tools, anchor.order, anchor.maxChars, anchor.priorities, inSubagents, anchor.inSubagents and friends are deployment decisions, not preferences, and stay in the composition entry — as does everything about the reminder itself. anchor.order in particular is fixed at registration: changing it means editing the composition entry and restarting. The ten-field ceiling is hard: the provider accepts one shared-namespace card of at most ten knobs, so an eleventh makes the whole card fail to mount.
  • The card is dispatched by the namespace key, so the section only renders it once the host has actually served the namespace. When no provider is mounted the card says so and stays read-only rather than pretending a write landed.

gate.dryRun, intent.dryRun and nudge.dryRun are all faithful simulations: a would-be denial still spends one slot of its budget, so a day of observation predicts exactly what enforcement would have done (including when it would have given up).

intentMinChars is read at judgement time, not at recording time. A turn records how many characters of explanation it has accumulated, and the threshold is applied when a tool call is about to dispatch — so dragging the slider re-tunes the current turn, not just the next one.

The intent gate

The second gate answers a different question than the precedent gate. The precedent gate asks "did you read the precedent before writing here?"; this one asks "did you say what this step is for before you ran it?" It exists because the expensive failure mode is not a bad write — it is acting before checking whether the action was worth taking at all, which burns tokens on work nobody asked for.

How it decides:

  • The host subscribes to session/event (a non-vetoing observer seam) and records, per turn, how much assistant prose the turn has produced and how many steps it has taken.
  • At tools/pre-execute — the only seam that can stop a call before dispatch — a call to one of intent.tools meets the staged judgement described in 5. A nudge stage above: a reminder first, a denial only once the reminder has been ignored, and never more denials than intent.maxBlocksPerTurn.
- id: maintainer-doc-guard
  config:
    intent:
      enabled: true
      dryRun: false
      tools: ["write", "edit", "bash", "pwsh"]
      maxBlocksPerTurn: 1
      minChars: 24
keydefaultmeaning
intent.enabledtruemount the intent gate at all
intent.dryRunfalseobserve only: count what it would deny, never block
intent.tools["write","edit","bash","pwsh"]side-effecting tools only — gating cheap exploration buys nothing
intent.maxBlocksPerTurn1denials per turn before it gives up and lets the turn run
intent.minChars24characters of assistant prose that count as "said what this is for"

The denial is a prompt, not a wall. It demands three things of the next attempt: what the step achieves, why it has to happen now, and what result is expected — and states plainly that if the value of the step cannot be stated, the step should not run and the user should be asked what they actually want. Its injected cousin says the same thing while the call is still allowed through, so a model that takes the reminder never meets the wall at all.

Three safety properties are worth naming:

  • Fail-open on a missing channel. If a tool call is already dispatching while the plugin has observed zero assistant messages this turn, the observation channel is not reaching it (a changed event shape, a re-ordered harness). It disarms itself for that turn and counts intentDisarmed — surfaced in the card as ⚠未观测到通道(已自停 N) — rather than denying blind. A gate that blocks on a broken sensor is worse than no gate.
  • Independent budgets. The intent gate runs first, but a denial downstream (the precedent gate, or the harness's own policy) does not spend the intent budget. One turn can therefore absorb one intent denial and one precedent denial without the second being swallowed. The nudge budget is a third, separate counter again.
  • Bounded. intent.maxBlocksPerTurn (default 1) is what keeps a stubborn model from deadlocking its own turn: once it is spent, the turn runs regardless and the give-up is counted as intentGaveUp. Raising it trades a longer stall for a firmer demand — 3 is a reasonable "insist" setting; there is no setting that blocks a turn indefinitely, by design.

The gate is off for any tool outside intent.tools, so read, glob, grep and every read-only probe are never touched by it.

UI — the right-sidebar tab

The bundle also ships a browser half (./client.js) that adds a 维护者文档 tab to the right sidebar:

  • a tab strip across the configured docs (default: the five above);
  • one editor per document — the raw markdown in a text area, saved with a click;
  • documents that do not exist yet are marked ·未建, so the panel doubles as a checklist of what still needs writing;
  • the header shows the working directory the panel resolved — the open session's cwd, with the host falling back to the cwd recorded at the last prompt assembly.

It is hand-written and build-free, and requires only the platform seed module react — deliberately, so it cannot trip the require(...) missed the module table failure that rc.*-era client bundles hit on 0.1.5.

Data plane (host routes, registered only when the host has a web server):

methodpathpayload
GET/dsh-maintainer-doc-guard/docsquery cwd=<abs path> (optional). Also returns a gate object — see below — so you can confirm the guards are actually firing.
PUT/dsh-maintainer-doc-guard/docsbody {cwd, name, content}
GET/dsh-maintainer-doc-guard/settingsthe settings card's read model: served, namespace, value, overridden (per-field presence in the user layer), revision, fields, plus the same gate counters
PUT/dsh-maintainer-doc-guard/settingsbody {patch:{…}} for edits, {unset:["gateDryRun", …]} to revert a field to the composition value. Unknown fields and wrong types are dropped; without a provider it answers 409 with the YAML fallback instead of pretending

The gate object carries every guard's counters:

countermeaning
seentool calls the gate pipeline examined
deniedprecedent denials issued
gaveUpprecedent gate exhausted maxDeniesPerTarget for a target
wouldDenygate.dryRun only: denials that would have happened
readsreads recorded as precedents
lastDenied{tool, target} of the most recent precedent denial
intentBlockedintent denials issued
intentWouldBlockintent.dryRun only: denials that would have happened
intentGaveUpintent gate exhausted maxBlocksPerTurn for a turn
intentDisarmedturns where the observation channel was missing and it stood down
lastIntentBlocked{tool, turn, step} of the most recent intent denial
nudgedreminders actually delivered to the model
nudgeWouldSendnudge.dryRun only: reminders that would have been delivered
nudgeCappedturn hit nudge.maxPerTurn, so a reminder was withheld
nudgeFaileddelivery failed (no agent.inject) — the call went through, uncorrected
lastNudge{tool, tag} of the most recent reminder; tag is intent or drift

The three counters that matter when tuning are nudged (is the cheap stage working?), intentBlocked (is the expensive stage still needed?), and nudgeFailed (is the reminder channel actually wired in this deployment — a non-zero value there means the staged path has silently degraded to "allow").

name must be one of the configured docs; writes are confined to cwd (path traversal is rejected) and parent directories are created on demand.

Open the tab from the right sidebar's "new tab" menu. Restart dsh after changing this plugin — bundles are not hot-loaded.

Install

Requires dsh 0.1.5-rc.* and Node >= 20. The plugin is build-free: lib/ is the shipped source, there is no compile step, and it depends on nothing but the harness itself.

Quick install (recommended)

dsh plugin --profile web add github:huxin7735-collab/dsh-maintainer-doc-guard

Then restart dsh (dsh web) and refresh the page. This is the command the plugin marketplaces expect in a README; substitute your own profile name for web if you run a different one.

The marketplace's one-click install uses this same spec. Nothing else needs to be written by hand.

Manual install (if you prefer to vendor the source)

1. Get the code anywhere on disk.

git clone https://github.com/huxin7735-collab/dsh-maintainer-doc-guard.git

2. Declare it in the profile — $DSH_HOME/profiles/web/package.json:

{
  "dependencies": {
    "dsh-maintainer-doc-guard": "link:/abs/path/to/dsh-maintainer-doc-guard"
  },
  "dsh": {
    "profile": {
      "bundles": [
        // …the bundles already listed…
        "dsh-maintainer-doc-guard"
      ]
    }
  }
}

Both halves matter: dependencies makes it resolvable, and dsh.profile.bundles is what actually loads it. A package that resolves but is not in the bundle list mounts nothing.

Use link: with an absolute path, never file:. With nodeLinker: hoisted, pnpm resolves a file: directory to a symlink into the source tree instead of a self-contained package — that either breaks resolution or silently drops the bundle from the graph.

3. Install and restart.

cd "$DSH_HOME/profiles/web" && pnpm install

Then restart dsh: bundles are not hot-loaded. A window close-reopen is often not a restart either — if the shell log shows adopted orphan service, it reused the old process and you are still on the old config. Kill the service PID and start again.

4. Verify. dsh web --dump-config should list the maintainer-doc-guard row exactly once, and 设置 → 插件 → 插件配置 should show the card with ten knobs. If the card renders read-only and says no provider is mounted, the harness has no settings service — every knob then falls back to this composition entry and the plugin still works, just without live tuning.

To confirm the anchor half is live without waiting for a drift to happen: send a message, then read GET /dsh-maintainer-doc-guard/settings and check that gate.nudged moves when you next take an unexplained action. gate.nudgeFailed staying at 0 is what tells you the reminder channel is genuinely wired in.

Upgrading

git pull into the same directory and restart dsh. Nothing is written into your profile beyond those two declarations, so there is no migration step.

Uninstalling

Drop the dsh.profile.bundles entry (that is what unmounts it), then the dependencies line, then pnpm install. The documents it manages — plan.md, conventions.md, stack.md, state.md, maintainer/README.md (now under .dsh-maintainer-doc-guard/) — are ordinary workspace files and are left untouched.

Note on this repository

This plugin was developed against a specific deployment's cordis.patch.yml, so the composition example above is the generic form. The guarded areas, the document names and the tuning all live in that one row's config: block, which ships in cordis.patch.yml here — read it before wiring the plugin into a harness that guards different directories.

Release notes

See CHANGELOG.md for what changed in each version, including the design notes behind the objective anchor and the nudge stage, the reason a hard-stop-the-turn escalation was deliberately not implemented, and what remains unverified.