Skip to content

feat(deepseek): add DeepSeek Harness (dsh) as a ninth CLI run mode - #337

Merged
Ark0N merged 6 commits into
masterfrom
feat/deepseek-harness
Aug 25, 2026
Merged

feat(deepseek): add DeepSeek Harness (dsh) as a ninth CLI run mode#337
Ark0N merged 6 commits into
masterfrom
feat/deepseek-harness

Conversation

@Ark0N

Copy link
Copy Markdown
Owner

What this adds

DeepSeek Harness (dsh) as a ninth Codeman run mode, alongside Claude Code, shell, OpenCode, Codex, Gemini, Antigravity, Pi and Grok. Plus a shortcut that opens the harness's own browser UI as a Codeman web tab.

DeepSeek Harness is DeepSeek's plugin-native agent framework, open-sourced 2026-08-13 under MIT. Everything in it (tools, skills, sandboxes, whole apps) is a plugin composed into a profile.

Why the wiring looks unlike the other eight CLIs

Three facts about the harness drove the design. None of them holds for any previous mode, so this is worth reading before reviewing the diff.

1. The agent is a PROFILE, not the binary

dsh is a launcher: dsh --profile <name> boots an ordered stack of plugin-bundle patch layers from $DSH_HOME/profiles/<name>. DeepSeek ships exactly three bundles and none of them is a terminal agent: web (browser UI), headless (one-shot, exits) and base (no app at all). The interactive terminal front door is always a third-party plugin the user installs.

So "is dsh installed" and "can Codeman start a session" are different questions, and the code answers both:

  • isDeepSeekAvailable(), the binary resolved and proved its identity
  • isDeepSeekRunnable(), the above AND a profile that can occupy a pane

The Run button gates on runnable. Gating on the binary alone would spawn a pane that dies on arrival, which is this mode's most confusing possible failure. When the binary is present but no usable profile is, the run menu shows "DeepSeek, add a terminal profile..." instead of a greyed-out entry, because there is a 30-second fix and the user should be offered it.

resolveDefaultDeepSeekProfile() prefers a recognised TUI, then an unrecognised profile, and refuses web/headless. Treating unknown as launchable is deliberate: anyone can publish an app bundle, so a classifier that has not heard of one must not hide it.

2. The permission switch is an env var, not a flag

The harness has no --dangerously-skip-permissions equivalent. Its sandbox and approval rows are configuration, driven by one input, DSH_PERMISSION_MODE, with three presets (read from dsh --dump-default-config):

