@hecer/yoke 1.10.0 → 1.12.0
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 +2 -2
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +37 -0
- package/README.md +40 -24
- package/canon/manifest.yaml +2 -2
- package/canon/tools/qwen-rtk-hook.mjs +25 -0
- package/dist/agents/contracts.js +1 -1
- package/dist/agents/host.js +4 -0
- package/dist/agents/providers.js +28 -2
- package/dist/agents/telemetry.js +35 -3
- package/dist/canon/manifest.js +1 -1
- package/dist/cli.js +27 -19
- package/dist/goals/command.js +2 -2
- package/dist/loop/claims.js +1 -1
- package/dist/loop/decision.js +2 -2
- package/dist/loop/prd.js +1 -1
- package/dist/loop/run-command.js +3 -3
- package/dist/quality/types.js +1 -1
- package/dist/retrofit/apply.js +8 -1
- package/dist/retrofit/config.js +1 -1
- package/dist/retrofit/detect.js +2 -0
- package/dist/retrofit/plan.js +2 -0
- package/dist/retrofit/planners/qwen.js +73 -0
- package/dist/retrofit/qwen-settings.js +17 -0
- package/dist/retrofit/skill-actions.js +2 -1
- package/dist/review/command.js +1 -1
- package/dist/routing/capability.js +1 -1
- package/dist/routing/router.js +1 -1
- package/dist/setup/command.js +27 -7
- package/dist/setup/model-presets.js +48 -0
- package/docs/QWEN-MODEL-SUPPORT.md +142 -0
- package/gemini-extension.json +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { MODEL_PROVIDERS } from './setup/model-presets.js';
|
|
2
3
|
import { pathToFileURL } from 'node:url';
|
|
3
4
|
import { realpathSync } from 'node:fs';
|
|
4
5
|
import { checkProject, checkExitCode, protectAcceptance } from './check/command.js';
|
|
@@ -104,7 +105,7 @@ export function main(argv) {
|
|
|
104
105
|
switch (cmd) {
|
|
105
106
|
case 'setup': {
|
|
106
107
|
const targetDir = rest.find(a => !a.startsWith('-')) ?? '.';
|
|
107
|
-
const valid = ['claude', 'codex', 'gemini'];
|
|
108
|
+
const valid = ['claude', 'codex', 'gemini', 'qwen'];
|
|
108
109
|
const hostArg = rest.find(a => a.startsWith('--host='))?.slice('--host='.length);
|
|
109
110
|
if (hostArg && !valid.includes(hostArg)) {
|
|
110
111
|
console.error(`Invalid --host value: ${hostArg}`);
|
|
@@ -114,7 +115,7 @@ export function main(argv) {
|
|
|
114
115
|
const agentTokens = agentArg === 'all' ? valid : agentArg?.split(',').map(a => a.trim());
|
|
115
116
|
const invalidAgents = agentTokens?.filter(a => !valid.includes(a)) ?? [];
|
|
116
117
|
if (agentArg && (invalidAgents.length > 0 || agentTokens?.length === 0)) {
|
|
117
|
-
console.error(`Invalid --agent value: ${agentArg} (expected claude,codex,gemini|all)`);
|
|
118
|
+
console.error(`Invalid --agent value: ${agentArg} (expected claude,codex,gemini,qwen|all)`);
|
|
118
119
|
return 1;
|
|
119
120
|
}
|
|
120
121
|
const agents = agentTokens ? [...new Set(agentTokens)] : undefined;
|
|
@@ -140,7 +141,14 @@ export function main(argv) {
|
|
|
140
141
|
console.error('Invalid routing strategy');
|
|
141
142
|
return 1;
|
|
142
143
|
}
|
|
144
|
+
const modelProviderArg = rest.find(a => a.startsWith('--model-provider='))?.slice('--model-provider='.length);
|
|
145
|
+
const modelProviders = modelProviderArg?.split(',').map(value => value.trim());
|
|
146
|
+
if (modelProviders?.some(value => !MODEL_PROVIDERS.includes(value))) {
|
|
147
|
+
console.error('Invalid --model-provider (expected deepseek,kimi)');
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
143
150
|
return runSetup(targetDir, {
|
|
151
|
+
modelProviders: modelProviders,
|
|
144
152
|
host: hostArg, agents, runner: runnerArg,
|
|
145
153
|
codeGraph: graphArg,
|
|
146
154
|
loop, routing, decisionPolicy: policyArg,
|
|
@@ -226,7 +234,7 @@ export function main(argv) {
|
|
|
226
234
|
}
|
|
227
235
|
if (sub === 'run' || sub === 'resume') {
|
|
228
236
|
const provider = value('runner') ?? 'codex';
|
|
229
|
-
if (!['codex', 'claude', 'gemini'].includes(provider))
|
|
237
|
+
if (!['codex', 'claude', 'gemini', 'qwen'].includes(provider))
|
|
230
238
|
throw new Error('Unknown runner');
|
|
231
239
|
return runProjectGoal(targetDir, { provider: provider, selection: { model: value('model') } }).then(goal => {
|
|
232
240
|
console.log(JSON.stringify(goal));
|
|
@@ -269,7 +277,7 @@ export function main(argv) {
|
|
|
269
277
|
const targetDir = rest.find(a => !a.startsWith('-')) ?? '.';
|
|
270
278
|
const loop = rest.includes('--loop');
|
|
271
279
|
const agentArg = rest.find(a => a.startsWith('--agent='))?.slice('--agent='.length);
|
|
272
|
-
const all = ['claude', 'codex', 'gemini'];
|
|
280
|
+
const all = ['claude', 'codex', 'gemini', 'qwen'];
|
|
273
281
|
const agents = !agentArg || agentArg === 'all'
|
|
274
282
|
? (agentArg === 'all' ? all : undefined)
|
|
275
283
|
: agentArg.split(',').filter((a) => all.includes(a));
|
|
@@ -468,10 +476,10 @@ export function main(argv) {
|
|
|
468
476
|
return 1;
|
|
469
477
|
}
|
|
470
478
|
const runnerArg = rest.find(a => a.startsWith('--runner='))?.slice('--runner='.length);
|
|
471
|
-
const valid = ['claude', 'codex', 'gemini'];
|
|
479
|
+
const valid = ['claude', 'codex', 'gemini', 'qwen'];
|
|
472
480
|
const agent = runnerArg && valid.includes(runnerArg) ? runnerArg : undefined;
|
|
473
481
|
if (runnerArg && !agent) {
|
|
474
|
-
console.error(`Invalid --runner value: ${runnerArg} (expected claude|codex|gemini)`);
|
|
482
|
+
console.error(`Invalid --runner value: ${runnerArg} (expected claude|codex|gemini|qwen)`);
|
|
475
483
|
return 1;
|
|
476
484
|
}
|
|
477
485
|
const isolate = rest.includes('--isolate') ? true : rest.includes('--no-isolate') ? false : undefined;
|
|
@@ -479,7 +487,7 @@ export function main(argv) {
|
|
|
479
487
|
let reviewer;
|
|
480
488
|
if (reviewerArg) {
|
|
481
489
|
if (!valid.includes(reviewerArg)) {
|
|
482
|
-
console.error(`Invalid --reviewer value: ${reviewerArg} (expected claude|codex|gemini)`);
|
|
490
|
+
console.error(`Invalid --reviewer value: ${reviewerArg} (expected claude|codex|gemini|qwen)`);
|
|
483
491
|
return 1;
|
|
484
492
|
}
|
|
485
493
|
reviewer = reviewerArg;
|
|
@@ -522,19 +530,19 @@ export function main(argv) {
|
|
|
522
530
|
}
|
|
523
531
|
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, ...qualityFlags.options });
|
|
524
532
|
}
|
|
525
|
-
console.log('usage: yoke loop <on|off|status|decision|answer|resume [--discard] [--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] [--parallel=<auto|N>] [--runner=<claude|codex|gemini>] [--reviewer=<claude|codex|gemini>] [--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]');
|
|
533
|
+
console.log('usage: yoke loop <on|off|status|decision|answer|resume [--discard] [--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] [--parallel=<auto|N>] [--runner=<claude|codex|gemini|qwen>] [--reviewer=<claude|codex|gemini|qwen>] [--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]');
|
|
526
534
|
return 1;
|
|
527
535
|
}
|
|
528
536
|
case 'new': {
|
|
529
537
|
const dir = rest.find(a => !a.startsWith('-'));
|
|
530
538
|
if (!dir) {
|
|
531
|
-
console.error('usage: yoke new <dir> [--idea="..."] [--agent=claude,codex,gemini|all] [--runner=<claude|codex|gemini>] [--loop]');
|
|
539
|
+
console.error('usage: yoke new <dir> [--idea="..."] [--agent=claude,codex,gemini,qwen|all] [--runner=<claude|codex|gemini|qwen>] [--loop]');
|
|
532
540
|
return 1;
|
|
533
541
|
}
|
|
534
542
|
const idea = rest.find(a => a.startsWith('--idea='))?.slice('--idea='.length);
|
|
535
543
|
const loop = rest.includes('--loop');
|
|
536
544
|
const agentArg = rest.find(a => a.startsWith('--agent='))?.slice('--agent='.length);
|
|
537
|
-
const all = ['claude', 'codex', 'gemini'];
|
|
545
|
+
const all = ['claude', 'codex', 'gemini', 'qwen'];
|
|
538
546
|
const agents = !agentArg || agentArg === 'all'
|
|
539
547
|
? (agentArg === 'all' ? all : undefined)
|
|
540
548
|
: agentArg.split(',').filter((a) => all.includes(a));
|
|
@@ -543,7 +551,7 @@ export function main(argv) {
|
|
|
543
551
|
}
|
|
544
552
|
const runnerArg = rest.find(a => a.startsWith('--runner='))?.slice('--runner='.length);
|
|
545
553
|
if (runnerArg && !all.includes(runnerArg)) {
|
|
546
|
-
console.error(`Invalid --runner value: ${runnerArg} (expected claude|codex|gemini)`);
|
|
554
|
+
console.error(`Invalid --runner value: ${runnerArg} (expected claude|codex|gemini|qwen)`);
|
|
547
555
|
return 1;
|
|
548
556
|
}
|
|
549
557
|
return runNew(dir, { idea, agents, runner: runnerArg, loop });
|
|
@@ -553,7 +561,7 @@ export function main(argv) {
|
|
|
553
561
|
const targetDir = rest.slice(1).find(a => !a.startsWith('-')) ?? '.';
|
|
554
562
|
if (sub === 'assess') {
|
|
555
563
|
const runner = rest.find(a => a.startsWith('--runner='))?.slice('--runner='.length);
|
|
556
|
-
if (runner && !['codex', 'claude', 'gemini'].includes(runner)) {
|
|
564
|
+
if (runner && !['codex', 'claude', 'gemini', 'qwen'].includes(runner)) {
|
|
557
565
|
console.error('Invalid planning runner');
|
|
558
566
|
return 1;
|
|
559
567
|
}
|
|
@@ -562,13 +570,13 @@ export function main(argv) {
|
|
|
562
570
|
if (sub === 'draft') {
|
|
563
571
|
const idea = rest.find(a => a.startsWith('--idea='))?.slice('--idea='.length);
|
|
564
572
|
if (!idea) {
|
|
565
|
-
console.error('usage: yoke prd draft [dir] --idea="..." [--runner=<claude|codex|gemini>] [--force] [--timeout=<minutes>]');
|
|
573
|
+
console.error('usage: yoke prd draft [dir] --idea="..." [--runner=<claude|codex|gemini|qwen>] [--force] [--timeout=<minutes>]');
|
|
566
574
|
return 1;
|
|
567
575
|
}
|
|
568
|
-
const valid = ['claude', 'codex', 'gemini'];
|
|
576
|
+
const valid = ['claude', 'codex', 'gemini', 'qwen'];
|
|
569
577
|
const runnerArg = rest.find(a => a.startsWith('--runner='))?.slice('--runner='.length);
|
|
570
578
|
if (runnerArg && !valid.includes(runnerArg)) {
|
|
571
|
-
console.error(`Invalid --runner value: ${runnerArg} (expected claude|codex|gemini)`);
|
|
579
|
+
console.error(`Invalid --runner value: ${runnerArg} (expected claude|codex|gemini|qwen)`);
|
|
572
580
|
return 1;
|
|
573
581
|
}
|
|
574
582
|
const force = rest.includes('--force');
|
|
@@ -586,7 +594,7 @@ export function main(argv) {
|
|
|
586
594
|
}
|
|
587
595
|
if (sub === 'check')
|
|
588
596
|
return runPrdCheck(targetDir);
|
|
589
|
-
console.log('usage: yoke prd <draft|check|assess> [dir] [--idea="..."] [--runner=<claude|codex|gemini>] [--story=<id>] [--reassess] [--force] [--timeout=<minutes>]');
|
|
597
|
+
console.log('usage: yoke prd <draft|check|assess> [dir] [--idea="..."] [--runner=<claude|codex|gemini|qwen>] [--story=<id>] [--reassess] [--force] [--timeout=<minutes>]');
|
|
590
598
|
return 1;
|
|
591
599
|
}
|
|
592
600
|
case 'context': {
|
|
@@ -601,10 +609,10 @@ export function main(argv) {
|
|
|
601
609
|
}
|
|
602
610
|
case 'review': {
|
|
603
611
|
const targetDir = rest.find(a => !a.startsWith('-')) ?? '.';
|
|
604
|
-
const valid = ['claude', 'codex', 'gemini'];
|
|
612
|
+
const valid = ['claude', 'codex', 'gemini', 'qwen'];
|
|
605
613
|
const reviewerArg = rest.find(a => a.startsWith('--reviewer='))?.slice('--reviewer='.length);
|
|
606
614
|
if (reviewerArg && !valid.includes(reviewerArg)) {
|
|
607
|
-
console.error(`Invalid --reviewer value: ${reviewerArg} (expected claude|codex|gemini)`);
|
|
615
|
+
console.error(`Invalid --reviewer value: ${reviewerArg} (expected claude|codex|gemini|qwen)`);
|
|
608
616
|
return 1;
|
|
609
617
|
}
|
|
610
618
|
const base = rest.find(a => a.startsWith('--base='))?.slice('--base='.length);
|
|
@@ -651,7 +659,7 @@ export function main(argv) {
|
|
|
651
659
|
return runUpgrade();
|
|
652
660
|
default:
|
|
653
661
|
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]');
|
|
654
|
-
console.log('usage: yoke <setup [dir] | new <dir> [--idea="..."] | validate [canonDir] | retrofit [targetDir] [--agent=claude,codex,gemini|all] [--code-graph=graphify|serena] [--loop] | change <add|status> [dir] | prd <draft|check|assess> [dir] | loop <on|off|status|decision|answer|resume|run|cleanup> | context <init|status> | review [dir] [--reviewer=<claude|codex|gemini>] [--base=<ref>] [--focus="..."] | design-scan [dir] [--max=N] [--report] | flow-smoke [dir] [--url=<baseUrl>] [--label=<name>] | upgrade>');
|
|
662
|
+
console.log('usage: yoke <setup [dir] | new <dir> [--idea="..."] | validate [canonDir] | retrofit [targetDir] [--agent=claude,codex,gemini,qwen|all] [--code-graph=graphify|serena] [--loop] | change <add|status> [dir] | prd <draft|check|assess> [dir] | loop <on|off|status|decision|answer|resume|run|cleanup> | context <init|status> | review [dir] [--reviewer=<claude|codex|gemini|qwen>] [--base=<ref>] [--focus="..."] | design-scan [dir] [--max=N] [--report] | flow-smoke [dir] [--url=<baseUrl>] [--label=<name>] | upgrade>');
|
|
655
663
|
return cmd ? 1 : 0;
|
|
656
664
|
}
|
|
657
665
|
}
|
package/dist/goals/command.js
CHANGED
|
@@ -12,8 +12,8 @@ import { killProcessTreeForCleanup } from '../loop/watchdog.js';
|
|
|
12
12
|
import { buildProviderInvocation, startProviderProcess } from '../agents/providers.js';
|
|
13
13
|
import { appendEvent } from '../observability/events.js';
|
|
14
14
|
import { statePath } from '../workspace/state.js';
|
|
15
|
-
const Attempt = z.object({ provider: z.enum(['codex', 'claude', 'gemini']), model: z.string().optional(), startedAt: z.string(), durationMs: z.number().nonnegative(), success: z.boolean(), summary: z.string(), checkId: z.string(), inputTokens: z.number().nonnegative().optional(), outputTokens: z.number().nonnegative().optional() });
|
|
16
|
-
const Goal = z.object({ version: z.literal(1), id: z.string().uuid(), objective: z.string().trim().min(1).max(16000), status: z.enum(['active', 'running', 'paused', 'blocked', 'complete']), createdAt: z.string(), updatedAt: z.string(), maxAttempts: z.number().int().min(1).max(20), maxMinutes: z.number().positive().max(1440), tokenBudget: z.number().int().positive().optional(), attempts: z.array(Attempt), reason: z.string().optional(), lastCheck: z.string().optional(), pendingAttempt: z.object({ provider: z.enum(['codex', 'claude', 'gemini']), model: z.string().optional(), startedAt: z.string().datetime() }).optional() }).strict();
|
|
15
|
+
const Attempt = z.object({ provider: z.enum(['codex', 'claude', 'gemini', 'qwen']), model: z.string().optional(), startedAt: z.string(), durationMs: z.number().nonnegative(), success: z.boolean(), summary: z.string(), checkId: z.string(), inputTokens: z.number().nonnegative().optional(), outputTokens: z.number().nonnegative().optional() });
|
|
16
|
+
const Goal = z.object({ version: z.literal(1), id: z.string().uuid(), objective: z.string().trim().min(1).max(16000), status: z.enum(['active', 'running', 'paused', 'blocked', 'complete']), createdAt: z.string(), updatedAt: z.string(), maxAttempts: z.number().int().min(1).max(20), maxMinutes: z.number().positive().max(1440), tokenBudget: z.number().int().positive().optional(), attempts: z.array(Attempt), reason: z.string().optional(), lastCheck: z.string().optional(), pendingAttempt: z.object({ provider: z.enum(['codex', 'claude', 'gemini', 'qwen']), model: z.string().optional(), startedAt: z.string().datetime() }).optional() }).strict();
|
|
17
17
|
const goalPath = (root) => statePath(root, 'goal.json');
|
|
18
18
|
function save(root, goal) {
|
|
19
19
|
const parsed = Goal.parse({ ...goal, updatedAt: new Date().toISOString() });
|
package/dist/loop/claims.js
CHANGED
|
@@ -18,7 +18,7 @@ const RichClaimSchema = LegacyClaimSchema.extend({
|
|
|
18
18
|
heartbeatAt: z.string().min(1),
|
|
19
19
|
baseCommit: z.string().min(1).optional(),
|
|
20
20
|
worktree: z.string().min(1).optional(),
|
|
21
|
-
provider: z.enum(['claude', 'codex', 'gemini']).optional(),
|
|
21
|
+
provider: z.enum(['claude', 'codex', 'gemini', 'qwen']).optional(),
|
|
22
22
|
model: z.string().min(1).optional(),
|
|
23
23
|
role: z.enum(['implementation', 'quality', 'repair', 'review']).optional(),
|
|
24
24
|
cancellation: z.object({
|
package/dist/loop/decision.js
CHANGED
|
@@ -60,9 +60,9 @@ export const DecisionResumeSchema = z.object({
|
|
|
60
60
|
answered: z.boolean().optional(),
|
|
61
61
|
/** Absent preserves the default unlimited run across a critical-decision resume. */
|
|
62
62
|
maxIterations: z.number().int().positive().optional(),
|
|
63
|
-
agent: z.enum(['claude', 'codex', 'gemini']).optional(),
|
|
63
|
+
agent: z.enum(['claude', 'codex', 'gemini', 'qwen']).optional(),
|
|
64
64
|
isolate: z.boolean().optional(),
|
|
65
|
-
reviewer: z.enum(['claude', 'codex', 'gemini']).optional(),
|
|
65
|
+
reviewer: z.enum(['claude', 'codex', 'gemini', 'qwen']).optional(),
|
|
66
66
|
review: z.boolean().optional(),
|
|
67
67
|
allowSelfReview: z.boolean().optional(),
|
|
68
68
|
timeoutMinutes: z.number().nonnegative().optional(),
|
package/dist/loop/prd.js
CHANGED
|
@@ -42,7 +42,7 @@ export const StorySchema = z.object({
|
|
|
42
42
|
area: z.string().min(1).optional(),
|
|
43
43
|
/** Advisory relative file/directory scopes; not a filesystem permission boundary. */
|
|
44
44
|
writes: z.array(z.string().min(1).max(500).refine(validWriteScope, 'Write scopes must be relative non-glob paths without traversal')).max(100).optional(),
|
|
45
|
-
agent: z.enum(['claude', 'codex', 'gemini']).optional(),
|
|
45
|
+
agent: z.enum(['claude', 'codex', 'gemini', 'qwen']).optional(),
|
|
46
46
|
/** Inbox request that created this story. Used for idempotent append-only intake. */
|
|
47
47
|
sourceChange: z.string().min(1).optional(),
|
|
48
48
|
quality: StoryQualityDeclarationSchema.optional(),
|
package/dist/loop/run-command.js
CHANGED
|
@@ -379,7 +379,7 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
379
379
|
let review = opts.reviewRunner;
|
|
380
380
|
let reviewProvider = 'unknown';
|
|
381
381
|
if (!review && (opts.review || opts.reviewer)) {
|
|
382
|
-
const reviewerAgent = opts.reviewer ?? ['codex', 'gemini', 'claude'].find(agent => agent !== runnerAgent && available(agent));
|
|
382
|
+
const reviewerAgent = opts.reviewer ?? ['codex', 'gemini', 'qwen', 'claude'].find(agent => agent !== runnerAgent && available(agent));
|
|
383
383
|
if (!reviewerAgent) {
|
|
384
384
|
if (!opts.allowSelfReview) {
|
|
385
385
|
console.error('No independent reviewer CLI is available. Install or select a second agent, or pass --allow-self-review explicitly.');
|
|
@@ -393,13 +393,13 @@ export function runLoopCommand(targetDir, opts) {
|
|
|
393
393
|
return 2;
|
|
394
394
|
}
|
|
395
395
|
if (!available(resolvedReviewer)) {
|
|
396
|
-
console.error(`Reviewer agent CLI "${resolvedReviewer}" was not found on PATH. Install it, or pick another with --reviewer=<claude|codex|gemini>.`);
|
|
396
|
+
console.error(`Reviewer agent CLI "${resolvedReviewer}" was not found on PATH. Install it, or pick another with --reviewer=<claude|codex|gemini|qwen>.`);
|
|
397
397
|
return 2;
|
|
398
398
|
}
|
|
399
399
|
review = context => {
|
|
400
400
|
const implementer = readStatus(targetDir)?.routingDecisions?.[context.story.id]?.provider ?? runnerAgent;
|
|
401
401
|
const selectedReviewer = !opts.reviewer && resolvedReviewer === implementer
|
|
402
|
-
? ["codex", "claude", "gemini"].find(agent => agent !== implementer && available(agent)) ?? resolvedReviewer : resolvedReviewer;
|
|
402
|
+
? ["codex", "claude", "gemini", "qwen"].find(agent => agent !== implementer && available(agent)) ?? resolvedReviewer : resolvedReviewer;
|
|
403
403
|
if (selectedReviewer === implementer && !opts.allowSelfReview)
|
|
404
404
|
return { success: false, summary: "Independent review requires a provider distinct from the routed implementer", reviewOutcome: { kind: "infrastructure", summary: "Routed implementation and reviewer share a provider" } };
|
|
405
405
|
reviewProvider = selectedReviewer;
|
package/dist/quality/types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
const QualityPolicySchema = z.enum(['blocking', 'advisory']);
|
|
3
|
-
const QualityAgentSchema = z.enum(['claude', 'codex', 'gemini']);
|
|
3
|
+
const QualityAgentSchema = z.enum(['claude', 'codex', 'gemini', 'qwen']);
|
|
4
4
|
const RelativePathSchema = z.string().min(1).refine(value => !/^(?:[A-Za-z]:[\\/]|[\\/])/.test(value) && !value.split(/[\\/]+/).includes('..'), 'path must stay within the project');
|
|
5
5
|
const QualityReferenceSchema = z.object({
|
|
6
6
|
name: z.string().min(1),
|
package/dist/retrofit/apply.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join, dirname } from 'node:path';
|
|
3
|
+
import { mergeQwenSettings } from './qwen-settings.js';
|
|
3
4
|
import { mergeJson } from './merge-json.js';
|
|
4
5
|
import { carryPreserved } from './preserve.js';
|
|
5
6
|
export function applyActions(actions, targetDir, opts) {
|
|
@@ -28,7 +29,7 @@ export function applyActions(actions, targetDir, opts) {
|
|
|
28
29
|
catch {
|
|
29
30
|
throw new Error(`yoke: cannot merge ${action.target} — existing file is not valid JSON. Fix or delete it and re-run.`);
|
|
30
31
|
}
|
|
31
|
-
const merged = JSON.stringify(mergeJson(parsedCurrent, JSON.parse(action.content)), null, 2) + '\n';
|
|
32
|
+
const merged = JSON.stringify((action.target === '.qwen/settings.json' ? mergeQwenSettings : mergeJson)(parsedCurrent, JSON.parse(action.content)), null, 2) + '\n';
|
|
32
33
|
if (merged === current) {
|
|
33
34
|
results.push({ target: action.target, status: 'unchanged', reason: action.reason });
|
|
34
35
|
continue;
|
|
@@ -43,6 +44,12 @@ export function applyActions(actions, targetDir, opts) {
|
|
|
43
44
|
}
|
|
44
45
|
if (typeof action.content === 'string') {
|
|
45
46
|
const current = currentBytes.toString('utf8');
|
|
47
|
+
// Exact matches need no preservation pass. In particular, skill examples
|
|
48
|
+
// may contain literal preserve markers which must not be reinterpreted.
|
|
49
|
+
if (current === action.content) {
|
|
50
|
+
results.push({ target: action.target, status: 'unchanged', reason: action.reason });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
46
53
|
// Carry user content marked with yoke preserve markers into the new file.
|
|
47
54
|
content = carryPreserved(current, action.content);
|
|
48
55
|
if (content !== action.content)
|
package/dist/retrofit/config.js
CHANGED
|
@@ -80,7 +80,7 @@ export const YokeConfigSchema = z.object({
|
|
|
80
80
|
model: z.string().min(1).optional(),
|
|
81
81
|
reasoningEffort: z.string().min(1).optional(),
|
|
82
82
|
}).optional(),
|
|
83
|
-
workers: z.array(RoutingWorkerSchema).max(
|
|
83
|
+
workers: z.array(RoutingWorkerSchema).max(32).default([]),
|
|
84
84
|
rules: z.array(RoutingRuleSchema).max(100).optional(),
|
|
85
85
|
}).optional(),
|
|
86
86
|
commit: z.object({
|
package/dist/retrofit/detect.js
CHANGED
package/dist/retrofit/plan.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { planClaude } from './planners/claude.js';
|
|
2
2
|
import { planCodex } from './planners/codex.js';
|
|
3
3
|
import { planGemini } from './planners/gemini.js';
|
|
4
|
+
import { planQwen } from './planners/qwen.js';
|
|
4
5
|
import { baseContextActions } from './context-actions.js';
|
|
5
6
|
export function planClaudeRetrofit(canonDir, targetDir) {
|
|
6
7
|
return planClaude(canonDir, targetDir);
|
|
@@ -9,6 +10,7 @@ export const PLANNERS = {
|
|
|
9
10
|
claude: (c, t, cg) => planClaude(c, t, undefined, cg),
|
|
10
11
|
codex: planCodex,
|
|
11
12
|
gemini: planGemini,
|
|
13
|
+
qwen: planQwen,
|
|
12
14
|
};
|
|
13
15
|
export function planRetrofit(canonDir, targetDir, agents, codeGraph = 'graphify') {
|
|
14
16
|
const seen = new Set();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { loadManifest } from '../../canon/manifest.js';
|
|
4
|
+
import { parseFrontmatter } from '../../canon/frontmatter.js';
|
|
5
|
+
import { mcpServers, rtkInstruction } from '../tools.js';
|
|
6
|
+
import { PRESERVE_SCAFFOLD } from '../preserve.js';
|
|
7
|
+
import { skillPackageActions } from '../skill-actions.js';
|
|
8
|
+
function tomlString(s) {
|
|
9
|
+
return '"""\n' + s.replace(/\\/g, '\\\\').replace(/"""/g, '\\"\\"\\"') + '\n"""';
|
|
10
|
+
}
|
|
11
|
+
export function planQwen(canonDir, _targetDir, codeGraph = 'graphify') {
|
|
12
|
+
const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
|
|
13
|
+
const actions = [];
|
|
14
|
+
// QWEN.md: baseline + instruction for commands outside the rewrite hook.
|
|
15
|
+
const baseline = readFileSync(join(canonDir, 'AGENTS.md'), 'utf8');
|
|
16
|
+
const autoSkillIndex = manifest.skills
|
|
17
|
+
.filter(skill => skill.invocation === 'auto')
|
|
18
|
+
.map(skill => {
|
|
19
|
+
const source = readFileSync(join(canonDir, skill.path, 'SKILL.md'), 'utf8');
|
|
20
|
+
const description = String(parseFrontmatter(source)?.description ?? skill.id).replace(/\s+/gu, ' ').trim();
|
|
21
|
+
return `- \`/${skill.id}\` — ${description}`;
|
|
22
|
+
})
|
|
23
|
+
.join('\n');
|
|
24
|
+
actions.push({
|
|
25
|
+
kind: 'write',
|
|
26
|
+
target: 'QWEN.md',
|
|
27
|
+
content: `${baseline}\n${rtkInstruction()}\n\n## Yoke automatic skills\n\nUse the matching command when its capability is relevant:\n\n${autoSkillIndex}\n\n${PRESERVE_SCAFFOLD}\n`,
|
|
28
|
+
reason: 'baseline + rtk instruction',
|
|
29
|
+
});
|
|
30
|
+
// One TOML slash command per skill.
|
|
31
|
+
for (const skill of manifest.skills) {
|
|
32
|
+
const body = readFileSync(join(canonDir, skill.path, 'SKILL.md'), 'utf8');
|
|
33
|
+
const fm = parseFrontmatter(body) ?? {};
|
|
34
|
+
// Collapse any newlines so the single-line TOML `description = "..."` stays valid
|
|
35
|
+
// even for ported skills whose frontmatter description spans multiple lines.
|
|
36
|
+
const description = String(fm.description ?? skill.id).replace(/\s*\r?\n\s*/g, ' ').trim();
|
|
37
|
+
const skillBody = body.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '').trim();
|
|
38
|
+
const prompt = `You are using the "${skill.id}" skill.\n\n${skillBody}\n\nFollow it for the current task.`;
|
|
39
|
+
actions.push({
|
|
40
|
+
kind: 'write',
|
|
41
|
+
target: `.qwen/commands/${skill.id}.toml`,
|
|
42
|
+
content: `description = "${description.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"\nprompt = ${tomlString(prompt)}\n`,
|
|
43
|
+
reason: `qwen command: ${skill.id}`,
|
|
44
|
+
});
|
|
45
|
+
actions.push(...skillPackageActions(canonDir, skill, 'qwen'));
|
|
46
|
+
}
|
|
47
|
+
actions.push({
|
|
48
|
+
kind: 'write',
|
|
49
|
+
target: '.qwen/hooks/qwen-rtk-hook.mjs',
|
|
50
|
+
content: readFileSync(join(canonDir, 'tools/qwen-rtk-hook.mjs'), 'utf8'),
|
|
51
|
+
reason: 'portable RTK PreToolUse retry guard',
|
|
52
|
+
});
|
|
53
|
+
// Merge to preserve user MCP servers, context and unrelated hooks.
|
|
54
|
+
actions.push({
|
|
55
|
+
kind: 'write',
|
|
56
|
+
target: '.qwen/settings.json',
|
|
57
|
+
merge: true,
|
|
58
|
+
content: JSON.stringify({
|
|
59
|
+
mcpServers: mcpServers(codeGraph),
|
|
60
|
+
context: { fileName: ['AGENTS.md', 'QWEN.md'] },
|
|
61
|
+
hooks: { PreToolUse: [{ matcher: '^run_shell_command$', hooks: [{ name: 'yoke-rtk', type: 'command', command: 'node .qwen/hooks/qwen-rtk-hook.mjs' }] }] },
|
|
62
|
+
}, null, 2) + '\n',
|
|
63
|
+
reason: 'MCP servers + AGENTS.md context',
|
|
64
|
+
});
|
|
65
|
+
// Also ship AGENTS.md so the context.fileName entry resolves.
|
|
66
|
+
actions.push({
|
|
67
|
+
kind: 'write',
|
|
68
|
+
target: 'AGENTS.md',
|
|
69
|
+
content: baseline,
|
|
70
|
+
reason: 'baseline instructions (shared)',
|
|
71
|
+
});
|
|
72
|
+
return actions;
|
|
73
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { mergeJson } from './merge-json.js';
|
|
2
|
+
const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
3
|
+
/** Remove only Yoke 1.11's inert Gemini-shaped hook, preserving unrelated hooks. */
|
|
4
|
+
export function mergeQwenSettings(current, incoming) {
|
|
5
|
+
if (!record(current) || !record(current.hooks) || !Array.isArray(current.hooks.BeforeTool))
|
|
6
|
+
return mergeJson(current, incoming);
|
|
7
|
+
const beforeTool = current.hooks.BeforeTool.flatMap(group => {
|
|
8
|
+
if (!record(group) || group.matcher !== '^run_shell_command$' || !Array.isArray(group.hooks))
|
|
9
|
+
return [group];
|
|
10
|
+
const hooks = group.hooks.filter(hook => !(record(hook) && hook.name === 'yoke-rtk' && hook.type === 'command' && hook.command === 'node .qwen/hooks/qwen-rtk-hook.mjs'));
|
|
11
|
+
return hooks.length ? [{ ...group, hooks }] : [];
|
|
12
|
+
});
|
|
13
|
+
const hooks = { ...current.hooks, BeforeTool: beforeTool };
|
|
14
|
+
if (!beforeTool.length)
|
|
15
|
+
delete hooks.BeforeTool;
|
|
16
|
+
return mergeJson({ ...current, hooks }, incoming);
|
|
17
|
+
}
|
|
@@ -4,6 +4,7 @@ const roots = {
|
|
|
4
4
|
claude: '.claude/skills',
|
|
5
5
|
codex: '.agents/skills',
|
|
6
6
|
gemini: '.gemini/skills',
|
|
7
|
+
qwen: '.qwen/skills',
|
|
7
8
|
};
|
|
8
9
|
function manualClaudeSkill(content, skill) {
|
|
9
10
|
const source = content.toString('utf8');
|
|
@@ -48,7 +49,7 @@ export function skillPackageActions(canonDir, skill, provider) {
|
|
|
48
49
|
.map(file => ({
|
|
49
50
|
kind: 'write',
|
|
50
51
|
target: `${roots[provider]}/${skill.id}/${file.relativePath}`,
|
|
51
|
-
content: provider === 'claude' && skill.invocation === 'manual' && file.relativePath === 'SKILL.md'
|
|
52
|
+
content: (provider === 'claude' || provider === 'qwen') && skill.invocation === 'manual' && file.relativePath === 'SKILL.md'
|
|
52
53
|
? manualClaudeSkill(file.content, skill)
|
|
53
54
|
: portableContent(file),
|
|
54
55
|
executable: file.executable,
|
package/dist/review/command.js
CHANGED
|
@@ -5,7 +5,7 @@ import { loadConfig } from '../retrofit/config.js';
|
|
|
5
5
|
import { parseReviewVerdict } from './verdict.js';
|
|
6
6
|
// Resolve to the first available agent, preferring a *second* model so the review
|
|
7
7
|
// is genuinely cross-model. claude last => a Claude-only box degrades to self-review.
|
|
8
|
-
const RESOLUTION_ORDER = ['codex', 'gemini', 'claude'];
|
|
8
|
+
const RESOLUTION_ORDER = ['codex', 'gemini', 'qwen', 'claude'];
|
|
9
9
|
export function runReview(targetDir, opts = {}) {
|
|
10
10
|
const available = opts.isAvailable ?? isAgentAvailable;
|
|
11
11
|
const implementer = opts.implementer ?? loadConfig(targetDir)?.agents[0] ?? 'claude';
|
|
@@ -68,7 +68,7 @@ export function chooseCapability(input) {
|
|
|
68
68
|
const worker = reliable[0];
|
|
69
69
|
const blocked = !worker && (input.fallback === 'block' || input.maxTier !== undefined);
|
|
70
70
|
const provider = worker?.agent ?? input.story.agent ?? input.parent;
|
|
71
|
-
const selection = worker ? { model: worker.model, reasoningEffort: worker.reasoningEffort, nativeMultiAgent: false, ...(provider !== 'gemini' && input.parentSelection?.bare !== undefined ? { bare: input.parentSelection.bare } : {}) }
|
|
71
|
+
const selection = worker ? { model: worker.model, reasoningEffort: worker.reasoningEffort, nativeMultiAgent: false, ...(provider !== 'gemini' && provider !== 'qwen' && input.parentSelection?.bare !== undefined ? { bare: input.parentSelection.bare } : {}) }
|
|
72
72
|
: { ...(provider === input.parent ? input.parentSelection : {}), nativeMultiAgent: false };
|
|
73
73
|
const reason = `${role}: ${tiers[level]}; ${input.assessment.reason}${failures.length ? `; ${failures.length} verified failure(s), ${failures.length === 1 ? 'one targeted repair' : 'escalated'}` : ''}${worker ? '' : '; no eligible profile, parent/provider fallback'}`;
|
|
74
74
|
return { worker, provider, selection, reason: blocked ? `${role}: no eligible profile within routing limits; execution blocked` : reason, blocked, requiredTier: baseTier, selectedTier: tiers[level], failures: failures.length, exhausted, next: input.maxTier && level >= tiers.indexOf(input.maxTier) ? 'stop at configured tier limit' : level < 3 ? tiers[level + 1] : 'stop after bounded attempts' };
|
package/dist/routing/router.js
CHANGED
|
@@ -247,7 +247,7 @@ function routingSteps(options) {
|
|
|
247
247
|
return blocked('Selected routing profile exceeds configured limits; execution blocked');
|
|
248
248
|
const provider = worker?.agent ?? options.parent;
|
|
249
249
|
const selection = worker
|
|
250
|
-
? { model: worker.model, reasoningEffort: worker.reasoningEffort, nativeMultiAgent: false, ...(provider !== 'gemini' ? { bare: options.parentSelection?.bare } : {}) }
|
|
250
|
+
? { model: worker.model, reasoningEffort: worker.reasoningEffort, nativeMultiAgent: false, ...(provider !== 'gemini' && provider !== 'qwen' ? { bare: options.parentSelection?.bare } : {}) }
|
|
251
251
|
: { ...(options.parentSelection ?? {}), nativeMultiAgent: false };
|
|
252
252
|
const workerStarted = now();
|
|
253
253
|
const result = yield () => makeWorker(provider, selection)(ctx);
|
package/dist/setup/command.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline/promises';
|
|
2
2
|
import { stdin as input, stdout as output } from 'node:process';
|
|
3
3
|
import { detectHostAgent } from '../agents/host.js';
|
|
4
|
-
import { loadConfig, saveConfig } from '../retrofit/config.js';
|
|
4
|
+
import { loadConfig, saveConfig, YokeConfigSchema } from '../retrofit/config.js';
|
|
5
5
|
import { detectProject } from '../retrofit/detect.js';
|
|
6
|
+
import { applyActions } from '../retrofit/apply.js';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { modelPresetWorkers, planModelPresets } from './model-presets.js';
|
|
6
9
|
import { runRetrofit } from '../retrofit/command.js';
|
|
7
|
-
const ALL_AGENTS = ['claude', 'codex', 'gemini'];
|
|
10
|
+
const ALL_AGENTS = ['claude', 'codex', 'gemini', 'qwen'];
|
|
8
11
|
export function defaultRoutingWorkers(agents) {
|
|
9
12
|
const workers = {
|
|
10
13
|
claude: [
|
|
@@ -25,6 +28,10 @@ export function defaultRoutingWorkers(agents) {
|
|
|
25
28
|
{ id: 'gemini-strong', agent: 'gemini', model: 'gemini-2.5-pro', tier: 'strong', costTier: 'medium', capabilities: ['debugging'] },
|
|
26
29
|
{ id: 'gemini-frontier', agent: 'gemini', model: 'gemini-2.5-pro', tier: 'frontier', costTier: 'high', capabilities: ['architecture'] },
|
|
27
30
|
],
|
|
31
|
+
qwen: [
|
|
32
|
+
// Respect the user's Qwen Code account/model. API presets are opt-in.
|
|
33
|
+
{ id: 'qwen-standard', agent: 'qwen', tier: 'standard', costTier: 'medium', capabilities: ['implementation'] },
|
|
34
|
+
],
|
|
28
35
|
};
|
|
29
36
|
return agents.flatMap(agent => workers[agent]);
|
|
30
37
|
}
|
|
@@ -44,6 +51,9 @@ function yes(value, fallback) {
|
|
|
44
51
|
}
|
|
45
52
|
export async function runSetup(targetDir, opts = {}) {
|
|
46
53
|
const existing = loadConfig(targetDir);
|
|
54
|
+
const modelProviders = opts.modelProviders ?? [];
|
|
55
|
+
const presetActions = planModelPresets(targetDir, modelProviders);
|
|
56
|
+
const presetWorkers = modelPresetWorkers(modelProviders);
|
|
47
57
|
const detected = detectProject(targetDir);
|
|
48
58
|
const host = opts.host ?? detectHostAgent();
|
|
49
59
|
const configuredAgents = existing?.agents.filter(a => ALL_AGENTS.includes(a)) ?? [];
|
|
@@ -53,7 +63,7 @@ export async function runSetup(targetDir, opts = {}) {
|
|
|
53
63
|
? configuredAgents
|
|
54
64
|
: detected.agents.length > 0
|
|
55
65
|
? detected.agents
|
|
56
|
-
: [host ?? 'claude'];
|
|
66
|
+
: modelProviders.length ? ['qwen'] : [host ?? 'claude'];
|
|
57
67
|
const defaultGraph = opts.codeGraph ?? existing?.codeGraph ?? 'graphify';
|
|
58
68
|
const defaultLoop = opts.loop ?? existing?.loop.enabled ?? true;
|
|
59
69
|
const defaultRunner = opts.runner ?? existing?.runner?.agent ?? (host && defaultAgents.includes(host) ? host : defaultAgents[0] ?? host ?? 'claude');
|
|
@@ -75,12 +85,12 @@ export async function runSetup(targetDir, opts = {}) {
|
|
|
75
85
|
let decisionPolicy = defaultPolicy;
|
|
76
86
|
let routing = defaultRouting;
|
|
77
87
|
if (interactive && ask) {
|
|
78
|
-
agents = parseAgents(await ask(`Agents [${defaultAgents.join(',')}] (claude,codex,gemini|all): `), defaultAgents);
|
|
88
|
+
agents = parseAgents(await ask(`Agents [${defaultAgents.join(',')}] (claude,codex,gemini,qwen|all): `), defaultAgents);
|
|
79
89
|
const graphAnswer = (await ask(`Code graph [${defaultGraph}] (graphify|serena): `)).trim().toLowerCase();
|
|
80
90
|
if (graphAnswer === 'graphify' || graphAnswer === 'serena')
|
|
81
91
|
codeGraph = graphAnswer;
|
|
82
92
|
loop = yes(await ask(`Enable autonomous loop? [${defaultLoop ? 'yes' : 'no'}]: `), defaultLoop);
|
|
83
|
-
const runnerAnswer = (await ask(`Default runner [${runner}] (claude|codex|gemini): `)).trim().toLowerCase();
|
|
93
|
+
const runnerAnswer = (await ask(`Default runner [${runner}] (claude|codex|gemini|qwen): `)).trim().toLowerCase();
|
|
84
94
|
if (ALL_AGENTS.includes(runnerAnswer))
|
|
85
95
|
runner = runnerAnswer;
|
|
86
96
|
const policyAnswer = (await ask(`Decision mode [${decisionPolicy}] (auto|critical): `)).trim().toLowerCase();
|
|
@@ -88,17 +98,26 @@ export async function runSetup(targetDir, opts = {}) {
|
|
|
88
98
|
decisionPolicy = policyAnswer;
|
|
89
99
|
routing = yes(await ask(`Enable adaptive multi-model routing? [${defaultRouting ? 'yes' : 'no'}]: `), defaultRouting);
|
|
90
100
|
}
|
|
101
|
+
if (modelProviders.length && !agents.includes('qwen'))
|
|
102
|
+
agents = [...agents, 'qwen'];
|
|
91
103
|
if (!agents.includes(runner))
|
|
92
104
|
agents = [...agents, runner];
|
|
93
105
|
const code = runRetrofit(targetDir, { loop, agents, codeGraph, host });
|
|
94
106
|
if (code !== 0)
|
|
95
107
|
return code;
|
|
108
|
+
applyActions(presetActions, targetDir, { backupDir: join(targetDir, '.yoke', 'backups', `model-presets-${Date.now()}`) });
|
|
96
109
|
const config = loadConfig(targetDir);
|
|
97
110
|
if (!config)
|
|
98
111
|
return 1;
|
|
99
112
|
config.loop = { parallel: 'auto', isolate: true, ...config.loop, enabled: loop, decisionPolicy };
|
|
100
|
-
|
|
113
|
+
const priorRunner = existing?.runner?.agent ?? existing?.agents[0];
|
|
114
|
+
const selectPresetModel = presetWorkers.length > 0 && runner === 'qwen' && (!existing || (priorRunner !== undefined && priorRunner !== runner));
|
|
115
|
+
if (selectPresetModel && priorRunner !== 'qwen')
|
|
116
|
+
config.runner = { permissions: config.runner?.permissions };
|
|
117
|
+
config.runner = { ...config.runner, agent: runner, ...(selectPresetModel && !config.runner?.model ? { model: presetWorkers[0].model } : {}) };
|
|
101
118
|
const existingWorkers = config.routing?.workers ?? [];
|
|
119
|
+
const baseWorkers = existingWorkers.length > 0 && !opts.routingPreset ? existingWorkers : defaultRoutingWorkers(agents).filter(worker => !modelProviders.length || worker.agent !== 'qwen');
|
|
120
|
+
const workers = [...baseWorkers, ...presetWorkers.filter(worker => !baseWorkers.some(existing => existing.id === worker.id))];
|
|
102
121
|
config.routing = {
|
|
103
122
|
...config.routing,
|
|
104
123
|
enabled: routing,
|
|
@@ -107,8 +126,9 @@ export async function runSetup(targetDir, opts = {}) {
|
|
|
107
126
|
assessmentPolicy: existing?.routing?.assessmentPolicy ?? (existing ? 'on-demand' : 'prepared'),
|
|
108
127
|
fallback: existing?.routing?.fallback ?? (existing ? 'parent' : 'block'),
|
|
109
128
|
...(config.routing?.orchestrator ? { orchestrator: config.routing.orchestrator } : {}),
|
|
110
|
-
workers
|
|
129
|
+
workers,
|
|
111
130
|
};
|
|
131
|
+
YokeConfigSchema.parse(config);
|
|
112
132
|
saveConfig(targetDir, config);
|
|
113
133
|
console.log(`Yoke setup complete: agents=${agents.join(',')} · runner=${runner} · loop=${loop ? 'on' : 'off'} · routing=${routing ? 'on' : 'off'} · decisions=${decisionPolicy}`);
|
|
114
134
|
return 0;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export const MODEL_PROVIDERS = ['deepseek', 'kimi'];
|
|
4
|
+
const PRESETS = {
|
|
5
|
+
deepseek: {
|
|
6
|
+
baseUrl: 'https://api.deepseek.com/v1', envKey: 'DEEPSEEK_API_KEY',
|
|
7
|
+
models: [
|
|
8
|
+
{ id: 'deepseek-v4-flash', workerId: 'deepseek-standard', tier: 'standard', costTier: 'low', contextWindowSize: 1_000_000 },
|
|
9
|
+
{ id: 'deepseek-v4-pro', workerId: 'deepseek-strong', tier: 'strong', costTier: 'medium', contextWindowSize: 1_000_000 },
|
|
10
|
+
],
|
|
11
|
+
},
|
|
12
|
+
kimi: {
|
|
13
|
+
baseUrl: 'https://api.moonshot.ai/v1', envKey: 'MOONSHOT_API_KEY',
|
|
14
|
+
models: [
|
|
15
|
+
{ id: 'kimi-k2.6', workerId: 'kimi-standard', tier: 'standard', costTier: 'medium', contextWindowSize: 262_144 },
|
|
16
|
+
{ id: 'kimi-k2.7-code', workerId: 'kimi-strong', tier: 'strong', costTier: 'medium', contextWindowSize: 262_144 },
|
|
17
|
+
{ id: 'kimi-k3', workerId: 'kimi-frontier', tier: 'frontier', costTier: 'high', contextWindowSize: 1_000_000 },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
export function modelPresetWorkers(providers) {
|
|
22
|
+
return [...new Set(providers)].flatMap(provider => PRESETS[provider].models.map(model => ({
|
|
23
|
+
id: model.workerId, agent: 'qwen', model: `openai::${model.id}`, tier: model.tier,
|
|
24
|
+
costTier: model.costTier, capabilities: ['implementation'],
|
|
25
|
+
})));
|
|
26
|
+
}
|
|
27
|
+
/** Explicit opt-in only. Keep user routes intact; new entries contain only environment key references. */
|
|
28
|
+
export function planModelPresets(targetDir, providers) {
|
|
29
|
+
if (!providers.length)
|
|
30
|
+
return [];
|
|
31
|
+
const file = join(targetDir, '.qwen/settings.json');
|
|
32
|
+
const settings = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : {};
|
|
33
|
+
if (!settings || typeof settings !== 'object' || Array.isArray(settings))
|
|
34
|
+
throw Error('Qwen settings must be an object');
|
|
35
|
+
if (settings.modelProviders !== undefined && (!settings.modelProviders || typeof settings.modelProviders !== 'object' || Array.isArray(settings.modelProviders)))
|
|
36
|
+
throw Error('Qwen modelProviders must be an object');
|
|
37
|
+
const existing = settings.modelProviders?.openai ?? [];
|
|
38
|
+
if (!Array.isArray(existing) || existing.some(model => !model || typeof model.id !== 'string'))
|
|
39
|
+
throw Error('Qwen modelProviders.openai must be an array of models with ids');
|
|
40
|
+
const entries = [...new Set(providers)].flatMap(provider => {
|
|
41
|
+
const preset = PRESETS[provider];
|
|
42
|
+
return preset.models.filter(model => !existing.some(entry => entry.id === model.id)).map(model => ({
|
|
43
|
+
id: model.id, baseUrl: preset.baseUrl, envKey: preset.envKey,
|
|
44
|
+
generationConfig: { contextWindowSize: model.contextWindowSize },
|
|
45
|
+
}));
|
|
46
|
+
});
|
|
47
|
+
return [{ kind: 'write', target: '.qwen/settings.json', merge: true, content: JSON.stringify({ modelProviders: { openai: entries } }, null, 2) + '\n', reason: 'explicit DeepSeek/Kimi API model presets (environment key references only)' }];
|
|
48
|
+
}
|