@jayyuen66/dsh-quality-gate
v0.1.1
Published
dsh 插件:回合收口质量门禁(在 agent/turn-stopping 按项目形态跑 pnpm check / cargo check / ruff check / tsc,未过则注入修复指令开新轮)
Readme
@jayyuen66/dsh-quality-gate
What it does
- A turn-end quality gate: when the current turn edited code or config files, the checks that the project itself declares run before the turn closes; a failure opens a new turn via
agent.followupand injects repair instructions into the model. - One run has exactly three outcomes:
pass(it really finished with exit 0),code-failure(it ran and reported code errors),not-run(it never ran). A check that did not truly complete is never recorded as passed. - It does not edit code, does not block dangerous commands (that is danger-guard's job) and does not write any file.
When it runs, and when it does not
- Trigger: the host event
agent/turn-stopping. Flow: read the incremental event window → take this session's successful edits → walk each file up to the nearest manifest root → detect the gate → run it.- Edits counted:
edit/write/str_replace_editorwrites;viewis read-only and does not count. - Manifest root: searched up from each file, max 10 levels; a monorepo package is upgraded to the workspace root holding
pnpm-workspace.yaml.
- Edits counted:
- Skipped when:
enabled: false; the turn has no checkable edit (only md/txt/images/.sh, or edits that were denied or failed).- Subagent sessions (
origin === "subagent"ordelegationDepth > 0); the inbox already has pending items (the gate does not talk over the user). - The root yields no gate; the turn signal is aborted or the plugin is uninstalled.
- With no shell service in the host it warns once and passes through; plan mode is not exempt — a planning turn that touched code is gated the same way.
Install
dsh plugin --profile web add @jayyuen66/dsh-quality-gate- The packages live on the public npm registry, so installs need no credentials.
- Requires a host at
@deepseek-ai/dsh0.2.1-alpha.2 or newer (engines.dsh); the plugin is compiled against that host's type surface. - The runtime dependency
@jayyuen66/dsh-plugin-sharedmust already be on the same registry, otherwise the whole set 404s. @deepseek-ai/schemasteryis resolved from the host, not installed by this package: the host's own fork is the only one whoseresolvewraps.volatile()fields into live references, so a publicschemasterywould turn "edit a setting" into "restart to apply".- Installing a local directory needs pnpm ≥ 12.3.0 (12.0.0 has a known bug in that path).
Enabling it in dsh
- Loading is driven by this package's
cordis.patch.yml(pointed at bydsh.bundle.patchinpackage.json), which inserts one line- id: quality-gatewithnameset to this package;dsh plugin --profile web add/removemaintains that line. - The host half hard-depends only on
settings(default.inject = ["settings"]);shell/sandboxPolicy/lessonLoop/credentials/sessionQueryare all optionalctx.getreads, so a missing one never stops the plugin from loading. - Precedence: card runtime value > the line's
config:> built-in default. - The client half (
dsh.client.platform = web,immediately: true) injects a card into theplugins.bundle.configslot of the plugin page (key = this package name, whileconfigForms.get()uses the bare entry idquality-gate). - Saving on the card is staged: edits stay in local pending state until Save writes them key by key into settings.
- The write channel hands the card the
Promise<boolean>acceptance bit verbatim: the firstfalsestops the loop, shows thesaveRejectedline and keeps the unwritten changes - swallowing the acceptance bit would let the card claim "saved" while the host had refused. A rejecting write showssaveFailed; Revert discards the pending edits.
- The write channel hands the card the
The commands it runs
Four whitelist commands are locked at type level (only these can reach the execution surface); one per root, priority workspace > Rust > Python > bare TS:
pnpm check: the root has bothpnpm-workspace.yamlandpackage.json, and the root scripts declare a non-emptycheck. Without that declaration it falls back to the next candidate — better no gate than inventing a command that fails deterministically.cargo check(Cargo.toml),ruff check .(pyproject.tomlorruff.toml),npx tsc --noEmit(package.json+tsconfig.json; withouttsconfig.jsonit is not a TS project).- Budget and concurrency: the total budget is
gateBudgetMs; each root gets the smaller ofmax(10s, budget/root count)and the time actually left.- When fewer than 5s remain the rest do not start and are injected as "did not run".
- Gates run sequentially and only one failure is injected at a time.
- Evidence caps: 64 KiB per stream stays in memory, and when truncated a bounded 4 KiB head excerpt is read back from the spill file (the first error is the root cause); the per-session edit accumulator holds 500 files, evicting oldest-first with a warning.
Settings
enabled: boolean, defaulttrue. Off means no turn-end verification at all.maxInjectsPerTurn: 1–5, default2. Repair-injection quota, accumulated per session across turns.gateBudgetMs: 10000–600000, default300000. A timeout injects a failure instead of passing silently.memoryFeedback: boolean, defaultfalse. Whether code-level failures are additionally persisted to the memory gateway.memoryGatewayUrl: string, default empty = disabled; the setting wins, and when empty the envTDAI_GATEWAY_URLis the fallback.memoryGatewayKeyRef: string (a credential ref name), defaultTDAI_GATEWAY_KEY; only the ref name is stored in settings.- Two further fields are non-volatile deployment values: the card has no row for them, they go on the registration line's
config:only (cordis passes plain values, so a change applies on restart).maxRootDepthdefaults to10- that is the "max 10 levels" root search;gateStdoutMaxBytesdefaults to65536- that is the "64 KiB per stream" evidence cap.
Public surface
- No HTTP surface: this package registers no routes or endpoints and offers no interface for external calls.
- To the host: the settings namespace
quality-gateis registered implicitly under 0.1.7 - the namespace IS the entry id, and the editable fields are the.volatile()ones of theConfigthe entry exports (nosettings.registercall is left).- The schema and the line
configstill share that oneConfig. - Only the page policy is declared: in the
svc.inject(["settings"], …)child fiber it callssettings.configure({ auto: false }, svc.fiber)- this package ships its own card, so the host must not generate a second, automatic form. - Injected followups are
role: "user"withsource: { kind: "plugin:quality-gate" }and an id built as the prefixquality-gate-plus a UUID.
- The schema and the line
- Logging goes through the host's named logger (
ctx.logger("quality-gate"), of which this package uses theinfo/warn/errorface), falling back toconsoleonly when that service is absent. - The text injected into the model comes in four kinds; every message carries a
[quality-gate]prefix plus command and root context:- A repair instruction.
- A "could not run" notice.
- A release note once the quota is spent.
- The list of gates skipped for lack of budget, embedded inside the "could not run" notice.
- Both languages ship with the package; the choice follows the official
localesettings namespace preference and defaults to Chinese when that namespace is not registered, taking effect on the next turn.- The card's own text goes through the official client locale runtime:
ctx.locale.register(NS, UI_MESSAGES)hands over the dictionary andctx.locale.bind(NS)yields the getter, so a language switch re-renders the slot with no page reload.
- The card's own text goes through the official client locale runtime:
- To the bus:
ctx.get("lessonLoop")?.report({ source: "quality-gate", category: "gate-failure" | "gate-not-run", cwd, sessionId, turn, signature: the command string, detail: the full evidence, evidence: { root, command, kind } }). - Optional outbound call: a
POST /v2/conversation/addto the address inmemoryGatewayUrl, under a hard 5s timeout.- Headers: a Bearer token and
x-tdai-service-id(env overridable, defaultdefault). - Body:
session_id: "dsh-agent"plus oneassistantmessage, whoseagent_idis the cwd's tail directory name with an 8-hex sha256 prefix (same derivation as lesson-loop's project buckets).
- Headers: a Bearer token and
Data and privacy
- Zero outbound traffic by default:
memoryFeedbackoff plus an empty URL are a double lock — with either empty nothing is pushed and the credential store is not even queried. - When enabled, the payload is only the command, the project root and the full (untruncated) error text, sent to the gateway you configured.
- Failures only warn, at most once per load, and never block the turn.
- The secret itself lives in the host credential service (a credentials file under
$DSH_HOME); a ref name that is not a valid POSIX variable name counts as unconfigured.
- The relation to lesson-loop is optional: if the bus is absent the report is simply dropped and the gate keeps working; installing it only adds lesson persistence, and the
memoryFeedbackswitch does not affect bus reporting.- This package only reads the disk (manifest presence, the root
package.jsonscripts, the head of a spilled truncated file); it writes no file and collects no telemetry.
- This package only reads the disk (manifest presence, the root
FAQ
- Why did my pnpm project not run
pnpm check? Its rootpackage.jsondeclares no non-emptycheckscript — a deliberate trade: a false failure costs more than a missed check. - The gate said "could not run" — is my code broken? No.
- Sandbox policy denial, a runner that will not start, a timeout, a cancellation, exit 126/127,
npxfetching a stub package and an exhausted budget all land here. - The injected text explicitly says "do not change code because of it", nothing is written to memory, and it is reported separately as
gate-not-run.
- Sandbox policy denial, a runner that will not start, a timeout, a cancellation, exit 126/127,
- Can it loop forever? Once the quota is spent it injects one user-visible release note, stops auto-repair, and only counts again after a fully clean pass.
- To stop it temporarily: turn off
enabledon the card, or rundsh plugin --profile web remove @jayyuen66/dsh-quality-gate. - What about code edited by subagents? In-process subagents are skipped and their edits get verified by the parent session's next turn.
- Out-of-process ones (ACP/claude-code/codex) emit no dsh events, so they are outside the gate by construction.
- Licensed MIT; the repository is in
repository.urlofpackage.json.
