# dsh host reference

Load when **`mstar-host`** detection resolves **dsh** (DeepSeek Harness — session
has the **`subagent`** model-facing delegation tool, the `@deepseek-ai/dsh`
cordis plugin stack; `@mstar-harness/dsh` mounted via the `web` profile bundle
or a custom profile).

## dsh-only context

- Plugin markers: **`@mstar-harness/dsh`** (cordis function plugin) + the
  **profile bundle** (`dsh.bundle.patch` manifest) installed into the `web`
  profile via `dsh plugin --profile web add <spec>`. The composed app rows:
  `@deepseek-ai/dsh-skill` (skill registry), `@deepseek-ai/dsh-tools` (tool
  registry), `@deepseek-ai/dsh-commands` (command registry), then the `mstar`
  row.
- Runtime skills: the plugin mounts the packaged **`harness-skills/`** mirror
  (repo `skills/`, synced by `bundle-assets`) through the dsh skill-local
  provider as a **single canonical mount** (`providerName: mstar`). Skills are
  loadable by **name** via `ctx.skills`; the canonical skill-root form is
  `$DSH_BUNDLED_SKILL_DIR/<name>[/<rel>]`.
- Plugin commands: the plugin registers the **seven** bundled
  **`harness-commands/`** commands on `ctx.commands` — **`/iteration-start`**,
  **`/iteration-drive`**, **`/iteration-loop`**, **`/codebase-audit`**,
  **`/amazing-pr-review`**, **`/amazing-e2e-check`**,
  **`/amazing-test-audit`**. Each
  command steers its command body into the receiving agent as a USER-source
  message (the mstar workflow prompt — the model executes it as a task, not
  injected context), returning a success result. The plugin registers **no
  duplicated** mstar command tools: the writable mstar command surface on this
  host is the **MCP** route (§ The native command surface below), and the
  only native tools are the three the MCP route cannot replace.
- **No `sessionStart.skill`** — enter PM manually via the `pm` skill (the
  `mstar-roles` load path), then **Read next** → `mstar-harness-core` →
  `project-manager.md`.
- Model-facing tools: the plugin registers exactly **three** tools on
  `ctx.tools` — **`mstar_plan_bind`** (the durable-slot producer),
  **`mstar_audit_validate`** (no MCP twin) and **`mstar_execution_clear`** (no
  shared command); the membership rule and each tool's unique contribution are
  in § The native command surface. Every other RUNTIME capability is an MCP tool
  of the one shared connection — store-level commands (`store.*`: init, upgrade,
  migrate, backup, activate, retire, execution restore/export) are CLI-only and
  are not published there, because their payload is the whole store; discovery
  marks those family members `transport: "cli"` — published one tool per command
  family
  (`mcp__mstar__mstar_plan`, `mstar_workflow`, `mstar_issue`, `mstar_sdd`,
  `mstar_iteration`, `mstar_design_md`, `mstar_audit`, `mstar_compound`,
  `mstar_roles`, …) with an `action` argument selecting the member command.
