Back to home

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

Quick Start License

TypeScript Node.js React Zod esbuild Cordis


Features

FeatureDescription
One Board Per RepositoryThe 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 SchedulerA 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 GatesAn 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 QueueAgent-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 PeerThe 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:

Board view: proposed, backlog, todo, in progress, in review, and done columns

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

Issue detail expanded from a card, with the acceptance controls, description, labels, and comment trail


Quick Start

Prerequisites

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-orchestrator on 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

KeyDefaultDescription
scheduler.concurrency1How many issues may run at once; changeable live from the board header
scheduler.autoPulltrueWhether the board pulls from todo on its own; changeable live from the board header
scheduler.sweepIntervalMs30000Safety-net sweep that frees slots occupied by vanished sessions
plan.subagentProviderspawnFresh structured-output subagent provider used for every planning round
plan.maxRounds32Default AND ceiling for one taskboard_plan run; a call may lower it, never raise it
plan.maxHandoffChars16384Maximum serialized characters in one round's structured report; an oversized report fails the run instead of being truncated
plan.maxIssues16Maximum 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.

MethodDescription
board.viewThe board this session belongs to (resolved from its workspace), with live scheduler state
project.listList every project
project.createCreate a project
task.listList issues, optionally filtered by project, status, or session
task.getRead one issue with its comments and activity trail
task.createCreate an issue
task.updateChange an issue; refuses a stale expectedVersion
comment.createAdd a comment to an issue
task.startOpen a FRESH session for one issue and hand it the work
task.startNextStart the next todo issue — highest priority first — without naming one
task.acceptAccept finished work (in_reviewdone) — the human gate no agent can pass
task.sendBackSend finished work back to todo with a reason (recorded as a comment), unbinding its session
scheduler.configureChange 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

TechnologyPurpose
TypeScriptSource language for both plugin halves
CordisHost plugin framework: services, effects, dependency injection
ZodSchema validation for the four storage-domain tables
SchemasteryPlugin Config validation
ReactBoard view rendering (peer dependency, supplied by the host at runtime)

Build & Test

TechnologyPurpose
esbuildBundles the browser half into the client-module envelope the host serves
Node.js test runnernode --test, no test framework dependency

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing)
  3. Commit your changes (git commit -m 'feat: add amazing feature')
  4. Push to the branch (git push origin feature/amazing)
  5. Open a Pull Request

License

Apache-2.0. The domain model and issue-flow rules derive from dashi-taskboard — see NOTICE.