EricJiang0423
dsh-orchestrator
Local-first issue board for DeepSeek Harness: board data, model-facing tools, a fresh-agent planning loop, and a human approval queue for agent-proposed issues.
- Stars
- 0
- Language
- TypeScript
- Created
- Aug 14, 2026
- Updated
- Aug 15, 2026
Introduction
English · 中文
dsh-orchestrator
Local-first issue orchestrator for DeepSeek Harness: one board per repository, a fresh session per issue, and a scheduler that works the queue itself
Cordis Plugin · Workspace-Scoped Board · Per-Issue Sessions · Auto-Pull Scheduler · Two Human Approval Gates
Features
| Feature | Description |
|---|---|
| One Board Per Repository | The board is bound to the session's working directory, not to the conversation: two sessions in the same repo share a board, and a session in another repo sees its own — the host resolves it, so the browser can never mix projects |
| Fresh Session Per Issue | "Work on this" opens a brand-new session with the issue's brief and binds the issue to it, so every issue's transcript and cost are its own — and several can run at once |
| Auto-Pull Scheduler | A scheduler keeps up to N issues in flight and refills from todo by itself — highest priority first — with live controls for concurrency and the auto-pull toggle in the board header |
| Three Human Gates | An agent can never move an issue out of proposed, can never mark one done, and can never shelve one as archieved: proposals need your approval, finished work needs your acceptance, and archiving accepted work is yours too — all enforced in the service layer, not the UI |
| Durable Approval Queue | Agent-proposed issues land in a proposed column and stay there until a human approves or rejects them — durable across restarts, unlike a one-shot approval prompt |
| Board as a Chat Peer | The board registers into the conversation view ring, so it appears as a tab beside Chat and Trajectory instead of a separate page |
Screenshots
The board rendered with sample data — no project details from any real deployment.
Board view: workspace-scoped board with the approval queue, the scheduler strip (auto-pull toggle, parallelism, live running/waiting counts), and the session chip on the in-flight issue:

Issue detail, expanded from a card, showing the unified acceptance controls — Accept, or Send back with a reason that lands as a comment:

Quick Start
Prerequisites
- Node.js 22.5+
- A running DeepSeek Harness profile
Install from source (recommended)
Clone, build, and link the checkout into the profile. dsh plugin add . from inside the checkout registers the local build, so later npm run build runs are picked up without reinstalling:
git clone https://github.com/EricJiang0423/dsh-orchestrator.git
cd dsh-orchestrator
npm install && npm run build
dsh plugin --profile web add .
lib/ is gitignored build output. npm test builds it automatically when it is missing (a pretest hook runs npm run build first), so on a fresh clone you can run tests right after npm install without building by hand. Once lib/ exists, npm test skips the rebuild to stay fast — run npm run build explicitly to test your latest source changes.
Install from npm (registry)
⚠️ The unscoped
dsh-orchestratoron the registry belongs to an unrelated project (zibo/dsh-agent-mesh). This project is published under a scope:
dsh plugin --profile web add @ericjiang0423/dsh-orchestrator
Run
dsh --profile web
Usage
Capture an issue without leaving the chat
/task Fix the flaky checkout test
Call the board from another plugin
import type {} from '@ericjiang0423/dsh-orchestrator'
export const inject = ['taskboard']
export function apply(ctx: Context) {
const open = ctx.taskboard.listTasks({ status: 'todo' })
}
Call the RPC endpoint
const res = await fetch('/_dsh/taskboard/rpc', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
method: 'task.update',
params: { id, patch: { status: 'in_review' }, expectedVersion: 3 },
}),
})
Configure the scheduler and the planning loop
- id: taskboard
config:
scheduler:
concurrency: 2
autoPull: true
plan:
maxRounds: 16
maxHandoffChars: 8192
Architecture
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '14px'}}}%%
graph LR
UI[Board View<br/>React] -->|RPC + SSE| SVC[Taskboard Service<br/>Cordis Plugin]
TOOLS[taskboard_* Tools<br/>Model-facing] --> SVC
PLAN[taskboard_plan<br/>Workflow Engine] -->|fresh subagents| TOOLS
SVC --> DB[(Storage Domain)]
SVC --> WS[Workspace Registry<br/>cwd → workspace]
SCHED[Scheduler<br/>session-link] -->|agents.create| ISS[Per-Issue Sessions<br/>ctx.agents]
SVC --> SCHED
ISS --> SVC
classDef client fill:#3B82F6,stroke:#2563EB,color:#fff,stroke-width:2px
classDef service fill:#10B981,stroke:#059669,color:#fff,stroke-width:2px
classDef data fill:#8B5CF6,stroke:#7C3AED,color:#fff,stroke-width:2px
class UI client
class SVC,TOOLS,PLAN,SCHED,ISS service
class DB,WS data
The browser half never talks to storage directly. Every read and write goes through ctx.taskboard, whether the caller is the board's own RPC route, a model-facing tool, the planning loop, or the scheduler — so the two human gates (no self-approval, no self-acceptance) live in one place and apply to every caller. The scheduler is the only thing that starts work on its own, and it draws exclusively from todo, which only a human can put an issue into.
Configuration
| Key | Default | Description |
|---|---|---|
scheduler.concurrency | 1 | How many issues may run at once; changeable live from the board header |
scheduler.autoPull | true | Whether the board pulls from todo on its own; changeable live from the board header |
scheduler.sweepIntervalMs | 30000 | Safety-net sweep that frees slots occupied by vanished sessions |
plan.subagentProvider | spawn | Fresh structured-output subagent provider used for every planning round |
plan.maxRounds | 32 | Default AND ceiling for one taskboard_plan run; a call may lower it, never raise it |
plan.maxHandoffChars | 16384 | Maximum serialized characters in one round's structured report; an oversized report fails the run instead of being truncated |
plan.maxIssues | 16 | Maximum issues admitted into one planning run |
API
The browser half talks to the host half over one endpoint, POST /_dsh/taskboard/rpc, with { method, params } in the body, rather than one REST path per resource. DeepSeek Harness's typed RPC layer requires build-time code generation this plugin's build does not run, so the route is deliberately explicit — see docs/spike-findings.md for why.
| Method | Description |
|---|---|
board.view | The board this session belongs to (resolved from its workspace), with live scheduler state |
project.list | List every project |
project.create | Create a project |
task.list | List issues, optionally filtered by project, status, or session |
task.get | Read one issue with its comments and activity trail |
task.create | Create an issue |
task.update | Change an issue; refuses a stale expectedVersion |
comment.create | Add a comment to an issue |
task.start | Open a FRESH session for one issue and hand it the work |
task.startNext | Start the next todo issue — highest priority first — without naming one |
task.accept | Accept finished work (in_review → done) — the human gate no agent can pass |
task.sendBack | Send finished work back to todo with a reason (recorded as a comment), unbinding its session |
scheduler.configure | Change concurrency or the auto-pull toggle; returns the resulting state |
Change notifications stream over GET /_dsh/taskboard/events as Server-Sent Events.
Directory Structure
src/
├── client/ # Browser half
│ ├── board.tsx # BoardView: columns, cards, scheduler strip, approval + acceptance controls
│ ├── index.tsx # Client plugin entry, slot registration
│ ├── rpc.ts # fetch()-based RPC client + SSE subscription
│ └── styles.ts # Layout-only CSS; every color is a theme token
├── domain.ts # Zod schemas and the status machine
├── service.ts # ctx.taskboard: reads, writes, version CAS
├── rpc.ts # Host RPC route + SSE change stream
├── tools.ts # Model-facing taskboard_* tools
├── command.ts # /task human command
├── plan-loop.ts # taskboard_plan: the fixed planning loop
├── session-link.ts # Workspace resolution, per-issue sessions, the scheduler
├── skill.ts # Registers the manage-taskboard skill
├── actors.ts # Actor identity
├── wire.ts # Shared browser <-> host RPC types
└── index.ts # Plugin entry: mounts every face
test/ # node:test suites
skills/manage-taskboard/ # Bundled working-agreement skill
docs/ # Extension-point research notes
Tech Stack
Runtime
| Technology | Purpose |
|---|---|
| TypeScript | Source language for both plugin halves |
| Cordis | Host plugin framework: services, effects, dependency injection |
| Zod | Schema validation for the four storage-domain tables |
| Schemastery | Plugin Config validation |
| React | Board view rendering (peer dependency, supplied by the host at runtime) |
Build & Test
| Technology | Purpose |
|---|---|
| esbuild | Bundles the browser half into the client-module envelope the host serves |
| Node.js test runner | node --test, no test framework dependency |
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing) - Commit your changes (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feature/amazing) - Open a Pull Request
License
Apache-2.0. The domain model and issue-flow rules derive from dashi-taskboard — see NOTICE.