- Web client plugin (workflow panel): the same `mstar` bundle row carries a
  browser client half (`dsh.client` + `exports["./client"]`) discovered
  automatically by `ClientModuleHostService` — no separate profile layer or
  install step. It registers a **right-Sidebar page tab type** (`id:
  '@mstar-harness/dsh'`, `kind: 'mstar-workflow'`, one guide-page capsule at
  `order: 20`) labeled **"晨星工作流" / "Morning Star Workflow"**.
  **Data path**: the panel reads the control-root `store.db` through the host's
  `mstar/panelSnapshot` endpoint, polled while the tab is visible
  (`connection.rpc.call('/api', 'mstar/panelSnapshot', { args: { sessionId,
  cwd, workflowId?, iterationId?, projectId? } })`). The host half composes five
  engine dashboard views SEQUENTIALLY (`workflow-detail`, `iteration-detail`,
  `roadmap`, `issues`, `projects` — the store read layer may refresh
  projections, so two views never overlap) into one `PanelSnapshot`. It does NOT
  read the model's last emission: the persisted `mstar-engine` catalog row stays
  as the model-visible anchor, but the panel renders the store. The retired
  `mstar/engineStatus` read method is gone; the `mstar` gateway keeps only the
  picker's durable WRITE (`mstar/selectWorkflow`).
  **Two explicit states, never a flattened empty**: every workspace view answers
  `ok` (the authority answered — `data: null` still means "nothing here"),
  `not-applicable` (nothing was read: no target resolved) or `unavailable` (the
  read failed; the store's own message verbatim). The whole snapshot is
  `unavailable` only when NO view reached the authority, and a workspace with no
  resolvable `{HARNESS_DIR}` answers the separate `no-harness` kind (an inactive
  workspace, not a failed read).
  **The browser supplies `{ sessionId, cwd }` and nothing else**: the workflow
  (the session's durable pick, then the store's single ACTIVE lifecycle), the
  iteration and the project (`_default`, else the single registered one) resolve
  SERVER-side on each read, so the roadmap and the stepper are not permanently
  `not-applicable`.
  **`retained` — the mixed-freshness half, disclosed**: three facts no dashboard
  DTO owns (the iteration gate, the knowledge digest, the resolved workflow
  selection) are REUSED from the apply-scoped catalog payload cache — the same
  entry, key and TTL the pre-step builds — and carry their OWN instant
  (`retained.at`), shown in the freshness footer separately from the snapshot's
  store-read time. A catalog fact can therefore be older than the snapshot, and
  the panel says so instead of implying one freshness.
  **Poll contract**: 5 s while visible, at most ONE request in flight (a slow
  answer never stacks), a 5 s deadline per request, the last good snapshot kept
  while a refresh is out (no flicker), an immediate refetch on a new connection
  generation, and nothing at all while the tab is not visible. The deadline is a
  real cancellation: the carrier's signal reaches the host read, which checks it
  before every view, so a timed-out request stops paying for store reads and
  projection captures.
  **Morning Star Workflow layout**: a narrow-column shell bound to the
  sidebar pane's definite height — exactly three zones: the **section nav**
  (任务迭代 / 项目路线图 / 事件记录; `data-mstar-tab-nav`), the panel-owned
  **single scroll body** (`[data-mstar-scroll]` — the ONLY `overflow-y`
  element in the panel; nothing scrolls horizontally; `data-mstar-graph`
  rides it), and the pinned **meta dock** (version + harness dir; never
  scrolls). The workspace-state digest (plans ≤5 time-desc + `+N more`,
  open residual findings ≤10 with severity chips + overflow hint, policy
  with **enforcement first** then push / worktree / integration worktree,
  leases, knowledge, direction) renders IN FLOW at the end of the scroll
  body, closed by the freshness footer (the store-read `snapshot {time}` plus,
  when the catalog cache answered, `catalog facts {time}` — two domains, never
  "live"). The three sections stack in the scroll body: the tasks page
  (iteration head + **vertical** 5-step stepper with the FOUR-STATE `current` /
  `next` / `done` / `idle` machine + the branch panel + five stacked status
  groups — the merged「受阻/未知」/「Blocked / Unknown」column kept, `PLAN_CAP`
  render caps + the clickable 「更多」/「收起」 expand button
  (`data-kanban-more`); the projection keeps ALL plan rows — then the
  project rollup), the roadmap page (the project authority's direction, goals
  and milestone↔issue membership, with its FOUR absence states — no project,
  no roadmap row, no milestones, unavailable — each on its own anchor), and the
  events page (ONE time-descending flow of dispatch / settle / violation rows,
  every row an expandable native `<details>` carrying the full catalog fields,
  missing fields render 「—」 never a guessed value; an unreadable ledger and an
  empty one are different states). The **iteration info section is shared by the
  tasks AND roadmap pages** (one `IterationInfoSection`). Empty branches are
  explicit states — `waiting` / `loading` / `unavailable` (with its reason) /
  no-harness each carry their OWN anchor and copy and render no tabs, no digest
  and no meta dock; no harness renders a CENTERED inactive-state card that
  activates automatically once a harness is detected (inside the same single
  scroll zone). Projection is the pure `projectGraph(snapshot)` function
  (schema constants vs catalog evidence strictly separated; never throws;
  explicit degraded states — muted empty states, never orange warn boxes). The
  workflow picker (「工作流选择」) commits through `useMstarWorkflowPicker` →
  `mstar/selectWorkflow`; it is never optimistic, contributes only the commit
  state, and the selection it renders comes from the next snapshot's
  `retained.selection` (the host rebuilds it from the same durable binding). Bundle served at
  `/plugins/@mstar-harness/dsh/client.js` (closure-factory CJS with NO
  graph library inlined — react-flow removed; the build asserts the bundle
  contains no `xyflow`/`reactflow` markers, no `@deepseek-ai/*` value
  imports, and no `import.meta` / ESM statements — the loader runs plugin
  bundles as classic scripts). Full realized layout detail:
  `packages/dsh/README.md` (§ Web client plugin). **Known limitations**:
  a store read happens every 5 s while the tab is visible (five sequential
  dashboard views on the host; a collapsed tab reads nothing); a cold/expired
  catalog cache makes the panel rebuild the payload (the pre-step's own rule,
  TTL-bounded — at most once a minute per open panel), and that build may
  refresh projections; the stepper's Step 1 (iteration-start) IS the current step while the
  steering compass is `status: active` (Phase 1 in flight — catalog
  `compassStatus` field), carrying NO PASS/FAIL badge (Phase 1 has no gate
  verdict); Step 5 (merge-ready) can never be the CURRENT step — the
  engine phase gate only evaluates Phase 2→3→4; the current step follows
  the TTL-refreshed `compassStatus` (`retained.at` discloses that age) — up to
  one catalog interval (60 s)
  behind a mid-session `active`→`locked` flip (bounded, documented
  staleness, never a wrong verdict); the agent-entity status derivation
  pairs a PAIRED settle exactly by its dispatch identity (`agent`, `role`,
  `planId`, `taskId`), and an unpaired dispatch stays `running` (never
  guessed, never faked); with NO steering compass the current-iteration
  filter infers the iteration from plan ids (8-digit date prefix) +
  doneAt — deterministic, documented heuristic, only provably
  cross-iteration events are dropped, no historical back-scan of resumed
  long logs; the sidebar chip title is captured at open time; a docked
  body renders nothing while `tab.visible === false`. Routine panel QA uses affected unit evidence only. Real-browser rebuilt-bundle
  verification or user-restart GUI acceptance belongs to an explicitly requested
  independent **`mstar-e2e`** workflow (`/amazing-e2e-check`), never an iteration QA gate.

## Runtime and upgrade

- **Runtime**: Bun-hosted host — the plugin, its gates and its in-process engine all run under dsh's Bun, which is **not version-gated**: the store's Node floor applies to Node entrypoints only, and `engines.bun` declares the development toolchain. Release-surface `enforcement` / `.mstarc` settings (see § Configuration) are gates, not a runtime floor: a runtime without the native `node:sqlite` capability refuses actionably instead of degrading to a transport or JSON.
- **Upgrade / reload**: `dsh plugin --profile web add <spec>` against the published version, or re-run `npx @mstar-harness/cli init --target dsh`; then reload the profile so the composed rows pick up the new build. The bundled `harness-skills/` mirror is a build-time sync — a checkout that has not run `bundle-assets` mounts no skills.
- **Readiness, not an action**: refreshing an *installed* copy is a bounded, authorized ops act — an authority flip first quiesces, then reloads/upgrades (or explicitly excludes) every installed reader/writer and attests the versions it saw. Editing harness docs or source performs none of it. If this host cannot reload safely, stop at the exact manual-restart step, have the user restart, then re-verify entrypoint/runtime/version/session identity read-only before the flip.

## MCP delivery

- **Native install and loading**: install `@mstar-harness/dsh` in the `web` profile (`dsh plugin --profile web add @mstar-harness/dsh`; `dsh-llm-fallbacks` remains a separate optional plugin). DSH config is a Cordis YAML row, not `mcp.json`: the shipped bundle patch (`packages/dsh/bundle/cordis.patch.yml`) inserts an `mstar-mcp` row that launches `npx @mstar-harness/cli mcp` over stdio through Cordis's `@deepseek-ai/dsh-mcp-client` bridge (`serverName: mstar`). The row loads only when the bridge plugin is in the profile's dependencies (`dsh plugin --profile web add @deepseek-ai/dsh-mcp-client`); app-boot tolerates the row failing without it — no mstar MCP tools appear until it is installed. The CLI process requires **Node >=24.18.0**; the dsh plugin itself remains Bun-hosted.
- **Context boundary**: optional `host` is validated context selection, not a role or authority grant. `sessionId` is the main conversation session. MCP does not require or perform per-call child-agent attribution; shared handler workflow ownership, path, state-transition and CAS checks remain authoritative.
- **One shared connection, declared control root**: that `mstar-mcp` row is the WHOLE MCP channel — the plugin registers no MCP client of its own and no per-agent mount exists, so a subagent shares the parent's connection. The control root is DECLARED, never inferred: the launch pin (`mstar mcp --harness <absolute path>`, else a non-blank `MSTAR_HARNESS_DIR`) wins, then the admitted call's `harness` field; a command whose contract reads no root (`schema`, `path.resolve`, `host.*`, `doctor`, the read-only validators) and `harness.scaffold` run with a null root, and any other call that declares no root is refused with `[mcp.control-root-unresolved]` plus its recoveries — the two launcher recoveries are universal, the per-call `harness` selector is tool-dependent. The launch cwd is the HOST's directory and is never a root candidate, and a root-free command may not take its answer from it either: with no declared workspace its cwd-derived inputs are REQUIRED as absolute paths (`harness.scaffold`'s `path`, `roles.validate`'s `rolesDir`, `path.resolve`'s `path`, `plugin.validate`'s `root`, `audit.secret-scan`/`supply-chain`'s `path`, `pr-review.seat-prompt`'s `worktree`/`skillRoot`, and `doctor`'s project scope), while `pr-review.size`/`post` (whose subject IS the checkout) refuse outright, each naming the cause and the two launcher recoveries; a declared `--workspace` or a pinned root lifts all of them; a pin beats a per-call `harness`; a relative path argument of a root-free validator resolves against the DECLARED workspace (so `--workspace /caller` reads `/caller`'s files, and an unnamed workspace refuses a relative one), and on an unpinned connection the root SELECTOR a command declares is published as REQUIRED on every tool whose root is not otherwise fixed (`harness`, or `controlRoot` for `sdd.workspace`). The connection carries **no identity of its own**; the shipped row passes `--trust-caller-session-id`, so the per-call `sessionId` selector **is published** there, and a connection launched without the flag refuses a supplied one with `mcp.caller-identity-unavailable`. On an opted-in connection the value is a **bearer identity**: it selects the caller's own coordinator binding, so knowing a live session id is enough to act as that session, while authorization stays that binding row plus the optional `--expect` CAS token. A **host-attested** route for a session-identity operation is a native tool, where identity is `exec.agent.id`; the native surface covers only what this MCP surface cannot do — plugin-local state it cannot write, or a capability it does not publish (plan bind, audit report validation, execution clear). plan prepare/progress/complete and every other identity-consuming action run on **this** MCP connection by passing `sessionId`, because the shipped row opts in; the CLI from the session's shell is the fallback for a connection launched without the flag.
- **Doctor and evidence**: `mstar-harness doctor --target dsh` parses the profile's Cordis sources (`cordis.yml` plus the installed bundle patch) structurally and reports the `mstar-mcp` row as `aligned` only when the `@deepseek-ai/dsh-mcp-client` bridge package is installed in the profile; a configured row without the bridge is a `mismatch`, no composed sources or no row is `unavailable`. It does not open `store.db`. An aligned config does not establish installed-host behavior; live verification is separately authorized.

## Skill loading

1. On entry: invoke **`pm`** (skill name via the mstar provider) → **Read
   next** loads `mstar-harness-core`, then `mstar-roles` →
   `project-manager.md` when PM is active.
2. Read `mstar-host` and this dsh reference.
3. Load `mstar-roles` and the active role reference.
4. Load topic skills on demand per the role reference (skill **names** —
   never app-cwd `skills/<name>/…`).

## Pick the delegation channel before you delegate

Two direct-delegation surfaces can be mounted at once, and they do NOT share a
roster. Read the live toolset, never the profile name:

| `spawn_teammate` | `subagent` / `subagent_fork` | Use |
|---|---|---|
| yes | no | Agent Teams only (§ Delegation channels) |
| yes | **yes** | Agent Teams for anything writable or addressable; a direct child only for a one-off read-only seat whose result needs no follow-up |
| no | yes | the direct channel (the fallback profile) |
| no | no | no delegation surface — read-only work may fan out through `workflow`; anything else uses the recovery below |

**When neither surface is mounted**, `workflow` carries READ-ONLY fan-out only —
writable work must not route there (§ Read-only fan-out via the `workflow` tool).
Read-only work fans out through it when mounted; writable work runs inline in the
coordinator. When a task genuinely requires a delegation surface, recover through
the host profile instead of reporting a bare refusal. Enumerate first —
`dsh --profile <name> --dump-config` prints the composed loader tree, disabled
rows included, so it names the missing row. Then install the bundle that supplies
it: `dsh plugin --profile <name> add
@deepseek-ai/dsh-experimental-agent-team-profile` for the Team tools (the direct
rows ship in `@deepseek-ai/dsh-base`), or `dsh --patch <file.yml>` to add a
patch-list overlay when the row exists but is disabled. These take effect at the
NEXT boot — the composition is re-read at launch, not in a running session — so
the current session keeps its toolset: keep executing inline, or report `Blocked`
naming the missing tool and that recovery only after it fails.

**Coexistence is the trap.** On a profile that mounts the Team layer, the names
`list_agents` / `send_message` / `interrupt_agent` belong to the TEAM tools and
resolve roster targets only. A `subagent` child is not on that roster, so it
cannot be listed, messaged or interrupted, and its result arrives only through
the settlement notice. A refusal such as `active teammate "<id>" not found`
(`TEAM_MEMBER_NOT_FOUND`) against a `subagent` id states exactly that: do not
retry it, do not rename it, and do not spawn a second child for the same work.
On a direct-only profile those three names belong to
`dsh-tool-subagent-control` and do reach a direct child (`send_message` takes
`agent_id` there, not `target`), so a child you started stays steerable.

**Activation is not failure.** `started subagent <id>` proves the child started;
absence from `list_agents` proves nothing. Wait for the settlement notice, and
never replace a running child with a second spawn because you could not see it.

## Tools map

| dsh tool | Harness use |
|----------|-------------|
| **`subagent`** / **`subagent_fork`** | Direct-delegation channel — one Assignment per invoke, returned at activation start (default `toolName`; a renamed instance must be declared via Config `dispatchTools`). The default channel on a profile WITHOUT the Team layer; also a one-off seat the board does not track. `subagent_fork` is the variant that seeds the child with the completed conversation (§ Delegation channels) |
| **`spawn_teammate`** / **`team_task_create`** / `team_task_list` / `team_task_get` / `team_task_update` / **`wait_agent`** / **`send_message`** / `list_agents` / `interrupt_agent` | Agent Teams — the DEFAULT dispatch channel whenever the Team layer is mounted (one board task per Assignment, one teammate per track). Protocol → `references/dsh-agent-teams.md` |
| **`workflow`** | Read-only fan-out — one run, one conversation `workflow-run` node. N≥3 is its design width; a one-seat read-only run is supported when no direct delegation can carry it (§ Read-only fan-out via the `workflow` tool; scripts → `references/dsh-workflow-scripts.md`) |
| **`mcp__mstar__mstar_<family>`** | The writable mstar command surface — one MCP tool per command family with an `action` argument (`mstar_plan` → `bind`/`prepare`/`progress`/`complete`, `mstar_workflow` → `register`, `mstar_issue` → `add`/`close`, `mstar_sdd` → `workspace`/`task-brief`, `mstar_iteration` → `gate`, `mstar_design_md` / `mstar_compound` / `mstar_roles` → `validate`, `mstar_audit` → `scaffold`/`secret-scan`/`supply-chain`). The one shared connection carries no identity of its own; the shipped row passes `--trust-caller-session-id`, so the per-call `sessionId` selector is published there (a connection launched without the flag publishes none and refuses a supplied one). A call may name the control root through `harness` when the connection is unpinned. An operation needing a session identity passes `sessionId` on an opted-in connection, or uses the native tool where one exists (plan bind, audit validate, execution clear), or the CLI (§ The native command surface) |
| **`mstar_plan_bind`** | Native bind — the only producer of the dsh-local durable record: it binds the engine coordinator row AND writes the session's `executionBinding` slot plus the adoption receipt that ledger attribution reads (§ The native command surface). A coordinating session that wants dsh-local ledger attribution uses this tool, not the MCP action |
| **`mstar_audit_validate`** | Native report validator — the review-report credential scan (`mstar-audit` Hard Rule 4); the `mstar_audit` family publishes no report validator, so this capability has no MCP action |
| **`mstar_execution_clear`** | Clear the calling session's own durable execution binding — no shared `execution.clear` command exists. No parameters, identity from `exec.agent.id`, leaf seats refused fail-closed |
| **bash / read / write / edit / grep / glob / web_search** | Standard agent tools — evidence per `mstar-coding-behavior` |

### `subagent` dispatch shape

The dsh `subagent` tool is the DIRECT delegation channel (schema rendered by
`@deepseek-ai/dsh-tool-subagent`; `provider`-bound, default toolName
`subagent`) — the one that carries an Assignment when the Team layer is not
mounted, and the shape below is what its callers emit. On a Team-mounted profile
the same Assignment text goes out through a board task + `spawn_teammate`
instead (§ Delegation channels). Either way the dispatch gate validates the
**Assignment header region** (`## Assignment`
+ `**Execute as**` / `**Delegation**` / `**Task category**` / `**Working
branch**` / `**Branch policy**` fields — engine `composeDispatchGate`, same
violation codes as opencode/omp/CLI).

Envelope-first discipline applies: put the header fields at the top of the
Assignment body — the dsh dispatch gate reads only the header region, so
body-quoted examples never leak into header fields.

## Delegation channels

dsh exposes three delegation surfaces. **The live toolset decides which channel
you start from, not which work is allowed** (§ Pick the delegation channel
before you delegate). When the Agent Teams layer is
installed it is the default delegation channel — a multi-task plan belongs on
its board — and `subagent` is the fallback for a profile without it. `workflow`
rides alongside both as the read-only fan-out surface.

| Channel | Surface | Use when | Not for |
|---|---|---|---|
| **Agent Teams** — `spawn_teammate` + `team_task_create` / `team_task_list` / `team_task_get` / `team_task_update` + `wait_agent` + `send_message` (+ `list_agents`, `interrupt_agent`) | a durable named roster plus a shared task board; a teammate IS a subagent child carrying the ordinary agent toolset (it can enter an assigned checkout through its own tools), and any root session is the implicit Lead | **the default whenever the layer is mounted** — list the plan's tasks on the board first (one task per Assignment), then spawn one teammate per track: multi-task plans, parallel writable tracks, and any seat that must stay addressable across rounds or be steered by name | read-only fan-out while `workflow` is mounted |
| **`subagent`** / **`subagent_fork`** | one model-facing call per Assignment; `{ kind: 'activation', subagentId }` at start, settlement notice at finish; continuable on the providers this deployment configures | the documented fallback when the Team layer is NOT mounted; also single one-off lookups that need no roster | work that must stay addressable across turns, or read-only fan-out while `workflow` is mounted |
| **`workflow`** | one model-written JS script over `agent()` / `parallel()` / `pipeline()`; its children are fresh one-shot subagents, never Team members | read-only fan-out of N≥3 seats (plan QC tri, audit categories, PR-review seats), chunked to the activation-pool width; a single read-only seat routes here when no direct delegation can carry it (Team profile, where `subagent` is disabled and a review must not take a roster seat) | writable fan-out — it has no per-child pre-start veto seam |

**Parallel writable tracks are a Team workload, not a Team exclusion.** The
channel does not create checkouts for you — that stays the repository's worktree
procedure (one writable seat per worktree, L2 isolation) — but a teammate runs a
full agent toolset and enters its assigned checkout itself. A track whose
checkout is inside the workspace records its prefixes on its board task
(`write_scopes` is advisory and never a lock); a sibling checkout entirely
outside the workspace omits the field, its prompt being the whole address
(`references/dsh-agent-teams.md` § Worktree hand-off and write isolation).
Keep disjoint seats in disjoint directories, and leave integration to the Lead.

**Every Assignment has EXACTLY one gated carrier.** `spawn_teammate.prompt`,
`team_task_create.description`, `team_task_update.description` and
`send_message.message` are all dispatch carriers. Two carriers for one piece of
work leave a permanently unresolved duplicate in the ledger; **zero carriers
leave the work ungated** — a teammate reused on a track is steered by
`send_message`, so a header-free steer means the field, lease and worktree checks
never run for that task. The board `description` is therefore always coordination
text, and each task's Assignment rides its own carrier: the track's first task in
`spawn_teammate.prompt`, every later task on that track in the `send_message`
that starts it (§ `references/dsh-agent-teams.md` § Exactly one gated carrier per
Assignment).

**Seats are budgeted per track, not per task.** The roster is finite and has no
removal operation, so one durable teammate per writable track (reused via
`send_message`) is the plan shape; task reviewers are fresh-context `workflow`
children that never join the roster. Budget, member migration between tracks (a wave frees no capacity), and the at-cap recovery →
`references/dsh-agent-teams.md` § Capacity.

**The live toolset decides the starting channel, never the profile name.** The
shipped Agent Teams profile layer
(`@deepseek-ai/dsh-experimental-agent-team-profile`) disables the direct
`subagent` / `subagent_fork` rows, but an agent preset layered after it can
re-insert them (`dsh-web-app`'s `cordis` preset does exactly that), so one profile
can expose BOTH surfaces. When `spawn_teammate` is mounted, Agent Teams is the
starting channel and a `subagent` child is a one-off seat the board does not track
— see § Pick the delegation channel before you delegate for the coexistence
limits. When `spawn_teammate` is absent the Team layer is not mounted — use
`subagent` and never attempt the Team protocol. `workflow` rides alongside both.

Team protocol — roster naming, plan-row ⇄ team-task mirroring, wake
discipline, capacity, and worktree hand-off — → **`references/dsh-agent-teams.md`**.

## Gates and enforcement

The plugin wires the engine gates on dsh seams (all in-process):

| Gate | Seam | Hard-mode channel |
|------|------|-------------------|
| Status gate | `fs/write-intent` + `fs/edit-intent` on `{HARNESS_DIR}/status.json` | repair-escape advisory (never vetoes the repairing write) — **except** the store-authority refusal class below |
| Store authority in the status gate | inside the status gate's document classification (register kind, snapshot cleanup extension) | **typed veto under hard, and no repair escape**: when the findings-cleanup verdict cannot be read because the issue authority is missing (`store.not-initialized`), staged (`store.not-active`), corrupt, below-floor or busy, the write is vetoed (`status.veto` / `findings.cleanup-authority-unavailable`) — the repair is a store command (`mstar store init|upgrade|migrate`), never a document write |
| Dispatch gate | `tools/pre-execute` on every delegation carrier — the configured `dispatchTools` (`subagent` / `subagent_fork` by default) **and** the Agent Teams carriers (`spawn_teammate`, `team_task_create`, `team_task_update`, `send_message`) | `PreToolDecision { kind: 'deny', reason }` |
| Lease gate | inside the dispatch gate (SDD / InProgress dispatches) | deny under hard |
| Worktree L1/L2 | inside the dispatch gate | deny under hard |
| Skill-authoring lint | `fs/write-intent` on `SKILL.md` under mounted roots | repair-escape advisory |
| Seam lints | `fs/write-intent` on DESIGN.md / audit / compound / roles | repair-escape advisory |

**Enforcement semantics**: warn-only by default. `Enforcement: hard` —
resolved from the plugin Config (`enforcement: hard`), the Assignment header
flag, the repo `.mstarc` `[config] enforcement`, or the iteration compass
frontmatter — escalates dispatch violations to
a real veto; 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) — **except** the status gate's store-authority refusal class,
whose veto rests on the authority being unreadable rather than on the
document's content, so no content can repair it. The register kind itself
stays synchronous shape validation: a project register is migration history,
and a retired path is not a lookup. Config / `.mstarc` `soft` are the local
rollbacks. Hard gates are never a global default.

A composed agent step carries ONE **`<mstar_engine_status>`** catalog
message when the row is emitted. The row is a compact DELTA, not a snapshot:
ONE delta line carrying the selected `workflow`, its non-Done `plans`, the
`branch` anchors, and — when a steering compass resolved — the iteration phase
`gate` verdict with its transition and violation codes. A `workflow warning:
<code> <message>` line follows that delta line when the selection carries a
warning (the registration-less fallback adds the watermark trio above it), so
the block is one delta line plus those disclosures. `plans:` distinguishes an
empty registry (`plans: none registered`) from a workflow whose every plan row
is Done (`plans: all done`); the non-Done rows are listed otherwise. When no
lifecycle is selected it renders `workflow selection: ERROR (<code>) <message>`
in place of that line, and that message names the execution authority plus a
real recovery (`mstar workflow register` / `mstar iteration register`, or
picking a registered lifecycle for this session) rather than a retired
`status.json` path.

Everything else the payload carries — the open-issue rollup and its detail
rows, the branch policy, the spec-integration branch anchor, plan row scopes (a
recorded scope, never a lease), the knowledge digest, the compass direction and
the agent-flow ledger — is deliberately NOT in the row. Those facts stay in
`MstarEngineStatusPayload` (the panel reads the store; the emission row is the model-side anchor)
and in the engine authorities behind it; there is no single `status` command
that serves them, so read the concrete one per fact: `mstar status tech-debt`
for the open-issue rollup, `mstar roadmap show` and the compass document
(`{ITERATION_DIR}/<id>/delivery-compass.md`) for direction, branch policy and
the spec-integration anchor, `mstar plan show` for plan rows and their recorded
scopes, `{KNOWLEDGE_DIR}` for the knowledge documents, and the persisted
`dsh_status_emissions` snapshot for the agent-flow ledger. The row is a
projection of the payload, never the authority for it: treat the row as a
change notice and re-read the authority when a decision needs the full state.

Emission is change-driven per session+workspace, and the two sides are gated
separately. The MODEL row is keyed on its own rendered text: first emission,
then only when that line changes — so a residual count, a ledger event, a
knowledge digest or a row scope moving does not append an identical-looking
row, and a long unchanged session does not accumulate stale copies. The PANEL
is keyed on the full payload projection: a change to any payload-only fact
still persists the new payload for the panel (the freshness footer keeps the
turn the model last saw a row). The start of a turn is neither trigger, and a
workspace with no resolved `{HARNESS_DIR}` composes no row at all.

The block is a projection of the payload: the version / harness dir /
enforcement watermark stays in the payload (the panel meta dock renders
version + harness dir from it) and stays model-visible once in the always-on
system prompt (`mstar:engine-status` digest + `mstar:harness-rules`), never
repeated in the row. The one exception is a composition without
`dsh-system-prompt`: the registration fails open and neither prompt section
exists, so the row falls back to re-rendering the trio itself (the listener's
`showWatermark` option, taken from that registration verdict) — the watermark
is never absent from the model's context. The newest per-session emission
persists in the control-root `store.db` (`dsh_status_emissions`); its
freshness footer `turn` / `at` name the last MODEL-row emission, so a
panel-only persist updates the payload without claiming the model saw a new
row. The row is not periodically re-anchored, so after host-side context
compaction it may be absent until the next rendered change — the system-prompt
digest still carries the version, the active workflow, its non-Done plans, the
`branch` anchors (when the payload resolved both) and the phase-`gate` verdict
(when a steering compass gated the iteration). The remaining facts are read on
demand: `mstar status tech-debt` for the open-issue rollup,
`mstar iteration gate --workflow <id> --compass <path>` for a fresh gate
evaluation, and the compass / plan documents for direction, policy and row
scopes. The build behind it is one per-workspace TTL cache (`catalogTtlMs`,
default 60 s).

### Execution authority in this host (source state)

The engine's own route (`resolveExecutionReadRoute`) decides which authority
answers, and no tool argument or config key selects one:

- While the control harness's execution authority is **ACTIVE**, the plugin's
  gates select the lifecycle they are about from the **DB authority** with
  explicit identities — never a newest/only guess, never a fallback to the
  retired `status.json` / snapshot bytes — and a write to a retired
  coordination document is refused **unconditionally** (`execution.direct-write-refused`),
  canonical path and symlink alias alike. A register write while an active store
  answers is refused as migration history; a register write while the authority
  cannot be read at all fails closed.
- A missing (`store.not-initialized`) or staged (`store.not-active`) store is not
  an execution route: coordination documents have no file fallback there
  (`execution.not-active` — the file route is retired), and the
  register kind stays under its own document validator.
- The host-native execution entry is the **shared MCP connection**, not a command and
  not a native duplicate: the writable mstar commands arrive as MCP tools
  (`mcp__mstar__mstar_<family>` with an `action`). Three dsh-native tools ride
  beside them, and only for what the MCP route cannot do — `mstar_plan_bind`
  (the durable-slot producer), `mstar_audit_validate` (no MCP twin) and
  `mstar_execution_clear` (no shared command); the rule is in § The native
  command surface below.

### Concurrent lifecycles and workflow selection

**Several ACTIVE lifecycles at once is the normal state**, not an error: each
plan, worktree, and PR advances on its own lifecycle, and nothing in the host
caps how many run concurrently. A session only has to answer "which lifecycle
does THIS operation belong to", and the resolver answers it from the operation
itself before it ever asks the operator. The order below is the resolver's own:

1. **A plan row's recorded worktree** contains the session cwd.
2. **A lifecycle's integration worktree** contains the session cwd.
3. **The session's explicit pick** (the panel selection or `mstar plan bind`).
4. **The plan row the operation names** — an Assignment's `Plan Path` / `SDD dir`,
   a `--plan` argument. A plan row belongs to exactly one lifecycle, so naming it
   identifies its owner even when the cwd matches several.
5. **The only active entry.**

Every automatic rung (1, 2, 4) needs **exactly one distinct** lifecycle: zero or
several matches fall through to the next rung, and nothing is ever decided by
registry order, recency, or a longest-prefix guess.

An explicit pick outranks rung 4 on purpose: a pick that names a lifecycle
WITHOUT the requested plan row is a contradiction the caller must see
(`lease.dispatch.plan-not-found`), never a silent switch of lifecycle. A pick
naming a lifecycle that is no longer active has expired, so the operation's own
plan row may resolve it (rung 4) — the pick named nothing the store can confirm.

Only when none of the five applies does a write report
`workflow.selection.unbound-multi-active`, and that refusal is scoped to the one
operation: it lists the active ids and the ways to name one. It is not a session
pause — every other operation keeps working, and the same session can drive
several lifecycles by naming the plan each call.

### The native command surface

The writable mstar command surface on this host is the **MCP route**: the
plugin's bundle patch inserts ONE `mstar-mcp` row (`npx @mstar-harness/cli mcp`
over stdio, § MCP delivery) and that row is the whole MCP channel — no
per-agent mount exists, so a subagent shares the connection its parent already
has. The connection carries **no identity and no control root of its own**: the
root is declared at launch (`--harness`, else a non-blank `MSTAR_HARNESS_DIR`)
or named per call through `harness` (REQUIRED while the connection is
unpinned), a command whose contract reads no root runs with a null one, and a
command that needs a session identity takes the published per-call `sessionId`
on an opted-in connection — the shipped row opts in — and a connection launched
without the flag has no caller-declared channel, so there the route is a
dsh-native tool, where identity comes from `exec.agent.id`, or the CLI (§ MCP
delivery). The native tools cover plan bind, audit report validation and
execution clear; plan prepare/progress/complete run on the MCP connection with
`sessionId`. Each MCP tool is one
command FAMILY with an `action` argument —
`mstar_plan` (`bind` / `prepare` / `progress` / `complete`), `mstar_workflow`
(`register`), `mstar_issue` (`add` / `close`), `mstar_sdd` (`workspace` /
`task-brief`), `mstar_iteration` (`gate`), `mstar_design_md` / `mstar_compound` /
`mstar_roles` (`validate`), `mstar_audit` (`scaffold` / `secret-scan` /
`supply-chain`), plus every other family. An unknown or missing `action` refuses
with that family's real action list, and a missing required field refuses with
the shared aggregated message naming every missing field — validation, refusal
aggregation and payload composition are the shared command layer's, unchanged.

