marchliu/dsh-cybernetics: A DSH plugin that turns agent orchestration into a control loop

Preface

A common problem when driving multi-step tasks with DSH is that the goal exists only within the current prompt. Writing requirements all at once and sending them down, the agent acts step-by-step. Once done, the responsibility for acceptance falls entirely on the human. When the results deviate from the goal, only a supplementary prompt can be used for correction, followed by another round of waiting—the entire system is open-loop, with measurement and calibration handled by the human.

The plugin introduced below moves this loop into the system itself. marchliu/dsh-cybernetics is maintained by MarchLiu. It falls under the category of “workflow” on GitHub, with 11 stars and 2 forks, an MIT license, and the protocol is named the Cybernetics Protocol. The official positioning is very direct: it is a DSH plugin that explicitly designs multi-agent collaboration as a control loop, transforming users from “authors of individual prompts” into “goal setters and outer-loop calibrators”. The full text of the protocol can be found in docs/protocol.md in the repository, and the evolution history in docs/CHANGELOG.md; both documents are in Chinese.

What Problem It Solves

The naming of the plugin comes from Wiener’s cybernetics: setting a goal (setpoint), taking action, measuring the deviation of the output from the goal, and correcting the next action based on the deviation. The README maps this design to four things:

  • session goal persists as a product of the agent’s thinking, rather than existing only in the current prompt;
  • task agent acts, verify agent measures, triage agent triages;
  • controller advances between rounds;
  • the user completes outer-loop calibration through conversation.

Goal, action, measurement, and calibration constitute a negative feedback loop. When the measurement results indicate that the goal itself has a problem, the loop stops automatic iteration and returns to conversation. This is exactly the distinction from traditional one-time prompts: the traditional method is an open-loop system, and the responsibility for measurement lies entirely with the human.

Core Concepts and Role Division

The plugin internally defines five types of roles. First, understand the division of labor, as later features rely on this:

Role Responsibility
session agent Controller: identifies user input, maintains the goal board, starts/cancels rounds, narrates to the user; never executes tasks personally
controller Inter-round state machine, located in src/controller.ts: startRound/foldRound/runRound, advances subgoals (task → verify → triage) by round, maintains audit logs
task agent Executor: independent sub-agent with no conversation history, executes based on the brief, produces structured delivery reports
verify agent Measuring instrument: verifies independently of the executor, produces structured verdict {met, criteria, gaps, goalItselfQuestionable}
triage agent Triage: sorts subsequent clues exposed by the verdict into auto / defer / reject / human
User Outer-loop calibrator: conversational calibration (goal revisions executed via revise by the session agent), adjudicates human categories, confirms completion

Two hard constraints are worth remembering first. First, the controller never proclaims completion; whether completion occurs is ultimately confirmed by the user. Second, context is granted, not inherited—the task agent only takes the brief, the verify agent only takes the goal, the task report, and evidence; it never takes the narrative of the task agent, otherwise verification is polluted. The plugin explicitly forbids ever using context: 'fork'.

Core Features

Goal is persistent state, inner loop is automatic, outer loop is a safety net

The goal is persistent state of a first-class citizen (objective + current subgoal + full brief), built on top of the dsh-goal service, and no longer depends on the goal.md file since v0.2.

The inner loop (task → verify → triage → subgoal correction) automatically iterates when the goal remains unchanged, with a continuous iteration limit per round as a safety net. The outer loop (user and session agent) intervenes only when the goal itself has a problem—once a verdict gives goalItselfQuestionable=true, the plugin prohibits automatic iteration and forces a return to conversation. This boundary is the safety valve for the entire protocol.

append-only goals and the revise revision decision tree

Once a goal exists, it is no longer modified in place. revise is the only verb to change the goal, and reason is mandatory. A revision creates a new goal with parentGoalId and immediately starts its 1st round; the old goal is set to superseded and records supersededBy, with each revision appending an audit line to .cybernetics/revisions-<newGoalId>.jsonl.

