@hecer/yoke 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/.claude-plugin/plugin.json +13 -13
  2. package/.codex-plugin/plugin.json +7 -7
  3. package/CHANGELOG.md +416 -398
  4. package/README.md +931 -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 +56 -56
  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/host.js +1 -1
  77. package/dist/agents/providers.js +18 -5
  78. package/dist/agents/telemetry.js +35 -36
  79. package/dist/cli.js +18 -10
  80. package/dist/dashboard/page.js +122 -122
  81. package/dist/dashboard/panels.js +91 -91
  82. package/dist/loop/run-command.js +3 -3
  83. package/dist/prd/command.js +17 -17
  84. package/dist/retrofit/apply.js +8 -1
  85. package/dist/retrofit/config.js +1 -1
  86. package/dist/retrofit/detect.js +2 -0
  87. package/dist/retrofit/planners/claude.js +14 -14
  88. package/dist/retrofit/planners/qwen.js +3 -3
  89. package/dist/retrofit/preserve.js +2 -2
  90. package/dist/retrofit/qwen-settings.js +17 -0
  91. package/dist/retrofit/skill-actions.js +1 -1
  92. package/dist/setup/command.js +22 -8
  93. package/dist/setup/model-presets.js +48 -0
  94. package/docs/CAPABILITY-ROUTING.md +51 -51
  95. package/docs/DASHBOARD-EVOLUTION.md +33 -33
  96. package/docs/MIGRATING-TO-1.0.md +33 -33
  97. package/docs/MIGRATING-TO-1.1.md +27 -27
  98. package/docs/MIGRATING-TO-1.4.md +70 -70
  99. package/docs/PRODUCT-DIRECTION-2026-09-05.md +210 -210
  100. package/docs/PUBLISHING.md +114 -114
  101. package/docs/QWEN-MODEL-SUPPORT.md +142 -0
  102. package/docs/VERIFIED-PROJECTS-VALIDATION.md +29 -29
  103. package/docs/VERIFIED-PROJECTS.md +167 -167
  104. package/docs/superpowers/plans/2026-06-28-baustein-e-context-layer.md +981 -981
  105. package/docs/superpowers/plans/2026-06-29-baustein-f-routing.md +258 -258
  106. package/docs/superpowers/plans/2026-06-29-baustein-g-loop-observability.md +1006 -1006
  107. package/docs/superpowers/plans/2026-06-29-baustein-h-loop-robustness.md +374 -374
  108. package/docs/superpowers/plans/2026-06-30-baustein-i-visual-design-verification.md +450 -450
  109. package/docs/superpowers/plans/2026-07-02-baustein-k-zero-to-100-bootstrap.md +1024 -1024
  110. package/docs/superpowers/plans/2026-07-02-baustein-m-flow-smoke-proofs.md +574 -574
  111. package/docs/superpowers/plans/2026-08-13-gauntlet-quality-loop.md +537 -537
  112. package/docs/superpowers/plans/2026-08-16-artifact-backed-output-compaction.md +329 -329
  113. package/docs/superpowers/plans/2026-09-05-verified-projects.md +83 -83
  114. package/docs/superpowers/specs/2026-06-28-baustein-e-context-layer-design.md +146 -146
  115. package/docs/superpowers/specs/2026-06-29-baustein-f-routing-design.md +106 -106
  116. package/docs/superpowers/specs/2026-06-29-baustein-g-loop-observability-design.md +186 -186
  117. package/docs/superpowers/specs/2026-06-29-baustein-h-loop-robustness-design.md +113 -113
  118. package/docs/superpowers/specs/2026-06-30-baustein-i-visual-design-verification-design.md +98 -98
  119. package/docs/superpowers/specs/2026-07-02-baustein-k-zero-to-100-bootstrap-design.md +200 -200
  120. package/docs/superpowers/specs/2026-07-02-baustein-m-flow-smoke-proofs-design.md +155 -155
  121. package/docs/superpowers/specs/2026-08-13-gauntlet-quality-loop-design.md +422 -422
  122. package/docs/superpowers/specs/2026-08-16-artifact-backed-output-compaction-design.md +166 -166
  123. package/gemini-extension.json +6 -6
  124. package/hooks/hooks.json +19 -19
  125. package/package.json +87 -87
  126. package/dist/dashboard/discovery.js +0 -73
  127. package/docs/community-outreach-2026-08-20.md +0 -85
  128. package/docs/launch-copy-2026-08-21.md +0 -193
