dsh-plugin-task-coordinator
Codex-style cross-task coordination for DeepSeek Harness: list, inspect, spawn, message and steer top-level sessions from a supervisor agent.
- Stars
- 2
- Language
- JavaScript
- Created
- Sep 4, 2026
- Updated
- Sep 7, 2026
Introduction
Codex-style cross-task coordination · a supervisor plugin for DeepSeek Harness
What is this · Screenshots · Quick start · Tools · Architecture · Host contract · Changelog · Chinese
What is this
Once installed, you just say what should run in parallel — the task_* tools handle every step of the orchestration:
Split this into three tasks and run them in parallel: A researches the approach,
B builds the prototype, C runs the tests. Summarize for me when they finish.
What happens behind the scenes: you (plain language) → supervisor session → decomposition analysis → an approval card you confirm → batch-spawned task sessions → each task reports its result back when done → the supervisor summarizes. You never watch a single step in between, but the decision points stay in your hands.
- This is a DSH plugin: any top-level session can discover tasks, read progress, spawn tasks and deliver instructions;
- Spawned tasks appear in the GUI session list immediately (the same
api-session/addedevent the sidebar consumes); - Every cross-task message is stamped with a
coordinatorsource — visible and attributable in the target session's transcript; - One prerequisite: DSH Desktop is installed and starts (the plugin never launches the host for you).
📌 Host contract verified on DSH 0.1.2-alpha.1; every capability passed real-host end-to-end testing after restart (criteria in Host contract).
Screenshots
![]() | ![]() |
![]() | ![]() |
Live GUI captures — the same four shots are declared in screenshots.json for the dsh-market detail view.
Quick start
Prerequisites
- DSH Desktop (contract verified on 0.1.2-alpha.1);
- Node.js
^22.19.0 || >=24(the host runtime usually satisfies this already); - PowerShell (the deploy script is
.ps1).
Install (one command)
🛒 Listed on awesome-dsh-plugin (Workflow & Automation) — with the in-app dsh-market plugin browser, search “task-coordinator” and install/upgrade with one click. The git-clone route below is the no-market equivalent.
git clone https://github.com/Kayungko/dsh-plugin-task-coordinator.git
cd dsh-plugin-task-coordinator
pwsh install.ps1 -Source .
The script does three things: copies the plugin into the profile's node_modules/ (no pnpm install, the lockfile stays untouched), registers the dependency + bundle in the profile manifest, and updates .package-map.json — everything is backed up first into backups/<timestamp>/.
Restart DSH Desktop afterwards — any session can then use the eleven tools and the /tasks command.
💡
install.ps1defaults-Sourceto$PSScriptRoot/plugin(workspace layout); when running from the plugin repo itself, pass-Source .explicitly. Re-running is safe: file copies are idempotent and manifest registration de-duplicates.
Verify
After the restart, send this to any session:
List the currently visible tasks
It calls task_list and returns the task list (an empty list is a valid answer) — the tools are mounted ✅
Uninstall: pwsh install.ps1 -Source . -Uninstall (also takes effect after restart).
Directing the supervisor (prompting that actually works)
The model only coordinates when it can map your words to the tools. Vague prompts like "you may use the /task plugin whenever you want" are discretionary — sessions tend to default to working solo (field-verified failure mode). Two rules:
- Name the tools, use imperative mood. Example: "Split the remaining work with
task_spawn_batchinto parallel sub-task sessions (give them a team name); collect results withtask_wait. Don't do everything in this session." - In /goal mode the coordination mandate must live inside the goal objective — every continuation round re-anchors on that text alone. Recommended objective:
As the supervisor session, take over the remaining development: ① splittable work MUST be dispatched to parallel sub-task sessions via task_spawn_batch (attach a team name) — do not do everything yourself; ② collect results with task_wait and integrate them; ③ sub-task sessions may further parallelize with their own subagents; ④ push to remote main at every milestone.
Loose wordings ("/task plugin", "coordinate things") are recognized too — the bundled skill carries an alias table and the tool descriptions carry trigger context since 0.9.0 — but the imperative template above is the reliable form, especially for goal objectives.
The eleven tools
| Tool | Purpose |
|---|---|
task_list | List coordination-visible tasks with stable session ids, status, titles, todo/goal progress; filterable by team |
task_progress | Read one task in depth: live/cold state, queued messages, conversation tail, todos, goal |
task_send | Deliver a visible follow-up prompt (mode: queue or steer; reference links an earlier instruction); returns a messageId plus a queueDepth receipt {nextTurn, nextStep} (post-send; one next-turn message is consumed per round, so depth N ≈ N rounds before it is read) |
task_spawn | Create + name + kick off a brand-new task (title follows the MMDD|type|topic rule; groupable via team); returns a correlationId; appends the report-back convention by default; the child attaches to the caller's workspace, and an explicit cwd exactly matching a workspace path is upgraded to that workspace's attachment (0.12.0); optional provider+model (+reasoningEffort) select the child's LLM route, installed before the kickoff (0.13.0) |
task_confirm | Present a decomposition/dispatch plan as an interactive approval card and block until the user answers; approval mints a single-use confirmationId |
task_confirm_select | Present the proposed task list as a multi-select card (host's neutral question UI — no amber styling): the user checks which tasks to dispatch (partial dispatch) with an optional custom-feedback row; approval binds the confirmationId to the selected subset and task_spawn_batch enforces it (confirmation-mismatch otherwise) |
task_spawn_batch | Spawn a whole decomposition plan in one call (tasks: [{title?, prompt}] + one team); requires the confirmationId once the batch reaches the confirmation threshold; one failed item does not abort the rest |
task_wait | Block until one task becomes idle (or timeout); multi-target (sessionIds + mode: all/any); cold targets (no live agent) report idle immediately — cold ≠ no pending work |
task_cancel | Cancel the target's active turn, keeping its queued messages (the stopped target needs a new message to wake — cancellation does not auto-drain the queue) |
task_workspace | List host workspaces, attach / detach an existing session (fix the ungrouped bucket), or migrate it to a different workspace (0.16.0): clones the full history into a new session born with the target cwd, attaches the clone, workspace-archives the original and returns the new id (workspace-level fold: the old session stays readable and resumable — messaging it would fork the work); refuses running sessions (task_wait first). Goes through the live workspace entity and never touches a session's conversation |
task_models | List the exact model routes this deployment serves — provider/model/reasoning-effort ids from the host's live catalog (the GUI picker's source) plus the app-wide default; consult before spawning with provider+model, never guess ids (0.14.0) |
/tasks — the no-model fast lane
Read-only lookups can bypass the model entirely: /tasks (all tasks), /tasks team <name> (one workstream), /tasks <sessionId> (one task's progress, short-id prefixes resolve when unique). The command executes directly in the host — zero tokens, instant answer. Anything that acts (send/spawn/wait/cancel) still goes through the tools.
Copying session ids — one click in the session header
The plugin ships a small web client module (client.js, declared via dsh.client in package.json) that occupies the official conversation.session.header.utilities slot — the same seam the shipped session-log-export package uses. Every session header gets a Copy Session ID button (filled pill matching the Session-log button geometry: black-on-white in light mode, white-on-black in dark mode) that copies the session's full sessionId to the clipboard, ready to paste into task_send, task_progress or /tasks <id> on the supervisor side. (The sidebar's per-session context menu is hard-coded in the host and cannot be extended — field-verified — so the header slot is the sanctioned place.)
Workspace placement & migration (0.12.0)
Spawned children attach to the caller's workspace by default; an explicit cwd that exactly matches a workspace path (case- and separator-insensitive) is upgraded to a workspace attachment automatically, so cross-directory dispatch no longer drops sessions into the ungrouped bucket. Sessions that landed ungrouped earlier migrate via task_workspace: list the host workspaces, then attach / detach by id or exact path. It calls the live workspace entity — the same attachSession API the host's session.create uses internally — so the session's stored cwd is validated against the workspace path and its conversation is never touched. (The GUI offers no such entry: sidebar dragging calls insertSessionBefore, which only reorders sessions already inside a workspace — field-verified.)
True cross-workspace moves (0.16.0): attach can never move a session whose stored cwd differs from the workspace path — the host validates and refuses, and no host API rewrites an existing session's cwd. action: migrate does the real move: it reads the complete replay-validated log (sessionQuery.readSession, without making the source live), seeds a NEW session born with the target workspace path as its cwd (sessions.create + flush — the same primitive the host's own fork() uses internally; fork() itself deliberately preserves the source cwd/workspace and cannot retarget), attaches the clone, archives the original at the workspace level (workspaceRegistry.archiveSession — a display-layer fold: the old session stays readable and resumable), and carries the plugin registry record (team/depth/parent/title) over to the new id — team filtering and recursion governance survive the move. The task continues under the returned sessionId; message that id, never the old one — the archive does not seal the original, and messaging the archived id would fork the work into two diverging copies. Running sources are refused (migrate-busy — the clone seeds from the persisted log, so settle the turn with task_wait first); a source whose cwd already matches the target is refused with an attach hint; every partial failure reports whether an orphan clone exists and whether the original was NOT archived.
Per-child model selection (0.13.0) & route discovery (0.14.0)
task_spawn — and every item of task_spawn_batch — accepts an optional provider + model pair (+ reasoningEffort). The route is validated against the host LLM catalog before the session is created (model-unavailable rejects an invalid pair with zero orphans), then installed through the host's sessionController.selectModel between creation and kickoff, so the child's very first turn runs on the requested model; the selection persists as a durable session event and survives restarts. Should installation fail after pre-validation, the spawn reports model-select-failed with the traceable orphan id and never kicks off on the wrong model. Host semantics, disclosed as-is: installing a session-local model also updates the app-wide default model (the GUI picker's "last selection wins" behavior — selectModel is the host's only public entry point), so in a mixed-model batch the last child's route becomes the app default.
Marketplace reality: every user connects different providers/models, so ids are never hardcoded and never guessed — task_models projects the host's live model catalog (the same source the GUI model picker renders) into the exact ids task_spawn accepts, including per-model reasoning efforts and the app-wide default; providers whose catalog listing fails are reported in isolation (failedProviders). A rejected model-unavailable spawn carries an actionable hint too: the error lists what the requested provider actually serves (or the routable providers when the provider itself is unknown). On host builds without modelCatalog(), task_models degrades to catalog-unavailable and the error hints remain the fallback.
Localized UI strings (0.15.0)
User-facing strings follow the host's Language preference (Settings → General → Language; the durable locale.preference, zh/en — the same channel the official session-log button uses). The browser-side header button registers dictionaries with the live client locale runtime and re-renders on every language switch; host-side surfaces (dispatch-confirmation cards, the report-back kickoff suffix, /tasks metadata) resolve through i18n.mjs per call, so a switch applies from the next card/kickoff without a restart. An absent or unknown preference keeps the historical Chinese strings — the host side cannot see the "follow the browser" delegation, so the plugin never guesses a language it does not ship. Model-facing surfaces stay as documented: tool descriptions are the English model contract, the SKILL manual is Chinese, and titles follow the MMDD|type|topic convention.
Dispatch confirmation (the anti-black-box gate)
Batch dispatches used to be a silent model decision — not anymore:
- The supervisor calls
task_confirm({ plan })with the full decomposition plan (markdown); the user gets a plan-review card rendered through the host'sctx.userQuestionsseam — the same isomorphic path the officialexit_plan_modeuses, so no client-side changes are needed; - Approve → the tool returns a
confirmationIdbound to the calling session; decline → the user's feedback comes back as the tool result; close the card →confirm-cancelled(the supervisor stops and waits); task_spawn_batchat or aboveconfirmBatchThreshold(default 2) refuses to run without a validconfirmationId(confirmation-required). The credential is single-use and consumed on success; an all-failed batch keeps it so the user is not asked twice for the same plan.
Multi-select variant (0.10.0): when the tasks are independently droppable, task_confirm_select({ tasks: [{title, scope}] }) renders the list in the host's neutral question UI (multi-select + custom input row, no amber plan-review styling) and the user checks which tasks to dispatch. The minted credential carries the selected subset, and task_spawn_batch rejects any batch title the user did not check (confirmation-mismatch). Present the full plan in chat first — the generic card carries the task list, not the plan body.
Mission-scoped approval (0.11.0): for a long autonomous run (e.g. goal mode), confirm ONCE — task_confirm({ plan, reusable: true }) mints a reusable credential that survives successful batches, so every later batch of the same mission passes the gate with the same confirmationId instead of raising a card per milestone. Single-use stays the default; reusable credentials are still caller-bound and in-process (a host restart clears them); task_confirm_select supports reusable too (subset enforcement applies on every reuse).
Degradation: with no UI connected, task_confirm returns no-question-channel and the bundled skill instructs the supervisor to fall back to a plain-text confirmation in chat. Subagent callers get delegated-caller (a child agent cannot ask a human).
Result report-back
Spawned tasks come with a report-back convention by default (reportBack): the kickoff prompt ends with an instruction to push a result summary (conclusion, output paths, remaining issues) back to the spawning session via task_send when the task finishes — with "write the summary into your final reply" as the fallback when the send fails. The supervisor therefore gets push + task_wait as the pull fallback instead of polling. Pass reportBack: false for fire-and-forget tasks you will read with task_progress anyway. Since 0.17.0 the convention also tells the child to end its turn right after sending (your reply auto-opens a new round on the idle child), and for multi-phase tasks to send a phase report and yield at phases that need your review — the bundled skill pairs this with the phase-review-gate pattern; respond to every phase report, the yielded child hangs until you do.
Recursion governance
Spawned coordinators can spawn further tasks — up to maxSpawnDepth (default 2) generations from the root session. The durable registry records each task's depth and parentSessionId; going deeper fails with spawn-depth-exceeded and the guidance to use subagents for deeper parallelism instead (subagents never consume depth budget).
Delivery semantics (the important part)
| Target state | task_send behavior |
|---|---|
| idle | Immediately starts a new round on the target |
running + queue (default) | Message queues, claimed at the next turn boundary |
running + steer | Message queues, claimed at the next step boundary (faster mid-course correction) |
Queue-drain mechanics (host-source verified): the next-turn queue is FIFO and exactly one message is consumed per round — the round's first step also absorbs all pending next-step messages — so a message queued at depth N is read after ~N rounds. task_send receipts carry queueDepth {nextTurn, nextStep} (post-send, including your message). steer skips the whole next-turn queue while the target is healthily running, at the cost of extending the current round (several steers land in one batch); on an idle or abort-winding-down target it degrades to next-turn queuing. task_wait returns immediately for cold targets (no live agent) — cold does not mean no pending work.
Three-tier interruption ladder:
| Tier | Tool | Takes effect | Skips the queue? | Cost |
|---|---|---|---|---|
| 1 | task_send queue | idle → new round at once; running → first step of the next round (FIFO, one per round) | no | none |
| 2 | task_send steer | healthily running → next step boundary; idle / abort-winding-down → degrades to queue | yes (running): ahead of the whole next-turn queue | extends the current round; several steers land in one batch |
| 3 | task_cancel | requests immediate stop (running tool calls finish first); queued messages kept | yes (vs the current round) | work in flight is lost; the stopped target needs a new message to wake |
Rule of thumb: if being one step late wastes a step, steer (stop / course change / conflict warning); otherwise queue (acknowledgments, background, non-urgent handoffs). Inside a single marathon tool call (a full test battery, say) there is no step boundary — even steer cannot get in; only tier 3 applies.
Conclusion: no polling needed — deliver, task_wait for idle, then task_progress for the result. To correct a running task right away use steer; a queue message will not take effect earlier.
⚠️ Delivered ≠ consumed:
delivered: trueonly means the message was accepted into the inbox. On timeout, errors or long silence, reconcile first withtask_progress(queued messages + conversation tail), then decide to resend or keep waiting — never blind-resend an uncertain delivery as a new message.
Correlation & traceability
task_sendreturns amessageId;task_spawnreturns acorrelationId— note down the ones you will need to reference;- To correct or continue an earlier instruction, pass
reference: <messageId or correlationId>totask_send— the reference is quoted as a visible annotation line in the delivered message, so the target knows exactly which instruction is being amended; - Every cross-task message carries the
coordinatorsource, attributable in the target's transcript.
Team workstreams & durable registry
- Pass
team: <workstream name>totask_spawn/task_spawn_batchto group tasks;task_list({ team })retrieves the whole group later; - Grouping is recorded in a durable spawn registry (default
<DSH_HOME or ~/.dsh>/task-coordinator/registry.json) together with each spawn's title, prompt excerpt,depthandparentSessionId— it survives host restarts (native session listing cannot answer "which tasks are mine and how do they group"); - Registry writes are near-atomic (temp file + rename); entries are capped by
registryMaxEntries(default 500, oldest pruned first); a corrupt file is preserved as*.corrupt-<timestamp>instead of being silently dropped.
Machine-readable error codes
Failures return { ok: false, code, error } — agents branch on code, never on prose. Two families: guard denials (self-send-denied / subagent-caller-denied / subagent-target-denied / target-not-found / rate-limited / queue-full…) and operation failures (bad-request / target-busy / target-cold / spawn-create-failed / kickoff-rejected / spawn-depth-exceeded / confirmation-required / confirm-cancelled / no-question-channel / delegated-caller / batch-all-failed…). Full table in Host contract §6.
Safety model
- Self-addressing is always rejected;
- Targets must be top-level sessions — subagent-owned sessions are fenced;
- Subagent callers are denied by default (
allowSubagentUseto opt in); - Batch dispatches above the threshold are impossible without explicit user approval (see above);
- Recursion depth is capped (
maxSpawnDepth) so spawn trees cannot grow unbounded; - Per-target rate limit (
minSendIntervalMs) and queue-depth limit (maxQueuePerTask) prevent runaway spam; - Caller identity is re-derived from the executing agent context on every tool call — never self-reported.
Spawn-title rule (MMDD|type|topic)
task_spawn splits the title responsibilities — the model supplies type|topic; the plugin stamps the date mechanically:
- The date prefix is stamped from the session creation time in
titleTimeZone(defaultAsia/Shanghai) — neverupdatedAt, never model-computed; - type must be one of
titleTypes. The shipped default set is a Chinese octet (literal values in the Chinese README — Feature / Design / Fix / Optimize / Release / Explore / Docs / Research in order); the set is fully customizable, and an all-English octet like['Feature', 'Design', 'Fix', 'Optimize', 'Release', 'Explore', 'Docs', 'Research']works out of the box. With the default set, English aliases (fix/bugfix,feature/feat,design,optimize/perf/refactor,release/publish,explore,doc(s)/documentation,research/investigate) are normalized to it case-insensitively; with any set, values also match their own members case-insensitively. Unclear types fall back totitleFallbackType(the shipped default is the Explore slot) instead of guessing; - topic is truncated to
titleMaxTopicChars(default 16) for sidebar display; with notitle, the topic is derived from the kickoff prompt's first line; - A stale leading
MMDD|is re-stamped from the real creation time; halfwidth|and legacy[team]prefixes are normalized.
Example (English set configured): Fix|reconciliation precision → 0904|Fix|reconciliation precision; with the shipped default set, fix|reconciliation precision resolves to the same title with the Chinese canonical type.
Bundled skill: task-coordination
The plugin ships one skill (skills/task-coordination/SKILL.md) teaching the supervisor when and how to orchestrate the tools: delivery semantics, the three-tier interruption ladder, decomposition criteria, confirmation semantics, fan-out/supervise/handoff patterns, yield-and-wake (goal-mode event loop) and phase-review-gate orchestration patterns, recursion governance, the naming rule, anti-patterns. Loaded on demand — it costs no context until coordination actually happens.
Mounting follows the shipped @openviking/dsh-memory-plugin precedent — an isolated dsh-skill-filesystem provider with providerName: 'task-coordinator', includeDefaultRoots: false, seeing only this plugin's skills/ directory. Consequences: hot-reload on edit, no shadowing of project/user skills, disappears on uninstall. If the provider package is unavailable the mount degrades to a warning — the eleven tools keep working.
Configuration (cordis.yml / patch)
- id: task-coordinator-runtime
name: 'dsh-plugin-task-coordinator'
config:
enabled: true
allowSubagentUse: false
includeSubagentsInList: false
titleTypes: ['Feature', 'Design', 'Fix', 'Optimize', 'Release', 'Explore', 'Docs', 'Research'] # fully customizable; shipped default is the Chinese octet (see Chinese README)
titleFallbackType: 'Explore'
titleFallbackTopic: 'New task' # topic when title and kickoff prompt are both blank
titleMaxTopicChars: 16
titleTimeZone: 'Asia/Shanghai'
registryFile: '' # empty = <DSH_HOME or ~/.dsh>/task-coordinator/registry.json
registryMaxEntries: 500
maxBatchSpawn: 6 # per-call cap for task_spawn_batch
maxSpawnDepth: 2 # spawn generations allowed below the root session
confirmBeforeBatch: true # dispatch confirmation gate
confirmBatchThreshold: 2 # batch size (>=) at which the gate engages
maxQueuePerTask: 5
minSendIntervalMs: 2000
waitDefaultTimeoutMs: 120000
waitMaxTimeoutMs: 600000
excerptChars: 400
progressTailMessages: 6
Config resolution rejects wrong types instead of guessing: a bad type fails fast with TypeError. See Host contract §5 for every option's semantics.
For developers
Module layering, DI boundaries and the degradation strategy live in docs/ARCHITECTURE.md. Quick reference only here.
Development & tests
node --check *.mjs # syntax check
node --test test/smoke.test.mjs # 89 unit tests (mocked host)
# after installing into a profile (see Quick start):
node verify-installed.mjs # installed-location integration check: real host packages + mock ctx
Directory
dsh-plugin-task-coordinator/
├── index.mjs cordis entry · wiring (only layer importing host packages directly)
├── config.mjs config resolution (pure module)
├── safety.mjs guards + rate limiter + denial codes (pure module)
├── title.mjs spawn-title rule (pure module)
├── registry.mjs durable spawn registry (near-atomic writes, corruption-tolerant)
├── i18n.mjs zh/en UI-string dictionaries · host-locale resolution
├── ops.mjs session operations · DI factory
├── tools.mjs eleven task_* tool registrations
├── commands.mjs /tasks slash command (direct execution, no model turn)
├── client.js web client module: copy-session-id header button (dsh.client)
├── skills.mjs isolated skill mount (dynamic import, fire-and-forget)
├── skills/task-coordination/ supervisor playbook (shipped with the bundle)
├── cordis.patch.yml isolated plugin-group mount descriptor
├── install.ps1 deploy script (copy-based install + automatic backups)
├── verify-installed.mjs installed-location integration check
├── test/smoke.test.mjs 89 unit tests
└── docs/ ARCHITECTURE.md · PROTOCOL.md
Documentation
- docs/ARCHITECTURE.md — architecture: why a plugin, module layering, guard layers, degradation strategy
- docs/PROTOCOL.md — host contract & delivery semantics, field-tested (injection surface, facade signatures, limits, verification records)
- CHANGELOG.md — release history
- skills/task-coordination/SKILL.md — the supervisor playbook the model actually reads
License
This plugin is MIT. The @deepseek-ai/* host packages it runs against belong to and are licensed by DeepSeek Harness; they are not covered by this repository's license.



