~/satyajit

dsh-superagent: an issue-and-PR agent built to cost a team as little attention as possible

mdjsonmcp

2026-11-01 · 17 min · agents · coding-agents · github · deepseek · harness

Why read this

Solidtop 85%

Reads the whole attention-first design and enumerates its gate, stage and slot rules by re-implementation; the repo's own tests could not be run here.

  • Original analysis
  • A new technique
  • Concrete numbers to act on

Agents & harnessesAPI onlyMITPractitioner teardown

How this was scored
Is it new?
2 of 3: A real new idea, method or capability
Can I trust it?
1 of 3: Spot-checks a few numbers
Can I run it?
1 of 3: API-only, gated or restrictive licence
Will I understand it?
2 of 3: Mechanism from first principles with figures
Can I act on it?
2 of 3: A concrete recipe, numbers or comparison
Will it last?
1 of 3: Relevant for months
Does it affect many?
1 of 3: A specialist community
Only here?
2 of 3: A teardown or measurement few others did

Score 53 of 100, ranked 513 of 666 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

When Leandro von Werra posted about it, the pitch was one line: an agent framework that works through a repository's issues and pull requests with many agents in parallel, designed to take as little human attention as possible. The second half of that sentence is the interesting half. Most coding-agent write-ups are about what the agent can do. This repository is about what it is allowed to say.

deepseek-ai/dsh-superagent-prototype@6337fc8 · snapshot 2026-11-01
tracked files
216
license
MIT
branch
main
tests
65 files
source
2.1 MB
commit date
2026-10-31
source by language
TypeScript2.1 MB(177)CSS40.6 kB(1)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-11-01 at 6337fc8 — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow, testFileCount

shallow clone: counts describe the pinned tree, not the history

