@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.
Files changed (155) hide show
  1. package/.claude-plugin/plugin.json +13 -13
  2. package/.codex-plugin/plugin.json +7 -7
  3. package/CHANGELOG.md +435 -398
  4. package/README.md +943 -915
  5. package/TODOS.md +5 -5
  6. package/agents/docs.toml +6 -6
  7. package/agents/implementer.toml +6 -6
  8. package/agents/reviewer.toml +6 -6
  9. package/agents/security.toml +6 -6
  10. package/bench/README.md +86 -86
  11. package/bench/RESULTS.md +35 -35
  12. package/bench/output-compaction.mjs +65 -65
  13. package/bench/result-schema.mjs +12 -12
  14. package/bench/results/claude-2026-07-27T18-03-26.json +50 -50
  15. package/bench/results/codex-unavailable-1785175418318.json +15 -15
  16. package/bench/results/gemini-2026-07-27T18-03-44.json +46 -46
  17. package/bench/run-matrix.mjs +26 -26
  18. package/bench/run.mjs +106 -106
  19. package/canon/AGENTS.md +30 -30
  20. package/canon/context/DECISIONS.md +4 -4
  21. package/canon/context/GLOSSARY.md +11 -11
  22. package/canon/context/KNOWLEDGE.md +4 -4
  23. package/canon/context/PROJECT.md +15 -15
  24. package/canon/loop/loop-spec.md +65 -65
  25. package/canon/loop/prd.schema.md +41 -41
  26. package/canon/manifest.yaml +59 -59
  27. package/canon/policy/gates.md +7 -7
  28. package/canon/policy/roles.md +9 -9
  29. package/canon/skills/ATTRIBUTION.md +99 -99
  30. package/canon/skills/authoring-prd/SKILL.md +57 -57
  31. package/canon/skills/brainstorming/SKILL.md +164 -164
  32. package/canon/skills/codebase-design/DEEPENING.md +15 -15
  33. package/canon/skills/codebase-design/DESIGN-IT-TWICE.md +12 -12
  34. package/canon/skills/codebase-design/SKILL.md +39 -39
  35. package/canon/skills/dispatching-parallel-agents/SKILL.md +182 -182
  36. package/canon/skills/document-release/SKILL.md +302 -302
  37. package/canon/skills/domain-modeling/ADR-FORMAT.md +19 -19
  38. package/canon/skills/domain-modeling/CONTEXT-FORMAT.md +39 -39
  39. package/canon/skills/domain-modeling/SKILL.md +35 -35
  40. package/canon/skills/executing-plans/SKILL.md +70 -70
  41. package/canon/skills/finishing-a-development-branch/SKILL.md +200 -200
  42. package/canon/skills/health/SKILL.md +177 -177
  43. package/canon/skills/maintaining-context/SKILL.md +34 -34
  44. package/canon/skills/minimal-code/SKILL.md +21 -21
  45. package/canon/skills/no-ai-slop/SKILL.md +103 -103
  46. package/canon/skills/no-ai-slop/eval.md +43 -43
  47. package/canon/skills/plan-ceo-review/SKILL.md +541 -541
  48. package/canon/skills/plan-eng-review/SKILL.md +362 -362
  49. package/canon/skills/receiving-code-review/SKILL.md +213 -213
  50. package/canon/skills/requesting-code-review/SKILL.md +105 -105
  51. package/canon/skills/resolving-merge-conflicts/SKILL.md +18 -18
  52. package/canon/skills/retro/SKILL.md +397 -397
  53. package/canon/skills/review/SKILL.md +246 -246
  54. package/canon/skills/ship/SKILL.md +691 -691
  55. package/canon/skills/subagent-driven-development/SKILL.md +277 -277
  56. package/canon/skills/systematic-debugging/SKILL.md +296 -296
  57. package/canon/skills/tdd/SKILL.md +371 -371
  58. package/canon/skills/unslop-ui/SKILL.md +34 -34
  59. package/canon/skills/using-git-worktrees/SKILL.md +218 -218
  60. package/canon/skills/verification-before-completion/SKILL.md +139 -139
  61. package/canon/skills/visual-verification/SKILL.md +54 -54
  62. package/canon/skills/workflow/SKILL.md +22 -22
  63. package/canon/skills/writing-for-agents/SKILL-MECHANICS.md +27 -27
  64. package/canon/skills/writing-for-agents/SKILL.md +42 -42
  65. package/canon/skills/writing-plans/SKILL.md +152 -152
  66. package/canon/skills/writing-skills/SKILL.md +655 -655
  67. package/canon/skills/yoke-retrofit/SKILL.md +26 -26
  68. package/canon/skills/yoke-workflow/SKILL.md +20 -20
  69. package/canon/tools/codex-rtk-hook.mjs +35 -35
  70. package/canon/tools/gemini-rtk-hook.mjs +25 -25
  71. package/canon/tools/graphify.md +3 -3
  72. package/canon/tools/playwright-mcp.md +3 -3
  73. package/canon/tools/qwen-rtk-hook.mjs +25 -0
  74. package/canon/tools/rtk.md +7 -7
  75. package/canon/tools/serena.md +6 -6
  76. package/dist/agents/catalog.js +7 -0
  77. package/dist/agents/contracts.js +3 -1
  78. package/dist/agents/host.js +5 -1
  79. package/dist/agents/process-streams.js +62 -0
  80. package/dist/agents/process.js +43 -3
  81. package/dist/agents/providers.js +61 -6
  82. package/dist/agents/telemetry.js +133 -37
  83. package/dist/canon/manifest.js +2 -1
  84. package/dist/change/inbox.js +1 -1
  85. package/dist/cli.js +30 -24
  86. package/dist/dashboard/page.js +122 -122
  87. package/dist/dashboard/panels.js +91 -91
  88. package/dist/goals/command.js +3 -2
  89. package/dist/loop/claims.js +2 -1
  90. package/dist/loop/decision.js +3 -2
  91. package/dist/loop/parallel-command.js +4 -2
  92. package/dist/loop/prd.js +2 -1
  93. package/dist/loop/reporter.js +1 -0
  94. package/dist/loop/run-command.js +31 -10
  95. package/dist/prd/command.js +19 -19
  96. package/dist/quality/candidate-comparison.js +6 -1
  97. package/dist/quality/command.js +17 -2
  98. package/dist/quality/types.js +6 -1
  99. package/dist/retrofit/apply.js +95 -2
  100. package/dist/retrofit/config.js +9 -1
  101. package/dist/retrofit/detect.js +8 -0
  102. package/dist/retrofit/plan.js +6 -0
  103. package/dist/retrofit/planners/claude.js +14 -14
  104. package/dist/retrofit/planners/kilo.js +44 -0
  105. package/dist/retrofit/planners/opencode.js +44 -0
  106. package/dist/retrofit/planners/pi.js +24 -0
  107. package/dist/retrofit/planners/qwen.js +3 -3
  108. package/dist/retrofit/preserve.js +2 -2
  109. package/dist/retrofit/qwen-settings.js +17 -0
  110. package/dist/retrofit/skill-actions.js +4 -1
  111. package/dist/retrofit/tools.js +8 -0
  112. package/dist/review/command.js +3 -2
  113. package/dist/review/verdict.js +1 -1
  114. package/dist/routing/capability.js +2 -2
  115. package/dist/routing/planning.js +2 -0
  116. package/dist/routing/registry.js +3 -1
  117. package/dist/routing/router.js +7 -3
  118. package/dist/setup/command.js +35 -11
  119. package/dist/setup/model-presets.js +48 -0
  120. package/docs/CAPABILITY-ROUTING.md +51 -51
  121. package/docs/DASHBOARD-EVOLUTION.md +33 -33
  122. package/docs/HARNESSES.md +81 -0
  123. package/docs/MIGRATING-TO-1.0.md +33 -33
  124. package/docs/MIGRATING-TO-1.1.md +27 -27
  125. package/docs/MIGRATING-TO-1.4.md +70 -70
  126. package/docs/PRODUCT-DIRECTION-2026-09-05.md +210 -210
  127. package/docs/PUBLISHING.md +114 -114
  128. package/docs/QWEN-MODEL-SUPPORT.md +142 -0
  129. package/docs/VERIFIED-PROJECTS-VALIDATION.md +29 -29
  130. package/docs/VERIFIED-PROJECTS.md +167 -167
  131. package/docs/superpowers/plans/2026-06-28-baustein-e-context-layer.md +981 -981
  132. package/docs/superpowers/plans/2026-06-29-baustein-f-routing.md +258 -258
  133. package/docs/superpowers/plans/2026-06-29-baustein-g-loop-observability.md +1006 -1006
  134. package/docs/superpowers/plans/2026-06-29-baustein-h-loop-robustness.md +374 -374
  135. package/docs/superpowers/plans/2026-06-30-baustein-i-visual-design-verification.md +450 -450
  136. package/docs/superpowers/plans/2026-07-02-baustein-k-zero-to-100-bootstrap.md +1024 -1024
  137. package/docs/superpowers/plans/2026-07-02-baustein-m-flow-smoke-proofs.md +574 -574
  138. package/docs/superpowers/plans/2026-08-13-gauntlet-quality-loop.md +537 -537
  139. package/docs/superpowers/plans/2026-08-16-artifact-backed-output-compaction.md +329 -329
  140. package/docs/superpowers/plans/2026-09-05-verified-projects.md +83 -83
  141. package/docs/superpowers/specs/2026-06-28-baustein-e-context-layer-design.md +146 -146
  142. package/docs/superpowers/specs/2026-06-29-baustein-f-routing-design.md +106 -106
  143. package/docs/superpowers/specs/2026-06-29-baustein-g-loop-observability-design.md +186 -186
  144. package/docs/superpowers/specs/2026-06-29-baustein-h-loop-robustness-design.md +113 -113
  145. package/docs/superpowers/specs/2026-06-30-baustein-i-visual-design-verification-design.md +98 -98
  146. package/docs/superpowers/specs/2026-07-02-baustein-k-zero-to-100-bootstrap-design.md +200 -200
  147. package/docs/superpowers/specs/2026-07-02-baustein-m-flow-smoke-proofs-design.md +155 -155
  148. package/docs/superpowers/specs/2026-08-13-gauntlet-quality-loop-design.md +422 -422
  149. package/docs/superpowers/specs/2026-08-16-artifact-backed-output-compaction-design.md +166 -166
  150. package/gemini-extension.json +6 -6
  151. package/hooks/hooks.json +19 -19
  152. package/package.json +91 -87
  153. package/dist/dashboard/discovery.js +0 -73
  154. package/docs/community-outreach-2026-08-20.md +0 -85
  155. 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/gemini-rtk-hook.mjs'), 'utf8'),
