@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
@@ -12,6 +12,13 @@ function parseJson(value) {
12
12
  function isRecord(value) {
13
13
  return typeof value === 'object' && value !== null && !Array.isArray(value);
14
14
  }
15
+ function textContent(value) {
16
+ if (typeof value === 'string')
17
+ return value;
18
+ if (!Array.isArray(value))
19
+ return '';
20
+ return value.filter(isRecord).filter(part => part.type === 'text' && typeof part.text === 'string').map(part => part.text).join('');
21
+ }
15
22
  function directMachineResult(value) {
16
23
  return isRecord(value) && value.schemaVersion === 1 ? value : undefined;
17
24
  }
@@ -22,6 +29,8 @@ export function parseProviderResult(agent, output) {
22
29
  if (direct !== undefined)
23
30
  return direct;
24
31
  }
32
+ if (agent === 'qwen')
33
+ return parseQwenResult(output);
25
34
  const fragments = [];
26
35
  for (const line of output.split(/\r?\n/u)) {
27
36
  const parsed = parseJson(line);
@@ -45,9 +54,19 @@ export function parseProviderResult(agent, output) {
45
54
  if (event.type === 'message' && event.role === 'assistant' && typeof event.content === 'string')
46
55
  fragments.push(event.content);
47
56
  break;
48
- case 'qwen':
49
- if (event.type === 'message' && event.role === 'assistant' && typeof event.content === 'string')
50
- fragments.push(event.content);
57
+ case 'opencode':
58
+ case 'kilo': {
59
+ const part = isRecord(event.part) ? event.part : undefined;
60
+ if (event.type === 'text' && typeof part?.text === 'string')
61
+ fragments.push(part.text);
62
+ break;
63
+ }
64
+ case 'pi':
65
+ if (event.type === 'message_end' && isRecord(event.message) && event.message.role === 'assistant') {
66
+ const text = textContent(event.message.content);
67
+ if (text)
68
+ fragments.push(text);
69
+ }
51
70
  break;
52
71
  }
53
72
  }
@@ -67,6 +86,34 @@ export function parseProviderResult(agent, output) {
67
86
  }
68
87
  return null;
69
88
  }