I cloned it shallow, read the documents and the code that backs them, and rebuilt three of its decision rules as small programs so I could enumerate them. I did not run the repository. The sandbox I work in refused the dependency install (more on that below), so the claim that a rule behaves a certain way comes from reading it and from my own re-implementation, never from its 794 test cases passing. Every claim here carries one of four labels: reported (the repository's own documents say it), read (I read the code and it does that), measured (I counted or ran my own code), reasoned (my inference from the other three).

What it is

Reported. dsh-superagent is "a proactive coding agent for one GitHub repository, packaged as a bundle for DeepSeek Harness". It listens to GitHub and, optionally, Feishu. When someone allowed asks it for work, it opens a work item, works in its own agent session and directory, and answers in the original conversation, usually with a pull request. A web panel shows what needs a person. The README is blunt about status: "Research prototype for reference only, not an official DeepSeek product", with much of the engineering done by coding agents, and some designs in the companion article "Exploring Super-agent Design: Towards a More Autonomous Agent System" not implemented here. The licence is MIT (read from LICENSE, copyright 2026 DeepSeek Harness Team).

Measured (by counting files): the HEAD I read, 6337fc8, has about 11,200 lines of TypeScript outside the client, 14,960 lines of tests and 7,529 words of prompts across 15 files. Counting it( and test( lines gives 794 test cases. I did not run any of them.

It is not a standalone program. It is a plugin bundle for DeepSeek Harness, the Cordis-based agent harness covered earlier on this site: package.json lists about 25 @deepseek-ai/dsh-* peer packages at 0.2.1-alpha.2, and a cordis.patch.yml inserts the plugin and two Executor presets into a DSH profile. So the agent loop, the shell tool, the session store, compaction and the subagent tool are all DSH's. What dsh-superagent adds is the layer above a single session: who may start work, what counts as one piece of work, how many pieces run at once, and when to bother a human.

The pipeline in one paragraph

Reported, in docs/001-domain.md. A Source (GitHub, Feishu) turns events into Triggers. Triggers collect into Threads, and at most one Eval runs on a Thread at a time. Eval is two layers: a deterministic gate (who sent this, are they allowed, did they address the agent) and a model assessment (is this work, does a written rule allow it). An accept creates a Ticket carrying a Mandate that records why the work is permitted and how far it may go. An Assignment binds the Ticket to an Executor, which is one DSH session named worker-<ticket id> plus a directory. Results return through a Channel: a comment, a review reply, a pull request.

One structural choice is worth noticing. A person is not a domain concept: a Trigger carries its actor's identifier as the Source gave it, and "identities on different Sources are never mapped to each other". The main session (the router, orchestrator in code) decides; the workers (worker) do the work; and the gate in front of the router is plain code.

The gate: who may start work

Read from src/policy.ts. The gate is a function of six inputs, and it is the part of the system a security reviewer should read first, because the model never sees a message the gate refused.

The rule, in the order the code tests it:

  1. A person on the Source's allowed list who addresses the agent (a mention or a direct message) makes an explicit request. No table is consulted.
  2. Otherwise a per-scope admission table decides, with three cells: listed but not addressing the agent (default: inferred), not listed but addressing it (default: ignore), neither (default: ignore). An unaddressed message only reaches the table when the scope receives every message; otherwise it is dropped upstream.
  3. A cell set to explicit wins outright. This is how a support channel can be opened to anyone who mentions the agent.
  4. A follow-up on a thread that already has a ticket continues it. A listed person's follow-up is explicit; an unlisted person's is inferred, and only when the accept mode is not off. A direct message from an unlisted person never counts as a follow-up.
  5. An inferred cell becomes an inferred candidate unless the mode is off.

Messages from bots take a separate path: only bots on the Source's bot list can produce inferred candidates, never an explicit request, and other bots are not admitted at all.

Measured. I wrote the human-trigger branch again from the code as an independent function and enumerated every combination of sender (listed or not), addressing (none, mention, direct), existing ticket, accept mode (off, observe, auto), scope observation, and the unlisted-and-addressed cell. With six inputs that is 72 combinations per table setting. With the default table: 30 end explicit, 10 inferred, 32 refused. Three properties hold across all of them. An unlisted person never produces an explicit request under the default table (0 of the unlisted cases). In mode off, an unlisted person is never admitted at all, by any route (0 admitted). And with the "open support" table, where the unlisted-and-addressed cell is explicit, a mention is explicit in every mode including off. That last one matches the documents: "explicit requests work in every mode".

The enumeration is only as good as my reading: a re-implementation, not their code, covering one branch of the gate (human triggers).

Reasoned. The design buys something specific: the set of messages that can start work is a table you can read, not a prompt. A cleverly worded comment from a stranger can reach a ticket that already exists (fan-out grants context, not authority), and it can reach the model as an inferred candidate if the table says so, but it cannot become an explicit request by phrasing alone. The document says the same thing in one sentence: "Messages, comments and quoted documents are inputs to a decision; they cannot change those permissions."

Accept modes, and the habit of doing nothing first

Reported and read. Three modes. off: nothing inferred becomes a candidate. observe (the default): inferred candidates are assessed, a positive assessment is recorded as an ignore decision marked "would accept", and nothing is accepted. auto: a positive assessment accepts. Explicit requests work in every mode.

The assessment itself is validated in code rather than trusted. assessmentOutcome() throws unless the model judged every enabled rule exactly once, whether it matched or not. An accept needs a direction block that passes directionAllowed(): implementation requested, a target that is specified or an existing-standard (not undetermined), and no open design decision. It also needs at least one matched rule and non-empty intent_evidence. Fail any, and the decision is rewritten to an ignore with a reason code (directionUnsettled, noRuleMatched). The shipped default rules are two narrow ones, copy fixes and visual fixes, and the visual rule carries a long instruction that says locating an element or calling it "inconsistent, unattractive or hard to understand does not establish a target".

Reasoned. This is the philosophy document made executable. "Work nobody asked for needs a rule the team wrote, and starts in a mode that only records what the agent would have done." The default observe mode produces a log of would-have-accepted decisions you review before widening anything. The cost is that a team gets nothing proactive until it has read that log, which is the point.

Not colliding

The question in the task I was given was how parallel agents avoid colliding. The honest answer from the code is that there is no lock manager. Collision is avoided by partitioning.

Reasoned. Two workers cannot race on a file because they do not share a checkout. Two racing on the same pull request is the case to watch: the prompt tells a worker to verify an existing PR's author, head branch and remote first, again an instruction to the model. The host's backstop is the branch-name rule tying a PR to one Ticket.

How many run at once

Read in WorkerSlots, src/executor/dsh.ts. maxWorkers defaults to 3 and the settings schema allows 1 to 16. A Ticket holds a slot while its turn runs, while its shell jobs or subagents still run after the turn ends, and while a launch or wake is starting. Launches reserve a slot synchronously before awaiting anything, so two launches cannot take the last slot.

The distinctive rule is the extra slot. A wake caused by a person's message may use one slot beyond the limit, so that "new work therefore cannot prevent a person from continuing an existing Ticket". Bot and CI inputs for an idle worker wait for a normal slot.

Measured, on my re-implementation: with 3 workers running at a limit of 3, a new launch is refused (Later), a CI wake is refused, a first person's message takes a fourth slot, and a second person's message is refused. At every limit I tried (1, 3, 8, 16), exactly max launches reserve before the next is refused, and then a person's wake still fits. The code's own launches() helper returns minus one in that state (3 limit, 4 used), which is harmless if the caller clamps it. I did not check whether it does.

The other limits are time, not money:

LimitValueSource
Eval timeout120 s default, 30 to 600EvalConfigSchema (read)
Expired Evalrequeued twice, then unavailabledocs/002 (reported)
Foreground shell commandmoves to a background job after 5 sworker preset (read)
Background work after a turn30 minutes, then jobs are killeddocs/003 (reported)
prepare command10 minutesdocs/003 (reported)
Provider/network failurethree retries with growing backoffdocs/003 (reported)
Redelivery / outgoing retry2 s to 60 s / 5 s to 300 s, doublingdocs/003 (reported)

Reasoned. I searched the source and documents for a token or spend cap and found none. The only "budget" in the code is a model-budget retry: a session waiting out a provider limit is listed in a models health entry. The system bounds concurrency and wall-clock per step; it does not bound what a ticket costs. For a prototype that is a reasonable omission and an operational gap you would fill with the provider's own limits.

What reaches a person

The panel opens with three questions: does anything need me now, is the system healthy, what is the history of one piece of work. The first one is answered by the Ticket stage table, which is the most carefully specified part of the project.

Read in src/core/stage.ts. The function is a chain of early returns, and the documentation says "the first matching row wins". Rows in order: ended; input delivery failed (person / delivery); an open question (person / answer); latest report ready (person / review); latest report blocked or failed with no human message since (person / blocked, failed); no assignment or a pending one (working / queued); report waiting or the assignment ended (idle); anything else is working.

Two consequences. First, a ticket can satisfy several rows and the panel shows one; a failed delivery hides an open question and a question hides a ready report. The stage means "the first thing a person must do", not "everything true of the ticket". Second, a human message after a blocked or failed report releases it back to the worker, so the label clears on its own.

The same discipline governs what the agent says. It confirms a request once, stays quiet while it works, and writes again for one of three reasons: a result is ready, it needs a decision, or it cannot finish. A worker closes a ticket as dropped when there is nothing for a person to do (not reproduced, not a defect, superseded), and a dropped ticket sends no message. The worker prompt says to stop by impact: "If direction is unresolved or code inspection reveals changed behavior, public interfaces or product decisions, stop and report dropped without expanding scope". Every GitHub message begins with "This is by an automated agent, not a human" by default.

Blast radius, as read

This section describes permissions as the project states them. It is not an audit and I tried nothing against it.

Reported, and stated plainly in docs/006-deployment.md: "Both presets run with danger-full-access as the operator's OS user." The account wrappers "choose the agent's credentials by default; they do not isolate the process. A worker can execute programs by path and read the operator's home directory. For isolation, use a dedicated OS account." The execution doc repeats it: the wrappers and directories "establish defaults, not a security boundary", and blocked commands "fail when invoked by name".

What the host does around that:

Reasoned. The authorization layer is strong and the execution layer is deliberately thin. The gate, the Mandate and revalidation decide whether work starts and stop a message from widening it. Once a worker runs, it has the operator's file permissions, and what constrains it is a preset's tool list, shims that route credentials, and prompts. If you deployed this, the document's own advice is the minimum: a dedicated OS account holding only the agent's credentials, branch protection that requires human approval and dismisses stale approvals, and the allowed list kept short.

A Mandate is immutable and ending a Ticket does not end it; only an accept, a person's stop, or a failed revalidation changes it. Weakening a policy cell does not cancel running work (the docs reject that because "a cancelled change leaves half-finished work"); the worker gets a notice, and the change is enforced when an ended Ticket tries to resume.

Compared with the other harness writing here

The closest sibling on this site is DeepSeek Harness itself, and the comparison is about layers. DSH holds the loop, tools and logging discipline; dsh-superagent is a policy and scheduling layer written as a plugin. Its workers use a trimmed preset with no question tool, no plan mode and no scheduling; spawned subagents run under DSH's standard preset.

Lilian Weng's framing in Agent harnesses is the loop around a model: tools, context, control flow, evaluation. Against it, dsh-superagent adds almost nothing to the inner loop and most of the outer control flow: admission, serialization, resumption, handoff. The harness effect argues orchestration sets an agent's token bill; here it is built to set a colleague's attention bill, measured by a panel rather than a benchmark. I found no evaluation in the repository of how often the gate or the assessment is right.

The isolation story differs most from DSec, which runs rollouts in layered sandboxes. dsh-superagent's workers share the operator's machine and OS account: coherent for a tool editing one team's repository under that team's supervision, poor for input from the open internet.

What I did not do

What to take from it

If you want to copy one idea, copy the gate: put "who may start work" in a small table a human can read, make the model's output pass a validator before it counts, and default to a mode that records what the agent would have done. If you want to copy two, add the stage table, where every state has one owner and the panel shows only the states that wait for a person. If you deploy it, the weakest link is documented by its authors: the workers run as you. Give them their own account first.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "dsh-superagent: an issue-and-PR agent built to cost a team as little attention as possible", ai.thesatyajit.com, November 2026.

bibtex
@misc{ghana2026dshsuperagent,
  author = {Satyajit Ghana},
  title  = {dsh-superagent: an issue-and-PR agent built to cost a team as little attention as possible},
  url    = {https://ai.thesatyajit.com/articles/dsh-superagent},
  year   = {2026}
}
share