**The native-surface rule** (memorize this instead of a list): a command is a
dsh-native tool only when it

1. **produces a dsh-local durable record the MCP route cannot write**, or
2. **exposes a capability the MCP surface does not publish at all**.

Everything else is MCP. Three tools satisfy the rule today:

- **`mstar_plan_bind`** — rule 1. It binds the engine coordinator row *and*
  persists the dsh plugin's own durable state: the session's `executionBinding`
  slot in the control root's `store.db` plus the in-process adoption receipt the
  workflow-ledger attribution resolver and `mstar_execution_clear` read. Shape:
  the shared `plan.bind` parameters mirrored mechanically from the published
  schema (the session selector is not one of them — identity comes from
  `exec.agent.id`), engine refusals returned verbatim, a known leaf seat refused
  fail-closed before admission.
- **`mstar_audit_validate`** — rule 2. It validates one review report under
  `{REVIEW_DIR}`: the credential scan of `mstar-audit` Hard Rule 4, the validator
  behind `mstar audit scaffold`. The `mstar_audit` family publishes `scaffold` /
  `secret-scan` / `supply-chain`; none of them validates an existing report file,
  so folding this one into the family would have dropped the capability.
- **`mstar_execution_clear`** — rule 2. It clears that same dsh-local
  `executionBinding` slot, and there is no shared `execution.clear` command for
  the MCP route to carry. No parameters at all: identity comes from
  `exec.agent.id`, so a `sessionId` argument (or any other argument) is ignored
  and only the caller's own binding is reachable; the user's workflow selection
  and exclusion floor are preserved. A known leaf seat
  (`header.origin === 'subagent'` or `delegationDepth > 0`) is refused
  fail-closed before the store read. Refusals: `execution.clear.no-harness` names
  the two routes that exist (open the session in a workspace declaring
  `.mstar/`/`.mstarc`, or point the plugin's `harnessDir` config at the control
  root); `execution.clear.no-binding` names the `harnessDir` config route for a
  binding recorded in another control root and states the no-action case;
  `execution.clear.store-refused` names the store's own reason and, when this
  session is keyed to another workspace, carries that workspace under
  `data.recordedCwd`.

