dsh-task-board · Autonomous Task Board & Cron Scheduling for DeepSeek Harness (DSH)
English | 中文
Autonomous Kanban Task Board & Cron Scheduling for DeepSeek Harness (DSH)
Host-Authoritative Ledger · Parent-Card Subtask Dispatch · Double-Blind Verification · Sleep Inhibition · GitHub Issue-to-PR
A hot-pluggable DeepSeek Harness (DSH) Web GUI and official desktop client task automation plugin with a Host-authoritative task ledger, real DSH agent session execution, cron background scheduling, cross-platform idle-sleep protection, parent-card subtask dispatch, and automated verification quality gates. It executes unattended background workflows including automated code reviews, routine health checks, and scheduled pipelines without requiring an open browser window. It mounts cleanly via cordis.patch.yml and profiles without modifying DSH core code.
- Host-authoritative architecture and offline background execution: The browser is merely an asynchronous view; closing tabs or dropping connections will not interrupt Host scheduling, session execution, or atomic ledger settlement. Built-in cross-platform sleep inhibition (macOS, Windows, Linux) allows screens to power down while preventing system idle sleep for reliable unattended automation.
- Parent-card dispatch over subtask trees: a task can carry subtasks, one to three levels deep as the deployment allows. Running a card opens one session, that card's own: the Host opens nothing for its subtasks, and that session reads them with
task_board_get, dispatches them with subagent in dependency order, and writes their conclusions back with task_board_manage before it finishes. A card whose subtasks are still unfinished runs under the liangshen dispatcher preset.
- Double-blind adversarial judge quality gates (No False Completion): Intercepts false agent completion claims; requires verification by an independent Judge Model using strict coding criteria, a 0.65 threshold, two-round A/B position swapping, and validation against real workspace filesystem diffs, returning actionable diagnostics for self-repair.
- Automated GitHub loop, freeze handover, and agent tool surface: Real-time bidirectional GitHub Issues sync automates agent bug fixing and PR creation; supports
<<<FREEZE session context snapshots with handover packages; exposes 8 native model tools for autonomous planning and external execution registration.
Features
Cards keep multi-line title and description previews with visible metadata. Previews strip Markdown formatting; task details render description and prompt as safe Markdown without raw HTML or automatic remote images, and content taller than about eight rendered lines is clamped to a fixed height with a fade hint and a "Show more" toggle that expands and collapses it, while shorter content renders untouched with no toggle. Stored task text is unchanged. A bracketed title prefix such as [Bug]: renders as a type badge, and the description preview skips issue-form boilerplate (checkbox lists, section headings, empty-field placeholders), preferring a summary section when one exists. A left stripe shows the latest run (running, succeeded, failed, cancelled); a card whose column disagrees with that run is labelled as set by hand. Each column shows the time its cards differ by — created in Backlog/To Do, started while running, settled in Done/Failed — read in the Host time zone. The latest execution session opens straight from the card. Every column header toggles compact single-line cards (Done starts compact, the choice is stored in the browser). All five columns and the archive first group cards by provider source — full owner/repository labels for GitHub — with unclaimed cards in Local tasks; source groups sort by label and their fold choices persist per browser. Done source groups start collapsed while the other columns and the archive start expanded, and search always reveals matching groups. Done, Failed and the archive nest Today / Last 7 days / Earlier within each source group and fold the oldest group once that source holds more than 30 cards, unless a search is active.
-
Task board UI: the board is the shell's own center-column panel — like the desktop application's Plugins page, it contributes a row to the shell's official sidebar panel list and a page to the layout's center-column seat, so row geometry, the active highlight, the collapsed rail, the label, and language switching are the shell's to own; the board provides five kanban columns, search, task details, archive/restore, execution history, and links to execution transcripts. Every column is reachable by hand, by dragging a card or from the detail view's move menu: the move writes the column and no execution record, so Done or Failed declares work that finished (or failed) outside a tracked run and Running parks work that is under way without a session, while a card the runner is actually executing refuses every move until it settles. Archived tasks are read-only except for restore, delete, and transcript viewing, and cannot run manually or on schedule until restored.
-
One scheduling entry in the sidebar: while the board is enabled, the shell's official Automation tasks entry (the host ui-schedule page over the Host Schedule service) is hidden, so the sidebar offers one scheduling surface instead of two. The board's own master switch owns the state: switching the board off brings the official entry back, and the official page keeps working everywhere else it is still reachable — the entry is hidden, not uninstalled.
-
Detail layout and folds: the task detail is two columns on a wide viewport — the left holds the task itself (description, execution prompt, labels, subtasks, frozen snapshot, handover bundle) and the right holds how this run will run (execution settings, scheduled runs, execution history, status); a narrow viewport falls back to one column in the same order. Workspace, agent preset, permission, model, and reasoning effort stay visible, while the three card-level switches (continue in the same conversation, start with /goal, skip this task's acceptance) fold into one "Advanced" row that echoes the switches that are on while it is closed, so the card's shape is readable without expanding it. Scheduled runs and execution history each take one fold row: the schedule row says whether a rule is armed and when it next fires, the history row says how many runs there are and what the latest one did, and the list itself shows the five most recent runs until it is expanded; an acceptance report shows one verdict badge on its row and the verdict, judge route, per-criterion scores, and findings only once expanded. The permission gate and the current status never fold. The view uses three type sizes (title, body, meta), status and result badges share one filled slot scale, and every fold row and button carries a hit target of at least 44 pixels.
-
Continuation cards (data plane): a new task may paste a <<<FREEZE ... >>>FREEZE block from a session; it parses into a goal/progress/next snapshot persisted with the task (ledger v3). Cards carry a frozen badge, the detail view shows the full snapshot and freeze time, search covers snapshot text, and archive/restore matches plain tasks. The snapshot reuses the freeze security gate at the protocol layer: sensitive patterns become with a marker, slash-prefixed command lines reject the whole snapshot, and each field is capped at 8 KiB.
Architecture and protocol
The board view renders on first open and keeps its local view state when closed and reopened. Host synchronization, scheduling and execution remain active independently of the view.
src/index.ts mounts the Host service through the official @deepseek-ai/dsh-api-gateway, @deepseek-ai/dsh-workspace, and @deepseek-ai/dsh-host-webserver SDKs.
src/host-ledger.ts serializes actions and persists { schemaVersion: 6, revision, tasks, scheduler, recentRequests } through a temporary file plus atomic rename.
src/host-service.ts owns schedule firing (a single timer armed at the ledger's nearest target through the host's timer service, falling back to process timers when the host serves none), missed-trigger skipping, runner launch, restart reconciliation, and power reasons; the ledger itself owns the run budget, so a one-shot or an exhausted capped rule can never be opened a second time.
src/client/native-panel.tsx registers the board's sidebar row and center-column page (the official sidebar.panellist list seat and the keyed main seat) and maintains the center-column exclusivity protocol with the ssh panel.
src/client/host-api.ts imports legacy browser data once, submits idempotent actions, and treats Host snapshots as the only confirmed UI state.
- Same-origin endpoints are
GET /api/task-board/state, GET /api/task-board/events, and POST /api/task-board/action.
- Every endpoint requires browser authentication. Under the board's own fence that means a browser same-origin marker:
sec-fetch-site: same-origin, an Origin header, or the Host's dsh-auth-* browser-auth cookie. The last one is how the DSH Desktop shell reaches the board: it serves the Web GUI from dsh-app://app/ and forwards that page's requests itself, dropping Origin and sec-fetch-site and attaching the authority-bound cookie it redeemed from the Host's launch URL at startup. Direct access is restricted to the DSH loopback origin. A deployment DSH itself serves through a trusted reverse proxy or ingress is instead admitted by the Host's connection service, which applies its own Host/Origin fence and verifies the signed browser session before the board sees the request; an authenticated same-host proxy that DSH does not front must use an explicit Host allowlist and a server-injected token, and a deployment whose own tunnel dials the loopback upstream can instead turn on trustForwardedProxy (see Configuration), which admits that shape on the same-origin marker alone. POST requests additionally require JSON. Ordinary actions are limited to 64 KiB and import to 2 MiB. The action union has no command, executable path, shell text, or arbitrary argument field.
Agent tools
The board is operable from a conversation, not only from the GUI. Each tool drives the same Host ledger, so a card created in the GUI is immediately visible to agents and vice versa, and every gate the UI honors still holds. The surface follows the enabled switch: a disabled board answers no tool call, and a deployment whose runtime serves no tool registry still mounts the board with the GUI intact.
task_board_list lists cards with filters (column, parent, roots only, label, free-text query, archived) plus the board summary: revision, time zone, subtask depth limit, session-default permission, and column counts.
task_board_get reads one card in full: prompt, labels, parent link, direct subtasks, execution targets, schedule, permission-gate state, and the last ten execution attempts with their session ids, initiator, and outcome.
task_board_create creates a task or, with parentId, a subtask; execution targets left unset inherit the parent, and the deployment subtask-depth limit applies.
task_board_update edits content, labels, and execution targets; an empty string clears a target and an empty label array clears the labels.
task_board_set_parent links an existing card under a parent, or detaches it with an empty parent id.
task_board_run runs a card now (optionally re-running a settled one) and nothing else: the card gets its own session, the Host opens none for its subtasks, and a card with unfinished subtasks is told which of them its session must dispatch; it consumes real API quota and refuses an unconfirmed above-default permission with confirmation-required.
task_board_manage moves a card to any column — the move writes the column and never an execution record, so backlog/todo plan, done/failed declare work that finished outside a tracked run and running parks work that is under way without a session, while a card with an open execution refuses the move until it settles — archives or restores it, deletes it, or settles a running card the board can no longer observe (recording a cancelled verdict with the caller as the reason and returning the card to todo), with the Host's running-task and subtask guards.
task_board_schedule arms, changes, or disarms a card's schedule — a recurring cron rule (optionally capped by maxRuns; 0 clears the cap back to unlimited) or a one-shot at an epoch-millisecond at — including its IANA time zone (an empty timeZone clears it back to the Host zone).
There is deliberately no tool that confirms a permission binding: that gate exists so a human lifts an above-default permission, and an agent able to stamp it would make the gate decorative. An agent that hits confirmation-required asks the user to confirm the card in the board UI. Tool calls are attributed: a run records the calling session as its initiator, and a create/update stamps it into a continuation card's snapshot.
Install
Install the aggregate package or this package alone, then restart dsh web:
dsh plugin --profile web add @linxin666/dsh-client-ui-task-board@latest
For local development:
git clone https://github.com/zhu1090093659/dsh-web.git
cd dsh-web
pnpm install
pnpm build
dsh plugin --profile web add link:$(pwd)/packages/dsh-task-board
Configuration
This plugin's settings card groups its options into collapsible sections — the board and its runtime behavior, task acceptance, new-card defaults, and the section a provider extension contributes (the GitHub Issues integration). Every section starts collapsed. A change takes effect as it is made: a pick is written at once, and a typed value once typing pauses, on Enter, or when the field is left; Escape puts the stored value back.
| Key | Default | Behavior |
|---|
enabled | true | Enables the Host service and browser board. |
announceToAgent | false | Opt-in: when true, adds the task-board guidance section to agent system prompts. |
preventIdleSleep | false | Holds one system idle-sleep assertion while any DSH session runs, any schedule is enabled, or session state is unknown. |
trustedProxyHosts | [] | Canonical host[:port] authorities accepted only through the authenticated loopback reverse-proxy path. |
proxyTokenEnv | DSH_TASK_BOARD_PROXY_TOKEN | Environment variable containing the reverse-proxy token; the token itself is never stored in plugin config. |
trustForwardedProxy | false | Admits a forwarded request that landed on this machine's loopback upstream — the shape your own reverse proxy or tunnel produces when it dials 127.0.0.1:<port> — on the browser same-origin marker alone. Off keeps every forwarded request remote (issue #1906); on still refuses an explicit sec-fetch-site: cross-site marker, and the token-allowlisted proxy path above is unchanged. Volatile: read at each request, so an edit binds the next one without a restart. |
sessionDefaultPermission | follow the Host | Explicit baseline for the permission confirmation gate. Unset, the board follows the Host's own default permission preset (what new DSH sessions start at) and falls back to read-only when the deployment serves no permission catalog. A card whose effective permission (handover bundle or pin) is above the baseline requires a human confirmation before it may run; cron refuses unconfirmed cards. |
maxSubtaskDepth | 1 | Subtask depth limit, 1 to 3. At 1 a task may carry one level of subtasks and a subtask cannot be given subtasks of its own. It decides only how deep the tree under one card can go: a run opens that card's own single session and dispatches its subtasks from there, so the depth never changes how many sessions one run opens. |
sessionPollSeconds | 5 | How often, in seconds, the Host re-reads the DSH session roster while the board has something to reconcile: a running card or an open execution. An idle board with neither does not poll at all. Range 1 to 300; a shorter interval settles runs sooner and costs the Host more. |
All four new-card defaults are blank by default and volatile: an edit takes effect without a restart, for the cards created afterwards and on a page that is already open (no reload is needed for the new-task dialog to offer the saved values), and a blank value pins nothing, so a deployment that configures none of them creates cards exactly as it does today. A new card takes each of its execution targets in one order — the explicit choice made in the new-task dialog wins over the values carried over from the card it was created from (a duplicate keeps its source's pins, a subtask keeps its parent's), which win over the board defaults — and the parent's permission is never carried over. None of the four lifts the permission gate: a default permission above the session baseline is only a prefill, and the card still waits for a human confirmation before it may run.
Direct browser access remains limited to the DSH loopback origin. A deployment that DSH already serves through its own trusted-proxy or ingress arrangement (for example dsh web --trusted-host dsh.example.internal behind TLS termination) needs no board-specific setting: the routes bridge the Host's connection service, so a request whose Host/Origin fence and signed dsh-auth-* browser session that service accepts is admitted on its verdict, while a request it declines still faces the board's own fence. This path grants nothing a browser without the Host-issued session could use, and it is independent of the settings below.
For a same-host authenticated reverse proxy, bind DSH Web to loopback, set trustedProxyHosts, place a high-entropy token in the environment variable selected by proxyTokenEnv, and configure the proxy to replace (not forward from the client) X-Dsh-Task-Board-Proxy-Token after it authenticates the request. The proxy Host must be allowlisted, and the browser Origin must have that same authority. Restart the Host after changing these composition-level proxy settings.
The alternative shape is a request an operator's own tunnel or reverse proxy forwarded to this machine. With trustForwardedProxy on, a request that carries a forwarding header and arrived on the loopback upstream (loopback socket and loopback Host) is admitted on the browser same-origin marker alone; an explicit sec-fetch-site: cross-site marker is still refused, and a request that came through a proxy that is not dialing this machine stays remote. The switch is volatile and read per request, so an edit binds the next request; leave it off unless you operate the hop yourself, because a proxy you do not control can forge those markers on your behalf.
On macOS the backend starts /usr/bin/caffeinate -i -w <host-pid> and never requests -d. On Windows it starts the absolute Windows PowerShell under SystemRoot with a fixed helper that requests only ES_CONTINUOUS | ES_SYSTEM_REQUIRED; it never requests ES_DISPLAY_REQUIRED, changes a power plan, or requires administrator privileges. On Linux it starts a systemd-logind idle block inhibitor only from /usr/bin/systemd-inhibit or /bin/systemd-inhibit; it does not request sleep, handle-lid-switch, or a display/screensaver inhibitor. A Linux host without systemd-logind reports unsupported or a visible error and does not start a desktop-specific fallback. Other platforms report unsupported.
Data storage and migration
- The authoritative ledger file is
$DSH_HOME/task-board/ledger-v2.json (the file name is historical); the current document schema is v7, and an older document (v2, v3, v4, v5 or v6) is migrated losslessly in place on the next Host start. New POSIX files use mode 0600; Windows inherits the user directory ACL.
- v5 adds the per-execution acceptance block (
verification). The migration deliberately does NOT stamp one, so an execution opened before v5 keeps settling on its historical verdict and no in-flight run is retroactively judged by a gate that was never armed for it. A malformed block is dropped (that execution is then treated as unverified — fail closed), and an imported execution record never carries one, because a fabricated pass record must not let imported work look accepted.
- v6 adds the schedule plan type and the run budget, and v7 adds the card attachment metadata (
attachments). Both are additive: a card written before the field existed keeps running exactly as it did, and a malformed attachment list is repaired entry by entry (unusable entries dropped, count and byte caps applied) instead of dropping the task row. The legacy browser import does NOT accept attachments: a v1 export never carried one, so a document claiming one is refused rather than let past the storage check.
- A v2 to v3 migration failure (structurally invalid task rows) fails closed with an explicit error and keeps the original file untouched; it never restarts from an empty ledger silently. A corrupt or unsupported-schema file is moved to a collision-resistant
ledger-v2.json.corrupt-* name and the Host starts with an empty ledger plus a visible scheduler error. The corrupt bytes are not overwritten.
- On the first upgraded page load for an origin,
dsh.taskBoard.v1 is imported by stable source and request ids. Tasks merge by id, strictly newer browser top-level fields win, equal timestamps keep Host fields, and execution records merge by execution id.
- The most recent 256 request ids and SHA-256 action fingerprints are stored with the ledger, so a retried mutation remains idempotent after a Host restart without duplicating full action payloads.
- An external-outcome execution record adds one optional boolean (
external). A ledger written before it existed carries none, and the loader neither invents one nor treats an absent field as external, so old documents read back unchanged; a non-boolean value is dropped rather than trusted as provenance. The ledger stays at v5: the field is absent from every existing row, which is exactly the additive change the v5 migration already describes.
- Task labels are an optional
tags field on the task row ({ name, promptPrefix? }[]) and needed no schema bump: a v3 document without it loads unchanged, and a malformed list is repaired entry by entry (blanks, repeats, and over-long names dropped, count capped) rather than dropping the task row.
Security model
- The plugin stays inside the existing DSH Web deployment and network boundary and emits no permissive CORS headers. State, action, and SSE routes share the same access fence; bare local command-line requests are not accepted as browser requests. When the deployment serves the Host's
connection service, the fence admits exactly the requests that service already authenticated — its Host/Origin fence plus the signed dsh-auth-* browser session — and keeps its own loopback and token checks as the fallback for every other request. The fence otherwise refuses every forwarded request unless trustForwardedProxy is on, and even then only the forwarded shape that landed on this machine's loopback upstream and carries no cross-site marker.
- All mutation payloads use a strict, versioned discriminated union; schedule-owned timestamps and execution outcomes cannot be written by the browser.
- Workspace, preset, permission, cron, task status, and imported records are validated again on the Host.
- A card's effective permission above the configured session default enters a pending-confirmation state: the Host refuses manual runs and cron triggers until a human confirms the exact binding, and changing the pinned permission or the handover bundle clears the confirmation (no confirm-then-swap escalation).
- A task prompt is data sent to a DSH agent session. The protocol does not accept shell commands, PowerShell bodies, executable paths, or configurable helper arguments.
- Task labels are client-asserted like the prompt itself and ride the same gated action channel: the protocol gate rejects a blank name, an unknown key, more than eight labels, and an over-long name or hint. A label's injected hint is delimiter-escaped (it cannot forge the continuation-card provenance markers) and is placed outside that provenance wrap.
- Attachment metadata is client-asserted too, and the Host does not believe it: every attachment an action carries is read back from the official attachment storage before the ledger commits it (an image is compared field by field, by digest and by its stored display name, a file is streamed and its digest recomputed), and a reference the storage cannot prove refuses the action. The upload route measures the ACTUAL request stream and refuses a body past the limit — the refused upload never reaches the ledger, and the storage SDK does not guarantee that a partially written backend object is cleaned up, so the board promises no such cleanup. The image read route serves an object only when a card of this board references it, so an unreferenced identifier is not readable.
- The subtask lineage is validated on the Host, not in the browser: the parent must exist and be on board, attaching a task under one of its own descendants is refused, and the resulting depth must stay within
maxSubtaskDepth. The permission gate judges the binding the card being run resolves on its own: a subtask's own unconfirmed elevated pin no longer blocks its parent, because the parent opens no session for it, while a subtask that runs on its own resolves an inherited binding down the ancestor chain, and the inherited binding carries that ancestor's human confirmation. The binding is never copied onto the card, so detaching a subtask cannot leave a confirmed elevated task behind.
Build and test
Node 20 or newer and the official NPM SDK packages are required; no DSH source checkout is used.
pnpm --filter @linxin666/dsh-client-ui-task-board typecheck
pnpm --filter @linxin666/dsh-client-ui-task-board test
pnpm --filter @linxin666/dsh-client-ui-task-board build
Set DSH_POWER_SMOKE=1 to opt into the native helper smoke test on Windows, macOS, or Linux. It starts the fixed helper, waits for readiness, releases it in cleanup, and confirms process exit without changing the system power plan. Linux first probes systemd-logind with a bounded timeout; without a usable system bus the native portion is skipped while pure logic tests remain available.
Manual verification
- Mount the package, restart
dsh web, open the task board, and confirm the Host time zone and power status are visible.
- Create and edit a task; refresh or open a second same-origin tab and confirm both show the same Host revision.
- Run a task with pinned workspace, preset, and permission; confirm a new session appears and the task settles from its
turn/end history.
- Run a task with the /goal option checked: confirm the session starts a goal and the card stays in
running across continuation rounds until the goal completes, then settles as succeeded. Uncheck the option and confirm the next run is a single turn that settles from one turn/end.
- Enable a near-future cron, close all browser pages, and confirm the Host still creates and settles exactly one execution. Then arm a one-shot at a near-future instant and confirm exactly one execution is created and the rule reads as ended; set a recurring rule's run limit to 2 and confirm the third occurrence creates nothing.
- Stop the Host past a cron occurrence, restart it, and confirm the missed occurrence is skipped and
nextRunAt rolls forward from current Host time.
- Enable
preventIdleSleep, run a long session, and let the display turn off; after restoring the display, confirm the session continued and the execution settled.
- Disable the setting and all schedules, stop DSH, and confirm the helper exits; on macOS,
pmset -g assertions should show no display-sleep assertion from this plugin.
- On Linux, use
systemd-inhibit --list to confirm that only an idle/block entry exists; the display should still follow desktop settings, while manual sleep and lid close remain under system policy.
Known limitations
-
Missed occurrences during Host downtime, system sleep, or a long pause are skipped and never queued for catch-up.
-
A task that is already running skips its due occurrence and rolls to the next cron match; task runs never overlap or queue. A one-shot has no next match: an instant missed because the card was running or the Host was down stops the rule as skipped, and is never replayed.
-
A capped rule counts the executions the scheduler opened, and a manual run never spends the scheduled budget. A launch that fails before any session exists is refunded, so the rule keeps its next occurrence; a run that did reach a session counts even if it then failed.
-
DST follows the Host local wall clock: a nonexistent spring-forward minute is skipped, and a repeated fall-back minute is not replayed a second time.
-
Power protection prevents only idle system sleep. It deliberately allows display sleep and lock.
-
Lid close, manual sleep, hibernation, shutdown, low-battery forced sleep, and enterprise power policy are outside the guarantee.
-
The plugin does not schedule wake timers and cannot wake a computer that is already asleep.
-
Linux requires systemd-logind and policy permission for the current user to acquire an idle block lock. Containers, WSL, hosts without a system bus, and non-systemd systems may report unsupported or error. Whether a desktop also associates a logind idle lock with display idleness is desktop policy; the plugin does not request a screensaver or display inhibitor.
-
Keeping enabled schedules armed may increase battery consumption because protection starts before their future trigger time.
-
Host execution consumes the same API quota as an ordinary DSH agent session.
-
Running a card opens one session, its own, and that session dispatches the unfinished subtasks with subagents inside it: a card with many subtasks consumes the quota of every subagent it dispatches, while the board itself opens nothing extra. A subtask whose conclusion is never written back inherits the parent run's verdict, which records the parent's judgement rather than a separate execution of that card.
-
Agent tool calls are model-driven: a run or a scheduled execution started from a conversation consumes the same API quota, and an agent can arm a schedule that keeps firing until it is disarmed or the board is switched off.
-
A goal run keeps working for as many rounds as the objective needs: every round consumes API quota, and the session stays busy until the goal completes or dsh blocks it (round limit, a rejected round, or a failed queue), which the board reports as a failed execution.
Telemetry
The browser half sends one anonymous install heartbeat per UTC day to dsh-market.com: a random localStorage id plus this package's name, nothing else. The server stores only a salted hash of that id, never IP addresses, and exposes aggregate counts only. See docs/telemetry.md for the full contract.