@mstar-harness/dsh
English | [中文](README.zh.md)
Morning Star as a first-class dsh (DeepSeek Harness) host — a cordis function plugin that mounts the mstar engine in-process, implements the engine HostAdapter (host: 'dsh'), guards {HARNESS_DIR}/status.json writes (validate + advisory; repair-escape under hard), blocks disallowed subagent dispatches when Enforcement: hard is on, lints SKILL.md writes under the mounted skill roots, mounts the mstar skills/ mirror through the dsh skill-filesystem provider (single canonical mount), and appends a durable mstar-engine-status catalog row to every composed agent step. Boot with a dsh Loader app; everything acts through the seam's refusal/advisory channels, never by patching the tools.
Usage
How a dsh app consumes the plugin — install paths, configuration, what mounts at boot, and the enforcement semantics.
Install paths
The package ships as a workspace package (workspaces: ["packages/*"]) with the engine bundled into dist/ at build time (bun run build; dist is gitignored). The install path is the profile bundle, added to the shipped web profile (dsh --profile web — the ready-made web app profile, dsh web), through the dsh.bundle.patch manifest — a patch layer mounted over the dsh-base defaults:
One-command CLI entry (recommended) — npx @mstar-harness/cli init --target dsh installs the full capability in one go: it runs the two dsh plugin --profile web add installs below in order (the mstar bundle first, then dsh-llm-fallbacks), and npx @mstar-harness/cli doctor --target dsh reports each plugin row as uninstalled / disabled / mounted. It is the same two-command install, orchestrated; --no-fallbacks skips the second row (and with it the seeded roles — see What you get below).
(a) Registry install (published form) — the npm package carries the built dist/ (no build step on install):
dsh plugin --profile web add @mstar-harness/dsh(b) Local checkout install (dev) — the package checkout itself, for iterating on the plugin:
cd <repo>/packages/dsh
dsh plugin --profile web add .dsh plugin --profile <name> add <spec> initializes the profile on first use (web starts from the shipped template: @deepseek-ai/dsh-base + @deepseek-ai/dsh-web-app), forwards <spec> to pnpm in the profile directory, and reconciles the profile's dsh.profile.bundles layer list from the installed state: any dependency whose package.json declares dsh.bundle joins the layer stack. Relative specs (., file:/link:) anchor to the invoking directory, so add . runs from the package checkout; pnpm must be on PATH. A local checkout needs a prior bun run build (the prepare script is intentionally NOT used — the monorepo builds packages explicitly, matching cli/opencode).
(c) Optional capability: dsh-llm-fallbacks (second command) — the role-based subagent configuration capability (see LLM fallbacks integration) is a SEPARATE plugin row and must be installed with its own command:
dsh plugin --profile web add dsh-llm-fallbacksThe two-command install is the contract — folding a dsh-llm-fallbacks row into this bundle's patch is explicitly rejected (roadmap §8.3 F4): the loader has no insert-if-absent semantics, so a same-id insert is a duplicate loader entry id boot failure (the whole dsh session fails to start), and a different-id insert mounts the plugin twice — two apply() runs with split fallback state (per-context state stores, double listeners, config-override lottery) for anyone who also installs the package directly. Layer order is the reconcile append order: dsh-llm-fallbacks lands after dsh-base/llm-retry (its hard ordering requirement) and after the mstar row. Single-command multi-activation is an upstream feature gap (reconcile dedup or insert-if-absent patch semantics), not actionable from this repo.
What you get with zero configuration — with BOTH rows installed (via the CLI entry or the two commands above), the mstar plugin declares the 13 mode: subagent mstar role seeds (derived from the bundled harness-agents/ mirror, project-manager excluded) into the fallbacks taxonomy at boot: each seeded role's persona defaults to its mirror description plus the mandatory role-loading guidance line, the seeded state stays revertible (the fallbacks/revert-seed gateway / the settings rollback button), and the runtime advisory reports missing ids and persona overrides. The seeds mechanism is B4 (delivered iter-20260816-dsh-seeds-bridges) — the installed-deployment e2e (tests/install-e2e.spec.ts) closes the verification loop: a real init --target dsh install into a temp DSH_HOME, booted from the installed artifacts, asserts all 13 ids present in the effective taxonomy with non-empty personas. Not included: model routing, automatch dispatch, or the dsh TUI.
> Fresh-publish age window: pnpm's minimumReleaseAge gate can make a dsh plugin add <spec> range resolution pick an older published version (without the seeds surface) for up to ~24h after a fresh publish — re-run npx @mstar-harness/cli init --target dsh after the window (or pin the version) to converge on the latest surface.
Configuration
| Key | Type | Default | Meaning | | --- | --- | --- | --- | | harnessDir | string | per-session workspace probe (.mstar/ → .agents/ → .plans/ → plans/, from the session workspace root — never the launch cwd) | Explicit harness root; wins over engine probing. Required for repos whose harness root is not a probed name — e.g. this mstar-workflow repo itself uses .harness/ (maintenance root, deliberately NOT probed); the probe starts from the session workspace root (never the launch cwd) and STOPS there — it never walks above the session workspace, so a harness dir above it (e.g. a global ~/.mstar) is never adopted. | | enforcement | 'hard' \| 'soft' | compass, else warn-only | Per-deployment override. Precedence: Config wins; else the Assignment's own Enforcement: hard header flag (dispatch gate only); else the iteration compass frontmatter; else warn-only. Config soft is the ONLY local rollback — an Assignment-level soft does NOT override a hard compass. | | dispatchTools | string[] | ['subagent', 'subagent_fork'] | Delegation tool names the dispatch gate matches — the dsh preset's TWO delegation tools, subagent and its fork sibling subagent_fork (both carry Assignment-shaped { description, prompt } args; a toolName config may rename instances). | | dispatchBinding | string | unset (precheck skipped) | The dispatching agent's own harness role; an Assignment whose Execute as equals it is self-recursion. | | roleMap | Record<string, string> | unset | mstar role id (Execute as) → dsh-llm-fallbacks role id. A taxonomy bridge for logging + future rule-driven interop ONLY — never consulted by the decoration (see LLM fallbacks integration). | | rolePersonas | Record<string, string> | unset (bundled mirror default) | mstar role id (Execute as) → persona text; the role-based subagent decoration's override source — a role-matched subagent/start registers the persona as the child's mstar:role-persona system-prompt section; when unset for a role, the bundled harness-agents/ mirror default is used (see LLM fallbacks integration). | | skillRoots | string[] | unset (no custom-root registration) | Additional skill roots registered with the dsh skill-filesystem provider (customSkillDirs semantics — scanned before user roots). Dev-time: the mirror <repo-root>/skills absolute path. | | bundledSkillDir | string | packaged harness-skills/ mirror (package-relative) | Bundled skill root registered with the dsh skill-filesystem provider (bundledSkillDir semantics — scanned last, trusted). Defaults to the package's OWN harness-skills/ mirror (synced by bundle-assets; gitignored) — package-relative, NOT cwd-anchored. An explicit value wins. | | catalogTtlMs | number | 60000 | Pre-step catalog cache refresh interval (ms): how often the per-workspace unified mstar-engine-status catalog row (watermark + iteration gate + workspace-state digest) re-reads status.json / the compass / the knowledge index. The hot path is a timestamp compare + cache hit between refreshes; a mid-session plan/compass/residual change lands within one interval. | | workflowGate | 'off' \| 'warn' \| 'ask' \| 'hard' | 'warn' | Workflow/ralph gate mode (see Gates → Workflow / ralph gate). off = pass-through with no verdict row; warn = advisory-only; ask = first-seen names route through the approval waterfall (P-c); hard = policy violations veto before any child starts. Default warn changes NO hard behavior — the gate is advisory-only unless the deployment opts into ask/hard. | | workflowNames | string[] | unset | Workflow name allowlist (P-a): meta.name values treated as KNOWN by the gate. Empty or absent ⇒ every name is unknown (documented — the gate is NOT "allow all" by omission). Ralph calls carry no meta.name — P-a never applies to them. |
bundledSkillDir defaults to the package's OWN harness-skills/ mirror (see Skills mount) — an explicit Config value still wins. A relative override remains cwd-anchored (skill-filesystem join() semantics against the dsh process cwd), so deployments overriding the default should pass an absolute path in the profile layer (see bundle/README.md).
Composed row set
The profile bundle composes the following rows — the registry rows come from the @deepseek-ai/dsh-base layer, and this bundle's patch inserts the mstar row over them with neutral defaults (the row set the full-app e2e fixture boots):
- name: '@deepseek-ai/dsh-skill' # skill registry (ctx.skills) — dsh-base row
- name: '@deepseek-ai/dsh-tools' # tool registry (ctx.tools) — dsh-base row
- name: '@deepseek-ai/dsh-commands' # command registry (ctx.commands) — dsh-base row
- name: '@mstar-harness/dsh' # this bundle's patch insert (config: {} — plugin defaults apply)The registry rows mount before the plugin so ctx.skills / ctx.tools / ctx.commands exist when the mstar gates, seam tools, and bundled commands register.
What the plugin does when mounted
- Status gate —
fs/write-intent+fs/edit-intentlisteners validate{HARNESS_DIR}/status.jsonwrites (enginevalidateStatus+ per-planfindingsCleanupGateover the pre-write document). - Dispatch gate — a
tools/pre-executelistener on the delegation tool(s) validates subagent Assignment text through the engine's singlecomposeDispatchGatecomposition (field gate, anti-recursion precheck, default-branch gate — opencode/omp/CLI parity, so violation codes are identical by construction) plus the dsh lease gate and worktree L1/L2 checks. - Skill-authoring lint —
SKILL.mdwrites under the configured skill roots run the engine skill-authoring lints (lintFrontmatter+lintFiveQuestion). - Seam lints —
DESIGN.md/ audit-plan / knowledge-doc / roles-dir writes under the harness get their artifact-specific engine lints. - Model-facing tools —
mstar_sdd_workspace,mstar_sdd_task_brief,mstar_iteration_gate,mstar_design_md_validate,mstar_audit_validate,mstar_compound_validate,mstar_roles_validateregister onctx.tools. - Bundled commands —
ctx.commandsregistrations for/iteration-start,/iteration-drive,/iteration-loop,/codebase-audit(from the packagedharness-commands/mirror; each declares a frontmatterinputhint so the web client claims/nameand waits for the user's follow-up args instead of executing immediately; handlers steer the command body + user args into the receiving agent). - Pre-step catalog row — every composed agent step appends ONE unified
mstar-engine-statuscatalog message: the watermark (unified mstar version, harness dir, enforcement), the iteration phase-gate section (when a steering compass resolves) and the workspace-state digest (plan registry, open residuals, branch/policy anchors, active leases, knowledge digest, compass direction — when the workspace has astatus.json). The row is digest-gated (injected once per turn, re-injected only when it changed) and shares one TTL-cached per-workspace build (catalogTtlMs, default 60 s).
Enforcement semantics
Warn-only by default: gate violations log and emit advisory events (mstar/status-gate, mstar/dispatch-gate, mstar/skill-lint, seam advisories) and the action proceeds. Enforcement: hard — from the iteration compass frontmatter, the Assignment header, or the plugin Config (enforcement: hard) — escalates violations to a real veto/deny through the cordis refusal channels: subagent dispatch returns PreToolDecision { kind: 'deny', reason } without delegating; status/skill-lint writes are never hard-vetoed because the intent waterfall is content-blind — an already-invalid document is allowed as a repair escape (hard: true, repair: true advisory) so the repairing write can land. Config soft is the only local rollback; hard gates are never a global default.
Gates
Status gate
fs/write-intent + fs/edit-intent listeners (registered prepend so they run before dsh-fs-policy) gate writes to {HARNESS_DIR}/status.json: validateStatus + per-plan findingsCleanupGate over the current on-disk document (parsed exactly once — no TOCTOU double read). The gate never throws: every decision surfaces as the mstar/status-gate advisory and the intent waterfall is delegated via next(). Warn mode (default) logs + emits on violations. Hard mode allows an already-invalid document as a repair escape (error-level log + advisory with hard: true, repair: true) — the intent waterfall carries no incoming content, so a hard veto on an invalid document would deadlock the very write that repairs it. Unexpected internal errors degrade to allow in BOTH modes with a degraded: true advisory (error-containment envelope); the corrupting write itself cannot be vetoed on this seam (see Known Limitations).
Dispatch gate
tools/pre-execute listener on the delegation tool(s): parses the payload's Assignment text and runs the engine's SINGLE dispatch-gate composition (composeDispatchGate — shape guard, validateAssignmentFields, antiRecursionPrecheck, default-branch gate, header-region enforcement; the same composition the opencode/omp/CLI bindings use, so violation codes are identical by construction) over the header region, plus the dsh-side worktree L1/L2 checks and the lease gate. The refusal channel is PreToolDecision { kind: 'deny', reason } returned without calling next(); warn mode logs, emits mstar/dispatch-gate, and delegates. Non-Assignment prompts and non-delegation tools are inert. Engine failures degrade to allow in both modes observably: the catch path emits the plugin-owned advisory with degraded: true + an error log, so a hard deployment can detect a dead control instead of a silent pass. Registered prepend so an earlier-mounted decision can never…
Lease gate
Additive beyond the opencode field set: for writable dispatches whose Assignment declares Execution mode: sdd or whose plan row is InProgress, verifyPlanExecutionLease + dispatch-context comparisons (holder, worktree_path, working_branch) run against {HARNESS_DIR}/status.json. Violations use the dsh-side lease.dispatch.* namespace; read-only roles skip the check entirely. A missing status.json on an sdd dispatch is NOT a silent fail-open: it surfaces lease.dispatch.unverifiable (advisory in warn, deny under hard) — the execution_lease cannot be confirmed without the status file. Non-SDD dispatches keep the degrade-allow (no lease obligation). All Assignment field reads are scoped to the engine assignmentHeaderRegion (body-quoted examples never leak into header fields).
Skill lint gate
fs/write-intent listener scoped to SKILL.md files under the configured skill roots runs the engine skill-authoring lints (lintFrontmatter + lintFiveQuestion — the CLI mstar skill lint combination) on the pre-write on-disk document. The slot is content-blind (the intent waterfall carries only (target, actor)), so: missing file = first create = pass; clean on-disk doc = silent pass; violations in warn mode = advisory + delegate; violations in hard mode = repair escape — the document is ALREADY invalid, so this write may BE the repair (error-level log + hard: true, repair: true advisory with the enforced hardBlocked verdict). Enforcement resolves like the other gates (Config override, else the iteration compass, else warn-only). The gate never throws; read failures and unexpected errors degrade to allow with a degraded: true advisory. The typed hard veto (SkillLintVetoError, code skill-lint.veto) lives on the incoming-document branch (lintSkillWrite) — see Known Limitations for its current wiring.
Workflow / ralph gate
A tools/pre-execute branch (BEFORE the subagent prompt branch) gates the workflow and ralph tool calls — the remaining model-reachable fan-out that carries no Assignment text. It matches the FIXED tool names (workflow / ralph); a renamed workflow instance is out of scope (the name guard is the fixed default). Non-workflow tools are untouched — the subagent branch owns them, semantics unchanged.
Four-tier mode (Config workflowGate, default warn): off (pass-through, no verdict row), warn (advisory-only), ask (first-seen names route through dsh's approval waterfall — {kind:'ask'}, fail-closed upstream; this gate invents no answerer), hard (policy violations veto before any child starts). The policy is the SINGLE decision point — P-b lease attribution runs FIRST and preempts P-a/P-c, then the P-a name allowlist, then P-c first-seen ask.
| Policy | off | warn (default) | ask | hard |
|---|---|---|---|---|
P-b: workspace has an InProgress plan without execution_lease coverage | allow (gate short-circuits off) | warn — allowed + advisory (workflow.lease.uncovered) + one warn | warn — allowed + advisory + one warn (the ask channel is for first-seen NAMES, never the workspace red line) | deny — veto before any child starts (workflow.lease.uncovered), reason cites the plan id |
P-a: workflow name ∈ workflowNames (non-empty list) | allow (short-circuit) | allow — no advisory (P-a passes under every mode) | allow — no ask | allow |
| P-a: workflow name unknown (empty/absent list ⇒ every name unknown) | allow (short-circuit) | warn — allowed + advisory (workflow.name.unknown) + one warn | ask (first-seen) → {kind:'ask'}; the cached decision (allow/deny) is reused afterwards — never a re-ask for a resolved name | deny — veto before any child starts (workflow.name.unknown), reason names the name |
ralph (no meta.name — no allowlist identity) | allow (short-circuit) | allow — P-a/P-c NEVER apply | allow — P-a/P-c NEVER apply | allow — P-a/P-c NEVER apply; P-b still applies (deny when uncovered) |
Default-warn rationale. warn is the default so the gate never surprises a deployment into a hard block: it is advisory-only unless the operator opts into ask (human ask channel) or hard (veto). An empty/absent workflowNames makes every name unknown — the gate is not "allow all" by omission, but the default mode turns that into an advisory, not a block.
Interaction with Enforcement: hard. The workflow gate's mode is its OWN Config knob — the cross-cutting Enforcement: hard resolution (compass / Assignment header / Config enforcement) does NOT escalate workflowGate. A hard-enforcement deployment still runs the workflow gate in
…