@hecer/yoke 1.20.0 → 1.21.1

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.
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "yoke",
4
4
  "displayName": "Yoke",
5
- "version": "1.20.0",
5
+ "version": "1.21.1",
6
6
  "description": "Cross-agent coding harness for Claude, Codex, Gemini, Qwen, OpenCode, Kilo, Pi and Hermes: one curated skill canon plus mechanical safety gates and an autonomous loop via the yoke CLI.",
7
7
  "author": { "name": "HECer", "url": "https://github.com/HECer" },
8
8
  "homepage": "https://github.com/HECer/yoke#readme",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yoke",
3
- "version": "1.20.0",
3
+ "version": "1.21.1",
4
4
  "description": "Cross-agent coding discipline, mechanical gates, and release workflows for eight supported harnesses",
5
5
  "skills": "./canon/skills/",
6
6
  "hooks": "./hooks/hooks.json"
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.21.1 — 2026-10-03
4
+
5
+ ### Fixed
6
+ - Fix runner model and reasoning effort inheritance in `yoke setup` when switching runners: changing from Codex to another agent no longer leaks stale model or reasoning effort.
7
+ - Fix RTK hook planning condition in Claude retrofit planner on non-Windows/WSL environments.
8
+
9
+ ## 1.21.0 — 2026-10-03
10
+
11
+ ### Added
12
+ - Add first-class git worktree management commands: `yoke worktrees list [dir] [--all]` and `yoke worktrees prune [dir] [--all] [--force]` to discover, unregister, and safely prune orphaned, dead, or retained worktrees across a single repository or the entire registered fleet.
13
+ - Add `--clean-worktrees` flag to `yoke retrofit` and `yoke setup` to automatically prune dead story worktrees during project updates.
14
+ - Add user-configurable, per-agent model and reasoning effort selection in `yoke retrofit` and `yoke setup` (`--runner-model=<model>`, `--runner-reasoning=<effort>`, `--model=<agent>:<model>`, `--reasoning=<agent>:<effort>`, and interactive `--configure-models`). Reasoning effort is never forced or hardcoded during a retrofit unless explicitly chosen.
15
+ - Add compact zero-token loop status mode (`yoke loop status [dir] --compact`) returning a single-line summary (`state=... story=... progress=... phase=... updated=...`) to avoid burning thousands of replay tokens when agents check loop progress.
16
+ - Add native Windows RTK hook detection (`rtk.exe` on PATH or `%USERPROFILE%\.local\bin\rtk.exe`) in Claude retrofit planner without requiring WSL.
17
+
18
+ ### Fixed
19
+ - Prevent exponential worktree accumulation in `.yoke/worktrees` and git metadata from consuming system disk space and causing IDE/runner OOM crashes.
20
+ - Make interactive question parser null-safe against empty inputs or aborted prompts.
21
+
3
22
  ## 1.20.0 — 2026-09-30
4
23
 
