cursor-route 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env bash
2
+ # generate-hero-demo.sh — regenerate docs/fixtures/hero-demo.log
3
+ #
4
+ # Dry-run only: no live Grok, no worker spawned, no secrets. Jobs dir is
5
+ # sandboxed (trap-cleaned). Output is path-scrubbed + id-normalized so the
6
+ # committed fixture is machine-agnostic and regenerates stably.
7
+ #
8
+ # Prerequisites: Bun or Node 20+; runnable ./bin/cursor-route
9
+ # (git clone: bun install; npm global install ships dist/).
10
+ set -euo pipefail
11
+
12
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
13
+ BIN="$ROOT/bin/cursor-route"
14
+ OUT="$ROOT/docs/fixtures/hero-demo.log"
15
+
16
+ if [[ ! -f "$BIN" ]]; then
17
+ echo "missing $BIN — run from a cursor-route checkout (need bun/node 20+)" >&2
18
+ exit 1
19
+ fi
20
+
21
+ # Neutralize inherited overrides that would leak private paths into the log.
22
+ unset CURSOR_ROUTE_CLAUDE_DS_BIN CURSOR_ROUTE_GROK_BIN CURSOR_ROUTE_DSH_BIN \
23
+ CURSOR_ROUTE_ALLOW_ANTHROPIC \
24
+ CURSOR_ROUTE_ALLOW_STOCK_CLAUDE CURSOR_ROUTE_ANTHROPIC_BASE_URL CURSOR_ROUTE_DS_MODEL \
25
+ CURSOR_ROUTE_OPENROUTER_MODEL OPENROUTER_API_KEY OPENROUTER_BASE_URL \
26
+ ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN ANTHROPIC_API_KEY ANTHROPIC_MODEL \
27
+ DEEPSEEK_API_KEY XAI_API_KEY 2>/dev/null || true
28
+
29
+ TMP="$(mktemp -d)"
30
+ trap 'rm -rf "$TMP"' EXIT
31
+ export CURSOR_ROUTE_JOBS_DIR="$TMP/.demo-jobs"
32
+ # Pin dsh to a missing path so worker:deepseek renders identically on machines
33
+ # that do have a real dsh installed (the adapter shows the install hint, no path).
34
+ export CURSOR_ROUTE_DSH_BIN="$TMP/no-such-dsh"
35
+
36
+ RAW="$TMP/hero-demo.raw"
37
+ NORM="$TMP/hero-demo.norm"
38
+
39
+ {
40
+ echo "\$ cursor-route --version"
41
+ "$BIN" --version
42
+ echo
43
+
44
+ echo "\$ CURSOR_ROUTE_RELAXED=1 cursor-route health"
45
+ CURSOR_ROUTE_RELAXED=1 "$BIN" health
46
+ echo
47
+
48
+ echo "\$ cursor-route start --lane mid --model flash --dry-run \"Add a unit test for shellQuote\""
49
+ "$BIN" start --lane mid --model flash --dry-run "Add a unit test for shellQuote"
50
+ echo
51
+
52
+ echo "\$ cursor-route start --lane easy --dry-run \"Rewrite this FAQ answer in 3 sentences\""
53
+ "$BIN" start --lane easy --dry-run "Rewrite this FAQ answer in 3 sentences"
54
+ echo
55
+
56
+ echo "\$ cursor-route start --lane hard --dry-run \"Refactor auth module; run tests; report verify evidence\""
57
+ "$BIN" start --lane hard --dry-run "Refactor auth module; run tests; report verify evidence"
58
+ echo
59
+
60
+ echo "\$ cursor-route start --lane mid --dry-run --json \"Add a failing test then make it pass\""
61
+ "$BIN" start --lane mid --dry-run --json "Add a failing test then make it pass"
62
+ echo
63
+
64
+ echo "\$ cursor-route jobs --json"
65
+ "$BIN" jobs --json
66
+ } > "$RAW"
67
+
68
+ jobs_display="$HOME/.local/share/cursor-route/jobs"
69
+ {
70
+ while IFS= read -r line || [[ -n "$line" ]]; do
71
+ line="${line//$TMP\/.demo-jobs/$jobs_display}"
72
+ line="${line//$ROOT/~/Projects/cursor-route}"
73
+ line="${line//$HOME/~}"
74
+ line="$(printf '%s' "$line" | sed -E \
75
+ -e 's#/Users/[^/\"'\'' ]+#~#g' \
76
+ -e 's#/home/[^/\"'\'' ]+#~#g' \
77
+ -e 's#/opt/cemini[^\"'\'' ]*#~#g' \
78
+ -e 's#/var/folders/[^\"'\'' ]+#/tmp#g')"
79
+ printf '%s\n' "$line"
80
+ done
81
+ } < "$RAW" > "$NORM"
82
+
83
+ python3 - "$NORM" "$OUT" <<'PY'
84
+ import re, sys
85
+ src, dst = sys.argv[1], sys.argv[2]
86
+ text = open(src, encoding="utf-8").read()
87
+ seq = ["a1b2c3d4", "b2c3d4e5", "c3d4e5f6", "d4e5f6a7"]
88
+ seen: dict[str, str] = {}
89
+
90
+ def take(jid: str) -> str:
91
+ if jid not in seen:
92
+ seen[jid] = seq[len(seen)] if len(seen) < len(seq) else f"{len(seen):08x}"
93
+ return seen[jid]
94
+
95
+ out = []
96
+ for line in text.splitlines(True):
97
+ def sub(m: re.Match[str]) -> str:
98
+ return m.group(1) + take(m.group(2)) + m.group(3)
99
+
100
+ line = re.sub(r"(dry-run job )([0-9a-f]{8})(\b)", sub, line)
101
+ line = re.sub(r'("id": ")([0-9a-f]{8})(")', sub, line)
102
+ line = re.sub(r"(jobs/)([0-9a-f]{8})(\.prompt)", sub, line)
103
+ line = re.sub(r"(headless-|cursor-route-)([0-9a-f]{8})(\b)", sub, line)
104
+ line = re.sub(r'("createdAt": ")[^"]+(")', r"\g<1>2026-08-13T00:00:00.000Z\2", line)
105
+ out.append(line)
106
+ open(dst, "w", encoding="utf-8").write("".join(out))
107
+ PY
108
+
109
+ echo "wrote $OUT"
@@ -0,0 +1,53 @@
1
+ $ cursor-route --version
2
+ 0.1.9
3
+
4
+ $ CURSOR_ROUTE_RELAXED=1 cursor-route health
5
+ cursor-route v0.1.9
6
+ health: OK
7
+
8
+ ✓ tmux ok
9
+ ✓ runtime bun ok
10
+ ✓ script(1) ok (tty log capture)
11
+ ✓ worker:grok ok (auth checked at first start — run grok login if jobs fail) @ ~/.grok/bin/grok
12
+ ✓ worker:claude-ds ok (claude-ds (DeepSeek shim); default model deepseek-v4-flash) @ ~/.local/bin/claude-ds
13
+ ✗ worker:openrouter OPENROUTER_API_KEY not set — export your OpenRouter key (easy lane model defaults to openrouter/free) @ node '~/Projects/cursor-route/dist/openrouter-run.js'
14
+ ✗ worker:deepseek dsh (@deepseek-ai/dsh) not found — install: npm i -g @deepseek-ai/dsh. Mid default remains claude-ds.
15
+ ✓ lane:mid DeepSeek proven (claude-ds (DeepSeek shim))
16
+ ✓ cursor_cli optional ok (agent on PATH) — v0 supervisor is Cursor skill, not CLI
17
+ ✓ relaxed CURSOR_ROUTE_RELAXED=1 — tmux optional (headless OK)
18
+ ✓ jobs_dir ~/.local/share/cursor-route/jobs
19
+
20
+ $ cursor-route start --lane mid --model flash --dry-run "Add a unit test for shellQuote"
21
+ dry-run job a1b2c3d4
22
+ worker: claude-ds
23
+ model: flash
24
+ command: cd '~/Projects/cursor-route' && '~/.local/bin/claude-ds' -PromptFile '~/.local/share/cursor-route/jobs/a1b2c3d4.prompt' -Model 'deepseek-v4-flash' --dangerously-skip-permissions
25
+
26
+ $ cursor-route start --lane easy --dry-run "Rewrite this FAQ answer in 3 sentences"
27
+ dry-run job b2c3d4e5
28
+ worker: openrouter
29
+ command: node '~/Projects/cursor-route/dist/openrouter-run.js' --prompt-file '~/.local/share/cursor-route/jobs/b2c3d4e5.prompt'
30
+
31
+ $ cursor-route start --lane hard --dry-run "Refactor auth module; run tests; report verify evidence"
32
+ dry-run job c3d4e5f6
33
+ worker: grok
34
+ command: '~/.grok/bin/grok' -p "$(cat '~/.local/share/cursor-route/jobs/c3d4e5f6.prompt')" --cwd '~/Projects/cursor-route' --no-auto-update --output-format plain --always-approve
35
+
36
+ $ cursor-route start --lane mid --dry-run --json "Add a failing test then make it pass"
37
+ {
38
+ "id": "d4e5f6a7",
39
+ "schema": "cursor-route.job.v1",
40
+ "status": "pending",
41
+ "worker": "claude-ds",
42
+ "lane": "mid",
43
+ "model": "flash",
44
+ "prompt": "Add a failing test then make it pass",
45
+ "cwd": "~/Projects/cursor-route",
46
+ "alwaysApprove": true,
47
+ "tmuxSession": "cursor-route-d4e5f6a7",
48
+ "createdAt": "2026-08-13T00:00:00.000Z",
49
+ "command": "cd '~/Projects/cursor-route' && '~/.local/bin/claude-ds' -PromptFile '~/.local/share/cursor-route/jobs/d4e5f6a7.prompt' -Model 'deepseek-v4-flash' --dangerously-skip-permissions"
50
+ }
51
+
52
+ $ cursor-route jobs --json
53
+ []
package/llms.txt CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Cursor stays the planner. DeepSeek (mid), Grok CLI (hard), and OpenRouter free models (easy) run parallel coding workers in tmux.
4
4
 