**The two bind routes are NOT equivalent.** MCP `mstar_plan` with
`action: "bind"` binds the engine row and stops there. The native
**`mstar_plan_bind`** binds the engine row *and* writes the dsh adoption receipt
plus the durable slot. A coordinating session on this host therefore uses the
**native** tool whenever it wants dsh-local ledger attribution (workflow-ledger
rows and the clear both resolve through that record); the MCP action remains the
route for any other bind caller, including every non-dsh host.

**Holding an old tool name?** Any `mstar_<family>_<action>` spelling of a
command — the former plan / workflow / issue / sdd / iteration / design-md /
compound / roles mirrors, twelve tools in all — is gone with no alias and no
compatibility tool. Those twelve duplicated an MCP tool 1:1, so two identical
surfaces competed for the same call and the whole set cost ~10k characters of
fixed context per request. **One exception to that 1:1 phrasing:** the dsh
compound-validate mirror also published a dsh-only `repo_root` parameter for
reference-existence checks (the compound-refresh Phase 2 check), and
`compound.validate` in `packages/commands/src/families/validation.ts` carries
neither that parameter nor that check — so the capability went with the tool and
has no MCP twin. Recovery: call the folded family instead —
`mcp__mstar__mstar_plan` with `action: "prepare"` where an older document spells
the prepare mirror natively, and so on — and read the family's real actions and
parameters with `mcp__mstar__mstar_schema` (`family: <name>`, or
`command: <command-id>` for one leaf contract) instead of guessing from the old
name. For the knowledge-doc checks use MCP `mstar_compound` with
`action: "validate"` (or the CLI `mstar compound validate`) without `repo_root`;
the reference-existence half is the `mstar-compound-refresh` knowledge-write
seam's job, not an on-demand tool. The former plugin-owned command
**`/mstar-execution`** stays removed; the
ADR `docs/adr/dsh-native-identity-channels.md` (§ Superseded rationale) explains
why identity lives on the mount and the clear rather than on a command.