Starting from v0.7, when abortPrevious is missing, it is decided by an automatic decision tree (pure function decideAbort, based on the goal board’s running state): if the old round is running and the revision rewrites the goal itself (objective or depth changes) → automatically abort the old round; if the old round is running but the goal is unchanged (only brief/title/role correction) → wait for the old round to end naturally; if the old task agent has already ended → directly start the new goal. Explicitly passing abortPrevious: true/false keeps manual control, and each revision’s audit line and the revise return value carry abort: {abort, mode: auto|explicit, basis}.

reviseing a superseded goal again will be rejected (already superseded) to avoid lineage forking.

quick / full dual-mode rounds

  • quick: a single task agent round produces results and directly feeds them back, skipping verify/triage. The cost is about one-third of a full round, and the verdict is derived from the task report.
  • full (default): fully runs the “work → verify → triage” loop, with delivery only occurring if acceptance passes. The protocol explicitly forbids using quick to cut corners for changes.

allGoalsMode: build goal before executing any request (default false)

After enabling, it injects the “ALL-GOALS MODE ACTIVE” routing protocol with four rules:

  1. Any user request (question, retrieval, analysis, change) must first build a goal (start + title/brief) before executing; session agent is forbidden from using read tools to complete tasks locally, even single file queries or simple calculations go through the loop.
  2. Simple read-only request → depth quick: bounded query, single file read, small statistics, single task agent round.
  3. Complex analysis/write task → depth full multi-round progressive: start starts asynchronously, collect converges round by round, controller advances subgoals by round; write tasks additionally rely on hard gates—the session agent never personally modifies anything; the goal loop is the only path, no matter how small the change.

When this mode is disabled, the prompt segment and revise behavior are completely consistent with v0.6. The configuration entry point is after v0.7.1: the plugin exports a schemastery Config schema, where allGoalsMode is a volatile field. It can be switched directly on the plugin entry panel in the DSH Web Editor’s Settings → Plugins, and after saving, the loader submits the new value to the running instance (without remounting), causing the prompt segment to be re-mounted immediately, without the need to manually edit the profile JSON.

hardGate: Structural gatekeeper with default allowlist

hardGate defaults to true and is activated upon installation. Starting from v0.8.10, it can be hot-switched on the plugin page settings card. hardGate is a volatile field: saving the switch submits the new value to the running instance, and the plugin immediately unloads or reinstalls the gate listener (assembling removal tools + distributing rejections + approval pass-through), taking effect immediately without requiring a host restart or plugin reload.

Effective semantics (v0.8.6, default safe): when hardGate is enabled and gateMode is not configured, the gate operates via allowlist. The top-level session agent only retains read + conversation + display + goal/control verbs (built-in DEFAULT_GATE_ALLOWLIST, extensible via gateAllowlist/gateAllowlistExtra), and all change tools are removed during assembly and rejected at the call distribution layer.

The reason for changing this default is that the old denylist default was a trap: it only hid three names of gatedTools (bash/edit/write), bsk_*, taskboard_*, schedule_*, farm_*, uno_*, ppt_*, config_restore, and the entire long tail of change tools could still be called—named a hard gate, but actually a half-gate. The schema no longer sets a default value for gateMode, so “not set” and “explicit denylist” can be distinguished. hardGate: false is consistent with the past, with no gate installed.

Explicitly configuring gateMode: 'denylist' is a residual risk and requires active choice: it only removes tool names from gatedTools (default bash/edit/write), and the top-level agent can still use bsk_* (including bsk_session start/stop), taskboard_* write operations (create/update/move/comment/checklist/delete), schedule_* write operations, farm_* service lifecycle (start/restart/stop/register/unregister), uno_* and ppt_* generators, config_restore, subagent/team derivation, knowledge_save, job_kill. This set grows with each newly installed plugin, and the denylist can never be complete; it is only explicitly chosen when one consciously accepts this risk and adds the required change tools one by one to gatedTools. The HARD GATE prompt injected in this mode honestly states the half-gate stance of “only removing gatedTools”, no longer falsely claiming that all world-changing tools are structurally unavailable.

