Skip to content

feat(agents): add first-class DeepSeek Harness (dsh) support - #22468

Merged
nwparker merged 20 commits into
mainfrom
nwparker/harneeses
Sep 28, 2026
Merged

nwparker merged 20 commits into
mainfrom
nwparker/harneeses

Conversation

@nwparker

@nwparker nwparker commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 18 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​857 $\color{#cf222e}{\Huge{\mathbf{−}}}$​5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​852
Prod 55 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1127 $\color{#cf222e}{\Huge{\mathbf{−}}}$​71 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1056

ELI5

DeepSeek's coding agent, DeepSeek Harness, can now be picked from Orca's agent menu like Claude or Codex. Orca launches it, knows it by name, shows whether it is thinking, finished, or waiting on you, and can pick the conversation back up later.

What Changed

Before: DSH only ran if you typed dsh-tui into a blank Orca terminal. It was absent from the agent menu, and once running Orca mislabelled the pane "Gemini CLI" and showed it as permanently working — no status, no notification, no "needs your answer", no resume.

After: DeepSeek Harness is in the agent menu, launches into a pane, and reports real status.

The pieces:

  • Catalog and detection. DSH ships one binary, dsh, that boots a profile; only the dsh-tui profile paints a composer, while web / headless / sdk / acp / plugin are all dsh in the process table. Detection reads the profile out of the launcher's own tokens so a server or a one-shot is never mistaken for an agent pane.
  • Status, questions and notifications ride DSH's own first-party @deepseek-ai/dsh-hooks-claude-code bridge rather than a bespoke protocol. Orca mounts it from a marker-delimited block it owns in $DSH_HOME/cordis.patch.yml — one layer above every profile, so a pane the user started themselves still reports, and Orca never edits a profile the user owns.
  • Two things had to be fixed for any of it to work, both found by running the real binary:
    • DSH's shell executor drops every env var whose name contains KEY, TOKEN, SECRET or PASSWORD before a hook starts — which took ORCA_PANE_KEY and ORCA_AGENT_LAUNCH_TOKEN with it, so every hook exited without posting. Orca now mirrors both onto scrub-safe aliases at spawn and the DSH hook script restores the canonical names first.
    • DSH's title is <prefix> 🐋 <session>, and its idle prefix ✦ is Gemini CLI's working glyph. That is why the pane read as a working Gemini. The whale now outranks the Gemini classifier and the title status detector, in the base module both copies of that classifier read.
  • Prompt delivery. The terminal app has no prompt flag, so the first prompt is pasted after startup. DSH animates its whale intro continuously behind the composer, so the default quiet window never settles; a dsh-composer-prompt readiness signal watches for the ❯ glyph inside the alternate screen instead.
  • Resume. dsh-tui --resume <id>, using the session_id the hook reports.
  • Headless. dsh --profile headless is registered for Source Control AI text generation. The prompt goes over stdin with DSH's - marker, not argv: it carries the whole diff, which on argv would sit in the process table and would eventually hit the argv limit. Measured against 0.1.5-rc.1 — omitting the positional entirely is rejected (a task is required) even when stdin is a pipe, so the - is required.

Also included: icon, labels, telemetry, mobile registry, SSH/remote installer, and a reference doc at docs/reference/dsh-harness-integration.md.

Why

The issue asks for first-class detect/launch, and the thread asks for status/notifications/resume rather than waiting on a second official CLI. DSH already publishes a first-party Claude-Code hook bridge, so the honest integration is to mount that instead of inventing a protocol or shipping a wrapper — which is also why this keeps working as DSH's own surface evolves.

Mounting at the home layer (rather than passing --patch at launch) was chosen so status does not depend on Orca having been the one to start the process, and so install/remove/status match how every other agent's managed hooks behave.

Two deliberate limits, both documented:

  • An approval pause reads as working, not waiting. DSH's bridge has no Notification or PermissionRequest event. The ask-the-user tool (ask_user_question) is a hook, so real questions surface correctly; an approval prompt has no signal, and working is the honest answer since the harness is mid-tool.
  • Agent Session History does not list DSH sessions yet. Its transcript is Zstandard-compressed, which the scanner cannot read. Resume works (it uses the hook's session id); browsing history is a follow-up.

Linked Issue

Relates to #14323

This adds the official dsh harness, launched through the dsh-tui profile that selects it. It does not close the broader tracking request.

Visual Proof

All captured from a hidden Electron renderer over CDP against the real @deepseek-harness-tui/dsh-tui 0.10.2 on official @deepseek-ai/dsh 0.1.5-rc.1. Model traffic is served by a local scripted endpoint so the demo needs no DeepSeek account; every hook, status row and title below is real.

1. DeepSeek Harness in the agent menu

pv-menu.png

2. Agent identity — before and after

The same pane, same app, before and after the title fix in this PR. Before, the tab and sidebar read Gemini CLI; after, they read DeepSeek Harness and carry DSH's own title.

Before After
pv-before-title.png pv-after-title.png

3. Question status — the pane is waiting on you

DSH's ask_user_question card is open; the sidebar row shows the attention state, and Orca's status store reports waiting with toolName: ask_user_question.

pv-question.png

4. Answered and finished

The answer lands, DSH reports the questionnaire answered, and the row returns to done.

pv-done.png

5. The live status rows behind those screens

Sampled from window.api.agentStatus.getSnapshot() while the turn ran:

idle     {"agent":"dsh","state":"done","origin":"hook","session":"6ab7ce38-b270-4d6a-9239-30536ed47fa1"}
working  {"state":"working"}
waiting  {"state":"waiting","tool":"ask_user_question"}
done     {"state":"done","tool":"ask_user_question","prompt":"Pick the release channel before you touch anything"}

origin: "hook" is the point: these come from DSH's bridge, not from guessing at the screen. The session id is the resume target.

6. Orchestration reader sees the pane

$ orca worktree ps
dsh-demo-repo refs/heads/master  host=local  live:2  pty:yes  unread:no
/private/tmp/dsh-demo-repo
preview: nwparker@MacBook-Pro-3 dsh-demo-repo (master) $ dsh-tui

7. The managed hook block preserves user rows

$DSH_HOME/cordis.patch.yml after Orca's install — the user's own row is untouched:

# User-owned rows: point DSH at a local scripted endpoint so this demo needs no account.
- id: llm-deepseek
  config:
    apiKeyEnv: 'DEEPSEEK_API_KEY'
    baseURL: 'http://127.0.0.1:8787'
# >>> orca-managed-dsh-hooks (managed by Orca; do not edit) >>>
- insert:
    - id: orca-agent-hooks
      name: '@deepseek-ai/dsh-hooks-claude-code'
      config:
        configPath: '/Users/…/.orca/agent-hooks/dsh-hooks.json'
# <<< orca-managed-dsh-hooks <<<

Testing

Platform tested: macOS arm64, Node 24, against @deepseek-harness-tui/dsh-tui 0.10.2 on @deepseek-ai/dsh 0.1.5-rc.1.

Manual, end to end in the app: detect → launch → SessionStart idle row → prompt → working → ask_user_question → waiting → answer → done, with the resume session id carried throughout. The hook bridge was additionally verified directly against dsh --profile headless, and DSH's env scrub rule was measured with a probe hook that printed its own environment.

Automated:

  • src/shared/agent-hook-listener/providers/dsh-events.test.ts — the event→state mapping, including the subagent events that must be ignored.
  • src/main/dsh/hook-service.test.ts, src/main/dsh/dsh-home-patch.test.ts — install/remove/status, user-row preservation, the [] empty-document case, and the truncated-region fail-closed path.
  • src/shared/dsh-launch-command.test.ts — profile-aware process recognition.
  • src/shared/dsh-terminal-title.test.ts — the Gemini/Claude title collisions.
  • src/shared/agent-hook-scrub-safe-env.test.ts — the alias contract.
  • src/main/runtime/dsh-readiness-transcript.test.ts — readiness and identity read from a committed PTY transcript (dsh-tui-ready-no-key.txt), captured per docs/reference/agent-pty-transcript-capture.md and secret-scanned clean.

pnpm tc clean; pnpm run check:code-quality:changed passes; the affected suites run 14,695 tests green.

Not tested on Windows or Linux. The Windows path follows the existing .cmd + PowerShell shape Muse uses, and DSH binds its shell to PowerShell on Windows, which is the same split wrapPosixHookCommand / wrapWindowsHookCommand already encode. The SSH installer mirrors the local one over SFTP and is covered by the existing remote-installer ratchet, but was not exercised against a live remote host.

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

AI Disclosure

Implemented with Claude Code (Opus).

Review

Review round 1 (all seven comments addressed; see the PR comment for the full reply):

  • findManagedDshPatchRegion paired an orphan start marker with a later block's end, so a truncated write made install/remove delete the user's own rows. Now paired with the nearest preceding start, with a regression test verified to fail without the fix.
  • The relay PTY env builder never applied the scrub-safe aliases (above).
  • dsh-tui web — a workspace folder named web or plugin — no longer marks a live agent pane non-interactive.
  • Source Control AI moved off argv (above).
  • The hook-service test now isolates USERPROFILE as well as HOME, so a Windows run cannot edit the real home.
  • Duplicate README badge removed, and an incidental Prettier reflow of unrelated files reverted.

Follow-up pass, no behaviour change: readManagedDshHookEvents was a near-verbatim copy of Muse's with byte-identical private helpers, so both now share one readManagedHookEventsFromJson (src/main/agent-hooks/managed-hooks-json-events.ts). Plus one reader instead of two spellings of the same read, one status builder instead of four inline literals, and simpler patch-file transforms. Net −79 lines.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Security: the scrub-safe aliases carry pane identity, never a credential — the hook token still comes from the endpoint file, which is unchanged. The managed hook block is marker-delimited and refuses to touch a truncated region rather than guessing its extent.
  • Cross-platform: POSIX and Windows hook script bodies both generated; no hardcoded separators. Not exercised on Windows/Linux (above).
  • Remote SSH: installRemote mirrors the local install over SFTP and is registered in the remote installer ratchet. Review caught that the scrub-safe aliases were applied only in the local and daemon PTY env builders — a remote pane's env is built on the relay side, so remote DSH status would have silently never appeared despite the remote hook installing fine. relay/pty-handler.ts now applies them too, and the reference doc states all three builders must.
  • Backwards compatibility: resume is gated behind a new agent-session.dsh-resume.v1 runtime capability so an older host answers a dsh session request safely instead of failing the launch. The canonical env var names are unchanged — the aliases are additive.
  • Performance: no new polling. Status is hook-driven; the title classifier gains one String.includes.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The changes add DeepSeek Harness as a supported TUI agent. They add command and process recognition, terminal-title and prompt-readiness detection, session resume handling, and commit-message agent configuration. They also add DSH hook event routing and status normalization, scrub-safe environment aliases, and local and remote managed-hook installation. Desktop, mobile, README, and supported-agent documentation now include DSH entries and assets.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to f5148

DeepSeek Harness support looks ready to merge. One test that checks owner-only file permissions will fail when the suite runs on Windows, even though the product behavior is correct. Skip that test on Windows so cross-platform test runs stay green.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 75 functions across 57 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding first-class DeepSeek Harness support.
Description check ✅ Passed The description is complete and directly aligned with the template. It explains the user impact, implementation, rationale, linked issue, visual proof, testing, AI disclosure, review notes, boundary s…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 26183221-55bd-4761-a0e8-968ab7bcb06c

📥 Commits

Reviewing files that changed from the base of the PR and between 8d6fec5 and 07531ed.

⛔ Files ignored due to path filters (1)
  • src/shared/agent-icons/dsh.png is excluded by !**/*.png
📒 Files selected for processing (83)
  • README.md
  • config/tsconfig.cli.json
  • docs/readme/README.es.md
  • docs/readme/README.fr.md
  • docs/readme/README.ja.md
  • docs/readme/README.ko.md
  • docs/readme/README.pt.md
  • docs/readme/README.zh-CN.md
  • docs/reference/agent-status-store.md
  • docs/reference/antigravity-readiness-evidence.md
  • docs/reference/ssh-execution-boundary.md
  • docs/site/content/docs/agents/supported.mdx
  • mobile/src/components/mobile-agent-icon-assets.ts
  • mobile/src/tasks/mobile-tui-agents.ts
  • src/main/agent-hooks/managed-agent-hook-registry.ts
  • src/main/agent-hooks/managed-hook-command-contract.test.ts
  • src/main/agent-hooks/remote-hook-service-installers.test.ts
  • src/main/agent-hooks/remote-managed-hook-installer-coverage.test.ts
  • src/main/agent-hooks/remote-managed-hook-installers.ts
  • src/main/agent-hooks/server-opencode-binder.test.ts
  • src/main/agent-hooks/server-retired-pane-new-turn.test.ts
  • src/main/daemon/pty-subprocess/spawn-environment.ts
  • src/main/dsh/dsh-home-patch.test.ts
  • src/main/dsh/dsh-home-patch.ts
  • src/main/dsh/hook-service.test.ts
  • src/main/dsh/hook-service.ts
  • src/main/dsh/hook-settings.ts
  • src/main/pi/agent-status-extension-source.ts
  • src/main/pi/titlebar-extension-service.test.ts
  • src/main/pi/titlebar-extension-service.ts
  • src/main/providers/local-pty-spawn-environment.ts
  • src/main/runtime/__fixtures__/dsh-tui-ready-no-key.meta.json
  • src/main/runtime/__fixtures__/dsh-tui-ready-no-key.txt
  • src/main/runtime/dsh-readiness-transcript.test.ts
  • src/main/runtime/orca-runtime-controller-knows-pty-is-live.ts
  • src/main/runtime/orca-runtime-terminal-handle-incarnation.test.ts
  • src/main/runtime/orca-runtime-write-terminal-agent-prompt.ts
  • src/main/runtime/rpc/methods/orchestration-worker-release-incarnation-fallback.test.ts
  • src/main/runtime/rpc/methods/orchestration-worker-release-reap-fixed.func.test.ts
  • src/main/text-generation/commit-message-model-discovery-policy.ts
  • src/renderer/src/components/terminal-pane/pty-connection-pi-synchronized-input.test.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/lib/agent-catalog.tsx
  • src/renderer/src/lib/agent-favicon-assets.ts
  • src/renderer/src/lib/agent-status.ts
  • src/renderer/src/runtime/agent-resume-host-authority-capability.test.ts
  • src/renderer/src/runtime/agent-resume-host-authority-capability.ts
  • src/renderer/src/runtime/web-session-tabs-sync/mirrored-status-tab-retractions.test.ts
  • src/renderer/src/runtime/web-session-tabs-sync/mirrored-status-tab-retractions.ts
  • src/shared/agent-headless-command.ts
  • src/shared/agent-hook-listener/provider-dispatch.ts
  • src/shared/agent-hook-listener/provider-event-routing.ts
  • src/shared/agent-hook-listener/providers/dsh-events.test.ts
  • src/shared/agent-hook-listener/providers/dsh-events.ts
  • src/shared/agent-hook-listener/source-routing.ts
  • src/shared/agent-hook-relay.ts
  • src/shared/agent-hook-scrub-safe-env.test.ts
  • src/shared/agent-hook-scrub-safe-env.ts
  • src/shared/agent-hook-types.ts
  • src/shared/agent-kind.ts
  • src/shared/agent-node-entrypoint-identities.ts
  • src/shared/agent-node-package-entrypoints.ts
  • src/shared/agent-process-recognition.ts
  • src/shared/agent-session-resume.ts
  • src/shared/agent-title-core.ts
  • src/shared/agent-title-status.ts
  • src/shared/agent-type-label.ts
  • src/shared/commit-message-agent-spec.test.ts
  • src/shared/commit-message-agent-specs-secondary.ts
  • src/shared/draft-paste-ready-scanner.ts
  • src/shared/dsh-launch-command.test.ts
  • src/shared/dsh-launch-command.ts
  • src/shared/dsh-terminal-title.test.ts
  • src/shared/protocol-version.ts
  • src/shared/skills-cli-agent-keys.ts
  • src/shared/source-control-ai-action-recipes.test.ts
  • src/shared/telemetry-property-schemas.ts
  • src/shared/terminal-title-agent-type.ts
  • src/shared/tui-agent-config.ts
  • src/shared/tui-agent-display-names.ts
  • src/shared/tui-agent-selection.ts
  • src/shared/tui-agent.ts
  • tests/tools/omp-source-control-runtime-smoke.mjs

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.

| `tabId` | `structuredAgentSessionTabId(sessionId)` |
| `worktreeId` | `summary.workspaceId` (a folder workspace id is a valid value) |
| `state` | `structuredAgentSessionAgentStatus(summary).state`: the lead's own status folded with its live `backgroundTasks`, so a settled lead whose subagent still runs reads `working` |
| `workingMode` | `'monitoring'` from the same fold when watch loops are the only live child work; omitted otherwise, which clears it on the row |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Describe all child work that produces monitoring.

The status tests expect workingMode: 'monitoring' when an idle lead has only a backgrounded shell or only a workflow. Neither case requires a watch loop. Replace “when watch loops are the only live child work” with wording that includes these cases.

Comment thread README.md Outdated
<a href="https://github.com/QwenLM/qwen-code"><kbd><img src="https://www.google.com/s2/favicons?domain=qwenlm.github.io&sz=64" alt="Qwen Code logo" width="16" valign="middle" /> Qwen Code</kbd></a> &nbsp;
<a href="https://support.atlassian.com/rovo/docs/install-and-run-rovo-dev-cli-on-your-device/"><kbd><img src="https://www.google.com/s2/favicons?domain=atlassian.com&sz=64" alt="Rovo Dev logo" width="16" valign="middle" /> Rovo Dev</kbd></a> &nbsp;
<a href="https://dev.meta.ai/docs/muse-code"><kbd><img src="https://www.google.com/s2/favicons?domain=dev.meta.ai&sz=64" alt="Muse logo" width="16" valign="middle" /> Muse</kbd></a> &nbsp;
<a href="https://deepseek-harness.github.io/deepseek-harness/"><kbd><img src="https://www.google.com/s2/favicons?domain=deepseek.com&amp;sz=64" alt="DeepSeek Harness logo" width="16" valign="middle" /> DeepSeek Harness</kbd></a> &nbsp;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove the duplicate DeepSeek Harness badge.

This list already adds the same label and link at Line 182. Keeping both entries renders DeepSeek Harness twice.

Comment thread src/main/dsh/dsh-home-patch.ts Resolved
Comment thread src/main/dsh/hook-service.test.ts Resolved
Comment on lines +147 to +148
promptDelivery: 'argv',
buildArgs: ({ prompt }) => ['--profile', 'headless', prompt],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🛡️ Detected with Advanced Tier | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

DeepSeek Harness dsh --profile headless read prompt from stdin

💡 Result:

Inspection citation: inspection_6eb391c43fc1e99786d5aceb2dcdfca3

<source_evidence>

<title>dsh-headless DeepSeek Plugin | DeepSeekPlugins</title> https://deepseekplugins.org/plugins/deepseek-ai/deepseek-harness~23packages~2Fbundle~2Fheadless `dsh-headless` runs one dsh task from the command line and prints the final answer, then exits — no GUI, no server, no browser. Type `dsh --profile headless "run the tests"` and the agent handles it with the same model, tools, and safety defaults as every other surface. It suits scripts, CI, and one-off jobs: it opens no ports and leaves nothing running behind. It also offers a JSON event stream (`--json`) and `--session-id` to resume a conversation. Exit code 0 means the task completed; 1 means it aborted or errored. The boundary: one task per invocation, no interactive follow-up. ... Run one task, get the final answer, and exit. The task is the command-line argument, or stdin when you omit it; the whole invocation is the smallest working example. ... ``` dsh --profile headless "run the tests" ``` ... The agent works through the task, streams each non-empty provider reasoning delta to stderr under a `dsh: reasoning:` heading, then prints the final answer on stdout and exits. Consecutive reasoning deltas stay in one section, and the runner closes that section before later output when the provider supplied no trailing newline. A successful run without reasoning keeps stderr empty; a failure exits 1 and prints `dsh:: ` to stderr. The task comes from the positional argument, or from stdin when the argument is omitted or is a lone `-`; a blank positional argument or an empty pipe is rejected before anything runs. A positional task is used as-is and stdin is not read, so put the whole prompt in the pipe when you want piped input; a piped task is sent verbatim, its trailing newline included. ... | Field | Default | Meaning | | --- | --- | --- | | `task` | stdin | The task text; stdin supplies it when omitted or `-` | | `sessionId` | `session- ` | Exact Session identity to adopt; an unknown id fails | | `json` | `false` | Project the run as newline-delimited events on stdout | ... `dsh --profile headless --help` prints the command&`#39`;s help text and exits without running anything. A whitespace-only positional task is a usage error on its own — nothing runs and the process exits 1, even when stdin is not a terminal, so an accidental blank argument never consumes a pipe. A task that is absent entirely is a usage error only when stdin is a terminal; otherwise the runner reads the task from stdin and rejects an empty result the same way. A lone `-` is the only stdin marker; mixing it with other task words is a usage error rather than a task that starts with a dash. In `--json` mode every usage error — including commander&`#39`;s own grammar rejections such as an unknown option or a missing option value — also writes an `error` event to stdout before the process exits, so a line-oriented supervisor sees a well-formed stream even when the runner never mounts; the event `message` carries the text without commander&`#39`;s `error: ` prefix. #### Understand the implementation Implementation internals — click to expand ... The runner awaits the complete application (`ctx.get(&`#39`;loader&`#39`;)?.await()`) so the composed tools and adapters are not half-mounted, reads the shared [`agentDefaultModel`](../../core/agent-default-model/README.md) selection, resolves the task from config or stdin, then resolves the Agent identity: a fresh `session- ` by default, or the persisted Session `--session-id` names, which it adopts through [`sessionQuery`](../../session-query/session-query/README.md) and refuses when no log exists. It submits the task as an ordinary user message. Without `--json` it streams that Agent&`#39`;s non-empty reasoning deltas to stderr; with `--json` it projects the run instead. It waits for quiescence, then flushes the Session and folds the owned interval (`firstSeq` onward) into the last non-empty `assistant/message` text and final `turn/end` reason. It writes the final text to stdout (or the `final` event) and requests exit. ... -base`: it ... prefix and separate ... base `system- ... Web surface, disabl…[truncated] <title>Proposal: headless driver protocol (--session-id / --model / --mode / --jsonl) · deepseek-ai deepseek-harness · Discussion `#1368` · GitHub</title> GitHub discussion 1368 in deepseek-ai/deepseek-harness (link omitted to avoid creating a cross-reference) Proposal: headless driver protocol (--session-id / --model / --mode / --jsonl) · deepseek-ai deepseek-harness · Discussion `#1368` · GitHub # Proposal: headless driver protocol (--session-id / --model / --mode / --jsonl) `#1368` Aug 14, 2026 baizhu945 Aug 14, 2026 ### Summary Proposing a small extension to the `headless` bundle so external tools can drive dsh as a real agent backend: an explicit session id (create-or-resume), per-run model / permission-mode overrides, and a JSONL event stream with an interactive approval channel. Motivation: I integrated dsh into cc-connect (a bridge that exposes AI coding agents over Feishu/Telegram/etc.) — see chenhg5/cc-connect#1682. The headless profile is the natural entry point, but the one-shot runner currently: - creates a fresh random session every run (no way to keep a conversation across runs), - prints only the final text (no streaming text/thinking/tool events), - has no interactive approval channel (approvals fail closed), - and cannot override the default model or permission mode per run. The patch below adds exactly those four flags to `packages/bundle/headless` (runner + startup + bundle config + tests): - `--session-id ` — resume the persisted session when it exists, otherwise create it with that exact id (create-once, resume-always), so an external driver can keep one conversation across runs. - `--model ` — override the default model selection for this run. - `--mode ` — pin the session&`#39`;s sandbox/approval knobs (`read-only` / `workspace-write` / `danger-full-access` / `confirm`). - `--jsonl` — stream machine-readable events on stdout (`text` / `thinking` deltas, `tool/call`, `tool/result`, `approval/request`, `result`, `done`) and read approval responses (`{"type":"approval/response","id":...,"outcome":"allowed-once"|"rejected"}`) from stdin. In `confirm` mode it also intercepts write/execute tools (`write`, `edit`, `str_replace_editor`, `bash`, `pwsh`, `terminal_send`) and asks the human first — the headless counterpart of a client-side confirm-writes plugin. The runner registers one terminal answerer on the scoped `approval/request` waterfall and relays each request to the driver, blocking until the driver answers on stdin. Without `--jsonl` the behavior is unchanged (final text on stdout, exit code mapping). ### Full patch gist: headless-cc-connect.patch — modifies: - `packages/bundle/headless/src/startup.ts` — parse the four flags - `packages/bundle/headless/src/index.ts` — create-or-resume, model/mode overrides, JSONL streaming, approval answerer + stdin reader, confirm-mode ask interception - `packages/bundle/headless/cordis.patch.yml` — wire the new runner config fields - `packages/bundle/headless/tests/headless.spec.ts` — regression + new tests (13 headless tests pass; full host `tsc -b` clean) ### Verification - `tsc -b tsconfig.host.json` clean; `vitest run packages/bundle/headless/tests/` 17/17 pass - End-to-end against the built CLI: approval → tool executes; rejection → `Error: the user rejected tool "write"` and the file is not created - Multi-turn resume + `--jsonl` + `--mode confirm` verified ### Questions for the team 1. Is a headless "driver protocol" direction you&`#39`;d be interested in? If so I&`#39`;m happy to iterate on naming/schema (e.g. JSONL event shapes, session-id semantics) and prepare a proper PR-compatible change set. 2. Any preference on the wire protocol (e.g. JSONL vs NDJSON envelope with a version field)? 3. Should the confirm-mode ask list be configurable through the bundle config instead of hard-coded in the runner? ## Replies: 2 comments Electricitysheep Aug 14, 2026 headless 驱动协议扩展(--session-id/--model/--mode/--jsonl)对 CI/外部工具集成很关键——当前 headless 适合一次性任务,加会话/模式控制后能支撑更复杂的自动化编排。 headless 的现有用法(一次性任务/CI 场景)见第 2 章: https://github.com/Electricitysheep/dsh-handbook/blob/main/docs/02-quickstart.md baizhu945 Update — v3 patch (gist file `headless-cc-connect…[truncated] <title>apps/cli/reference/README.md</title> https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/reference/README.md `dsh --profile ` boots the profile at `$DSH_HOME/profiles/ `. The effective tree is ... over an empty root by applying, in order ... each bundle patch named in the profile manifest ... s `dsh.profile.bundles ... list, the profile ... s own `cordis.patch.yml`, the home-level `$DSH_HOME/cordis ... patch.yml` (machine-local preferences shared by every profile, so ... outranks the per-profile layer), and each `--patch ` overlay in argv order. Later layers win per row; a patch replaces the targeted row&`#39`;s complete `config` value rather ... deep-merging keys ... and may insert new rows. A parse, schema, resolution, or plugin boot failure is reported and exits nonzero. SIGINT and SIGTERM dispose the mounted root before exit. ... The `web` and `headless` profiles auto-initialize from shipped templates on first use (`web`: base + web-app; `headless`: base + headless). Any other missing profile fails loud with a hint to run `dsh plugin --profile add `. ... The launcher&`#39`;s ... profile verbatim through `ctx ... dsh-cmdline`). `dsh ... reaches the web ... ` prints that ... and boots nothing, and ... hand it to) prints ... Launcher flags must come before app arguments, and the launcher&`#39`;s parser consumes one `--`: an app argument that must arrive as a literal `--` needs `-- --`. A first app argument equal to `web` or `plugin` selects that subcommand instead. `ctx.cmdlineArgs.get()` is a shared immutable read: multiple plugins may parse the same snapshot, while a profile with no reader ignores its app arguments. ... The shipped apps own these command lines: ... | Profile | Arguments | |---|---| | `web` | `--host`, `--port`, repeatable `--trusted-host` | | `headless` | the task text, as the positional argument | ... A one-shot task (`dsh --profile headless "run the tests"`) creates one fresh persisted Agent through the core registry, submits the task, waits for quiescence, and flushes the Session before deriving the last non-empty assistant text and final `turn/end` reason from its durable interval. It prints the text on stdout and exits 0 for `completed`, else 1. An invocation with no task is a usage error from that app. The shipped headless profile mounts no ApiProxy, Host, HTTP server, Web runtime, or browser client; a successful run writes nothing to stderr and opens no listening port. <title>packages/bundle/headless/README.md</title> https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/bundle/headless/README.md `dsh-headless` runs one dsh task from the command line and prints the final answer, then exits — no GUI, no server, no browser. Type `dsh --profile headless "run the tests"` and the agent works through the task with the same model, tools, and safety defaults as every other surface. It is ideal for scripts, CI, and one-off jobs: the process opens no ports and leaves nothing running behind. The exit code tells you the outcome — 0 when the task completed, 1 when it aborted or errored. The main boundary: one task per invocation, with no interactive follow-up. ... Run one task, get the final answer, and exit. The task is the command line itself, so the whole invocation is the smallest working example. ... ```sh dsh --profile headless "run the tests" ``` ... The agent works through the task, streams each non-empty provider reasoning delta to stderr under a `dsh: reasoning:` heading, then prints the final answer on stdout and exits. Consecutive reasoning deltas stay in one section, and the runner closes that section before later output when the provider supplied no trailing newline. A successful run without reasoning keeps stderr empty; a failure exits 1 and prints `dsh:: ` to stderr. A missing or blank task is rejected before anything runs. The task text is supplied through the single `task` setting: ... | Field | Default | Meaning | ... |---|---|---| ... | `task` | required | The task text for the single run | ... Use headless for scripted or automated dsh runs — CI steps, batch jobs, quick answers from a terminal. Avoid it when you need a multi-turn interactive session or a GUI; the browser surface (dsh-web-app) serves that. The process stays alive only for the run, opens no listening port, and exits on its own, so it fits pipelines that wait on the process. ... `dsh --profile headless --help` prints the command&`#39`;s help text and exits without running anything. A missing or whitespace-only task is a usage error: nothing runs and the process exits 1. ... The runner awaits the complete application (`ctx.get(&`#39`;loader&`#39`;)?.await()`) so the composed tools and adapters are not half-mounted, reads the shared `agentDefaultModel` selection, creates one fresh persisted Agent with that provider and model, and submits the task as an ordinary user message. It streams that Agent&`#39`;s non-empty reasoning deltas to stderr, waits for quiescence, then flushes the Session and folds the owned interval (`firstSeq` onward) into the last non-empty `assistant/message` text and final `turn/end` reason. It writes the final text to stdout and requests exit. ... The patch rides over `dsh-base`: it inherits the projection cache, sets the coding persona prefix and separate cwd suffix on the base `system-prompt` row, keeps the same temporary process-wide PTC mode opt-in (`DSH_TOOLS_MODE`) as the Web surface, disables the shared HMR row, inserts PTC mode&`#39`;s worker as a core execution capability, and mounts the startup provider and the runner. The cache checkpoints each persisted one-shot session for later consumers; its durability barrier flushes each covered log prefix before publishing the cache row and may split otherwise coalesced JSONL runs. The startup provider (`src/startup.ts`) injects `ctx.cmdlineArgs` (`dsh-cmdline`), reads the positional argument, prints the app&`#39`;s `--help`, and provides `headlessStartup`; the runner injects that service and reads its task from lazy config. ... | File | Role | |---|---| | `src/index.ts` | The `headless-runner` plugin: run flow, output contract, exit mapping | | `src/startup.ts` | The `headless-startup` provider: task positional and `--help` | | `cordis.patch.yml` | The one-shot patch over `dsh-base` | | — | No runtime invariant companion is published; the runner&`#39`;s observable contract (provider reasoning on stderr, final text on stdout, exit code by turn-end reason) is process-level and owned by the launcher e2e; it registers nothing and holds no mutable relation to aud…[truncated] <title>Command Line | DeepSeek Harness</title> https://deepseekdocs.com/en/docs/user-guide/cli dsh --profile web # Start the web profile (equivalent to dsh web) dsh web # Web UI (alias for --profile web) dsh --profile headless "run the tests" # One-off task; exits after it completes dsh plugin ... # Manage profile plugin dependencies > The `dsh run` subcommand has been removed; one-off tasks now use `--profile headless `. ... ## 4. headless one-off tasks​ ... `--profile headless` runs a one-off task: the task text is the rest of the command line, submitted as an ordinary user message, and after it finishes the last non-empty assistant text is written to stdout before exiting. ... dsh --profile headless "run pnpm test, summarize the failures into a list" dsh --profile headless "review the auth logic in src/server.ts and give 3 actionable suggestions" dsh --profile headless "use glob to list all call sites still using the old API" > /tmp/out.txt Behavior notes: ... - Exit code: `turn/end` is `completed` → `0`, otherwise `1`. - The source build streams non-empty provider reasoning fragments to stderr under a `dsh: reasoning:` heading and prints only the final text on stdout; a successful response with no reasoning keeps stderr empty. Errors also report their code and message there. - Submits only one task, with no interactive follow-up UI; listens on no ports (no Host/Web/browser attached). - It is a "run-and-exit" form friendly to CI/scripts, suitable for putting into a shell, cron, or Makefile. ... | What you want | Command | | --- | --- | | Run a web session | `dsh web` | | Run a one-off task | `dsh --profile headless "task"` | | Save the result of a one-off task | `dsh --profile headless "task" > /tmp/out.txt` | | Start with app-layer flags | `dsh --profile tui --resume abc` | | Inspect the current composition | `dsh web --dump-config` | | Bundle layer only | `dsh web --dump-default-config` | | Install a plugin | `dsh plugin --profile web add "github:..."` | | Install a plugin from a local directory | `dsh plugin --profile web add link:/path/to/plugin` | | Start with a custom patch | `dsh --profile tui --patch ./extra.yml` | ... Source builds launch these applications through `dsh` profiles instead of separate demo bins. `sdk`, `headless`, and `acp` use `read`/`write`/`edit` for file editing by default (0.1.3-alpha.2), while `sdk-minimal` and Web minimal keep `str_replace_editor`. `sdk-minimal` is standalone, does not inherit `base`, and pins full filesystem access: use an isolated disposable workspace. SDK / ACP reserve stdout for protocol frames. See SDK and ACP.

Citations:


🏁 Script executed:

#!/bin/bash
set -e
file=src/shared/commit-message-agent-specs-secondary.ts
git diff --unified=35 51c3434851bdf8cb4c563f0b38cff257b413ef16 07531ed6ec2751ea554b1886332c6c0d053126cd -- "$file"
printf '\n--- target file size and outline ---\n'
wc -l "$file"
ast-grep outline "$file"
printf '\n--- delivery references ---\n'
rg -n -C 5 'promptDelivery|buildArgs|spawn|stdin|dsh' src/shared/commit-message-agent-spec.ts src/shared/tui-agent.ts "$file"
printf '\n--- exact reviewed lines ---\n'
nl -ba "$file" | sed -n '125,165p'
printf '\n--- dsh config and caller references ---\n'
rg -n -C 3 'commit-message-agent-specs-secondary|promptDelivery|buildArgs' src

Repository: stablyai/orca

Length of output: 45611


🏁 Script executed:

#!/bin/bash
set -e
file=src/shared/commit-message-agent-specs-secondary.ts
printf '%s\n' '--- PR diff for target file ---'
git diff --unified=24 51c3434851bdf8cb4c563f0b38cff257b413ef16 07531ed6ec2751ea554b1886332c6c0d053126cd -- "$file"
printf '%s\n' '--- target code ---'
nl -ba "$file" | sed -n '105,165p'
printf '%s\n' '--- spec definitions and delivery hooks ---'
nl -ba src/shared/commit-message-agent-spec.ts | sed -n '1,240p'
printf '%s\n' '--- tui agent declarations ---'
nl -ba src/shared/tui-agent.ts | sed -n '1,240p'
printf '%s\n' '--- focused argv and spec references ---'
rg -n -C 3 "promptDelivery: 'argv'|promptDelivery.*argv|buildArgs|commit-message-agent-specs-secondary" src/shared src/main | head -n 240

Repository: stablyai/orca

Length of output: 36005


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- consumers of delivery selector ---'
rg -n -C 4 "spec\\.promptDelivery|promptDelivery === 'stdin'|promptDelivery.*stdin|stdin.*promptDelivery" src
printf '%s\n' '--- agent specification dispatch ---'
rg -n -C 4 "getCommitMessageAgentSpec|COMMIT_MESSAGE_AGENT_SPECS|commitMessageAgentSpec|commit-message-agent-spec" src/main
printf '%s\n' '--- repo-wide exact promptDelivery selector references ---'
rg -n -F "promptDelivery === 'stdin'" src

Repository: stablyai/orca

Length of output: 14172


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- stdinPayload consumers ---'
rg -n -C 5 'stdinPayload' src
printf '%s\n' '--- child process spawn and stdin write sites ---'
rg -n -C 4 'stdio:|\\.stdin\\.end|stdin\\.write|spawn\\(' src/main | head -n 220

Repository: stablyai/orca

Length of output: 42007


Information Disclosure

Reachability: Internal
Exploitability: Difficult
CWE: CWE-214

Pipe the headless task through stdin. The prompt includes the staged diff, so passing it in argv exposes repository content to local process inspection and can exceed OS command-line limits. The headless profile accepts a task from stdin when no positional task is provided.

Deliver the task via stdin
-      promptDelivery: 'argv',
-      buildArgs: ({ prompt }) => ['--profile', 'headless', prompt],
+      promptDelivery: 'stdin',
+      buildArgs: () => ['--profile', 'headless'],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
promptDelivery: 'argv',
buildArgs: ({ prompt }) => ['--profile', 'headless', prompt],
promptDelivery: 'stdin',
buildArgs: () => ['--profile', 'headless'],

Source: Learnings

Comment thread src/shared/dsh-launch-command.ts Resolved

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Remote (SSH relay) DSH panes will not report status: the scrub-safe aliases are only produced on the local spawn paths. Details in the inline comment. Everything else is minor.

Reviewed changes

  • Agent registration. Adds dsh across the TuiAgent tables — catalog (dsh-tui), display name, agent-kind, telemetry, mobile icon/favicon, auto-pick order, headless command (--profile headless), commit-message spec, and agent-session.dsh-resume.v1.
  • Detection & launch. New dsh-launch-command.ts reads only the launcher's own tokens to separate the interactive dsh-tui profile from web/headless/sdk/acp/plugin; agent-node-package-entrypoints.ts extracts the node-shim data out of agent-process-recognition.ts and adds the dsh-tui package path.
  • Managed hooks. dsh-home-patch.ts manages a marker-delimited insert row in $DSH_HOME/cordis.patch.yml, preserving everything outside the markers and failing closed on a truncated region; hook-service.ts writes the hook script plus a Claude-shaped managed hooks file and implements installRemote over SFTP.
  • Status bridge. dsh-events.ts maps DSH's Claude-shaped events to states (SessionStart/Stop → done, UserPromptSubmit/PostToolUse → working, PreToolUse on ask_user_question → waiting; subagent/Notification/PermissionRequest ignored).
  • Env-scrub workaround. agent-hook-scrub-safe-env.ts mirrors ORCA_PANE_KEY/ORCA_AGENT_LAUNCH_TOKEN onto ORCA_AGENT_PANE/ORCA_AGENT_LAUNCH; the hook script restores the canonical names.
  • Title & readiness. DSH's whale outranks the Gemini classifier and status detector (DSH's resting ✦ is Gemini's working glyph); a dsh-composer-prompt signal watches for the composer inside the alternate screen.
  • Docs. README plus localized readmes, the supported-agents table, and reference-doc touch-ups.

ℹ️ Nitpicks

  • The PR body says a reference doc is included at docs/reference/dsh-harness-integration.md, but that file is not in the diff and nothing in the tree references it. Either add it or drop the claim.
  • About a dozen files carry reformatting-only churn unrelated to DSH — e.g. src/main/runtime/rpc/methods/orchestration-worker-release-incarnation-fallback.test.ts (relocated oxlint-disable comments), src/main/text-generation/commit-message-model-discovery-policy.ts, src/main/pi/titlebar-extension-service.ts, src/renderer/src/runtime/web-session-tabs-sync/mirrored-status-tab-retractions.ts, tests/tools/omp-source-control-runtime-smoke.mjs. It widens the reviewed surface and invites merge conflicts; worth splitting out.
  • isDshNonInteractiveCommand's first loop returns false at the first non-flag token after index 1, so an interpreter invocation whose script path is not at index 1 (e.g. node --no-warnings …/@deepseek-ai/dsh/lib/bin.js --profile acp) is read as interactive. Only the plain node <script> --profile … shape is covered by the tests.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@nwparker
nwparker force-pushed the nwparker/harneeses branch 2 times, most recently from 35f497f to 805bada Compare September 23, 2026 19:26
@nwparker

Copy link
Copy Markdown
Contributor Author

Thanks — worked through all seven. Five were real; two I'm pushing back on with evidence.

Fixed

  1. Truncated managed block deleting user rows (dsh-home-patch.ts) — correct, and worse than the comment I'd written there claimed (buildStatus never did report the duplicate; that comment was wrong and is gone). findManagedDshPatchRegion now pairs each end marker with the nearest preceding start, so an orphan start stays un-paired and inert instead of becoming a deletion range. Added the regression test you asked for, and verified it fails without the fix (× never pairs an orphan start with a later block's end) rather than just passing after it.

  2. dsh-tui web misclassified (dsh-launch-command.ts) — correct. Once dsh-tui/dst has run, everything after it is the terminal app's argv, so a workspace folder named web or plugin was costing a live pane its status hooks. The launcher name is now checked first, at argv 0 and 1 (node shim), with extension/path normalization. Added cases for dsh-tui web, dst web, dsh-tui plugin, dsh-tui --continue web, a /…/projects/web target, the node-shim form and a Windows .cmd form — plus cases asserting those panes are still recognized as the dsh agent.

  3. Remote panes never got the aliases (local-pty-spawn-environment.ts thread) — this was the most valuable catch: the relay builds a remote pane's env itself and the client forwards only the canonical names, so remote DSH status would have silently never appeared despite the remote installer running. applyScrubSafeAgentEnvAliases now also runs in relay/pty-handler.ts buildSpawnEnv, and the reference doc states that all three builders must apply it.

  4. Diff on argv (commit-message-agent-specs-secondary.ts) — switched to promptDelivery: 'stdin'. I measured both forms against the pinned 0.1.5-rc.1 rather than trusting the newer docs: omitting the positional entirely is rejected (error: a task is required) even when stdin is a pipe, while a lone - works. So it's ['--profile', 'headless', '-'].

  5. USERPROFILE (hook-service.test.ts) — correct; this file asserts the Windows script name, so a Windows run would have edited the real home. Both names are now set and restored.

Not changing

  1. agent-status-store.md monitoring wording — that file's only change here was an incidental Prettier table reflow, and the wording you flagged is pre-existing. I've reverted my reformat so this PR no longer touches the file at all. The wording point looks right, but it belongs to whoever owns that rollup rather than to a DSH PR.

  2. Duplicate README badge — you're right that it renders twice, and I've removed one (keeping the bundled-icon entry; the favicon-service copy is the one Orca deliberately avoids, since that service is unreachable in some regions — which is why agent-favicon-assets.ts exists). Worth flagging that this is pre-existing: lines 175-211 are a single <p>, and Muse is already listed twice in it. I copied that shape; I've only de-duplicated my own entry and left Muse alone as out of scope.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — the fix commit closes the remote-status gap cleanly. Two minor suggestions inline.

Reviewed changes

  • Marker-region pairing hardened. findManagedDshPatchRegion now pairs each end marker with the nearest preceding start, so an orphan start above user rows can no longer swallow them when a later block is installed — with a regression test that fails on the old logic.
  • Remote status gap closed. buildSpawnEnv in the SSH relay now applies the scrub-safe pane-identity aliases, so a remote DSH hook can recover ORCA_PANE_KEY after DSH's credential scrub.
  • Source Control AI prompt moved off argv. DSH's headless one-shot now delivers the diff over stdin (--profile headless -) instead of placing it in the process table.
  • TUI launcher recognized as interactive. dsh-tui/dst short-circuit isDshNonInteractiveCommand, so a workspace target named web/plugin no longer marks a live pane as a headless one-shot.
  • Test isolation. The DSH hook-service test also isolates USERPROFILE; a duplicated README badge was dropped.

ℹ️ Nitpicks

  • commit-message-agent-spec.test.ts asserts dsh is in the spec list but nothing pins the new promptDelivery: 'stdin' / buildArgs() === ['--profile', 'headless', '-'] contract this commit changed; a one-line assertion would lock it in.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread .cache/pi-owner/package.json Outdated
@@ -0,0 +1,5 @@
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is an npm install prefix generated by .github/workflows/pi-owner-runtime.yml (npm install --prefix .cache/pi-owner …), not source. It looks like it was swept into the commit by a bulk git add; nothing in the repo references it as a tracked input. Please delete it and add /.cache/ to .gitignore so a future git add -A cannot re-commit the install tree.

Technical details
# Accidental generated npm prefix committed

## Affected sites
- `.cache/pi-owner/package.json:1` — new file added by `805bada0fd`; declares only `@earendil-works/pi-coding-agent: ^0.83.0`.
- `.github/workflows/pi-owner-runtime.yml:28` — `npm install --prefix .cache/pi-owner --ignore-scripts … @earendil-works/pi-coding-agent@0.83.0` is what produces it. The pinned version there, not the committed `^0.83.0`, is the source of truth.
- `.gitignore` — has no repo-relative `.cache/` rule (existing `.cache` entries are all `$HOME/.cache`-style).

## Required outcome
- The generated prefix is not part of the product and must not be tracked; the runtime workflow should continue to create it on demand.

Comment thread src/relay/pty-handler.ts Resolved

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 599de5c8-41c2-43e5-87b4-1999199a348f

📥 Commits

Reviewing files that changed from the base of the PR and between 35f497f and 6316e63.

⛔ Files ignored due to path filters (1)
  • src/shared/agent-icons/dsh.png is excluded by !**/*.png
📒 Files selected for processing (7)
  • src/main/dsh/dsh-home-patch.test.ts
  • src/main/dsh/dsh-home-patch.ts
  • src/main/dsh/hook-service.test.ts
  • src/relay/pty-handler.ts
  • src/shared/commit-message-agent-specs-secondary.ts
  • src/shared/dsh-launch-command.test.ts
  • src/shared/dsh-launch-command.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.

Comment thread src/main/dsh/dsh-home-patch.ts Resolved
Comment thread src/relay/pty-handler.ts Resolved

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta — the shared managed-hooks reader extraction and module cleanups are behavior-preserving.

Reviewed changes

  • Extracted a shared managed-hooks event reader — new managed-hooks-json-events.ts; DSH's and Muse's near-identical readers now delegate to it and their duplicated private helpers are gone.
  • Consolidated the DSH hook-service file I/O — one readTextOrAbsent replaces the existsSync+read pair (dropping a TOCTOU) and one status() builder replaces four inline status literals.
  • Simplified the DSH patch transforms — isEmptyFlowDocument/withoutTrailingBlanks replace the index-juggling empty-flow scan and the manual trailing-blank pop loops; rmSync(..., { force: true }) replaces exists-then-unlink.
  • Tightened launcher-token detection — flagName normalizes --profile=web, and the two bare-word subcommands share one set.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 943367ce-2815-44ee-8644-2d84fc8c81a3

📥 Commits

Reviewing files that changed from the base of the PR and between 6316e63 and 7c50d79.

📒 Files selected for processing (7)
  • config/tsconfig.cli.json
  • src/main/agent-hooks/managed-hooks-json-events.ts
  • src/main/dsh/dsh-home-patch.ts
  • src/main/dsh/hook-service.ts
  • src/main/dsh/hook-settings.ts
  • src/main/muse/hook-settings.ts
  • src/shared/dsh-launch-command.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review.

Comment thread src/main/dsh/hook-service.ts Resolved

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — the flow-style refusal, mode preservation, and relay identity drop are correct and covered. One minor suggestion inline.

Reviewed changes

This run re-reviewed commit b56b147d10 — the delta since pullfrog review 5296419149 (at 7c50d79925).

  • Refuses an unappendable flow-style patch. applyManagedDshPatch now returns null for a non-empty […] body, and install()/installRemote() report an actionable error instead of appending a block entry DSH could not parse.
  • Handles [] # comment. isEmptyFlowDocument strips a trailing comment, so DSH's commented empty sequence is recognized and only the [] token is dropped.
  • Preserves patch-file permissions. The rewrite goes through writeHooksJson(..., { preserveMode: true }), so an owner-only cordis.patch.yml no longer widens to the umask default.
  • Stops the relay inheriting a pane. buildSpawnEnv drops inherited ORCA_PANE_KEY/ORCA_AGENT_LAUNCH_TOKEN before mirroring the scrub-safe aliases, so a relay launched from an Orca pane cannot attribute hooks to the wrong pane.
  • Tests. New dsh-home-patch.test.ts and hook-service.test.ts cases cover the flow refusal, the [] # comment shape, and mode preservation; the 65 targeted tests pass locally.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/dsh/hook-service.ts Resolved

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 2b8908ef-e0bc-4b18-b9d6-a9351ff2d8b0

📥 Commits

Reviewing files that changed from the base of the PR and between 7c50d79 and f5148e0.

⛔ Files ignored due to path filters (1)
  • src/shared/agent-icons/dsh.png is excluded by !**/*.png
📒 Files selected for processing (5)
  • src/main/dsh/dsh-home-patch.test.ts
  • src/main/dsh/dsh-home-patch.ts
  • src/main/dsh/hook-service.test.ts
  • src/main/dsh/hook-service.ts
  • src/relay/pty-handler.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 3 remain after this review.

Comment thread src/main/dsh/hook-service.test.ts Outdated
Comment on lines +160 to +169
it('keeps an owner-only patch file owner-only', () => {
// CWE-732: the temp+rename replacement must not widen the file to the umask default.
const userRows = '- id: llm-deepseek\n config: {}\n'
mkdirSync(join(home, '.dsh'), { recursive: true })
writeFileSync(configPath(), userRows, 'utf-8')
chmodSync(configPath(), 0o600)

expect(new DshHookService().install().state).toBe('installed')
expect(statSync(configPath()).mode & 0o777).toBe(0o600)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Skip the owner-only mode test on Windows.

This file explicitly supports Windows runs (Line 59 selects dsh-hook.cmd). On Windows, chmodSync(path, 0o600) changes only the read-only attribute. statSync().mode & 0o777 then reports 0o666 for a writable file. The assertion on Line 168 fails on every Windows run, even when preserveMode works correctly. POSIX permission bits are what this test checks, so run it only on POSIX.

🧪 Proposed fix
-  it('keeps an owner-only patch file owner-only', () => {
+  it.skipIf(process.platform === 'win32')('keeps an owner-only patch file owner-only', () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('keeps an owner-only patch file owner-only', () => {
// CWE-732: the temp+rename replacement must not widen the file to the umask default.
const userRows = '- id: llm-deepseek\n config: {}\n'
mkdirSync(join(home, '.dsh'), { recursive: true })
writeFileSync(configPath(), userRows, 'utf-8')
chmodSync(configPath(), 0o600)
expect(new DshHookService().install().state).toBe('installed')
expect(statSync(configPath()).mode & 0o777).toBe(0o600)
})
it.skipIf(process.platform === 'win32')('keeps an owner-only patch file owner-only', () => {
// CWE-732: the temp+rename replacement must not widen the file to the umask default.
const userRows = '- id: llm-deepseek\n config: {}\n'
mkdirSync(join(home, '.dsh'), { recursive: true })
writeFileSync(configPath(), userRows, 'utf-8')
chmodSync(configPath(), 0o600)
expect(new DshHookService().install().state).toBe('installed')
expect(statSync(configPath()).mode & 0o777).toBe(0o600)
})

Register DSH as a supervised Orca agent: catalog entry and detection for its
dsh-tui profile, status/question hooks through DeepSeek's own Claude-Code hook
bridge, composer-ready prompt delivery, session resume, headless Source Control
AI, and title identity that no longer collides with Gemini's.
…its title as Gemini

DSH runs command hooks through its own shell executor, which drops every env var whose
name contains KEY, TOKEN, SECRET or PASSWORD — taking ORCA_PANE_KEY and
ORCA_AGENT_LAUNCH_TOKEN with it, so every hook exited without posting. Mirror both onto
scrub-safe aliases at spawn and restore them at the top of the DSH hook script.

Its title collided too: DSH rests on the same glyph Gemini works on, so a resting DSH
pane was relabelled Gemini CLI and reported working forever. Defer both the Gemini
classifier and the title status detector on DSH's whale, in the base module both copies
of that classifier read.
…, keep the diff off argv

- findManagedDshPatchRegion paired an orphan start marker with a later block's end, so a
  truncated write made install/remove delete the user's own rows. Pair each end with the
  nearest preceding start; regression test fails without the fix.
- The relay PTY env builder never applied the scrub-safe aliases, so remote DSH status
  silently never appeared even with the remote hook installed.
- Source Control AI sent the whole diff on argv; send it over stdin with DSH's '-' marker.
- dsh-tui/dst already chose the interactive profile, so a workspace folder named 'web' or
  'plugin' no longer marks a live agent pane non-interactive.
- Isolate USERPROFILE as well as HOME so a Windows run cannot edit the real home.
- Drop the duplicate README badge and revert an incidental doc reformat.
…ules

Reuse before reimplementing: readManagedDshHookEvents was a near-verbatim copy of Muse's,
with byte-identical private helpers. Both now call one readManagedHookEventsFromJson.

Also: one readTextOrAbsent instead of two spellings of the same read (dropping an
existsSync TOCTOU), one status() builder instead of four inline literals, rmSync(force)
instead of exists-then-unlink, and a redundant empty-string guard before JSON.parse.
The patch-file transforms lose their index juggling for a predicate plus a filter.
… relay inheriting a pane

- applyManagedDshPatch matched only an exact `[]`, so `[] # keep empty` or a non-empty
  flow sequence got a block entry appended after it — invalid YAML that would leave DSH
  unable to load the user's own patch layer either. It now strips the token from an empty
  sequence (keeping a trailing comment) and returns null for a non-empty one; install
  reports that and changes nothing.
- The patch rewrite dropped an owner-only file to the umask default (CWE-732); pass
  preserveMode.
- The relay PTY env never dropped inherited pane identity the way the local and daemon
  builders do, so a spawn that specified none could inherit the relay's own and every
  agent's hook would report against that pane.
… the mode test to POSIX

A refused patch file carries no managed region, so getStatus() fell through to a bare
not_installed with detail null — the actionable 'rewrite it as a block sequence' message
only ever reached the one-shot install() return. Export the predicate and check it first,
behind one shared message constant.

The owner-only mode assertion cannot hold on Windows, where chmod only toggles the
read-only attribute and mode & 0o777 reads 0o666 for any writable file.
Measured, not derived: 4220 without the DSH icon entry, 4221 with it. Two of the three
modules above main's 4218 pin are not this change's — they arrived with the mobile work
after #22570 and were never repinned; the changelog records that split explicitly.
@nwparker

Copy link
Copy Markdown
Contributor Author

Second review round — both new comments were right, and both are fixed.

Flow-style refusal now survives a status read (hook-service.ts:142). Exactly the gap described: a refused file carries no managed region, so getStatus() fell through the pointer comparison to a bare not_installed with detail: null, and the actionable message only ever reached the one-shot install() return — so Settings would have shown "not installed" with no reason. isNonEmptyFlowDocument is now exported as isDshPatchFileUnappendable and checked in buildStatus before the pointer comparison, behind a single shared message constant so status cannot drift from what install said. The regression test re-polls after the refusal and asserts the detail is still there.

Owner-only mode test scoped to POSIX (hook-service.test.ts:169). Correct — this file asserts the Windows script name, and on Windows chmod only toggles the read-only attribute, so mode & 0o777 reads 0o666 for any writable file and the assertion could never mean what it says. Now it.skipIf(process.platform === 'win32') with that reason in a comment.


Also rebased onto current main (11 commits). Three things worth flagging from that:

  1. main had de-duplicated the Muse badge — the pre-existing duplication I noted last round. My resolution took main's removal, which also dropped my own badge; restored so the README carries exactly one DeepSeek Harness entry, the bundled-icon one.

  2. The mobile closure pin is measured, not derived. I resolved the conflict to 4219 by arithmetic, then measured and got 4221, so the arithmetic was wrong. Isolating it by removing just the dsh.png entry and re-running: 4220 without it, 4221 with it — my change is +1, as expected.

  3. Two of those three modules are not mine. main pins 4218 but measures 4220 at 37820f9; the drift arrived with the mobile work after feat(mobile): the page owns its safe area, like a native screen (OTA phase C follow-up) #22570 and was never repinned. I've pinned the measured 4221 and recorded the split explicitly in the file's changelog rather than folding someone else's +2 in silently. Worth a look from whoever owns that area — main's own mobile web app bundle job should be red on that pin today.

Verification: pnpm tc clean, changed-code quality gate passes, the closure test passes 5/5, and 11,851 tests green across src/main/dsh, src/shared, src/main/agent-hooks and src/relay.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta. The flow-style refusal now persists into status, which closes the prior review thread; this is not an approval because two earlier threads (relay alias test coverage, /.cache/ gitignore) are still open and untouched by these commits.

Reviewed changes

This run re-reviewed the delta since pullfrog review 5297947652 (at b56b147d) — commits 28652cd, 279be4c, a916199.

  • Flow-style refusal now reported on every status read. isNonEmptyFlowDocument became the exported isDshPatchFileUnappendable(text), and buildStatus checks it before the pointer comparison, so a refused […] patch file keeps returning the actionable error from getStatus() instead of decaying to a bare not_installed.
  • One refusal message for install, remote install, and status. The text moved into a shared FLOW_STYLE_DETAIL constant, so the one-shot and polled reads cannot drift.
  • Mode-preservation test scoped to POSIX. it.skipIf(process.platform === 'win32'), since chmod only toggles the read-only bit on Windows and the mode assertion cannot mean what it says there.
  • Flow-refusal test now re-reads status. The hook-service case asserts a later getStatus() still reports the refusal, which would fail against the pre-fix pointer path.
  • Mobile session-route closure repinned to 4221, with the drift from 4218 documented inline as pre-existing inputs rather than folded into the DSH delta.
Technical details
# DeepSeek Harness increment review

## Delta verified
- `src/main/dsh/dsh-home-patch.ts:133` — predicate exported; same flow-sequence-only semantics as before.
- `src/main/dsh/hook-service.ts:140` — flow check sits after the managed-file read and before
  `readManagedDshHooksConfigPath`, so the refused file's detail survives.
- `src/main/dsh/hook-service.test.ts:186` — re-read assertion fails without the fix.
- Tests run locally: `dsh-home-patch` (17) + `dsh hook-service` (12) → 29 green.

## Carried-forward risks (not new; tracked in the PR summary snapshot)
- Relay alias mirroring still has no test in the relay spawn-env suite.
- `/.cache/` remains absent from `.gitignore`.
- PR body references `docs/reference/dsh-harness-integration.md`, which is not in the tree.
- Windows/Linux and a live SSH remote were not exercised.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta — the DSH tui-idle lane is correct and covered, and the origin/main merges preserve every DSH registration. This is not an approval because two earlier threads (relay alias test coverage, /.cache/ gitignore) are still open and untouched by these commits.

Reviewed changes

This run re-reviewed the delta since pullfrog review 5298516087 (at a916199) — the authored tui-idle commit 36ad7b7 plus the two origin/main merges (ead7e14, d49efbf).

  • DSH settles tui-idle on its own hook. hasFreshDoneFirstPartyStatus adds a first-party lane to isTuiIdleSatisfied: a fresh done row for a dsh pane satisfies the wait, since DSH's title deliberately carries no idle status (its ✦ rest glyph is Gemini's working glyph). Scoped to dsh, because agents whose hooks report child turns can emit a mid-turn done.
  • Merge integration. origin/main's ZCode agent and extracted entrypoint helpers were reconciled with this branch's splits; DSH remains registered in the managed and remote hook registries, the shared coverage ratchet, and the node entrypoint tables, with no duplicate entries.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Adding the dsh-tui capture to __fixtures__ enrolled it in the serialize replay sweep, where
it reports 10 I2 divergences and failed the unlisted-transcript default of 0.

Every one is the same shape — visible-grid row=0, a 24-bit background the round trip does
not restore to default — which is DSH's whale intro painting whole rows of true colour.
Verified as an upstream limitation rather than a regression by replaying against the
previous build (build-serialize-addon-at-ref.mjs --ref origin/main): I1 and I3 both hold.
@nwparker

Copy link
Copy Markdown
Contributor Author

Thanks — this is a careful report, and the caution at the end is the most useful part of it.

Re-confirmed against current main, since your table came from a checkout pinned in August. It holds, with one correction:

title detectAgentStatusFromTitle resolveExplicitTerminalTitleAgentType getAgentLabel
DeepSeek Build null null null
⠋ - Waiting… - <session> - DeepSeek Build working null Claude Code

So the misattribution is in the label path, not the agent-type resolver — the resolver still returns null. The cause is the generic braille-spinner fallback in computeAgentLabel: any unrecognised title carrying a braille frame falls through to Claude Code.

That is the same bug class this PR hit with DSH's own working frames (⠂/⠐), which is exactly why the whale guard exists. It is pre-existing, it predates this PR, and it will catch any third-party TUI that spins with braille and is not otherwise recognised. Worth its own issue — it is not something this PR should fix on behalf of a CLI Orca does not ship support for.

Your caution is right, and it is the same reasoning behind the whale. Adding deepseek to AGENT_NAMES would be the cheap fix and a bad one: these titles are free text, buildAgentNameRe would match any pane whose task text happens to contain the token, and your Korean-title example is a real instance of exactly that. A glyph only one CLI emits is the safer shape. I would not take the word.

OSC 9999 dropping unregistered agentType is deliberate. Identity is a closed set — isRecognizedAgentType gates it — so a name Orca does not know is dropped rather than surfaced. Your read is correct.

On whether Orca would take a dsb entry: that is a maintainer call, not mine to make on this PR. I would not want to imply otherwise by answering it here. What I can say is what this PR establishes as the bar for the DSH entry, since it is the closest precedent: a published binary someone can install, a first-party status signal rather than screen-scraping (DSH's own dsh-hooks-claude-code bridge), a captured PTY transcript committed as evidence for any rule that reads the screen, and a detection rule that cannot collide with another agent. If dsb can meet that, the discussion is a lot shorter. Best raised on #14323 or a new issue where a maintainer will see it — this PR's thread is not the right venue for a scoping decision about a different CLI.

One practical note: 🐋 occurring zero times in the dsb tree matches what I would expect, and I agree the two rules never meet. Nothing here changes this PR.

Only conflict was the serialize known-I2 table, where main added the Codex 0.157 fixtures
beside this branch's DSH entry; both sets kept.
Same single conflict as before: the serialize known-I2 table, where main added the Claude
trust-dialog fixture beside this branch's DSH entry. Both kept.
Main refactored isTuiIdleSatisfied into evaluateTuiIdle, which returns a verdict rather
than a boolean. The DSH lane still returned `true`; it is tier-1 positive evidence, so it
returns READY_STRONG like the title/body lane above it. Re-verified the regression test
still fails without the lane.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta — the origin/main merge and the one-line DSH adaptation are correct and the affected suites pass. Not an approval: the two earlier threads (relay alias test coverage, /.cache/ gitignore) remain open and untouched by these commits.

Reviewed changes

This run re-reviewed the delta since pullfrog review 5324067086 (at d49efbf892) — the origin/main merges up to base 78771646af, plus the authored commit 3d45c3b09c.

  • DSH tui-idle lane adapted to the new verdict API. origin/main (#22927) replaced the boolean isTuiIdleSatisfied with evaluateTuiIdle returning a TuiIdleVerdict; the DSH first-party-done lane now returns READY_STRONG instead of a bare true, keeping its position after the title lane and before the working veto. done and a fresh working are mutually exclusive on one status row, so the ordering is sound.
  • New DSH transcript pinned in the serializer replay. dsh-tui-ready-no-key joins KNOWN_PREEXISTING_I2_FAILURES with a count of 10, documented as true-colour background divergences verified against the previous build (build-serialize-addon-at-ref.mjs --ref origin/main). Test-only, and the fixture is new in this PR.
  • Merge reconciliation. Main's Claude trust-dialog / terminal-wait work integrates without disturbing the DSH registrations; the two touched test files keep their DSH entries.

Verified locally on this delta: tui-idle-evidence.test.ts + dsh-readiness-transcript.test.ts (26 tests) and serialize-grid-transcript-replay.test.ts (28 tests, including the new DSH fixture) pass.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

…awn path

The relay builds a remote pane's env itself, so the alias mirroring there had no
test: removing the call left every suite green while remote DSH status silently
vanished. Both cases fail without it.
The reference doc had no inbound link from anywhere in the repo.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — the relay alias regression tests close the prior gap, and one new doc pointer dangles.

Reviewed changes

This run re-reviewed the two commits since pullfrog review 5334146983 (at 3d45c3b09c).

  • Pinned the relay's scrub-safe alias mirroring with regression tests. pty-handler-spawn-environment.test.ts now spawns with explicit ORCA_PANE_KEY/ORCA_AGENT_LAUNCH_TOKEN and asserts both aliases land on callArgs.env, plus a second case that seeds the relay's own environment and asserts no alias survives a spawn claiming no identity. Both fail when the applyScrubSafeAgentEnvAliases(result) call is stubbed.
  • Pointed DshHookService at the integration reference. A JSDoc comment now links to docs/reference/dsh-harness-integration.md for the profile/patch-layer and env-scrub model.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

: status(configPath, 'partial', `Managed hook missing for events: ${missing.join(', ')}`, true)
}

/** Installs Orca's status hooks into DSH. See `docs/reference/dsh-harness-integration.md`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs/reference/dsh-harness-integration.md does not exist anywhere in the tree or in this PR's diff, so this pointer dangles and the commit's premise — that the reference doc simply needed an inbound link — cannot hold. Either commit the reference doc or drop this pointer and the matching claim in the PR body.

@nwparker
nwparker merged commit 45f3512 into main Sep 28, 2026
59 checks passed
@ckng

ckng commented Sep 28, 2026

Copy link
Copy Markdown

Linux: first-class DSH integration not detected, pane misclassified as Gemini CLI

Environment

OS Zorin 18 (Ubuntu 22.04 base), x86_64
Orca 1.4.211 (AppImage, launched via orca-ide)
Node v22.23.2 (fnm)
DSH @deepseek-ai/dsh@0.1.7-rc.2, installed via npm i -g @deepseek-ai/dsh@next
TUI profile dsh-tui — dsh --profile dsh-tui launches and works standalone

Summary

The DSH agent entry never appears in the catalog on Linux, and a manually launched dsh-tui pane is classified exactly as the pre-integration behavior described in this PR (Gemini CLI, permanently "working"). PATH visibility has been ruled out.

Repro

  1. Install DSH globally, install the dsh-tui profile, confirm dsh --profile dsh-tui runs in a normal terminal.
  2. Launch Orca (both from the desktop and from the same shell with full PATH).
  3. Open Settings → Agents — 34 catalog entries, none is DSH.
  4. Open an Orca terminal pane, run dsh --profile dsh-tui.
  5. Run orca status --json | grep -i dsh.

Actual

  • Settings → Agents: no DSH entry at all (not even "Not installed").
  • Manual dsh --profile dsh-tui inside Orca: pane labelled Gemini CLI, shown as permanently working — identical to the bug this PR fixes.
  • orca status --json | grep -i dsh → empty.
  • $DSH_HOME/cordis.patch.yml is never created by Orca.
  • Creating it empty makes DSH abort: patches …/cordis.patch.yml must be a top-level YAML array of loader patch entries. [] is accepted.

Expected

  • DSH listed in the agent catalog with dsh-tui recognised as the interactive profile.
  • Pane labelled "DeepSeek Harness", with status/question/resume via the hook bridge.

Evidence that PATH is not the cause

dsh was made reachable at all of:

  • ~/.local/share/fnm/aliases/default/bin/dsh
  • ~/.local/bin/dsh
  • /usr/local/bin/dsh

and Orca was launched from a shell where dsh --version succeeds. Behavior is unchanged, which points at the catalog/detection layer rather than binary discovery.

Suspected causes

  1. The DSH catalog entry / profile-token parser appears to be gated or registered only on darwin.
  2. The title classifier still matches DSH's idle prefix (✦) to Gemini CLI's working glyph.
  3. Hook-bridge mount into $DSH_HOME/cordis.patch.yml never runs on Linux, so status/resume never initialise.
  4. loadOptionalPatches rejects an empty patch file; a missing or zero-byte file should be treated as [] rather than fatal.

Workaround

Settings → Agents → Custom agents → Add

  • Label: DeepSeek Harness
  • Launch: dsh --profile dsh-tui
  • Detect: dsh --version

This yields a correctly-labelled, interactive DSH pane. Hook-based status, question notifications, and resume are still unavailable, since those depend on the built-in bridge mount.

@Meisburg

Copy link
Copy Markdown

Environment: Orca 1.4.211, Linux (Omarchy/Hyprland), dsh on the tui profile (@tomowang/dsh-tui@0.9.0), 7 Orca panes each running dsh --profile tui.

How it presents here — every dsh worktree reports no agent at all, while a pi pane in the same app reports a live one, so those panes get no status row, no unread marker, no completion notification:

$ orca worktree ps --json   # condensed
# /home/…/Projects/agentic-engineering   status=working  agents=[{"agentType":"pi","state":"working"}]
# /home/…/workspaces/playdate-app/Inbox  status=active   agents=[]
# … six more dsh panes the same: agents=[]

Corroborating the mislabel in the "Before" section: one of these panes reports agentIdentity: "hermes" in orca terminal list --json while it is running dsh --profile tui — the same class of fallback as the "Gemini CLI" case you describe.

One finding relevant to the "needs your answer" half of this PR: dsh-tui already attempts a waiting-for-you signal — Orca just can't see it. @tomowang/dsh-tui/lib/tui/TuiApp.js, in updateOverlay() when an approval or userQuestion overlay appears:

// OSC 9 desktop notification — same mechanism Claude Code's own CLI uses
this.tui.terminal.write(`\x1b]9;${message}\x07`);

Orca registers OSC handlers for 0,1,2,4,7,8,10,11,12,52,104,110,111,112,133,1337 — not 9 — so that sequence is dropped on the floor. So "dsh-tui is waiting for your answer" never reaches the notification centre or any pane state, independently of whether a dsh agent hook exists yet.

That may be worth an explicit decision in this PR: teach the terminal OSC 9 → attention/notification (it would also cover anything else using the Claude Code convention), or have the dsh hook publish the waiting state itself. It can't be derived from turn boundaries, because a dsh turn stays open while it waits on a question — record.rows.turnBoundary.val.openTurnStartSeq is still set, so "working" and "blocked on the user" look identical on disk.

What I'm running meanwhile (not a request — just data, and it's the only reason my panes show anything at all): Orca's bell path is agent-agnostic, so a BEL on the pane's pty calls markWorktreeUnread / markTerminalTabUnread and schedules the terminal-bell notification. A small watcher reads the on-disk session store (~/.dsh/storages/session_projcache/sessions/*.json, keyed on turnBoundary.openTurnStartSeq) and writes \a to the pane's /dev/pts/N on a working→idle transition, with a 15s settle window so an orchestrator firing short directives doesn't ring on every turn. That buys the sidebar/tab unread marker today; it is no substitute for a real agent status row, which is what this PR adds.

ggbdpq added a commit to ggbdpq/orca that referenced this pull request Nov 5, 2026
…nother tabbed worktree

A tmux server started from an Orca pane freezes that pane's environment,
including the scrub-safe pane alias ORCA_AGENT_PANE (stablyai#22468). Sessions a
user later creates detached in that server inherit the alias, and since
an agent of a different worktree posts its hooks under the creating
pane's key. The claimed pane key and worktree id are inherited claims,
while the conversation folder the agent reports in its hook payload is
not, so a record written from such a hook made the next Orca restart type
`cd <other worktree> && claude --resume <other session>` into the wrong
tab, and the tab's own conversation was not restored (stablyai#37799).

Guard the resume-record boundary on the execution host: a hook status or
capture whose provider session cwd lies inside another worktree that has
its own tabs in the same window session never writes, replaces, clears,
or restores that pane's record. A cwd outside every tabbed worktree keeps
today's behavior (stablyai#16960), and the pane's own record stays kept so a
later hook from the pane's own conversation still refreshes it.

- cwdInsideOtherTabbedWorktree: shared predicate over the session's
  tabbed worktree paths, boundary-aware and spelling-normalized; when the
  pane's own worktree nests below another tabbed one, the longest matching
  worktree path owns the folder so a cwd inside the pane's tree is not
  misread as foreign (coderabbit Major PRRT_kwDORpCz1s6zN5uJ)
- hookStatusResumeCommand and quitResumeRecords: skip foreign statuses
- applyWorktreeSleepResumeRecords: skip foreign sleep captures
- resolvePaneOwnedResumeStartup: veto restore of a stored record or a
  hook-store read naming another tabbed worktree; a stored record stays
  kept and the pane reopens as a plain shell

Fixes stablyai#37799

Generated-by: GLM-5.3-Flash (ZCode)
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.

4 participants