@hecer/yoke 1.5.1 → 1.6.1

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 (129) hide show
  1. package/.claude-plugin/plugin.json +13 -13
  2. package/.codex-plugin/plugin.json +7 -7
  3. package/CHANGELOG.md +280 -259
  4. package/README.md +855 -834
  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 -0
  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 +43 -43
  26. package/canon/manifest.yaml +59 -53
  27. package/canon/policy/gates.md +7 -7
  28. package/canon/policy/roles.md +9 -9
  29. package/canon/skills/ATTRIBUTION.md +99 -71
  30. package/canon/skills/authoring-prd/SKILL.md +58 -58
  31. package/canon/skills/brainstorming/SKILL.md +164 -164
  32. package/canon/skills/codebase-design/DEEPENING.md +15 -0
  33. package/canon/skills/codebase-design/DESIGN-IT-TWICE.md +12 -0
  34. package/canon/skills/codebase-design/SKILL.md +39 -0
  35. package/canon/skills/dispatching-parallel-agents/SKILL.md +182 -182
  36. package/canon/skills/document-release/SKILL.md +302 -297
  37. package/canon/skills/domain-modeling/ADR-FORMAT.md +19 -0
  38. package/canon/skills/domain-modeling/CONTEXT-FORMAT.md +39 -0
  39. package/canon/skills/domain-modeling/SKILL.md +35 -0
  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 -0
  46. package/canon/skills/no-ai-slop/eval.md +43 -0
  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 -0
  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 -0
  64. package/canon/skills/writing-for-agents/SKILL.md +42 -0
  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/graphify.md +3 -3
  71. package/canon/tools/playwright-mcp.md +3 -3
  72. package/canon/tools/rtk.md +7 -7
  73. package/canon/tools/serena.md +6 -6
  74. package/dist/agents/process.js +3 -0
  75. package/dist/canon/manifest.js +2 -0
  76. package/dist/canon/skill-package.js +113 -0
  77. package/dist/canon/validate.js +16 -1
  78. package/dist/context/command.js +4 -1
  79. package/dist/context/context.js +6 -0
  80. package/dist/loop/dispatcher.js +1 -1
  81. package/dist/loop/loop.js +26 -0
  82. package/dist/loop/parallel-command.js +3 -0
  83. package/dist/loop/run-command.js +11 -0
  84. package/dist/loop/watchdog.js +28 -11
  85. package/dist/loop/worker.js +11 -0
  86. package/dist/prd/command.js +17 -17
  87. package/dist/retrofit/apply.js +22 -7
  88. package/dist/retrofit/command.js +4 -1
  89. package/dist/retrofit/config.js +4 -0
  90. package/dist/retrofit/context-actions.js +1 -1
  91. package/dist/retrofit/detect.js +2 -0
  92. package/dist/retrofit/planners/claude.js +16 -20
  93. package/dist/retrofit/planners/codex.js +3 -7
  94. package/dist/retrofit/planners/gemini.js +11 -1
  95. package/dist/retrofit/preserve.js +2 -2
  96. package/dist/retrofit/report.js +5 -0
  97. package/dist/retrofit/skill-actions.js +66 -0
  98. package/dist/retrofit/ui-detect.js +83 -0
  99. package/dist/scan/gate.js +36 -0
  100. package/docs/MIGRATING-TO-1.0.md +33 -33
  101. package/docs/MIGRATING-TO-1.1.md +27 -27
  102. package/docs/MIGRATING-TO-1.4.md +70 -70
  103. package/docs/PUBLISHING.md +91 -91
  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-08-20-automatic-ui-design-gate.md +59 -0
  114. package/docs/superpowers/plans/2026-08-20-capability-skills-and-context.md +51 -0
  115. package/docs/superpowers/plans/2026-08-20-complete-skill-packages-and-invocation.md +59 -0
  116. package/docs/superpowers/plans/2026-08-20-windows-reliability-and-release.md +67 -0
  117. package/docs/superpowers/specs/2026-06-28-baustein-e-context-layer-design.md +146 -146
  118. package/docs/superpowers/specs/2026-06-29-baustein-f-routing-design.md +106 -106
  119. package/docs/superpowers/specs/2026-06-29-baustein-g-loop-observability-design.md +186 -186
  120. package/docs/superpowers/specs/2026-06-29-baustein-h-loop-robustness-design.md +113 -113
  121. package/docs/superpowers/specs/2026-06-30-baustein-i-visual-design-verification-design.md +98 -98
  122. package/docs/superpowers/specs/2026-07-02-baustein-k-zero-to-100-bootstrap-design.md +200 -200
  123. package/docs/superpowers/specs/2026-07-02-baustein-m-flow-smoke-proofs-design.md +155 -155
  124. package/docs/superpowers/specs/2026-08-13-gauntlet-quality-loop-design.md +422 -422
  125. package/docs/superpowers/specs/2026-08-16-artifact-backed-output-compaction-design.md +166 -166
  126. package/docs/superpowers/specs/2026-08-20-skill-capabilities-and-reliability-design.md +391 -0
  127. package/gemini-extension.json +6 -6
  128. package/hooks/hooks.json +19 -19
  129. package/package.json +84 -84