The task/verify/triage sub-agents are never within the gate range, retaining all tools. Deployment note: when this build is first online and the host still holds pre-0.8.10 code, the host needs to be restarted once to load the new code; after that, the switch is hot-effective (just refresh the browser in lib/client.js).

Sub-agent Approval Pass-through gateApprovalRelay (default true)

The host pins the approval strategy of every sub-agent (workflow sub-agent, teammate) at the delegation boundary to never—every approval request is deterministically rejected before any answerer sees it. The result is that when the task agent needs to sandbox-scope to write paths outside the session workspace (e.g., ~/.dsh/profiles/web/cordis.patch.yml), the user has no chance to be asked.

When gateApprovalRelay is enabled (default, accompanying hardGate), the plugin does two things:

  1. Remove the pinned strategy when creating: listen to agent/created, append a persistent approval/policy {policy:'ask'} event to the sub-agent’s session on delegation. The host’s folding rules guarantee the later-written event overwrites the pinned value at delegation; without this step, the approval service would deterministically reject the request to rejected before any answerer runs.
  2. Re-initiate the request on the top-level session agent: the plugin registers the approval/request answerer as the outermost (prepend: true). The host’s approval/request forwarder registered at startup (earlier than all user plugins) is claimed by the browser approval answerer first, invisible to the sub-session panel user, and the forwarder never gets to run; after the plugin runs at the outermost level, it finds the top-level living ancestor of the sub-agent via the persistent parentSession chain, directly calls the host’s ApprovalService.request, allowing the native allow-one/reject panel to appear in the session the user is currently viewing, and leaving a trace in the session’s approval/asked + approval/decided audit pair. The sub-agent’s tool call id is not forwarded upwards; the panel title (localized displayReason) will specify which sub-agent sent the request.

Fallback and terminal semantics (v0.8.8): if the top-level round is already closed (a continuable sub-agent lives longer than the parent round), or the native panel answer is unavailable (no answerer claims this request, e.g., the frame is not claimed by the browser), the relay continues to fall back to the ask_user_question card visible on the same top-level agent (“Sub-agent approval pass-through”)—the native unavailable is not a terminal state. It only ends with a terminal state unavailable when the question card channel is also unavailable; if there is no living top-level agent, it falls back to the native answer chain. Rejection semantics are always explicit: forwarding failure answers rejected, sub-agent already aborted answers cancelled, and the sub-agent model can always get the real result through its own approval audit pair, never silent success.

Observability and troubleshooting (v0.8.9): relay records two places for every decision—one line cybernetics approval-relay: path=… outcome=… root=… elapsed=…ms on the host logger (fallback to console.debug when there is no logger), and a JSON appended to <workspace>/.cybernetics/relay-log.jsonl ({ts, root, path, outcome, elapsedMs}). This file is the ground truth during acceptance and can be read directly without the host stdout or browser console. path values: native-panel, question-card, next-passthrough (no living top-level agent → native chain). Typical troubleshooting: sub-agent fast fails and the audit is decided unavailable, but relay-log.jsonl has no records, indicating the request didn’t reach the relay—first confirm the host actually reloaded the plugin build; if path=native-panel outcome=unavailable is followed by path=question-card, it means the frame left the host but the browser didn’t claim it—at this point the user can still see the question card.

Setting gateApprovalRelay: false allows returning to the strict deterministic rejection stance (CI/unattended deployment). This field is an accompanying item to hardGate, a non-volatile / configuration file maintenance field; please maintain it in the profile configuration documentation, and saving triggers a plugin reload. Host-side evidence (file:line), rejected schemes, and reproduction methods are recorded in docs/approval-relay-investigation.md.

Persistent executor

