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

Ark0N merged 6 commits into
masterfrom
feat/deepseek-harness

Conversation

@Ark0N

@Ark0N Ark0N commented Aug 24, 2026

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_MODE sandbox approvals
read-only read-only ask
workspace-write workspace-write ask (the harness's own default)
danger-full-access danger-full-access never

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, not read-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 state Hook event Effect
blocked permission_prompt red "needs you" alert + answerable Approvals Inbox item
idle stop definitive end of turn: respawn triggers, wait returns
working agent_working clears 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.

claude added 5 commits August 24, 2026 03:37
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 master Aug 25, 2026
2 checks passed
@Ark0N
Ark0N deleted the feat/deepseek-harness branch August 25, 2026 17:12
Ark0N pushed a commit that referenced this pull request Sep 14, 2026
Auditing every release after the previous backfill turned up two more. 1.23.0
had no Thanks in either artifact; its three PRs (#337, #341, #338) are authored
by the maintainer, so like the others it credits the release it follows.
1.22.0 had the section on its GitHub release but never in CHANGELOG.md, which
is the drift that happens whenever the block is added post-hoc instead of in
the changeset.

Every release from 1.21.0 forward now carries a Thanks section in both places.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to 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