Skill-driven harness/loop engineering workflow agent plugin.
Install
# from npm (prebuilt)
dsh plugin --profile web add @mstar-harness/dsh
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:btspoony/mstar-harness#path:/packages/dsh
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time. Only install sources you trust, and pin a commit (github:owner/repo#sha).
README
English | 中文
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-fallbacks
The 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
minimumReleaseAgegate can make adsh plugin add <spec>range resolution pick an older published version (without the seeds surface) for up to ~24h after a fresh publish — re-runnpx @mstar-harness/cli init --target dshafter 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 its configured mode (default warn = advisory-only) unless it also sets workflowGate: 'ask' or 'hard'; conversely workflowGate: 'hard' vetoes regardless of the cross-cutting resolution. The two must not be confused: the workflow gate closes the "ungated fan-out under Enforcement: hard" gap only when the deployment opts its mode in.
Fail-open edges (documented, never crash a compliant call). (1) Malformed args — a workflow call without a non-empty string meta.name (after control-char normalization), or a ralph call without a string objective → pass-through + ONE warn under every mode (hard included), and NO verdict row (no policy verdict was produced). A name that is only control characters normalizes to empty → malformed. (2) Unreadable status.json — the P-b status read through the contained resolver path throws → P-b is degraded for that call only with ONE warn; P-a/P-c (name-based, no status dependency) still run. The gate NEVER throws: every read is structural.
Verdict ledger rows. Every gated call records ONE durable workflow-verdict row in the agent-flow ledger (the P2 ledger plan's record path, fully contained — a failing ledger write never reaches the gate): tool (workflow | ralph), workflow (normalized meta.name) or objective, mode (never off — off short-circuits before the policy), and the verdict vocabulary ok / advisory / denied / ask (the ask verdict is the extension: a first-seen ask is itself a gated call, so its row carries ask until the approval waterfall resolves it — "one ledger row per gated call"). Violation codes come from the verdict, never guessed: workflow.name.unknown (P-a) vs workflow.lease.uncovered (P-b). Fail-open paths (malformed args / unreadable status) record nothing; calls with no resolved harness dir skip the row (same silent no-op as the dispatch record path).
P-c answer-observation seam. The gate cannot observe the ask outcome — the tool registry's serviceAsk consumes the approval result internally. The run-start observation IS the answer seam: an ALLOWED ask executes the call → the durable tool-workflow/run-start session event lands in the parent session log → the workflow-ledger consumer records the W-B2 workflow-run row AND caches allow for the run's name into the apply-scoped WorkflowAskCache. A DENIED answer produces no run → no observation → the next same-name call under ask re-asks (fail-closed — no grant evidence, never an invented allow). Cache keys are the normalized (ASCII control chars stripped) uncapped name at BOTH seams — the gate composes meta.name and the observation records runName through the SAME normalizeWorkflowName, so a control-char name (au\u0000dit) can never wedge the cache (asks once, observes under the same key), and a >1024-char name still keys on the full name (the ledger ROW display name is capped separately; the identity axis is never truncated). The cache is apply-scoped — a fresh apply (HMR reload) starts empty, so an unresolved first-seen re-asks per call until an observation (or an explicit record()) lands. A throwing cache record degrades the observation with one warn — the ledger row is already appended, the run is never affected.
P-b preemption. The lease red line runs FIRST: an uncovered InProgress plan in the calling workspace means NO writable fan-out should start children until the plan is recovered — independent of the workflow name (the same red line as the Assignment-keyed lease gate), and it applies to ralph too. Under warn/ask it is advisory-only (allowed + one warn); the ask channel never substitutes for the workspace red line.
LLM fallbacks integration
The optional dsh-llm-fallbacks plugin (installed with the second command — see Install paths) powers role-based subagent configuration: a role-matched subagent dispatch injects the configured persona into the child session's system prompt. The mstar plugin declares it as a registry dependencies entry and imports it type-only — dist/ carries no bundled runtime import of the package (only three string literals naming it — the probe's loader-entry match and the two decoration log lines — never an import; --external dsh-llm-fallbacks in the build stays as the guard for any future library-form import); the interop is a decision-point capability probe, never a module-internals read.
Capability probe
Two views over the mounted state (point-in-time reads at decision points, no cache — loader mounts entries concurrently):
fallbacksService(ctx)— the named cordis service (ctx.get('llm-fallbacks')) while the plugin is applied;undefinedduring HMR/fiber-swap windows even when the loader entry lives (the entry is declarative and outlives a fiber swap).fallbacksMounted(ctx)— capability view, service-first with a loader-entries fallback: the loader entry nameddsh-llm-fallbacksis present, enabled (respectingentry.disabledand group rows), and has a live fiber.
Distinct states: mounted (service applied — full capability), unmounted (no entry: the fallbacks plugin was not installed — the mstar capability degrades, never breaks), disabled (entry present but disabled/grouped — capability off), and the HMR window (entry lives, service absent — the loader fallback covers it).
Role-based decoration
Decoration rides the subagent/start EMIT — not tools/pre-execute (tool args are deep-frozen snapshots; persona/agentOptions come from tool-subagent's own Config, never call args). The synchronous listener resolves the published child via ctx.get('agents')?.get(info.id) and, when the child's seeded task prompt is Assignment-shaped, registers the persona as the child's agent-scoped mstar:role-persona system-prompt section (order 1 — right after the deployment persona slot; unwinds on child disposal). Role identity uses the SAME engine Assignment header grammar as the dispatch gate. Persona lookup is the single personaFor chain — rolePersonas[executeAs] → bundled mirror default → skip — never gated on roleMap or on the fallbacks mounted state.
Zero-config defaults: when rolePersonas has no entry for a role, the persona comes from the bundled harness-agents/ mirror — the repo-root agents/ shells synced by bundle-assets at build (shipped in the published tarball; package-relative resolution, so the bundle works from any launch cwd). The shell file stem is the role id; the default is its frontmatter description block scalar. A shell is eligible when its frontmatter mode is absent or subagent — the primary shell (project-manager) is never offered as a subagent persona default. A default whose description carries the interpolation hazard ({{ paired with a later }}) is warned + skipped at extraction (never a boot throw); a shell edit (mtime change) re-extracts on the next decision-point read. With the mirror absent (bundle-assets not run) lookups are config-only, and a config miss logs one debug per apply.
Unmounted degradation is same-channel + one debug log: no fallbacks entry → the persona is still injected through the identical decoration channel (from the mstar Config or the harness-agents mirror default), with exactly one source-aware debug log; mounted → one info-level interop log carrying the service version. The listener never throws — agents service absent, child unresolvable, non-Assignment or role-unmatched prompts are skip/no-op (the dispatch itself is never affected).
Persona text constraints: dsh system-prompt renders persona text with STRICT {{variable}} interpolation, so persona values MUST NOT contain {{ paired with a later }} (the renderer throws on unknown/malformed references at child prompt assembly — breaking every role-matched dispatch). The Config schema rejects such rolePersonas values at plugin mount with a clear error. A lone {{ with no later }} renders as literal prose; the escape rule is single braces or rewording. Keep persona text concise (a few sentences); bound its length at deployment.
Role seeds + adoption advisory
When the optional dsh-llm-fallbacks capability is mounted (the second install command — see Install paths), the mstar plugin zero-config declares the 13 mode: subagent mstar role seeds into the fallbacks seed registry: persona = the harness-agents/ mirror description (verbatim) + one mandatory-load guide line (Load mstar-roles (references/<role-id>.md) and the role's Required Skill Dependencies before acting.); a persona carrying the {{...}} interpolation hazard is skipped + warned, never declared. The declaration merge-preserves the currently-seeded non-mstar ids from the readback — e.g. the 7 omp-style preset roles the upstream package self-declares at its own apply: upstream declare REPLACES the whole registry, so without preservation a mstar-only batch would strip preset ids of their seeded annotations (rows remain, unseeded). The declaration re-fires idempotently on every fallbacks (re-)apply (HMR/fiber swap) — never from a one-shot latch — so either boot order (presets first or mstar first) converges to the same 20-id fully-seeded registry.
A warn-only advisory pass (logger mstar/fallbacks-advisory) runs once per apply — attempted at apply and, when the fallbacks row mounts after dsh (the loader mounts entries concurrently), once at the first subagent/start decision point. With the service present, the pass FIRST awaits the idempotent re-declare (closing the boot race) then reads the EFFECTIVE state (getEffectiveRoles) and reports, bounded to at most one warn per category:
- missing mstar roles — an mstar id with no effective row (one warn listing them; the id set is derived from the
harness-agents/mirror — never hardcoded; no mirror → the check is skipped with one debug); - persona overrides — an mstar role whose row persona differs from the seed default (one warn naming them + the revert entry: the
fallbacks/revert-seedgateway / the fallbacks settings-card rollback button — the operator override is retained until reverted); - empty personas — rows with a missing/blank persona, only when non-seeded or overridden-empty (one warn naming them);
- legacy keys —
chains,roles.default,roles.list[].label/.description, danglingroles.rules[].rolereferences, via the applied service's owndetectLegacyKeys(one warn citing its semantics); - declare skips/conflicts — local skips (
interpolation/no-persona) + upstream skips/conflicts (codepersona-source— operator override retained) merge into ONE warn; seeded-at-default is silent (one debug naming the ids).
On the loader-fallback path (no service) the structural roles.list read is preserved (missing ids / empty personas; no revert entry — no seeds surface; the legacy-keys check is skipped — never reimplemented). A row config that is absent or not an object, or an unreadable roles.list, skips the pass with one debug log. The advisory never writes the fallbacks config — the only write path is the idempotent seeds re-declare through the released seeds surface (no-delta → no settings write upstream) — never throws, and is not invoked when fallbacks is unmounted: it is a signal, not a gate.
Config surface
| Key | Type | Default | Meaning |
|---|---|---|---|
roleMap |
Record<string, string> |
unset | mstar role id → fallbacks role id. Taxonomy bridge for logging + future rule-driven interop only — unused in decoration by design (persona injection is rolePersonas/mirror-sourced). |
rolePersonas |
Record<string, string> |
unset (bundled mirror default) | mstar role id → persona text; the decoration's override source — a mirror default is used when a role has no entry. |
Service
apply constructs ctx.dshMstar (engine-backed: validateStatus, validateResidual, findingsCleanupGate, resolveCompassEnforcement, resolveHarnessDir, readHarnessVersion, applyEnforcement). Layering: the P1 gates are co-located engine wrappers in this package importing the engine directly (same plugin, engine bundled at build time); ctx.dshMstar is the composition/test façade for inject consumers; the host adapter (below) is the host-facing facade. The engine is the single grammar for both paths. The companion entry @mstar-harness/dsh/invariant reserves package ownership with a documented no-op installer.
Host adapter
The plugin implements the engine HostAdapter contract (host: 'dsh') as DshHostAdapter, exposed as the ctx.dshHostAdapter service. Detection: the engine detectHost maps the dsh delegation tool name — ToolSignal subagent (the model-facing dsh subagent tool) — to 'dsh', evaluated after omp and before kimi/zcode/codex; hybrid sessions lose to earlier rows by fixed order. The adapter routes through the SAME validation cores as the in-plugin gates (one code path): beforeStatusWrite(path, doc) validates the incoming document when the host provides it, else the on-disk fallback (missing file = first create = pass); beforeDispatch(assignment) runs the field + branch + anti-recursion gate with the enforced hardBlocked verdict (the lease gate stays listener-side — it binds the ToolExecution session context the hook does not carry); beforeMerge(lease) is a thin wrapper over the engine validateIntegrationMergeLease (the reservation write into status.json is a P3 seam). log defaults to the dsh ctx logger mstar/host-adapter.
The frozen skill-root form for dsh (engine resolveSkillRoot('dsh', …)) is $DSH_BUNDLED_SKILL_DIR/<name>[/<rel>] — the resolver defines the canonical form used by skill-relative path resolvers (resolveAssetPath); it does NOT mount the directory. Mounting is the plugin's job (next section).
Skills mount
The mstar skills mount through the dsh skill-filesystem provider as a single canonical mount: the plugin registers its configured roots as one provider (providerName: 'mstar', includeDefaultRoots: false — isolated, it must never see the host app's own project/user skills), and the engine form above is the shared skill-root contract. Two Config paths populate it:
| Path | Mechanism | When |
|---|---|---|
| Bundled default | bundledSkillDir defaults to the package's OWN harness-skills/ mirror — the repo-root skills/ (19 mstar-* + pm) synced by bundle-assets at build/postinstall (gitignored), resolved package-relative (not cwd-anchored — works from any launch cwd) |
Published package / any deployment without an override |
| Custom roots | skillRoots / explicit bundledSkillDir → skill-filesystem customSkillDirs / bundledSkillDir entries (explicit values win) |
Local development / tests / deployments with a different mirror |
The packaged mirror is a single canonical mount: skill content lives once in the repo-root skills/ mirror and is synced into the package (like opencode's harness-skills/), so mstar skills stay standalone-usable everywhere. No double-loading: the opencode plugin ships the same skills in its own package, so dsh must mount them ONLY through this single skill-filesystem path.
Dev-time reality: the @deepseek-ai/dsh-skill-filesystem runtime is a peer-stub (contract-mirroring registration, no file watcher), so the mount is verified through real composition against the stub + the actual mirror skills/ frontmatter (engine lintSkillFrontmatter); real-runtime composition (real seam packages, watcher, $DSH_BUNDLED_SKILL_DIR env flow) is the deployment target, not covered by this package's suite.
Commands
The plugin registers the bundled mstar commands (omp/opencode parity surface) on ctx.commands: harness-commands/*.md — the repo-root commands/ mirror (iteration-start, iteration-drive, iteration-loop, codebase-audit) synced by bundle-assets at build/postinstall (gitignored). Each registration reads the command's name/description/input frontmatter; a declared input hint is advertised as input.hint, which flips the dsh web client's decision table from detached bare execution to a leadingInput claim — the menu pick inserts /name into the composer (command-colored token, the hint as ghost text) and the line submits only on Enter, so the user can type follow-up args (the /plan / /goal / /advisor interaction). The handler steers the command body into the receiving agent as a USER-source message (the dsh-plan-mode command precedent — source: { kind: 'user' }, so the model treats the body as a task to execute, not injected context; the dsh-commands "explicitly schedule model-visible work through the receiving Agent" path), appending the user's typed args as a ## User input section when present, and returns a success result. Registration is deferred with ctx.inject(['commands'], …) — the same optional-unit pattern as the tools — so the plugin boots without the commands service; an absent mirror (no bundle-assets run) registers nothing.
Engine seam mapping
Every engine module attaches to a dsh surface — delivered except the lint module's plan/tdd fs gates (deferred; see Known Limitations):
| Engine module | dsh seam | Status |
|---|---|---|
| core (applyEnforcement, GateResult/Severity) | cross-cutting veto/reject | delivered (P1) |
| path (resolveHarnessDir) | harness-dir probing + {HARNESS_DIR}/status.json target matching |
delivered (P1) |
| status (validateStatus, validateResidual, findingsCleanupGate) | fs/write-intent + fs/edit-intent on status.json |
delivered (P1) |
| lease (verifyPlanExecutionLease, validateIntegrationMergeLease) | exec lease: tools/pre-execute (inside the dispatch gate); merge lease: HostAdapter.beforeMerge |
delivered (P1 exec / P3 merge) |
| dispatch (composeDispatchGate, isReadOnlyAssignmentRole, parseAssignmentFields) | tools/pre-execute on the subagent tool (PreToolDecision.deny to block); agent/pre-step advisory |
delivered (P1) |
| host (resolveSkillRoot, HostAdapter) | engine host.ts detection row + plugin adapter (host: 'dsh') |
delivered (P2) |
| skill-authoring (lintFrontmatter, lintFiveQuestion) | skill-filesystem roots + fs/write-intent on SKILL.md |
delivered (P2) |
| lint (lintSkillFrontmatter, planQualityBar, assertSddTddTriple) | not wired — plan/tdd fs gates are a deferral; lintSkillFrontmatter runs only in the skills-mount test suite |
deferred |
| agent catalog | MessageSourceMap mstar-engine-status (model-visible ⟺ logged) |
delivered (P2) |
| sdd (sddWorkspace, taskBrief) | defineTool wrappers registered on ctx.tools |
delivered (P3) |
| iteration (evaluatePhaseGate, parseCompassFrontmatter) | agent/pre-step + iteration gate |
delivered (P3) |
| worktree (l1PreDispatchCheck, l2PreDispatchCheck) | tools/pre-execute L1/L2 (inside the dispatch gate) |
delivered (P3) |
| design-md / audit / compound / roles | fs/write-intent + defineTool wrappers on ctx.tools |
delivered (P3) |
Engine-status catalog
An advisory agent/pre-step waterfall listener appends ONE mstar-engine-status catalog MessageSource to every composed step (the kind/form: 'catalog' contract, mirroring the dsh tool-skill precedent): the model-visible <mstar_engine_status> block renders the watermark fields — mstar version (plugin own manifest; the single-version invariant pins the bundled engine to the same version), harness dir (resolved {HARNESS_DIR}, none when absent), and enforcement (compass mode, soft / hard (compass)) — plus the iteration phase-gate section (when a steering compass + status.json resolve: iteration id, transition, all-plans-done, gate verdict + violation codes — the mstar iteration gate tool result shape) and the workspace-state digest section (when the workspace has a status.json): plans (id(status) registry), residuals (open counts by severity), branch (base → target, spec integration), policy (push policy, worktree mode, control root), leases (active plan execution leases: holder + worktree), knowledge (knowledge-index doc count + categories) and direction (the steering compass's problem-statement one-liner). The listener calls next() first and builds on the delegated decision — it never vetoes a step and never replaces the composed messages. Model-visible ⟺ logged: the durable catalog-form source records the facts it published beside the model-facing prose, so the session log reconstructs the row without re-parsing the block (dsh packages/AGENTS.md). Fiber disposal removes the listener (HMR-safe).
The row is digest-gated: per agent+workspace it is injected once per turn and re-injected only when its rendered text changed — a 20-step turn shows the catalog once, not 20 times. The source shares ONE per-workspace cache entry, built at boot for an explicit harnessDir (else on the workspace's first pre-step) and TTL-refreshed (catalogTtlMs, default 60 s) — the hot path is a timestamp compare + Map lookup between refreshes, and a mid-session plan/compass/residual change lands within one interval.
Agent-flow ledger (workflow rows)
The agent-flow ledger — {HARNESS_DIR}/agent-flow.jsonl, the same JSONL the catalog's state.agentFlow evidence reads — also records workflow / ralph fan-out runs: a session-event consumer (logger mstar/workflow-ledger, registered at apply) maps the FOUR durable tool-workflow/* session events into three new ledger kinds. Source of record is the durable session events appended into the CALLING PARENT session's log (top-level runs only — nested transport calls record nothing upstream), not the in-memory workflow/* emits (roadmap §10.4 N4): the session log is the replayable truth, so the consumer covers it with a cold scan at apply (constructor-seeded events never hit the firehose — firstLiveSeq) plus a live session/event firehose listener, deduped by ONE durable per-session watermark — the session-log seq position — persisted to {HARNESS_DIR}/workflow-ledger-cursors.json (a small bounded sidecar next to the ledger, written atomically temp-file + rename).
tool-workflow/* event |
Ledger row | Fields |
|---|---|---|
run-start |
workflow-run |
runId, name, agent? (the carrying parent session id) |
agent-start |
workflow-agent |
runId, seq (1-based member sequence), label, phase?, childId |
run-end |
workflow-run-end |
runId, stopReason (completed / cancelled / error) |
tool-workflow/agent-end is upstream member bookkeeping with no ledger kind (the member outcome is intentionally not persisted) and is filtered out. Optional fields (agent / phase) are omitted from the serialized line when absent (lossless-JSON discipline); the three kinds share the ledger's AGENT_FLOW_MAX_EVENTS truncation + size gate, and malformed lines narrow to undefined on read (never re-serialized). Display fields (name / label / phase) are length-capped deterministically at the boundary (WORKFLOW_LEDGER_MAX_NAME_LENGTH 1024, WORKFLOW_LEDGER_MAX_LABEL_LENGTH 512 — oversized values truncate with a … marker); id-sized fields (runId / childId, cap 512) SKIP the row when oversized — never truncated into collisions.
A FOURTH kind, workflow-verdict, is written by the workflow/ralph GATE (not this consumer) — one row per gated call (tool, workflow/objective, mode, verdict ok/advisory/denied/ask, violation code) — see Gates → Workflow / ralph gate. Display identity fields (workflow / objective) carry the same 1024-char cap; the verdict's violation code is never guessed (P-a workflow.name.unknown vs P-b workflow.lease.uncovered).
Dedupe + replay scope. The durable watermark is the dedupe mechanism: one row per (runId, kind, seq) across cold+live overlap AND across plugin re-applies / restarts — a re-registration consults the persisted watermark instead of starting with empty cursors. A session created after apply with a constructor-seeded log (resumed / forked conversation — its seeds never publish on the firehose) is cold-scanned once on the upstream session/created event, and the watermark keeps that backfill idempotent too. The watermark sidecar is bounded (per-harness session cap, eviction preferring sessions no longer live) and fully contained: an unreadable/unwritable watermark degrades to in-memory-only with one warn — a restart then re-records (honest under-dedupe, never data loss, never gating).
childId linkage + member counts. The workflow-agent row preserves the published member's childId (the child session id); the run's display name lives on the workflow-run row only, and the panel resolves it for agent/end rows via the window lookup (same runId — a member row itself carries no name). The panel attaches the member COUNT to the workflow-run row (the window's workflow-agent rows for that runId; window-bound — members truncated out of the ≤50-event window are honestly absent, never a 0 guess).
Depth advisory (observe-time). On agent-start, the consumer resolves the child session via sessions.get(childId) and warns when its header.delegationDepth is ≥ 2 — ONCE per run (per-runId latch), logger mstar/workflow-ledger. Observe-time only, never a refusal path: a throwing child read degrades the advisory, never the row or the run.
No-behavior-change guarantee. The consumer is observe-only: ZERO gating — every read and append is try/catch-contained; a failing ledger write never crashes or alters a workflow run; a throwing session read logs one warn and the pass continues. The sessions service is read STRUCTURALLY via ctx.get('sessions') — no runtime dependency on @deepseek-ai/dsh-session.
Mount-order note. The consumer activates only when the sessions service is available at apply — the dsh-session row must mount BEFORE the mstar row (the standard web profile order does). A composition where dsh-session mounts after the plugin (or is absent) degrades silently: ONE debug log (sessions service absent — workflow-ledger consumer disabled) and no rows are recorded — never an error, never a broken run.
Panel visibility. The three workflow rows render in the 事件记录 (Event Log) tab's Agent 流转事件 partition through the existing event-row chrome (no redesign): the summary identity is the run NAME (agent/end rows resolve it via the window lookup; fallback runId → 「未知」), the detail body adds four workflow fields — run-id / name / members / stop-reason (missing → 「—」) — and the expected/settled seats render 「—」 (a workflow row is not a role dispatch — no settle pairing exists, same precedent as settle rows). Unknown kind strings render as GENERIC rows (verbatim kind, no workflow fields) — never dropped, never guessed. The catalog summary counts workflow rows as a DISTINCT workflow bucket (by role: workflow N in the model-facing line) — never folded into dispatch-role counts.
Web client plugin (workflow panel)
The package ships a browser client half for the dsh web profile, discovered
automatically on the already-installed mstar bundle row (package.json
dsh.client declaration + exports["./client"] → dist/client.js — the
upstream web dsh.client discovery scans loader entries and resolves each
client's exports["./client"] into the boot graph) — no separate profile
layer or install step (spec §6.1). The web app serves the bundle at
/plugins/@mstar-harness/dsh/client.js and loads it through the
closure-factory loader handoff (window.__ModuleLoader__.load({ id, factory })).
The client entry registers a conversation.view view-ring tab
(id: 'mstar-workflow', order: 20 — the trajectory precedent shape), labeled
"MStar Workflow" (en) / "MStar 工作流" (zh) through the mstar-panel
locale namespace. The panel is the MStar Workflow layout: a fixed 300px
right sidebar — plans (≤5, time-desc, +N more), open residual findings (≤10,
severity chips, overflow hint), policy (enforcement first, then push /
worktree / control worktree), leases, knowledge, direction — over a bottom
fixed meta dock (version + harness dir; small muted, does not scroll with
the sidebar digest; the former header row was removed), an HTML/CSS zone
dashboard as the main body, and a freshness footer (last-updated HH:MM:SS + the catalog-re-emission refresh note). The branches block moved
out of the sidebar to the iteration zone (plan 20260810-panel-canvas-zones).
Below 860px the sidebar stacks under the main area.
The canvas is a pure render of the latest mstar-engine-status catalog row
(from the useSession snapshot — refresh follows the snapshot, no polling):
the page fills the Tab (no page-level scrolling — the zone container is the
only scroll body) and the zone dashboard (replacing the react-flow cyclic
graph, plan 20260810-panel-canvas-zones) lays out three zones — the
**
…
Links
More in this category
Q00/ouroboros#integrations/dsh-plugin★ 5530
Config-only bundle that mounts Ouroboros through the DSH MCP client, exposing 36 interview, Seed, execution, evaluation, and evolution workflow tools in DSH.
NanmiCoder/dsh-agent-teams★ 529
AgentTeams multi-agent teams.
EthanYoQ/AI-Novel-Writer#dsh-ai-novel-writer★ 385
Installs a dedicated AI novel-writing preset and workbench: revisioned local project assets, a compact side drawer, and native approval-gated single-file changes.
orziz/odai#odai-dsh-plugin★ 87
Profile-wide DSH governance and routing bundle with configurable compaction calls and scoped local semantic memory; compatible with DSH 0.1.0-rc.6 and 0.1.0-rc.7.
icetomoyo/dsh_workflow★ 80
UltraCode-style multi-agent orchestration: a generatable, savable, governable, observable, resumable workflow layer.
titanwings/dsh-automation★ 53
Scheduled coding runs in fresh agent sessions with auditable history.