@@ -1,26 +1,26 @@
1
- ---
2
- name: yoke-retrofit
3
- description: Use when asked to "retrofit", "yoke this project", or set up the Yoke harness in a project — runs the shared setup wizard and configures the same behavior for Claude, Codex, and Gemini.
4
- ---
5
-
6
- # Yoke Retrofit
7
-
8
- Set up or update Yoke through the shared `yoke setup` contract.
9
-
10
- 1. Inspect the project and identify the current host (`claude`, `codex`, or `gemini`).
11
- 2. Ask these setup questions one at a time and give a direct recommendation:
12
- - target agents (recommend the current host; use `all` for deliberately cross-agent projects),
13
- - code-graph tool,
14
- - autonomous loop on/off,
15
- - default runner (recommend the current host),
16
- - decision mode: `auto` or `critical`.
17
- 3. Recommend the code graph based on this project:
18
- - **Serena** is LSP-accurate and best for large typed codebases or systematic symbol refactors where a missed reference is costly. It needs a language server per language.
19
- - **graphify** is fast and multimodal, and is best for exploration, migration, onboarding, or mixed code and document repositories. Its graph is an index and can become stale.
20
- 4. Apply the answers without a second round of prompts:
21
- `yoke setup . --yes --host=<host> --agent=<agents> --code-graph=<choice> --runner=<runner> --decision-policy=<auto|critical> --loop|--no-loop`.
22
- A human who runs `yoke setup .` directly receives the same five terminal questions.
23
- 5. Show the generated report and backup paths. Existing files are backed up under `.yoke/backup/`; settings are merged where supported.
24
- 6. If an old generated `CLAUDE.md` or `GEMINI.md` contained project-specific instructions, restore them inside its `<!-- yoke:preserve:start -->` / `<!-- yoke:preserve:end -->` block. Preserve blocks survive every later retrofit.
25
-
26
- The generated harness includes the provider-neutral `yoke-workflow` skill. It owns the planning questions, approved-plan handoff, autonomous stories, and critical-decision resume flow.
1
+ ---
2
+ name: yoke-retrofit
3
+ description: Use when asked to "retrofit", "yoke this project", or set up the Yoke harness in a project — runs the shared setup wizard and configures the same behavior for Claude, Codex, and Gemini.
4
+ ---
5
+
6
+ # Yoke Retrofit
7
+
8
+ Set up or update Yoke through the shared `yoke setup` contract.
9
+
10
+ 1. Inspect the project and identify the current host (`claude`, `codex`, or `gemini`).
11
+ 2. Ask these setup questions one at a time and give a direct recommendation:
12
+ - target agents (recommend the current host; use `all` for deliberately cross-agent projects),
13
+ - code-graph tool,
14
+ - autonomous loop on/off,
15
+ - default runner (recommend the current host),
16
+ - decision mode: `auto` or `critical`.
17
+ 3. Recommend the code graph based on this project:
18
+ - **Serena** is LSP-accurate and best for large typed codebases or systematic symbol refactors where a missed reference is costly. It needs a language server per language.
19
+ - **graphify** is fast and multimodal, and is best for exploration, migration, onboarding, or mixed code and document repositories. Its graph is an index and can become stale.
20
+ 4. Apply the answers without a second round of prompts:
21
+ `yoke setup . --yes --host=<host> --agent=<agents> --code-graph=<choice> --runner=<runner> --decision-policy=<auto|critical> --loop|--no-loop`.
22
+ A human who runs `yoke setup .` directly receives the same five terminal questions.
23
+ 5. Show the generated report and backup paths. Existing files are backed up under `.yoke/backup/`; settings are merged where supported.
24
+ 6. If an old generated `CLAUDE.md` or `GEMINI.md` contained project-specific instructions, restore them inside its `<!-- yoke:preserve:start -->` / `<!-- yoke:preserve:end -->` block. Preserve blocks survive every later retrofit.
25
+
26
+ The generated harness includes the provider-neutral `yoke-workflow` skill. It owns the planning questions, approved-plan handoff, autonomous stories, and critical-decision resume flow.
@@ -1,20 +1,20 @@
1
- ---
2
- name: yoke-workflow
3
- description: Use when the user asks Yoke to plan and build a feature, run stories autonomously, continue a Yoke loop, or only interrupt for major decisions.
4
- ---
5
-
6
- # Yoke Workflow
7
-
8
- Provide the same interaction contract in Claude, Codex, and Gemini.
9
-
10
- 1. Read `.yoke/config.yaml`. If it is missing, offer `yoke setup . --host=<current-agent>` and run the setup flow before planning.
11
- 2. Plan before starting the loop. Inspect the project, then ask one focused question at a time only where the answer changes product behavior, scope, architecture, security, data ownership, external cost, or an irreversible choice. Include a recommended answer. Resolve routine implementation details yourself.
12
- 3. Summarize the agreed design in `.yoke/plan.md`, including goals, non-goals, constraints, and decisions. Use the `authoring-prd` skill to turn it into small stories with testable acceptance criteria. Run `yoke prd check .`.
13
- 4. Ask once for approval of the complete plan and story set. Do not begin implementation before that approval.
14
- 5. If `loop.enabled` is true, run the stories without routine follow-up questions using the configured runner. Prefer `yoke loop run . --max=5 --isolate`, report status after each batch, and continue until complete or genuinely blocked.
15
- 6. Respect `loop.decisionPolicy`:
16
- - `auto`: choose the most suitable option from the plan, current code, and established conventions. Record the interpretation and continue.
17
- - `critical`: routine ambiguity is still resolved automatically. If the loop reports a pending critical decision, run `yoke loop decision .`, present its options and recommendation to the user, ask exactly that question, then run `yoke loop answer . --choice=<id> --rationale="<answer>"`. The answer command records the decision and resumes the same story.
18
- 7. Never ask whether to run tests, review, commit, or continue to the next approved story. Those are part of the approved workflow.
19
-
20
- The user's configured commit identity is authoritative. Do not add an AI co-author unless `commit.allowCoAuthors` explicitly permits it.
1
+ ---
2
+ name: yoke-workflow
3
+ description: Use when the user asks Yoke to plan and build a feature, run stories autonomously, continue a Yoke loop, or only interrupt for major decisions.
4
+ ---
5
+
6
+ # Yoke Workflow
7
+
8
+ Provide the same interaction contract in Claude, Codex, and Gemini.
9
+
10
+ 1. Read `.yoke/config.yaml`. If it is missing, offer `yoke setup . --host=<current-agent>` and run the setup flow before planning.
11
+ 2. Plan before starting the loop. Inspect the project, then ask one focused question at a time only where the answer changes product behavior, scope, architecture, security, data ownership, external cost, or an irreversible choice. Include a recommended answer. Resolve routine implementation details yourself.
12
+ 3. Summarize the agreed design in `.yoke/plan.md`, including goals, non-goals, constraints, and decisions. Use the `authoring-prd` skill to turn it into small stories with testable acceptance criteria. Run `yoke prd check .`.
13
+ 4. Ask once for approval of the complete plan and story set. Do not begin implementation before that approval.
14
+ 5. If `loop.enabled` is true, run the stories without routine follow-up questions using the configured runner. Prefer `yoke loop run . --max=5 --isolate`, report status after each batch, and continue until complete or genuinely blocked.
15
+ 6. Respect `loop.decisionPolicy`:
16
+ - `auto`: choose the most suitable option from the plan, current code, and established conventions. Record the interpretation and continue.
17
+ - `critical`: routine ambiguity is still resolved automatically. If the loop reports a pending critical decision, run `yoke loop decision .`, present its options and recommendation to the user, ask exactly that question, then run `yoke loop answer . --choice=<id> --rationale="<answer>"`. The answer command records the decision and resumes the same story.
18
+ 7. Never ask whether to run tests, review, commit, or continue to the next approved story. Those are part of the approved workflow.
19
+
20
+ The user's configured commit identity is authoritative. Do not add an AI co-author unless `commit.allowCoAuthors` explicitly permits it.
@@ -1,36 +1,36 @@
1
1
  import { spawnSync } from 'node:child_process'