5
- MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route (latest **0.1.7**)
5
+ MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route (latest **0.1.9**)
6
6
  GitHub: https://github.com/cemini23/cursor-route
7
7
 
8
8
  ## FAQ
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-route",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) + OpenRouter easy lane are the parallel army \u2014 lane-aware /route orchestration in tmux.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,7 +18,7 @@ You are the **orchestrator**. Do **not** implement bulk code in this Cursor sess
18
18
  - Mid/hard implementation that should run on a subscription worker (Grok CLI / claude-ds)
19
19
  - Multi-file investigation that benefits from parallel panes
20
20
 
21
- **Do not steal federation `/route`.** Private Cemini `/route` (route-task → SIP → verify → Grok/claude-ds chain) is a different skill. This public skill only drives the `cursor-route` CLI.
21
+ **Do not steal federation `/route`.** Private Cemini `/route` (route-task → verify → Grok/claude-ds chain) is a different skill. This public skill only drives the `cursor-route` CLI.
22
22
 
23
23
  ## Lanes (public core)
24
24
 
@@ -46,10 +46,23 @@ cursor-route start --lane mid --model pro --dir "$PWD" "…"
46
46
 
47
47
  If `worker:grok` is ✗ on health, that is usually **auth** (`grok login` / `XAI_API_KEY`) — not the Pro stand-in case.