executor: 'ephemeral' | 'persistent', orthogonal to depth. persistent uses a persistent teammate from Agent Teams to execute task work: the first round spawnTeammate(context:'fresh') is spawned, and subsequent rounds only deliver brief revisions (new constraints/acceptance criteria changes/scope narrowing, open-ended conversation is prohibited). In full rounds, the teammate’s report is used directly as the task report, the task agent is skipped, and verify/triage still runs as temporary agents independently—verification does not inherit the task narrative. The verdict of quick is dispatched from the teammate’s output (the output contract requires the last line {"done": bool, "summary": "...", "evidence": "..."}, with a missing marker meaning incomplete).

This is a soft integration: runtime priority uses Cordis’s non-injected reflection to read ctx.reflect.get('agentTeams') (no need for inject, returns the service value or undefined), then falls back to a try/catch safe accessor to read ctx.agentTeams (Cordis’s context proxy throws directly when reading un-injected service properties, and cannot be treated as “service does not exist”). If neither path is available, it falls back to the workflow, does not hard-bind the experimental package, and the error is structured needs Agent Teams … fall back to executor "ephemeral", not a Cordis proxy exception. When the goal completes/cancels, it interrupts and cleans up the permanent teammates, with at most one executor per goal. The goal board keeps the unique truth and does not double-write to the team’s task board.

Multiple Goal Boards and Client Tabs

The same session supports multiple concurrent goals (the plugin’s own goal board; dsh-goal’s single-slot fallback is the focus goal). The goal board is a process memory state; every write point (goal creation, round start, settlement decision, cancel) simultaneously snapshots the entire board to <workspace>/.cybernetics/board-<sessionId>.json; when first touching an empty board, it hydrates from the snapshot, and immediately publishBoard() once after restoration, so tabs do not need to wait for the next round to show historical goals. Sidecar file write failures are swallowed (try/catch), and the read-only workspace can still run rounds; snapshots are the current state, not event logs; event logs remain in the session. Rounds killed midway will resume with status: 'running' but no running handle, and the next round/start will restart normally.

The client lib/client.js provides two session tabs, pushed and updated in real-time via the plugin’s own session projection cyberneticsGoals:

  • “目标 / Goal”: All goals in this session (title, status badge, round N, current subgoal, depth, executor small tag, superseded and revision source annotations), sorted in two sections: “Incomplete first, Completed last”, empty board falls back to dsh-goal single goal rendering.
  • “控制 / Control”: Recursively displays the running status of all agents in this session (main controller + subagent catalog) and their respective goal views, summarizing running counts at the top, and listing each goal’s executor row (status + title + executor temporary/persistent + persistent teammate name + round), with the same data source as the “Goal” tab.

Both tabs have no second set of state; the data source is the same cyberneticsGoals whole value projection. Starting from v0.10.0, each session header permanently displays a badge: a green dot with “On” indicates that the current control mode is effective (either the current session /cybernetics on explicit override, or inheriting the global allGoalsMode), and a gray dot with “Off” indicates it is turned off.

Installation and Enablement

The plugin is published in the community directory SkillHub and on GitHub. Installation commands should be obtained from the community directory page, not by concatenating the repository name:

  • Community directory page: https://www.skillhub.cn/plugins/MarchLiu/dsh-cybernetics
  • GitHub repository: https://github.com/MarchLiu/dsh-cybernetics

It is worth noting first that the community directory is an independent site with no official affiliation with DeepSeek / Fangfang; do not treat the installation source as the official app store. Package info (from repo package.json): package name @mars.liu/dsh-cybernetics, version 0.15.0, license MIT, type: module, with badge versions in the two READMEs being 0.10.0. The peer dependency is @deepseek-ai/dsh-session, and the runtime dependency is @deepseek-ai/schemastery ^3.18.5; engines.node requires >=23.6.