## Agent-flow ledger

The plugin records ACTUAL subagent dispatch and real-completion settle events —
the evidence of what really happened, distinct from the client-side expected
role flow. The workflow panel's 事件记录 tab (`EventLogPage`) and its workspace digest are
pure consumers of this evidence.

- **Recording point (one core)**: `DshHostAdapter.dispatchGate` is the SINGLE
  record path behind both dispatch surfaces — the `tools/pre-execute` listener
  (exec-bound; the lease gate joins here) and the host `beforeDispatch` hook
  (exec-less). Every Assignment-shaped dispatch that reaches the gate records,
  including hard denies (verdict derived: ok / advisory / denied); the shape
  guard lives at the shared core, so non-Assignment text stays silent on BOTH
  surfaces (the listener's own guard plus the core's guard for the exec-less
  hook path — no phantom records). Recording is advisory (try/catch-contained,
  logs only `mstar/agent-flow`) — a failing ledger never blocks dispatch.
  Known tradeoff: the same logical dispatch crossing BOTH surfaces (a host
  `beforeDispatch` followed by the identical text as an in-loop subagent tool
  call) records two dispatch events — the surfaces are mutually exclusive by
  design; the double record is documented, not deduplicated.
- **Native storage / bounds**: host machine state lives in the existing
  control-root `store.db` domain: `dsh_ledger_events`,
  `dsh_accepted_identities`, `dsh_scan_bounds`, `dsh_session_bindings` and
  `dsh_status_emissions`. Catalog/panel windows are read projections, not
  storage retention. No shared JSONL tail/index/cursor/cache authority,
  compaction journal, file freeze or directory lock remains. An unavailable
  authority is absent evidence, never a fabricated empty view. Before ACTIVE,
  native host session logging continues; authority persistence reports
  unavailable/degraded with supported `store init` / `store upgrade` recovery,
  not a legacy-file fallback.
- **Record identity and accepted source replay**: verified native workflow
  events retain `{sessionId, streamId, seq}` and
  `wfe1:<kind>:<sessionId>:<streamId>:<seq>`. The stream incarnation comes
  from the durable native session header, never the store epoch or file mtime.
  Their append, accepted-identity/digest check and monotone scan-bound advance
  commit in one native transaction. Reusing an accepted identity with different
  bytes refuses without overwriting it. Live dispatch/settle/link/verdict rows
  are append evidence and may repeat; a correlation ID does not deduplicate
  them. Original legacy tail/history/index/cursor bytes remain read-only
  one-time import/history through `mstar store upgrade`, never live authority.
- **Settle = real completion pairing, never faked**: `tools/post-execute`
  IS part of the
  verified dsh-tools registry surface (`runPostExecute` dispatches the
  waterfall for every tool call — verified against the upstream source and
  pinned by a real-call probe). The pairing listener matches dispatch TOOLS
  (Config `dispatchTools`, default `['subagent', 'subagent_fork']`), looks up
  the exec's agent-namespaced call key in the apply-scoped pairing store, and
  branches on the verified result shapes:
  - `{ kind: 'background', jobId }` (the registry job id, `<kind>-N`) → store
    `jobId → dispatchRef` and the bounded job id as the ref's `taskRef`; the
    REAL settle arrives via `ctx.inject(['jobs'])` → `jobs.onJobDone`
    (terminal mapping completed → ok / killed → denied / failed → error,
    `durationMs` when available). A background value without a valid `jobId` →
    nothing mappable (no settle).
  - `{ kind: 'continuable', subagentId }` → no terminal signal this round →
    no settle (documented limit — the child owns its turns); the value
    authorizes the child-identity join below and nothing is copied onto a
    settle.
  - any other successful value (foreground included) → settle `ok`; a failed
    result (`isError` or an `error` payload) → settle `error`. A returned
    foreground `runId` is the settle's `childId`, extracted independently of
    the outcome (an error settle keeps its identity without becoming `ok`).
  Pairing is apply-scoped (in-memory `callId → dispatchRef` /
  `jobId → dispatchRef` maps created in the entry `apply`; an HMR restart
  resets them, and completions outside the window stay unpaired). Every
  PAIRED settle carries the paired dispatch's identity (`role`/`planId`/
  `taskId` — same field names + semantics as the dispatch event; the registry
  job id is never written as `taskId` — `taskId` stays the Assignment `Task N`
  tag, `taskRef` is reserved for the registry id). Unpaired payloads
  (non-dispatch tools, calls outside the pairing window) record NOTHING — the
  ledger stays dispatch-only, never a fabricated settle.
