@hecer/yoke 1.19.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 +46 -0
- package/README.md +4 -1
- package/bench/RESULTS.md +30 -0
- package/bench/analyze-codex-comparison.mjs +22 -0
- package/bench/compare-codex.mjs +71 -0
- package/bench/fixtures/independent-utils/.yoke/prd.yaml +30 -0
- package/bench/fixtures/independent-utils/bench-verify.mjs +4 -0
- package/bench/fixtures/independent-utils/package.json +1 -0
- package/bench/fixtures/independent-utils/src/chunk.mjs +1 -0
- package/bench/fixtures/independent-utils/src/range.mjs +1 -0
- package/bench/fixtures/independent-utils/src/unique.mjs +1 -0
- package/bench/fixtures/independent-utils/tests/STORY-1.test.mjs +8 -0
- package/bench/fixtures/independent-utils/tests/STORY-2.test.mjs +8 -0
- package/bench/fixtures/independent-utils/tests/STORY-3.test.mjs +8 -0
- package/bench/probe-goal-integration.mjs +93 -0
- package/bench/probe-parallel-gate-context.mjs +29 -0
- package/bench/results/codex-comparison-2026-09-29.json +305 -0
- package/bench/results/goal-integration-probes-2026-09-29.json +107 -0
- package/bench/results/native-goal-validation-2026-09-30.json +122 -0
- package/bench/results/parallel-validation-2026-09-30.json +86 -0
- package/dist/agents/process-record-identity.js +33 -0
- package/dist/check/command.js +218 -0
- package/dist/cli.js +85 -12
- package/dist/dashboard/server.js +26 -6
- package/dist/goals/codex-native.js +340 -0
- package/dist/goals/command.js +230 -29
- package/dist/loop/cleanup.js +110 -1
- package/dist/loop/dispatcher.js +82 -10
- package/dist/loop/git.js +1 -1
- package/dist/loop/parallel-adapters.js +53 -7
- package/dist/loop/parallel-command.js +8 -1
- package/dist/loop/recovery.js +81 -2
- package/dist/loop/reporter.js +3 -0
- package/dist/loop/resource-pool.js +21 -15
- package/dist/loop/run-command.js +98 -27
- package/dist/loop/run-state.js +116 -0
- package/dist/prd/explore.js +12 -4
- package/dist/retrofit/command.js +22 -0
- package/dist/retrofit/config.js +1 -0
- package/dist/retrofit/gitignore.js +3 -0
- package/dist/retrofit/planners/claude.js +4 -4
- package/dist/retrofit/wsl.js +23 -3
- package/dist/setup/command.js +47 -5
- package/docs/CODEX-COMPARISON-2026-09-29.md +27 -0
- package/docs/CONTINUOUS-EXPLORATION.md +6 -2
- package/docs/GOALS-RESOURCE-AUDIT-2026-09-29.md +92 -0
- package/docs/GOALS.md +109 -0
- package/docs/RELEASE-VALIDATION-1.20.0.md +100 -0
- package/docs/VERIFIED-PROJECTS.md +4 -4
- package/docs/parallel-execution.md +15 -0
- package/docs/superpowers/plans/2026-09-30-goals-resources-release.md +78 -0
- package/gemini-extension.json +1 -1
- package/package.json +5 -1
package/dist/loop/run-command.js
CHANGED
|
@@ -2,13 +2,15 @@ import { roleSelection } from "../routing/capability.js";
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { existsSync, unlinkSync } from 'node:fs';
|
|
4
4
|
import { loadConfig, saveConfig, defaultConfig, resolveOutputPolicy, resolveVerifyCommand } from '../retrofit/config.js';
|
|
5
|
-
import { allPass, loadPrd, progress, selectNextStory } from './prd.js';
|
|
5
|
+
import { allPass, loadPrd, progress, selectNextStory, storyPathSegment } from './prd.js';
|
|
6
6
|
import { pauseFilePath, requestLoopPause, runLoop } from './loop.js';
|
|
7
7
|
import { commitPaths, realGitOps } from './git.js';
|
|
8
8
|
import { makeRunner, makeReviewRunner, isAgentAvailable } from './runner.js';
|
|
9
9
|
import { commandVerifier, commandsVerifier, retryingVerifier } from './verify.js';
|
|
10
10
|
import { readStatus, makeReporter, fmtDuration } from './reporter.js';
|
|
11
|
-
import { acquireLock, releaseLock } from './lock.js';
|
|
11
|
+
import { acquireLock, releaseLock, readLock } from './lock.js';
|
|
12
|
+
import { statePath } from '../workspace/state.js';
|
|
13
|
+
import { accountRunIterations, createLoopRun, readRunState, writeRunState } from './run-state.js';
|
|
12
14
|
import { maybeAutoUpgrade } from '../update/upgrade.js';
|
|
13
15
|
import { resolveCommitIdentity } from './identity.js';
|
|
14
16
|
import { runAudit } from '../audit/command.js';
|
|
@@ -51,7 +53,7 @@ export function setLoopEnabled(targetDir, enabled) {
|
|
|
51
53
|
config.loop = { ...config.loop, enabled };
|
|
52
54
|
saveConfig(targetDir, config);
|
|
53
55
|
}
|
|
54
|
-
export function loopStatus(targetDir, now = () => new Date()) {
|
|
56
|
+
export function loopStatus(targetDir, now = () => new Date(), opts) {
|
|
55
57
|
const config = loadConfig(targetDir);
|
|
56
58
|
const enabled = config?.loop.enabled ?? false;
|
|
57
59
|
const path = prdPath(targetDir);
|
|
@@ -60,6 +62,12 @@ export function loopStatus(targetDir, now = () => new Date()) {
|
|
|
60
62
|
const p = progress(loadPrd(path));
|
|
61
63
|
prog = `${p.passed}/${p.total} stories pass`;
|
|
62
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
|
+
}
|
|
63
71
|
const sharedPoolLine = () => {
|
|
64
72
|
try {
|
|
65
73
|
const pool = sharedPoolStatus();
|
|
@@ -69,7 +77,6 @@ export function loopStatus(targetDir, now = () => new Date()) {
|
|
|
69
77
|
return `Shared pool unavailable: ${error.message}`;
|
|
70
78
|
}
|
|
71
79
|
};
|
|
72
|
-
const st = readStatus(targetDir);
|
|
73
80
|
if (!st)
|
|
74
81
|
return `Loop: ${enabled ? 'enabled' : 'disabled'}\nPRD: ${prog}\n${sharedPoolLine()}`;
|
|
75
82
|
const head = `Loop: ${st.state.toUpperCase()}${st.story ? ` on ${st.story}${st.storyTitle ? ` "${st.storyTitle}"` : ''}` : ''}`;
|
|
@@ -232,7 +239,7 @@ async function runContinuousExploration(targetDir, options) {
|
|
|
232
239
|
console.error('--explore-limit must be a positive, supported duration');
|
|
233
240
|
return 2;
|
|
234
241
|
}
|
|
235
|
-
const limitDeadline = options.exploreLimitMs === undefined ? undefined : Date.now() + options.exploreLimitMs;
|
|
242
|
+
const limitDeadline = options.savedRun?.exploreDeadline ?? (options.exploreLimitMs === undefined ? undefined : Date.now() + options.exploreLimitMs);
|
|
236
243
|
const reporter = options.reporter ?? makeReporter(targetDir, { json: options.json, quiet: true });
|
|
237
244
|
let config;
|
|
238
245
|
try {
|
|
@@ -242,11 +249,11 @@ async function runContinuousExploration(targetDir, options) {
|
|
|
242
249
|
config = null;
|
|
243
250
|
}
|
|
244
251
|
const defaultImplementationAgent = options.agent ?? resolveRunnerAgent(config, undefined, detectHostAgent());
|
|
245
|
-
const defaultExplorer = options.agent ?? config?.planning?.agent ?? defaultImplementationAgent;
|
|
252
|
+
const defaultExplorer = options.explorationPlanner?.agent ?? options.agent ?? config?.planning?.agent ?? defaultImplementationAgent;
|
|
246
253
|
const available = options.isAvailable ?? isAgentAvailable;
|
|
247
254
|
const reviewRequested = !options.reviewRunner && Boolean(options.review || options.reviewer);
|
|
248
255
|
const innerOptions = { ...options, isolate: true, explore: true, explorationSupervisor: true, reporter, quiet: true };
|
|
249
|
-
let usedIterations = 0;
|
|
256
|
+
let usedIterations = options.savedRun?.consumedIterations ?? 0;
|
|
250
257
|
let retryCount = 0;
|
|
251
258
|
let explorationFailures = 0;
|
|
252
259
|
let noActionScans = 0;
|
|
@@ -284,6 +291,8 @@ async function runContinuousExploration(targetDir, options) {
|
|
|
284
291
|
return 'limit';
|
|
285
292
|
const explorer = retryProviders(targetDir, explorationAgent, available);
|
|
286
293
|
const result = runPrdExplore(targetDir, {
|
|
294
|
+
ownedLockToken: options.ownedLockToken,
|
|
295
|
+
...(explorationAgent === options.explorationPlanner?.agent ? { selection: options.explorationPlanner.selection } : {}),
|
|
287
296
|
runner: explorationAgent,
|
|
288
297
|
timeoutMinutes: options.timeoutMinutes,
|
|
289
298
|
isAvailable: options.isAvailable ?? isAgentAvailable,
|
|
@@ -370,7 +379,9 @@ async function runContinuousExploration(targetDir, options) {
|
|
|
370
379
|
}
|
|
371
380
|
const afterStatus = readStatus(targetDir);
|
|
372
381
|
const changedStatus = afterStatus?.updatedAt !== beforeStatus?.updatedAt;
|
|
373
|
-
if (
|
|
382
|
+
if (options.savedRun)
|
|
383
|
+
usedIterations = options.savedRun.consumedIterations ?? usedIterations;
|
|
384
|
+
else if (changedStatus && priorProgress.passed < priorProgress.total)
|
|
374
385
|
usedIterations += Math.max(0, afterStatus?.iteration ?? 0);
|
|
375
386
|
if (unexpectedFailure) {
|
|
376
387
|
retryCount++;
|
|
@@ -468,7 +479,7 @@ async function runContinuousExploration(targetDir, options) {
|
|
|
468
479
|
}
|
|
469
480
|
retryCount++;
|
|
470
481
|
advanceRecoveryProviders();
|
|
471
|
-
retryWorktree =
|
|
482
|
+
retryWorktree = (afterStatus?.parallel?.reopened ?? 0) === 0;
|
|
472
483
|
const wait = retryDelay(retryCount);
|
|
473
484
|
reporter.phase('waiting-recovery', `story ${afterStatus?.story ?? 'unknown'} remains incomplete: ${afterStatus?.reason ?? 'no passing completion status'}; retrying with ${implementationAgent ?? 'the configured runner'} in ${Math.ceil(wait / 1_000)} seconds`, currentProgress(targetDir));
|
|
474
485
|
say(`The current task remains unfinished; its isolated work is retained. Next: retry with ${implementationAgent ?? 'the configured runner'} in ${Math.ceil(wait / 1_000)} seconds.`);
|
|
@@ -480,7 +491,7 @@ async function runContinuousExploration(targetDir, options) {
|
|
|
480
491
|
}
|
|
481
492
|
export function runLoopCommand(targetDir, opts) {
|
|
482
493
|
if (opts.explore && !opts.explorationSupervisor)
|
|
483
|
-
return
|
|
494
|
+
return startContinuousExploration(targetDir, opts);
|
|
484
495
|
let parallel = opts.parallel ?? 1;
|
|
485
496
|
const candidates = opts.candidates ?? 1;
|
|
486
497
|
if (opts.resumeWorktree && (opts.isolate === false || parallel !== 1 || candidates !== 1)) {
|
|
@@ -554,7 +565,8 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
554
565
|
return 2;
|
|
555
566
|
}
|
|
556
567
|
const isolate = opts.isolate ?? config.loop.isolate ?? true;
|
|
557
|
-
const
|
|
568
|
+
const retainedParallel = opts.explorationSupervisor && loadPrd(path).some(story => !story.passes && existsSync(statePath(targetDir, 'integration-recovery', `${storyPathSegment(story.id)}.json`)));
|
|
569
|
+
const useParallelDispatcher = parallel > 1 || candidates > 1 || retainedParallel === true;
|
|
558
570
|
if (opts.resumeWorktree && (!isolate || parallel !== 1)) {
|
|
559
571
|
console.error('--resume-worktree requires isolation and one worker');
|
|
560
572
|
return 2;
|
|
@@ -668,11 +680,13 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
668
680
|
const routingRequested = opts.routing ?? config.routing?.enabled ?? true;
|
|
669
681
|
const routingEnabled = routingRequested && Boolean(config.routing && (config.routing.workers.length || config.routing.fallback === 'block' || config.routing.maxTier || config.routing.assessmentPolicy === 'prepared'));
|
|
670
682
|
const runnerSelection = {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
683
|
+
...(opts.selection ?? {
|
|
684
|
+
provider: config.runner?.provider,
|
|
685
|
+
model: config.runner?.model,
|
|
686
|
+
reasoningEffort: config.runner?.reasoningEffort,
|
|
687
|
+
variant: config.runner?.variant,
|
|
688
|
+
bare: config.runner?.bare,
|
|
689
|
+
}),
|
|
676
690
|
nativeMultiAgent: false,
|
|
677
691
|
};
|
|
678
692
|
const parallelProviders = [{
|
|
@@ -735,13 +749,7 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
735
749
|
timeoutMs: idleMs,
|
|
736
750
|
isAvailable: available,
|
|
737
751
|
permissions,
|
|
738
|
-
selection:
|
|
739
|
-
provider: config.runner?.provider,
|
|
740
|
-
model: config.runner?.model,
|
|
741
|
-
reasoningEffort: config.runner?.reasoningEffort,
|
|
742
|
-
variant: config.runner?.variant,
|
|
743
|
-
bare: config.runner?.bare,
|
|
744
|
-
},
|
|
752
|
+
selection: runnerSelection,
|
|
745
753
|
commit: (_path, request) => commitPaths(targetDir, ['.yoke/prd.yaml'], `yoke: plan change ${request.id}`, commitIdentity),
|
|
746
754
|
}));
|
|
747
755
|
let runner = opts.runner;
|
|
@@ -854,7 +862,14 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
854
862
|
}
|
|
855
863
|
let lock;
|
|
856
864
|
try {
|
|
857
|
-
|
|
865
|
+
statePath(targetDir, 'loop.lock');
|
|
866
|
+
if (opts.ownedLockToken) {
|
|
867
|
+
if (readLock(targetDir)?.ownerToken !== opts.ownedLockToken)
|
|
868
|
+
throw new Error('Invalid borrowed loop lock');
|
|
869
|
+
lock = { acquired: true, ownerToken: opts.ownedLockToken };
|
|
870
|
+
}
|
|
871
|
+
else
|
|
872
|
+
lock = acquireLock(targetDir);
|
|
858
873
|
}
|
|
859
874
|
catch (error) {
|
|
860
875
|
console.error(`Cannot acquire the Yoke loop lock: ${error.message}`);
|
|
@@ -867,8 +882,22 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
867
882
|
if (lock.stalePid !== undefined) {
|
|
868
883
|
console.warn(`Took over a stale loop lock (pid ${lock.stalePid} is gone).`);
|
|
869
884
|
}
|
|
870
|
-
|
|
885
|
+
let reporter = opts.reporter ?? makeReporter(targetDir, { json: opts.json, quiet: opts.quiet });
|
|
871
886
|
executionReporter = reporter;
|
|
887
|
+
try {
|
|
888
|
+
const saved = opts.savedRun ?? createLoopRun({ ...opts, agent: runnerAgent, selection: runnerSelection, parallel, isolate, routing: routingEnabled, timeoutMinutes: opts.timeoutMinutes ?? config.loop.timeoutMinutes });
|
|
889
|
+
if (opts.savedRun && readRunState(targetDir)?.runId !== opts.savedRun.runId)
|
|
890
|
+
throw new Error('Saved run changed before resume');
|
|
891
|
+
writeRunState(targetDir, saved, lock.ownerToken);
|
|
892
|
+
reporter = accountRunIterations(targetDir, saved, lock.ownerToken, reporter);
|
|
893
|
+
executionReporter = reporter;
|
|
894
|
+
}
|
|
895
|
+
catch (error) {
|
|
896
|
+
if (!opts.ownedLockToken)
|
|
897
|
+
releaseLock(targetDir, lock.ownerToken);
|
|
898
|
+
reporter.blocked(`could not persist loop run: ${error.message}`);
|
|
899
|
+
return 1;
|
|
900
|
+
}
|
|
872
901
|
const buildResume = (storyId, requestId) => buildTrustedDecisionResumeState({
|
|
873
902
|
storyId,
|
|
874
903
|
requestId,
|
|
@@ -938,7 +967,8 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
938
967
|
completion,
|
|
939
968
|
quality,
|
|
940
969
|
onCriticalDecision: decision => writeDecisionResume(targetDir, buildResume(decision.storyId, decisionRequestId(decision))),
|
|
941
|
-
}).then(code => reconcileDecisionResume() ?? code).finally(() =>
|
|
970
|
+
}).then(code => reconcileDecisionResume() ?? code).finally(() => { if (!opts.ownedLockToken)
|
|
971
|
+
releaseLock(targetDir, lock.ownerToken); });
|
|
942
972
|
}
|
|
943
973
|
try {
|
|
944
974
|
const maxIterations = opts.maxIterations ?? Number.POSITIVE_INFINITY;
|
|
@@ -984,6 +1014,47 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
984
1014
|
return 1;
|
|
985
1015
|
}
|
|
986
1016
|
finally {
|
|
987
|
-
|
|
1017
|
+
if (!opts.ownedLockToken)
|
|
1018
|
+
releaseLock(targetDir, lock.ownerToken);
|
|
988
1019
|
}
|
|
989
1020
|
}
|
|
1021
|
+
async function startContinuousExploration(targetDir, options) {
|
|
1022
|
+
let lock;
|
|
1023
|
+
try {
|
|
1024
|
+
if (options.isolate === false)
|
|
1025
|
+
throw new Error('--explore requires isolated story worktrees');
|
|
1026
|
+
if (options.exploreLimitMs !== undefined && (!Number.isSafeInteger(options.exploreLimitMs) || options.exploreLimitMs <= 0 || Date.now() + options.exploreLimitMs > 8_640_000_000_000_000))
|
|
1027
|
+
throw new Error('--explore-limit must be a positive, supported duration');
|
|
1028
|
+
statePath(targetDir, 'loop.lock');
|
|
1029
|
+
lock = acquireLock(targetDir);
|
|
1030
|
+
if (!lock.acquired) {
|
|
1031
|
+
console.error('Another loop is already running here');
|
|
1032
|
+
return 2;
|
|
1033
|
+
}
|
|
1034
|
+
const config = loadConfig(targetDir);
|
|
1035
|
+
const implementationAgent = options.agent ?? resolveRunnerAgent(config, undefined, detectHostAgent());
|
|
1036
|
+
const implementationSelection = options.selection ?? config?.runner ?? {};
|
|
1037
|
+
const explorationPlanner = options.explorationPlanner ?? resolvePlanner(config, implementationAgent, implementationSelection, options.agent);
|
|
1038
|
+
const run = options.savedRun ?? createLoopRun({ ...options, agent: implementationAgent, selection: implementationSelection, explorationPlanner, timeoutMinutes: options.timeoutMinutes ?? config?.loop.timeoutMinutes, ...(options.parallel === undefined && !options.parallelAuto ? typeof config?.loop.parallel === 'number' ? { parallel: config.loop.parallel } : { parallelAuto: true } : {}) });
|
|
1039
|
+
if (options.savedRun && readRunState(targetDir)?.runId !== run.runId)
|
|
1040
|
+
throw new Error('Saved run changed before resume');
|
|
1041
|
+
writeRunState(targetDir, run, lock.ownerToken);
|
|
1042
|
+
return await runContinuousExploration(targetDir, { ...options, selection: run.options.selection, explorationPlanner: run.options.explorationPlanner ?? explorationPlanner, savedRun: run, ownedLockToken: lock.ownerToken });
|
|
1043
|
+
}
|
|
1044
|
+
catch (error) {
|
|
1045
|
+
console.error(`Cannot start exploration: ${error.message}`);
|
|
1046
|
+
return 2;
|
|
1047
|
+
}
|
|
1048
|
+
finally {
|
|
1049
|
+
if (lock?.acquired)
|
|
1050
|
+
releaseLock(targetDir, lock.ownerToken);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
/** Resume the saved execution identity without extending its exploration deadline. */
|
|
1054
|
+
export function resumeLoopCommand(targetDir) {
|
|
1055
|
+
const run = readRunState(targetDir);
|
|
1056
|
+
if (!run || run.mode === 'goal')
|
|
1057
|
+
throw new Error('No saved loop run to resume');
|
|
1058
|
+
const { native: _native, ...options } = run.options;
|
|
1059
|
+
return runLoopCommand(targetDir, { ...options, ...(run.mode === 'loop' && options.maxIterations !== undefined ? { maxIterations: Math.max(0, options.maxIterations - (run.consumedIterations ?? 0)) } : {}), explore: run.mode === 'explore', savedRun: run, permissions: 'safe' });
|
|
1060
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { lstatSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { statePath } from '../workspace/state.js';
|
|
5
|
+
import { readLock } from './lock.js';
|
|
6
|
+
import { AgentSchema, ModelSelectionSchema } from '../agents/contracts.js';
|
|
7
|
+
const agent = AgentSchema;
|
|
8
|
+
const bounded = z.number().int().min(1).max(1_000_000);
|
|
9
|
+
const selection = ModelSelectionSchema.omit({ nativeMultiAgent: true }).strict();
|
|
10
|
+
const Options = z.object({
|
|
11
|
+
agent: agent.optional(), reviewer: agent.optional(), maxIterations: bounded.optional(), isolate: z.boolean().optional(),
|
|
12
|
+
resumeWorktree: z.boolean().optional(), review: z.boolean().optional(), timeoutMinutes: z.number().finite().min(0).max(10080).optional(),
|
|
13
|
+
json: z.boolean().optional(), onAmbiguity: z.enum(['resolve', 'abort', 'auto', 'critical']).optional(), decisionPolicy: z.enum(['auto', 'critical']).optional(),
|
|
14
|
+
parallel: z.number().int().min(1).max(64).optional(), parallelAuto: z.boolean().optional(), routing: z.boolean().optional(),
|
|
15
|
+
quality: z.boolean().optional(), qualityRounds: bounded.optional(), qualityMinutes: z.number().finite().positive().max(10080).optional(),
|
|
16
|
+
qualityPolicy: z.enum(['blocking', 'advisory']).optional(), candidates: z.number().int().min(1).max(5).optional(),
|
|
17
|
+
exploreIntervalMinutes: z.number().int().min(1).max(1440).optional(), selection: selection.optional(), native: z.boolean().optional(),
|
|
18
|
+
explorationPlanner: z.object({ agent, selection }).strict().optional(),
|
|
19
|
+
}).strict();
|
|
20
|
+
const State = z.object({ version: z.literal(1), runId: z.string().uuid(), startedAt: z.string().datetime(), mode: z.enum(['loop', 'explore', 'goal']), goalId: z.string().uuid().optional(), consumedIterations: z.number().int().min(0).max(1_000_000_000).optional(), exploreDeadline: z.number().int().min(0).max(8_640_000_000_000_000).optional(), options: Options }).strict().superRefine((run, ctx) => {
|
|
21
|
+
if ((run.mode === 'goal') !== Boolean(run.goalId))
|
|
22
|
+
ctx.addIssue({ code: 'custom', message: 'Run goal identity does not match mode' });
|
|
23
|
+
if (run.mode !== 'explore' && run.exploreDeadline !== undefined)
|
|
24
|
+
ctx.addIssue({ code: 'custom', message: 'Exploration deadline does not match mode' });
|
|
25
|
+
});
|
|
26
|
+
function safeOptions(options) {
|
|
27
|
+
const selected = {};
|
|
28
|
+
for (const key of Object.keys(Options.shape)) {
|
|
29
|
+
const value = options[key];
|
|
30
|
+
if (value !== undefined) {
|
|
31
|
+
if (key === 'selection') {
|
|
32
|
+
const { nativeMultiAgent: _native, ...model } = ModelSelectionSchema.parse(value);
|
|
33
|
+
selected[key] = selection.parse(model);
|
|
34
|
+
}
|
|
35
|
+
else if (key === 'explorationPlanner') {
|
|
36
|
+
const planner = value;
|
|
37
|
+
const { nativeMultiAgent: _native, ...model } = ModelSelectionSchema.parse(planner.selection);
|
|
38
|
+
selected[key] = { agent: planner.agent, selection: selection.parse(model) };
|
|
39
|
+
}
|
|
40
|
+
else
|
|
41
|
+
selected[key] = value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return Options.parse(selected);
|
|
45
|
+
}
|
|
46
|
+
export function createLoopRun(options, now = Date.now()) {
|
|
47
|
+
const deadline = options.exploreLimitMs === undefined ? undefined : now + options.exploreLimitMs;
|
|
48
|
+
return State.parse({ version: 1, runId: randomUUID(), startedAt: new Date(now).toISOString(), mode: options.explore ? 'explore' : 'loop', options: safeOptions(options), ...(deadline !== undefined ? { exploreDeadline: deadline } : {}) });
|
|
49
|
+
}
|
|
50
|
+
export function readRunState(root) {
|
|
51
|
+
const file = statePath(root, 'run-state.json');
|
|
52
|
+
try {
|
|
53
|
+
const stat = lstatSync(file);
|
|
54
|
+
if (!stat.isFile())
|
|
55
|
+
throw new Error('Saved run is not a file');
|
|
56
|
+
if (stat.size > 65536)
|
|
57
|
+
throw new Error('Saved run is too large');
|
|
58
|
+
return State.parse(JSON.parse(readFileSync(file, 'utf8')));
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code === 'ENOENT')
|
|
62
|
+
return null;
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function writeRunState(root, run, ownerToken) {
|
|
67
|
+
const validated = State.parse(run);
|
|
68
|
+
statePath(root, 'loop.lock');
|
|
69
|
+
if (!ownerToken || readLock(root)?.ownerToken !== ownerToken)
|
|
70
|
+
throw new Error('Saving run requires ownership of the loop lock');
|
|
71
|
+
const file = statePath(root, 'run-state.json');
|
|
72
|
+
const temp = statePath(root, `run-state.${randomUUID()}.tmp`);
|
|
73
|
+
try {
|
|
74
|
+
writeFileSync(temp, JSON.stringify(validated), { flag: 'wx', mode: 0o600 });
|
|
75
|
+
statePath(root, 'run-state.json');
|
|
76
|
+
renameSync(temp, file);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
rmSync(temp, { force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function recordGoalRun(root, goalId, ownerToken, options = {}) {
|
|
83
|
+
const values = options;
|
|
84
|
+
let previous = null;
|
|
85
|
+
try {
|
|
86
|
+
previous = readRunState(root);
|
|
87
|
+
}
|
|
88
|
+
catch { /* an explicitly started goal may replace invalid old execution metadata */ }
|
|
89
|
+
const sameGoal = previous?.mode === 'goal' && previous.goalId === goalId ? previous : null;
|
|
90
|
+
writeRunState(root, State.parse({ version: 1, runId: sameGoal?.runId ?? randomUUID(), startedAt: sameGoal?.startedAt ?? new Date().toISOString(), mode: 'goal', goalId, options: safeOptions({ ...values, agent: values.agent ?? values.provider }) }), ownerToken);
|
|
91
|
+
}
|
|
92
|
+
/** Charge each dispatch before its reporter callback can begin provider work. */
|
|
93
|
+
export function accountRunIterations(root, run, ownerToken, reporter) {
|
|
94
|
+
const previousIterations = run.consumedIterations ?? 0;
|
|
95
|
+
let charged = 0;
|
|
96
|
+
const account = (iteration) => {
|
|
97
|
+
if (iteration === undefined || !Number.isSafeInteger(iteration) || iteration <= charged)
|
|
98
|
+
return;
|
|
99
|
+
const current = readRunState(root);
|
|
100
|
+
if (current?.runId !== run.runId)
|
|
101
|
+
throw new Error('Execution identity changed before dispatch');
|
|
102
|
+
run.consumedIterations = previousIterations + iteration;
|
|
103
|
+
writeRunState(root, run, ownerToken);
|
|
104
|
+
charged = iteration;
|
|
105
|
+
};
|
|
106
|
+
return new Proxy(reporter, {
|
|
107
|
+
get(target, key) {
|
|
108
|
+
if (key === 'storyStart')
|
|
109
|
+
return (...args) => { account(args[1]); target.storyStart(...args); };
|
|
110
|
+
if (key === 'parallel')
|
|
111
|
+
return (status) => { account(status.iteration); target.parallel?.(status); };
|
|
112
|
+
const value = Reflect.get(target, key, target);
|
|
113
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
package/dist/prd/explore.js
CHANGED
|
@@ -10,7 +10,8 @@ import { readPlanningFile } from '../routing/contracts.js';
|
|
|
10
10
|
import { loadConfig } from '../retrofit/config.js';
|
|
11
11
|
import { commitPaths, realGitOps } from '../loop/git.js';
|
|
12
12
|
import { resolveCommitIdentity } from '../loop/identity.js';
|
|
13
|
-
import { acquireLock, releaseLock } from '../loop/lock.js';
|
|
13
|
+
import { acquireLock, releaseLock, readLock } from '../loop/lock.js';
|
|
14
|
+
import { statePath } from '../workspace/state.js';
|
|
14
15
|
import { allPass, AcceptanceCriterionSchema, criterionCommandProblem, isAcceptanceCriterion, loadPrd, StorySchema, validateDependencies } from '../loop/prd.js';
|
|
15
16
|
import { writeScopesOverlap, validWriteScope } from '../loop/scheduler.js';
|
|
16
17
|
import { withSharedWorkerSync } from '../loop/resource-pool.js';
|
|
@@ -360,7 +361,14 @@ export function runPrdExplore(root, options = {}) {
|
|
|
360
361
|
const prompt = promptFor(brief, context, recentCompleted, options.focus);
|
|
361
362
|
if (prompt.length > MAX_PROMPT_CHARS)
|
|
362
363
|
throw Error(`exploration input exceeds ${MAX_PROMPT_CHARS} characters; condense the project brief and context`);
|
|
363
|
-
|
|
364
|
+
statePath(root, 'loop.lock');
|
|
365
|
+
if (options.ownedLockToken) {
|
|
366
|
+
if (readLock(root)?.ownerToken !== options.ownedLockToken)
|
|
367
|
+
throw Error('Invalid borrowed exploration lock');
|
|
368
|
+
lock = { acquired: true, ownerToken: options.ownedLockToken };
|
|
369
|
+
}
|
|
370
|
+
else
|
|
371
|
+
lock = acquireLock(root);
|
|
364
372
|
if (!lock.acquired)
|
|
365
373
|
return { kind: 'retry', provider: planner.agent, summary: 'another Yoke operation owns the project lock' };
|
|
366
374
|
if (options.pause?.())
|
|
@@ -370,7 +378,7 @@ export function runPrdExplore(root, options = {}) {
|
|
|
370
378
|
|| readPlanningFile(root, RECENT_EXPLORATION_FILE, RECENT_EXPLORATION_BYTES) !== recent.text
|
|
371
379
|
|| !realGitOps.isClean(root))
|
|
372
380
|
throw Error('project changed while exploration was being prepared');
|
|
373
|
-
const invocation = buildWatchdogInvocation(runnerInvocation(planner.agent, prompt, root, true, 'read-only', planner.selection), options.timeoutMinutes === undefined ? 20 * 60_000 : options.timeoutMinutes > 0 ? options.timeoutMinutes * 60_000 : 0);
|
|
381
|
+
const invocation = buildWatchdogInvocation(runnerInvocation(planner.agent, prompt, root, true, 'read-only', options.selection ?? planner.selection), options.timeoutMinutes === undefined ? 20 * 60_000 : options.timeoutMinutes > 0 ? options.timeoutMinutes * 60_000 : 0);
|
|
374
382
|
const started = Date.now();
|
|
375
383
|
const result = withSharedWorkerSync({ targetDir: root, storyId: 'project-exploration', provider: planner.agent, role: 'implementation' }, () => (options.run ?? runCapturedAgent)(planner.agent, invocation));
|
|
376
384
|
options.onUsage?.({
|
|
@@ -420,7 +428,7 @@ export function runPrdExplore(root, options = {}) {
|
|
|
420
428
|
return { kind: 'retry', ...(plannerAgent ? { provider: plannerAgent } : {}), summary: error instanceof Error ? error.message : String(error) };
|
|
421
429
|
}
|
|
422
430
|
finally {
|
|
423
|
-
if (lock?.acquired)
|
|
431
|
+
if (lock?.acquired && !options.ownedLockToken)
|
|
424
432
|
releaseLock(root, lock.ownerToken);
|
|
425
433
|
}
|
|
426
434
|
}
|
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 } } : {}),
|
package/dist/retrofit/config.js
CHANGED
|
@@ -88,6 +88,7 @@ export const YokeConfigSchema = z.object({
|
|
|
88
88
|
bare: z.boolean().optional(),
|
|
89
89
|
permissions: PermissionProfileSchema.optional(),
|
|
90
90
|
}).optional(),
|
|
91
|
+
goals: z.object({ nativeCodex: z.boolean().optional() }).optional(),
|
|
91
92
|
planning: z.object({
|
|
92
93
|
agent: AgentSchema.optional(),
|
|
93
94
|
provider: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/).optional(),
|
|
@@ -2,6 +2,9 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
export const YOKE_IGNORE_LINES = [
|
|
4
4
|
'.yoke/worktrees/',
|
|
5
|
+
'.yoke/integration-recovery/',
|
|
6
|
+
'.yoke/run-state.json',
|
|
7
|
+
'.yoke/run-state.*.tmp',
|
|
5
8
|
'.yoke/backup/',
|
|
6
9
|
'.yoke/code-intelligence/',
|
|
7
10
|
'.yoke/loop-status.json',
|
|
@@ -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 {
|