5
24
  ### Added
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
12
12
  ![Node.js 20+](https://img.shields.io/badge/node-%E2%89%A520-339933?logo=node.js&logoColor=white)
13
13
 
14
- <!-- yoke:version:start -->1.20.0<!-- yoke:version:end --> · <!-- yoke:tests:start -->1420<!-- yoke:tests:end --> test cases · <!-- yoke:skills:start -->34<!-- yoke:skills:end --> skills
14
+ <!-- yoke:version:start -->1.21.1<!-- yoke:version:end --> · <!-- yoke:tests:start -->1426<!-- yoke:tests:end --> test cases · <!-- yoke:skills:start -->34<!-- yoke:skills:end --> skills
15
15
 
16
16
  <!-- yoke:agents:start -->Claude | Codex | Gemini | Qwen | OpenCode | Kilo | Pi | Hermes<!-- yoke:agents:end -->
17
17
 
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ import { runNew } from './new/command.js';
17
17
  import { runPrdDraft, runPrdCheck } from './prd/command.js';
18
18
  import { runPrdAssess } from './prd/assess.js';
19
19
  import { runPrdDecompose } from './prd/decompose.js';
20
- import { runLoopCleanup } from './loop/cleanup.js';
20
+ import { runLoopCleanup, pruneWorktrees, pruneFleetWorktrees, listWorktrees, listFleetWorktrees } from './loop/cleanup.js';
21
21
  import { runFlowSmoke } from './smoke/command.js';
22
22
  import { maybeNotifyUpdate, currentYokeVersion } from './update/check.js';
23
23
  import { runUpgrade } from './update/upgrade.js';
@@ -62,6 +62,22 @@ export function runDesignScan(targetDir, opts) {
62
62
  console.log(`${label} — ✓`);
63
63
  return 0;
64
64
  }
65
+ export function parseKeyValueFlags(args, flagName) {
66
+ const result = {};
67
+ for (const arg of args) {
68
+ if (arg.startsWith(`--${flagName}=`)) {
69
+ const raw = arg.slice(flagName.length + 3);
70
+ const eqIdx = raw.indexOf(':');
71
+ if (eqIdx > 0) {
72
+ const key = raw.slice(0, eqIdx).trim();
73
+ const val = raw.slice(eqIdx + 1).trim();
74
+ if (key && val)
75
+ result[key] = val;
76
+ }
77
+ }
78
+ }
79
+ return result;
80
+ }
65
81
  export function parseQualityFlags(args) {
66
82
  const quality = args.includes('--quality') ? true : args.includes('--no-quality') ? false : undefined;
67
83
  const qualityUnbounded = args.includes('--quality-unbounded') || undefined;
@@ -185,9 +201,16 @@ export function main(argv) {
185
201
  console.error('Invalid --model-provider (expected deepseek,kimi)');
186
202
  return 1;
187
203
  }
204
+ const runnerModel = rest.find(a => a.startsWith('--runner-model='))?.slice('--runner-model='.length);
205
+ const runnerReasoning = rest.find(a => a.startsWith('--runner-reasoning='))?.slice('--runner-reasoning='.length);
206
+ const agentModels = parseKeyValueFlags(rest, 'model');
207
+ const agentReasoning = parseKeyValueFlags(rest, 'reasoning');
208
+ const cleanWorktrees = rest.includes('--clean-worktrees');
188
209
  return runSetup(targetDir, {
189
210
  modelProviders: modelProviders,
190
211
  host: hostArg, agents, runner: runnerArg,
212
+ runnerModel, runnerReasoning, agentModels, agentReasoning, cleanWorktrees,
213
+ configureModels: rest.includes('--configure-models'),
191
214
  codeGraph: graphArg,
192
215
  codeIntelligence: intelligenceArg,
193
216
  loop, routing, decisionPolicy: policyArg,
@@ -218,6 +241,47 @@ export function main(argv) {
218
241
  return 2;
219
242
  }
220
243
  }
244
+ case 'worktrees': {
245
+ const sub = rest[0] ?? 'list';
246
+ const targetDir = rest.slice(1).find(a => !a.startsWith('-')) ?? '.';
247
+ const all = rest.includes('--all');
248
+ const force = rest.includes('--force');
249
+ if (sub === 'list') {
250
+ if (all) {
251
+ const fleet = listFleetWorktrees();
252
+ for (const [name, list] of Object.entries(fleet)) {
253
+ console.log(`[${name}] ${list.length} worktree(s):`);
254
+ for (const wt of list)
255
+ console.log(` ${wt}`);
256
+ }
257
+ return 0;
258
+ }
259
+ const list = listWorktrees(targetDir);
260
+ console.log(`Worktrees in ${targetDir} (${list.length}):`);
261
+ for (const wt of list)
262
+ console.log(` ${wt}`);
263
+ return 0;
264
+ }
265
+ if (sub === 'prune') {
266
+ if (all) {
267
+ const fleet = pruneFleetWorktrees({ force });
268
+ let totalRemoved = 0;
269
+ for (const [name, res] of Object.entries(fleet)) {
270
+ if (res.removed.length > 0 || res.failed.length > 0) {
271
+ console.log(`[${name}] Removed: ${res.removed.length}, Failed: ${res.failed.length}`);
272
+ totalRemoved += res.removed.length;
273
+ }
274
+ }
275
+ console.log(`Fleet prune complete: ${totalRemoved} worktree(s) removed.`);
276
+ return 0;
277
+ }
278
+ const res = pruneWorktrees(targetDir, { force });
279
+ console.log(`Pruned ${res.removed.length} worktree(s) in ${targetDir}${res.failed.length > 0 ? ` (${res.failed.length} failed)` : ''}.`);
280
+ return res.failed.length === 0 ? 0 : 1;
281
+ }
282
+ console.log('usage: yoke worktrees <list|prune> [dir] [--all] [--force]');
283
+ return 1;
284
+ }
221
285
  case 'dashboard': {
222
286
  const port = rest.find(a => a.startsWith('--port='))?.slice('--port='.length);
223
287
  if (port !== undefined && (!Number.isInteger(Number(port)) || Number(port) < 0 || Number(port) > 65535)) {
@@ -319,6 +383,11 @@ export function main(argv) {
319
383
  case 'retrofit': {
320
384
  const targetDir = rest.find(a => !a.startsWith('-')) ?? '.';
321
385
  const loop = rest.includes('--loop');
386
+ const cleanWorktrees = rest.includes('--clean-worktrees');
387
+ const runnerModel = rest.find(a => a.startsWith('--runner-model='))?.slice('--runner-model='.length);
388
+ const runnerReasoning = rest.find(a => a.startsWith('--runner-reasoning='))?.slice('--runner-reasoning='.length);
389
+ const agentModels = parseKeyValueFlags(rest, 'model');
390
+ const agentReasoning = parseKeyValueFlags(rest, 'reasoning');
322
391
  const agentArg = rest.find(a => a.startsWith('--agent='))?.slice('--agent='.length);
323
392
  const all = [...SUPPORTED_AGENTS];
324
393
  const agents = !agentArg || agentArg === 'all'
@@ -338,7 +407,7 @@ export function main(argv) {
338
407
  console.error(`Invalid --code-intelligence value: ${ciArg} (expected off|shadow|active)`);
339
408
  return 1;
340
409
  }
341
- return runRetrofit(targetDir, { loop, agents, codeGraph, codeIntelligence: ciArg });
410
+ return runRetrofit(targetDir, { loop, agents, codeGraph, codeIntelligence: ciArg, cleanWorktrees, runnerModel, runnerReasoning, agentModels, agentReasoning });
342
411
  }
343
412
  case 'change': {
344
413
  const sub = rest[0];
@@ -387,7 +456,7 @@ export function main(argv) {
387
456
  return 0;
388
457
  }
389
458
  if (sub === 'status') {
390
- console.log(loopStatus(targetDir));
459
+ console.log(loopStatus(targetDir, undefined, { compact: rest.includes('--compact') }));
391
460
  return 0;
392
461
  }
393
462
  if (sub === 'pause') {
@@ -618,7 +687,7 @@ export function main(argv) {
618
687
  }
619
688
  return runLoopCommand(targetDir, { maxIterations: rawMax, agent, isolate, resumeWorktree: rest.includes('--resume-worktree'), parallel, parallelAuto: parallelArg === '--parallel=auto', reviewer, review, allowSelfReview, timeoutMinutes, json, routing, onAmbiguity: oaArg, decisionPolicy: dpArg, permissions, ...(explore ? { explore: true } : {}), ...(exploreIntervalMinutes !== undefined ? { exploreIntervalMinutes } : {}), ...(exploreLimit.milliseconds !== undefined ? { exploreLimitMs: exploreLimit.milliseconds } : {}), ...qualityFlags.options });
620
689
  }
621
- console.log(`usage: yoke loop <on|off|status|pause|decision|answer|resume [--discard] [--explore] [--explore-interval=<minutes>] [--explore-limit=<Nh|Nd|Nw>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N]|cleanup [--remove-worktrees] [--discard-stale-recovery]|run [--max=N] [--explore] [--explore-interval=<minutes>] [--explore-limit=<Nh|Nd|Nw>] [--parallel=<auto|N>] [--runner=<${AGENT_LIST}>] [--reviewer=<${AGENT_LIST}>] [--review] [--allow-self-review] [--routing|--no-routing] [--isolate|--no-isolate] [--unsafe] [--timeout=<minutes>] [--decision-policy=<auto|critical>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N] [--json]> [targetDir]`);
690
+ console.log(`usage: yoke loop <on|off|status [--compact]|pause|decision|answer|resume [--discard] [--explore] [--explore-interval=<minutes>] [--explore-limit=<Nh|Nd|Nw>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N]|cleanup [--remove-worktrees] [--discard-stale-recovery]|run [--max=N] [--explore] [--explore-interval=<minutes>] [--explore-limit=<Nh|Nd|Nw>] [--parallel=<auto|N>] [--runner=<${AGENT_LIST}>] [--reviewer=<${AGENT_LIST}>] [--review] [--allow-self-review] [--routing|--no-routing] [--isolate|--no-isolate] [--unsafe] [--timeout=<minutes>] [--decision-policy=<auto|critical>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N] [--json]> [targetDir]`);
622
691
  return 1;
623
692
  }
624
693
  case 'new': {
@@ -764,7 +833,7 @@ export function main(argv) {
764
833
  return runUpgrade();
765
834
  default:
766
835
  console.log('Project workflows: yoke check [dir] [--json|--protect] | goal set|run|resume|pause|status|handoff|budget [dir] | projects add|list|remove | dashboard [dir] [--port=N]');
767
- console.log(`usage: yoke <setup [dir] | new <dir> [--idea="..."] | validate [canonDir] | retrofit [targetDir] [--agent=${AGENT_LIST}|all] [--code-graph=graphify|serena] [--code-intelligence=off|shadow|active] [--loop] | change <add|status> [dir] | code-intelligence-server --workspace=<dir> --mode=<mode> | prd <draft|check|assess|decompose> [dir] | loop <on|off|status|decision|answer|resume|run|cleanup> | context <init|status> | review [dir] | design-scan [dir] | flow-smoke [dir] | upgrade>`);
836
+ console.log(`usage: yoke <setup [dir] | new <dir> [--idea="..."] | validate [canonDir] | retrofit [targetDir] [--agent=${AGENT_LIST}|all] [--code-graph=graphify|serena] [--code-intelligence=off|shadow|active] [--clean-worktrees] [--runner-model=<model>] [--runner-reasoning=<effort>] [--model=<agent>:<model>] [--reasoning=<agent>:<effort>] [--loop] | worktrees <list|prune> [dir] [--all] [--force] | change <add|status> [dir] | code-intelligence-server --workspace=<dir> --mode=<mode> | prd <draft|check|assess|decompose> [dir] | loop <on|off|status|decision|answer|resume|run|cleanup> | context <init|status> | review [dir] | design-scan [dir] | flow-smoke [dir] | upgrade>`);
768
837
  return cmd ? 1 : 0;
769
838
  }
770
839
  }
@@ -8,6 +8,7 @@ import { killProcessForCleanup, killProcessTreeForCleanup } from './watchdog.js'
8
8
  import { cleanupClaims } from './claims.js';
9
9
  import { clearStatus } from './reporter.js';
10
10
  import { discardParallelRecoveryRecords } from './recovery.js';
11
+ import { listProjects } from '../dashboard/registry.js';
11
12
  // Reap orphaned runners PROJECT-SCOPED: kill only pids recorded in this project's
12
13
  // .yoke/runner.pid files (main dir + each worktree). Never by process-name or
13
14
  // command-line pattern — that takes down runners belonging to OTHER projects,
@@ -266,3 +267,102 @@ function markCandidateWorktreeRemoved(targetDir, worktree) {
266
267
  }
267
268
  }
268
269
  }
270
+ export function pruneWorktrees(targetDir, opts = {}) {
271
+ const result = { targetDir, removed: [], failed: [] };
272
+ const wtDir = join(targetDir, '.yoke', 'worktrees');
273
+ const candidates = new Set();
274
+ if (existsSync(wtDir)) {
275
+ try {
276
+ for (const entry of readdirSync(wtDir, { withFileTypes: true })) {
277
+ if (entry.isDirectory())
278
+ candidates.add(join(wtDir, entry.name));
279
+ }
280
+ }
281
+ catch { /* ignore read errors */ }
282
+ }
283
+ try {
284
+ const raw = execFileSync('git', ['worktree', 'list', '--porcelain'], { cwd: targetDir, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
285
+ const lines = raw.split(/\r?\n/);
286
+ for (const line of lines) {
287
+ if (line.startsWith('worktree ')) {
288
+ const wtPath = line.slice(9).trim();
289
+ if (wtPath.includes('.yoke/worktrees') || wtPath.includes('.yoke\\worktrees') || wtPath.includes('/.worktrees/') || wtPath.includes('\\.worktrees\\') || wtPath.includes('-story')) {
290
+ candidates.add(wtPath);
291
+ }
292
+ }
293
+ }
294
+ }
295
+ catch { /* not a git repo or git error */ }
296
+ for (const wtPath of candidates) {
297
+ try {
298
+ try {
299
+ execFileSync('git', ['worktree', 'remove', '--force', wtPath], { cwd: targetDir, stdio: ['ignore', 'pipe', 'pipe'] });
300
+ }
301
+ catch {
302
+ if (existsSync(wtPath)) {
303
+ rmSync(wtPath, { recursive: true, force: true });
304
+ }
305
+ }
306
+ markCandidateWorktreeRemoved(targetDir, wtPath);
307
+ result.removed.push(wtPath);
308
+ }
309
+ catch (e) {
310
+ result.failed.push(`${wtPath}: ${e.message}`);
311
+ }
312
+ }
313
+ try {
314
+ execFileSync('git', ['worktree', 'prune'], { cwd: targetDir, stdio: ['ignore', 'pipe', 'pipe'] });
315
+ }
316
+ catch { /* best-effort */ }
317
+ return result;
318
+ }
319
+ export function listWorktrees(targetDir) {
320
+ const wtDir = join(targetDir, '.yoke', 'worktrees');
321
+ const candidates = new Set();
322
+ if (existsSync(wtDir)) {
323
+ try {
324
+ for (const entry of readdirSync(wtDir, { withFileTypes: true })) {
325
+ if (entry.isDirectory())
326
+ candidates.add(join(wtDir, entry.name));
327
+ }
328
+ }
329
+ catch { /* ignore read errors */ }
330
+ }
331
+ try {
332
+ const raw = execFileSync('git', ['worktree', 'list', '--porcelain'], { cwd: targetDir, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
333
+ const lines = raw.split(/\r?\n/);
334
+ for (const line of lines) {
335
+ if (line.startsWith('worktree ')) {
336
+ const wtPath = line.slice(9).trim();
337
+ if (wtPath.includes('.yoke/worktrees') || wtPath.includes('.yoke\\worktrees') || wtPath.includes('/.worktrees/') || wtPath.includes('\\.worktrees\\') || wtPath.includes('-story')) {
338
+ candidates.add(wtPath);
339
+ }
340
+ }
341
+ }
342
+ }
343
+ catch { /* not a git repo or git error */ }
344
+ return [...candidates];
345
+ }
346
+ export function listFleetWorktrees() {
347
+ const projects = listProjects();
348
+ const results = {};
349
+ for (const proj of projects) {
350
+ if (proj.root && !proj.error && existsSync(proj.root)) {
351
+ const list = listWorktrees(proj.root);
352
+ if (list.length > 0) {
353
+ results[proj.name || proj.root] = list;
354
+ }
355
+ }
356
+ }
357
+ return results;
358
+ }
359
+ export function pruneFleetWorktrees(opts = {}) {
360
+ const projects = listProjects();
361
+ const fleetResults = {};
362
+ for (const proj of projects) {
363
+ if (proj.root && !proj.error && existsSync(proj.root)) {
364
+ fleetResults[proj.name || proj.root] = pruneWorktrees(proj.root, opts);
365
+ }
366
+ }
367
+ return fleetResults;
368
+ }
@@ -53,7 +53,7 @@ export function setLoopEnabled(targetDir, enabled) {
53
53
  config.loop = { ...config.loop, enabled };
54
54
  saveConfig(targetDir, config);
55
55
  }
56
- export function loopStatus(targetDir, now = () => new Date()) {
56
+ export function loopStatus(targetDir, now = () => new Date(), opts) {
57
57
  const config = loadConfig(targetDir);
58
58
  const enabled = config?.loop.enabled ?? false;
59
59
  const path = prdPath(targetDir);
@@ -62,6 +62,12 @@ export function loopStatus(targetDir, now = () => new Date()) {
62
62
  const p = progress(loadPrd(path));
63
63
  prog = `${p.passed}/${p.total} stories pass`;
64
64
  }
65
+ const st = readStatus(targetDir);
66
+ if (opts?.compact) {
67
+ if (!st)
68
+ return `state=${enabled ? 'enabled' : 'disabled'} prd="${prog}"`;
69
+ return `state=${st.state} story=${st.story ?? 'none'} progress=${st.progress.passed}/${st.progress.total} phase=${st.phase} updated=${relativeTime(st.updatedAt, now())}`;
70
+ }
65
71
  const sharedPoolLine = () => {
66
72
  try {
67
73
  const pool = sharedPoolStatus();
@@ -71,7 +77,6 @@ export function loopStatus(targetDir, now = () => new Date()) {
71
77
  return `Shared pool unavailable: ${error.message}`;
72
78
  }
73
79
  };
74
- const st = readStatus(targetDir);
75
80
  if (!st)
76
81
  return `Loop: ${enabled ? 'enabled' : 'disabled'}\nPRD: ${prog}\n${sharedPoolLine()}`;
77
82
  const head = `Loop: ${st.state.toUpperCase()}${st.story ? ` on ${st.story}${st.storyTitle ? ` "${st.storyTitle}"` : ''}` : ''}`;
@@ -8,9 +8,16 @@ import { ensureGitignore } from './gitignore.js';
8
8
  import { loadConfig, saveConfig, defaultConfig } from './config.js';
9
9
  import { loadManifest } from '../canon/manifest.js';
10
10
  import { detectHostAgent } from '../agents/host.js';
11
+ import { pruneWorktrees } from '../loop/cleanup.js';
11
12
  export function runRetrofit(targetDir, opts) {
12
13
  const canonDir = resolveCanonDir();
13
14
  const canonVersion = loadManifest(join(canonDir, 'manifest.yaml')).version;
15
+ if (opts.cleanWorktrees) {
16
+ const pruneRes = pruneWorktrees(targetDir);
17
+ if (pruneRes.removed.length > 0) {
18
+ console.log(`Pruned ${pruneRes.removed.length} orphaned worktree(s).`);
19
+ }
20
+ }
14
21
  const detection = detectProject(targetDir);
15
22
  const agents = opts.agents && opts.agents.length > 0
16
23
  ? opts.agents
@@ -26,6 +33,19 @@ export function runRetrofit(targetDir, opts) {
26
33
  ensureGitignore(targetDir);
27
34
  const priorAgents = existing?.agents ?? [];
28
35
  const mergedAgents = [...new Set([...priorAgents, ...agents])];
36
+ const runnerConfig = { ...(existing?.runner ?? {}) };
37
+ if (opts.runnerModel !== undefined)
38
+ runnerConfig.model = opts.runnerModel;
39
+ if (opts.runnerReasoning !== undefined)
40
+ runnerConfig.reasoningEffort = opts.runnerReasoning;
41
+ let updatedWorkers = existing?.routing?.workers;
42
+ if (updatedWorkers && (opts.agentModels || opts.agentReasoning)) {
43
+ updatedWorkers = updatedWorkers.map(w => {
44
+ const model = opts.agentModels?.[w.agent] ?? w.model;
45
+ const reasoningEffort = opts.agentReasoning?.[w.agent] ?? w.reasoningEffort;
46
+ return { ...w, ...(model ? { model } : {}), ...(reasoningEffort ? { reasoningEffort } : {}) };
47
+ });
48
+ }
29
49
  const config = {
30
50
  ...(existing ?? defaultConfig(canonVersion)),
31
51
  canonVersion,
@@ -33,6 +53,8 @@ export function runRetrofit(targetDir, opts) {
33
53
  loop: { ...existing?.loop, enabled: opts.loop },
34
54
  codeGraph,
35
55
  codeIntelligence: { ...(existing?.codeIntelligence ?? {}), mode: codeIntelligence },
56
+ ...(Object.keys(runnerConfig).length > 0 ? { runner: runnerConfig } : {}),
57
+ ...(existing?.routing && updatedWorkers ? { routing: { ...existing.routing, workers: updatedWorkers } } : {}),
36
58
  ...(existing?.design
37
59
  ? { design: existing.design }
38
60
  : detection.ui.detected ? { design: { mode: 'auto', max: 4 } } : {}),
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { loadManifest } from '../../canon/manifest.js';
4
4
  import { mcpServers, rtkInstruction } from '../tools.js';
5
- import { hasWsl } from '../wsl.js';
5
+ import { hasWsl, hasRtk } from '../wsl.js';
6
6
  import { detectGstack } from '../gstack.js';
7
7
  import { PRESERVE_SCAFFOLD } from '../preserve.js';
8
8
  import { skillPackageActions } from '../skill-actions.js';
@@ -22,7 +22,7 @@ This project uses the Yoke harness. Baseline instructions:
22
22
  ${rtkNote ? `\n${rtkNote}\n` : ''}${composeNote ? `\n${composeNote}\n` : ''}
23
23
  ${PRESERVE_SCAFFOLD}
24
24
  `;
25
- export function planClaude(canonDir, targetDir, wslAvailable = hasWsl(), codeGraph = 'graphify', gstackDetected = detectGstack(targetDir), codeIntelligence = 'off') {
25
+ export function planClaude(canonDir, targetDir, wslAvailable = (hasWsl() || hasRtk()), codeGraph = 'graphify', gstackDetected = detectGstack(targetDir), codeIntelligence = 'off', rtkAvailable) {
26
26
  const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
27
27
  const actions = [];
28
28
  for (const skill of manifest.skills) {
@@ -34,8 +34,8 @@ export function planClaude(canonDir, targetDir, wslAvailable = hasWsl(), codeGra
34
34
  content: readFileSync(join(canonDir, 'AGENTS.md'), 'utf8'),
35
35
  reason: 'baseline instructions',
36
36
  });
37
- // rtk: PreToolUse hook needs WSL on Windows; otherwise fall back to instruction mode.
38
- const rtkHookable = wslAvailable;
37
+ // rtk: PreToolUse hook works natively if rtk is available, or via WSL on Windows.
38
+ const rtkHookable = rtkAvailable !== undefined ? (wslAvailable || rtkAvailable) : wslAvailable;
39
39
  actions.push({
40
40
  kind: 'write',
41
41
  target: 'CLAUDE.md',
@@ -1,7 +1,6 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- // True only on Windows where a WSL distribution responds. Used to decide whether
3
- // rtk can use its transparent PreToolUse hook (needs WSL) or must fall back to
4
- // instruction mode. Never throws.
2
+ import { join } from 'node:path';
3
+ // True only on Windows where a WSL distribution responds.
5
4
  export function hasWsl() {
6
5
  if (process.platform !== 'win32')
7
6
  return false;
@@ -13,3 +12,24 @@ export function hasWsl() {
13
12
  return false;
14
13
  }
15
14
  }
15
+ // True if the rtk binary is available natively or on PATH. Never throws.
16
+ export function hasRtk() {
17
+ try {
18
+ const cmd = process.platform === 'win32' ? 'rtk.exe' : 'rtk';
19
+ execFileSync(cmd, ['--version'], { stdio: 'pipe', timeout: 3000 });
20
+ return true;
21
+ }
22
+ catch {
23
+ if (process.platform === 'win32') {
24
+ const home = process.env.USERPROFILE ?? process.env.HOME;
25
+ if (home) {
26
+ try {
27
+ execFileSync(join(home, '.local', 'bin', 'rtk.exe'), ['--version'], { stdio: 'pipe', timeout: 3000 });
28
+ return true;
29
+ }
30
+ catch { /* ignore */ }
31
+ }
32
+ }
33
+ return false;
34
+ }
35
+ }
@@ -55,6 +55,8 @@ function parseAgents(value, fallback) {
55
55
  return parsed.length > 0 ? [...new Set(parsed)] : fallback;
56
56
  }
57
57
  function yes(value, fallback) {
58
+ if (!value)
59
+ return fallback;
58
60
  const normalized = value.trim().toLowerCase();
59
61
  if (['y', 'yes', 'j', 'ja', 'true', '1'].includes(normalized))
60
62
  return true;
@@ -99,6 +101,10 @@ export async function runSetup(targetDir, opts = {}) {
99
101
  let runner = defaultRunner;
100
102
  let decisionPolicy = defaultPolicy;
101
103
  let routing = defaultRouting;
104
+ let runnerModel = opts.runnerModel ?? existing?.runner?.model;
105
+ let runnerReasoning = opts.runnerReasoning ?? existing?.runner?.reasoningEffort;
106
+ const agentModels = { ...(opts.agentModels ?? {}) };
107
+ const agentReasoning = { ...(opts.agentReasoning ?? {}) };
102
108
  if (interactive && ask) {
103
109
  agents = parseAgents(await ask(`Agents [${defaultAgents.join(',')}] (${SUPPORTED_AGENTS.join(',')}|all): `), defaultAgents);
104
110
  const graphAnswer = (await ask(`Code graph [${defaultGraph}] (graphify|serena): `)).trim().toLowerCase();
@@ -112,12 +118,39 @@ export async function runSetup(targetDir, opts = {}) {
112
118
  if (policyAnswer === 'auto' || policyAnswer === 'critical')
113
119
  decisionPolicy = policyAnswer;
114
120
  routing = yes(await ask(`Enable adaptive multi-model routing? [${defaultRouting ? 'yes' : 'no'}]: `), defaultRouting);
121
+ if (opts.configureModels) {
122
+ const configureModels = yes(await ask(`Configure model & reasoning effort per agent? [no]: `), false);
123
+ if (configureModels) {
124
+ const rm = (await ask(`Runner model [${runnerModel ?? 'default'}]: `)).trim();
125
+ if (rm)
126
+ runnerModel = rm;
127
+ const rr = (await ask(`Runner reasoning effort (none|low|medium|high|xhigh) [${runnerReasoning ?? 'default'}]: `)).trim();
128
+ if (rr)
129
+ runnerReasoning = rr;
130
+ for (const a of agents) {
131
+ const am = (await ask(`Model for agent "${a}" [${agentModels[a] ?? 'default'}]: `)).trim();
132
+ if (am)
133
+ agentModels[a] = am;
134
+ const ar = (await ask(`Reasoning effort for agent "${a}" (none|low|medium|high|xhigh) [${agentReasoning[a] ?? 'default'}]: `)).trim();
135
+ if (ar)
136
+ agentReasoning[a] = ar;
137
+ }
138
+ }
139
+ }
115
140
  }
116
141
  if (modelProviders.length && !agents.includes('qwen'))
117
142
  agents = [...agents, 'qwen'];
118
143
  if (!agents.includes(runner))
119
144
  agents = [...agents, runner];
120
- const code = runRetrofit(targetDir, { loop, agents, codeGraph, codeIntelligence, host });
145
+ const priorRunner = existing?.runner?.agent ?? existing?.agents[0];
146
+ const runnerChanged = priorRunner !== undefined && priorRunner !== runner;
147
+ if (runnerChanged) {
148
+ if (opts.runnerModel === undefined)
149
+ runnerModel = undefined;
150
+ if (opts.runnerReasoning === undefined)
151
+ runnerReasoning = undefined;
152
+ }
153
+ const code = runRetrofit(targetDir, { loop, agents, codeGraph, codeIntelligence, host, cleanWorktrees: opts.cleanWorktrees, runnerModel, runnerReasoning, agentModels, agentReasoning });
121
154
  if (code !== 0)
122
155
  return code;
123
156
  applyActions(presetActions, targetDir, { backupDir: join(targetDir, '.yoke', 'backups', `model-presets-${Date.now()}`) });
@@ -125,14 +158,23 @@ export async function runSetup(targetDir, opts = {}) {
125
158
  if (!config)
126
159
  return 1;
127
160
  config.loop = { parallel: 'auto', isolate: true, ...config.loop, enabled: loop, decisionPolicy };
128
- const priorRunner = existing?.runner?.agent ?? existing?.agents[0];
129
161
  const selectPresetModel = presetWorkers.length > 0 && runner === 'qwen' && (!existing || (priorRunner !== undefined && priorRunner !== runner));
130
162
  if (selectPresetModel && priorRunner !== 'qwen')
131
163
  config.runner = { permissions: config.runner?.permissions };
132
- config.runner = { ...config.runner, agent: runner, ...(selectPresetModel && !config.runner?.model ? { model: presetWorkers[0].model } : {}) };
164
+ config.runner = {
165
+ ...config.runner,
166
+ agent: runner,
167
+ ...(runnerModel ? { model: runnerModel } : selectPresetModel && !config.runner?.model ? { model: presetWorkers[0].model } : {}),
168
+ ...(runnerReasoning ? { reasoningEffort: runnerReasoning } : {}),
169
+ };
133
170
  const existingWorkers = config.routing?.workers ?? [];
134
171
  const baseWorkers = existingWorkers.length > 0 && !opts.routingPreset ? existingWorkers : defaultRoutingWorkers(agents).filter(worker => !modelProviders.length || worker.agent !== 'qwen');
135
- const workers = [...baseWorkers, ...presetWorkers.filter(worker => !baseWorkers.some(existing => existing.id === worker.id))];
172
+ const rawWorkers = [...baseWorkers, ...presetWorkers.filter(worker => !baseWorkers.some(existing => existing.id === worker.id))];
173
+ const workers = rawWorkers.map(w => {
174
+ const model = agentModels[w.agent] ?? w.model;
175
+ const reasoningEffort = agentReasoning[w.agent] ?? w.reasoningEffort;
176
+ return { ...w, ...(model ? { model } : {}), ...(reasoningEffort ? { reasoningEffort } : {}) };
177
+ });
136
178
  config.routing = {
137
179
  ...config.routing,
138
180
  enabled: routing,
@@ -145,7 +187,7 @@ export async function runSetup(targetDir, opts = {}) {
145
187
  };
146
188
  YokeConfigSchema.parse(config);
147
189
  saveConfig(targetDir, config);
148
- console.log(`Yoke setup complete: agents=${agents.join(',')} · runner=${runner} · loop=${loop ? 'on' : 'off'} · routing=${routing ? 'on' : 'off'} · decisions=${decisionPolicy}`);
190
+ console.log(`Yoke setup complete: agents=${agents.join(',')} · runner=${runner}${runnerModel ? ` (${runnerModel})` : ''}${runnerReasoning ? ` [${runnerReasoning}]` : ''} · loop=${loop ? 'on' : 'off'} · routing=${routing ? 'on' : 'off'} · decisions=${decisionPolicy}`);
149
191
  return 0;
150
192
  }
151
193
  finally {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yoke",
3
- "version": "1.20.0",
3
+ "version": "1.21.1",
4
4
  "description": "Cross-agent coding harness for eight supported CLIs: curated skill canon, mechanical safety gates, autonomous loop with proof artifacts. CLI: npm i -g @hecer/yoke",
5
5
  "contextFileName": "GEMINI-EXTENSION.md"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hecer/yoke",
3
- "version": "1.20.0",
3
+ "version": "1.21.1",
4
4
  "description": "One harness, eight agents, zero trust in \"done\" — cross-agent coding harness for Claude Code, Codex CLI, Gemini CLI, Qwen Code, OpenCode, Kilo, Pi and Hermes: one skill canon, mechanical safety gates, an autonomous loop with screenshot/video proofs.",
5
5
  "type": "module",
6
6
  "bin": {