- **Child identity (`subagent-link`, nonterminal)**: the child session id is
  published upstream as a PARENT-OWNED `subagent/catalog` session event
  (`{ version: 0, childId, childCreatedAt, mode, label }`; `label` = the
  delegation `description`), appended by the tool body — for the continuable
  path BEFORE the tool returns. The join spans a per-dispatch CALL WINDOW: the
  pre-execute reserves the first raw-label slot under the live parent Session
  object and captures its `seq` as the window start; a valid `background` /
  `continuable` result at `tools/post-execute` makes that exact candidate
  eligible (every other outcome retires it to a tombstone; a duplicate label
  was already refused a candidate at reservation). Eligibility walks
  `eventAt(seq)` over `[fromSeq, end)`, where `end` is the session's `seq`
  CAPTURED when the candidate became eligible — recovering a catalog appended
  before the tool returned — while ONE root-context `session/event` observer
  feeds the same matcher for later arrivals against the session's CURRENT
  `seq`: only that live observer follows the session forward, so a catalog
  appended after eligibility still joins through it (the catch-up scan stays
  frozen at its captured endpoint). A matched
  candidate is consumed once and appends `{ v: 1, ts, kind: 'subagent-link',
  agent?, childId, label, role, planId?, taskId?, taskRef? }` to the
  DISPATCH's own workflow dir (`ts` = observation time), correlating the
  catalog child back to the dispatch identity mstar recorded. It is an
  IDENTITY record, NOT a completion: no `outcome`, no `verdict`, no `paired`
  marker. A background one-shot link also carries its registry `taskRef`; a
  continuable link omits it. Settle rows carry an optional `childId` — a
  foreground `runId`, or for background only when the join has already
  supplied one.
- **Join bounds (honest degrade)**: NO row when the label is missing/empty,
  the dispatch unpaired, a background result carries no valid `jobId` (the
  reserved candidate is retired — nothing mappable: no settle, no link),
  the catalog version unknown, the mode not matching the result kind,
  a continuable catalog naming a different child than the tool returned,
  the slot map at capacity (500 labels per parent Session), or the slot
  already consumed. The join is apply-scoped: no whole-history cold
  scan and no `session/created` backfill — a catalog written before apply
  (constructor seeds) can never label a new dispatch. Duplicate labels are
  deterministic best-effort (first reservation + first matching catalog wins),
  NOT proof of unique ownership. Not every provider emits a catalog — a remote
  run without a `localAgent` produces none, so a dispatch may legitimately
  have no link row.
- **Catalog**: `state.agentFlow` carries the ledger view (`events` ≤ 50,
  latest-first, + `summary`) for the panel — the compact
  `<mstar_engine_status>` row does NOT render a ledger line (a ledger event is
  not a lifecycle delta the model acts on, and it kept spending a full row).
  A ledger record (dispatch/settle/link) invalidates the affected workspace's
  TTL cache entry IMMEDIATELY (apply-scoped `harnessDir → cache key` reverse
  map + invalidation closure) → the next pre-step rebuilds, persists the new
  payload for the panel and re-injects a row only if a RENDERED fact moved.
  The 60 s TTL no longer bounds ledger-change latency for the panel; it still
  bounds non-ledger staleness.
- **Explicit ledger target (ACTIVE authority)**: each event boundary resolves
  the session's selected native target and validates its current store identity,
  epoch and workflow scope. Missing, stale, revoked, corrupt or mismatched
  authority records no accepted fact and never selects a newest/only workflow,
  root file or legacy target. Native session logging remains independent of
  whether authority persistence is available.
- **Maintainer view**: change the ledger shape (event schema, bounds, settle
  seam) and update the projections together — `gates/agent-flow.ts` (record /
  read / settle / catalog-join listeners), `gates/catalog.ts` (the `agentFlow`
  payload view + the panel-projection emission key) and
  `client/panel/graph/project-graph.ts` (the ZoneView flow/agents projection) —
  the panel renders ONLY what the evidence shows.

## PM dispatch

Harness **dispatch** on dsh carries the full Assignment text as the delegation
prompt (role binding in the prompt — `Execute as` / `Act as` + skill load;
there is no separate `agent` field, the Assignment body IS the prompt). **Which
tool carries it is the mounted channel's decision** (§ Delegation channels):
with Agent Teams mounted, one board task plus one `spawn_teammate` per assignee;
on a direct-`subagent` profile, one `subagent` call per Assignment. **N
assignees = N independent delegations** on either channel (dispatch-gate
口径: one assistant message carries all N invokes — the gate counts each
dispatched Assignment, board task or `subagent` call alike). Paste-only
Assignment without an invoke is **not** dispatch.

**Role-binding field:** none — binding is **prompt-only** on dsh (the tool has
no role field; the header region of the Assignment body is what the gate and
the role-preset channel read, so `Execute as` / `Act as` + skill load must be
in the prompt). A dispatch that drops those lines has no field to fall back on
— it is a bare delegation.

**Role gate (`roleGate`, default `hard`):** the plugin normalizes an explicit
declaration at the subagent start seam before the preset merge, so a
`Role: \`<role>\`` line or a backticked/bold already-declared role id IN THE
ASSIGNMENT HEADER is rewritten into the canonical `Execute as: <role>` header
and binds normally; free prose is never mined, so write the header explicitly.
A body mention is not a declaration (the token tier is header-scoped), and a
canonical declaration that exists only inside a fenced code block is refused
too — the seam's header read is fence-unaware and would bind that quote. A
harness-project start with NO reliable declaration is refused under `hard` with
a message naming the exact fix (`Execute as: <role-id>` under `## Assignment`)
and the `roleGate: warn|off` escape hatch; a start naming two or more distinct
roles is refused with the ids and the "exactly one declaration" rule instead,
and a role field that IS present but unusable (empty, or not a lowercase role
id) gets its own message naming the raw value and the accepted shape. The
non-harness exemption is judged at the child's EFFECTIVE start directory (an
absolute `cwd` as-is, a relative one resolved against the parent's CURRENT
directory, an omitted one inherited from it — the working-directory projection
the runtime itself resolves against, never the immutable header alone), so the
default `subagent` / `spawn_teammate` starts — which omit `cwd` — are gated by
the directory the parent is in NOW rather than exempted. A request already
carrying `agentPreset` and an unknown provider are exempt too. `off` skips only
the refusal; a single explicit declaration is still normalized. `roleGate` and
`dispatchBinding` are the plugin's live-editable settings fields — a dsh
plugin-configuration save applies on the next start (next dispatch for the
binding) without a reload.

**Execution: same-message invokes start concurrently.** The installed `subagent`
tool declares **`isConcurrencySafe: () => true`**, so a batch of invokes emitted
in ONE assistant message is admitted concurrently instead of serialized. Each
call is a **one-shot activation**, not a synchronous run: it returns
`{ kind: 'activation', subagentId }` (rendered `started subagent <id>`) as soon
as the runtime accepts the child, and the parent receives a **settlement
notice** when that child finishes — the call itself waits for nothing. The
tool's input schema is `cwd` / `description` / `prompt` plus the optional
preset and child-route fields: it has **no background-mode parameter** and **no
structured-output schema parameter**, so a child's structured result arrives
through its settlement notice and follow-up turns travel through
`send_message` (continuable providers), never through a tool argument.

Dispatch consequences:

- **N assignees = N invokes in one message.** Admission is concurrent, so an
  N-seat batch costs roughly one seat of wall time; nothing needs to be
  "backgrounded" to overlap.
- **Capacity is bounded.** The activation pool defaults to **8 concurrent
  children** (`maxActiveSubagents`, a deployment setting); an invoke beyond the
  free pool is rejected with `subagent limit reached (active child limit: 8)`,
  which means that seat never started. Keep one message at or below the free
  pool width, or dispatch the remaining seats after earlier children settle.
- **A missing or rejected invoke is dispatch-incomplete for its seat** — record
  it plainly and recover as the owning skill allows. It is never a reason to
  declare the whole batch `Blocked`: there is no background mode to switch to.
