DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Feature Loop — DeepSeek Harness 插件(DSH Plugin)
← Plugins
F

@freepeak/dsh-feature-loop

Feature Loop

面向修复 bug 和实现小型功能的书本式策略层:设置预算上限、优先采用低成本方案、提供步骤级审查门槛,并使用 Laya 本地评判器。基于 DeepSeek Harness 代理循环运行,而非将其分叉。

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add github:FreePeak/dsh-feature-loop#0ae4ddefa5a7697eea630a7d4e4c62255643d4a6
README兼容性版本
The Feature Loop dashboard inside the DSH UI: approval thread, workspace tree, run meters and activity feedThe Start a loop control, showing the session the run will use
The same page in dark theme
The Settings tab: status, judge, attention and gate mode
Two columns at a 1000px page width
One column at a 640px page width, with settings and Start stacked

兼容性与来源证明

Feature Loop 以 @freepeak/dsh-feature-loop 发布,当前版本为 0.4.8。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
web
发布来源
github
Registry 更新时间
2026/10/16

版本

0.4.8stable
2026/10/16
0.4.7stable
2026/10/7
0.4.5stable
2026/10/7
查看其余 17 个版本收起版本
0.4.2stable
2026/10/6
0.4.1stable
2026/10/6
0.3.1stable
2026/10/6
0.3.0stable
2026/10/6
0.2.5stable
2026/10/5
0.2.3stable
2026/10/5
0.2.2stable
2026/10/5
0.1.10stable
2026/10/4
0.1.9stable
2026/9/30
0.1.8stable
2026/9/30
0.1.7stable
2026/9/29
0.1.6stable
2026/9/29
0.1.5stable
2026/9/28
0.1.4stable
2026/9/28
0.1.2stable
2026/9/28
0.1.1stable
2026/9/24
0.1.0stable
2026/9/23

相关插件

正在加载相关插件…

最新版
0.4.8
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
未提供
文件数
未提供
Surface
web
许可证
MIT
发布源
github
GitHub
★ 4
周下载
0
最近提交
2026/10/16
查看源码 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录

相关插件

继续浏览 developer-tools 分类下经过校验的插件。

Web App@deepseek-ai/dsh-web-appdsh 浏览器界面捆绑包:位于 dsh-base 之上的 Web 补丁层,加上运行时粘合插件(提供前端 dist、Web 界面提示符、bash 运行时变量和 URL 行)Sdk App@deepseek-ai/dsh-sdk-appdsh SDK 配置包:基于 dsh-base 提供 stdio JSON-RPC 服务和进程生命周期管理Experimental Inspector@deepseek-ai/dsh-experimental-inspector用于 Host 调试和 Client Runtime 检查的实验性跨 realm CDP hubExperimental Inspector Profile@deepseek-ai/dsh-experimental-inspector-profile用于原始 Session 日志和 Chat 节点检查的可选 Web 捆绑包

README

dsh-feature-loop

@freepeak/dsh-feature-loop

Bounded agent loop for DeepSeek Harness
Budget ceilings · cheap-first routing · loop hygiene · human-in-the-loop review

Quick start · HITL dashboard · Book mapping · Architecture · Setup · Contributing

A book-shaped policy layer for bug-fixing and small features: budget ceilings, cheap-first routing, a step-level review gate, and a local judge that decides which steps are worth your attention.

Hosted on @deepseek-ai/dsh-agent-loop's own extension points (agent/pre-step, agent/request, tools/pre-execute) rather than forking it, plus a standalone runner that exercises the same policies without a harness build.

De-forked. This package used to vendor a copy of the agent loop. It no longer does — the harness publishes an extension point for every policy this package adds, so ~3,064 lines of forked code were removed and replaced by src/plugin.ts. See docs/PRD.md for the audit and the acceptance criteria.


The 0→1 loop

Describe a product in one sentence. The loop researches it, writes a PRD, implements it, proves it with your test suite, and opens a pull request — every step carrying an evidence artifact, every phase carrying its own budget.

New in dsh/zero-to-one. Off by default; flip pipeline.enabled in the patch row. Full design and rationale: docs/PRD-0to1.md.

make install                       # build, add to a profile, verify it is NOT inert

Then, in the Feature Loop page, describe what you want built.

Five phases, five budgets, five gates

PhaseWhat it doesExit gate — observableShare
researchDecompose into sub-questions, ≤5 sources each, every claim citeddocs/0-research.md exists and cites a source10%
prdProduct-agent fit test, scope, explicit non-goals, success criteria, metricsdocs/PRD.md has Scope / Success / Metrics10%
implementRead before write, search-and-replace edits, smallest change≥1 tracked file changed35%
testWrite-test-fix, 3 attempts then report, diff carried forwardyour test command exits 015%
shipCommit on the run's branch → push → gh pr createa PR URL is recorded20%
—buffer — reserved for the terminal report, never spent on work—10%

A phase that reaches its own ceiling stops, even with the run budget untouched. A phase cannot borrow from the next one — the book's rule is to simplify the plan rather than steal from execution.

Set pipeline.testCommand to your project's runner. There is deliberately no default: the test phase's gate is that command's exit code, and a guessed one would report success for a project whose tests never ran.

YOLO — bounded autonomy

gateMode: auto never prompts. Safety moves into an envelope instead:

AllowedDenied outright
reads, writes inside the run's worktreepush to main / any protected branch (every refspec spelling)
test commandsforce-push, git reset --hard, git clean, rebase
git commit on the run's own branchgh pr merge, gh release, gh api
git push -u origin fl/<slug>npm publish, terraform apply, kubectl, docker push
gh pr createany write escaping the worktree, or touching .env / *.pem / credentials
any tool or command shape it cannot read — deny, not guess