48
48
 
49
+ ## Experimental: --worker deepseek (dsh)
50
+
51
+ Official DeepSeek Harness headless as an opt-in worker — **not the mid default** (mid stays `claude-ds`).
52
+
53
+ ```bash
54
+ npm i -g @deepseek-ai/dsh
55
+ export DEEPSEEK_API_KEY=... # platform.deepseek.com
56
+ cursor-route start --worker deepseek --dir "$PWD" "…"
57
+ cursor-route start --worker deepseek --model pro --dir "$PWD" "…" # --model applies here too
58
+ ```
59
+
60
+ Health ✓ needs `dsh` on PATH and `DEEPSEEK_API_KEY` set. The adapter pins `--model` via a per-job `--patch` (never touches `~/.dsh/settings.yaml`); always-approve → `DSH_PERMISSION_MODE=danger-full-access`, `--ask` → `workspace-write`. The key never enters the command or patch.
61
+
49
62
  ## Workflow
50
63
 
51
- 1. Run `cursor-route health` (or `CURSOR_ROUTE_RELAXED=1` for headless). If the **target worker** is unhealthy, fix before spawning.
52
- 2. Write a clear handoff prompt with **verify criteria** (no secrets, no LIVE Discord).
64
+ 1. Run `cursor-route health` (or `CURSOR_ROUTE_RELAXED=1` for headless). If the **target worker** is unhealthy, fix before spawning. If targeting **mid**, require `lane:mid` ✓ (or health JSON `lanes.mid.deepseek`) before spawn — `CURSOR_ROUTE_ALLOW_ANTHROPIC=1` is not DeepSeek proof.
65
+ 2. Write a clear handoff prompt with **Success criteria** + **Verify** + **NEVER** (no secrets, no LIVE Discord).
53
66
  3. Spawn:
54
67
 
55
68
  ```bash
@@ -57,24 +70,48 @@ cursor-route start --lane hard --dir "$PWD" "$(cat <<'EOF'
57
70
  ## Task
58
71
  ...
59
72
 
73
+ ## Success criteria
74
+ - [ ] ...
75
+
60
76
  ## Verify
61
77
  - [ ] ...
78
+
79
+ ## NEVER
80
+ - ...
62
81
  EOF
63
82
  )"
64
83
  ```
65
84
 
66
- Or `--worker grok` / `--worker claude-ds` / `--worker openrouter` (or `--lane easy`). Use `--no-tmux` only when tmux is unavailable.
85
+ Or `--worker grok` / `--worker claude-ds` / `--worker deepseek` (experimental) / `--worker openrouter` (or `--lane easy`). Use `--no-tmux` only when tmux is unavailable.
67
86
 
68
87
  4. Monitor: `cursor-route jobs --json` · `cursor-route capture <id>` · `cursor-route send <id> "…"` (tmux only).
69
- 5. Summarize worker results with **verify evidence** — no status-only “done”. If verify fails, `send` a correction or spawn a follow-up do not invent success.
88
+ 5. Summarize worker results with **verify evidence** — no status-only “done (see Verify / claim closeout). If verify fails, reconsider the plan/definition (not only retry) — `send` a correction or spawn a follow-up; do not invent success.
89
+
90
+ ## Verify / claim closeout
91
+
92
+ Verify criteria are an **external eval contract** (AutoDesign pattern), fixed by the parent — not a checklist the worker may rewrite:
93
+
94
+ - Workers must **not rewrite Success criteria / Verify** to claim done
95
+ - Parent closeout is an **evidence tree**: report **spawn** (job id, worker, lane, model) + **execute** (status, exit) + **verify** (`capture` excerpt / exit). A single “done” scalar is not enough.
96
+ - Parent closes a job only on **capture / exit evidence** (`cursor-route capture <id>`, job exit status)
97
+ - `cursor-route status --json` `.evidence.verify.claim` stays `"unverified"` until the parent reads capture
98
+ - **activity ≠ verification** — busy panes, many tool calls, or long transcripts do not make a claim true
99
+
100
+ ## Eval & skill hygiene
101
+
102
+ - **External eval contract (AutoDesign):** do not rewrite Verify / Success criteria mid-run to make a failing job look green — capture + exit status are the contract (see Verify / claim closeout).
103
+ - **Skill misevolution:** do not auto-edit `route-orch` or promote skill variants from worker trajectories without operator HITL — write-time approval ≠ safe retrieval later.
104
+ - **On verify fail:** prefer reconsidering the plan/definition (wrong approach) over grinding the same tactic; attribute failure to stage when possible (spawn vs execute vs verify).
70
105
 