- **Activation is not completion.** A returned `subagentId` proves only that
  the child started; completion evidence is the settlement notice (and, where
  the role hands back a report, the child's closing message).

**Leaf completion discipline — closing message, not the report tool (PM
2026-08-12).** Leaf subagents hand back their Completion Report in the
**final (closing) message** — do NOT call the `report` tool to deliver it.
The dsh tool-subagent-report default `reportDelivery: quiet` routes a report
through `parent.inject` into the parent's **next-step queue**; when the
parent's turn has ended (no step boundary follows), the report strands in
the "queued messages" dock instead of reaching the parent (observed on dsh).
The closing message is the guaranteed delivery channel; reserve `report` for
MID-turn findings that change what the parent should do next.

### Progress discipline — native workflow, never `/goal`

dsh progress is driven by the **native workflow**: the workflow snapshot phases
+ the dispatch gates + **subagent settle notifications**. mstar **stops arming**
a goal on dsh — the surviving bridge is advisory-only (no `create` / `edit` /
`complete` / `pause` / `resume`, no goal read) — so no goal round loop drives
mstar work here. Never drive a dsh session with a `/goal` objective or a goal
round loop: `goal-round-driver` opens a round whenever the goal is active +
armed and the agent is idle, and it knows nothing about running subagents, so
an operator who arms `/goal` manually can still get rounds firing while a
dispatched child owns the critical path.

**Phase 2 continuous execution is a PM-local loop**, not a one-wave-at-a-time
queue: each child's settle notification is a **`result-settled`
`Rescheduling checkpoint`** — run that checkpoint and dispatch what is already
ready and authorized (independent plans and plan-local tasks, each in its own
isolated track) before waiting, and wait only when it finds none. Waiting stays
the correct action for work a running child already owns — never open another
unit of work **against the same worktree**. Procedure (and its frozen reason
vocabulary) → `mstar-iteration/references/phase-2-worktree-lease.md` §2.4.

No-argument iteration-drive resumes the primary coordinator's workflow; nonempty arguments refuse. It still never arms a goal here. The coordinator advances rows directly through ordinary prepare/progress/complete, with native leaf scheduling and real serial integration.

### QC default

- **`Execution mode: sdd`**: **N=3** seats — one per QC seat (`qc-specialist`,
  `qc-specialist-2`, `qc-specialist-3`), each body **Act as** the respective QC
  role + QC skill load. Channel follows the **`QC report path`** field: a
  plan-backed QC seat must itself write its report file, so it routes through a
  **writable** channel (Agent Teams roster seat; `subagent` when the Team layer
  is not mounted) — `workflow` children are read-only and only carry the
  plan-QC tri when the Assignment explicitly declares conversation-only review
  (`QC report path: N/A`), in which case the caller persists
  `{SDD_DIR}/review/qcN.md` from the returned envelopes (`mstar-qc-tri` script —
  § Read-only fan-out via the
  `workflow` tool): one run, three concurrent children, one conversation
  `workflow-run` node. When the tool is not mounted (the `ptc` preset hides it),
  fall back to the `subagent` path below. **The `subagent` path emits all three
  invokes in ONE message**: the host admits every dispatch before any
  settlement, so all N are admitted before the first seat settles and the seats
  run CONCURRENTLY (wall ≈ single seat). There is no serialization promise
  either way — a PM that waits for each settlement before emitting the next
  invoke is what spaces the batch out and costs the wall time. The call return
  (`started subagent <id>`) is an activation, never a completion. A seat that
  cannot be started at all → **`Blocked`**.
- **`inline`**: **N=1**.

### SDD implement

- **`Execution mode: sdd`**: one implementer delegation per ready task id, on the
  mounted channel: with Agent Teams, one board task per task id and one
  `spawn_teammate` per track, each handed its own worktree; on a
  direct-`subagent` profile, one `subagent` call per task id. Independent tracks
  are started together so their activations overlap, then the PM waits for
  settlement, per **`mstar-sdd`** § Ready-task scheduling. The task reviewer is a
  fresh separate seat on either channel. Sticky resume is a durable teammate
  resumed by name on the Team channel, and a recorded continuable-subagent id on
  the direct one.

## PR-review posting

- **Route:** the main agent (the command's orchestrator) posts at Stage 3 through
  MCP **`mstar_pr_review`** (`mcp__mstar__mstar_pr_review`) with
  `action: "post"`, `pr: "<n>"` and `bodyFile: "<absolute-body-path>"`, or
  CLI `mstar pr-review post --pr <n> --body-file <path>`; optional inline
  findings use `findings: "<file.json>"` / `--findings <file.json>`.
  MCP workspace/control-root requirements remain those in § MCP delivery.
  Read-only seats never post, including on a Team-only profile.
- **Body and event:** follow **mstar-audit** →
  [§ Report template](../../mstar-audit/references/pr-review.md#report-template-github-review-body)
  and publish with event **`COMMENT`**, per
  [§ Comment posting](../../mstar-audit/references/pr-review.md#comment-posting).
- **Save before cleanup:** the main agent must save the local report whether
  posting succeeds, fails, or is `n/a-no-pr`; dsh seat completion or POST failure
  never waives that obligation. Archive details belong to **mstar-audit** →
  [§ Local report archive](../../mstar-audit/references/pr-review.md#local-report-archive).

## Read-only fan-out via the `workflow` tool

dsh also exposes the upstream **`workflow`** tool
(`@deepseek-ai/dsh-tool-workflow`, mounted by the shipped agent presets; the
`ptc` preset disables it in favour of its own orchestration surface). It runs a
model-written plain-JavaScript script that fans children out inside ONE run; the
run is recorded as durable `tool-workflow/*` session events and the stock dsh UI
(`dsh-client-ui-workflow-run`) folds them into one conversation **`workflow-run`**
node the operator expands by phase and member. Use it for **read-only fan-out of
N ≥ 3 seats** — plan QC tri, large-repo audit categories, `/amazing-pr-review
deep` seats — and copy the `script` + `meta` + `args` from this skill →
`references/dsh-workflow-scripts.md`. For **1–2** seats **`subagent`** is the
carrier — read-only fan-out never takes a teammate at any N. Following the tool's
own guidance, while `subagent` is available the two-seat default tier of
`/amazing-pr-review` shows two subagent cards and no `workflow-run` node.

**Where the Team layer disables `subagent`, pick the script by the WORK, not by
the seat count.** `mstar-review-single` (§ 4) is an SDD **task**-review script
and only that: its guard requires `planId`, `taskId`, `reviewCwd`, `branch`,
`briefPath` and `implementerReport`, so a PR or audit review cannot feed it —
placeholder inputs force its task-review mode and return task reports instead of
findings. On a Team-only profile the carriers are:

| Work | Carrier |
|---|---|
| SDD task review (one fresh-context reviewer, no roster seat) | § 4 `mstar-review-single` — the one-seat route below |
| 2-seat **PR review** (`/amazing-pr-review` default tier) | § 3 `mstar-pr-seats` with the two domain lenses the tier selected — its guard accepts 2–3 labels (`args.domains must hold 2-3 domain labels`), so two labels are legal; the run's seats are what the tier asked for (plus the cross-domain security seat only when the tier selected one) |
| 2-category **codebase audit** | § 2 `mstar-audit-fanout` with `categories: [<c1>, <c2>]` — an audit reviews a REPO, not a diff, and § 2's guard asks only for `repo`, `auditRef` and `recon`; `categories` is optional (absent → all nine categories), so two labels are the two-seat form. § 3 is not an audit carrier: it requires `target` and `diffBase` |
| a 2-seat PR review that still cannot run in that profile | no § 4 substitute and no wrapper: run the deep tier once a `subagent`-capable profile is available, or a single-seat audit |

A § 3 run at two-domain width is a **domain-seat run at the default tier's width**,
not a "2-seat fan-out" and not one node per seat: the operator sees the one
`workflow-run` node that run produces. A § 2 run is the same shape at category
width — one `workflow-run` node, one child per named category.

**One read-only seat is supported when nothing else can carry it.** On a Team
profile the direct `subagent` rows are disabled, and a task review needs a fresh
context without taking a roster seat — so a **one-seat `workflow` run** is the
supported route there, with its executable form in
`references/dsh-workflow-scripts.md` § 4 `mstar-review-single`. It costs one
`workflow-run` node instead of a subagent card; that is the price of keeping the
reviewer off the finite roster, not an unsupported workaround.

**Read-only only.** A workflow child is a delegated child (the shipped `spawn`
provider pins `approval: never` for the whole delegation), and the run has **no
per-child pre-start veto seam** — so a script is never the channel for writable
work; writable fan-out goes through the mounted delegation channel
(`spawn_teammate`, or `subagent` on a direct profile) behind the dispatch and
lease gates.
Seats return findings in their result payload and must never depend on writing
files — the caller persists the seat reports.

**Every `agent()` prompt starts with the Assignment header** — `## Assignment`
plus `Execute as` / `Delegation` / `Task category` as the first lines:

```markdown
## Assignment

Execute as: qc-specialist
Delegation: forbidden
Task category: audit
**Budget (review / QC seats)**: /1 round — one full pass over the review range
**Return shape (review / QC seats)**: the verdict envelope (seat, verdict, summary, findings); stop when the range and its directly affected interfaces are covered
```

Role binding on dsh is prompt-only (there is no `agent` field), and the same
engine grammar is what the role-preset channel parses
(`packages/dsh/src/gates/role-persona.ts` reads only the header region) — so
`Execute as: qc-specialist` selects the agent preset the deployment declared for
that role (dsh 0.2.1 delivers a child's identity through preset selection; a
role with no configured preset leaves the child on the delegating agent's
composition). Keep body-quoted field examples out of the header region, and
never pass the deferred `agentType` option: the engine rejects it loudly.

| Operator types | N | Tool | `meta.name` | Operator sees |
|---|---|---|---|---|
| `/codebase-audit` (large repo) | ≥3 | native `workflow` | `mstar-audit-fanout` | conversation `workflow-run` node |
| `/amazing-pr-review deep` | ≥3 | native `workflow` | `mstar-pr-seats` | same |
| `/amazing-pr-review` default tier | 2 | `subagent`; on a Team-only profile (no `subagent`) § 3 `mstar-pr-seats` with the tier's two domain lenses — `mstar-review-single` is SDD task review only | `mstar-pr-seats` (Team-only) | two subagent cards while `subagent` is available; otherwise that single § 3 run node |
| Plan QC tri (PM already in session, no extra slash) | 3 | native `workflow` | `mstar-qc-tri` | conversation `workflow-run` node |
| Any 1–2 read-only delegation | 1–2 | `subagent`; else the script that matches the work — SDD task review → § 4 `mstar-review-single`, PR review → § 3 `mstar-pr-seats` with the two lenses, codebase audit → § 2 `mstar-audit-fanout` with two `categories` | per the script | subagent cards while `subagent` is available; otherwise the one `workflow-run` node of that run |

`meta.name` is the gate identity — keep it kebab-case and on the recommended
list. With the default Config (`workflowNames` unset) every name is *unknown*,
which under the default `workflowGate: warn` is one `workflow.name.unknown`
**advisory that the run survives** — acceptable on a first run, not a failure. A
production overlay may set `workflowNames: ['mstar-qc-tri', 'mstar-audit-fanout',
'mstar-pr-seats']` (and, separately, `workflowGate: hard`); both are operator
choices, never mstar defaults. The same run also reaches the panel's 事件记录 tab
through the agent-flow ledger (§ Agent-flow ledger).

## Commands and skills paths

| Surface | Path / invocation |
|---------|-------------------|
| Plugin skills | Skill **name** via the mstar skill-local provider (`ctx.skills`); canonical `$DSH_BUNDLED_SKILL_DIR/<name>` |
| Plugin commands | The seven bundled `harness-commands/` commands — `/iteration-start`, `/iteration-drive`, `/iteration-loop`, `/codebase-audit`, `/amazing-pr-review`, `/amazing-e2e-check`, `/amazing-test-audit` |
| Session entry | `pm` skill → `mstar-harness-core` via pm **Read next** |

## Command delivery (dsh host, updated 2026-08-11)

The dsh web client resolves slash commands against a client-side lexicon driven by the registry's `input.hint`. Every mstar command declares a frontmatter `input` hint (see `commands/*.md`), so the client **claims** it on menu pick: `/name ` is inserted into the composer with the command highlight and the hint as ghost text (e.g. `/iteration-start [direction] [pause]`), the user types follow-up args (or just presses Enter for arg-less commands), and the line submits only on Enter. The handler steers the command body into the receiving agent as a USER-source message, appending the typed args as a `## User input` section when present.

**Degradation fallback:** when a command is NOT claimed client-side (lexicon fetch timing, args parsing, manual typing), the model receives the **bare text** (`/iteration-loop <方向> …`) with NO command body — unlike opencode/cursor/omp where the body always arrives.

**Rule:** when a user message begins with a registered mstar command name (`/iteration-start`, `/iteration-drive`, `/iteration-loop`, `/codebase-audit`, `/amazing-pr-review`, `/amazing-e2e-check`, `/amazing-test-audit`) but carries no command body, treat it as that command invoked with the user text as its argument — execute the command's OWN semantics from the repo `commands/<name>.md` (or the mirrored `harness-commands/`): in particular **`/iteration-loop` = autonomous (code-first direction lock, NO grill-me questions)**, `/iteration-drive` = Phase 2–5 on the active iteration, `/iteration-start` = interactive (grill-me). Do not silently substitute the interactive start flow for `/iteration-loop`. Also do not re-ask what the command already specifies (e.g. scale auto → M default, branch policy continuity).

## Harness dir and environment

- `{HARNESS_DIR}` resolves via the engine `resolveHarnessDir` (`.mstarc`
  `[config] harness_dir` → `.mstar/` → `.agents/` → `.plans/`/`plans/`),
  with the plugin Config `harnessDir`
  override winning. The probe starts from the SESSION workspace root (the
  session cwd — **never the dsh launch/process cwd**) and **STOPS there** — it
  never walks above the session workspace, so the watermark and gates follow
  the workspace the session actually works in. Repos using a
  non-standard harness root MUST set Config `harnessDir`
  (absolute path) or declare it in a repo `.mstarc` — the gates are inert
  without a resolvable harness dir.
- The dispatch gate needs the dispatching agent's own role for the
  anti-recursion precheck: Config **`dispatchBinding`** declares it (dsh
  exposes no per-agent role on the tool-execution context) and is
  live-editable in the dsh plugin settings form. The value is a **ROOT-session
  identity**, never inherited: it is read ONLY when the calling session is the
  ROOT session (`session.header.parentSession` absent, the coordinating agent
  in this harness), where an explicit binding wins verbatim, an explicitly
  blank one fails closed (under hard enforcement the plugin logs it AND every
  Assignment-shaped dispatch fails until a role is configured), and an unset
  one reads as `project-manager` — an identity read, never fabricated. A CHILD
  session stays EMPTY whatever the field says and fails closed
  (`dispatch.anti-recursion.empty-binding` → every Assignment-shaped dispatch
  denied under hard), because a leaf must not dispatch and no configuration may
  lift that. A session the gate cannot read (the exec-less host
  `beforeDispatch` hook) falls back to EMPTY too — a path reachable ONLY from a
  host hook that supplies no calling session (in-repo, only tests drive that
  hook), while every production dispatch carries its calling agent through the
  `tools/pre-execute` listener, so the refusal never fires on a production
  dispatch path. An embedder that wires such an exec-less hook must resolve a
  session before dispatching — this field is not its recovery. The root read
  trusts `session.header.parentSession`: a host provider that does not stamp a
  child's parent would be read as root and receive the coordinator identity.
  **A coordinator that is not the root session cannot be served by this
  field.**
- **Caller-scoped engine enforcement (the dsh-only half of issue #156).** That
  binding is what makes the precheck real here: with a declared dispatcher
  identity the engine compares the **dispatching seat's own role** against the
  Assignment `Execute as` and hard-enforces it (`callerRequired` — an unset
  binding fails closed, never skipped). Hosts whose reference declares **no**
  dispatcher binding expose only the **spawn target** in their role-binding
  field — and target == `Execute as` is the compliant C5 pattern — so there the
  leg is skipped and the red line stays **prompt-level**
  (`mstar-dispatch-gates` § 承接方反递归红线).

## Files, shell, and approvals

- Prefer host search/edit tools over shell find/sed when available.
- Respect dsh approval prompts for destructive operations.
- Do not edit `$DSH_HOME` credentials or user secrets without explicit consent.

## Git and final evidence

- Git work follows `mstar-branch-worktree` and Assignment **Working branch** /
  **Branch policy**; the worktree L1/L2 gates run in-process.
- Completion reports cite concrete commands, artifacts, and commit lines when
  required.

## Gotchas

- Do not confuse dsh **`subagent`** with opencode **`task_subagent`** or Cursor
  **`subagent_type`** — the detect rows differ by tool shape.
- A renamed `subagent` tool (Config `toolName`) silently disables the dispatch
  gate AND host detection unless `dispatchTools` declares the new name (the
  plugin warns under hard enforcement).
- A renamed Agent Teams tool (`spawn_teammate` / `send_message` /
  `team_task_create` / `team_task_update`) is out of reach until its fixed name
  is added to the plugin's `TEAM_DISPATCH_TOOLS` map; team names have no Config
  key, so the map is source-only — the same caveat as renamed `subagent`
  instances, one level stricter.
- Role binding is prompt-only on dsh: always include **`Execute as`** +
  **`Act as`** + skill load in the Assignment body — there is no separate
  `agent` field.
- Session plan UI / todos are not durable SSOT unless mirrored to
  `{HARNESS_DIR}`.
- The plugin's bundled skills/commands mirror is synced by `bundle-assets`
  (gitignored, package-local) — an explicit `bundledSkillDir` /
  `skillRoots` Config override wins when a deployment wants a different
  mirror.
