@hecer/yoke 1.11.0 → 1.13.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 +13 -13
- package/.codex-plugin/plugin.json +7 -7
- package/CHANGELOG.md +435 -398
- package/README.md +943 -915
- package/TODOS.md +5 -5
- package/agents/docs.toml +6 -6
- package/agents/implementer.toml +6 -6
- package/agents/reviewer.toml +6 -6
- package/agents/security.toml +6 -6
- package/bench/README.md +86 -86
- package/bench/RESULTS.md +35 -35
- package/bench/output-compaction.mjs +65 -65
- package/bench/result-schema.mjs +12 -12
- package/bench/results/claude-2026-07-27T18-03-26.json +50 -50
- package/bench/results/codex-unavailable-1785175418318.json +15 -15
- package/bench/results/gemini-2026-07-27T18-03-44.json +46 -46
- package/bench/run-matrix.mjs +26 -26
- package/bench/run.mjs +106 -106
- package/canon/AGENTS.md +30 -30
- package/canon/context/DECISIONS.md +4 -4
- package/canon/context/GLOSSARY.md +11 -11
- package/canon/context/KNOWLEDGE.md +4 -4
- package/canon/context/PROJECT.md +15 -15
- package/canon/loop/loop-spec.md +65 -65
- package/canon/loop/prd.schema.md +41 -41
- package/canon/manifest.yaml +59 -59
- package/canon/policy/gates.md +7 -7
- package/canon/policy/roles.md +9 -9
- package/canon/skills/ATTRIBUTION.md +99 -99
- package/canon/skills/authoring-prd/SKILL.md +57 -57
- package/canon/skills/brainstorming/SKILL.md +164 -164
- package/canon/skills/codebase-design/DEEPENING.md +15 -15
- package/canon/skills/codebase-design/DESIGN-IT-TWICE.md +12 -12
- package/canon/skills/codebase-design/SKILL.md +39 -39
- package/canon/skills/dispatching-parallel-agents/SKILL.md +182 -182
- package/canon/skills/document-release/SKILL.md +302 -302
- package/canon/skills/domain-modeling/ADR-FORMAT.md +19 -19
- package/canon/skills/domain-modeling/CONTEXT-FORMAT.md +39 -39
- package/canon/skills/domain-modeling/SKILL.md +35 -35
- package/canon/skills/executing-plans/SKILL.md +70 -70
- package/canon/skills/finishing-a-development-branch/SKILL.md +200 -200
- package/canon/skills/health/SKILL.md +177 -177
- package/canon/skills/maintaining-context/SKILL.md +34 -34
- package/canon/skills/minimal-code/SKILL.md +21 -21
- package/canon/skills/no-ai-slop/SKILL.md +103 -103
- package/canon/skills/no-ai-slop/eval.md +43 -43
- package/canon/skills/plan-ceo-review/SKILL.md +541 -541
- package/canon/skills/plan-eng-review/SKILL.md +362 -362
- package/canon/skills/receiving-code-review/SKILL.md +213 -213
- package/canon/skills/requesting-code-review/SKILL.md +105 -105
- package/canon/skills/resolving-merge-conflicts/SKILL.md +18 -18
- package/canon/skills/retro/SKILL.md +397 -397
- package/canon/skills/review/SKILL.md +246 -246
- package/canon/skills/ship/SKILL.md +691 -691
- package/canon/skills/subagent-driven-development/SKILL.md +277 -277
- package/canon/skills/systematic-debugging/SKILL.md +296 -296
- package/canon/skills/tdd/SKILL.md +371 -371
- package/canon/skills/unslop-ui/SKILL.md +34 -34
- package/canon/skills/using-git-worktrees/SKILL.md +218 -218
- package/canon/skills/verification-before-completion/SKILL.md +139 -139
- package/canon/skills/visual-verification/SKILL.md +54 -54
- package/canon/skills/workflow/SKILL.md +22 -22
- package/canon/skills/writing-for-agents/SKILL-MECHANICS.md +27 -27
- package/canon/skills/writing-for-agents/SKILL.md +42 -42
- package/canon/skills/writing-plans/SKILL.md +152 -152
- package/canon/skills/writing-skills/SKILL.md +655 -655
- package/canon/skills/yoke-retrofit/SKILL.md +26 -26
- package/canon/skills/yoke-workflow/SKILL.md +20 -20
- package/canon/tools/codex-rtk-hook.mjs +35 -35
- package/canon/tools/gemini-rtk-hook.mjs +25 -25
- package/canon/tools/graphify.md +3 -3
- package/canon/tools/playwright-mcp.md +3 -3
- package/canon/tools/qwen-rtk-hook.mjs +25 -0
- package/canon/tools/rtk.md +7 -7
- package/canon/tools/serena.md +6 -6
- package/dist/agents/catalog.js +7 -0
- package/dist/agents/contracts.js +3 -1
- package/dist/agents/host.js +5 -1
- package/dist/agents/process-streams.js +62 -0
- package/dist/agents/process.js +43 -3
- package/dist/agents/providers.js +61 -6
- package/dist/agents/telemetry.js +133 -37
- package/dist/canon/manifest.js +2 -1
- package/dist/change/inbox.js +1 -1
- package/dist/cli.js +30 -24
- package/dist/dashboard/page.js +122 -122
- package/dist/dashboard/panels.js +91 -91
- package/dist/goals/command.js +3 -2
- package/dist/loop/claims.js +2 -1
- package/dist/loop/decision.js +3 -2
- package/dist/loop/parallel-command.js +4 -2
- package/dist/loop/prd.js +2 -1
- package/dist/loop/reporter.js +1 -0
- package/dist/loop/run-command.js +31 -10
- package/dist/prd/command.js +19 -19
- package/dist/quality/candidate-comparison.js +6 -1
- package/dist/quality/command.js +17 -2
- package/dist/quality/types.js +6 -1
- package/dist/retrofit/apply.js +95 -2
- package/dist/retrofit/config.js +9 -1
- package/dist/retrofit/detect.js +8 -0
- package/dist/retrofit/plan.js +6 -0
- package/dist/retrofit/planners/claude.js +14 -14
- package/dist/retrofit/planners/kilo.js +44 -0
- package/dist/retrofit/planners/opencode.js +44 -0
- package/dist/retrofit/planners/pi.js +24 -0
- package/dist/retrofit/planners/qwen.js +3 -3
- package/dist/retrofit/preserve.js +2 -2
- package/dist/retrofit/qwen-settings.js +17 -0
- package/dist/retrofit/skill-actions.js +4 -1
- package/dist/retrofit/tools.js +8 -0
- package/dist/review/command.js +3 -2
- package/dist/review/verdict.js +1 -1
- package/dist/routing/capability.js +2 -2
- package/dist/routing/planning.js +2 -0
- package/dist/routing/registry.js +3 -1
- package/dist/routing/router.js +7 -3
- package/dist/setup/command.js +35 -11
- package/dist/setup/model-presets.js +48 -0
- package/docs/CAPABILITY-ROUTING.md +51 -51
- package/docs/DASHBOARD-EVOLUTION.md +33 -33
- package/docs/HARNESSES.md +81 -0
- package/docs/MIGRATING-TO-1.0.md +33 -33
- package/docs/MIGRATING-TO-1.1.md +27 -27
- package/docs/MIGRATING-TO-1.4.md +70 -70
- package/docs/PRODUCT-DIRECTION-2026-09-05.md +210 -210
- package/docs/PUBLISHING.md +114 -114
- package/docs/QWEN-MODEL-SUPPORT.md +142 -0
- package/docs/VERIFIED-PROJECTS-VALIDATION.md +29 -29
- package/docs/VERIFIED-PROJECTS.md +167 -167
- package/docs/superpowers/plans/2026-06-28-baustein-e-context-layer.md +981 -981
- package/docs/superpowers/plans/2026-06-29-baustein-f-routing.md +258 -258
- package/docs/superpowers/plans/2026-06-29-baustein-g-loop-observability.md +1006 -1006
- package/docs/superpowers/plans/2026-06-29-baustein-h-loop-robustness.md +374 -374
- package/docs/superpowers/plans/2026-06-30-baustein-i-visual-design-verification.md +450 -450
- package/docs/superpowers/plans/2026-07-02-baustein-k-zero-to-100-bootstrap.md +1024 -1024
- package/docs/superpowers/plans/2026-07-02-baustein-m-flow-smoke-proofs.md +574 -574
- package/docs/superpowers/plans/2026-08-13-gauntlet-quality-loop.md +537 -537
- package/docs/superpowers/plans/2026-08-16-artifact-backed-output-compaction.md +329 -329
- package/docs/superpowers/plans/2026-09-05-verified-projects.md +83 -83
- package/docs/superpowers/specs/2026-06-28-baustein-e-context-layer-design.md +146 -146
- package/docs/superpowers/specs/2026-06-29-baustein-f-routing-design.md +106 -106
- package/docs/superpowers/specs/2026-06-29-baustein-g-loop-observability-design.md +186 -186
- package/docs/superpowers/specs/2026-06-29-baustein-h-loop-robustness-design.md +113 -113
- package/docs/superpowers/specs/2026-06-30-baustein-i-visual-design-verification-design.md +98 -98
- package/docs/superpowers/specs/2026-07-02-baustein-k-zero-to-100-bootstrap-design.md +200 -200
- package/docs/superpowers/specs/2026-07-02-baustein-m-flow-smoke-proofs-design.md +155 -155
- package/docs/superpowers/specs/2026-08-13-gauntlet-quality-loop-design.md +422 -422
- package/docs/superpowers/specs/2026-08-16-artifact-backed-output-compaction-design.md +166 -166
- package/gemini-extension.json +6 -6
- package/hooks/hooks.json +19 -19
- package/package.json +91 -87
- package/dist/dashboard/discovery.js +0 -73
- package/docs/community-outreach-2026-08-20.md +0 -85
- package/docs/launch-copy-2026-08-21.md +0 -193
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { loadManifest } from '../../canon/manifest.js';
|
|
4
|
+
import { openCodeMcpServers, rtkInstruction } from '../tools.js';
|
|
5
|
+
import { PRESERVE_SCAFFOLD } from '../preserve.js';
|
|
6
|
+
import { skillPackageActions } from '../skill-actions.js';
|
|
7
|
+
const reviewerAgent = `---
|
|
8
|
+
description: Read-only Yoke reviewer for correctness and acceptance criteria
|
|
9
|
+
mode: primary
|
|
10
|
+
permission:
|
|
11
|
+
edit: deny
|
|
12
|
+
write: deny
|
|
13
|
+
bash: deny
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
Review the observed diff and test evidence. Do not modify files. Return only actionable findings grounded in evidence.
|
|
17
|
+
`;
|
|
18
|
+
export function planKilo(canonDir, _targetDir, codeGraph = 'graphify') {
|
|
19
|
+
const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
|
|
20
|
+
const baseline = readFileSync(join(canonDir, 'AGENTS.md'), 'utf8');
|
|
21
|
+
const actions = manifest.skills.flatMap(skill => skillPackageActions(canonDir, skill, 'kilo'));
|
|
22
|
+
actions.push({
|
|
23
|
+
kind: 'write',
|
|
24
|
+
target: 'AGENTS.md',
|
|
25
|
+
content: `${baseline.trimEnd()}\n\n${rtkInstruction()}\n\n${PRESERVE_SCAFFOLD}\n`,
|
|
26
|
+
reason: 'baseline instructions (Kilo reads AGENTS.md natively)',
|
|
27
|
+
}, {
|
|
28
|
+
kind: 'write',
|
|
29
|
+
target: 'kilo.jsonc',
|
|
30
|
+
merge: true,
|
|
31
|
+
content: JSON.stringify({
|
|
32
|
+
$schema: 'https://app.kilo.ai/config.json',
|
|
33
|
+
instructions: ['AGENTS.md', '.yoke/context/*.md'],
|
|
34
|
+
mcp: openCodeMcpServers(codeGraph),
|
|
35
|
+
}, null, 2) + '\n',
|
|
36
|
+
reason: 'Kilo instructions + MCP servers',
|
|
37
|
+
}, {
|
|
38
|
+
kind: 'write',
|
|
39
|
+
target: '.kilo/agents/yoke-reviewer.md',
|
|
40
|
+
content: reviewerAgent,
|
|
41
|
+
reason: 'Kilo read-only reviewer agent',
|
|
42
|
+
});
|
|
43
|
+
return actions;
|
|
44
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { loadManifest } from '../../canon/manifest.js';
|
|
4
|
+
import { openCodeMcpServers, rtkInstruction } from '../tools.js';
|
|
5
|
+
import { PRESERVE_SCAFFOLD } from '../preserve.js';
|
|
6
|
+
import { skillPackageActions } from '../skill-actions.js';
|
|
7
|
+
const reviewerAgent = `---
|
|
8
|
+
description: Read-only Yoke reviewer for correctness and acceptance criteria
|
|
9
|
+
mode: primary
|
|
10
|
+
tools:
|
|
11
|
+
edit: false
|
|
12
|
+
write: false
|
|
13
|
+
bash: false
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
Review the observed diff and test evidence. Do not modify files. Return only actionable findings grounded in evidence.
|
|
17
|
+
`;
|
|
18
|
+
export function planOpenCode(canonDir, _targetDir, codeGraph = 'graphify') {
|
|
19
|
+
const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
|
|
20
|
+
const baseline = readFileSync(join(canonDir, 'AGENTS.md'), 'utf8');
|
|
21
|
+
const actions = manifest.skills.flatMap(skill => skillPackageActions(canonDir, skill, 'opencode'));
|
|
22
|
+
actions.push({
|
|
23
|
+
kind: 'write',
|
|
24
|
+
target: 'AGENTS.md',
|
|
25
|
+
content: `${baseline.trimEnd()}\n\n${rtkInstruction()}\n\n${PRESERVE_SCAFFOLD}\n`,
|
|
26
|
+
reason: 'baseline instructions (OpenCode reads AGENTS.md natively)',
|
|
27
|
+
}, {
|
|
28
|
+
kind: 'write',
|
|
29
|
+
target: 'opencode.json',
|
|
30
|
+
merge: true,
|
|
31
|
+
content: JSON.stringify({
|
|
32
|
+
$schema: 'https://opencode.ai/config.json',
|
|
33
|
+
instructions: ['AGENTS.md', '.yoke/context/*.md'],
|
|
34
|
+
mcp: openCodeMcpServers(codeGraph),
|
|
35
|
+
}, null, 2) + '\n',
|
|
36
|
+
reason: 'OpenCode instructions + MCP servers',
|
|
37
|
+
}, {
|
|
38
|
+
kind: 'write',
|
|
39
|
+
target: '.opencode/agents/yoke-reviewer.md',
|
|
40
|
+
content: reviewerAgent,
|
|
41
|
+
reason: 'OpenCode read-only reviewer agent',
|
|
42
|
+
});
|
|
43
|
+
return actions;
|
|
44
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { loadManifest } from '../../canon/manifest.js';
|
|
4
|
+
import { rtkInstruction } from '../tools.js';
|
|
5
|
+
import { PRESERVE_SCAFFOLD } from '../preserve.js';
|
|
6
|
+
import { skillPackageActions } from '../skill-actions.js';
|
|
7
|
+
export function planPi(canonDir, _targetDir, _codeGraph = 'graphify') {
|
|
8
|
+
const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
|
|
9
|
+
const baseline = readFileSync(join(canonDir, 'AGENTS.md'), 'utf8');
|
|
10
|
+
const actions = manifest.skills.flatMap(skill => skillPackageActions(canonDir, skill, 'pi'));
|
|
11
|
+
actions.push({
|
|
12
|
+
kind: 'write',
|
|
13
|
+
target: 'AGENTS.md',
|
|
14
|
+
content: `${baseline.trimEnd()}\n\n${rtkInstruction()}\n\n## Pi integration\n\nPi has no MCP or native sub-agent layer; use the installed Yoke skills and the explicit Yoke loop for orchestration.\n\n${PRESERVE_SCAFFOLD}\n`,
|
|
15
|
+
reason: 'baseline instructions (Pi reads AGENTS.md natively)',
|
|
16
|
+
}, {
|
|
17
|
+
kind: 'write',
|
|
18
|
+
target: '.pi/settings.json',
|
|
19
|
+
merge: true,
|
|
20
|
+
content: JSON.stringify({ skills: ['.pi/skills'] }, null, 2) + '\n',
|
|
21
|
+
reason: 'Pi project skill discovery',
|
|
22
|
+
});
|
|
23
|
+
return actions;
|
|
24
|
+
}
|
|
@@ -47,8 +47,8 @@ export function planQwen(canonDir, _targetDir, codeGraph = 'graphify') {
|
|
|
47
47
|
actions.push({
|
|
48
48
|
kind: 'write',
|
|
49
49
|
target: '.qwen/hooks/qwen-rtk-hook.mjs',
|
|
50
|
-
content: readFileSync(join(canonDir, 'tools/
|
|
51
|
-
reason: 'portable RTK
|
|
50
|
+
content: readFileSync(join(canonDir, 'tools/qwen-rtk-hook.mjs'), 'utf8'),
|
|
51
|
+
reason: 'portable RTK PreToolUse retry guard',
|
|
52
52
|
});
|
|
53
53
|
// Merge to preserve user MCP servers, context and unrelated hooks.
|
|
54
54
|
actions.push({
|
|
@@ -58,7 +58,7 @@ export function planQwen(canonDir, _targetDir, codeGraph = 'graphify') {
|
|
|
58
58
|
content: JSON.stringify({
|
|
59
59
|
mcpServers: mcpServers(codeGraph),
|
|
60
60
|
context: { fileName: ['AGENTS.md', 'QWEN.md'] },
|
|
61
|
-
hooks: {
|
|
61
|
+
hooks: { PreToolUse: [{ matcher: '^run_shell_command$', hooks: [{ name: 'yoke-rtk', type: 'command', command: 'node .qwen/hooks/qwen-rtk-hook.mjs' }] }] },
|
|
62
62
|
}, null, 2) + '\n',
|
|
63
63
|
reason: 'MCP servers + AGENTS.md context',
|
|
64
64
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const PRESERVE_START = '<!-- yoke:preserve:start -->';
|
|
2
2
|
export const PRESERVE_END = '<!-- yoke:preserve:end -->';
|
|
3
|
-
export const PRESERVE_SCAFFOLD = `${PRESERVE_START}
|
|
4
|
-
<!-- Project-specific instructions go here. Yoke keeps this block across retrofits. -->
|
|
3
|
+
export const PRESERVE_SCAFFOLD = `${PRESERVE_START}
|
|
4
|
+
<!-- Project-specific instructions go here. Yoke keeps this block across retrofits. -->
|
|
5
5
|
${PRESERVE_END}`;
|
|
6
6
|
/**
|
|
7
7
|
* Extract the inner content of every balanced preserve-marker pair, in order.
|
|
@@ -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
|
+
}
|
|
@@ -5,6 +5,9 @@ const roots = {
|
|
|
5
5
|
codex: '.agents/skills',
|
|
6
6
|
gemini: '.gemini/skills',
|
|
7
7
|
qwen: '.qwen/skills',
|
|
8
|
+
opencode: '.opencode/skills',
|
|
9
|
+
kilo: '.kilo/skills',
|
|
10
|
+
pi: '.pi/skills',
|
|
8
11
|
};
|
|
9
12
|
function manualClaudeSkill(content, skill) {
|
|
10
13
|
const source = content.toString('utf8');
|
|
@@ -49,7 +52,7 @@ export function skillPackageActions(canonDir, skill, provider) {
|
|
|
49
52
|
.map(file => ({
|
|
50
53
|
kind: 'write',
|
|
51
54
|
target: `${roots[provider]}/${skill.id}/${file.relativePath}`,
|
|
52
|
-
content: provider === 'claude' && skill.invocation === 'manual' && file.relativePath === 'SKILL.md'
|
|
55
|
+
content: (provider === 'claude' || provider === 'qwen') && skill.invocation === 'manual' && file.relativePath === 'SKILL.md'
|
|
53
56
|
? manualClaudeSkill(file.content, skill)
|
|
54
57
|
: portableContent(file),
|
|
55
58
|
executable: file.executable,
|
package/dist/retrofit/tools.js
CHANGED
|
@@ -14,6 +14,14 @@ export function mcpServers(codeGraph = 'graphify') {
|
|
|
14
14
|
playwright: { command: 'npx', args: ['@playwright/mcp@latest'] },
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
+
/** OpenCode-family CLIs use an object with a local command array, not Claude's mcpServers shape. */
|
|
18
|
+
export function openCodeMcpServers(codeGraph = 'graphify') {
|
|
19
|
+
return Object.fromEntries(Object.entries(mcpServers(codeGraph)).map(([name, server]) => [name, {
|
|
20
|
+
type: 'local',
|
|
21
|
+
command: [server.command, ...server.args],
|
|
22
|
+
enabled: true,
|
|
23
|
+
}]));
|
|
24
|
+
}
|
|
17
25
|
// rtk has no transparent-rewrite hook on Codex/Gemini; those agents get this
|
|
18
26
|
// instruction instead. On Claude (Windows) it is also the WSL-less fallback.
|
|
19
27
|
export function rtkInstruction() {
|
package/dist/review/command.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AGENT_LIST } from '../agents/catalog.js';
|
|
1
2
|
import { agentInvocation, buildStandaloneReviewPrompt, buildWatchdogInvocation, runCapturedAgent, repositoryFingerprint, isAgentAvailable, } from '../loop/runner.js';
|
|
2
3
|
import { parseProviderResult } from '../agents/telemetry.js';
|
|
3
4
|
import { resolveIdleMs } from '../loop/run-command.js';
|
|
@@ -5,14 +6,14 @@ import { loadConfig } from '../retrofit/config.js';
|
|
|
5
6
|
import { parseReviewVerdict } from './verdict.js';
|
|
6
7
|
// Resolve to the first available agent, preferring a *second* model so the review
|
|
7
8
|
// is genuinely cross-model. claude last => a Claude-only box degrades to self-review.
|
|
8
|
-
const RESOLUTION_ORDER = ['codex', 'gemini', 'qwen', 'claude'];
|
|
9
|
+
const RESOLUTION_ORDER = ['codex', 'gemini', 'qwen', 'claude', 'opencode', 'kilo', 'pi'];
|
|
9
10
|
export function runReview(targetDir, opts = {}) {
|
|
10
11
|
const available = opts.isAvailable ?? isAgentAvailable;
|
|
11
12
|
const implementer = opts.implementer ?? loadConfig(targetDir)?.agents[0] ?? 'claude';
|
|
12
13
|
let reviewer = opts.reviewer;
|
|
13
14
|
if (reviewer) {
|
|
14
15
|
if (!available(reviewer)) {
|
|
15
|
-
console.error(`Reviewer agent CLI "${reviewer}" was not found on PATH. Install it, or pick another with --reviewer
|
|
16
|
+
console.error(`Reviewer agent CLI "${reviewer}" was not found on PATH. Install it, or pick another with --reviewer=<${AGENT_LIST}>.`);
|
|
16
17
|
return 2;
|
|
17
18
|
}
|
|
18
19
|
if (reviewer === implementer && !opts.allowSelfReview) {
|
package/dist/review/verdict.js
CHANGED
|
@@ -70,7 +70,7 @@ export function formatReviewContract(path, provider) {
|
|
|
70
70
|
return [
|
|
71
71
|
`Write your final verdict to this absolute path: ${path}`,
|
|
72
72
|
'The file must contain exactly one JSON object with this contract:',
|
|
73
|
-
`{"schemaVersion":1,"approved":boolean,"summary":"non-empty string","findings":[{"id":"optional id","severity":"blocking|warning|info","message":"non-empty string","file":"optional path","line":1,"actionable":true,"suggestedFix":"optional repair","evidence":["optional evidence reference"]}],"provenance":{"provider":"${provider ?? 'claude|codex|gemini'}","model":"provider-reported model","role":"review","promptVersion":1,"permissions":"safe"}}`,
|
|
73
|
+
`{"schemaVersion":1,"approved":boolean,"summary":"non-empty string","findings":[{"id":"optional id","severity":"blocking|warning|info","message":"non-empty string","file":"optional path","line":1,"actionable":true,"suggestedFix":"optional repair","evidence":["optional evidence reference"]}],"provenance":{"provider":"${provider ?? 'claude|codex|gemini|qwen|opencode|kilo|pi'}","model":"provider-reported model","role":"review","promptVersion":1,"permissions":"safe"}}`,
|
|
74
74
|
'Set approved=false when any blocking finding exists. Create the file even when the process also exits non-zero.',
|
|
75
75
|
].join('\n');
|
|
76
76
|
}
|
|
@@ -57,7 +57,7 @@ export function chooseCapability(input) {
|
|
|
57
57
|
const candidates = input.workers.filter(w => w.tier && tiers.indexOf(w.tier) >= level && (!input.maxTier || tiers.indexOf(w.tier) <= tiers.indexOf(input.maxTier)) && (!w.roles || w.roles.includes(role)) && (!input.story.agent || w.agent === input.story.agent) && (input.available?.(w.agent) ?? true));
|
|
58
58
|
const history = readRoutingObservations().filter(e => e.projectHash === projectHash(input.root) && e.taskClass === input.assessment.taskClass && e.requiredTier === baseTier && e.role === role && e.failureKind !== 'infrastructure' && Date.now() - Date.parse(e.recordedAt) < 30 * 86400000);
|
|
59
59
|
const evidence = (w) => {
|
|
60
|
-
const matching = history.filter(e => e.provider === w.agent && e.requestedModel === w.model && e.requestedReasoningEffort === w.reasoningEffort && e.actualModel);
|
|
60
|
+
const matching = history.filter(e => e.provider === w.agent && e.requestedProvider === w.provider && e.requestedModel === w.model && e.requestedReasoningEffort === w.reasoningEffort && e.requestedVariant === w.variant && e.actualModel);
|
|
61
61
|
const actual = matching.at(-1)?.actualModel;
|
|
62
62
|
return actual ? matching.filter(e => e.actualModel === actual) : [];
|
|
63
63
|
};
|
|
@@ -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' && provider !== 'qwen' && input.parentSelection?.bare !== undefined ? { bare: input.parentSelection.bare } : {}) }
|
|
71
|
+
const selection = worker ? { provider: worker.provider, model: worker.model, reasoningEffort: worker.reasoningEffort, variant: worker.variant, nativeMultiAgent: false, ...(provider !== 'gemini' && provider !== 'qwen' && provider !== 'pi' && 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/planning.js
CHANGED
|
@@ -4,8 +4,10 @@ export function resolvePlanner(config, start, selection = {}, override) {
|
|
|
4
4
|
const inherited = agent === start ? selection : {};
|
|
5
5
|
const planning = !override || override === (config?.planning?.agent ?? start) ? config?.planning : undefined;
|
|
6
6
|
return { agent, selection: {
|
|
7
|
+
provider: planning?.provider ?? inherited.provider,
|
|
7
8
|
model: planning?.model ?? inherited.model,
|
|
8
9
|
reasoningEffort: planning?.reasoningEffort ?? inherited.reasoningEffort,
|
|
10
|
+
variant: planning?.variant ?? inherited.variant,
|
|
9
11
|
bare: inherited.bare,
|
|
10
12
|
nativeMultiAgent: false,
|
|
11
13
|
} };
|
package/dist/routing/registry.js
CHANGED
|
@@ -68,8 +68,10 @@ export function historyForWorkers(workers) {
|
|
|
68
68
|
// Capability evidence belongs to the provider/model that produced it. This
|
|
69
69
|
// prevents a reused worker id from inheriting scores from a retired model.
|
|
70
70
|
if (event.provider !== worker.agent
|
|
71
|
+
|| event.requestedProvider !== worker.provider
|
|
71
72
|
|| event.requestedModel !== worker.model
|
|
72
|
-
|| event.requestedReasoningEffort !== worker.reasoningEffort
|
|
73
|
+
|| event.requestedReasoningEffort !== worker.reasoningEffort
|
|
74
|
+
|| event.requestedVariant !== worker.variant)
|
|
73
75
|
continue;
|
|
74
76
|
if (typeof event.verificationSuccess !== 'boolean')
|
|
75
77
|
continue;
|
package/dist/routing/router.js
CHANGED
|
@@ -101,8 +101,10 @@ function callUsage(role, provider, selection, tokens, durationMs, profile) {
|
|
|
101
101
|
role,
|
|
102
102
|
provider,
|
|
103
103
|
...(profile ? { profile } : {}),
|
|
104
|
+
...(selection.provider ? { requestedProvider: selection.provider } : {}),
|
|
104
105
|
...(selection.model ? { requestedModel: selection.model } : {}),
|
|
105
106
|
...(selection.reasoningEffort ? { requestedReasoningEffort: selection.reasoningEffort } : {}),
|
|
107
|
+
...(selection.variant ? { requestedVariant: selection.variant } : {}),
|
|
106
108
|
...(tokens?.model ? { actualModel: tokens.model } : {}),
|
|
107
109
|
inputTokens: tokens?.inputTokens ?? 0,
|
|
108
110
|
...(tokens?.cachedInputTokens !== undefined ? { cachedInputTokens: tokens.cachedInputTokens } : {}),
|
|
@@ -191,7 +193,7 @@ function routingSteps(options) {
|
|
|
191
193
|
if (!assessment)
|
|
192
194
|
return { success: false, summary: 'Routing assessment unavailable or invalid; implementation was not started', tokens: aggregateCalls(calls), routing: { recordOutcome: () => undefined, blocked: true } };
|
|
193
195
|
const choice = chooseCapability({ root, story: ctx.story, assessment, workers: eligibleWorkers, parent: options.parent, parentSelection: options.parentSelection, maxAttempts: options.maxAttempts, fallback: options.fallback, maxTier: options.maxTier });
|
|
194
|
-
options.onDecision?.(ctx.story.id, { profile: choice.worker?.id ?? 'SELF', provider: choice.provider, model: choice.selection.model, reasoningEffort: choice.selection.reasoningEffort, reason: choice.reason, next: choice.next, assessment });
|
|
196
|
+
options.onDecision?.(ctx.story.id, { profile: choice.worker?.id ?? 'SELF', provider: choice.provider, model: choice.selection.model, reasoningEffort: choice.selection.reasoningEffort, variant: choice.selection.variant, providerModel: choice.selection.provider, reason: choice.reason, next: choice.next, assessment });
|
|
195
197
|
if (choice.blocked)
|
|
196
198
|
return { ...blocked(choice.reason), tokens: aggregateCalls(calls) };
|
|
197
199
|
if (choice.exhausted)
|
|
@@ -209,7 +211,7 @@ function routingSteps(options) {
|
|
|
209
211
|
return;
|
|
210
212
|
recorded = true;
|
|
211
213
|
recordRoutingObservation({ projectHash: projectHash(root), storyHash: storyHash(projectHash(root), ctx.story.id), assessmentKey: routingAssessmentKey(root, ctx.story), taskClass: assessment.taskClass, requiredTier: choice.requiredTier,
|
|
212
|
-
role: 'implementation', strategy: 'capability', selected: choice.worker?.id ?? 'SELF', provider: choice.provider, requestedModel: choice.selection.model, requestedReasoningEffort: choice.selection.reasoningEffort,
|
|
214
|
+
role: 'implementation', strategy: 'capability', selected: choice.worker?.id ?? 'SELF', provider: choice.provider, requestedProvider: choice.selection.provider, requestedModel: choice.selection.model, requestedReasoningEffort: choice.selection.reasoningEffort, requestedVariant: choice.selection.variant,
|
|
213
215
|
actualModel: result.tokens?.model, orchestratorProvider: options.planner?.agent ?? options.parent, orchestratorModel: (options.planner?.selection ?? options.parentSelection)?.model, orchestratorDurationMs: calls.filter(c => c.role === 'orchestrator').reduce((s, c) => s + c.durationMs, 0), workerDurationMs: calls[calls.length - 1].durationMs,
|
|
214
216
|
processSuccess: result.success, verificationSuccess: infrastructureFailure ? false : verified, failureKind: infrastructureFailure ? 'infrastructure' : failureKind ?? 'implementation', usageAvailable: result.tokens !== undefined && result.tokens.measurementComplete !== false,
|
|
215
217
|
inputTokens: result.tokens?.inputTokens ?? 0, outputTokens: result.tokens?.outputTokens ?? 0, totalCostUsd: result.tokens?.totalCostUsd });
|
|
@@ -247,7 +249,7 @@ function routingSteps(options) {
|
|
|
247
249
|
return blocked('Selected routing profile exceeds configured limits; execution blocked');
|
|
248
250
|
const provider = worker?.agent ?? options.parent;
|
|
249
251
|
const selection = worker
|
|
250
|
-
? { model: worker.model, reasoningEffort: worker.reasoningEffort, nativeMultiAgent: false, ...(provider !== 'gemini' && provider !== 'qwen' ? { bare: options.parentSelection?.bare } : {}) }
|
|
252
|
+
? { provider: worker.provider, model: worker.model, reasoningEffort: worker.reasoningEffort, variant: worker.variant, nativeMultiAgent: false, ...(provider !== 'gemini' && provider !== 'qwen' && provider !== 'pi' ? { bare: options.parentSelection?.bare } : {}) }
|
|
251
253
|
: { ...(options.parentSelection ?? {}), nativeMultiAgent: false };
|
|
252
254
|
const workerStarted = now();
|
|
253
255
|
const result = yield () => makeWorker(provider, selection)(ctx);
|
|
@@ -296,6 +298,8 @@ function routingSteps(options) {
|
|
|
296
298
|
provider,
|
|
297
299
|
...(selection.model ? { requestedModel: selection.model } : {}),
|
|
298
300
|
...(selection.reasoningEffort ? { requestedReasoningEffort: selection.reasoningEffort } : {}),
|
|
301
|
+
...(selection.provider ? { requestedProvider: selection.provider } : {}),
|
|
302
|
+
...(selection.variant ? { requestedVariant: selection.variant } : {}),
|
|
299
303
|
...(result.tokens?.model ? { actualModel: result.tokens.model } : {}),
|
|
300
304
|
orchestratorProvider: options.parent,
|
|
301
305
|
...(orchestratorSelection.model ? { orchestratorModel: orchestratorSelection.model } : {}),
|
package/dist/setup/command.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
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
|
-
|
|
10
|
+
import { SUPPORTED_AGENTS } from '../agents/catalog.js';
|
|
11
|
+
const ALL_AGENTS = [...SUPPORTED_AGENTS];
|
|
8
12
|
export function defaultRoutingWorkers(agents) {
|
|
9
13
|
const workers = {
|
|
10
14
|
claude: [
|
|
@@ -26,10 +30,17 @@ export function defaultRoutingWorkers(agents) {
|
|
|
26
30
|
{ id: 'gemini-frontier', agent: 'gemini', model: 'gemini-2.5-pro', tier: 'frontier', costTier: 'high', capabilities: ['architecture'] },
|
|
27
31
|
],
|
|
28
32
|
qwen: [
|
|
29
|
-
|
|
30
|
-
{ id: 'qwen-standard', agent: 'qwen',
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
// Respect the user's Qwen Code account/model. API presets are opt-in.
|
|
34
|
+
{ id: 'qwen-standard', agent: 'qwen', tier: 'standard', costTier: 'medium', capabilities: ['implementation'] },
|
|
35
|
+
],
|
|
36
|
+
opencode: [
|
|
37
|
+
{ id: 'opencode-standard', agent: 'opencode', tier: 'standard', costTier: 'medium', capabilities: ['implementation'] },
|
|
38
|
+
],
|
|
39
|
+
kilo: [
|
|
40
|
+
{ id: 'kilo-standard', agent: 'kilo', tier: 'standard', costTier: 'medium', capabilities: ['implementation'] },
|
|
41
|
+
],
|
|
42
|
+
pi: [
|
|
43
|
+
{ id: 'pi-standard', agent: 'pi', tier: 'standard', costTier: 'medium', capabilities: ['implementation'] },
|
|
33
44
|
],
|
|
34
45
|
};
|
|
35
46
|
return agents.flatMap(agent => workers[agent]);
|
|
@@ -50,6 +61,9 @@ function yes(value, fallback) {
|
|
|
50
61
|
}
|
|
51
62
|
export async function runSetup(targetDir, opts = {}) {
|
|
52
63
|
const existing = loadConfig(targetDir);
|
|
64
|
+
const modelProviders = opts.modelProviders ?? [];
|
|
65
|
+
const presetActions = planModelPresets(targetDir, modelProviders);
|
|
66
|
+
const presetWorkers = modelPresetWorkers(modelProviders);
|
|
53
67
|
const detected = detectProject(targetDir);
|
|
54
68
|
const host = opts.host ?? detectHostAgent();
|
|
55
69
|
const configuredAgents = existing?.agents.filter(a => ALL_AGENTS.includes(a)) ?? [];
|
|
@@ -59,7 +73,7 @@ export async function runSetup(targetDir, opts = {}) {
|
|
|
59
73
|
? configuredAgents
|
|
60
74
|
: detected.agents.length > 0
|
|
61
75
|
? detected.agents
|
|
62
|
-
: [host ?? 'claude'];
|
|
76
|
+
: modelProviders.length ? ['qwen'] : [host ?? 'claude'];
|
|
63
77
|
const defaultGraph = opts.codeGraph ?? existing?.codeGraph ?? 'graphify';
|
|
64
78
|
const defaultLoop = opts.loop ?? existing?.loop.enabled ?? true;
|
|
65
79
|
const defaultRunner = opts.runner ?? existing?.runner?.agent ?? (host && defaultAgents.includes(host) ? host : defaultAgents[0] ?? host ?? 'claude');
|
|
@@ -81,12 +95,12 @@ export async function runSetup(targetDir, opts = {}) {
|
|
|
81
95
|
let decisionPolicy = defaultPolicy;
|
|
82
96
|
let routing = defaultRouting;
|
|
83
97
|
if (interactive && ask) {
|
|
84
|
-
agents = parseAgents(await ask(`Agents [${defaultAgents.join(',')}] (
|
|
98
|
+
agents = parseAgents(await ask(`Agents [${defaultAgents.join(',')}] (${SUPPORTED_AGENTS.join(',')}|all): `), defaultAgents);
|
|
85
99
|
const graphAnswer = (await ask(`Code graph [${defaultGraph}] (graphify|serena): `)).trim().toLowerCase();
|
|
86
100
|
if (graphAnswer === 'graphify' || graphAnswer === 'serena')
|
|
87
101
|
codeGraph = graphAnswer;
|
|
88
102
|
loop = yes(await ask(`Enable autonomous loop? [${defaultLoop ? 'yes' : 'no'}]: `), defaultLoop);
|
|
89
|
-
const runnerAnswer = (await ask(`Default runner [${runner}] (
|
|
103
|
+
const runnerAnswer = (await ask(`Default runner [${runner}] (${SUPPORTED_AGENTS.join('|')}): `)).trim().toLowerCase();
|
|
90
104
|
if (ALL_AGENTS.includes(runnerAnswer))
|
|
91
105
|
runner = runnerAnswer;
|
|
92
106
|
const policyAnswer = (await ask(`Decision mode [${decisionPolicy}] (auto|critical): `)).trim().toLowerCase();
|
|
@@ -94,17 +108,26 @@ export async function runSetup(targetDir, opts = {}) {
|
|
|
94
108
|
decisionPolicy = policyAnswer;
|
|
95
109
|
routing = yes(await ask(`Enable adaptive multi-model routing? [${defaultRouting ? 'yes' : 'no'}]: `), defaultRouting);
|
|
96
110
|
}
|
|
111
|
+
if (modelProviders.length && !agents.includes('qwen'))
|
|
112
|
+
agents = [...agents, 'qwen'];
|
|
97
113
|
if (!agents.includes(runner))
|
|
98
114
|
agents = [...agents, runner];
|
|
99
115
|
const code = runRetrofit(targetDir, { loop, agents, codeGraph, host });
|
|
100
116
|
if (code !== 0)
|
|
101
117
|
return code;
|
|
118
|
+
applyActions(presetActions, targetDir, { backupDir: join(targetDir, '.yoke', 'backups', `model-presets-${Date.now()}`) });
|
|
102
119
|
const config = loadConfig(targetDir);
|
|
103
120
|
if (!config)
|
|
104
121
|
return 1;
|
|
105
122
|
config.loop = { parallel: 'auto', isolate: true, ...config.loop, enabled: loop, decisionPolicy };
|
|
106
|
-
|
|
123
|
+
const priorRunner = existing?.runner?.agent ?? existing?.agents[0];
|
|
124
|
+
const selectPresetModel = presetWorkers.length > 0 && runner === 'qwen' && (!existing || (priorRunner !== undefined && priorRunner !== runner));
|
|
125
|
+
if (selectPresetModel && priorRunner !== 'qwen')
|
|
126
|
+
config.runner = { permissions: config.runner?.permissions };
|
|
127
|
+
config.runner = { ...config.runner, agent: runner, ...(selectPresetModel && !config.runner?.model ? { model: presetWorkers[0].model } : {}) };
|
|
107
128
|
const existingWorkers = config.routing?.workers ?? [];
|
|
129
|
+
const baseWorkers = existingWorkers.length > 0 && !opts.routingPreset ? existingWorkers : defaultRoutingWorkers(agents).filter(worker => !modelProviders.length || worker.agent !== 'qwen');
|
|
130
|
+
const workers = [...baseWorkers, ...presetWorkers.filter(worker => !baseWorkers.some(existing => existing.id === worker.id))];
|
|
108
131
|
config.routing = {
|
|
109
132
|
...config.routing,
|
|
110
133
|
enabled: routing,
|
|
@@ -113,8 +136,9 @@ export async function runSetup(targetDir, opts = {}) {
|
|
|
113
136
|
assessmentPolicy: existing?.routing?.assessmentPolicy ?? (existing ? 'on-demand' : 'prepared'),
|
|
114
137
|
fallback: existing?.routing?.fallback ?? (existing ? 'parent' : 'block'),
|
|
115
138
|
...(config.routing?.orchestrator ? { orchestrator: config.routing.orchestrator } : {}),
|
|
116
|
-
workers
|
|
139
|
+
workers,
|
|
117
140
|
};
|
|
141
|
+
YokeConfigSchema.parse(config);
|
|
118
142
|
saveConfig(targetDir, config);
|
|
119
143
|
console.log(`Yoke setup complete: agents=${agents.join(',')} · runner=${runner} · loop=${loop ? 'on' : 'off'} · routing=${routing ? 'on' : 'off'} · decisions=${decisionPolicy}`);
|
|
120
144
|
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
|
+
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
# Routing by task requirements
|
|
2
|
-
|
|
1
|
+
# Routing by task requirements
|
|
2
|
+
|
|
3
3
|
Capability routing is available in Yoke 1.9.0. Batch preparation, separate planning settings and routing limits described below are local, unreleased additions.
|
|
4
|
-
|
|
5
|
-
New setups use `routing.strategy: capability`. Existing explicit strategies and profiles remain unchanged. To opt an existing project into capability routing with its current profiles:
|
|
6
|
-
|
|
7
|
-
```sh
|
|
8
|
-
yoke setup . --yes --routing --routing-strategy=capability
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Give each existing worker a `tier: light|standard|strong|frontier`. Profiles without a tier remain usable with legacy strategies but are not candidates for capability selection. To explicitly replace worker profiles with the supplied provider presets, add `--routing-preset`. This replaces customized worker profiles; omit it to retain them.
|
|
12
|
-
|
|
13
|
-
## Planning and selection
|
|
14
|
-
|
|
4
|
+
|
|
5
|
+
New setups use `routing.strategy: capability`. Existing explicit strategies and profiles remain unchanged. To opt an existing project into capability routing with its current profiles:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
yoke setup . --yes --routing --routing-strategy=capability
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Give each existing worker a `tier: light|standard|strong|frontier`. Profiles without a tier remain usable with legacy strategies but are not candidates for capability selection. To explicitly replace worker profiles with the supplied provider presets, add `--routing-preset`. This replaces customized worker profiles; omit it to retain them.
|
|
12
|
+
|
|
13
|
+
## Planning and selection
|
|
14
|
+
|
|
15
15
|
The start provider/model remains the planning default. Optional `planning.agent`, `planning.model` and `planning.reasoningEffort` select a separate planner without changing the execution model. Draft and change-inbox planning request complete assessments in the same pass that creates the tasks. The inbox still performs its separate coverage review.
|
|
16
16
|
|
|
17
17
|
New setups use `routing.assessmentPolicy: prepared` and `routing.fallback: block`. Before dispatch, each unfinished task must have 2–5 executable criteria and a current assessment. No per-task planning call runs in this mode. Existing configurations retain `on-demand` and `parent` unless explicitly changed; on-demand routing makes a read-only planning call for an unassessed task and caches its result.
|
|
@@ -41,42 +41,42 @@ routing:
|
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
`maxTier` limits automatic execution and escalation, including routing-rule selections. If a task needs frontier while the ceiling is strong, it blocks; Yoke does not lower the required capability. Planning itself may still use Astra. Explicit quality-role model overrides retain precedence. Goal execution retains its own protected manifest and budgets, uses the configured planner on demand, and honors routing fallback/tier limits; PRD preparation policy does not apply to synthetic goal tasks.
|
|
44
|
-
|
|
45
|
-
An assessment is a planning judgment, not a measured success probability. High testability means executable checks can detect an incorrect implementation. High uncertainty, architecture work or high risk require the frontier tier; difficult or broadly coupled work requires strong; routine implementation requires standard. Light is reserved for clear, low-risk mechanical work with strong checks. Weak testability raises the minimum tier. Reviews and critics have a standard minimum even for light tasks.
|
|
46
|
-
|
|
47
|
-
```yaml
|
|
48
|
-
assessment:
|
|
49
|
-
taskClass: implementation
|
|
50
|
-
difficulty: medium
|
|
51
|
-
uncertainty: low
|
|
52
|
-
risk: low
|
|
53
|
-
scope: low
|
|
54
|
-
testability: high
|
|
55
|
-
reason: Existing handler pattern and executable contract tests
|
|
56
|
-
approach: Extend the handler, cover the boundary cases, run contract tests
|
|
57
|
-
```
|
|
58
|
-
|
|
44
|
+
|
|
45
|
+
An assessment is a planning judgment, not a measured success probability. High testability means executable checks can detect an incorrect implementation. High uncertainty, architecture work or high risk require the frontier tier; difficult or broadly coupled work requires strong; routine implementation requires standard. Light is reserved for clear, low-risk mechanical work with strong checks. Weak testability raises the minimum tier. Reviews and critics have a standard minimum even for light tasks.
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
assessment:
|
|
49
|
+
taskClass: implementation
|
|
50
|
+
difficulty: medium
|
|
51
|
+
uncertainty: low
|
|
52
|
+
risk: low
|
|
53
|
+
scope: low
|
|
54
|
+
testability: high
|
|
55
|
+
reason: Existing handler pattern and executable contract tests
|
|
56
|
+
approach: Extend the handler, cover the boundary cases, run contract tests
|
|
57
|
+
```
|
|
58
|
+
|
|
59
59
|
Yoke chooses an eligible profile at or above the required tier, then compares declared cost tiers. Optional `roles: [implementation, reviewer, critic, repair]` limits a profile's uses. Task `agent` affinity restricts implementation to that provider. Explicit routing rules and explicit quality role models retain precedence. With legacy `fallback: parent` and no tier ceiling, a missing suitable profile falls back to the start model (or the explicitly bound provider's default) and labels the fallback; it does not prove sufficient capability. `fallback: block` or a configured tier ceiling prevents that fallback. An invalid assessment blocks implementation.
|
|
60
|
-
|
|
61
|
-
## Initial profiles
|
|
62
|
-
|
|
63
|
-
| Tier | Codex | Claude | Gemini |
|
|
64
|
-
| --- | --- | --- | --- |
|
|
65
|
-
| light | gpt-5.6-luna, low | haiku | gemini-2.5-flash |
|
|
66
|
-
| standard | gpt-5.6-terra, medium | sonnet | gemini-2.5-pro |
|
|
67
|
-
| strong | gpt-5.6-sol, high | sonnet, high effort | gemini-2.5-pro |
|
|
68
|
-
| frontier | gpt-6-astra, high | opus | gemini-2.5-pro |
|
|
69
|
-
|
|
70
|
-
These are editable starting hypotheses, not measured equivalences or price claims. The Codex names follow the requested profile family. Account access is not established by finding an installed CLI. Gemini uses documented explicit model IDs and receives no unsupported reasoning-effort parameter. Several Gemini tiers deliberately share Pro; moving between those tiers alone is not a stronger-model transition. Adjust the presets to the models available to your account. Claude aliases can resolve to different concrete models over time. Provider-reported model identity remains separate from requested identity.
|
|
71
|
-
|
|
72
|
-
Provider references: [Claude model configuration](https://code.claude.com/docs/en/model-config), [Gemini model selection](https://geminicli.com/docs/cli/model/).
|
|
73
|
-
|
|
74
|
-
## Repair, escalation and evidence
|
|
75
|
-
|
|
76
|
-
After an independent mechanical failure, capability routing permits one targeted repair at the initial tier, then raises the required tier on further failures. Attempts retain the current worktree and receive the previous gate findings. Every returned candidate still passes the normal acceptance, protection, quality, review and integration gates. Critical decisions, pause/cancellation and protected-acceptance violations stop retries. Provider process failures are classified conservatively as infrastructure; they do not count as evidence that a stronger model is needed.
|
|
77
|
-
|
|
78
|
-
`routing.maxAttempts` limits implementation calls per unchanged task contract (default 5, configurable 1–8). The initial tier imposes an additional bound: light at most 5, standard 4, strong 3, frontier 2. An exhausted task blocks and requires a revised plan. These are inner implementation attempts; the outer loop's iteration count still counts task dispatches. Existing quality repair rounds and time limits remain separate bounds, and quality repairs can raise their profile tier by round. Goal execution keeps its existing global attempt, time and token budgets.
|
|
79
|
-
|
|
80
|
-
Routing observations record task class, required tier, requested and reported models, effort, independent result, duration and available consumption. Selection considers matching project/task-class/tier history from the last 30 days within the bounded registry read. At least ten matching observations are required before an observed success rate below 80% excludes a profile. History is scoped to the concrete reported model to avoid mixing changed aliases. This is a conservative exclusion rule; it does not lower the planner's safety floor or claim calibrated probabilities. Missing usage remains unknown. Financial optimization and cross-provider performance require authenticated benchmarks.
|
|
81
|
-
|
|
82
|
-
The dashboard's Now view shows the last recorded implementation profile, requested model/effort, rationale and next escalation tier. Usage & time retains reported model and role consumption, including assessment calls. Cached planning has no new model-call charge. Routing state is local runtime data and excluded from Yoke story commits.
|
|
60
|
+
|
|
61
|
+
## Initial profiles
|
|
62
|
+
|
|
63
|
+
| Tier | Codex | Claude | Gemini |
|
|
64
|
+
| --- | --- | --- | --- |
|
|
65
|
+
| light | gpt-5.6-luna, low | haiku | gemini-2.5-flash |
|
|
66
|
+
| standard | gpt-5.6-terra, medium | sonnet | gemini-2.5-pro |
|
|
67
|
+
| strong | gpt-5.6-sol, high | sonnet, high effort | gemini-2.5-pro |
|
|
68
|
+
| frontier | gpt-6-astra, high | opus | gemini-2.5-pro |
|
|
69
|
+
|
|
70
|
+
These are editable starting hypotheses, not measured equivalences or price claims. The Codex names follow the requested profile family. Account access is not established by finding an installed CLI. Gemini uses documented explicit model IDs and receives no unsupported reasoning-effort parameter. Several Gemini tiers deliberately share Pro; moving between those tiers alone is not a stronger-model transition. Adjust the presets to the models available to your account. Claude aliases can resolve to different concrete models over time. Provider-reported model identity remains separate from requested identity.
|
|
71
|
+
|
|
72
|
+
Provider references: [Claude model configuration](https://code.claude.com/docs/en/model-config), [Gemini model selection](https://geminicli.com/docs/cli/model/).
|
|
73
|
+
|
|
74
|
+
## Repair, escalation and evidence
|
|
75
|
+
|
|
76
|
+
After an independent mechanical failure, capability routing permits one targeted repair at the initial tier, then raises the required tier on further failures. Attempts retain the current worktree and receive the previous gate findings. Every returned candidate still passes the normal acceptance, protection, quality, review and integration gates. Critical decisions, pause/cancellation and protected-acceptance violations stop retries. Provider process failures are classified conservatively as infrastructure; they do not count as evidence that a stronger model is needed.
|
|
77
|
+
|
|
78
|
+
`routing.maxAttempts` limits implementation calls per unchanged task contract (default 5, configurable 1–8). The initial tier imposes an additional bound: light at most 5, standard 4, strong 3, frontier 2. An exhausted task blocks and requires a revised plan. These are inner implementation attempts; the outer loop's iteration count still counts task dispatches. Existing quality repair rounds and time limits remain separate bounds, and quality repairs can raise their profile tier by round. Goal execution keeps its existing global attempt, time and token budgets.
|
|
79
|
+
|
|
80
|
+
Routing observations record task class, required tier, requested and reported models, effort, independent result, duration and available consumption. Selection considers matching project/task-class/tier history from the last 30 days within the bounded registry read. At least ten matching observations are required before an observed success rate below 80% excludes a profile. History is scoped to the concrete reported model to avoid mixing changed aliases. This is a conservative exclusion rule; it does not lower the planner's safety floor or claim calibrated probabilities. Missing usage remains unknown. Financial optimization and cross-provider performance require authenticated benchmarks.
|
|
81
|
+
|
|
82
|
+
The dashboard's Now view shows the last recorded implementation profile, requested model/effort, rationale and next escalation tier. Usage & time retains reported model and role consumption, including assessment calls. Cached planning has no new model-call charge. Routing state is local runtime data and excluded from Yoke story commits.
|