71
106
  ## Always-approve
72
107
 
73
- Defaults on for workers. Opt out: `cursor-route start … --ask` or `CURSOR_ROUTE_ASK=1`.
108
+ Defaults on for workers. Opt out: `cursor-route start … --ask` or `CURSOR_ROUTE_ASK=1`. Always-approve is for **coding worktrees only** — it does not authorize LIVE Discord, trading, or irreversible SaaS.
74
109
 
75
110
  ## Anti-patterns
76
111
 
77
112
  - Do not paste API keys / private keys into prompts or `send`
78
- - Do not claim DeepSeek-native harness until that adapter shipstoday is **claude-ds**
79
- - Do not open-source or dump private Cemini `agent-toolkit` paths into public handoffs
113
+ - Do not claim the official DeepSeek harness (`@deepseek-ai/dsh`) is the mid default `--worker deepseek` is an opt-in experiment (cheap to abandon), not a product fork; mid stays **claude-ds**
114
+ - Do not fork a second mid harness
115
+ - Do not open-source or dump private cemini `agent-toolkit` paths into public handoffs
80
116
  - Do not mark done without reading `capture` / exit status
117
+ - When editing this skill itself, treat changes as **skill-evolution** — do not auto-promote harmful instructions; prefer **HITL** (no unattended promote from worker trajectories)
@@ -124,6 +124,25 @@ function resolveClaudeDs(): { binary: string; mode: string } | null {
124
124
  return null;
125
125
  }
126
126
 