51
- reason: 'portable RTK BeforeTool argument adapter',
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: { BeforeTool: [{ matcher: '^run_shell_command$', hooks: [{ name: 'yoke-rtk', type: 'command', command: 'node .qwen/hooks/qwen-rtk-hook.mjs' }] }] },
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,
@@ -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() {
@@ -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=<claude|codex|gemini>.`);
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) {
@@ -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' };
@@ -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
  } };
@@ -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;
@@ -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 } : {}),
@@ -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
- const ALL_AGENTS = ['claude', 'codex', 'gemini', 'qwen'];
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
- { id: 'qwen-light', agent: 'qwen', model: 'qwen-turbo-latest', tier: 'light', costTier: 'low', capabilities: ['mechanical', 'tests'] },
30
- { id: 'qwen-standard', agent: 'qwen', model: 'qwen3-coder-plus', tier: 'standard', costTier: 'medium', capabilities: ['implementation'] },
31
- { id: 'qwen-strong', agent: 'qwen', model: 'qwen3-coder-plus', tier: 'strong', costTier: 'medium', capabilities: ['debugging'] },
32
- { id: 'qwen-frontier', agent: 'qwen', model: 'qwen3-235b-a22b', tier: 'frontier', costTier: 'high', capabilities: ['architecture'] },
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(',')}] (claude,codex,gemini,qwen|all): `), defaultAgents);
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}] (claude|codex|gemini|qwen): `)).trim().toLowerCase();
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
- config.runner = { ...config.runner, agent: runner };
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: existingWorkers.length > 0 && !opts.routingPreset ? existingWorkers : defaultRoutingWorkers(agents),
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.