2
- import { resolve } from 'node:path'
3
- import { pathToFileURL } from 'node:url'
4
-
5
- function rtkCheck(command) {
6
- const result = spawnSync('rtk', ['hook', 'check', command], { encoding: 'utf8', timeout: 3000 })
7
- return result.status === 0 ? result.stdout.trim() : ''
8
- }
9
-
10
- export function rewriteHookInput(input, check = rtkCheck) {
11
- if (input?.tool_name !== 'Bash' && input?.toolName !== 'Bash') return null
12
- const toolInput = input.tool_input ?? input.toolInput
13
- const command = toolInput?.command
14
- if (typeof command !== 'string' || command.trim() === '') return null
15
- const rewritten = check(command)
16
- if (!rewritten || rewritten === command) return null
17
- return {
18
- hookSpecificOutput: {
19
- hookEventName: 'PreToolUse',
20
- updatedInput: { ...toolInput, command: rewritten },
21
- },
22
- }
23
- }
24
-
25
- async function main() {
26
- let raw = ''
27
- for await (const chunk of process.stdin) raw += chunk
28
- try {
29
- const output = rewriteHookInput(JSON.parse(raw))
30
- if (output) process.stdout.write(JSON.stringify(output))
31
- } catch {
32
- // Compression is an optimization. Malformed input must never block Codex.
33
- }
34
- }
35
-
36
- if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) await main()
2
+ import { resolve } from 'node:path'
3
+ import { pathToFileURL } from 'node:url'
4
+
5
+ function rtkCheck(command) {
6
+ const result = spawnSync('rtk', ['hook', 'check', command], { encoding: 'utf8', timeout: 3000 })
7
+ return result.status === 0 ? result.stdout.trim() : ''
8
+ }
9
+
10
+ export function rewriteHookInput(input, check = rtkCheck) {
11
+ if (input?.tool_name !== 'Bash' && input?.toolName !== 'Bash') return null
12
+ const toolInput = input.tool_input ?? input.toolInput
13
+ const command = toolInput?.command
14
+ if (typeof command !== 'string' || command.trim() === '') return null
15
+ const rewritten = check(command)
16
+ if (!rewritten || rewritten === command) return null
17
+ return {
18
+ hookSpecificOutput: {
19
+ hookEventName: 'PreToolUse',
20
+ updatedInput: { ...toolInput, command: rewritten },
21
+ },
22
+ }
23
+ }
24
+
25
+ async function main() {
26
+ let raw = ''
27
+ for await (const chunk of process.stdin) raw += chunk
28
+ try {
29
+ const output = rewriteHookInput(JSON.parse(raw))
30
+ if (output) process.stdout.write(JSON.stringify(output))
31
+ } catch {
32
+ // Compression is an optimization. Malformed input must never block Codex.
33
+ }
34
+ }
35
+
36
+ if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) await main()
@@ -1,25 +1,25 @@
1
- #!/usr/bin/env node
2
- // Pure BeforeTool argument adapter. Never evaluates or executes tool commands.
3
- import { readFileSync } from 'node:fs'
4
-
5
- const record = value => value !== null && typeof value === 'object' && !Array.isArray(value)
6
- try {
7
- const event = JSON.parse(readFileSync(0, 'utf8'))
8
- if (!record(event) || typeof event.tool_name !== 'string' ||
9
- (event.hook_event_name !== undefined && event.hook_event_name !== 'BeforeTool')) throw new Error('event')
10
- let response = {}
11
- if (event.tool_name === 'run_shell_command') {
12
- if (!record(event.tool_input) || typeof event.tool_input.command !== 'string' || !event.tool_input.command.trim() || event.tool_input.command.includes('\0')) throw new Error('command')
13
- const command = event.tool_input.command
14
- // Restrict rewriting to simple supported invocations. Leave shell syntax,
15
- // quoted executables, assignments and existing RTK wrappers untouched.
16
- if (!/[;&|<>`$\r\n()]/u.test(command) && /^\s*(?:git|rg|npm|npx|cargo|pytest|go|docker|kubectl)\s/u.test(command)) {
17
- const rewritten = command.trimStart().replace(/^rg\s/u, 'grep ')
18
- response = { hookSpecificOutput: { tool_input: { ...event.tool_input, command: `rtk ${rewritten}` } } }
19
- }
20
- }
21
- process.stdout.write(JSON.stringify(response) + '\n')
22
- } catch {
23
- process.stderr.write('Invalid Gemini BeforeTool hook input\n')
24
- process.exitCode = 2
25
- }
1
+ #!/usr/bin/env node
2
+ // Pure BeforeTool argument adapter. Never evaluates or executes tool commands.
3
+ import { readFileSync } from 'node:fs'
4
+
5
+ const record = value => value !== null && typeof value === 'object' && !Array.isArray(value)
6
+ try {
7
+ const event = JSON.parse(readFileSync(0, 'utf8'))
8
+ if (!record(event) || typeof event.tool_name !== 'string' ||
9
+ (event.hook_event_name !== undefined && event.hook_event_name !== 'BeforeTool')) throw new Error('event')
10
+ let response = {}
11
+ if (event.tool_name === 'run_shell_command') {
12
+ if (!record(event.tool_input) || typeof event.tool_input.command !== 'string' || !event.tool_input.command.trim() || event.tool_input.command.includes('\0')) throw new Error('command')
13
+ const command = event.tool_input.command
14
+ // Restrict rewriting to simple supported invocations. Leave shell syntax,
15
+ // quoted executables, assignments and existing RTK wrappers untouched.
16
+ if (!/[;&|<>`$\r\n()]/u.test(command) && /^\s*(?:git|rg|npm|npx|cargo|pytest|go|docker|kubectl)\s/u.test(command)) {
17
+ const rewritten = command.trimStart().replace(/^rg\s/u, 'grep ')
18
+ response = { hookSpecificOutput: { tool_input: { ...event.tool_input, command: `rtk ${rewritten}` } } }
19
+ }
20
+ }
21
+ process.stdout.write(JSON.stringify(response) + '\n')
22
+ } catch {
23
+ process.stderr.write('Invalid Gemini BeforeTool hook input\n')
24
+ process.exitCode = 2
25
+ }
@@ -1,3 +1,3 @@
1
- # Tool: graphify (code-graph)
2
-
3
- MIT, multimodal code/doc graph. Wired as an MCP server for all three agents (stdio). Prefer symbol/graph lookups over reading whole files. Caveat: heuristic edges (INFERRED/AMBIGUOUS) and a static index that can go stale — rebuild on significant changes.
1
+ # Tool: graphify (code-graph)
2
+
3
+ MIT, multimodal code/doc graph. Wired as an MCP server for all three agents (stdio). Prefer symbol/graph lookups over reading whole files. Caveat: heuristic edges (INFERRED/AMBIGUOUS) and a static index that can go stale — rebuild on significant changes.
@@ -1,3 +1,3 @@
1
- # Tool: Playwright MCP (browser / dogfooding)
2
-
3
- Microsoft Playwright MCP, Apache-2.0. Wired as an MCP server for all three agents — the only browser tool with native MCP parity across Claude/Codex/Gemini. Used for QA, dogfooding user flows, screenshots, and deploy verification.
1
+ # Tool: Playwright MCP (browser / dogfooding)
2
+
3
+ Microsoft Playwright MCP, Apache-2.0. Wired as an MCP server for all three agents — the only browser tool with native MCP parity across Claude/Codex/Gemini. Used for QA, dogfooding user flows, screenshots, and deploy verification.
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ // PreToolUse guard requesting a retry through RTK. Never evaluates or executes tool commands.
3
+ import { readFileSync } from 'node:fs'
4
+
5
+ const record = value => value !== null && typeof value === 'object' && !Array.isArray(value)
6
+ try {
7
+ const event = JSON.parse(readFileSync(0, 'utf8'))
8
+ if (!record(event) || typeof event.tool_name !== 'string' ||
9
+ (event.hook_event_name !== undefined && event.hook_event_name !== 'PreToolUse')) throw new Error('event')
10
+ let response = {}
11
+ if (event.tool_name === 'run_shell_command') {
12
+ if (!record(event.tool_input) || typeof event.tool_input.command !== 'string' || !event.tool_input.command.trim() || event.tool_input.command.includes('\0')) throw new Error('command')
13
+ const command = event.tool_input.command
14
+ // Restrict rewriting to simple supported invocations. Leave shell syntax,
15
+ // quoted executables, assignments and existing RTK wrappers untouched.
16
+ if (!/[;&|<>`$\r\n()]/u.test(command) && /^\s*(?:git|rg|npm|npx|cargo|pytest|go|docker|kubectl)\s/u.test(command)) {
17
+ const rewritten = command.trimStart().replace(/^rg\s/u, 'grep ')
18
+ response = { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: `Retry this command through RTK: rtk ${rewritten}` } }
19
+ }
20
+ }
21
+ process.stdout.write(JSON.stringify(response) + '\n')
22
+ } catch {
23
+ process.stderr.write('Invalid Qwen PreToolUse hook input\n')
24
+ process.exitCode = 2
25
+ }
@@ -1,7 +1,7 @@
1
- # Tool: rtk (token compression)
2
-
3
- Per-agent wiring (generated by Baustein B):
4
-
5
- - **Claude Code:** PreToolUse hook auto-rewrites commands (`git status` → `rtk git status`). On Windows this needs WSL; otherwise fall back to instruction mode (this file injected into CLAUDE.md telling the agent to prefix commands with `rtk`).
6
- - **Codex CLI:** inject `RTK.md` / AGENTS.md instruction.
7
- - **Gemini CLI:** no hook system — register rtk as an MCP tool or inject the GEMINI.md instruction.
1
+ # Tool: rtk (token compression)
2
+
3
+ Per-agent wiring (generated by Baustein B):
4
+
5
+ - **Claude Code:** PreToolUse hook auto-rewrites commands (`git status` → `rtk git status`). On Windows this needs WSL; otherwise fall back to instruction mode (this file injected into CLAUDE.md telling the agent to prefix commands with `rtk`).
6
+ - **Codex CLI:** inject `RTK.md` / AGENTS.md instruction.
7
+ - **Gemini CLI:** no hook system — register rtk as an MCP tool or inject the GEMINI.md instruction.
@@ -1,9 +1,9 @@
1
- # Tool: Serena (code-graph, LSP-accurate)
2
-
3
- MIT, MCP-first. The alternative to graphify, selected via `yoke retrofit --code-graph=serena`. Serena uses real language servers (LSP) for symbol-accurate, cross-file retrieval and refactoring (`find_symbol`, `find_referencing_symbols`, rename/move) — no static index that goes stale, so it will not miss a reference.
4
-
5
- Wired as an MCP server for all three agents. Best for large, strongly-typed codebases (TypeScript, Python, Go) doing systematic refactoring, where missing a caller is costly.
6
-
1
+ # Tool: Serena (code-graph, LSP-accurate)
2
+
3
+ MIT, MCP-first. The alternative to graphify, selected via `yoke retrofit --code-graph=serena`. Serena uses real language servers (LSP) for symbol-accurate, cross-file retrieval and refactoring (`find_symbol`, `find_referencing_symbols`, rename/move) — no static index that goes stale, so it will not miss a reference.
4
+
5
+ Wired as an MCP server for all three agents. Best for large, strongly-typed codebases (TypeScript, Python, Go) doing systematic refactoring, where missing a caller is costly.
6
+
7
7
  Caveat: needs one language server per language (can be fiddly on Windows for exotic languages) and requires `uv`. The launch command is a best-effort template — adjust to your install, e.g. `uvx --from git+https://github.com/oraios/serena serena-mcp-server`.
8
8
 
9
9
  Yoke disables Serena's automatic web-dashboard launch in generated MCP configurations. The
@@ -7,7 +7,7 @@ export function detectHostAgent(env = process.env) {
7
7
  return 'claude';
8
8
  if (env.GEMINI_CLI)
9
9
  return 'gemini';
10
- if (env.QWEN_CLI)
10
+ if (env.QWEN_CODE || env.QWEN_CODE_SESSION_ID || env.QWEN_CLI)
11
11
  return 'qwen';
12
12
  if (env.CODEX_HOME)
13
13
  return 'codex';
@@ -17,12 +17,12 @@ const argsFor = (agent, permissions) => {
17
17
  // Automatic review already selects workspace-write and conflicts with --sandbox.
18
18
  return ['exec', '--approve-for-me', '--json'];
19
19
  }
20
- // Qwen CLI is a Gemini fork with similar arguments
20
+ // Qwen Code uses its own approval modes and native tool exclusions.
21
21
  if (agent === 'qwen') {
22
22
  if (permissions === 'unsafe')
23
23
  return ['--yolo', '--output-format', 'stream-json'];
24
- const approval = permissions === 'read-only' ? 'plan' : 'auto_edit';
25
- return ['--approval-mode', approval, '--sandbox', '--output-format', 'stream-json'];
24
+ const approval = permissions === 'read-only' ? 'plan' : 'auto-edit';
25
+ return ['--approval-mode', approval, '--sandbox', '--output-format', 'stream-json', ...(permissions === 'safe' ? ['--allowed-tools', 'run_shell_command'] : [])];
26
26
  }
27
27
  if (permissions === 'unsafe')
28
28
  return ['--yolo', '--output-format', 'stream-json'];
@@ -59,8 +59,18 @@ export function buildProviderInvocation(agent, prompt, cwd, permissions = 'safe'
59
59
  else
60
60
  throw new Error(`${agent} structured output schema requires ${agent === 'codex' ? 'schemaFile' : agent === 'claude' ? 'jsonSchema' : 'a supported native schema option (unavailable)'}`);
61
61
  }
62
- if (parsedSelection.model)
63
- args.push('--model', parsedSelection.model);
62
+ if (parsedSelection.model) {
63
+ const qualified = agent === 'qwen' && parsedSelection.model.includes('::')
64
+ ? parsedSelection.model.match(/^(openai|anthropic|gemini|vertex-ai|qwen-oauth)::(.+)$/u) : undefined;
65
+ if (agent === 'qwen' && parsedSelection.model.includes('::') && !qualified)
66
+ throw Error('Invalid Qwen auth/model selector');
67
+ if (qualified) {
68
+ const model = ModelSelectionSchema.shape.model.parse(qualified[2]);
69
+ args.push('--auth-type', qualified[1], '--model', model);
70
+ }
71
+ else
72
+ args.push('--model', parsedSelection.model);
73
+ }
64
74
  if (parsedSelection.reasoningEffort) {
65
75
  if (agent === 'claude')
66
76
  args.push('--effort', parsedSelection.reasoningEffort);
@@ -71,6 +81,9 @@ export function buildProviderInvocation(agent, prompt, cwd, permissions = 'safe'
71
81
  args.push('--disable', 'multi_agent');
72
82
  if (agent === 'claude' && parsedSelection.nativeMultiAgent === false)
73
83
  args.push('--disallowedTools', 'Agent', 'Task', 'TeamCreate', 'SendMessage');
84
+ if (agent === 'qwen' && parsedSelection.nativeMultiAgent === false) {
85
+ args.push('--exclude-tools', 'agent', 'task', 'create_sub_session', 'team_create', 'send_message');
86
+ }
74
87
  if (parsedSelection.bare) {
75
88
  if (agent === 'codex')
76
89
  args.push('--ignore-user-config');
@@ -22,6 +22,8 @@ export function parseProviderResult(agent, output) {
22
22
  if (direct !== undefined)
23
23
  return direct;
24
24
  }
25
+ if (agent === 'qwen')
26
+ return parseQwenResult(output);
25
27
  const fragments = [];
26
28
  for (const line of output.split(/\r?\n/u)) {
27
29
  const parsed = parseJson(line);
@@ -45,10 +47,6 @@ export function parseProviderResult(agent, output) {
45
47
  if (event.type === 'message' && event.role === 'assistant' && typeof event.content === 'string')
46
48
  fragments.push(event.content);
47
49
  break;
48
- case 'qwen':
49
- if (event.type === 'message' && event.role === 'assistant' && typeof event.content === 'string')
50
- fragments.push(event.content);
51
- break;
52
50
  }
53
51
  }
54
52
  const joined = parseJson(fragments.join(''));
@@ -67,6 +65,34 @@ export function parseProviderResult(agent, output) {
67
65
  }
68
66
  return null;
69
67
  }
68
+ /** Qwen uses assistant content blocks and result envelopes, not Gemini messages. */
69
+ function parseQwenResult(output) {
70
+ let candidate = null;
71
+ for (const line of output.split(/\r?\n/u)) {
72
+ const parsed = parseJson(line);
73
+ if (!parsed.ok || !isRecord(parsed.value))
74
+ continue;
75
+ const event = parsed.value;
76
+ if (event.parent_tool_use_id != null)
77
+ continue;
78
+ if (event.type === 'result') {
79
+ if (event.is_error === true) {
80
+ candidate = null;
81
+ continue;
82
+ }
83
+ const structured = directMachineResult(event.structured_result);
84
+ const result = typeof event.result === 'string' ? parseJson(event.result) : { ok: false };
85
+ candidate = structured ?? (result.ok ? directMachineResult(result.value) : undefined) ?? null;
86
+ }
87
+ else if (event.type === 'assistant' && isRecord(event.message) && Array.isArray(event.message.content)) {
88
+ const text = event.message.content.filter(isRecord).filter(part => part.type === 'text' && typeof part.text === 'string').map(part => part.text).join('');
89
+ const result = parseJson(text);
90
+ if (result.ok)
91
+ candidate = directMachineResult(result.value) ?? candidate;
92
+ }
93
+ }
94
+ return candidate;
95
+ }
70
96
  export function parseProviderTelemetry(agent, lines) {
71
97
  let inputTokens;
72
98
  let cachedInputTokens;
@@ -87,6 +113,8 @@ export function parseProviderTelemetry(agent, lines) {
87
113
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
88
114
  continue;
89
115
  const event = parsed;
116
+ if (agent === 'qwen' && event.parent_tool_use_id != null)
117
+ continue;
90
118
  const message = event.message && typeof event.message === 'object' ? event.message : undefined;
91
119
  const stats = event.stats && typeof event.stats === 'object' ? event.stats : undefined;
92
120
  const usage = (event.usage && typeof event.usage === 'object'
@@ -105,41 +133,12 @@ export function parseProviderTelemetry(agent, lines) {
105
133
  // Older JSON stats only provide model-local token objects. Sum a field
106
134
  // only when every model measured it; a missing measurement is not zero.
107
135
  let source = usage ?? nestedModelTokens ?? modelUsage;
108
- if (agent === 'gemini' && modelEntries.length > 0) {
109
- reportedModels = modelEntries.map(([name]) => name);
110
- model = firstModel?.[0];
111
- 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) {
136
+ if ((agent === 'gemini' || agent === 'qwen') && modelEntries.length > 0) {
138
137
  reportedModels = modelEntries.map(([name]) => name);
139
138
  model = firstModel?.[0];
140
139
  const fields = {
141
- input_tokens: ['input_tokens', 'inputTokens', 'promptTokenCount', 'input'],
142
- output_tokens: ['output_tokens', 'outputTokens', 'candidatesTokenCount', 'output'],
140
+ input_tokens: ['input_tokens', 'inputTokens', 'promptTokenCount', 'input', 'prompt'],
141
+ output_tokens: ['output_tokens', 'outputTokens', 'candidatesTokenCount', 'output', 'candidates'],
143
142
  cached_input_tokens: ['cached_input_tokens', 'cachedInputTokens', 'cachedContentTokenCount', 'cached'],
144
143
  reasoning_output_tokens: ['reasoning_output_tokens', 'reasoningOutputTokens', 'thoughtsTokenCount', 'thoughts'],
145
144
  };
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';
@@ -114,7 +115,7 @@ export function main(argv) {
114
115
  const agentTokens = agentArg === 'all' ? valid : agentArg?.split(',').map(a => a.trim());
115
116
  const invalidAgents = agentTokens?.filter(a => !valid.includes(a)) ?? [];
116
117
  if (agentArg && (invalidAgents.length > 0 || agentTokens?.length === 0)) {
117
- console.error(`Invalid --agent value: ${agentArg} (expected claude,codex,gemini|all)`);
118
+ console.error(`Invalid --agent value: ${agentArg} (expected claude,codex,gemini,qwen|all)`);
118
119
  return 1;
119
120
  }
120
121
  const agents = agentTokens ? [...new Set(agentTokens)] : undefined;
@@ -140,7 +141,14 @@ export function main(argv) {
140
141
  console.error('Invalid routing strategy');
141
142
  return 1;
142
143
  }
144
+ const modelProviderArg = rest.find(a => a.startsWith('--model-provider='))?.slice('--model-provider='.length);
145
+ const modelProviders = modelProviderArg?.split(',').map(value => value.trim());
146
+ if (modelProviders?.some(value => !MODEL_PROVIDERS.includes(value))) {
147
+ console.error('Invalid --model-provider (expected deepseek,kimi)');
148
+ return 1;
149
+ }
143
150
  return runSetup(targetDir, {
151
+ modelProviders: modelProviders,
144
152
  host: hostArg, agents, runner: runnerArg,
145
153
  codeGraph: graphArg,
146
154
  loop, routing, decisionPolicy: policyArg,
@@ -479,7 +487,7 @@ export function main(argv) {
479
487
  let reviewer;
480
488
  if (reviewerArg) {
481
489
  if (!valid.includes(reviewerArg)) {
482
- console.error(`Invalid --reviewer value: ${reviewerArg} (expected claude|codex|gemini)`);
490
+ console.error(`Invalid --reviewer value: ${reviewerArg} (expected claude|codex|gemini|qwen)`);
483
491
  return 1;
484
492
  }
485
493
  reviewer = reviewerArg;
@@ -522,13 +530,13 @@ export function main(argv) {
522
530
  }
523
531
  return runLoopCommand(targetDir, { maxIterations: rawMax, agent, isolate, resumeWorktree: rest.includes('--resume-worktree'), parallel, parallelAuto: parallelArg === '--parallel=auto', reviewer, review, allowSelfReview, timeoutMinutes, json, routing, onAmbiguity: oaArg, decisionPolicy: dpArg, permissions, ...qualityFlags.options });
524
532
  }
525
- console.log('usage: yoke loop <on|off|status|decision|answer|resume [--discard] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N]|cleanup [--remove-worktrees] [--discard-stale-recovery]|run [--max=N] [--parallel=<auto|N>] [--runner=<claude|codex|gemini>] [--reviewer=<claude|codex|gemini>] [--review] [--allow-self-review] [--routing|--no-routing] [--isolate|--no-isolate] [--unsafe] [--timeout=<minutes>] [--decision-policy=<auto|critical>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N] [--json]> [targetDir]');
533
+ console.log('usage: yoke loop <on|off|status|decision|answer|resume [--discard] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N]|cleanup [--remove-worktrees] [--discard-stale-recovery]|run [--max=N] [--parallel=<auto|N>] [--runner=<claude|codex|gemini|qwen>] [--reviewer=<claude|codex|gemini|qwen>] [--review] [--allow-self-review] [--routing|--no-routing] [--isolate|--no-isolate] [--unsafe] [--timeout=<minutes>] [--decision-policy=<auto|critical>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N] [--json]> [targetDir]');
526
534
  return 1;
527
535
  }
528
536
  case 'new': {
529
537
  const dir = rest.find(a => !a.startsWith('-'));
530
538
  if (!dir) {
531
- console.error('usage: yoke new <dir> [--idea="..."] [--agent=claude,codex,gemini|all] [--runner=<claude|codex|gemini>] [--loop]');
539
+ console.error('usage: yoke new <dir> [--idea="..."] [--agent=claude,codex,gemini,qwen|all] [--runner=<claude|codex|gemini|qwen>] [--loop]');
532
540
  return 1;
533
541
  }
534
542
  const idea = rest.find(a => a.startsWith('--idea='))?.slice('--idea='.length);
@@ -543,7 +551,7 @@ export function main(argv) {
543
551
  }
544
552
  const runnerArg = rest.find(a => a.startsWith('--runner='))?.slice('--runner='.length);
545
553
  if (runnerArg && !all.includes(runnerArg)) {
546
- console.error(`Invalid --runner value: ${runnerArg} (expected claude|codex|gemini)`);
554
+ console.error(`Invalid --runner value: ${runnerArg} (expected claude|codex|gemini|qwen)`);
547
555
  return 1;
548
556
  }
549
557
  return runNew(dir, { idea, agents, runner: runnerArg, loop });
@@ -562,7 +570,7 @@ export function main(argv) {
562
570
  if (sub === 'draft') {
563
571
  const idea = rest.find(a => a.startsWith('--idea='))?.slice('--idea='.length);
564
572
  if (!idea) {
565
- console.error('usage: yoke prd draft [dir] --idea="..." [--runner=<claude|codex|gemini>] [--force] [--timeout=<minutes>]');
573
+ console.error('usage: yoke prd draft [dir] --idea="..." [--runner=<claude|codex|gemini|qwen>] [--force] [--timeout=<minutes>]');
566
574
  return 1;
567
575
  }
568
576
  const valid = ['claude', 'codex', 'gemini', 'qwen'];
@@ -586,7 +594,7 @@ export function main(argv) {
586
594
  }
587
595
  if (sub === 'check')
588
596
  return runPrdCheck(targetDir);
589
- console.log('usage: yoke prd <draft|check|assess> [dir] [--idea="..."] [--runner=<claude|codex|gemini>] [--story=<id>] [--reassess] [--force] [--timeout=<minutes>]');
597
+ console.log('usage: yoke prd <draft|check|assess> [dir] [--idea="..."] [--runner=<claude|codex|gemini|qwen>] [--story=<id>] [--reassess] [--force] [--timeout=<minutes>]');
590
598
  return 1;
591
599
  }
592
600
  case 'context': {
@@ -601,10 +609,10 @@ export function main(argv) {
601
609
  }
602
610
  case 'review': {
603
611
  const targetDir = rest.find(a => !a.startsWith('-')) ?? '.';
604
- const valid = ['claude', 'codex', 'gemini'];
612
+ const valid = ['claude', 'codex', 'gemini', 'qwen'];
605
613
  const reviewerArg = rest.find(a => a.startsWith('--reviewer='))?.slice('--reviewer='.length);
606
614
  if (reviewerArg && !valid.includes(reviewerArg)) {
607
- console.error(`Invalid --reviewer value: ${reviewerArg} (expected claude|codex|gemini)`);
615
+ console.error(`Invalid --reviewer value: ${reviewerArg} (expected claude|codex|gemini|qwen)`);
608
616
  return 1;
609
617
  }
610
618
  const base = rest.find(a => a.startsWith('--base='))?.slice('--base='.length);
@@ -651,7 +659,7 @@ export function main(argv) {
651
659
  return runUpgrade();
652
660
  default:
653
661
  console.log('Project workflows: yoke check [dir] [--json|--protect] | goal set|run|resume|pause|status|handoff|budget [dir] | projects add|list|remove | dashboard [dir] [--port=N]');
654
- console.log('usage: yoke <setup [dir] | new <dir> [--idea="..."] | validate [canonDir] | retrofit [targetDir] [--agent=claude,codex,gemini|all] [--code-graph=graphify|serena] [--loop] | change <add|status> [dir] | prd <draft|check|assess> [dir] | loop <on|off|status|decision|answer|resume|run|cleanup> | context <init|status> | review [dir] [--reviewer=<claude|codex|gemini>] [--base=<ref>] [--focus="..."] | design-scan [dir] [--max=N] [--report] | flow-smoke [dir] [--url=<baseUrl>] [--label=<name>] | upgrade>');
662
+ console.log('usage: yoke <setup [dir] | new <dir> [--idea="..."] | validate [canonDir] | retrofit [targetDir] [--agent=claude,codex,gemini,qwen|all] [--code-graph=graphify|serena] [--loop] | change <add|status> [dir] | prd <draft|check|assess> [dir] | loop <on|off|status|decision|answer|resume|run|cleanup> | context <init|status> | review [dir] [--reviewer=<claude|codex|gemini|qwen>] [--base=<ref>] [--focus="..."] | design-scan [dir] [--max=N] [--report] | flow-smoke [dir] [--url=<baseUrl>] [--label=<name>] | upgrade>');
655
663
  return cmd ? 1 : 0;
656
664
  }
657
665
  }