127
+ /**
128
+ * Mid lane is proven DeepSeek only via shim (`claude-ds` / `deepseek-claude`)
129
+ * or stock `claude` routed to DeepSeek. The Anthropic escape hatch is not proof.
130
+ */
131
+ export function isMidDeepSeekProven(): boolean {
132
+ const resolved = resolveClaudeDs();
133
+ if (!resolved) return false;
134
+ return !resolved.mode.startsWith("claude → Anthropic");
135
+ }
136
+
137
+ /** Human detail for health `lane:mid` (mode when proven; install hint otherwise). */
138
+ export function midDeepSeekProofDetail(): string {
139
+ const resolved = resolveClaudeDs();
140
+ if (!resolved || resolved.mode.startsWith("claude → Anthropic")) {
141
+ return "mid not proven DeepSeek — install claude-ds / set ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic. CURSOR_ROUTE_ALLOW_ANTHROPIC=1 is not proof.";
142
+ }
143
+ return `DeepSeek proven (${resolved.mode})`;
144
+ }
145
+
127
146
  function pickModel(requested?: DsModelAlias, modelId?: string): { alias: DsModelAlias; id: string } {
128
147
  if (modelId) {
129
148
  const alias = requested ?? resolveDsModel(modelId).alias;
@@ -1,24 +1,158 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, writeFileSync, unlinkSync } from "node:fs";
1
3
  import type { Adapter, WorkerHealth } from "./types.ts";
4
+ import { shellQuote } from "../util.ts";
5
+ import {
6
+ DS_MODEL_IDS,
7
+ resolveDsModel,
8
+ type DsModelAlias,
9
+ } from "../config.ts";
2
10
 
3
11
  /**
4
- * Reserved slot for the official DeepSeek coding harness when it ships.
5
- * Mid lane stays on claude-ds until then do not route jobs here.
12
+ * Experimental official DeepSeek Harness (`dsh`, npm @deepseek-ai/dsh) as a
13
+ * coding worker `dsh --profile headless` with a per-job Cordis patch that
14
+ * pins the model. Mid lane stays on claude-ds; this is an opt-in worker only
15
+ * (`--worker deepseek`), not a mid replacement.
16
+ *
17
+ * We never write ~/.dsh/settings.yaml (parallel jobs would race) and never
18
+ * put DEEPSEEK_API_KEY in the command or patch — the key travels via plan.env.
6
19
  */
20
+
21
+ function findDsh(): string | null {
22
+ // Env override lets tests pin a fake dsh — but it must exist, so a stale
23
+ // override cannot pass health with a dangling path.
24
+ const override = process.env.CURSOR_ROUTE_DSH_BIN;
25
+ if (override) return existsSync(override) ? override : null;
26
+ try {
27
+ return (
28
+ execSync("command -v dsh", {
29
+ encoding: "utf8",
30
+ stdio: ["ignore", "pipe", "ignore"],
31
+ // Bun may ignore mutated process.env.PATH unless env is passed explicitly
32
+ env: { ...process.env },
33
+ }).trim() || null
34
+ );
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /** Same resolution as claude-ds: passed model/modelId wins, else env, else Flash. */
41
+ function pickModel(
42
+ requested?: DsModelAlias,
43
+ modelId?: string,
44
+ ): { alias: DsModelAlias; id: string } {
45
+ if (modelId) {
46
+ const alias = requested ?? resolveDsModel(modelId).alias;
47
+ return { alias, id: modelId };
48
+ }
49
+ if (requested) {
50
+ return { alias: requested, id: DS_MODEL_IDS[requested] };
51
+ }
52
+ // Env default (startJob normally resolves this; kept for direct buildLaunch callers)
53
+ return resolveDsModel(process.env.CURSOR_ROUTE_DS_MODEL || process.env.ANTHROPIC_MODEL);
54
+ }
55
+
56
+ /** Whitelist model ids before interpolating into YAML (no newlines / injection). */
57
+ function assertPatchModelId(modelId: string): string {
58
+ if (!/^[a-z0-9][a-z0-9.\-[\]]*$/i.test(modelId)) {
59
+ throw new Error(`Invalid DeepSeek model id for dsh patch: ${modelId}`);
60
+ }
61
+ return modelId;
62
+ }
63
+
64
+ /** Per-job Cordis patch path. Never reuse the prompt path (would overwrite it). */
65
+ export function patchPathForPrompt(promptFile: string): string {
66
+ return promptFile.endsWith(".prompt")
67
+ ? promptFile.replace(/\.prompt$/, ".dsh-patch.yml")
68
+ : `${promptFile}.dsh-patch.yml`;
69
+ }
70
+
71
+ /** Per-job Cordis patch (whole-row replace). `name` is required or dsh silently skips. */
72
+ function patchYaml(modelId: string): string {
73
+ const id = assertPatchModelId(modelId);
74
+ return [
75
+ "- id: agent-default-model",
76
+ " name: '@deepseek-ai/dsh-agent-default-model'",
77
+ " config:",
78
+ " provider: deepseek-official",
79
+ ` model: '${id}'`,
80
+ ].join("\n") + "\n";
81
+ }
82
+
7
83
  export const deepseekAdapter: Adapter = {
8
84
  kind: "deepseek",
9
- label: "Official DeepSeek harness (unreleased)",
85
+ label: "Official DeepSeek Harness (dsh)",
10
86
  health(): WorkerHealth {
87
+ const binary = findDsh();
88
+ if (!binary) {
89
+ return {
90
+ worker: "deepseek",
91
+ ok: false,
92
+ binary: null,
93
+ detail:
94
+ "dsh (@deepseek-ai/dsh) not found — install: npm i -g @deepseek-ai/dsh. Mid default remains claude-ds.",
95
+ };
96
+ }
97
+ if (!process.env.DEEPSEEK_API_KEY) {
98
+ return {
99
+ worker: "deepseek",
100
+ ok: false,
101
+ binary,
102
+ detail:
103
+ "DEEPSEEK_API_KEY not set — export your DeepSeek API key to use dsh (@deepseek-ai/dsh). Mid default remains claude-ds.",
104
+ };
105
+ }
11
106
  return {
12
107
  worker: "deepseek",
13
- ok: false,
14
- binary: null,
15
- detail:
16
- "unreleased — mid lane uses claude-ds (DeepSeek behind Claude Code). See README.",
108
+ ok: true,
109
+ binary,
110
+ detail: "ok (dsh @deepseek-ai/dsh headless; mid default remains claude-ds)",
17
111
  };
18
112
  },
19
- buildLaunch() {
20
- throw new Error(
21
- "Official DeepSeek harness is not available yet use --lane mid / --worker claude-ds",
22
- );
113
+ buildLaunch({ promptFile, cwd, alwaysApprove, model, modelId, dryRun }) {
114
+ // Missing dsh is tolerated here so `--dry-run` can still print the command;
115
+ // real starts are gated by the health preflight (binary + DEEPSEEK_API_KEY).
116
+ const binary = findDsh() || "dsh";
117
+ const choice = pickModel(model, modelId);
118
+
119
+ // Per-job patch next to the prompt file (never touch ~/.dsh/settings.yaml).
120
+ const patchFile = patchPathForPrompt(promptFile);
121
+ writeFileSync(patchFile, patchYaml(choice.id), { mode: 0o600 });
122
+ if (dryRun) {
123
+ // Dry-run keeps no durable artifacts (jobs.ts removes the prompt likewise).
124
+ try {
125
+ unlinkSync(patchFile);
126
+ } catch {
127
+ /* ignore */
128
+ }
129
+ }
130
+
131
+ // Launcher flags before the task; prompt inlined via cat (never the key).
132
+ const parts = [
133
+ shellQuote(binary),
134
+ "--profile",
135
+ "headless",
136
+ "--patch",
137
+ shellQuote(patchFile),
138
+ `"$(cat ${shellQuote(promptFile)})"`,
139
+ ];
140
+
141
+ const ask = process.env.CURSOR_ROUTE_ASK === "1";
142
+ const skip = alwaysApprove && !ask;
143
+ const env: Record<string, string> = {
144
+ DSH_PERMISSION_MODE: skip ? "danger-full-access" : "workspace-write",
145
+ };
146
+ // Key travels via env only — never interpolated into command or patch.
147
+ if (process.env.DEEPSEEK_API_KEY) {
148
+ env.DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY;
149
+ }
150
+
151
+ return {
152
+ worker: "deepseek",
153
+ command: `cd ${shellQuote(cwd)} && ${parts.join(" ")}`,
154
+ alwaysApprove: skip,
155
+ env,
156
+ };
23
157
  },
24
158
  };
@@ -24,9 +24,11 @@ export interface Adapter {
24
24
  promptFile: string;
25
25
  cwd: string;
26
26
  alwaysApprove: boolean;
27
- /** Mid-lane DeepSeek flash|pro (ignored by other workers / Anthropic escape hatch). */
27
+ /** Mid-lane DeepSeek flash|pro (claude-ds + deepseek; ignored by grok/openrouter / Anthropic escape hatch). */
28
28
  model?: DsModelAlias;
29
29
  /** Concrete DeepSeek model id for -Model/--model (preserves pro[1m]). */
30
30
  modelId?: string;
31
+ /** True on --dry-run: adapters may drop artifacts they just wrote (e.g. dsh patch). */
32
+ dryRun?: boolean;
31
33
  }): LaunchPlan;
32
34
  }