DSH_PERMISSION_MODEsandboxapprovals
read-onlyread-onlyask
workspace-writeworkspace-writeask (the harness's own default)
danger-full-accessdanger-full-accessnever

It is exported via tmux setenv, never on the spawn line, and a test pins that nothing permission-shaped ever reaches the command line.

This is the one legitimate exception to the repo's ban on carrying agent settings as env vars. CLAUDE_CODE_EFFORT_LEVEL is banned because it hard-locks in-session switching; the harness reads this one with ?? as a boot-time default, so it stays soft and the user can still change mode inside the session.

Because omitting it leaves the harness on workspace-write (which still asks), the multi-user clamp only needs the "only if sent" branch, like codex/antigravity/grok rather than pi. It clamps down to workspace-write, notread-only: the clamp exists to remove privilege, not to break a session's ability to edit its own workspace.

3. It is the first non-Claude mode with REAL idle detection

Every external CLI mode so far is readiness-guessed: Codeman watches the PTY go quiet and infers a turn ended. Claude is the exception because Claude Code fires hooks.

DeepSeek is the second exception. The community terminal front door already reports its own lifecycle to a supervising process through a fully generic, env-var-gated contract (inherited from Herdr). With HERDR_ENV=1, HERDR_BIN_PATH and HERDR_PANE_ID set it runs, on every state change:

"$HERDR_BIN_PATH" pane report-agent "$HERDR_PANE_ID" \
--source custom:dsh-tui --agent dsh-tui \
--state idle|working|blocked [--message ...] --seq N

and treats exit 0 as delivered. So deepseek-status-shim.ts generates a small script into the data dir, points HERDR_BIN_PATH at it, and forwards each report to POST /api/hook-event:

Harness stateHook eventEffect
blockedpermission_promptred "needs you" alert + answerable Approvals Inbox item
idlestopdefinitive end of turn: respawn triggers, wait returns
workingagent_workingclears an alert answered in the terminal, immediately

This makes deepseek the only non-Claude mode passing hooksAvailableForMode(). It earned that by emitting definitive signals rather than having them inferred.

It is an interface implementation, not an impersonation: no real herdr binary is ever executed, HERDR_BIN_PATH is our own script in our own data dir. A terminal profile that does not implement the contract simply never calls the shim and falls back to output stabilisation, so the feature is inert rather than harmful there. Per-session opt-out via deepSeekConfig.statusReporting: false.

agent_working is a new SSE event (the 157th) and the only hook event with no Claude Code hook behind it. It exists because a harness turn cannot run while one of its own modal approvals is on screen, so "the agent started working" proves the dialog was answered in the terminal. It joins APPROVAL_RESOLVING_EVENTS. Without it a dsh red alert would survive until the next stop, which is exactly the stuck-alert bug the Claude path already had to fix once, and the pane-capture staleness sweep that fixed it there is Claude-dialog-shaped and cannot help here.

The resolver needs the strictest identity probe of the family

dsh is not merely a squattable npm name like pi or grok. Debian and Ubuntu ship an unrelated dsh ("dancer's shell", apt install dsh), which would answer a version probe convincingly and then be handed a spawn line. So every candidate must first print the harness's own help banner (DeepSeek Harness) before a version is even read. test/deepseek-cli-resolver.test.ts pins both the rejection and the VITEST hermeticity gate with a real executable fixture.

Other decisions worth noting

  • Model is not a session field. It is a composition entry (agent-default-model) in the profile's config tree, set in ~/.dsh/settings.yaml plus cordis.patch.yml. Both create paths deliberately resolve no model for this mode rather than inventing a flag.
  • Env allowlist gains DSH_* and DEEPSEEK_* only. A dsh settings.yaml can nominate any env var as a provider credential via apiKeyEnv, and the allowlist is one global list applied with no mode context, so admitting those would widen it for every mode at once. This is pi's 34-provider-key problem in a new shape, and gets the same answer.
  • POST /api/deepseek/install-profile is the only endpoint in Codeman that installs third-party code, so it is fenced: the specifier is regex-confined to an npm name at the schema boundary (no path, URL, git spec or leading dash), the spawn is an argv array with no shell, and multi-user mode requires the can-bypass-permissions grant (the same bar as a shell session, which can already do strictly more). The default package is a default, not a hardcoding.
  • Docker: the image installs dsh and bootstraps a profile into the agent HOME. Profiles are deliberately not seeded from the host (each is a per-profile node_modules tree, host-arch-specific and far too large to copy per container start); only ~/.dsh/.env, settings.yaml and cordis.patch.yml are seeded.
  • The web profile gets a shortcut, not a run mode.Run > DeepSeek web UI... starts dsh web in an ordinary shell session and opens the URL as an ordinary web tab, so nothing new supervises a long-lived HTTP server. --trusted-host is load-bearing: dsh fences its /api behind a browser-trust check on the request authority, and a web tab reaches it through Codeman's origin via the webview proxy.

Verification

Full CI gate green (313 files, 6124 tests), plus typecheck, lint, frontend-syntax, public-assets and lockfile checks. 31 new unit tests across two files.

Live-verified on an isolated instance (own data dir and tmux socket) against a real dsh 0.1.1-rc.2 and @deepseek-harness-tui/dsh-tui:

  1. GET /api/deepseek/status returned the two-part answer correctly: available: true, runnable: false with only web and headless installed, both classified correctly.
  2. Launching with no usable profile was refused with the exact fix command, no dead pane.
  3. POST /api/deepseek/install-profile produced a genuine profile (29MB, real lockfile), and runnable flipped to true.
  4. A real session's pane ran dsh --profile dsh-tui, with DSH_PERMISSION_MODE=danger-full-access, HERDR_ENV, HERDR_BIN_PATH and HERDR_PANE_ID all confirmed present via tmux show-environment and absent from the command line. The TUI rendered.
  5. The full status bridge: a send-and-wait returned signal: "stop" from a real turn (21.9s, not a timeout), and driving blocked then working through the shim created and then cleared an Approvals Inbox item. Unknown verbs and unmapped states exit 0 without posting, so there is no retry storm.

Not verified (honest gaps, all recorded in the plan doc)

  • The turn in step 5 errored (Request timed out) because the box's configured local provider was down. The signalling chain is proven end to end; a successful model round-trip is not.
  • Local-echo 'buffer' policy against the TUI's real composer. If it turns out per-keystroke reactive like codex's, the fallback is the 'off' branch.
  • A Docker case with mode: 'deepseek' (needs a --no-cache agent-image rebuild).
  • A remote-SSH deepseek case.
  • The web-UI shortcut end to end through the webview proxy, in particular whether --trusted-host <codeman-authority> is right in every deployment shape.

Docs

  • docs/deepseek-integration.md, user guide
  • docs/deepseek-integration-plan.md, every decision with its reasoning, plus the gaps above
  • CLAUDE.md and docs/architecture-invariants.md updated with the rules that prevent mistakes here

Note for the release

No changeset is included: COM writes one covering everything since the last tag, and a merged-but-unconsumed changeset turns the Release workflow red.

Adds `mode: 'deepseek'` alongside claude/shell/opencode/codex/gemini/
antigravity/pi/grok, plus a shortcut that opens the harness's own browser UI
as a Codeman web tab.
DeepSeek is wired unlike its siblings in three ways, each of which is the
reason for a design decision rather than an accident:
1. The agent is a PROFILE, not the binary. `dsh` is a launcher over
$DSH_HOME/profiles/<name>, and DeepSeek ships only `web`, `headless` and
`base` -- the interactive terminal front door is always a third-party
plugin. So availability is two questions: `isDeepSeekAvailable()` (binary)
and `isDeepSeekRunnable()` (binary AND a pane-capable profile). The Run
button gates on the latter, because reporting only the binary would spawn a
pane that dies on arrival. When the binary is present but no profile is,
the run menu offers to install one (POST /api/deepseek/install-profile).
2. The permission switch is an env var, not a flag. The harness has no
command-line permission option; its sandbox/approval rows read
DSH_PERMISSION_MODE (read-only / workspace-write / danger-full-access).
Exported via `tmux setenv`, never on the spawn line. Absent = the harness's
own workspace-write, which still asks, so the multi-user clamp is the
only-if-sent branch and clamps to workspace-write, never read-only.
3. It is the only non-claude mode that passes hooksAvailableForMode(), and it
earned that. The terminal front door reports idle/working/blocked to a
supervising process over a generic env-gated contract; a generated shim
(deepseek-status-shim.ts) makes Codeman that supervisor and forwards each
report to /api/hook-event as stop / agent_working / permission_prompt. So a
dsh session gets definitive respawn triggers, real wait-endpoint signals and
real Approvals Inbox items instead of output-stabilization guesswork.
`agent_working` is new (157th SSE constant) and joins
APPROVAL_RESOLVING_EVENTS so a dialog answered in the terminal clears its
alert at once.
The resolver needs the strictest identity probe of the family: `dsh` is not
merely a squattable npm name, Debian ships an unrelated `dsh` (dancer's shell),
so `dsh --help` must print the harness's own banner before a candidate is
handed a spawn line.
Model is deliberately not a session field -- it is a composition entry in the
profile's config tree. Env allowlist gains DSH_* and DEEPSEEK_* only; provider
keys named by a settings-file `apiKeyEnv` stay out, which is pi's
34-provider-key problem in a new shape.
Verified live against dsh 0.1.1-rc.2 and @deepseek-harness-tui/dsh-tui: the
status endpoint's two-part answer, the no-profile refusal, the profile
bootstrap, a real session whose pane runs `dsh --profile dsh-tui` with the
permission mode injected via setenv, and the full status bridge -- a
send-and-wait returned signal "stop" from a real turn, and blocked/working
created and cleared an Approvals Inbox item.
Docs: docs/deepseek-integration.md (guide), docs/deepseek-integration-plan.md
(decisions + honest gaps). Tests: test/deepseek-mode.test.ts,
test/deepseek-cli-resolver.test.ts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…l, make the hook gate per-session
Three review findings on the DeepSeek Harness mode, plus one the third exposed.
1. The multi-user clamp was bypassable by a sibling field on the same request.
clampExternalCliBypassForOwner() clamps deepSeekConfig.permissionMode, but
DSH_* is an allowlisted envOverrides prefix and applyEnvOverrides() runs AFTER
_configureDeepSeek(), so a non-granted owner sending
envOverrides.DSH_PERMISSION_MODE landed last and won. Measured on an isolated
instance: a session created with permissionMode "read-only" and that override
ran with DSH_PERMISSION_MODE=danger-full-access in its pane.
Every other CLI's bypass is a command-line flag reachable only through the
per-CLI config, which is why the config clamp alone is the whole gate for
them. clampEnvOverridesForOwner() adds the env-var half: for a non-granted
owner it DROPS DSH_PERMISSION_MODE and DSH_HOME (dropping falls through to
what _configureDeepSeek() exports, i.e. the clamped value). DSH_HOME is on
that list because it aims the launcher at a profile tree whose plugin code
runs at boot, before any approval row can apply. Verified end to end in real
multi-user mode: a non-granted user sending both now gets workspace-write and
no DSH_HOME, while an unrelated DSH_TELEMETRY_MODE passes through untouched.
2. POST /api/deepseek/install-profile could hang forever. spawn's own `timeout`
signals only the direct child, and a plugin install fans out into
package-manager children that keep the inherited stdio pipes open, so `close`
never fires and the held-open request leaks with no route-level deadline.
Reproduced: with a 1.5s built-in timeout the promise was still unsettled after
6s and both fan-out children were alive. Now detached: true plus negative-pid
SIGTERM/SIGKILL, the same escalation runGit() uses for the same reason, with a
last-resort reap for a grandchild that escaped the group. Same probe after the
change: close fires, direct child and both grandchildren dead.
3. hooksAvailableForMode() promised more than a dsh session can deliver.
deepSeekConfig.statusReporting: false disarms the HERDR_* export, and that
triple is the only reason a dsh session posts hook events, so `until=stop` was
accepted and then blocked for the caller's whole timeout: the exact
infinite-wait-dressed-as-a-timeout the predicate exists to prevent. It now
takes HookCapabilityOptions and every call site passes sessionHookOptions(),
with the deepseek arm reading `!== false` so a forgotten one degrades to the
old behaviour. The refusal names the setting rather than saying "no Claude
Code hooks", which would send the caller hunting a bug that is really a
setting they chose. Profile conformance stays unknowable at request time and
is documented as such. The stale "True for `claude` and nothing else" docblock
is corrected.
4. Exposed by (3): hooksAvailableForMode() was doing double duty as "is this a
claude session". Read My Mind (POST /api/sessions/:id/readmymind) and intent
capture read Claude's own transcript, and adding deepseek silently widened
both to a mode that has none. They compare mode === 'claude' directly now, and
a static check pins them there.
Verified: full CI gate green (6132 passed), typecheck/lint/format clean, and the
wait-signal gating exercised against a live server with a real dsh 0.1.1-rc.2 --
bridge off plus explicit until=stop is a 400 naming the setting, bridge off with
no `until` still 200s on idle/exit, bridge on accepts stop.
…llback profile classifier
The three smaller review nits, plus the first real test coverage for the status
shim (it had none: it is emitted as a STRING, so tsc never sees it).
1. The shim was written with a plain writeFileSync. The TUI can be exec'ing that
exact path while an upgraded Codeman refreshes it, and a reader catching a
half-written file gets a syntax error, exits non-zero, and is retried four
times per state change for a file that will never parse. Now temp + rename
(atomic within the directory), with the temp chmod'ed before the rename since
writeFileSync's mode only applies on create, and removed if the write throws.
SHIM_VERSION bumped to 2, because SHIM_SOURCE changed and an existing v1 shim
would otherwise keep matching the embedded marker and never be refreshed.
2. The pane-id comment claimed the ambient env "cannot be spoofed by an argument
the agent itself could influence". The agent runs IN that pane and can invoke
the shim with CODEMAN_SESSION_ID unset and any argv it likes. It buys nothing
it did not already have (the hook-secret file is readable from the same pane,
so it can POST /api/hook-event directly), but the comment read like a security
boundary. Rewritten to say what the preference actually buys: correct
attribution when a TUI mangles or re-uses the pane argument. Accidents, not
adversaries.
3. classifyProfile() folded the directory name into the same haystack as the
bundles, but only the TUI arm could match a bare name, so a stock profile
whose package.json has no dsh.profile.bundles (hand-edited, older layout,
mid-install) classified as `unknown` -> launchable -> eligible as the DEFAULT
pick, which is exactly the pane-dies-on-arrival failure the two-part
availability gate exists to prevent. The stock names are now a LAST-resort
fallback consulted after the bundle patterns, so real bundle evidence still
wins over a name the user chose. The loose `tui` arm gained word boundaries:
it decides which profile boots by default, and matching the middle of
`intuition` is not a rule anyone could predict.
New test/deepseek-status-shim.test.ts runs the generated script the way the
harness does -- real node process, real argv, real env, real listener -- and
covers the exit-code contract that makes the retry behaviour safe: mapped states
post and exit 0, an unknown verb or unmapped state exits 0 WITHOUT posting (a
non-zero there would be four HTTP requests per state change forever), a rejecting
server or an unreachable one exits non-zero so the caller retries, the hook secret
is read at execution time, and `node --check` parses the file (a template-literal
typo in SHIM_SOURCE is invisible to tsc).
Trap worth recording, hit while writing it: the tests must spawn the shim
ASYNCHRONOUSLY. The listener lives in the test process, so spawnSync blocks the
event loop that has to accept the connection, the shim waits out its own 1500ms
socket timeout and exits 1, and it reads exactly like a broken shim (measured:
Socket._onTimeout in its --trace-exit output, server logging nothing).
Verified: full gate green (6142 passed, +10), typecheck/lint/format clean.
…and trust its frame
The `Run > DeepSeek web UI...` shortcut failed three ways at once against a real
install, and the three are independent.
1. It hardcoded `--port 3080`. That is dsh web's OWN default, which makes it
precisely the port a DeepSeek user is most likely to be serving on already,
so the launch died with EADDRINUSE against the user's own server. The port
now comes from `GET /api/deepseek/web-port`, which walks 3080..3119 for a
free loopback port by BINDING it (a connect probe cannot tell "free" from
"listening but not answering yet").
2. It opened the tab unconditionally. The crashed server left a saved dashboard
pointing at nothing, with the failure only visible in a shell tab nobody had
a reason to look at. The launch now polls the existing webview probe until
the URL answers, and on timeout reports the error naming the shell tab
instead of persisting a dead dashboard.
3. The saved tab was untrusted, so the frame was sandboxed without
`allow-same-origin` and the dashboard was broken twice over: the dsh
client-runtime reads `localStorage` while loading its plugins and died there
("the document is sandboxed and lacks the 'allow-same-origin' flag"), and an
opaque-origin frame sends `Origin: null`, so dsh's own trust fence 403'd
every `/api` call no matter which authority `--trusted-host` named. Passing
`location.host` only means anything once the frame actually carries that
origin, so `--trusted-host` had never once done its job. The managed tab is
now created `trusted: true`.
That trade is real and deliberate: a trusted proxied frame is same-origin
with Codeman and can reach Codeman's API. It is defensible only because this
dashboard is an agent harness Codeman just started itself, on loopback, which
can already run code as the user. It is not a precedent for trusting
third-party dashboards, which is why it is set at this one call site rather
than defaulted.
Separately, the shortcut listed its own dashboard twice: once as the menu entry
that starts it and once as the row that entry had written on the previous click.
Webviews now carry an optional `managed` marker, managed rows are filtered out
of the saved-dashboard list, and a relaunch repoints the existing row rather
than stacking one dead dashboard per restart (which the per-launch port would
otherwise guarantee). `managed` is declared in the schema because a plain
`z.object` strips undeclared keys, so an undeclared marker would never survive
the round trip.
`DEEPSEEK_WEB_PORT` is gone from constants.js; its doc comment asserted that a
hand-started `dsh web` and the shortcut "land on the same place and share one
saved tab", which is the bug stated as a feature.
Verified on a real install with the user's own `dsh web` holding 3080: the
shortcut takes 3081, the server answers, exactly one DeepSeek entry shows in the
run menu, and the proxied dashboard renders its workspaces and completes its own
API calls (the previously-403'd `api/settings.describe` now succeeds). Full gate
green (6142 passed), typecheck/lint/format/public-assets clean.
…l tab
Clicking "DeepSeek web UI..." opened two tabs: the web tab asked for, and a
shell tab running the server next to it. The shell was deliberate - the server
lived in an ordinary session so it was visible, scrollable, killable and died
with its tab, and nothing new had to supervise a long-lived HTTP server. That
reasoning was sound and the result was still wrong in use: opening a dashboard
should open one tab, and after the first launch the terminal is pure noise.
The server moves to a background child process owned by a new
`src/deepseek-web-server.ts`, behind `POST /api/deepseek/web`. What the session
gave away for free is now explicit, which is most of the module:
- Exactly one server. A second click reuses the running one instead of racing
it for a port; the session flow could not do this at all, because two clicks
were simply two sessions.
- Restarted when the requested authority changes. `--trusted-host` fences dsh's
own /api against the browser authority, and a Codeman reachable at both
loopback and a tailnet name has two. Reusing a server fenced for the other
origin renders a page whose every call 403s, which reads as a broken
dashboard rather than a misconfigured one, so a mismatch restarts instead.
- Killed on shutdown. The child is detached so its whole plugin tree can be
signalled at once, which also means it would outlive Codeman and hold its
port against the next start - the exact EADDRINUSE this feature already got
wrong once.
- Boot output captured and returned. With no shell tab there is nowhere else
for a stack trace to land, so a failed spawn reports its own tail.
The endpoint is fenced at the same bar as the profile installer and for the
same reason: booting a dsh profile executes the plugin code in it, so this is a
privileged action even though it reads as "open a page". `authority` comes from
the client (`location.host`) because only the browser knows which origin is in
play, and it is regex-confined at the schema boundary - defence in depth behind
the argv-array spawn, admitting host:port in the shapes a browser authority can
take and nothing readable as a second argument.
`GET /api/deepseek/web-port` is gone; port selection moved into the supervisor,
which is the thing that knows whether a server is already running. The two
client-side probe helpers went with it, since the server now owns the wait.
Verified over the tailnet authority end to end: no session is created (session
count unchanged, one tab), the server runs on 3081 beside the user's own dsh
web on 3080, status reports the tailnet authority, and the proxied dashboard
renders with zero 4xx. Full gate green (6148 passed, +6).
Fifteen review findings on the dsh mode, the serious ones first:
- Multi-user: DEEPSEEK_BASE_URL joins the owner-clamped env keys.
_configureDeepSeek() forwards the SERVER's own DEEPSEEK_API_KEY into
every dsh pane and applyEnvOverrides() lands after it, so a non-granted
owner who could redirect the base URL would have the operator's key sent
as a bearer credential to a host of their choosing.
- Wait registry: until=stop/blocked is refused on docker and remote-SSH
dsh sessions (new deepSeekBridgeUnreachable fact in sessionHookOptions).
The HERDR triple is set via LOCAL tmux setenv, which crosses neither
docker exec nor ssh, so such a session can never post a hook event and
the wait burned its whole timeout on every turn.
- Approvals: a dsh item is an ALERT, not an answerable card. The answer
route refuses (the '1'/Esc keystrokes are Claude-dialog-shaped and the
option parser cannot read a third-party TUI's frames, so an answer was a
blind keystroke into a foreign composer), and the push notification
carries no Approve/Deny actions for dsh sessions.
- Status shim (v3): --seq is forwarded and the server drops stale retried
reports inside a 60s window (the TUI retries with backoff, so a retried
'working' could land after 'blocked' and resolve an approval whose
dialog was still on screen); 4xx responses exit 0 instead of retrying,
so one misconfigured session cannot feed the auth rate-limit bucket
until the hook endpoint 429s for the whole instance.
- Web-UI server: concurrent starts are serialized through a lock (two
racing POSTs used to pick the same port and orphan the winner), and the
readiness poll / timeout paths only clear or stop the singleton while it
is still theirs. First click actually opens the tab now
(refreshWebviews, not the nonexistent loadWebviews). DELETE
/api/deepseek/web requires the privileged grant in multi-user mode.
- Cron: deepseek jobs run the same two-part launch gate as the HTTP
create paths (impl moved into the resolver so all three share it) and no
longer stamp a Claude default model on the session.
- Parity sweeps: quick-start's docker branch rejects deepSeekConfig like
the remote branch; the Ralph auto-enable list gained deepseek;
HookEventType gained agent_working; the phone overview run menu filters
managed webview records like the desktop menu.
- install.sh: the dsh identity probe closes stdin (under curl|bash a
child that reads stdin eats the rest of the script), bounds the exec
with timeout where available, and is memoized to one scan per install.
- Welcome screen: .welcome-btn-deepseek styled in the #4d6bfe brand
identity (it rendered as an unstyled UA-grey button); stale markup
comment about the web shortcut rewritten; clamp docs updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Ark0N
Ark0N merged commit 975705a into masterAug 25, 2026
2 checks passed
@Ark0N
Ark0N deleted the feat/deepseek-harness branch August 25, 2026 17:12
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Ark0N@claude