The run lives in a throwaway git worktree, so a runaway run can dirty a directory you delete with one command and never your checkout. Outside a git repository YOLO refuses to start rather than degrading.

Stop it: touch .feature-loop/worktrees/<runId>/.feature-loop/STOP, or the Stop button on the page. Checked on every tool call, so it lands within one call.

Ship stops at the pull request. There is no merge, release or deploy path in this package, and the envelope denies the commands that would perform one.

Evidence at every step

.feature-loop/runs/<runId>/
  REPORT.md     # goal · phase table with budgets · outcomes · artifacts · PR · verdict
  steps.jsonl   # one line per step: tool, cost, latency, signals, evidence
  phases.json   # per-phase spend beside the ceiling that would have stopped it
  artifacts/    # research.md · PRD.md · test output · pr-url.txt

A step with no artifact cannot be reported as a success. Write steps whose evidence list is empty are counted, carried in the run record, and printed in their own section — a run that reached its goal and wrote something it cannot show reads UNVERIFIED, not green. Checking only the final output misses the 80% of failures that live in intermediate steps.


Features

0→1 product loopresearch → PRD → implement → test → ship, each phase gated and budgeted
Bounded YOLONever prompts; a worktree, a tested deny-list and a kill switch instead
Evidence per stepA bundle per run; unverified writes can never render as a success
Budget the loopStep and USD ceilings — per run and per phase — checked before the call
Cheap-first routingModel ladder by step type (onegw/cheap → execution)
Loop hygieneTool-cycle, error-cascade, budget trajectory, and related detectors
Fail-closed review gateTiered by reversibility; low judge confidence → ask the human
HITL ops dashboardPhase rail, workspaces, activity, grouped runs, Allow / Reject
One policy, two pathsStandalone runner and DSH plugin share the same rules

Workers — dsh orchestrates, coding CLIs build

Turn on workers.enabled and the session becomes an orchestrator: it hands the implement, test and validate work to xdev, claude or opencode, each run in a dsh terminal session, and receives the result as a dispatch_worker tool result in its own thread. Every dispatch passes the same human gate (or the YOLO envelope: allow-listed CLI, cwd inside the worktree). Off by default. Design, safety model and verification: docs/WORKERS.md.

HITL ops dashboard

The approval surface is a page inside the DSH web UI — the Feature Loop sidebar entry — not a second application you visit. When a tool hits the review gate the ask appears in a stockbroker-style thread with optional feedback, not a modal interrupt. Live runs are grouped by workspace; Activity and Runs scroll independently.

The Feature Loop dashboard inside the DSH UI: approval thread, workspace tree, run meters and activity feed

You can start the loop from that page — describe the task and it is submitted as a normal turn, so the ceilings, detectors and gate apply exactly as they do to anything typed in the composer.

The Start a loop control, showing the session the run will use

The page follows the harness palette, so it tracks the host's light/dark theme rather than carrying its own:

The same page in dark theme

Configuration lives in the same place:

The Settings tab: status, judge, attention and gate mode

It responds to the page, not the window

The dashboard renders inside the harness's main column, which is narrower than the window — the sidebar and right bar take their share. So the layout is driven by container queries: at a page width of ~1000px the run rail moves below the thread, and below ~520px everything is one column and the form controls stack.

Two columns at a 1000px page width

One column at a 640px page width, with settings and Start stacked

PaneWhat you get
LeftLive workspaces / sessions tree + scrollable activity ledger
CenterApproval thread with review brief, Allow once / Reject, optional feedback
RightRuns grouped by workspace with step/spend meters and signals

Live updates are pushed: the host emits a coalesced change event and the page re-reads on it, so a new run or a pending ask appears immediately rather than on a poll. A slow interval remains only as a safety net for a dropped frame.

An ask is claimed only while a page is actually watching; otherwise it falls through to the composer panel, so a dashboard nobody has open can never strand a decision.

For headless and CI there is still an opt-in loopback page — set dashboard.standalone: true in the patch row — which serves the same components on 127.0.0.1 with its own token. Details: docs/VERIFY-DASHBOARD.md.


The demo works

$ bash demo/run.sh
...
── step 10 · xiaomi/mimo-v2.5 · spent $0.0044
[signal:critical] error-cascade — 3 consecutive failing steps — the first failure is the one to read
[review] ASK HUMAN via signal — 3 consecutive failing steps — the first failure is the one to read

── step 12 · xiaomi/mimo-v2.5 · spent $0.0047
[judge] review-worthiness 0/3
[model] 3 failures, all in the `percentile` method. The doc comment says nearest-rank uses
        `ceil(p/100*n) - 1`, but the code uses `Math.floor((p/100) * n)`. These differ when
        `p/100*n` is an integer — `floor` gives that index, but `ceil(n)-1` gives one before.
[review] ASK HUMAN via policy — edit_file: reversible-write below the confidence bar (0.00 < 0.70)
[tool] edit_file (reversible-write) ok — Replaced 1 occurrence in src/latency-window.ts at line 88
[check] GOAL MET

[run-end] goal-met · 12 steps · $0.0066 · 1 review(s) (8% of steps)

The loop read the bug report, found the root cause in the source, was stopped for review twice (once by a critical signal, once by the write gate), made a one-line fix, and proved it with the test suite. Full transcript in demo/TRANSCRIPT.txt.

Two details worth noticing. The loop diagnosed the bug precisely — it read the module's doc comment, compared the stated formula against the code, and identified the exact condition under which they diverge. And the gate held it at the write: the judge scored the step 0/3, which is below the confidence bar, so the gate asked instead of assuming. That is the fail-closed direction working, even though the judge's score was itself wrong.

Verified runs

All three terminal paths, against the real model through onegw:

CommandOutcomeStepsCostReviews
bash demo/run.shgoal-met12 of 15$0.00661 (8%)
bash demo/run.sh --max-steps 5budget-stop (step ceiling)5 of 5$0.00391 (20%)
bash demo/run.sh --budget 0.00001budget-stop (cost ceiling)2 of 10$0.00020

The step-ceiling run stops at 5 without spending a sixth call — the ceiling is a limit, not an invoice. The cost-ceiling run stops after 2 steps because the budget was $0.00001 and it had already spent $0.000232.

Repeat runs land between 9 and 12 steps and 8–20% review rate, depending on whether an error cascade happens to trip. The book's target is <10%; critical signals are deliberately not rate-limited, so a run with a cascade will exceed it and that is the intended behaviour, not a miss.


Quick start

# 600 tests, no network, no model call — the policy layer is pure
node --experimental-strip-types --test test/*.test.ts

# the end-to-end demo (needs onegw on :8080 and xiaomi/mimo-v2.5)
bash demo/run.sh

# watch the ceilings actually fire
bash demo/run.sh --max-steps 6        # step ceiling
bash demo/run.sh --budget 0.000001    # cost ceiling
bash demo/run.sh --judge none         # detectors only, no judge

demo/run.sh resets the planted bug first, so every run has real work to do.

Running it

Docker (recommended) — one command, no toolchain, approval panel working:

make up          # build, start, and print the URL + token
make help        # all targets

make up picks the gateway key up from ~/.dsh/.credentials.yaml when it is not in the environment. Without make:

ONEGW_API_KEY=sk-... docker compose -f docker/docker-compose.yml up -d --build
docker compose -f docker/docker-compose.yml logs -f   # the URL + token

It installs the harness CLI and its bundles from npm (no monorepo build), builds only this plugin, and pins the permission preset that makes the approval panel appear. See docker/README.md — including the two hosts DSH refuses to bind, and the small relay that resolves it.

Installing it into DSH

make install PROFILE=fl      # build, add, and verify the plugin is not inert
make uninstall PROFILE=fl

make install exists because of the two ways an install can silently do nothing:

The peers may not resolve. The harness packages are optional peers and this repo disables peer auto-installation, so a profile that pulls in nothing else from the harness installs the plugin, composes it, shows it in the boot graph, and then does nothing: no gate, no review, no approval. make install checks they resolved and exits non-zero with the fix if they did not.

The package may not be in the profile's bundles. dsh plugin add installs the dependency and nothing else — it does not append the package to dsh.profile.bundles, and a plugin composes only when it is listed there. So a plugin can be installed, present in package.json, absent from --dump-config, and governing nothing. make install appends it with scripts/add-bundle.mjs, then verifies the rows are actually composed.

Both states look identical from the UI — a healthy-looking page with no policies running — which is why an installer that reports success on either of them teaches you a lie. If you hand-write the profile manifest instead, the bundles array must include this package by name: see docs/SETUP.md.

Install by hand

To run the plugin inside a local harness profile — including alongside Agent Teams — follow docs/SETUP.md. It covers the build, a scratch profile, the cordis patch, and a small task that makes the ceilings and the review gate visibly fire, plus the failure modes people actually hit.

pnpm build                                       # the harness loads built JS, not .ts
dsh plugin --profile <name> add -w file:$PWD     # `-w` is required for a profile
dsh --profile <name> --dump-config | grep -A8 feature-loop   # verify composition

The profile must supply the plugin's runtime peers. The built plugin imports @deepseek-ai/dsh-llm and @deepseek-ai/dsh-typert-protocol, both are optional peerDependencies, and this repo's .npmrc sets auto-install-peers: false — so a profile that pulls in nothing else from the harness installs the plugin, composes it, shows it in the boot graph, and then does nothing at all: no gate, no review, no approval. Depend on a harness bundle (Agent Teams' profile bundle is the usual one) and the peers resolve. The one-second check is pnpm's virtual-store key — a _@deepseek-ai+c_… suffix means resolved, no suffix means the plugin is inert: docs/KNOWN-ISSUES.md §4, and docs/SETUP.md §Step 2. make install performs this check for you and refuses to finish on an inert install.

Once it is running, a gated step prompts you in the browser: the composer shows the reason with Reject / Allow once, and your answer decides whether the tool runs. This has been observed end to end — a real browser rendered this plugin's own REVIEW REQUESTED reason, Allow once wrote the file, and Reject on the same prompt wrote nothing: docs/VERIFY-E2E-APPROVAL.md. You are not writing that UI — it ships with DSH as @deepseek-ai/dsh-client-ui-approval; the plugin's job is to emit ask so it gets reached.

  • docs/SETUP.md — install and drive it end to end.
  • docs/RUNBOOK-SERVER.md — the verified live server: an acceptance checklist, the boot command, the URL, and the permission-preset trap that silently disables the approval prompt.
  • docs/PRD.md — the audit behind the current design.
  • docs/VERIFY-APPROVAL.md — independent verification of all five approval outcomes, with file:line evidence.
  • docs/VERIFY-INTEGRATION.md — the same five outcomes executed in a real DSH context (5/5 pass), plus the exact string the approval panel renders.
  • docs/VERIFY-DASHBOARD.md — the approval dashboard: 296 unit + 11 integration green, a live HTTP transcript (page 200, token 401, approve → allowed-once, 409, 403), the browser click verified via make e2e-dashboard, and the model-authored review brief.
  • docs/VERIFY-E2E-APPROVAL.md — a real browser on the containerised deployment: the panel appears with this plugin's reason, Allow once writes the file, Reject blocks it.
  • docker/README.md — the one-command container run.

The book mapping

Book conceptModuleWhere
8-dimension loop specspec.tsLoopSpec — all 8 required, validated at load
"Name the termination"spec.tsTerminationSpec.successCommand
"Budget the loop, not the request"budget.tsLoopBudget.verdict()
"Route models by step type"routing.tsModelLadder.forStep()
"Ask the human <10% of the time"review.tsAttentionRouter.route()
Approval gate, tiered by reversibilityreview.tsReviewGate.check()
Tool-cycle detector (threshold 3)signals.tstrailingRepeat()
Error-cascade detector (3 consecutive)signals.tsdetectSignals()
Budget warning (0.8)budget.tswarnAt default
Progressive autonomyreview.tsDEFAULT_GATE_POLICIES
BUG_FIX_PROMPT (6 rules)prompts.tsBUG_FIX_RULES
FEATURE_PROMPT (5 rules)prompts.tsFEATURE_RULES
"Separate success from stopping"runner.tssuccess check after every step
Plan-and-Execute above ReActpipeline.tsphases plan; the harness's ReAct loop runs inside each
"Model every agent as a state machine"pipeline.tsVALID_TRANSITIONS; an illegal move throws
"3 attempts, then report"pipeline.ts, phases.tsthe 4th TEST → IMPLEMENT is an invalid transition

Every threshold is quoted from the playbook in BOOK_THRESHOLDS, because a threshold with no provenance is a number someone liked.


Two paths, one policy layer

The policies are shared. Only transport and session state differ.

Standalone runner (src/runner.ts, src/cli.ts)DSH plugin (src/plugin.ts)
Transportsrc/llm.ts → onegwharness llm service
Budget ceilings✅ wired✅ wired (agent/pre-step, reject)
Cheap-first ladder✅ wired✅ wired (agent/request)
Metering✅ wired⚠️ see "Known limits"
Signals✅ wired✅ wired (agent/pre-step)
Review gate✅ wired (blocks)✅ wired (tools/pre-execute, asks)
Human approval in the browser➖ console prompt✅ Web UI composer prompt
HITL approval dashboard➖✅ ops console: workspace tree, activity, grouped runs, Allow/Reject + feedback (dashboard: config)
Judge✅ wired✅ wired (agent/pre-step, awaited)
Operator review✅ wired (blocks)✅ prompts, then blocks

The gate is decided before dispatch, and a human answers it in the browser. This used to be the one honest difference between the two paths, and it was a defect: the old fork consulted the gate from its own copy of executeToolCalls, so a gate-raised review arrived one step late — after the tool had already run. The fork's README filed blocking approval as an unfinished refinement.

Hosting on the harness closed it. tools/pre-execute is a first-class pre-dispatch hook returning PreToolDecision (allow / deny / ask):

ctx.on('tools/pre-execute', async ({ agent, name }, next) => {
  const gate = gateForTool(policyFor(agent), name)
  if (gate.kind === 'proceed') return next()
  return { kind: 'ask', reason: gate.reason }   // answered, not dropped
})

ask routes to the deployment's approval channel — in the Web UI, the conversation composer prompt from @deepseek-ai/dsh-client-ui-approval. You approve and the tool dispatches; you reject and the model is told a human said no. The plugin's job ends at emitting the decision; the harness (serviceAsk in @deepseek-ai/dsh-tools) maps the outcome to allow or deny.

A blocked call is answered, not dropped: the assistant's tool-call block must receive a result or session replay is invalidated. deny materializes a tool error the model can read and react to.

gateMode — how the human is asked

ModeBehaviourUse it for
ask (default)Prompts the approval channel. Fails closed to a refusal when none is mounted, or the outcome is unavailable.Interactive Web UI sessions
denyRefuses outright, never prompts.Unattended runs and CI

ask is the default because it degrades to exactly deny when there is no approval channel — it is strictly more capable without being less safe. Set deny when no human is watching.

- id: feature-loop
  config:
    gateMode: ask          # or: deny (CI / unattended)

Allow for this run — fewer clicks, same gate

A run that writes a dozen files would otherwise ask a dozen times. On an ask for write or edit that came from a gatePolicies rule, the Feature Loop page offers a third button, Allow for this run, between Allow once and Reject. One click releases that ask, any other pending ask for the same tool in the same run, and the later asks for it.

It is deliberately narrower than "always allow":

LimitWhy
One run. Keyed by the agent id; a new session starts with none.A decision about this task is not a decision about the next one.
One tool. Granting write says nothing about edit.Each tool class is its own risk.
Memory only. Never written to the config; a restart drops it.A click must not widen the gate behind the deployment's back.
Policy asks only. A critical-signal ask, or any ask without the REVIEW REQUESTED (policy) marker, still reaches a human.The grant waives a rule you configured, not a safety signal.
Never bash. The button is not offered, and a forged request is refused (400).One approved command says nothing about the next. For no prompts on shell, use gateMode: auto, where the envelope is enforced.

Each use is a line in the feed (allowed for this run: write (no prompt)), so the log still shows every call the grant covered.

Approvals that were a toll, not a gate

A live run in the web UI asked for 24 clicks in its first three minutes of research — pwd && ls -la, every todo_write, every web_fetch. Under gateMode: ask these no longer ask, and everything that could do damage still does:

CallUnder askWhy
todo_write, web_search, web_fetchrunsBuilt-in class read. The spec's actuator still overrides it; any other unlisted tool stays irreversible.
bash that can only read (pwd, ls, cat, grep, go vet, go doc, git status/log/diff, …)runsEvery segment of the command must be provably read-only (src/shell-readonly.ts).
bash with a redirect, $(…), `, a variable, ~, .., an absolute path outside the run's directory, a secret file, find -exec, sort -o, git -c, …asksNot provable, so left to a human.
The configured pipeline.testCommand (each && piece in full, optionally piped to a reader)runsThe operator chose it. An extended variant is a different command and asks.
bash anything else, write, edit, subagentasksThe card now shows the call: Call: bash … / Call: write docs/PRD.md.

Under gateMode: deny a read-only shell is not waved through — deny means no unsupervised shell at all.

A supervised run also advances now. The pipeline's sandbox descriptor was only attached under YOLO, so with gateMode: ask every phase gate and the "you stopped early" nudge returned at "no sandbox": the model wrote its research note, said "phase 1 complete", and the run sat in research forever with nothing in the log. Ship likewise records the files a supervised run asked to write, so a run that passes every gate no longer commits nothing.

The approval dashboard

Besides the composer prompt, the plugin can host its own loopback web page (src/dashboard.ts + src/dashboard-page.ts, Node builtins only; the UI is a vendored React bundle built from web/app.tsx with assistant-ui): pending approval cards carrying the gate's REVIEW REQUESTED reason with Allow once / Reject, the live run state (step vs ceiling, spend, ladder route, judge score, signals) over SSE, and an activity feed of gate decisions and approval outcomes. A per-response nonce CSP (default-src 'none', frame-ancestors 'none') states the posture: the page runs its own script and style and nothing else.

- id: feature-loop
  config:
    dashboard:
      enabled: true            # start the server; omitted = composer only
      # host: 127.0.0.1        # 127.0.0.1 | 0.0.0.0 — the closed set
      # port: 8100             # Docker: 0.0.0.0:8100 in-container,
      #                        # published as 127.0.0.1:3092 (loopback only)
      # answers: true          # false = observe only, composer keeps answering
      # answerTimeoutMs: 600000   # a pending ask fails closed after this
      # brief:                 # model-authored review brief, off by default
      #   enabled: false
      #   model: xiaomi/mimo-v2.5   # required when enabled
      #   maxTokens: 1024
      #   timeoutMs: 15000

make up prints the URL + token (make dashboard reprints it): the token is generated per boot and required on every request. An existing Docker volume seeded before this feature needs FORCE_REINIT=1 make up to pick the row up.

The optimize block — refinement passes from measured history

optimize: sits beside the spec and changes nothing when omitted. When present it is validated at load (a loops: 30 stops the plugin from loading, naming the field and the 3–10 band):

- id: feature-loop
  config:
    optimize:
      loops: 3              # refinement passes, integer 3–10 (CLI `runRefined` only)
      derive: true          # accepted; the CLI's `--derive` derives envelopes, the plugin records the history they come from
      history: .feature-loop/runs.jsonl   # run-history file: the plugin appends one line per closed turn and feeds Metrics from it
      # judge: chat         # none | chat | laya — who scores across passes (CLI only)
      # totalBudgetUSD: 3.00  # refinement budget; default derived × loops × 0.6 (CLI only)

Two halves, split where they belong:

  • The CLI derives and advises. Every CLI run appends one line to the history file (JSONL, Node builtins only). With --derive, the next run's envelope (maxSteps, costBudgetUSD) is the P95 of this goal's recorded runs plus 30% headroom — an explicit --max-steps/--budget is a pin and is never overridden, and the configured budget is a cap the derivation may tighten but never raise. The run then prints its Metrics roll-up and the judge's proposals. With too little history (under five usable runs) nothing is applied — a floor is not a measurement. Every number's provenance is printed.
  • The plugin records and rolls up. On session/event turn/end — the exactly-once run seam, which a pre-step reject also closes through — the plugin appends one record built from metered numbers only (budget snapshot for steps/cost/unpriced steps, latencyKind: 'round-trip'), then refreshes the dashboard's Metrics panel from the file. The judge battery is deliberately NOT asked on this path: one hot-path event must stay cheap, so Optimizations proposals stay a CLI affair until a cheaper cadence exists. Records are written whether or not the dashboard page is up, so a headless deployment with history: still learns.

Proposals are display only — applying one means copying its snippet by hand; there is deliberately no endpoint that lets the judge loosen its own ceilings. loops/totalBudgetUSD are validated at load but consumed only by the CLI's runRefined: iteration belongs to the caller, not to a step waterfall, so a plugin config that granted passes from inside a hook would be a timeout wearing a feature hat.

The guard is the safety property. The dashboard's answerer is registered ahead of every other approval/request listener — required, because the harness's remote forwarder holds the request without calling next() while a Web UI tab is attached — but it claims a request only while a dashboard tab is actually connected. No tab → it delegates, and the composer answers exactly as before this feature existed. Every failure path (last tab closed, ask withdrawn, timeout, shutdown) settles the pending ask unavailable, which the harness maps to a refusal. Verified at every level including the browser click: docs/VERIFY-DASHBOARD.md.

The UI is assistant-ui; the decision is still ours. The dashboard renders through assistant-ui: pending asks become tool-call message parts carrying an approval gate, and onRespondToToolApproval is the single callback that turns a click into a request. That callback posts to this plugin's own guarded endpoint (POST /api/approvals/:id) — the same endpoint, token, and fail-closed behaviour the composer path uses. assistant-ui decides how a decision is asked for; it never decides whether one is valid.

The mapping lives in src/approval-bridge.ts, which imports nothing at all so it can be tested in CI's no-install job. It is deliberately narrow: only allow-once and reject-once are offered, never allow-always/reject-always, because a browser click must not widen the deployment's gate — that is what gatePolicies and actuator are for. An unknown option throws rather than resolving to an authorisation.

The review brief is purely advisory. With brief.enabled, each claimed ask also kicks off a model call that authors plain prose (src/brief.ts), shown inside the card. There is no model-authored component language any more — with OpenUI the model emitted component calls and a parser-plus-allowlist made that survivable; assistant-ui has no such language, so what remains is the part that was always load-bearing: bounds. A brief is capped by node count, list length, per-string length, and total size, and one that exceeds any cap is rejected whole rather than truncated — a half-rendered brief reads as a complete one, which is the failure that matters when the text is what a human decides on. The brief never blocks, delays, or settles the ask: it resolves to a card, to "brief unavailable", or to nothing at all.

The vendored bundle is auditable and offline. web/build.mjs emits assets/assistant-ui/dashboard.js (~470 KB; ~142 KB gzipped) from web/app.tsx, and refuses to build if the output carries assistant-cloud, a telemetry reporter, or a phone-home — @assistant-ui/react depends on assistant-cloud, which ships engagement/run reporters and tree-shakes out today; the build turns "today" into a checked invariant. The page loads the bundle from its own origin under a default-src 'none' CSP with a per-response nonce, and the bundle contains exactly one URL (https://react.dev).


Architecture

src/
  ── the product: pure policy, no harness import ──
  spec.ts        the book's 8 dimensions, validated at load
  budget.ts      step/cost ceilings, USD price table, unpriced-step tracking
  routing.ts     cheap-first ladder, escalation on evidence
  signals.ts     6 deterministic detectors — cycle, cascade, dominance, budget, steps, quality
  review.ts      reversibility gate + attention router (<10% budget)
  agent-policy.ts the plugin agent's decisions, extracted so they are testable
  judge.ts       chat judge (works anywhere)
  laya.ts        Laya judge via System One / Jev (local sidecar :8092 by default)
  questioner.ts  LLM→Laya→LLM: actor uncertainty → typed questions → Laya decides
  messages.ts    notice text; imports nothing, which keeps the test suite runnable
  prompts.ts     BUG_FIX_PROMPT / FEATURE_PROMPT / REFACTOR_PROMPT

  ── the harness host ──
  plugin.ts      agent/pre-step · agent/request · tools/pre-execute
  dashboard.ts   the loopback HITL server (brief state + nonce CSP)
  dashboard-page.ts the page as a string (DOM only, no innerHTML)
  brief.ts       the brief's bounds, prompt, and normalizer (no UI language)
  approval-bridge.ts the dashboard <-> assistant-ui approval mapping (pure)
  explainer.ts   the brief's model call (NO_EXPLAINER by default)

  ── the standalone proof: the same policies, no harness ──
  runner.ts      spec → budget → route → judge → review → model → tools
  llm.ts         OpenAI-compatible client + scripted client for tests
  tools.ts       sandboxed read/write/edit/list/run_tests, path-confined
  cli.ts         the demo entry point
demo/
  src/latency-window.ts   the planted bug (nearest-rank off-by-one)
  test/                  14 tests, 3 of which fail on the bug
  README.md              the bug report the loop is given
  verify.sh              exit 0 = goal met
  reset.sh               re-plant the bug
  run.sh                 reset + run, one command
  TRANSCRIPT.txt         a captured successful run

The judge

The router's question — "does this step deserve a human's eyes?" — is a classification, so it has three implementations behind one interface:

JudgeCostLatencyStatus
OnegwJudge (Laya)$0, local~1–4 s cold-ish, <200 ms warm*client ready; verified live against the native Laya service (:8092, all three primitives answer, full battery returns recommendations)
ChatJudgemetered~10–40 swhat the demo uses
NO_JUDGE$00detectors-only, a supported mode

* Laya-sidecar timings measured on this machine: first predict ~7 s (cold weights), then ~0.9–4 s per call warm — far above the JEV doc's 73 ms (that figure is raw forward-pass; ours includes HTTP + routing + a cold-ish process). Still 10× cheaper in wall-clock than a chat judge, and $0.

To point the demo at local Laya (the native sidecar on :8092, same System One / Jev / TypeSafe wire):

bash demo/run.sh --judge laya --judge-base-url http://127.0.0.1:8092
# or: SYSTEMONE_BASE_URL=http://127.0.0.1:8092 bash demo/run.sh --judge laya

--judge laya defaults the judge base URL to http://127.0.0.1:8092 (override with --judge-base-url / SYSTEMONE_BASE_URL). The actor still talks to onegw; only the judge URL splits. Score criteria go as ordered arrays so Laya keeps the human labels; noul answers map onto probability.

The demo uses ChatJudge with xiaomi/mimo-v2.5 because no systemone provider is configured in ~/.onegw/onegw.toml. Two measured facts argue for Laya beyond cost:

  1. A reasoning model is a poor judge. mimo-v2.5 always thinks; at max_tokens: 256 it returns finish_reason: "length" with empty content, and it needs ~859 thinking tokens before emitting one digit. The default is now 2048, and the error message names the cause instead of saying "no digit".
  2. It is miscalibrated. Asked about a routine read_file with no detector fired, it answered SCORE=3 — the top of the scale. A purpose-built decision engine is the right tool; a general chat model is a fallback. Live Laya scored the same shape 1.409 vs 1.465 for routine-vs-dangerous — directionally right but near-chance, matching the JEV doc's warning that base checkpoints need specialisation before their levels drive policy.

Fork delta — removed

This package used to vendor nine files from @deepseek-ai/dsh-agent-loop and mark every customisation FORK-DELTA (35 markers), pinned via upstream.lock to c291e796 (v0.1.5-rc.2) and reconciled by scripts/sync-upstream.sh.

All of that is gone. The harness publishes an extension point for every policy this package adds, so the vendored loop, the sync script, the lock and cordis.patch.yml were deleted and replaced by src/plugin.ts.

RemovedLinesUpstream diff
src/agent.ts968364 changed lines
src/index.ts1069222 changed lines
src/tool-calls.ts32846 changed lines
src/inbox.ts2477 changed lines
src/runtime-context.ts1590 — byte-identical
src/assistant-stream.ts1400 — byte-identical
src/constants.ts60 — byte-identical
src/invariant.ts650 — byte-identical, imported by nothing
src/notices.ts82wrapper for the vendored inbox.ts
scripts/sync-upstream.sh, upstream.lock, cordis.patch.yml140fork machinery

≈3,064 lines out, one 368-line plugin in. The full audit is in docs/PRD.md.

Two genuine bugs were found in the fork while it existed, both now moot:

  • import { FiberState } from '@deepseek-ai/cordis' was a hard SyntaxError — cordis declares it as an ambient const enum with no runtime export. It only works upstream because the bundler inlines the values. (Inherited from upstream.)
  • this.policy read inside a function* generator was undefined at runtime, so the fork's fifth constructor argument never reached the agent.

Known limits

The 0→1 pipeline

  • Phase progression is not yet driven end to end. The state machine, the budgets, the gate, the envelope, the sandbox and the ship phase all exist and are tested, but a run still advances through research until something calls transition(). The page shows the rail; it does not yet walk it. Until the step hook evaluates each phase's gate and advances, a run is a well-bounded research phase. This is the largest open item in the change.
  • The gate evaluators are pure, but nothing fills a PhaseObservation yet. evaluateGate reads an already-observed snapshot; the code that reads docs/0-research.md, asks git what changed, and runs your test command is not written. The gates fail closed, so today no phase can pass — which is the safe direction, but it means the pipeline does not complete.
  • ship() is not yet called by the plugin. It is tested against its exact argv and is ready; nothing invokes it at the end of a ship phase.
  • createSandbox is not yet called by the plugin. Same: tested, ready, and CreatePolicyOptions.worktree accepts the result, but the caller that creates one does not exist yet. Until it does, gateMode: auto denies every write, because containment with nothing to contain against is not containment.
  • No end-to-end run has been performed. Every claim in this section is from unit tests and a typecheck. There is no docs/evidence/ pack for a real five-phase run yet, and that pack is the next thing to produce.
  • commandWords is not a shell parser. It tokenises with quotes honoured, which answers "which subcommand is this" and nothing more. cat x | sh is four tokens to it, not three. Anything it cannot read is denied rather than allowed, so the limit is in the safe direction — but a determined bypass through shell syntax is not what this defends against.
  • The test phase's ceiling is the book's 3 attempts, enforced by control flow. pipeline.ts refuses the fourth transition, so a loop that ignores the prompt cannot reach it. What the loop does with those three attempts is still prompt text.

Everything else

  • src/plugin.ts is not typechecked in CI. It imports @deepseek-ai/dsh-* at versions CI cannot resolve (there is no lockfile, and @deepseek-ai/cordis@0.4.0 has no published version). It is typechecked locally against the prebuilt packages.

  • test/plugin-approval.test.ts is not run in CI. It imports src/plugin.ts, whose @deepseek-ai/dsh-* imports the no-install test job cannot resolve. Like src/plugin.ts it is run locally against the prebuilt packages; the same handshake is covered by the integration suite (test/integration/plugin-in-dsh.spec.ts), which is where the plugin's behaviour is exercised against the real harness.

  • Spend is metered on both paths. runner.ts prices every model result into LoopBudget, and the plugin drains settled assistant/message events into spend() from both hooks (cursor-deduped, assistant/attempt retries still unpriced — marked ponytail: at the call site). The cost ceiling is load-bearing: a run with a deliberately tiny costBudgetUSD stops on cost.

  • The approval prompt depends on the session's permission preset, and the user's settings win. A fresh session's approval policy comes from permission.defaultPreset in ~/.dsh/settings.yaml, which permission-presets applies after any config default. On this machine it is danger-full-access, whose preset is approval: never — so an ask is refused with Error: the user rejected tool "X" before any UI is consulted, and no panel can appear. Three fixes: switch the preset in the session's UI selector, set that key in settings.yaml (global, and relaxes the sandbox), or run a server under a private DSH_HOME whose settings pin workspace-write — the last changes nothing globally. Details in docs/RUNBOOK-SERVER.md §2.5-2.6.

    This is proven, not assumed: the same headless task returns rejected under the default home and unavailable ("no approval channel is available") under a workspace-write home. The second is the gate's ask ; the first is refusing before any UI is consulted.


Phase status

  • Phase 0 — policy layer ✅ tests, pure, no harness dependency
  • Phase 1 — loop integration ✅ runner + CLI + tools + demo, working end to end
  • Phase 1b — plugin compiles ✅ tsc --noEmit clean against prebuilt @deepseek-ai/dsh-*
  • Phase 2 — plugin review gate ✅ all six detectors, the judge, the router and the gate are read by src/plugin.ts; the gate asks before dispatch.
  • Phase 1c — de-fork ✅ the vendored loop is gone; the policies are hosted on agent/pre-step / agent/request / tools/pre-execute. See docs/PRD.md.
  • Phase 1d — human approval in the Web UI ✅ the gate emits ask, DSH routes it to @deepseek-ai/dsh-client-ui-approval, and a human approves or rejects in the composer. gateMode: ask | deny. Verified live on a scratch profile with Agent Teams.
  • Phase 1e — HITL approval dashboard ✅ optional loopback web surface (src/dashboard.ts + src/dashboard-page.ts): pending cards, live run state over SSE, Allow/Reject over HTTP — guarded so a tab-less deployment behaves byte-identically to the composer-only path. 296 unit + 11 integration tests; the browser click verified via make e2e-dashboard (docs/VERIFY-DASHBOARD.md).
  • Phase 1f — review briefs ✅ model-authored brief per ask (src/brief.ts + src/explainer.ts): plain prose, bounded, rendered inside the approval card — purely advisory, never blocking. Disabled by default (dashboard.brief.enabled).
  • Phase 1g — assistant-ui dashboard ✅ the UI runs on assistant-ui (web/app.tsx, vendored as assets/assistant-ui/); approvals ride ToolCallMessagePart.approval and onRespondToToolApproval, mapped by the pure src/approval-bridge.ts onto the existing guarded endpoint.
  • Phase 2b — real spend accounting ✅ runner.ts prices every model result; the plugin drains settled attempts from both hooks. The cost ceiling stops runs; asserts spend is non-zero after a priced attempt.

Verifying the whole thing

node --experimental-strip-types --test test/*.test.ts   # 296 pass
pnpm test:integration                                   # 9 pass, in the real harness
tsc --noEmit                                            # clean
bash demo/run.sh                                        # goal-met
grep -rn "FORK-DELTA" src/ | wc -l                      # 0 — the fork is gone

pnpm test:integration is the one that matters for human approval: it mounts this plugin into a real cordis context — real tool runtime, real approval service, real session — and drives the actual dispatch path, asserting that approving runs the write and rejecting stops it. See docs/VERIFY-INTEGRATION.md.

Four gaps closed along the way

  • The gate was one step late. The fork consulted its gate from its own copy of executeToolCalls, so a gate-raised review arrived after the tool had already run. Hosting the gate on tools/pre-execute decides the call before dispatch — the fix the fork could not reach without forking.
  • error-cascade could not fire in the plugin path. StepObservation.error was never populated: per-call isError was internal to tool-calls.ts and thrown away at the executeToolCalls boundary. Since error-cascade is one of only two critical signals, the plugin gate was silently running on four detectors instead of six. plugin.ts records the outcome from the tool boundary, so the step's observation learns the result.
  • tool-dominance fired on step 1. At one or two steps every tool is trivially 100% of all steps, so the detector fired on the first step of every run — a signal that always fires is noise that trains its reader to ignore the real ones. It now needs a five-step floor before it is allowed to speak.
  • The gate failed open for an agent-less call. policyFor returned undefined for agent === undefined, so the handler delegated and the call dispatched ungated. An agent-less call now gets a shared policy and is gated like any other; the refusal happens downstream, where the harness denies an agent-less ask. Caught by independent verification, not by CI.

Contributing

See CONTRIBUTING.md. Please open issues with the templates under .github/ISSUE_TEMPLATE. Security reports: SECURITY.md.

License

MIT © Linh Doan / FreePeak

Phase token budget (30/50/20 + buffer)
phases.ts PIPELINE_BUDGET
10/10/35/15/20 + a 10% reserved buffer
"Check budget BEFORE each call, at 90%"phase-budget.ts preCallGuardreserves the buffer for the terminal report
Timeout guard (independent of steps)phase-budget.tsper-phase and per-run wall-clock ceilings
"File operations happen in a restricted directory"sandbox.tsa throwaway git worktree; refuses without a repo
"Never swallow errors silently"ship.ts3 legal dispositions; no failure path yields a prUrl
Uncited = unverified (15–25% → <3%)phases.ts RESEARCH_PHASEthe research gate requires a citation
"Every write must be idempotency-checked"yolo.tsENVELOPE — irreversible ops are denied, not asked
reaching the approval seam
never
  • gatePolicies / actuator keys must be the harness's real tool names (read, write, edit, bash, glob, grep). The read_file / edit_file / run_tests names belong to this repo's standalone runner and match nothing in a DSH session. A non-matching name is not an error — the tool falls through to the unclassified irreversible default, so the gate still fires but with always-approve rather than the policy you configured.

  • An agent-less tool call cannot prompt. It is still gated (it no longer bypasses the gate), but with no agent there is no session to audit to and no UI to reach, so the harness resolves the ask to a refusal.

  • The plugin path needs a build. It imports @deepseek-ai/dsh-*; the standalone runner does not.

  • Review rate is 20% in the demo, not the book's <10%. Critical signals are deliberately not rate-limited — safety is not subject to an attention budget — so a run with an error cascade will exceed the target. The budget governs judge-driven reviews only.

  • run_tests timeouts kill the direct child, not grandchildren (marked ponytail: in tools.ts; upgrade path is detached spawn + kill(-pid)).

  • The price table is an estimate. mimo-v2.5 runs on a subscription plan, so marginal cost is near zero; the rates in cli.ts are illustrative and exist so the ceiling has something to measure against.

  • The dashboard's rendered page click is verified via make e2e-dashboard (headless Chromium clicks Allow/Reject against the real page and asserts the ask settles). docs/VERIFY-DASHBOARD.md records the transcript. Related: spend shown on the dashboard inherits the metering gap above, and a Docker volume seeded before the dashboard existed needs FORCE_REINIT=1 to re-seed its profile.

  • test/budget.test.ts
  • Phase 2c — Laya-guided optimization ✅ optimize: block, run history, derived envelopes, metrics, Laya-as-advisor proposals (display only), refinement passes with the book's < 0.05 stop rule, and SSE metrics + recommendations on the dashboard snapshot. Composition is wired both ways: the CLI derives envelopes and prints metrics + proposals (--derive), the plugin records one history line per closed turn and feeds Metrics from the file (optimize.history); proposals stay CLI-only so the hot path stays cheap.
  • Phase 3 — Laya ✅ verified live 2026-09-23 (all three primitives + full battery via scripts/laya-sidecar.py); remaining: a --judge-base-url flag so the CLI can point at the sidecar without onegw, and calibration before levels drive policy (routine-vs-dangerous discriminated by only +0.056)