cursor-route 0.1.8 → 0.1.10

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.
package/dist/jobs.js CHANGED
@@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, unlink
2
2
  import { join, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { dirname } from "node:path";
5
- import { config, sessionName, defaultDsModelFromEnv, DS_MODEL_IDS, } from "./config.js";
5
+ import { config, sessionName, defaultDsModelFromEnv, DS_MODEL_IDS, openCodeModel, } from "./config.js";
6
6
  import { getAdapter } from "./adapters/index.js";
7
7
  import { spawn } from "node:child_process";
8
8
  import { spawnSync } from "node:child_process";
@@ -59,6 +59,29 @@ export function readJob(id) {
59
59
  export function writeJob(job) {
60
60
  writeSecure(jobPaths(job.id).json, JSON.stringify(job, null, 2));
61
61
  }
62
+ export function jobEvidence(job, sessionAlive) {
63
+ return {
64
+ spawn: {
65
+ jobId: job.id,
66
+ worker: job.worker,
67
+ lane: job.lane ?? null,
68
+ model: job.model ?? null,
69
+ startedAt: job.startedAt ?? job.createdAt,
70
+ },
71
+ execute: {
72
+ status: job.status,
73
+ exitCode: job.exitCode ?? null,
74
+ tmuxSession: job.tmuxSession,
75
+ pid: job.pid ?? null,
76
+ sessionAlive,
77
+ },
78
+ verify: {
79
+ captureHint: `cursor-route capture ${job.id}`,
80
+ logBytes: job.logBytes ?? null,
81
+ claim: "unverified",
82
+ },
83
+ };
84
+ }
62
85
  function pidAlive(pid) {
63
86
  try {
64
87
  process.kill(pid, 0);
@@ -73,6 +96,9 @@ function pidAlive(pid) {
73
96
  encoding: "utf8",
74
97
  stdio: ["ignore", "pipe", "ignore"],
75
98
  });
99
+ // spawnSync does not throw on EPERM (macOS sandbox); empty stdout is not "dead".
100
+ if (r.error || r.status !== 0)
101
+ return true;
76
102
  const state = (r.stdout || "").trim();
77
103
  return state !== "" && !state.startsWith("Z");
78
104
  }
@@ -219,14 +245,17 @@ export function startJob(opts) {
219
245
  writeSecure(paths.prompt, opts.prompt);
220
246
  let model;
221
247
  let modelId;
248
+ let dsAlias;
222
249
  if (worker === "claude-ds" || worker === "deepseek") {
223
250
  if (opts.model) {
251
+ dsAlias = opts.model;
224
252
  model = opts.model;
225
253
  modelId = opts.modelId ?? DS_MODEL_IDS[opts.model];
226
254
  }
227
255
  else {
228
256
  try {
229
257
  const choice = defaultDsModelFromEnv();
258
+ dsAlias = choice.alias;
230
259
  model = choice.alias;
231
260
  modelId = opts.modelId ?? choice.id;
232
261
  }
@@ -241,13 +270,29 @@ export function startJob(opts) {
241
270
  }
242
271
  }
243
272
  }
273
+ else if (worker === "opencode") {
274
+ try {
275
+ const ocModel = openCodeModel(opts.modelId);
276
+ model = ocModel;
277
+ modelId = ocModel;
278
+ }
279
+ catch (e) {
280
+ try {
281
+ unlinkSync(paths.prompt);
282
+ }
283
+ catch {
284
+ /* ignore */
285
+ }
286
+ return { ok: false, error: e.message };
287
+ }
288
+ }
244
289
  let plan;
245
290
  try {
246
291
  plan = adapter.buildLaunch({
247
292
  promptFile: paths.prompt,
248
293
  cwd,
249
294
  alwaysApprove,
250
- model,
295
+ model: dsAlias,
251
296
  modelId,
252
297
  dryRun: Boolean(opts.dryRun),
253
298
  });
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  title: cursor-route workspace — working brief (edit in place)
3
3
  repo: ~/Projects/cursor-route
4
- npm: cursor-route@0.1.8 (LIVE on npm latest)
4
+ npm: cursor-route@0.1.10 (unreleased OpenCode worker)
5
5
  created: 2026-08-12
6
- updated: 2026-08-14
6
+ updated: 2026-08-21
7
7
  ---
8
8
 
9
9
  # cursor-route — living brief
@@ -21,10 +21,11 @@ Cursor Agent plans. Workers run in tmux via `cursor-route`:
21
21
  | `easy` | OpenRouter free | Wording / drafts — non-secret prompts only |
22
22
  | `mid` | claude-ds (DeepSeek behind Claude Code) | Default implement (**Flash**; `--model pro` when needed) |
23
23
  | `hard` | Grok CLI | Hard implement |
24
+ | opt-in | OpenCode | `--worker opencode` coding agent on Zen free models (default `opencode/big-pickle`) |
24
25
 
25
- Always-approve on (`--ask` / `CURSOR_ROUTE_ASK=1` to opt out). Jobs live in `~/.local/share/cursor-route/jobs`, not in this clone.
26
+ Always-approve on for coding worktrees (`--ask` / `CURSOR_ROUTE_ASK=1` to opt out) — not LIVE Discord/trading. Jobs live in `~/.local/share/cursor-route/jobs`, not in this clone.
26
27
 
27
- Install: `npm i -g cursor-route` → **0.1.8**. Release notes: [CHANGELOG.md](../../CHANGELOG.md).
28
+ Install: `npm i -g cursor-route` → **0.1.9** live; **0.1.10** unreleased (OpenCode worker). Release notes: [CHANGELOG.md](../../CHANGELOG.md).
28
29
 
29
30
  ## Open (edit / check off)
30
31
 
@@ -36,6 +37,9 @@ Install: `npm i -g cursor-route` → **0.1.8**. Release notes: [CHANGELOG.md](..
36
37
  - [x] **Official DeepSeek Harness** — `deepseek` adapter slot present; `@deepseek-ai/dsh` 0.1.0-rc.6 (2026-08-13, github.com/deepseek-ai/deepseek-harness, MIT) is a developer-preview plugin kernel (web UI + `dsh --profile headless "job"`), not a Claude Code replacement; mid does not swap
37
38
  - [x] **0.1.7 debug fixes** — env default wired through `startJob`; Anthropic hatch omits DS ids; preserve `[1m]`
38
39
  - [x] **Experimental `--worker deepseek`** — 0.1.8: real dsh adapter (`dsh --profile headless` + per-job Cordis patch pins the model; never writes `~/.dsh/settings.yaml`). Always-approve → `DSH_PERMISSION_MODE=danger-full-access`, `--ask` → `workspace-write`; key via env only. Health ✓ needs `dsh` + `DEEPSEEK_API_KEY` (override `CURSOR_ROUTE_DSH_BIN`). Mid stays **claude-ds**.
40
+ - [x] **route-orch brief steals (2026-08-14)** — AutoDesign / misevolution / Vero habits into the public skill: **Verify / claim closeout** (external eval contract; activity ≠ verification), **Eval & skill hygiene** (mid-run Verify-rewrite ban; skill misevolution HITL — no auto-promotion of worker-trajectory variants; verify-fail → reconsider plan/definition + stage attribution spawn/execute/verify); handoff shape Success criteria + Verify + NEVER; skills synced; mid stays claude-ds.
41
+ - [x] **Health proves mid DeepSeek + evidence tree** — 0.1.9: `lane:mid` ✓ only when DeepSeek is proven (shim or DeepSeek `ANTHROPIC_BASE_URL`; Anthropic hatch is not proof); health JSON `lanes.mid`; `status --json` evidence tree (`spawn` / `execute` / `verify.claim=unverified`); skill health-before-mid + evidence-tree closeout. Mid stays **claude-ds**.
42
+ - [x] **Opt-in `--worker opencode`** — 0.1.10: `opencode run --dir` + `--model` (default Zen free `opencode/big-pickle` / `--model free`); always-approve → `--auto`; `--ask` omits it; never rewrites `~/.config/opencode/opencode.json`. Health ✓ needs `opencode` on PATH (`CURSOR_ROUTE_OPENCODE_BIN`). Mid stays **claude-ds**; easy stays OpenRouter chat. Free Zen may log/train — non-secret prompts.
39
43
  - [ ] **Hero GIF** — still outstanding; dry-run fixture ships as the substitute for now (`docs/fixtures/hero-demo.log` — see `docs/DEMO_GIF.md`)
40
44
  - [x] **Do not** paste private `ROUTE_KIT`, SIP, prod paths, or hang-watchdog env into this public repo
41
45
 
@@ -48,6 +52,7 @@ Install: `npm i -g cursor-route` → **0.1.8**. Release notes: [CHANGELOG.md](..
48
52
  | `src/adapters/deepseek.ts` | Experimental dsh worker (`--worker deepseek`; mid stays claude-ds) |
49
53
  | `src/adapters/grok.ts` | Hard worker |
50
54
  | `src/adapters/openrouter.ts` | Easy worker |
55
+ | `src/adapters/opencode.ts` | Opt-in OpenCode worker (`--worker opencode`; mid stays claude-ds) |
51
56
  | `skills/route-orch/SKILL.md` | Cursor skill — spawn CLI, do not implement in-session |
52
57
  | `CHANGELOG.md` | Release notes |
53
58
  | `SECURITY.md` | Secret refuse gate |
@@ -64,3 +69,6 @@ Install: `npm i -g cursor-route` → **0.1.8**. Release notes: [CHANGELOG.md](..
64
69
  | 2026-08-13 | GPTSOL fixes: scrub inherited env/paths, stable job ids, ignore `docs/briefs/handoffs/`, GIF checkbox wording. |
65
70
  | 2026-08-14 | DeepSeek Harness eval: `@deepseek-ai/dsh` 0.1.0-rc.6 is a developer-preview plugin kernel, not a mid replacement; `--worker deepseek` stays unhealthy; mid remains claude-ds (docs-only, no version bump). |
66
71
  | 2026-08-14 | Experimental `--worker deepseek` wired to official dsh (headless + per-job patch + `DSH_PERMISSION_MODE` + key-via-env); `--model` applies to claude-ds + deepseek; mid stays claude-ds → 0.1.8 LIVE. |
72
+ | 2026-08-14 | route-orch brief steals (AutoDesign / misevolution / Vero): **Verify / claim closeout** + **Eval & skill hygiene**; handoff Success criteria + Verify + NEVER; both skill copies synced; mid stays claude-ds. Docs-only, no version bump. |
73
+ | 2026-08-18 | Health `lane:mid` + `lanes.mid` prove DeepSeek; status evidence tree (`verify.claim` stays unverified); skill health-before-mid + closeout tree; mid stays claude-ds → 0.1.9 LIVE. |
74
+ | 2026-08-21 | Opt-in `--worker opencode` (Zen free `opencode/big-pickle`, `--auto`, no config rewrite); mid stays claude-ds → 0.1.10. |
@@ -6,10 +6,10 @@ Simulated capture output for README / tweet assets — replace with a real GIF o
6
6
 
7
7
  ```
8
8
  $ cursor-route --version
9
- 0.1.8
9
+ 0.1.9
10
10
 
11
11
  $ cursor-route health
12
- cursor-route v0.1.8
12
+ cursor-route v0.1.9
13
13
  health: OK
14
14
  ✓ tmux
15
15
  ✓ runtime bun ok
@@ -18,6 +18,7 @@ health: OK
18
18
  ✓ worker:claude-ds
19
19
  ✓ worker:openrouter
20
20
  ✗ worker:deepseek dsh not found — mid default remains claude-ds
21
+ ✓ lane:mid DeepSeek proven (claude-ds (DeepSeek shim))
21
22
  ✓ jobs_dir ~/.local/share/cursor-route/jobs
22
23
 
23
24
  $ cursor-route start --lane mid "Add a failing test then make it pass"
@@ -1,8 +1,8 @@
1
1
  $ cursor-route --version
2
- 0.1.8
2
+ 0.1.9
3
3
 
4
4
  $ CURSOR_ROUTE_RELAXED=1 cursor-route health
5
- cursor-route v0.1.8
5
+ cursor-route v0.1.9
6
6
  health: OK
7
7
 
8
8
  ✓ tmux ok
@@ -12,6 +12,7 @@ health: OK
12
12
  ✓ worker:claude-ds ok (claude-ds (DeepSeek shim); default model deepseek-v4-flash) @ ~/.local/bin/claude-ds
13
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
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))
15
16
  ✓ cursor_cli optional ok (agent on PATH) — v0 supervisor is Cursor skill, not CLI
16
17
  ✓ relaxed CURSOR_ROUTE_RELAXED=1 — tmux optional (headless OK)
17
18
  ✓ jobs_dir ~/.local/share/cursor-route/jobs
package/llms.txt CHANGED
@@ -1,14 +1,14 @@
1
1
  # cursor-route
2
2
 
3
- > Cursor stays the planner. DeepSeek (mid), Grok CLI (hard), and OpenRouter free models (easy) run parallel coding workers in tmux.
3
+ > Cursor stays the planner. DeepSeek (mid), Grok CLI (hard), and OpenRouter free models (easy) run parallel coding workers in tmux. `--worker opencode` is an opt-in coding agent on OpenCode Zen free models.
4
4
 
5
- MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route (latest **0.1.8**)
5
+ MIT CLI + Cursor skill. npm: https://www.npmjs.com/package/cursor-route (latest **0.1.10**)
6
6
  GitHub: https://github.com/cemini23/cursor-route
7
7
 
8
8
  ## FAQ
9
9
 
10
10
  ### What is cursor-route?
11
- cursor-route is a public MIT CLI and Cursor skill that runs parallel coding workers in tmux while Cursor remains the planner. DeepSeek handles the mid lane, Grok CLI handles the hard lane, and OpenRouter free models handle the easy lane (wording/drafts, non-secret prompts only).
11
+ cursor-route is a public MIT CLI and Cursor skill that runs parallel coding workers in tmux while Cursor remains the planner. DeepSeek handles the mid lane, Grok CLI handles the hard lane, and OpenRouter free models handle the easy lane (wording/drafts, non-secret prompts only). `--worker opencode` is an opt-in coding agent (default `opencode/big-pickle`) — not a lane default.
12
12
 
13
13
  ### How is this different from Codex orchestrator?
14
14
  It uses the familiar strategist and worker-pane shape, but it is not a Codex clone. cursor-route uses Cursor as the planner and DeepSeek plus Grok CLI as workers. Codex is not required.
@@ -22,15 +22,19 @@ No. The mid worker is DeepSeek. Claude Code is the harness, configured with `ANT
22
22
  | `--model flash` (default) | `deepseek-v4-flash` | Cheap mid execute |
23
23
  | `--model pro` | `deepseek-v4-pro` | Harder mid / Grok usage stand-in |
24
24
 
25
+ ### How do I use OpenCode free models?
26
+ Install OpenCode (`npm i -g opencode-ai`), run `opencode auth login`, then `cursor-route start --worker opencode "…"`. Default model is `opencode/big-pickle` (`--model free`). Mid stays `claude-ds`.
27
+
25
28
  ### How do I install?
26
29
  Run `npm i -g cursor-route`, install tmux if needed, then run `cursor-route health`. Copy the Cursor skill from `$(npm root -g)/cursor-route/skills/route-orch`. Source: https://github.com/cemini23/cursor-route
27
30
 
28
31
  ### Is it free?
29
- The cursor-route code is open source under MIT. It does not make the worker services free. Your costs depend on Cursor, DeepSeek API usage, and the Grok access or balance available to you. The easy lane can be free on OpenRouter's free-model route (`openrouter/free`).
32
+ The cursor-route code is open source under MIT. It does not make the worker services free. Your costs depend on Cursor, DeepSeek API usage, and the Grok access or balance available to you. The easy lane can be free on OpenRouter's free-model route (`openrouter/free`). `--worker opencode` can use OpenCode Zen free models.
30
33
 
31
34
  ## Install
32
35
 
33
36
  - npm: `npm i -g cursor-route`
34
37
  - Health: `cursor-route health`
35
38
  - Mid: `cursor-route start --lane mid "…"` (Flash) · `--model pro` when needed
39
+ - OpenCode (opt-in): `cursor-route start --worker opencode "…"`
36
40
  - Skill: `/route-orch` (Cursor)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cursor-route",
3
- "version": "0.1.8",
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.",
3
+ "version": "0.1.10",
4
+ "description": "Cursor stays the brain. Grok CLI + DeepSeek (claude-ds) + OpenRouter easy lane + OpenCode (opt-in free) are the parallel army \u2014 lane-aware /route orchestration in tmux.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -41,6 +41,7 @@
41
41
  "deepseek",
42
42
  "claude-ds",
43
43
  "openrouter",
44
+ "opencode",
44
45
  "tmux",
45
46
  "orchestrator",
46
47
  "agents",
@@ -2,10 +2,10 @@
2
2
  name: route-orch
3
3
  description: >-
4
4
  Delegate coding work from Cursor to parallel Grok CLI / claude-ds (DeepSeek) /
5
- OpenRouter (easy lane) workers via cursor-route. Use when the user says
6
- /route-orch, spawn workers, parallel agents with cursor-route, or explicitly
7
- asks to outsource implementation to Grok/DeepSeek panes — not for private
8
- Cemini /route.
5
+ OpenRouter (easy lane) / OpenCode (opt-in free) workers via cursor-route. Use
6
+ when the user says /route-orch, spawn workers, parallel agents with
7
+ cursor-route, or explicitly asks to outsource implementation to
8
+ Grok/DeepSeek/OpenCode panes — not for private Cemini /route.
9
9
  ---
10
10
 
11
11
  # route-orch (cursor-route)
@@ -14,11 +14,11 @@ You are the **orchestrator**. Do **not** implement bulk code in this Cursor sess
14
14
 
15
15
  ## When to activate
16
16
 
17
- - User says `/route-orch`, `spawn workers`, or asks for parallel Grok/DeepSeek via **cursor-route**
18
- - Mid/hard implementation that should run on a subscription worker (Grok CLI / claude-ds)
17
+ - User says `/route-orch`, `spawn workers`, or asks for parallel Grok/DeepSeek/OpenCode via **cursor-route**
18
+ - Mid/hard implementation that should run on a subscription worker (Grok CLI / claude-ds) or opt-in OpenCode free models
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
 
@@ -59,10 +59,24 @@ cursor-route start --worker deepseek --model pro --dir "$PWD" "…" # --model
59
59
 
60
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
61
 
62
+ ## Experimental: --worker opencode (free Zen)
63
+
64
+ OpenCode as an opt-in **coding agent** on OpenCode Zen free models — **not a lane default** (mid stays `claude-ds`; easy stays OpenRouter chat). Use this to cut Grok / DeepSeek usage on implement work.
65
+
66
+ ```bash
67
+ npm i -g opencode-ai
68
+ opencode auth login
69
+ cursor-route start --worker opencode --dir "$PWD" "…"
70
+ cursor-route start --worker opencode --model free --dir "$PWD" "…"
71
+ cursor-route start --worker opencode --model opencode/hy3-free --dir "$PWD" "…"
72
+ ```
73
+
74
+ Health ✓ needs `opencode` on PATH (override `CURSOR_ROUTE_OPENCODE_BIN`). Default model is `opencode/big-pickle` (`CURSOR_ROUTE_OPENCODE_MODEL` / `--model free`). Always-approve → `opencode run --auto`; `--ask` omits `--auto`. Never rewrites `~/.config/opencode/opencode.json`. Free Zen models may log/train — keep secrets off this worker (same refuse gate as easy).
75
+
62
76
  ## Workflow
63
77
 
64
- 1. Run `cursor-route health` (or `CURSOR_ROUTE_RELAXED=1` for headless). If the **target worker** is unhealthy, fix before spawning.
65
- 2. Write a clear handoff prompt with **verify criteria** (no secrets, no LIVE Discord).
78
+ 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.
79
+ 2. Write a clear handoff prompt with **Success criteria** + **Verify** + **NEVER** (no secrets, no LIVE Discord).
66
80
  3. Spawn:
67
81
 
68
82
  ```bash
@@ -70,24 +84,49 @@ cursor-route start --lane hard --dir "$PWD" "$(cat <<'EOF'
70
84
  ## Task
71
85
  ...
72
86
 
87
+ ## Success criteria
88
+ - [ ] ...
89
+
73
90
  ## Verify
74
91
  - [ ] ...
92
+
93
+ ## NEVER
94
+ - ...
75
95
  EOF
76
96
  )"
77
97
  ```
78
98
 
79
- Or `--worker grok` / `--worker claude-ds` / `--worker deepseek` (experimental) / `--worker openrouter` (or `--lane easy`). Use `--no-tmux` only when tmux is unavailable.
99
+ Or `--worker grok` / `--worker claude-ds` / `--worker deepseek` (experimental) / `--worker opencode` (opt-in free) / `--worker openrouter` (or `--lane easy`). Use `--no-tmux` only when tmux is unavailable.
80
100
 
81
101
  4. Monitor: `cursor-route jobs --json` · `cursor-route capture <id>` · `cursor-route send <id> "…"` (tmux only).
82
- 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.
102
+ 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.
103
+
104
+ ## Verify / claim closeout
105
+
106
+ Verify criteria are an **external eval contract** (AutoDesign pattern), fixed by the parent — not a checklist the worker may rewrite:
107
+
108
+ - Workers must **not rewrite Success criteria / Verify** to claim done
109
+ - 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.
110
+ - Parent closes a job only on **capture / exit evidence** (`cursor-route capture <id>`, job exit status)
111
+ - `cursor-route status --json` `.evidence.verify.claim` stays `"unverified"` until the parent reads capture
112
+ - **activity ≠ verification** — busy panes, many tool calls, or long transcripts do not make a claim true
113
+
114
+ ## Eval & skill hygiene
115
+
116
+ - **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).
117
+ - **Skill misevolution:** do not auto-edit `route-orch` or promote skill variants from worker trajectories without operator HITL — write-time approval ≠ safe retrieval later.
118
+ - **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).
83
119
 
84
120
  ## Always-approve
85
121
 
86
- Defaults on for workers. Opt out: `cursor-route start … --ask` or `CURSOR_ROUTE_ASK=1`.
122
+ 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.
87
123
 
88
124
  ## Anti-patterns
89
125
 
90
126
  - Do not paste API keys / private keys into prompts or `send`
91
- - Do not claim the official DeepSeek harness (`@deepseek-ai/dsh`) is the mid default — `--worker deepseek` is experimental only; mid stays **claude-ds**
92
- - Do not open-source or dump private Cemini `agent-toolkit` paths into public handoffs
127
+ - 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**
128
+ - Do not claim OpenCode is the mid default — `--worker opencode` is opt-in for free Zen (or other) models; mid stays **claude-ds**
129
+ - Do not fork a second mid harness
130
+ - Do not open-source or dump private cemini `agent-toolkit` paths into public handoffs
93
131
  - Do not mark done without reading `capture` / exit status
132
+ - 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;
@@ -4,12 +4,14 @@ import { grokAdapter } from "./grok.ts";
4
4
  import { claudeDsAdapter } from "./claude-ds.ts";
5
5
  import { openRouterAdapter } from "./openrouter.ts";
6
6
  import { deepseekAdapter } from "./deepseek.ts";
7
+ import { opencodeAdapter } from "./opencode.ts";
7
8
 
8
9
  const registry: Record<WorkerKind, Adapter> = {
9
10
  grok: grokAdapter,
10
11
  "claude-ds": claudeDsAdapter,
11
12
  openrouter: openRouterAdapter,
12
13
  deepseek: deepseekAdapter,
14
+ opencode: opencodeAdapter,
13
15
  };
14
16
 
15
17
  export function getAdapter(worker: WorkerKind): Adapter {
@@ -0,0 +1,238 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdirSync, writeFileSync, chmodSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { opencodeAdapter } from "./opencode.ts";
6
+ import { startJob } from "../jobs.ts";
7
+ import {
8
+ openCodeModel,
9
+ assertOpenCodeModel,
10
+ OPENCODE_DEFAULT_MODEL,
11
+ } from "../config.ts";
12
+
13
+ describe("openCodeModel", () => {
14
+ test("defaults to OpenCode Zen Big Pickle", () => {
15
+ const prev = process.env.CURSOR_ROUTE_OPENCODE_MODEL;
16
+ delete process.env.CURSOR_ROUTE_OPENCODE_MODEL;
17
+ try {
18
+ expect(openCodeModel()).toBe("opencode/big-pickle");
19
+ expect(openCodeModel("")).toBe(OPENCODE_DEFAULT_MODEL);
20
+ expect(openCodeModel("free")).toBe("opencode/big-pickle");
21
+ } finally {
22
+ if (prev === undefined) delete process.env.CURSOR_ROUTE_OPENCODE_MODEL;
23
+ else process.env.CURSOR_ROUTE_OPENCODE_MODEL = prev;
24
+ }
25
+ });
26
+
27
+ test("env override wins for empty/free", () => {
28
+ const prev = process.env.CURSOR_ROUTE_OPENCODE_MODEL;
29
+ process.env.CURSOR_ROUTE_OPENCODE_MODEL = "opencode/hy3-free";
30
+ try {
31
+ expect(openCodeModel()).toBe("opencode/hy3-free");
32
+ expect(openCodeModel("free")).toBe("opencode/hy3-free");
33
+ } finally {
34
+ if (prev === undefined) delete process.env.CURSOR_ROUTE_OPENCODE_MODEL;
35
+ else process.env.CURSOR_ROUTE_OPENCODE_MODEL = prev;
36
+ }
37
+ });
38
+
39
+ test("explicit provider/model wins over env", () => {
40
+ const prev = process.env.CURSOR_ROUTE_OPENCODE_MODEL;
41
+ process.env.CURSOR_ROUTE_OPENCODE_MODEL = "opencode/hy3-free";
42
+ try {
43
+ expect(openCodeModel("opencode/mimo-v2.5-free")).toBe("opencode/mimo-v2.5-free");
44
+ } finally {
45
+ if (prev === undefined) delete process.env.CURSOR_ROUTE_OPENCODE_MODEL;
46
+ else process.env.CURSOR_ROUTE_OPENCODE_MODEL = prev;
47
+ }
48
+ });
49
+
50
+ test("accepts OpenRouter-style extra slash and :free", () => {
51
+ expect(assertOpenCodeModel("openrouter/qwen/qwen3-coder:free")).toBe(
52
+ "openrouter/qwen/qwen3-coder:free",
53
+ );
54
+ });
55
+
56
+ test("rejects injection / flash|pro aliases", () => {
57
+ expect(() => openCodeModel("flash")).toThrow(/provider\/model/);
58
+ expect(() => openCodeModel("x\n--evil")).toThrow(/provider\/model/);
59
+ expect(() => openCodeModel("opencode/big pickle")).toThrow(/provider\/model/);
60
+ });
61
+ });
62
+
63
+ describe("opencode adapter", () => {
64
+ const makeFake = (suffix: string) => {
65
+ const dir = join(tmpdir(), `cr-oc-${process.pid}-${suffix}`);
66
+ mkdirSync(dir, { recursive: true });
67
+ const bin = join(dir, "opencode");
68
+ writeFileSync(bin, "#!/bin/sh\necho fake-opencode\n");
69
+ chmodSync(bin, 0o755);
70
+ return { dir, bin };
71
+ };
72
+
73
+ const withEnv = (patch: Record<string, string | undefined>, fn: () => void) => {
74
+ const prev = new Map<string, string | undefined>();
75
+ for (const k of Object.keys(patch)) {
76
+ prev.set(k, process.env[k]);
77
+ const v = patch[k];
78
+ if (v === undefined) delete process.env[k];
79
+ else process.env[k] = v;
80
+ }
81
+ try {
82
+ fn();
83
+ } finally {
84
+ for (const [k, v] of prev) {
85
+ if (v === undefined) delete process.env[k];
86
+ else process.env[k] = v;
87
+ }
88
+ }
89
+ };
90
+
91
+ test("health: no binary → not ok, install hint names npm package", () => {
92
+ withEnv(
93
+ { CURSOR_ROUTE_OPENCODE_BIN: join(tmpdir(), `missing-oc-${process.pid}`) },
94
+ () => {
95
+ const h = opencodeAdapter.health();
96
+ expect(h.ok).toBe(false);
97
+ expect(h.detail).toMatch(/npm i -g opencode-ai/);
98
+ },
99
+ );
100
+ });
101
+
102
+ test("health: binary → ok (auth at first start)", () => {
103
+ const { bin } = makeFake("ok");
104
+ withEnv({ CURSOR_ROUTE_OPENCODE_BIN: bin }, () => {
105
+ const h = opencodeAdapter.health();
106
+ expect(h.ok).toBe(true);
107
+ expect(h.binary).toBe(bin);
108
+ expect(h.detail).toContain("opencode/big-pickle");
109
+ });
110
+ rmSync(join(tmpdir(), `cr-oc-${process.pid}-ok`), { recursive: true, force: true });
111
+ });
112
+
113
+ test("buildLaunch: default free model + --auto; prompt via cat", () => {
114
+ const { dir, bin } = makeFake("launch");
115
+ const promptFile = join(dir, "job.prompt");
116
+ writeFileSync(promptFile, "ping");
117
+ withEnv(
118
+ {
119
+ CURSOR_ROUTE_OPENCODE_BIN: bin,
120
+ CURSOR_ROUTE_OPENCODE_MODEL: undefined,
121
+ CURSOR_ROUTE_ASK: undefined,
122
+ },
123
+ () => {
124
+ const plan = opencodeAdapter.buildLaunch({
125
+ promptFile,
126
+ cwd: dir,
127
+ alwaysApprove: true,
128
+ });
129
+ expect(plan.command).toContain("run");
130
+ expect(plan.command).toContain("--auto");
131
+ expect(plan.command).toContain("opencode/big-pickle");
132
+ expect(plan.command).toContain("$(cat");
133
+ expect(plan.command).toContain("--dir");
134
+ expect(plan.alwaysApprove).toBe(true);
135
+ expect(plan.command).not.toContain("npx");
136
+ },
137
+ );
138
+ rmSync(dir, { recursive: true, force: true });
139
+ });
140
+
141
+ test("buildLaunch: alwaysApprove false (--ask) omits --auto", () => {
142
+ const { dir, bin } = makeFake("ask");
143
+ const promptFile = join(dir, "job.prompt");
144
+ writeFileSync(promptFile, "ping");
145
+ withEnv({ CURSOR_ROUTE_OPENCODE_BIN: bin }, () => {
146
+ const plan = opencodeAdapter.buildLaunch({
147
+ promptFile,
148
+ cwd: dir,
149
+ alwaysApprove: false,
150
+ modelId: "opencode/hy3-free",
151
+ });
152
+ expect(plan.command).toContain("opencode/hy3-free");
153
+ expect(plan.command).not.toContain("--auto");
154
+ expect(plan.alwaysApprove).toBe(false);
155
+ });
156
+ rmSync(dir, { recursive: true, force: true });
157
+ });
158
+
159
+ test("buildLaunch: CURSOR_ROUTE_ASK=1 opts out even when alwaysApprove true", () => {
160
+ const { dir, bin } = makeFake("ask-env");
161
+ const promptFile = join(dir, "job.prompt");
162
+ writeFileSync(promptFile, "ping");
163
+ withEnv({ CURSOR_ROUTE_OPENCODE_BIN: bin, CURSOR_ROUTE_ASK: "1" }, () => {
164
+ const plan = opencodeAdapter.buildLaunch({
165
+ promptFile,
166
+ cwd: dir,
167
+ alwaysApprove: true,
168
+ });
169
+ expect(plan.command).not.toContain("--auto");
170
+ expect(plan.alwaysApprove).toBe(false);
171
+ });
172
+ rmSync(dir, { recursive: true, force: true });
173
+ });
174
+
175
+ test("buildLaunch: missing binary falls back to plain `opencode` for dry-run", () => {
176
+ const { dir } = makeFake("nobin");
177
+ const promptFile = join(dir, "job.prompt");
178
+ writeFileSync(promptFile, "ping");
179
+ const empty = join(dir, "empty");
180
+ mkdirSync(empty, { recursive: true });
181
+ withEnv(
182
+ {
183
+ CURSOR_ROUTE_OPENCODE_BIN: undefined,
184
+ PATH: empty,
185
+ CURSOR_ROUTE_OPENCODE_MODEL: undefined,
186
+ },
187
+ () => {
188
+ const plan = opencodeAdapter.buildLaunch({
189
+ promptFile,
190
+ cwd: dir,
191
+ alwaysApprove: true,
192
+ dryRun: true,
193
+ });
194
+ expect(plan.command).toContain("'opencode' run");
195
+ expect(plan.command).toContain("--auto");
196
+ },
197
+ );
198
+ rmSync(dir, { recursive: true, force: true });
199
+ });
200
+
201
+ test("--worker opencode dry-run succeeds with fake binary", () => {
202
+ const dir = join(tmpdir(), `cr-oc-start-${process.pid}`);
203
+ mkdirSync(dir, { recursive: true });
204
+ const bin = join(dir, "opencode");
205
+ writeFileSync(bin, "#!/bin/sh\necho fake-opencode\n");
206
+ chmodSync(bin, 0o755);
207
+ const prev = {
208
+ bin: process.env.CURSOR_ROUTE_OPENCODE_BIN,
209
+ model: process.env.CURSOR_ROUTE_OPENCODE_MODEL,
210
+ jobs: process.env.CURSOR_ROUTE_JOBS_DIR,
211
+ };
212
+ process.env.CURSOR_ROUTE_OPENCODE_BIN = bin;
213
+ delete process.env.CURSOR_ROUTE_OPENCODE_MODEL;
214
+ process.env.CURSOR_ROUTE_JOBS_DIR = join(dir, "jobs");
215
+ try {
216
+ const result = startJob({
217
+ prompt: "ping",
218
+ worker: "opencode",
219
+ dryRun: true,
220
+ });
221
+ expect(result.ok).toBe(true);
222
+ if (!result.ok) return;
223
+ expect(result.job.worker).toBe("opencode");
224
+ expect(result.job.model).toBe("opencode/big-pickle");
225
+ expect(result.command).toContain("run");
226
+ expect(result.command).toContain("--auto");
227
+ expect(result.command).toContain("$(cat");
228
+ } finally {
229
+ if (prev.bin === undefined) delete process.env.CURSOR_ROUTE_OPENCODE_BIN;
230
+ else process.env.CURSOR_ROUTE_OPENCODE_BIN = prev.bin;
231
+ if (prev.model === undefined) delete process.env.CURSOR_ROUTE_OPENCODE_MODEL;
232
+ else process.env.CURSOR_ROUTE_OPENCODE_MODEL = prev.model;
233
+ if (prev.jobs === undefined) delete process.env.CURSOR_ROUTE_JOBS_DIR;
234
+ else process.env.CURSOR_ROUTE_JOBS_DIR = prev.jobs;
235
+ rmSync(dir, { recursive: true, force: true });
236
+ }
237
+ });
238
+ });