Saltar al contenido principal
Y

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-devlead

Este plugin se encuentra en el subdirectorio packages/bundle del repositorio.

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

CapabilityDescription
DevLead modeOne durable switch per session; enabling it adds the devlead_* tools, the DevLead system prompt, and the status strip
Role modelDispatch-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 entitiesChild sessions are ordinary Sessions (visible in the sidebar; humans can open and take them over); Workspaces go through workspaceRegistry, not subagents
Optional worktree isolationWhen 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 toolsRegister/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 loopA 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 sourcingMode / watch set / notifications / reset are ignorable plugin:devlead/* records with strict replay validation; survive restart / resume / fork
Damaged-state recoveryA 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 projectionThe 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
UIOff: 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 modelThe mode switch can only be triggered by human commands/UI; notifications are runtime notices, not human instructions (see Permission model)
One-command installThe 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 on works 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-sized devLeadMode projection, 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):

Off: the DevLead icon in the composer toolbar

On (the full status strip):

On: the DevLead status strip above the composer

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):

DevLead conversation example

StateUIInteraction
devLead projection not loaded yetnothing renders—
Mode offthe small toolbar icon buttonclick sends /devlead on; the surface re-renders from the projection
Mode on, no watched sessionsDevLeadBar: switch + watched 0 + help hintflipping the switch sends /devlead off
Mode onDevLeadBar: switch + watched N / running M + latest notification excerptsame; the switch is disabled while a command is in flight
A command settles with a logical erroron: an inline error row in the strip; off: the error text in the icon tooltipfix 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 injected executeCommand('/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 devlead with zh and en dictionaries.

How it differs from subagent / agent-team

Dimensionsubagent / agent-teamThis plugin (DevLead)
Identity of the work productinternal child agents of the dispatching side, usually not independently openable ordinary sessionsgenuinely registered Workspaces + ordinary Sessions, visible in the sidebar, openable/continuable/archivable by humans
Human visibilitythe human usually only sees the final summarythe human sees everything and can take over any child session at any time
Interaction modelthe model dispatches and waits inside a single turnthe 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
Lifecycleends with the parent turn / parent agentchild sessions exist independently and can be continued; turn/end notifications are durable and survive restarts
Wake-up mechanismparent agent blocks, or team messageswatched session turn/end → durable notification → coalesced wake of the DevLead (while its agent is alive)
Switchnonehuman-only /devlead on|off or the UI switch (toolbar icon / strip switch); no model tool can enable it
Best forone-off parallel subtaskscross-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

ItemRequirement
DSH0.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)
pnpm12.x (CI pins pnpm@12.16.1)
Web UIthe DSH desktop app or dsh web; DevLead UI renders on the Web platform only
Local E2Ean installed DSH desktop runtime (dsh on PATH, or DSH_BIN); pnpm e2e boots an isolated instance with it
# 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):

  1. symlinks all 6 workspace packages into $DSH_HOME/profiles/<profile>/node_modules/@yankeguo/;
  2. appends @yankeguo/dsh-devlead-bundle to the profile's dsh.profile.bundles;
  3. backs up the profile's package.json before writing (only when the content changes; repeated runs are idempotent);
  4. validates every package's exports/main artifact 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

CommandPurpose
/devleadstatus: mode, whether optional worktree isolation is available, watched sessions (title / live / running / latest turn), latest notifications, help line
/devlead statusalias of /devlead
/devlead onenable DevLead mode for the current session
/devlead offdisable DevLead mode for the current session; on a frozen (damaged) fold this also performs the implicit reset and recovers the session
/devlead resethuman-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 (e59ab350 addresses session-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 / unwatch require 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).
  1. Run /devlead on in 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.
  2. Decide ownership and register workspaces: decide which Workspace owns the request, then use devlead_list_workspaces to see what is registered. devlead_use_workspace takes the repository's absolute path; re-registering is idempotent, and create defaults to false: a missing directory is reported as DEVLEAD_DIRECTORY_MISSING instead of being created. Set create: true only 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.
  3. Let the model dispatch work: devlead_create_session (optional workspace / 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 omit workspace only 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 with devlead_prompt_session instead 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, pass isolation: "worktree" (see Optional worktree isolation); without that capability, never run two writing Sessions in the same checkout at the same time — dispatch them serially.
  4. The child runs independently; the human can open it from the sidebar, continue the conversation, or take over at any time.
  5. 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_session to inspect state, devlead_prompt_session to follow up or dispatch new work, and devlead_notifications to read the digest.
  6. 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:

ToolArgumentsReturns (canonical JSON)
devlead_list_workspaces{}{ workspaces }
devlead_use_workspacepath (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_sessionsworkspace?, watched_only?, limit?{ sessions, watched }
devlead_create_sessionworkspace?, prompt?, title?, watch? (default true), preset?, isolation? (default none; worktree = best-effort isolated checkout)creation result (including the isolation outcome)
devlead_get_sessionsession (required){ session }
devlead_prompt_sessionsession, prompt (required), watch? (default true)prompt result
devlead_watch_sessionsession, watching (required bool)watch mutation result
devlead_notificationslimit? (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_session accepts isolation: "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 its create method. No @deepseek-ai/dsh-experimental-* package is imported or required, no experimental service is in any inject array, 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 worktree run, 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 not worktree, 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 reports unavailable, and prompting it still fails with the pre-existing DEVLEAD_SESSION_UNAVAILABLE error — 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 / baseCommit on success, and /devlead status always prints Worktree 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; the isolation parameter'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/watch record replaces the whole watch set; only watched sessions produce notifications.
  • Trigger: every turn/end of a watched session (completed / failed / blocked / cancelled / max-tokens / …) appends a plugin:devlead/notification record 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's summary field). 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 sourced devlead (form: 'notice'). If the agent is running, the followup queues for the next turn; an agent/status transition 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/end is 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 their plugin: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 to false to 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/end cannot 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 undelivered devlead notice, 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. With notificationBatchMs: 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 off stops 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 report DEVLEAD_PROJECTION_INVALID. Recovery is human-only: /devlead reset appends a plugin:devlead/reset record that discards the session's mode / watch set / digest / frozen failure and returns to the default disabled state (/devlead off performs that reset implicitly when the fold is frozen; /devlead on refuses and tells you to run /devlead reset first). 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)
KeyDefaultRange / notes
notificationBatchMs400integer in [0, 2147483647] (Node's timer ceiling); notifications in the window are merged into one wake-up message
maxWatchedSessions64positive integer; a watch operation over the cap reports DEVLEAD_WATCH_LIMIT
maxListedSessions200positive 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
wakeColdSessiontruewhen 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; every devlead_* tool is gated by when, 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, and watchSession revalidate 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 core useWorkspace(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 passes create: 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 wakeColdSession is explicitly set to false (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 on command if that is not obvious enough.
  • Sidebar mark backfill and blank rows: the mark renders from the summary-sized devLeadMode projection. 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 report DEVLEAD_PROJECTION_INVALID. The human-only /devlead reset (or /devlead off on a frozen fold) appends a plugin:devlead/reset record 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 on refuses while frozen and asks for the reset. Older plugin versions ignore the reset record as an unknown record (no crash, no recovery).

Troubleshooting

SymptomLikely cause / fix
No DevLead icon / strip after installingbundle 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_* toolsbundle 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" firstrun 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 ambiguitythe prefix matches several sessions; use a longer prefix or the full id from the candidates in the error
/devlead watch reports the mode is offrun /devlead on first; watch-set changes require the mode
A child's turn settled but the DevLead was not wokenthe 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 nothingthe 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 sidebarthe 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 offit 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 restartexpected: 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 Sessionthe 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 bundlea 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 effectexpected: title only applies when registering a new Workspace record; an existing record keeps its title
/devlead / tools report DEVLEAD_PROJECTION_INVALIDa 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_MISSINGthe 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 restartexpected 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: when gating, 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: /devlead grammar parsing (including reset), 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

MIT

Plugins relacionados