- Início
- Plugins
- Fluxos e automação
- dsh-devlead (bundle)
dsh-devlead (bundle)
yankeguo/dsh-devlead/packages/bundle
DevLead mode for DeepSeek Harness: a human-only switch turns one ordinary session into a lead with eight devlead_* tools to register workspaces and create, read, continue and watch other ordinary sessions; it dispatches work to those sessions, reviews the delivery and reports, is woken through a durable notification digest when a watched session's turn settles, and can give a dispatched writing session its own Git worktree when the host provides the experimental worktree service.
Instalar
dsh plugin --profile web add github:yankeguo/dsh-devleadEste plugin está no subdiretório packages/bundle do repositório.
README
dsh-devlead
A DeepSeek Harness (DSH) plugin suite that upgrades one ordinary session into a DevLead session: it can operate DSH Workspaces and Sessions directly, create and continue other Sessions, read their state, and be woken when their state changes — so one agent can orchestrate multiple repositories inside a standard interface that the human can see and interact with.
English · 简体中文
- A DevLead session = an ordinary session + one durable switch. Once enabled, that session
gains 8
devlead_*tools, a DevLead system prompt, and a status strip above the composer. The switch itself is a human-only action; no tool can enable it. While the mode is off, the UI only shows a small composer-toolbar icon — no permanent bar on every conversation. - Dispatcher by default, not implementer. The DevLead session decides which Workspace owns each request (from registered Workspace metadata, not by reading another repository's code), dispatches a self-contained brief to a Session there, watches it, reviews the delivery, and reports. Read-only inspection is allowed for writing the brief and reviewing a delivery; deeper investigation belongs to the dispatched Session. Writes (file edits, builds, commits, pushes, deploys) belong to a Session in the owning Workspace — even when the target is the lead's own workspace — unless the human explicitly authorizes the lead to do it directly in this conversation; direct upkeep of the lead's own record files may follow that repository's instructions. Subagents and Agent Teams are not a substitute for dispatching the work to an openable ordinary Session.
- Outputs must be standard DSH entities. Workspaces are genuinely registered through
ctx.workspaceRegistry, and child sessions are real ordinary Sessions (they appear in the sidebar, can be opened, continued, and archived by a human), not subagents. This keeps every child visible and able to be taken over by the human. - Notification loop. A DevLead watches child sessions. Whenever a watched session's turn
settles (completed / failed / blocked / cancelled / …), the Host writes a notification into
the DevLead session's durable log and wakes it with a
devlead-sourced message so it can decide what to do next (follow up, dispatch new work, or report to the human). The watch index survives a Host restart / plugin reload (rebuilt from the durable records) and a disposed lead keeps observing, so a queued wake is not silently lost. See Notification loop for the cold-lead policy and Known limitations for the residual cases.
Documentation: Design notes (中文) · Publishing guide (中文) · Audit record (中文) · v0.2.0 release notes (中文) · Bundle package (中文) · Package READMEs: protocol · devlead · tool · command · client-ui
Features
| Capability | Description |
|---|---|
| DevLead mode | One durable switch per session; enabling it adds the devlead_* tools, the DevLead system prompt, and the status strip |
| Role model | Dispatch-first: determine the owning Workspace from registered metadata, send a self-contained brief to a Session there, watch it, review the delivery, report; read-only inspection is bounded to the brief and the review, writes stay in the owning Workspace's Session (including the lead's own workspace; direct work only when the human explicitly authorizes it in this conversation; record-file upkeep may follow the lead's repository instructions) |
| Standard DSH entities | Child sessions are ordinary Sessions (visible in the sidebar; humans can open and take them over); Workspaces go through workspaceRegistry, not subagents |
| Optional worktree isolation | When the Host composes DSH's experimental Git worktree service (ctx.worktrees), devlead_create_session can request isolation: "worktree" so a writing Session gets its own branch and checkout instead of sharing the Workspace checkout; detection is optional and a missing capability degrades to the shared checkout with a reported status, never an error (see Optional worktree isolation) |
| 8 model tools | Register/list Workspaces, create/read/continue/watch Sessions, read the notification digest (see Model tools) |
| Human commands | /devlead (status), /devlead on|off, /devlead reset (damaged-state recovery), /devlead watch|unwatch <session>; sessions are addressable by full id or unique prefix |
| Notification loop | A watched session's turn/end writes a durable notification and wakes an indexed DevLead through a coalescing window (configurable window; cold leads are resumed by default); the watch index is rebuilt from durable records after a Host restart, and a disposed lead keeps observing (see Notification loop) |
| Event sourcing | Mode / watch set / notifications / reset are ignorable plugin:devlead/* records with strict replay validation; survive restart / resume / fork |
| Damaged-state recovery | A corrupt plugin:devlead/* record freezes the fold, but the human-only /devlead reset (or /devlead off on a frozen fold) appends a plugin:devlead/reset record that restarts both projections from the default disabled state (see Notification loop) |
| Session projection | The devLead projection ({ enabled, sessions, notifications }) syncs state to the browser without a custom Remote API; its summary-sized companion devLeadMode ({ enabled }) rides every Session-list summary so sidebar rows can render the mark |
| UI | Off: only a small composer-toolbar icon (unobtrusive, no permanent strip). On: a DevLeadBar above the composer with the switch, watched/running counts, and the latest notification; the UI only sends /devlead on|off commands. Enabled Sessions are also marked in the sidebar session list |
| Permission model | The mode switch can only be triggered by human commands/UI; notifications are runtime notices, not human instructions (see Permission model) |
| One-command install | The bundle's cordis.patch.yml inserts all 4 runtime plugin rows; individual packages can also be installed separately |
UI
The UI has two states. The plugin deliberately never puts an attention-grabbing permanent bar on every conversation:
- Off: the composer toolbar (
conversation.input.left, next to the permission dropdown) shows only a small user-icon button; hovering it shows "Turn on DevLead mode", and clicking it sends/devlead on. There is no status strip in the conversation./devlead onworks too. - On: the icon disappears and the full DevLeadBar (
conversation.input.dock) appears above the composer: the mode switch (click sends/devlead off), the number of watched sessions, the number running, and the latest notification — or a help hint when there is none. - Marked while enabled: every listed DevLead-enabled Session carries a small DevLead mark
in the sidebar session list, after its time label (
sidebar.session.row.trailing), so the mode is recognizable after the conversation is closed. The mark reads the summary-sizeddevLeadModeprojection, so it renders for cold Sessions too without an extra Host read per row. DSH 0.2.2-rc.1 omits the trailing seat for a blank Session (no turn yet), so enabling the mode on a brand-new Session shows no mark until its first turn; the row's hover card restates "DevLead mode is on" (sidebar.session.row.hover). On hover-capable devices the shell hides the row's resting marks while the row is hovered.
Off (only the small toolbar icon):

On (the full status strip):

A real multi-session orchestration (the DevLead session creates a summary test child, the
child settles, the notification carries its last assistant excerpt back, and the DevLead
reports):

| State | UI | Interaction |
|---|---|---|
devLead projection not loaded yet | nothing renders | — |
| Mode off | the small toolbar icon button | click sends /devlead on; the surface re-renders from the projection |
| Mode on, no watched sessions | DevLeadBar: switch + watched 0 + help hint | flipping the switch sends /devlead off |
| Mode on | DevLeadBar: switch + watched N / running M + latest notification excerpt | same; the switch is disabled while a command is in flight |
| A command settles with a logical error | on: an inline error row in the strip; off: the error text in the icon tooltip | fix and retry |
- The strip and the icon only read
useProjection('devLead')and the standard Session list — no custom Remote API; the sidebar mark and its hover-card line read the Session-list summary only. Both call the injectedexecuteCommand('/devlead on'|'/devlead off'), never flip optimistically, and always follow the projection. - The browser half only renders on the Web platform; the i18n namespace is
devleadwithzhandendictionaries.
How it differs from subagent / agent-team
| Dimension | subagent / agent-team | This plugin (DevLead) |
|---|---|---|
| Identity of the work product | internal child agents of the dispatching side, usually not independently openable ordinary sessions | genuinely registered Workspaces + ordinary Sessions, visible in the sidebar, openable/continuable/archivable by humans |
| Human visibility | the human usually only sees the final summary | the human sees everything and can take over any child session at any time |
| Interaction model | the model dispatches and waits inside a single turn | the DevLead session is itself an ordinary session; the model orchestrates across turns with devlead_* — after dispatching it ends the turn and the settle wake-up starts a new one — while the human can join in the same UI |
| Lifecycle | ends with the parent turn / parent agent | child sessions exist independently and can be continued; turn/end notifications are durable and survive restarts |
| Wake-up mechanism | parent agent blocks, or team messages | watched session turn/end → durable notification → coalesced wake of the DevLead (while its agent is alive) |
| Switch | none | human-only /devlead on|off or the UI switch (toolbar icon / strip switch); no model tool can enable it |
| Best for | one-off parallel subtasks | cross-repository, long-running orchestration that needs continuous human participation and review |
Rule of thumb: use subagent / agent-team for one-off parallel subtasks; use DevLead when an agent should own several repositories over time and the human wants to see and take over every child session.
Package layout
A pnpm workspace under packages/*:
packages/
protocol/ @yankeguo/dsh-devlead-protocol pure types + constants + formatting (shared by host and browser), devLead/devLeadMode projection types
devlead/ @yankeguo/dsh-devlead core service: mode, Workspace/Session orchestration, watch index, notification delivery, strict fold
tool/ @yankeguo/dsh-tool-devlead 8 model tools + conditional system prompt
command/ @yankeguo/dsh-command-devlead the /devlead human command
client-ui/ @yankeguo/dsh-client-ui-devlead browser UI: toolbar switch + DevLeadBar strip + sidebar row mark
bundle/ @yankeguo/dsh-devlead-bundle bundle: cordis.patch.yml + dependencies
Dependency direction: protocol ← devlead ← {tool, command}; client-ui only depends on
protocol (browser side). pnpm build produces 7 lib/*.js files: protocol's index/client,
core, tool, command, and client-ui's index/client; bundle ships only the patch file and
icon, no JS. Builds use esbuild: host entries keep @deepseek-ai/*, @yankeguo/*, and node:*
external, so workspace packages stay out of the bundle and the whole process shares one
protocol/core instance. The browser entry's platform singletons come from a single source,
scripts/client-externals.mjs (it must match the DSH shell's module table; the build rejects any
@deepseek-ai/* browser import outside it, while pure third-party code may be inlined).
Each package has its own short README: protocol · devlead · tool · command · client-ui · bundle.
Every publishable package also ships plugin display metadata for the DSH plugin manager and
plugin market: locale/en.json and locale/zh.json with a user-facing
{"meta":{"title":"…","description":"…"}}, wired through files: ["locale/*.json"] and the
"./locale/*.json" export. DSH resolves <package>/locale/en.json, reads the sibling language
files as translations, and falls back to the package name / package.json description when a
field is missing; the bundle additionally keeps its manifest icon. pnpm check:packages
validates the locale files, the packaging wiring, and that no marketing superlatives slipped in,
so a new publishable package must add both locale files or CI fails.
scripts/ provides the build (build.mjs), package invariants (check-packages.mjs), profile
install (install-profile.mjs), dev overlay (dev-patch.mjs), local E2E (e2e-smoke.mjs), and
the staged-release helpers (stage-release.mjs, promote-release.mjs, trusted-publishers.sh).
Getting started
Requirements
| Item | Requirement |
|---|---|
| DSH | 0.2.2-rc.1 (plugin / bundle packages declare engines.dsh: >=0.2.2-rc.1 <0.3.0) |
| Node.js | ≥ 24.9 (local build / publish only; installing from npm needs no local Node) |
| pnpm | 12.x (CI pins pnpm@12.16.1) |
| Web UI | the DSH desktop app or dsh web; DevLead UI renders on the Web platform only |
| Local E2E | an installed DSH desktop runtime (dsh on PATH, or DSH_BIN); pnpm e2e boots an isolated instance with it |
Install from npm (recommended once published)
# Main bundle: core service + model tools + command + status strip
dsh plugin --profile desktop add @yankeguo/dsh-devlead-bundle
You can also install it from the Web UI via Plugins → Add plugin and the name
@yankeguo/dsh-devlead-bundle. Afterwards, confirm the bundle is enabled on the Plugins
page (freshly installed bundles are usually enabled by default; tick it if not) and reload the
Web page. Advanced users can install individual packages and insert the corresponding loader
rows into the profile's cordis.patch.yml themselves; the main bundle is the recommended path.
Install from a local checkout (development / acceptance)
pnpm install
pnpm build
pnpm install:profile # defaults to --profile desktop
# or point at a specific profile / DSH home
node scripts/install-profile.mjs --profile desktop --dsh-home ~/.dsh
The script does four things (--dry-run only prints the plan):
- symlinks all 6 workspace packages into
$DSH_HOME/profiles/<profile>/node_modules/@yankeguo/; - appends
@yankeguo/dsh-devlead-bundleto the profile'sdsh.profile.bundles; - backs up the profile's
package.jsonbefore writing (only when the content changes; repeated runs are idempotent); - validates every package's
exports/mainartifact plus the bundle's patch/icon before writing, and makes no changes when validation fails.
These symlinks also make DSH treat this checkout as a linked root: the plugin's
@deepseek-ai/* imports resolve to the installation's instances through peerDependencies,
avoiding a second DSH runtime in the process (see the development overlay below). DSH profile
HMR loads the host plugins live; reload the Web page for the browser half.
Uninstall with pnpm uninstall:profile.
The dependency-install-free debug path (development / E2E): an absolute-path overlay needs a
dedicated dev profile to hold the linked root (the default web profile usually does not
exist, and a profile that already enables this bundle is rejected because the loader row ids
would be duplicated):
pnpm build
# 1. create a dedicated dev profile once (copied from the shipped web template)
dsh devlead --from-default-profile web --dump-config
# 2. generate the overlay and register this checkout as that profile's linked root
pnpm dev:patch --profile devlead # optional --dsh-home / --no-link
# 3. boot the command printed by the script
dsh --profile devlead --patch .tmp/cordis.patch.yml --no-open --port 34000
Why the linked root matters: an absolute-path plugin without one resolves @deepseek-ai/* from
this repository's pnpm node_modules, producing a second DSH runtime inside the process; the
first cross-copy private-class check (for example writing a Session plugin record) then throws
gateway/internal: Receiver must be an instance of class Session. The linked root makes DSH
resolve those peerDependencies to the installation's instances (the official app-boot
profile-resolution mechanism). --no-link only generates the overlay for inspection and cannot
be used for a real boot. To undo the link, delete
<profile>/node_modules/devlead-repo.
Upgrade / uninstall
# upgrade to a specific version (once published to npm)
dsh plugin --profile desktop add @yankeguo/dsh-devlead-bundle@0.2.0
# uninstall (DSH cleans up the removed bundle selection the next time it loads the profile)
dsh plugin --profile desktop remove @yankeguo/dsh-devlead-bundle
# local checkout
pnpm install:profile # run pnpm build first, then re-link
pnpm uninstall:profile # remove links and the bundle selection
Before upgrading to a new DSH major (0.3), update the plugins' / bundle's engines.dsh and
regress; incompatible bundles are skipped by DSH.
Usage
Toggle the mode
The mode switch is human-only: run the command in any session, or click the UI switch — the DevLead icon in the composer toolbar while off, the strip switch while on.
/devlead on # enable DevLead mode for the current session (requires a live Agent)
/devlead off # disable; stops notification delivery immediately, retained records stay in the Session log
/devlead reset # human-only recovery: discard this session's DevLead state (also recovers a frozen fold)
/devlead # show status: switch, watched sessions (title/state), latest notifications, help
/devlead status # explicit alias of the above
Commands
| Command | Purpose |
|---|---|
/devlead | status: mode, whether optional worktree isolation is available, watched sessions (title / live / running / latest turn), latest notifications, help line |
/devlead status | alias of /devlead |
/devlead on | enable DevLead mode for the current session |
/devlead off | disable DevLead mode for the current session; on a frozen (damaged) fold this also performs the implicit reset and recovers the session |
/devlead reset | human-only recovery: append a plugin:devlead/reset record, discarding this session's mode / watch set / notification digest / frozen failure and returning to the default disabled state |
/devlead watch <session> | watch a session (full id, unique prefix, or bare uuid first segment) |
/devlead unwatch <session> | stop watching it |
- Session addressing supports a full id, a unique id prefix, or the uuid first segment with the
session-prefix omitted (e59ab350addressessession-e59ab350-…); when a reference matches several sessions the command returns a friendly error with the candidates (titles included) — use a longer prefix or the full id. References cannot contain spaces; one matching pair of surrounding quotes is stripped. watch/unwatchrequire the current session to have DevLead mode enabled; you cannot watch yourself; the watch set is capped (default 64, see Configuration).- The command does not accept composer attachments; every domain failure (stable
DEVLEAD_*error code) is rendered as friendly text instead of being thrown. The command lists candidates from the session listing (default up to 200 rows) and the current watch set first, then confirms a reference with the core's exhaustive resolver before mutating, so a match outside the listing window cannot be silently shadowed (full ids, unique prefixes, and bare uuid first segments all work through that resolver).
Recommended workflow
- Run
/devlead onin the session that should orchestrate several repositories (or click the DevLead icon in the composer toolbar). The model then treats that session as the dispatcher, not as the implementation site. - Decide ownership and register workspaces: decide which Workspace owns the request, then use
devlead_list_workspacesto see what is registered.devlead_use_workspacetakes the repository's absolute path; re-registering is idempotent, andcreatedefaults tofalse: a missing directory is reported asDEVLEAD_DIRECTORY_MISSINGinstead of being created. Setcreate: trueonly when the human explicitly asked to create that directory (the tool description says the same). If no repository matches, or more than one could own it, ask and stop instead of guessing or inspecting another repository first. - Let the model dispatch work:
devlead_create_session(optionalworkspace/title/preset) with a self-contained prompt — background, scope, decisions already made, deliverables, how to verify them, and what not to touch; pass the owning Workspace and omitworkspaceonly when the work truly belongs to the lead's own directory (the new Session then lives there). The child cannot see this conversation, so restate every necessary fact in the prompt. New sessions are watched by default and start their first turn immediately (a blank Session does not appear in the sidebar before its first turn, so "create + prompt" is the normal usage). If the owning Workspace already has a live Session working on the request, continue it withdevlead_prompt_sessioninstead of opening a duplicate. When the Host composes the experimental worktree service and the new Session may write concurrently with another Session in the same checkout, passisolation: "worktree"(see Optional worktree isolation); without that capability, never run two writing Sessions in the same checkout at the same time — dispatch them serially. - The child runs independently; the human can open it from the sidebar, continue the conversation, or take over at any time.
- When a child's turn settles (completed / failed / blocked / cancelled / …), the DevLead
session receives a notification excerpt and is woken; the model uses
devlead_list_sessions/devlead_get_sessionto inspect state,devlead_prompt_sessionto follow up or dispatch new work, anddevlead_notificationsto read the digest. - Before finishing a turn, the model reports to the human: which Workspaces and Sessions it created or changed, what each child was asked to do, how the latest turn ended, whether any implementation work was (exceptionally) done directly in the lead session, and anything that needs a human decision.
A request you can paste at the DevLead session:
After turning DevLead mode on: register /Users/me/Workspace/src/api and
/Users/me/Workspace/src/webapp as workspaces, then open one child session in each — fix the
CI failure in the first and add the missing README in the second. Summarize when both finish;
follow up on failures instead of claiming success.
Model tools
All tools carry when: ({ agent }) => ctx.devLead.isEnabled(agent) and are only visible in a
DevLead session:
| Tool | Arguments | Returns (canonical JSON) |
|---|---|---|
devlead_list_workspaces | {} | { workspaces } |
devlead_use_workspace | path (required, absolute), title?, create? (default false: a missing directory is an error; true creates it with mkdir -p after the human asked) | { workspace } |
devlead_list_sessions | workspace?, watched_only?, limit? | { sessions, watched } |
devlead_create_session | workspace?, prompt?, title?, watch? (default true), preset?, isolation? (default none; worktree = best-effort isolated checkout) | creation result (including the isolation outcome) |
devlead_get_session | session (required) | { session } |
devlead_prompt_session | session, prompt (required), watch? (default true) | prompt result |
devlead_watch_session | session, watching (required bool) | watch mutation result |
devlead_notifications | limit? (default 20) | { notifications, watched } |
Tools translate the core's DevLeadError into a HarnessError with a stable code. When reading
state, trust lastTurn / lastTransition / lastError / lastAssistant instead of guessing.
Every session argument accepts the full Session id, a unique id prefix, or the uuid first
segment with the session- prefix omitted — e59ab350 resolves session-e59ab350-… when it is
unique. A first segment shared by two Sessions is rejected as ambiguous with the candidate ids
instead of being guessed, and a no-match error says the id may be incomplete or truncated and
points at devlead_list_sessions.
Optional worktree isolation
Multiple Sessions dispatched into the same checkout can interleave their edits (mixed working-tree hunks are expensive and risky to separate). DevLead can borrow DSH's experimental Git worktree facility to give one dispatched Session its own checkout, but only when that facility is actually composed — the plugin never depends on it.
-
What it does.
devlead_create_sessionacceptsisolation: "worktree". After the Session is created (and attached to the Workspace) but before its first prompt is admitted, DevLead asks the Host's worktree service to create a branch and checkout for that Session's Agent and enters it. The Session keeps its Workspace membership and its immutable header cwd, so the sidebar grouping is unchanged; only its working directory (and therefore its shell/file operations) moves into<checkout root>/.agents/worktrees/<generated name>. -
Capability detection. DevLead looks the service up optionally (
ctx.get('worktrees')) and duck-types itscreatemethod. No@deepseek-ai/dsh-experimental-*package is imported or required, no experimental service is in anyinjectarray, and the default Web composition (base + web-app) does not provide it: isolation is therefore off by default. Enable it by composing the experimental worktree service in the profile (for example the shipped "Change requests & worktrees" experimental bundle) — never by installing anything extra just for DevLead. -
Degradation. A missing or malformed service, or a failed
git worktreerun, never fails the dispatch: the Session stays in the Workspace checkout, the creation result carries an explicit outcome, and an initial prompt is still admitted:isolation: { requested: 'worktree', status: 'unavailable' | 'failed', reason }. When the outcome is notworktree, the lead must serialize writing Sessions instead of running them in the same checkout concurrently.status: 'none'means no isolation was requested. (One defensive corner: if the Host cannot make the newly created Session live, isolation reportsunavailable, and prompting it still fails with the pre-existingDEVLEAD_SESSION_UNAVAILABLEerror — the shipped Session controller never produces that state because it returns a live Agent.) -
Constraints worth knowing. The worktree starts at the source checkout's
HEAD; uncommitted, staged, untracked, and ignored files are not copied, so commit or stash anything the isolated Session must see. The child Session's sandbox policy must allow writing the checkout and the shared Git administration directory. The lead owns the branch: review the diff and merge or cherry-pick it when the work is accepted; nothing is merged automatically. Isolation applies only to newly created Sessions — an existing Session cannot be moved into a worktree (create a new one, or serialize the existing Sessions). -
Visibility. The result field reports
path/branch/baseCommiton success, and/devlead statusalways printsWorktree isolation: available|unavailable, so a human can tell whether the Host could isolate at all. The DevLead system prompt advertises worktree isolation only when the capability is present; theisolationparameter's own description always documents it as best-effort and capability-gated, never as available.
Notification loop
- The watch set is durable state: a
plugin:devlead/watchrecord replaces the whole watch set; only watched sessions produce notifications. - Trigger: every
turn/endof a watched session (completed / failed / blocked / cancelled / max-tokens / …) appends aplugin:devlead/notificationrecord to the DevLead session's log, containing a deterministic id, the session title, the turn number, the transition kind, and either a failure excerpt or the last assistant-text excerpt before the turn settled (single line, max 240 characters, stored in the notification'ssummaryfield). If the lead Session log is attached, the record is appended immediately and its durability checkpoint is requested right away; if the lead is cold, the notification waits in an in-memory queue and is written the moment the lead becomes live. - Retention: the projection keeps the latest 64 entries by constant; older records remain in
the Session log, and
/devlead/ the strip show the most recent few. - Coalescing window: notifications are queued and merged for
notificationBatchMs(default 400 ms); when the window ends and the DevLead agent is still alive, it is woken with a message sourceddevlead(form: 'notice'). If the agent is running, the followup queues for the next turn; anagent/statustransition to idle also flushes the queue once. - Cold and disposed DevLead sessions: the hub keeps an indexed lead even after its Agent is
disposed, so a watched session's
turn/endis still observed and recorded. At plugin load (Host restart / plugin reload) the watch index is rebuilt from durable state. The Host cannot report application-ready before that rebuild:[Service.init]waits for the durable corpus to become readable (bounded at 2 s; a fresh Host with no stored Sessions proceeds immediately, a steady Host pays one corpus comparison), then runs exactly one authoritative cold scan — persisted Sessions are listed, the projection cache pre-filters them without log I/O, candidates are exactly folded from theirplugin:devlead/*records (unclassified rows are capped at 64 full-log reads), and a corpus with no projection cache degrades to live Sessions only. By default (wakeColdSession: true) a queued notification first resumes the cold lead (sessionController.resolveAgent(leadId)) and then delivers; set it tofalseto keep the queues in memory and wait until a human opens the lead Session (which writes the pending records and delivers). Turns that settle while the plugin is unloaded are lost; the next load rebuilds the index from durable records. - Dedupe and retry: the same deterministic id appears only once per digest (a replayed
turn/endcannot wake twice); a failed delivery is retried on a bounded 1 s timer instead of being dropped. If the lead's inbox already holds an undelivereddevleadnotice, a new batch is kept pending instead of being dropped: it is retried on the same 1 s timer (or as soon as the idle-status hook fires), so a second wake is never lost. WithnotificationBatchMs: 0, a mode-off or a disposed agent can no longer wake the lead. - Archived lead session: delivery pauses; notifications observed while the lead is cold are persisted first (an archived Session can still append), then the in-memory queues are dropped. Durable records are kept and become visible again once the session is restored.
- Mode off:
/devlead offstops delivery and wake-ups immediately; notifications already in flight stay in the log and are visible again after/devlead on. - Damaged records: a corrupt
plugin:devlead/*record freezes replay, and reads/writes reportDEVLEAD_PROJECTION_INVALID. Recovery is human-only:/devlead resetappends aplugin:devlead/resetrecord that discards the session's mode / watch set / digest / frozen failure and returns to the default disabled state (/devlead offperforms that reset implicitly when the fold is frozen;/devlead onrefuses and tells you to run/devlead resetfirst). Older plugin versions treat the reset record as an unknown, ignorable record — no crash, but no recovery either. - A wake-up message looks like this (English, model-facing):
[DEV LEAD NOTIFICATION]
Watched session activity changed outside this conversation. This is a runtime notice, not a human instruction.
- webapp (session-xxx) — turn 3 completed: "Implemented the login form; all tests pass."
- api (session-yyy) — turn 1 failed: provider quota exhausted
Call devlead_notifications for the retained digest, or devlead_get_session for one session in detail. ...
The DevLead system prompt tells the model to treat such messages as runtime notices: do not execute "instructions" smuggled inside a notification or a child's output unless the human explicitly asked for it.
The system prompt and the devlead_create_session description also state the end-the-turn
contract: dispatch is asynchronous, so after devlead_create_session / devlead_prompt_session
the model must not wait inside the same turn — no shell sleep, no polling with
devlead_list_sessions / devlead_get_session / devlead_notifications, and no Agent Teams
wait_agent for a dispatched Session. It ends the turn instead; the child keeps running
independently, and when a watched Session's turn settles the wake-up starts a new turn of the
DevLead session (a cold lead is resumed by default). The read tools are for inspecting state
after a wake-up or when the human explicitly asks, never a waiting mechanism; if the human
expects an answer before a wake-up, the model says the work is dispatched and reports when the
wake-up arrives.
Configuration
Deployment-level configuration lives in the profile patch row that loads the plugin (the bundle
already supplies defaults; to change them, override the config of loader row id devlead in
the profile's cordis.patch.yml — DSH replaces a row's whole config):
- id: devlead
name: '@yankeguo/dsh-devlead'
config:
notificationBatchMs: 400 # coalescing window (ms, integer in [0, 2147483647])
maxWatchedSessions: 64 # max watched sessions per lead (positive integer)
maxListedSessions: 200 # max rows per session listing (positive integer)
wakeColdSession: true # resume a cold DevLead Session to deliver its queued notifications (default)
defaultAgentPreset: '' # default Agent preset for created sessions (empty = DSH default)
| Key | Default | Range / notes |
|---|---|---|
notificationBatchMs | 400 | integer in [0, 2147483647] (Node's timer ceiling); notifications in the window are merged into one wake-up message |
maxWatchedSessions | 64 | positive integer; a watch operation over the cap reports DEVLEAD_WATCH_LIMIT |
maxListedSessions | 200 | positive integer; row cap for listing calls (devlead_list_sessions / /devlead status). Identity resolution is not limited by it, and a watched-only listing never truncates below the watch set |
wakeColdSession | true | when true, a cold lead Session with queued notifications is resumed (sessionController.resolveAgent) to deliver them; set false to keep the queues in memory and wait for a human to open the lead Session. Observation is not affected by this flag |
defaultAgentPreset | '' | default value used when a created session names no preset; an empty string means no preset |
tool / command / client-ui have no deployment configuration; all domain limits (watch cap,
notification retention, etc.) are owned by the core service and protocol constants.
Optional worktree isolation has no configuration key: whether it exists is decided entirely by
the profile composition (is the experimental worktree service mounted?) and whether it is used is
decided per dispatch by the isolation argument. To disable it, stop passing
isolation: "worktree" (or remove the experimental worktree service from the profile); a Host
without it keeps the default single-checkout behavior and reports unavailable.
Permission model
- Only humans can flip the mode:
/devlead on|off, or the UI switch (composer-toolbar icon / strip switch — both merely send the same command). No model tool can enable it; everydevlead_*tool is gated bywhen, so an ordinary session never sees them. This matches the design principle of never making permission decisions on the human's behalf. - Writes require the mode: every
devlead_*write tool requires a live calling Agent and is only visible while the mode is enabled;createSession,promptSession, andwatchSessionrevalidate live + mode in the core (DEVLEAD_NOT_ENABLED/DEVLEAD_AGENT_NOT_LIVE), and the command side only mutates the watch set within the human-authorized scope. Workspace registration is a Host-level operation too, but it is no longer caller-free: since 0.2.0 the coreuseWorkspace(agent, { path, title?, create? }, signal?)requires the same live lead Agent and an enabled mode (DEVLEAD_NOT_ENABLED/DEVLEAD_AGENT_NOT_LIVE), and it never creates a missing directory unless the caller explicitly passescreate: true. - Notifications are not instructions: both wake-up messages and child output can embed text; the DevLead system prompt requires treating notifications as runtime notices and never performing unauthorized work or making permission decisions from them.
- Ordinary session permissions are unchanged: child sessions are ordinary Sessions following DSH's sandbox / permission mode; this suite offers no privilege-escalation path and does not change DSH's security boundary.
- Installing is trusting: plugins run in the Host process; installing a third-party plugin is equivalent to trusting its code. The mode switch only controls whether DevLead capability is enabled — it is not a security sandbox switch.
Known limitations
- One durable mode per Session: once enabled it stays enabled across DSH restarts and session
resumes; a human must run
/devlead off. - Cold leads are resumed by default: after a Host restart / plugin reload the watch index is
rebuilt from durable records, and a queued notification resumes the cold lead Session unless
wakeColdSessionis explicitly set tofalse(then the queues wait in memory for a human to open the lead). Resume requires the Host to compose a Session activator; without it the records stay durable and are delivered when the Session is opened. - The watch index is process-local transport: a disposed lead keeps observing and keeps recording notifications, and the index is rebuilt after a restart/reload; only while the plugin itself is unloaded are settled turns unobserved (the events are not replayed per watcher), so a turn that ends during an unload is not notified.
- 64 retained notifications: the projection truncates at that constant; older records remain in the Session log.
- Default caps of 64 watched / 200 listed: configurable; the command's candidate list is capped by that limit. Sessions beyond the listing are still addressable by full id or unique prefix (core identity resolution is exhaustive and not capped by the listing limit). The command lists candidates and ambiguity hints from the bounded listing + watch set, then confirms any prefix with the core resolver before a watch mutation.
- No Git repository creation / clone / general worktree management: use shell tools, then register the Workspace. The optional per-Session isolation described above only borrows an already-composed experimental DSH worktree service; the plugin itself performs no Git operations — it does not create repositories, clone, fetch, merge, or manage branches. The lead must arrange review and merge/cherry-pick of an isolated branch (by dispatching it to a Session or leaving it to the human), and existing Sessions cannot be re-homed into a worktree.
- Worktree isolation is best-effort and capability-gated: without the experimental worktree service (the default Web profile) it is simply unavailable; with it, creation can still fail under a restrictive sandbox, in a non-Git checkout, or when the generated branch or checkout path already exists, and the Session then continues in the shared checkout (reported in the result). Uncommitted changes are not copied into a worktree.
- No cross-process / cross-DSH-instance orchestration; no custom Remote API; the UI only reads the projection and goes through commands.
- Small off-state entry point: to avoid a permanent strip on every conversation, the off
state is only a composer-toolbar icon; remember the
/devlead oncommand if that is not obvious enough. - Sidebar mark backfill and blank rows: the mark renders from the summary-sized
devLeadModeprojection. A Session enabled before this feature shipped shows the mark only once its projection cache carries the key (open that Session once, or toggle the mode again); v1 does not backfill on Host startup. DSH rc.1 does not render the trailing seat on a blank (no-turn-yet) Session row, so a freshly enabled blank Session shows the mark only after its first turn. - Child sessions are ordinary sessions: they appear in the sidebar and consume model quota normally — they are not hidden subagents.
- Damaged DevLead records fail closed but are recoverable: replay freezes at the first
corrupt
plugin:devlead/*record and tool/status reads and writes reportDEVLEAD_PROJECTION_INVALID. The human-only/devlead reset(or/devlead offon a frozen fold) appends aplugin:devlead/resetrecord that discards the session's DevLead state (mode, watch set, digest, frozen failure) and returns to the default disabled state; the old records stay in the log but are no longer folded./devlead onrefuses while frozen and asks for the reset. Older plugin versions ignore the reset record as an unknown record (no crash, no recovery).
Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
| No DevLead icon / strip after installing | bundle not enabled (tick it on the Plugins page), page not reloaded, or DSH not restarted so preset changes take effect |
No /devlead command or devlead_* tools | bundle not enabled / skipped by DSH (check the DSH version and engines.dsh); a local checkout needs pnpm build first |
The local install script says run "pnpm build" first | run pnpm build, then pnpm install:profile |
/devlead on says "no longer live" | the Agent is no longer the live instance in the registry; reopen the session from the sidebar and retry |
/devlead watch reports ambiguity | the prefix matches several sessions; use a longer prefix or the full id from the candidates in the error |
/devlead watch reports the mode is off | run /devlead on first; watch-set changes require the mode |
| A child's turn settled but the DevLead was not woken | the mode is off, the session is not watched, the lead is cold and wakeColdSession: false (queues wait for the Session to be opened), the Host composes no Session activator to resume it, the notification is still inside the coalescing window, or the wake is waiting for an undelivered notice to leave the inbox (retried within 1 s) |
| The DevLead switch seems to do nothing | the command may still be running (the switch is disabled); when enabling fails, the error shows inline in the strip or in the icon tooltip |
| The strip's counts disagree with the sidebar | the strip computes from the projection's watch set and the standard Session list; a just-changed value can lag briefly — reload the page to re-read |
| Cannot find the switch while off | it is the user icon in the composer toolbar (next to the permission dropdown); hover shows "Turn on DevLead mode"; /devlead on also works |
| DevLead state survives a restart | expected: mode / watch set / notifications are durable records; run /devlead off when no longer needed |
Under a dev overlay, /devlead on throws gateway/internal: Receiver must be an instance of class Session | the overlay has no linked root (stale generated overlay, hand-written patch, or the profile was swapped). Re-run pnpm dev:patch --profile <dev> and confirm <profile>/node_modules/devlead-repo exists, then restart; or use pnpm e2e to run the correct flow |
pnpm dev:patch reports profile not found / already enables bundle | a dedicated dev profile is required: dsh devlead --from-default-profile web --dump-config; a profile that enables this bundle cannot take a second loader row with the same id |
Passing title to an already-registered Workspace has no effect | expected: title only applies when registering a new Workspace record; an existing record keeps its title |
/devlead / tools report DEVLEAD_PROJECTION_INVALID | a plugin:devlead/* record in this Session's log is corrupt and replay froze. Run /devlead reset (human-only; discards the mode, watch set, and notification digest) — /devlead off does that implicitly, /devlead on refuses until the reset. Old plugin versions cannot recover. |
devlead_use_workspace reports DEVLEAD_DIRECTORY_MISSING | the directory does not exist and creation is not allowed by default. Create it first, or retry with create: true once the human explicitly asked for that directory |
| A cold lead was resumed unexpectedly after a restart | expected default: wakeColdSession is true since 0.2.0; set it to false to keep queued notifications in memory until a human opens the lead Session |
devlead_create_session reports isolation.status: "unavailable" | the profile does not compose DSH's experimental worktree service (the default Web profile never does); either enable the experimental worktree bundle or dispatch writing Sessions serially — the Session was still created and works in the shared checkout |
devlead_create_session reports isolation.status: "failed" | the worktree service is present but creation failed (restrictive sandbox, existing branch name, non-Git checkout, existing checkout path); the reason carries the Git/service error, and the Session continues in the shared checkout |
Optional plugins and dependencies
DevLead is built for the default DSH Web composition (dsh-base + dsh-web-app) and needs no
experimental bundle and no third-party plugin. Every non-default capability is read through
optional service lookups (ctx.get(...)) and degrades to a documented behavior instead of
failing: the Workspace registry, Session controller, Session title service, and Session query
service are optional peers; the Session projection cache and persistence services are probed to
rebuild the watch index after a restart (missing ones degrade cold rehydration to live Sessions
only); and the experimental Git worktree service is only borrowed for opt-in isolation. The full package-by-package judgment table, including the client-side
dsh.client.inject ids, is in docs/OPTIONAL-DEPENDENCIES.md.
Development and verification
pnpm typecheck # whole-repo TS check (strict + exactOptionalPropertyTypes + noUnused*)
pnpm test # unit tests (vitest)
pnpm build # esbuild artifacts (7 lib/*.js)
pnpm check:packages # package invariants: versions / engines.dsh / peer rules / module table / path aliases / lockfile / CI pins
pnpm verify # typecheck + test + build
pnpm e2e # local E2E: isolated DSH_HOME + real DSH Web + /devlead on/watch/notification/off
pnpm dev:patch # generate the overlay and register the linked root (see "Install from a local checkout")
pnpm e2e defaults to the patch loading flow (equivalent to the dev overlay above);
pnpm e2e --flow installed uses the install-profile package-link flow. It runs locally only
(needs an installed DSH runtime) and CI does not include it. Both flows drive the real browser
HTTP API. Since 0.2.0 the smoke test runs multiple phases on the same scratch root/profile:
phase 1 is the default-composition run (/devlead on, projection flip, watching a real child
session, the watched child's turn/end → durable notification → visible projection, and
Worktree isolation: unavailable proving the no-experimental-plugin posture); phase 1b restarts
the same flow against the same throwaway DSH_HOME and verifies cold lead rehydration (the
cold lead's projection still folds from the durable log, the cold child Session is resumed, its
next turn/end produces a new notification, and /devlead status + /devlead off work after
the lead is resumed); phase 2 adds @deepseek-ai/dsh-experimental-forge-bundle to the throwaway
profile's dsh.profile.bundles, restarts with a probe command, and asserts the worktree happy
path end to end (Worktree isolation: available; creation result status: 'worktree' with
non-empty path/branch/baseCommit; git worktree list and the branch exist; baseCommit
equals the checkout HEAD; the child's effective cwd is the worktree path while its header cwd
still points at the Workspace; the Session still belongs to that Workspace). Per-phase and total
check counts are 61/61 per flow (phase 1 = 27, phase 1b = 12, phase 2 = 22);
--no-worktree skips only the experimental phase, reporting 39/39 (phases 1 + 1b) for
debugging. The phase-2 probe is a private
test-only package under scripts/e2e-probe/, not a published or workspace package. The profile
mutations happen only in the throwaway profile under os.tmpdir(); no pnpm install, network, or
lockfile change is involved.
Test layout (details in docs/DESIGN.md §9, Chinese):
protocol: formatting / transition detection / shared types and other pure functions;devlead: strict fold (mode / watch / notifications / corrupt records / truncation / reset recovery), strict decoders (field sets / time ranges), notification pump (coalescing / dedupe / retry / queued-notice retention / cold sessions / dispose), cold lead rehydration (fake Session query + projection cache), promptSession auto-watch compensation, Workspace and Session orchestration (fake services), listing order and truncation, identity resolution (addressable beyond the listing window);tool:whengating, argument validation, value rendering, error translation, execute forwarding and cancellation-signal forwarding for every tool, and the policy text contract (dispatch-first role, write boundary, ownership resolution, opt-in directory creation);command:/devleadgrammar parsing (includingreset), session prefix resolution (unique / ambiguous / missing), status rendering, frozen-fold recovery, concurrent title/body consistency, every error path;client-ui: formatting / command-result extraction / admission state machine / style-injection contract and other pure functions, plus React render tests (react-test-renderer) for the two entry points and the admission hook, and the cross-instance style-tag refcount.
Local end-to-end acceptance: pnpm verify && pnpm e2e. The phased E2E boots an isolated DSH:
phase 1 walks /devlead on → watch a real child session → the child's turn settles → the
notification reaches the lead projection; phase 1b restarts it and verifies cold lead rehydration
(cold child resumed, new notification after the restart); phase 2 re-composes the experimental
forge bundle and verifies the worktree happy path end to end. --flow patch and --flow installed
cover both loading paths.
Publishing
Once the packages are on npm, a single command installs everything:
dsh plugin --profile desktop add @yankeguo/dsh-devlead-bundle
PRs and pushes to main run verify.yml:
pnpm install --frozen-lockfile → typecheck → test → build → check:packages.
Releases use npm staged publishing so a release is never partially public. Pushing a v*
tag (or dispatching publish.yml with an existing release_tag
after a repair) first validates that the tag matches the version of all 6 publishable packages
and the root package.json, then installs, typechecks, tests, builds, checks the package
invariants, and finally stages every package in dependency order through npm stage publish
with a keyless, short-lived OIDC token (id-token: write; no NPM_TOKEN secret; provenance
is attached on a public repository). Staging publishes nothing: the registry holds the versions
until a maintainer approves them. CI has no permission to approve: npm stage approve
requires a maintainer's logged-in session and 2FA.
Each package needs a one-time trusted-publisher configuration that allows staged publishes
(--allow-stage-publish); an old --allow-publish configuration must be revoked and recreated:
npm login
scripts/trusted-publishers.sh # configures all 6 packages (--dry-run / --list available)
After CI stages the versions, a maintainer approves them from a terminal and the script creates
the GitHub Release from docs/releases/v<version>.md:
npm login
node scripts/promote-release.mjs --version 0.3.0 --list-only # inspect the staged versions
node scripts/promote-release.mjs --version 0.3.0 # approve (2FA prompt) + gh release
The setup and the full release flow (version bump, pre-flight, tag push, re-staging an existing
tag, approve, partial-failure recovery, troubleshooting) are documented in
docs/PUBLISHING.md (Chinese).
License
Plugins relacionados
webnovel-writer
lingfengqaq/webnovel-writer
loopx (dsh-loopx-plugin)
loopx-project/loopx
ouroboros (dsh-plugin)
q00/ouroboros
loopx (dsh-loopx-plugin)
huangruiteng/loopx