89
+ /** Qwen uses assistant content blocks and result envelopes, not Gemini messages. */
90
+ function parseQwenResult(output) {
91
+ let candidate = null;
92
+ for (const line of output.split(/\r?\n/u)) {
93
+ const parsed = parseJson(line);
94
+ if (!parsed.ok || !isRecord(parsed.value))
95
+ continue;
96
+ const event = parsed.value;
97
+ if (event.parent_tool_use_id != null)
98
+ continue;
99
+ if (event.type === 'result') {
100
+ if (event.is_error === true) {
101
+ candidate = null;
102
+ continue;
103
+ }
104
+ const structured = directMachineResult(event.structured_result);
105
+ const result = typeof event.result === 'string' ? parseJson(event.result) : { ok: false };
106
+ candidate = structured ?? (result.ok ? directMachineResult(result.value) : undefined) ?? null;
107
+ }
108
+ else if (event.type === 'assistant' && isRecord(event.message) && Array.isArray(event.message.content)) {
109
+ const text = event.message.content.filter(isRecord).filter(part => part.type === 'text' && typeof part.text === 'string').map(part => part.text).join('');
110
+ const result = parseJson(text);
111
+ if (result.ok)
112
+ candidate = directMachineResult(result.value) ?? candidate;
113
+ }
114
+ }
115
+ return candidate;
116
+ }
70
117
  export function parseProviderTelemetry(agent, lines) {
71
118
  let inputTokens;
72
119
  let cachedInputTokens;
@@ -76,6 +123,10 @@ export function parseProviderTelemetry(agent, lines) {
76
123
  let totalCostUsd;
77
124
  let model;
78
125
  let reportedModels = [];
126
+ const harnessTotals = agent === 'opencode' || agent === 'kilo'
127
+ ? { input: 0, output: 0, cached: 0, cacheWrite: 0, reasoning: 0, cost: 0, hasInput: false, hasOutput: false, hasCached: false, hasCacheWrite: false, hasReasoning: false, hasCost: false }
128
+ : undefined;
129
+ let piUsage;
79
130
  for (const line of lines) {
80
131
  let parsed;
81
132
  try {
@@ -87,8 +138,47 @@ export function parseProviderTelemetry(agent, lines) {
87
138
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
88
139
  continue;
89
140
  const event = parsed;
141
+ if (agent === 'qwen' && event.parent_tool_use_id != null)
142
+ continue;
90
143
  const message = event.message && typeof event.message === 'object' ? event.message : undefined;
91
144
  const stats = event.stats && typeof event.stats === 'object' ? event.stats : undefined;
145
+ const part = event.part && typeof event.part === 'object' ? event.part : undefined;
146
+ const partTokens = part?.tokens && typeof part.tokens === 'object' ? part.tokens : undefined;
147
+ if (harnessTotals && (event.type === 'step_finish' || part?.type === 'step-finish') && partTokens) {
148
+ const cache = partTokens.cache && typeof partTokens.cache === 'object' ? partTokens.cache : undefined;
149
+ const stepInput = finite(partTokens.input);
150
+ const stepOutput = finite(partTokens.output);
151
+ const stepCached = finite(partTokens.cached ?? partTokens.cacheRead ?? cache?.read);
152
+ const stepCacheWrite = finite(partTokens.cacheWrite ?? cache?.write);
153
+ const stepReasoning = finite(partTokens.reasoning);
154
+ const stepCost = finite(part?.cost ?? event.cost);
155
+ if (stepInput !== undefined) {
156
+ harnessTotals.input += stepInput;
157
+ harnessTotals.hasInput = true;
158
+ }
159
+ if (stepOutput !== undefined) {
160
+ harnessTotals.output += stepOutput;
161
+ harnessTotals.hasOutput = true;
162
+ }
163
+ if (stepCached !== undefined) {
164
+ harnessTotals.cached += stepCached;
165
+ harnessTotals.hasCached = true;
166
+ }
167
+ if (stepCacheWrite !== undefined) {
168
+ harnessTotals.cacheWrite += stepCacheWrite;
169
+ harnessTotals.hasCacheWrite = true;
170
+ }
171
+ if (stepReasoning !== undefined) {
172
+ harnessTotals.reasoning += stepReasoning;
173
+ harnessTotals.hasReasoning = true;
174
+ }
175
+ if (stepCost !== undefined) {
176
+ harnessTotals.cost += stepCost;
177
+ harnessTotals.hasCost = true;
178
+ }
179
+ }
180
+ if (agent === 'pi' && event.type === 'message_update' && event.usage && typeof event.usage === 'object')
181
+ piUsage = event.usage;
92
182
  const usage = (event.usage && typeof event.usage === 'object'
93
183
  ? event.usage
94
184
  : message?.usage && typeof message.usage === 'object'
@@ -105,41 +195,12 @@ export function parseProviderTelemetry(agent, lines) {
105
195
  // Older JSON stats only provide model-local token objects. Sum a field
106
196
  // only when every model measured it; a missing measurement is not zero.
107
197
  let source = usage ?? nestedModelTokens ?? modelUsage;
108
- if (agent === 'gemini' && modelEntries.length > 0) {
198
+ if ((agent === 'gemini' || agent === 'qwen') && modelEntries.length > 0) {
109
199
  reportedModels = modelEntries.map(([name]) => name);
110
200
  model = firstModel?.[0];
111
201
  const fields = {
112
- input_tokens: ['input_tokens', 'inputTokens', 'promptTokenCount', 'input'],
113
- output_tokens: ['output_tokens', 'outputTokens', 'candidatesTokenCount', 'output'],
114
- cached_input_tokens: ['cached_input_tokens', 'cachedInputTokens', 'cachedContentTokenCount', 'cached'],
115
- reasoning_output_tokens: ['reasoning_output_tokens', 'reasoningOutputTokens', 'thoughtsTokenCount', 'thoughts'],
116
- };
117
- const totals = {};
118
- for (const [field, aliases] of Object.entries(fields)) {
119
- const aggregate = aliases.map(key => finite(usage?.[key])).find(value => value !== undefined);
120
- if (aggregate !== undefined) {
121
- totals[field] = aggregate;
122
- continue;
123
- }
124
- const values = modelEntries.map(([, value]) => {
125
- const entry = isRecord(value) ? value : {};
126
- const tokens = isRecord(entry.tokens) ? entry.tokens : entry;
127
- return aliases.map(key => finite(tokens[key])).find(value => value !== undefined);
128
- });
129
- if (values.every(value => value !== undefined))
130
- totals[field] = values.reduce((sum, value) => sum + value, 0);
131
- }
132
- source = { ...totals, ...usage };
133
- const aggregateCached = finite(usage?.cached_input_tokens ?? usage?.cached);
134
- if (aggregateCached !== undefined)
135
- source.cached_input_tokens = aggregateCached;
136
- }
137
- if (agent === 'qwen' && modelEntries.length > 0) {
138
- reportedModels = modelEntries.map(([name]) => name);
139
- model = firstModel?.[0];
140
- const fields = {
141
- input_tokens: ['input_tokens', 'inputTokens', 'promptTokenCount', 'input'],
142
- output_tokens: ['output_tokens', 'outputTokens', 'candidatesTokenCount', 'output'],
202
+ input_tokens: ['input_tokens', 'inputTokens', 'promptTokenCount', 'input', 'prompt'],
203
+ output_tokens: ['output_tokens', 'outputTokens', 'candidatesTokenCount', 'output', 'candidates'],
143
204
  cached_input_tokens: ['cached_input_tokens', 'cachedInputTokens', 'cachedContentTokenCount', 'cached'],
144
205
  reasoning_output_tokens: ['reasoning_output_tokens', 'reasoningOutputTokens', 'thoughtsTokenCount', 'thoughts'],
145
206
  };
@@ -164,7 +225,7 @@ export function parseProviderTelemetry(agent, lines) {
164
225
  source.cached_input_tokens = aggregateCached;
165
226
  }
166
227
  const inValue = finite(source?.input_tokens ?? source?.inputTokens ?? source?.prompt_tokens ?? source?.promptTokenCount ?? source?.input);
167
- const cachedValue = finite(source?.cached_input_tokens ?? source?.cache_read_input_tokens ?? source?.cachedInputTokens ?? source?.cachedContentTokenCount ?? source?.cached);
228
+ const cachedValue = finite(source?.cached_input_tokens ?? source?.cache_read_input_tokens ?? source?.cachedInputTokens ?? source?.cacheRead ?? source?.cachedContentTokenCount ?? source?.cached);
168
229
  const cacheWriteValue = finite(source?.cache_write_input_tokens ?? source?.cache_creation_input_tokens ?? source?.cacheWriteInputTokens ?? source?.cacheWrite);
169
230
  const outValue = finite(source?.output_tokens ?? source?.outputTokens ?? source?.completion_tokens ?? source?.candidatesTokenCount ?? source?.output);
170
231
  const reasoningValue = finite(source?.reasoning_output_tokens ?? source?.reasoningOutputTokens ?? source?.thoughtsTokenCount ?? source?.thoughts);
@@ -178,13 +239,48 @@ export function parseProviderTelemetry(agent, lines) {
178
239
  outputTokens = outValue;
179
240
  if (reasoningValue !== undefined)
180
241
  reasoningOutputTokens = reasoningValue;
181
- const costValue = finite(event.total_cost_usd ?? event.totalCostUsd ?? stats?.total_cost_usd);
242
+ const usageCost = isRecord(usage?.cost) ? finite(usage.cost.total) : undefined;
243
+ const costValue = finite(event.total_cost_usd ?? event.totalCostUsd ?? stats?.total_cost_usd ?? usageCost);
182
244
  if (costValue !== undefined)
183
245
  totalCostUsd = costValue;
184
246
  const eventModel = event.model ?? message?.model ?? firstModel?.[0];
185
247
  if (typeof eventModel === 'string' && eventModel && reportedModels.length <= 1)
186
248
  model = eventModel;
187
249
  }
250
+ if (piUsage) {
251
+ const piInput = finite(piUsage.input);
252
+ const piOutput = finite(piUsage.output);
253
+ const piCached = finite(piUsage.cacheRead);
254
+ const piCacheWrite = finite(piUsage.cacheWrite);
255
+ const piReasoning = finite(piUsage.reasoning);
256
+ const piCost = isRecord(piUsage.cost) ? finite(piUsage.cost.total) : undefined;
257
+ if (piInput !== undefined)
258
+ inputTokens = piInput;
259
+ if (piOutput !== undefined)
260
+ outputTokens = piOutput;
261
+ if (piCached !== undefined)
262
+ cachedInputTokens = piCached;
263
+ if (piCacheWrite !== undefined)
264
+ cacheWriteInputTokens = piCacheWrite;
265
+ if (piReasoning !== undefined)
266
+ reasoningOutputTokens = piReasoning;
267
+ if (piCost !== undefined)
268
+ totalCostUsd = piCost;
269
+ }
270
+ if (harnessTotals) {
271
+ if (harnessTotals.hasInput)
272
+ inputTokens = harnessTotals.input;
273
+ if (harnessTotals.hasOutput)
274
+ outputTokens = harnessTotals.output;
275
+ if (harnessTotals.hasCached)
276
+ cachedInputTokens = harnessTotals.cached;
277
+ if (harnessTotals.hasCacheWrite)
278
+ cacheWriteInputTokens = harnessTotals.cacheWrite;
279
+ if (harnessTotals.hasReasoning)
280
+ reasoningOutputTokens = harnessTotals.reasoning;
281
+ if (harnessTotals.hasCost)
282
+ totalCostUsd = harnessTotals.cost;
283
+ }
188
284
  if (inputTokens === undefined || outputTokens === undefined) {
189
285
  const partialUsage = {
190
286
  ...(inputTokens !== undefined ? { inputTokens } : {}),
@@ -1,7 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { parse } from 'yaml';
3
3
  import { readFileSync } from 'node:fs';
4
- export const AgentSchema = z.enum(['claude', 'codex', 'gemini', 'qwen']);
4
+ import { AgentSchema } from '../agents/contracts.js';
5
+ export { AgentSchema };
5
6
  export const InvocationSchema = z.enum(['auto', 'manual']);
6
7
  export const SkillEntrySchema = z.object({
7
8
  id: z.string().min(1),
@@ -104,7 +104,7 @@ export function buildChangePrompt(request, proposalPath, stories, brief = '') {
104
104
  'Every proposed story must have passes: false and 2-5 structured acceptance criteria.',
105
105
  'Every criterion must have a stable id, behavioral text, and one or more executable verify commands.',
106
106
  'Each criterion id must appear in every verify command; each entry must be one approved test command without shell control operators.',
107
- 'Use only claude, codex, or gemini if an optional agent affinity is useful.',
107
+ 'Use only a configured Yoke harness if an optional agent affinity is useful (claude, codex, gemini, qwen, opencode, kilo, or pi).',
108
108
  '',
109
109
  `Write ONLY a YAML array of the NEW stories to this exact file: ${proposalPath}`,
110
110
  'Do not edit .yoke/prd.yaml or any source file. Do not commit.',
package/dist/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { MODEL_PROVIDERS } from './setup/model-presets.js';
2
3
  import { pathToFileURL } from 'node:url';
3
4
  import { realpathSync } from 'node:fs';
4
5
  import { checkProject, checkExitCode, protectAcceptance } from './check/command.js';
@@ -18,6 +19,7 @@ import { runLoopCleanup } from './loop/cleanup.js';
18
19
  import { runFlowSmoke } from './smoke/command.js';
19
20
  import { maybeNotifyUpdate, currentYokeVersion } from './update/check.js';
20
21
  import { runUpgrade } from './update/upgrade.js';
22
+ import { AGENT_LIST, isSupportedAgent, SUPPORTED_AGENTS } from './agents/catalog.js';
21
23
  import { printAudit, runAudit } from './audit/command.js';
22
24
  import { runSetup } from './setup/command.js';
23
25
  import { pendingChanges, queueChange } from './change/inbox.js';
@@ -104,7 +106,7 @@ export function main(argv) {
104
106
  switch (cmd) {
105
107
  case 'setup': {
106
108
  const targetDir = rest.find(a => !a.startsWith('-')) ?? '.';
107
- const valid = ['claude', 'codex', 'gemini', 'qwen'];
109
+ const valid = [...SUPPORTED_AGENTS];
108
110
  const hostArg = rest.find(a => a.startsWith('--host='))?.slice('--host='.length);
109
111
  if (hostArg && !valid.includes(hostArg)) {
110
112
  console.error(`Invalid --host value: ${hostArg}`);
@@ -114,7 +116,7 @@ export function main(argv) {
114
116
  const agentTokens = agentArg === 'all' ? valid : agentArg?.split(',').map(a => a.trim());
115
117
  const invalidAgents = agentTokens?.filter(a => !valid.includes(a)) ?? [];
116
118
  if (agentArg && (invalidAgents.length > 0 || agentTokens?.length === 0)) {
117
- console.error(`Invalid --agent value: ${agentArg} (expected claude,codex,gemini|all)`);
119
+ console.error(`Invalid --agent value: ${agentArg} (expected ${AGENT_LIST}|all)`);
118
120
  return 1;
119
121
  }
120
122
  const agents = agentTokens ? [...new Set(agentTokens)] : undefined;
@@ -140,7 +142,14 @@ export function main(argv) {
140
142
  console.error('Invalid routing strategy');
141
143
  return 1;
142
144
  }
145
+ const modelProviderArg = rest.find(a => a.startsWith('--model-provider='))?.slice('--model-provider='.length);
146
+ const modelProviders = modelProviderArg?.split(',').map(value => value.trim());
147
+ if (modelProviders?.some(value => !MODEL_PROVIDERS.includes(value))) {
148
+ console.error('Invalid --model-provider (expected deepseek,kimi)');
149
+ return 1;
150
+ }
143
151
  return runSetup(targetDir, {
152
+ modelProviders: modelProviders,
144
153
  host: hostArg, agents, runner: runnerArg,
145
154
  codeGraph: graphArg,
146
155
  loop, routing, decisionPolicy: policyArg,
@@ -226,7 +235,7 @@ export function main(argv) {
226
235
  }
227
236
  if (sub === 'run' || sub === 'resume') {
228
237
  const provider = value('runner') ?? 'codex';
229
- if (!['codex', 'claude', 'gemini', 'qwen'].includes(provider))
238
+ if (!SUPPORTED_AGENTS.includes(provider))
230
239
  throw new Error('Unknown runner');
231
240
  return runProjectGoal(targetDir, { provider: provider, selection: { model: value('model') } }).then(goal => {
232
241
  console.log(JSON.stringify(goal));
@@ -269,7 +278,7 @@ export function main(argv) {
269
278
  const targetDir = rest.find(a => !a.startsWith('-')) ?? '.';
270
279
  const loop = rest.includes('--loop');
271
280
  const agentArg = rest.find(a => a.startsWith('--agent='))?.slice('--agent='.length);
272
- const all = ['claude', 'codex', 'gemini', 'qwen'];
281
+ const all = [...SUPPORTED_AGENTS];
273
282
  const agents = !agentArg || agentArg === 'all'
274
283
  ? (agentArg === 'all' ? all : undefined)
275
284
  : agentArg.split(',').filter((a) => all.includes(a));
@@ -468,18 +477,17 @@ export function main(argv) {
468
477
  return 1;
469
478
  }
470
479
  const runnerArg = rest.find(a => a.startsWith('--runner='))?.slice('--runner='.length);
471
- const valid = ['claude', 'codex', 'gemini', 'qwen'];
472
- const agent = runnerArg && valid.includes(runnerArg) ? runnerArg : undefined;
480
+ const agent = runnerArg && isSupportedAgent(runnerArg) ? runnerArg : undefined;
473
481
  if (runnerArg && !agent) {
474
- console.error(`Invalid --runner value: ${runnerArg} (expected claude|codex|gemini|qwen)`);
482
+ console.error(`Invalid --runner value: ${runnerArg} (expected ${AGENT_LIST})`);
475
483
  return 1;
476
484
  }
477
485
  const isolate = rest.includes('--isolate') ? true : rest.includes('--no-isolate') ? false : undefined;
478
486
  const reviewerArg = rest.find(a => a.startsWith('--reviewer='))?.slice('--reviewer='.length);
479
487
  let reviewer;
480
488
  if (reviewerArg) {
481
- if (!valid.includes(reviewerArg)) {
482
- console.error(`Invalid --reviewer value: ${reviewerArg} (expected claude|codex|gemini)`);
489
+ if (!isSupportedAgent(reviewerArg)) {
490
+ console.error(`Invalid --reviewer value: ${reviewerArg} (expected ${AGENT_LIST})`);
483
491
  return 1;
484
492
  }
485
493
  reviewer = reviewerArg;
@@ -522,19 +530,19 @@ export function main(argv) {
522
530
  }
523
531
  return runLoopCommand(targetDir, { maxIterations: rawMax, agent, isolate, resumeWorktree: rest.includes('--resume-worktree'), parallel, parallelAuto: parallelArg === '--parallel=auto', reviewer, review, allowSelfReview, timeoutMinutes, json, routing, onAmbiguity: oaArg, decisionPolicy: dpArg, permissions, ...qualityFlags.options });
524
532
  }
525
- console.log('usage: yoke loop <on|off|status|decision|answer|resume [--discard] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N]|cleanup [--remove-worktrees] [--discard-stale-recovery]|run [--max=N] [--parallel=<auto|N>] [--runner=<claude|codex|gemini>] [--reviewer=<claude|codex|gemini>] [--review] [--allow-self-review] [--routing|--no-routing] [--isolate|--no-isolate] [--unsafe] [--timeout=<minutes>] [--decision-policy=<auto|critical>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N] [--json]> [targetDir]');
533
+ console.log(`usage: yoke loop <on|off|status|decision|answer|resume [--discard] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N]|cleanup [--remove-worktrees] [--discard-stale-recovery]|run [--max=N] [--parallel=<auto|N>] [--runner=<${AGENT_LIST}>] [--reviewer=<${AGENT_LIST}>] [--review] [--allow-self-review] [--routing|--no-routing] [--isolate|--no-isolate] [--unsafe] [--timeout=<minutes>] [--decision-policy=<auto|critical>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N] [--json]> [targetDir]`);
526
534
  return 1;
527
535
  }
528
536
  case 'new': {
529
537
  const dir = rest.find(a => !a.startsWith('-'));
530
538
  if (!dir) {
531
- console.error('usage: yoke new <dir> [--idea="..."] [--agent=claude,codex,gemini|all] [--runner=<claude|codex|gemini>] [--loop]');
539
+ console.error(`usage: yoke new <dir> [--idea="..."] [--agent=${AGENT_LIST}|all] [--runner=<${AGENT_LIST}>] [--loop]`);
532
540
  return 1;
533
541
  }
534
542
  const idea = rest.find(a => a.startsWith('--idea='))?.slice('--idea='.length);
535
543
  const loop = rest.includes('--loop');
536
544
  const agentArg = rest.find(a => a.startsWith('--agent='))?.slice('--agent='.length);
537
- const all = ['claude', 'codex', 'gemini', 'qwen'];
545
+ const all = [...SUPPORTED_AGENTS];
538
546
  const agents = !agentArg || agentArg === 'all'
539
547
  ? (agentArg === 'all' ? all : undefined)
540
548
  : agentArg.split(',').filter((a) => all.includes(a));
@@ -542,8 +550,8 @@ export function main(argv) {
542
550
  console.warn('Unknown agent(s) in --agent; falling back to detection');
543
551
  }
544
552
  const runnerArg = rest.find(a => a.startsWith('--runner='))?.slice('--runner='.length);
545
- if (runnerArg && !all.includes(runnerArg)) {
546
- console.error(`Invalid --runner value: ${runnerArg} (expected claude|codex|gemini)`);
553
+ if (runnerArg && !isSupportedAgent(runnerArg)) {
554
+ console.error(`Invalid --runner value: ${runnerArg} (expected ${AGENT_LIST})`);
547
555
  return 1;
548
556
  }
549
557
  return runNew(dir, { idea, agents, runner: runnerArg, loop });
@@ -553,7 +561,7 @@ export function main(argv) {
553
561
  const targetDir = rest.slice(1).find(a => !a.startsWith('-')) ?? '.';
554
562
  if (sub === 'assess') {
555
563
  const runner = rest.find(a => a.startsWith('--runner='))?.slice('--runner='.length);
556
- if (runner && !['codex', 'claude', 'gemini', 'qwen'].includes(runner)) {
564
+ if (runner && !SUPPORTED_AGENTS.includes(runner)) {
557
565
  console.error('Invalid planning runner');
558
566
  return 1;
559
567
  }
@@ -562,13 +570,12 @@ export function main(argv) {
562
570
  if (sub === 'draft') {
563
571
  const idea = rest.find(a => a.startsWith('--idea='))?.slice('--idea='.length);
564
572
  if (!idea) {
565
- console.error('usage: yoke prd draft [dir] --idea="..." [--runner=<claude|codex|gemini>] [--force] [--timeout=<minutes>]');
573
+ console.error(`usage: yoke prd draft [dir] --idea="..." [--runner=<${AGENT_LIST}>] [--force] [--timeout=<minutes>]`);
566
574
  return 1;
567
575
  }
568
- const valid = ['claude', 'codex', 'gemini', 'qwen'];
569
576
  const runnerArg = rest.find(a => a.startsWith('--runner='))?.slice('--runner='.length);
570
- if (runnerArg && !valid.includes(runnerArg)) {
571
- console.error(`Invalid --runner value: ${runnerArg} (expected claude|codex|gemini|qwen)`);
577
+ if (runnerArg && !isSupportedAgent(runnerArg)) {
578
+ console.error(`Invalid --runner value: ${runnerArg} (expected ${AGENT_LIST})`);
572
579
  return 1;
573
580
  }
574
581
  const force = rest.includes('--force');
@@ -586,7 +593,7 @@ export function main(argv) {
586
593
  }
587
594
  if (sub === 'check')
588
595
  return runPrdCheck(targetDir);
589
- console.log('usage: yoke prd <draft|check|assess> [dir] [--idea="..."] [--runner=<claude|codex|gemini>] [--story=<id>] [--reassess] [--force] [--timeout=<minutes>]');
596
+ console.log(`usage: yoke prd <draft|check|assess> [dir] [--idea="..."] [--runner=<${AGENT_LIST}>] [--story=<id>] [--reassess] [--force] [--timeout=<minutes>]`);
590
597
  return 1;
591
598
  }
592
599
  case 'context': {
@@ -601,10 +608,9 @@ export function main(argv) {
601
608
  }
602
609
  case 'review': {
603
610
  const targetDir = rest.find(a => !a.startsWith('-')) ?? '.';
604
- const valid = ['claude', 'codex', 'gemini'];
605
611
  const reviewerArg = rest.find(a => a.startsWith('--reviewer='))?.slice('--reviewer='.length);
606
- if (reviewerArg && !valid.includes(reviewerArg)) {
607
- console.error(`Invalid --reviewer value: ${reviewerArg} (expected claude|codex|gemini)`);
612
+ if (reviewerArg && !isSupportedAgent(reviewerArg)) {
613
+ console.error(`Invalid --reviewer value: ${reviewerArg} (expected ${AGENT_LIST})`);
608
614
  return 1;
609
615
  }
610
616
  const base = rest.find(a => a.startsWith('--base='))?.slice('--base='.length);
@@ -651,7 +657,7 @@ export function main(argv) {
651
657
  return runUpgrade();
652
658
  default:
653
659
  console.log('Project workflows: yoke check [dir] [--json|--protect] | goal set|run|resume|pause|status|handoff|budget [dir] | projects add|list|remove | dashboard [dir] [--port=N]');
654
- console.log('usage: yoke <setup [dir] | new <dir> [--idea="..."] | validate [canonDir] | retrofit [targetDir] [--agent=claude,codex,gemini|all] [--code-graph=graphify|serena] [--loop] | change <add|status> [dir] | prd <draft|check|assess> [dir] | loop <on|off|status|decision|answer|resume|run|cleanup> | context <init|status> | review [dir] [--reviewer=<claude|codex|gemini>] [--base=<ref>] [--focus="..."] | design-scan [dir] [--max=N] [--report] | flow-smoke [dir] [--url=<baseUrl>] [--label=<name>] | upgrade>');
660
+ console.log(`usage: yoke <setup [dir] | new <dir> [--idea="..."] | validate [canonDir] | retrofit [targetDir] [--agent=${AGENT_LIST}|all] [--code-graph=graphify|serena] [--loop] | change <add|status> [dir] | prd <draft|check|assess> [dir] | loop <on|off|status|decision|answer|resume|run|cleanup> | context <init|status> | review [dir] [--reviewer=<${AGENT_LIST}>] [--base=<ref>] [--focus="..."] | design-scan [dir] [--max=N] [--report] | flow-smoke [dir] [--url=<baseUrl>] [--label=<name>] | upgrade>`);
655
661
  return cmd ? 1 : 0;
656
662
  }
657
663
  }