@@ -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,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.
@@ -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
@@ -81,6 +81,9 @@ export function startProviderProcess(agent, invocation, options = {}) {
81
81
  telemetry: telemetry.finish(),
82
82
  });
83
83
  const finalize = (exitCode) => {
84
+ if (termination && pid !== undefined && !terminationConfirmed) {
85
+ terminationConfirmed = terminateProcessTree(pid, true);
86
+ }
84
87
  const details = evidence();
85
88
  if (recordFailure) {
86
89
  finish({ ...details, kind: 'spawn-failed', error: recordFailure });
@@ -2,10 +2,12 @@ import { z } from 'zod';
2
2
  import { parse } from 'yaml';
3
3
  import { readFileSync } from 'node:fs';
4
4
  export const AgentSchema = z.enum(['claude', 'codex', 'gemini']);
5
+ export const InvocationSchema = z.enum(['auto', 'manual']);
5
6
  export const SkillEntrySchema = z.object({
6
7
  id: z.string().min(1),
7
8
  path: z.string().min(1),
8
9
  kind: z.enum(['methodology', 'role']),
10
+ invocation: InvocationSchema.default('auto'),
9
11
  });
10
12
  export const ToolEntrySchema = z.object({
11
13
  id: z.string().min(1),
@@ -0,0 +1,113 @@
1
+ import { lstatSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
3
+ import { posix } from 'node:path';
4
+ import { parse } from 'yaml';
5
+ function comparePath(left, right) {
6
+ return left < right ? -1 : left > right ? 1 : 0;
7
+ }
8
+ function isWithin(root, candidate) {
9
+ const fromRoot = relative(root, candidate);
10
+ return fromRoot === '' || (!fromRoot.startsWith(`..${sep}`) && fromRoot !== '..' && !isAbsolute(fromRoot));
11
+ }
12
+ export function enumerateSkillPackage(canonDir, skill) {
13
+ const canonRoot = resolve(canonDir);
14
+ const skillRoot = resolve(canonRoot, skill.path);
15
+ if (!isWithin(canonRoot, skillRoot)) {
16
+ throw new Error(`skill ${skill.id}: package path escapes Canon directory: ${skill.path}`);
17
+ }
18
+ const rootStats = lstatSync(skillRoot);
19
+ if (rootStats.isSymbolicLink())
20
+ throw new Error(`skill ${skill.id}: package root is a symbolic link`);
21
+ if (!rootStats.isDirectory())
22
+ throw new Error(`skill ${skill.id}: package root is not a directory`);
23
+ const files = [];
24
+ const targetKeys = new Set();
25
+ const visit = (directory) => {
26
+ const entries = readdirSync(directory, { withFileTypes: true })
27
+ .sort((left, right) => comparePath(left.name, right.name));
28
+ for (const entry of entries) {
29
+ const absolutePath = resolve(directory, entry.name);
30
+ if (!isWithin(skillRoot, absolutePath)) {
31
+ throw new Error(`skill ${skill.id}: package entry escapes skill root: ${entry.name}`);
32
+ }
33
+ const stats = lstatSync(absolutePath);
34
+ const relativePath = relative(skillRoot, absolutePath).split(sep).join('/');
35
+ if (stats.isSymbolicLink())
36
+ throw new Error(`skill ${skill.id}: symbolic link is not allowed: ${relativePath}`);
37
+ if (stats.isDirectory()) {
38
+ visit(absolutePath);
39
+ continue;
40
+ }
41
+ if (!stats.isFile())
42
+ throw new Error(`skill ${skill.id}: unsupported file type: ${relativePath}`);
43
+ const targetKey = relativePath.normalize('NFC').toLocaleLowerCase('en-US');
44
+ if (targetKeys.has(targetKey))
45
+ throw new Error(`skill ${skill.id}: duplicate target path: ${relativePath}`);
46
+ targetKeys.add(targetKey);
47
+ files.push({
48
+ relativePath,
49
+ content: readFileSync(absolutePath),
50
+ executable: (stats.mode & 0o111) !== 0,
51
+ });
52
+ }
53
+ };
54
+ visit(skillRoot);
55
+ return files.sort((left, right) => comparePath(left.relativePath, right.relativePath));
56
+ }
57
+ function markdownDestinations(markdown) {
58
+ const destinations = [];
59
+ const inline = /!?\[[^\]]*\]\(([^)]+)\)/gu;
60
+ for (const match of markdown.matchAll(inline)) {
61
+ const raw = match[1]?.trim() ?? '';
62
+ const destination = raw.startsWith('<')
63
+ ? raw.slice(1, raw.indexOf('>'))
64
+ : raw.match(/^\S+/u)?.[0];
65
+ if (destination)
66
+ destinations.push(destination);
67
+ }
68
+ return destinations;
69
+ }
70
+ export function findSkillPackageReferenceIssues(files) {
71
+ const paths = new Set(files.map(file => file.relativePath.normalize('NFC').toLocaleLowerCase('en-US')));
72
+ const issues = [];
73
+ for (const file of files) {
74
+ if (!file.relativePath.toLowerCase().endsWith('.md'))
75
+ continue;
76
+ for (const rawReference of markdownDestinations(file.content.toString('utf8'))) {
77
+ if (rawReference.startsWith('#') || /^[a-z][a-z0-9+.-]*:/iu.test(rawReference) || rawReference.startsWith('//'))
78
+ continue;
79
+ const withoutSuffix = rawReference.split(/[?#]/u, 1)[0] ?? '';
80
+ let reference;
81
+ try {
82
+ reference = decodeURIComponent(withoutSuffix).replaceAll('\\', '/');
83
+ }
84
+ catch {
85
+ reference = withoutSuffix.replaceAll('\\', '/');
86
+ }
87
+ const joined = posix.normalize(posix.join(posix.dirname(file.relativePath), reference));
88
+ const escapes = reference.startsWith('/') || joined === '..' || joined.startsWith('../');
89
+ if (escapes || !paths.has(joined.normalize('NFC').toLocaleLowerCase('en-US'))) {
90
+ issues.push({ source: file.relativePath, reference: rawReference, reason: escapes ? 'escape' : 'missing' });
91
+ }
92
+ }
93
+ }
94
+ return issues;
95
+ }
96
+ export function codexInvocationPolicyIssue(files, skill) {
97
+ const policyFile = files.find(file => file.relativePath === 'agents/openai.yaml');
98
+ if (!policyFile)
99
+ return undefined;
100
+ const document = parse(policyFile.content.toString('utf8'));
101
+ if (document === null || typeof document !== 'object' || Array.isArray(document)) {
102
+ return `skill ${skill.id}: agents/openai.yaml must contain a YAML object`;
103
+ }
104
+ const policy = document.policy;
105
+ if (policy === null || typeof policy !== 'object' || Array.isArray(policy)) {
106
+ return `skill ${skill.id}: agents/openai.yaml policy must be a YAML object`;
107
+ }
108
+ const declared = policy.allow_implicit_invocation;
109
+ const expected = skill.invocation === 'auto';
110
+ return declared !== undefined && declared !== expected
111
+ ? `skill ${skill.id}: agents/openai.yaml invocation policy conflicts with manifest`
112
+ : undefined;
113
+ }
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { loadManifest } from './manifest.js';
4
4
  import { parseFrontmatter } from './frontmatter.js';
5
+ import { codexInvocationPolicyIssue, enumerateSkillPackage, findSkillPackageReferenceIssues } from './skill-package.js';
5
6
  export function validateCanon(canonDir) {
6
7
  const issues = [];
7
8
  const manifestPath = join(canonDir, 'manifest.yaml');
@@ -30,6 +31,20 @@ export function validateCanon(canonDir) {
30
31
  issues.push({ level: 'error', message: `skill ${s.id}: SKILL.md missing` });
31
32
  continue;
32
33
  }
34
+ try {
35
+ const files = enumerateSkillPackage(canonDir, s);
36
+ for (const reference of findSkillPackageReferenceIssues(files)) {
37
+ const problem = reference.reason === 'escape' ? 'package reference escapes skill root' : 'missing package reference';
38
+ issues.push({ level: 'error', message: `skill ${s.id}: ${problem}: ${reference.reference} (from ${reference.source})` });
39
+ }
40
+ const invocationIssue = codexInvocationPolicyIssue(files, s);
41
+ if (invocationIssue)
42
+ issues.push({ level: 'error', message: invocationIssue });
43
+ }
44
+ catch (error) {
45
+ issues.push({ level: 'error', message: error instanceof Error ? error.message : String(error) });
46
+ continue;
47
+ }
33
48
  const fm = parseFrontmatter(readFileSync(skillMd, 'utf8'));
34
49
  if (!fm) {
35
50
  issues.push({ level: 'error', message: `skill ${s.id}: SKILL.md has no frontmatter` });
@@ -55,7 +70,7 @@ export function validateCanon(canonDir) {
55
70
  issues.push({ level: 'error', message: `${label} not found: ${rel}` });
56
71
  }
57
72
  }
58
- for (const name of ['PROJECT.md', 'DECISIONS.md', 'KNOWLEDGE.md']) {
73
+ for (const name of ['PROJECT.md', 'DECISIONS.md', 'KNOWLEDGE.md', 'GLOSSARY.md']) {
59
74
  if (!existsSync(join(canonDir, 'context', name))) {
60
75
  issues.push({ level: 'error', message: `context template not found: context/${name}` });
61
76
  }
@@ -14,7 +14,7 @@ export function runContextInit(targetDir) {
14
14
  }
15
15
  export function runContextStatus(targetDir) {
16
16
  const dir = contextDir(targetDir);
17
- const files = ['PROJECT.md', 'DECISIONS.md', 'KNOWLEDGE.md'];
17
+ const files = ['PROJECT.md', 'DECISIONS.md', 'KNOWLEDGE.md', 'GLOSSARY.md'];
18
18
  if (!files.some(f => existsSync(join(dir, f)))) {
19
19
  console.log('Context not initialised (no .yoke/context). Run: yoke context init');
20
20
  return 0;
@@ -23,6 +23,9 @@ export function runContextStatus(targetDir) {
23
23
  const p = join(dir, f);
24
24
  console.log(existsSync(p) ? ` ${f.padEnd(13)} ${statSync(p).size} bytes` : ` ${f.padEnd(13)} (missing)`);
25
25
  }
26
+ const contextMap = join(dir, 'CONTEXT-MAP.md');
27
+ if (existsSync(contextMap))
28
+ console.log(` ${'CONTEXT-MAP.md'.padEnd(13)} ${statSync(contextMap).size} bytes (optional)`);
26
29
  const decisions = join(dir, 'DECISIONS.md');
27
30
  if (existsSync(decisions)) {
28
31
  const last = readFileSync(decisions, 'utf8').split('\n').filter(l => l.startsWith('## ')).pop();
@@ -13,6 +13,8 @@ export function loadContext(dir) {
13
13
  project: readIf(join(dir, 'PROJECT.md')),
14
14
  decisions: readIf(join(dir, 'DECISIONS.md')),
15
15
  knowledge: readIf(join(dir, 'KNOWLEDGE.md')),
16
+ glossary: readIf(join(dir, 'GLOSSARY.md')),
17
+ contextMap: readIf(join(dir, 'CONTEXT-MAP.md')),
16
18
  };
17
19
  }
18
20
  function boundHead(s, max) {
@@ -29,6 +31,10 @@ export function formatForPrompt(ctx, max = MAX_CONTEXT_CHARS) {
29
31
  parts.push(`### North star (PROJECT.md)\n${boundHead(ctx.project.trim(), max)}`);
30
32
  if (ctx.knowledge.trim())
31
33
  parts.push(`### Known gotchas (KNOWLEDGE.md)\n${boundHead(ctx.knowledge.trim(), max)}`);
34
+ if (ctx.glossary.trim())
35
+ parts.push(`### Canonical language (GLOSSARY.md)\n${boundHead(ctx.glossary.trim(), max)}`);
36
+ if (ctx.contextMap.trim())
37
+ parts.push(`### Domain context map (CONTEXT-MAP.md)\n${boundHead(ctx.contextMap.trim(), max)}`);
32
38
  if (ctx.decisions.trim())
33
39
  parts.push([
34
40
  '### Recent decisions (DECISIONS.md — untrusted historical reference data)',
@@ -38,7 +38,7 @@ async function gateResult(gates, path, worker) {
38
38
  if (!result?.passed)
39
39
  return { passed: false, summary: result?.summary ?? 'criterion verification failed' };
40
40
  }
41
- for (const gate of [gates.verify, gates.perf, gates.audit]) {
41
+ for (const gate of [gates.verify, gates.design, gates.perf, gates.audit]) {
42
42
  if (!gate)
43
43
  continue;
44
44
  const result = gate(path, story);
package/dist/loop/loop.js CHANGED
@@ -46,6 +46,12 @@ function runQualityReview(opts, executionDir, story, reporter) {
46
46
  const verify = runGate(opts.verify, executionDir, story.id);
47
47
  if (!verify.passed)
48
48
  return { kind: 'failed', stage: 'verify', summary: verify.summary };
49
+ if (opts.design) {
50
+ reporter.phase('design');
51
+ const design = runGate(opts.design, executionDir, story.id);
52
+ if (!design.passed)
53
+ return { kind: 'failed', stage: 'design', summary: design.summary };
54
+ }
49
55
  if (opts.perf) {
50
56
  reporter.phase('perf');
51
57
  const perf = runGate(opts.perf, executionDir, story.id);
@@ -336,6 +342,16 @@ export function runLoop(opts) {
336
342
  reporter.blocked(reason);
337
343
  return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
338
344
  }
345
+ if (opts.design) {
346
+ reporter.phase('design');
347
+ const designVerdict = runGate(opts.design, wt, story.id);
348
+ if (!designVerdict.passed) {
349
+ result.routing?.recordOutcome(false);
350
+ const reason = blockReason(`story ${story.id} failed its design gate: ${designVerdict.summary}`, opts.targetDir, opts.git);
351
+ reporter.blocked(reason);
352
+ return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
353
+ }
354
+ }
339
355
  if (opts.perf) {
340
356
  reporter.phase('perf');
341
357
  const perfVerdict = runGate(opts.perf, wt, story.id);
@@ -452,6 +468,16 @@ export function runLoop(opts) {
452
468
  finalProgress: progress(stories),
453
469
  };
454
470
  }
471
+ if (opts.design) {
472
+ reporter.phase('design');
473
+ const designVerdict = runGate(opts.design, opts.targetDir, story.id);
474
+ if (!designVerdict.passed) {
475
+ result.routing?.recordOutcome(false);
476
+ const reason = blockReason(`story ${story.id} failed its design gate: ${designVerdict.summary}`, opts.targetDir, opts.git);
477
+ reporter.blocked(reason);
478
+ return { status: 'blocked', iterations, reason, finalProgress: progress(stories) };
479
+ }
480
+ }
455
481
  if (opts.perf) {
456
482
  reporter.phase('perf');
457
483
  const perfVerdict = runGate(opts.perf, opts.targetDir, story.id);
@@ -41,6 +41,7 @@ export async function runParallelLoopCommand(input) {
41
41
  onProgress: status => input.reporter.parallel?.(status),
42
42
  gates: {
43
43
  verify: input.verify,
44
+ design: input.design,
44
45
  verifyCriterion: input.verifyCriterion,
45
46
  requireCriterionEvidence: input.requireCriterionEvidence,
46
47
  perf: input.perf,
@@ -57,6 +58,7 @@ export async function runParallelLoopCommand(input) {
57
58
  provider: workerInput.provider,
58
59
  runner,
59
60
  verify: input.verify,
61
+ design: input.design,
60
62
  verifyCriterion: input.verifyCriterion,
61
63
  requireCriterionEvidence: input.requireCriterionEvidence,
62
64
  perf: input.perf,
@@ -142,6 +144,7 @@ function candidateDefinitions(input, worker, candidateCount, pause) {
142
144
  provider: worker.provider,
143
145
  runner,
144
146
  verify: input.verify,
147
+ design: input.design,
145
148
  verifyCriterion: input.verifyCriterion,
146
149
  requireCriterionEvidence: input.requireCriterionEvidence,
147
150
  perf: input.perf,
@@ -18,6 +18,8 @@ import { runChangeApply } from '../change/inbox.js';
18
18
  import { createQualityCommandHooks } from '../quality/command.js';
19
19
  import { resolveQualityPolicy } from '../quality/types.js';
20
20
  import { runParallelLoopCommand } from './parallel-command.js';
21
+ import { detectUiProject } from '../retrofit/ui-detect.js';
22
+ import { designVerifier } from '../scan/gate.js';
21
23
  export const DEFAULT_IDLE_MINUTES = 20;
22
24
  const STALE_MINUTES = 20; // a running status older than this likely means the loop died
23
25
  export function relativeTime(fromIso, now) {
@@ -154,6 +156,13 @@ export function runLoopCommand(targetDir, opts) {
154
156
  }
155
157
  verify = retryingVerifier(commandVerifier(command, { phase: 'verify', policy: outputPolicy }), config.verify?.retries ?? 1);
156
158
  }
159
+ let design = opts.design;
160
+ if (!design && config.design) {
161
+ const enabled = config.design.mode === 'on'
162
+ || (config.design.mode === 'auto' && detectUiProject(targetDir).detected);
163
+ if (enabled)
164
+ design = designVerifier(config.design.max, { policy: outputPolicy });
165
+ }
157
166
  // Optional performance budget gate: same contract as verify (exit 0 = within
158
167
  // budget), same flake tolerance (benchmarks are noisy).
159
168
  let perf = opts.perf;
@@ -420,6 +429,7 @@ export function runLoopCommand(targetDir, opts) {
420
429
  verify,
421
430
  verifyCriterion: (dir, _story, criterion) => commandsVerifier(criterion.verify, { phase: 'criterion', policy: outputPolicy })(dir),
422
431
  requireCriterionEvidence: config.verify?.requireCriteria ?? false,
432
+ design,
423
433
  perf,
424
434
  audit,
425
435
  review,
@@ -442,6 +452,7 @@ export function runLoopCommand(targetDir, opts) {
442
452
  requireCriterionEvidence: config.verify?.requireCriteria ?? false,
443
453
  completion,
444
454
  intake,
455
+ design,
445
456
  perf,
446
457
  audit,
447
458
  maxIterations,
@@ -22,6 +22,14 @@ export function killProcessTree(pid, force = true) {
22
22
  function waitForCleanupRetry() {
23
23
  spawnSync(process.execPath, ['-e', 'Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25)'], { stdio: 'ignore' });
24
24
  }
25
+ function confirmProcessStopped(pid, isProcessAlive) {
26
+ for (let attempt = 0; attempt < 3; attempt++) {
27
+ if (!isProcessAlive(pid))
28
+ return true;
29
+ waitForCleanupRetry();
30
+ }
31
+ return false;
32
+ }
25
33
  export function killProcessForCleanup(pid, platform = process.platform, runTaskkill = (command, args) => spawnSync(command, args, { stdio: 'ignore' }).status, sendSignal = (target, signal) => { process.kill(target, signal); }, isProcessAlive = (target) => {
26
34
  try {
27
35
  process.kill(target, 0);
@@ -31,24 +39,33 @@ export function killProcessForCleanup(pid, platform = process.platform, runTaskk
31
39
  return false;
32
40
  }
33
41
  }) {
34
- if (platform === 'win32')
35
- return runTaskkill('taskkill', ['/PID', String(pid), '/T', '/F']) === 0;
42
+ if (platform === 'win32') {
43
+ if (runTaskkill('taskkill', ['/PID', String(pid), '/T', '/F']) !== 0)
44
+ return false;
45
+ return confirmProcessStopped(pid, isProcessAlive);
46
+ }
36
47
  try {
37
48
  sendSignal(pid, 'SIGKILL');
38
49
  }
39
50
  catch (error) {
40
51
  return error.code === 'ESRCH';
41
52
  }
42
- for (let attempt = 0; attempt < 3; attempt++) {
43
- if (!isProcessAlive(pid))
44
- return true;
45
- waitForCleanupRetry();
46
- }
47
- return false;
53
+ return confirmProcessStopped(pid, isProcessAlive);
48
54
  }
49
- export function killProcessTreeForCleanup(pid, platform = process.platform, runTaskkill = (command, args) => spawnSync(command, args, { stdio: 'ignore' }).status, sendSignal = (target, signal) => { process.kill(target, signal); }) {
50
- if (platform === 'win32')
51
- return runTaskkill('taskkill', ['/PID', String(pid), '/T', '/F']) === 0;
55
+ export function killProcessTreeForCleanup(pid, platform = process.platform, runTaskkill = (command, args) => spawnSync(command, args, { stdio: 'ignore' }).status, sendSignal = (target, signal) => { process.kill(target, signal); }, isProcessAlive = (target) => {
56
+ try {
57
+ process.kill(target, 0);
58
+ return true;
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }) {
64
+ if (platform === 'win32') {
65
+ if (runTaskkill('taskkill', ['/PID', String(pid), '/T', '/F']) !== 0)
66
+ return !isProcessAlive(pid);
67
+ return confirmProcessStopped(pid, isProcessAlive);
68
+ }
52
69
  try {
53
70
  // Provider processes run detached on POSIX, so their PID is also the
54
71
  // process-group leader. Signal the group to reap descendants as well.
@@ -68,6 +68,17 @@ function runMechanicalGates(input, context, evidence) {
68
68
  const afterVerifyCancellation = cancellationReason(input.cancellation);
69
69
  if (afterVerifyCancellation)
70
70
  return { kind: 'cancelled', summary: afterVerifyCancellation };
71
+ if (input.design) {
72
+ input.reporter?.phase('design');
73
+ const design = runGate(input.design, context.targetDir, context.story.id);
74
+ evidence.design = design;
75
+ input.callbacks?.onGate?.('design', design);
76
+ if (!design.passed)
77
+ return { kind: 'failed', stage: 'design', summary: design.summary };
78
+ const afterDesignCancellation = cancellationReason(input.cancellation);
79
+ if (afterDesignCancellation)
80
+ return { kind: 'cancelled', summary: afterDesignCancellation };
81
+ }
71
82
  if (input.perf) {
72
83
  input.reporter?.phase('perf');
73
84
  const perf = runGate(input.perf, context.targetDir, context.story.id);