Behavior after enabling:

  • hardGate defaults to on, no extra config needed to get the gate; gateMode/gatedTools/gateAllowlist/gateAllowlistExtra/gateApprovalRelay/executor/depth/axiom* etc. are non-volatile, maintained in profile config docs, saving triggers plugin reload.
  • allGoalsMode defaults to off, switch in Settings → Plugins panel to enable full goal routing.
  • When installing this build for the first time, if the host still holds old code, the host needs to be restarted once to load new code, after that the switch is hot-effective.
  • The repository root directory provides cordis.patch.yml as a bundle patch, lib/client.js side depends on several @deepseek-ai/dsh-client-* injection packages, confirm the host’s plugin mechanism supports bundle patch and client inject before loading.

Typical Usage

The plugin’s tool surface is concentrated on the goal lifecycle, with cybernetics as the core verb. The path for running a round within a session is as follows:

  1. Initiate goal. Created by session agent (giving title/brief/objective), or automatically triggered by any user request after allGoalsMode is enabled. A running record immediately appears on the goal board.
  2. Observe round progression. The plugin drives the inner loop by running a workflow once per round: task agent reads the brief to execute and produces a structured delivery report, verify agent produces a verdict, triage agent sorts clues into auto / defer / reject / human; the auto category iterates on its own, the human category returns to conversation waiting for user adjudication.
  3. Revise goal with revise. Call in the form cybernetics { action:'revise', goalId, reason, objective?, title?, depth?, executor?, abortPrevious? }—the new goal inherits the old goal’s depth/executor/role/objective (can be overridden by parameters), writes parentGoalId, and immediately starts the 1st round; the old goal is set to superseded.
  4. Verify hard gates and approval pass-through under full write tasks. The session agent will delegate to the task agent; if the sub-agent requests to write outside the session workspace, gateApprovalRelay will bring the allow-one/reject panel to the current session, and after the decision, you can verify the actual path in <workspace>/.cybernetics/relay-log.jsonl. If the sub-agent request is silently rejected, check if this session has gateApprovalRelay enabled and confirm the host has reloaded the plugin build.
  5. Confirm completion on the client. Check this round’s verdict and executor row in the “Goal” or “Control” tab. The controller will not proclaim completion; it requires user confirmation.

Applicable Scenarios and Considerations

Suitable for these types of practitioners:

  • People who need to hand over the “measurement” step to the system in multi-step tasks, rather than manual acceptance every time. Iterative analysis and multi-round write tasks benefit especially.
  • Teams that want clear security boundaries. The structural gatekeeper of the default allowlist is closer to its claimed strength than a denylist that only hides a few tool names.
  • Scenarios that need to track multiple goals and revise them separately. The multi-goal board and two tabs are designed exactly for this.

Three considerations:

  1. Scope of permissions. The plugin runs with the current dsh process permissions; read the source code and LICENSE (repository LICENSE is MIT) and verify dependencies and peer dependencies declared in package.json before installation; the community directory page is only for discovery, not official endorsement.
  2. Don’t confuse gate semantics. “Explicitly configuring gateMode: 'denylist'” is a residual risk and requires active choice, only removing tool names from the gatedTools list; keep gateMode unset to use the default allowlist when structural isolation is needed.
  3. Boundary of state and audit. The goal board is a process memory state, relying on sidecar snapshots to restore across processes; snapshots store current state; audit and event logs are within the session; relay-log.jsonl and revisions-<goalId>.jsonl are disk evidence with different purposes, do not mix them.

Summary

One-sentence review: marchliu/dsh-cybernetics reconstructs agent orchestration in DSH using the language of cybernetics—goal persistence, separation of action and measurement, triage deciding the next step, and the user only calibrating the outer loop. Combined with the default structural gatekeeper and approval pass-through, it transforms an easy-to-drift, unverified open-loop process into a negative feedback loop with measurement, safety nets, and manual confirmation.

For more details, refer to the full protocol docs/protocol.md and the evolution history docs/CHANGELOG.md:

  • Community directory page: https://www.skillhub.cn/plugins/MarchLiu/dsh-cybernetics
  • GitHub: https://github.com/MarchLiu/dsh-cybernetics
羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

Xiaoye