@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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +19 -0
- package/README.md +1 -1
- package/dist/cli.js +74 -5
- package/dist/loop/cleanup.js +100 -0
- package/dist/loop/run-command.js +7 -2
- package/dist/retrofit/command.js +22 -0
- package/dist/retrofit/planners/claude.js +4 -4
- package/dist/retrofit/wsl.js +23 -3
- package/dist/setup/command.js +47 -5
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
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",
|
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)
|
|
12
12
|

|
|
13
13
|
|
|
14
|
-
<!-- yoke:version:start -->1.
|
|
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
|
}
|
package/dist/loop/cleanup.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/loop/run-command.js
CHANGED
|
@@ -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}"` : ''}` : ''}`;
|
package/dist/retrofit/command.js
CHANGED
|
@@ -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
|
|
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',
|
package/dist/retrofit/wsl.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
|
|
3
|
-
//
|
|
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
|
+
}
|
package/dist/setup/command.js
CHANGED
|
@@ -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
|
|
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 = {
|
|
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
|
|
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 {
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yoke",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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": {
|