@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
@@ -5,23 +5,23 @@ import { acceptanceText, criterionCommandProblem, isAcceptanceCriterion, loadPrd
5
5
  import { agentInvocation, buildWatchdogInvocation, runAgent, isAgentAvailable, } from '../loop/runner.js';
6
6
  import { resolveIdleMs } from '../loop/run-command.js';
7
7
  import { detectHostAgent, resolveRunnerAgent } from '../agents/host.js';
8
- export const PRD_TEMPLATE = `# Yoke PRD — the loop picks the lowest-priority open story each iteration.
9
- # Story format (see canon/loop/prd.schema.md):
10
- # - id: STORY-1
11
- # title: scaffold the project with a runnable test suite
12
- # priority: 1
13
- # needs: [] # optional story IDs that must pass first
14
- # area: foundation # optional collision domain for parallel runs
15
- # agent: codex # optional claude|codex|gemini affinity
16
- # acceptance:
17
- # - id: suite-runs
18
- # text: "the project test suite can run"
19
- # verify: ["npm run test:suite-runs"]
20
- # - id: scaffold-starts
21
- # text: "the scaffolded application starts"
22
- # verify: ["npm run test:scaffold-starts"]
23
- # passes: false
24
- []
8
+ export const PRD_TEMPLATE = `# Yoke PRD — the loop picks the lowest-priority open story each iteration.
9
+ # Story format (see canon/loop/prd.schema.md):
10
+ # - id: STORY-1
11
+ # title: scaffold the project with a runnable test suite
12
+ # priority: 1
13
+ # needs: [] # optional story IDs that must pass first
14
+ # area: foundation # optional collision domain for parallel runs
15
+ # agent: codex # optional claude|codex|gemini affinity
16
+ # acceptance:
17
+ # - id: suite-runs
18
+ # text: "the project test suite can run"
19
+ # verify: ["npm run test:suite-runs"]
20
+ # - id: scaffold-starts
21
+ # text: "the scaffolded application starts"
22
+ # verify: ["npm run test:scaffold-starts"]
23
+ # passes: false
24
+ []
25
25
  `;
26
26
  export const MAX_PLANNING_BRIEF_CHARS = 20_000;
27
27
  export const MAX_PLANNING_BRIEF_BYTES = MAX_PLANNING_BRIEF_CHARS * 4;
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
1
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { join, dirname } from 'node:path';
3
3
  import { mergeJson } from './merge-json.js';
4
4
  import { carryPreserved } from './preserve.js';
@@ -10,13 +10,17 @@ export function applyActions(actions, targetDir, opts) {
10
10
  let backedUp;
11
11
  let content = action.content;
12
12
  let reason = action.reason;
13
+ if (action.merge && typeof action.content !== 'string') {
14
+ throw new Error(`yoke: binary content cannot be merged: ${action.target}`);
15
+ }
13
16
  if (existsSync(dest)) {
14
17
  if (action.ifAbsent) {
15
18
  results.push({ target: action.target, status: 'unchanged', reason: `${action.reason} (exists, left untouched)` });
16
19
  continue;
17
20
  }
18
- const current = readFileSync(dest, 'utf8');
21
+ const currentBytes = readFileSync(dest);
19
22
  if (action.merge) {
23
+ const current = currentBytes.toString('utf8');
20
24
  let parsedCurrent;
21
25
  try {
22
26
  parsedCurrent = JSON.parse(current);
@@ -37,11 +41,18 @@ export function applyActions(actions, targetDir, opts) {
37
41
  results.push({ target: action.target, status: 'merged', backedUp, reason: action.reason });
38
42
  continue;
39
43
  }
40
- // Carry user content marked with yoke preserve markers into the new file.
41
- content = carryPreserved(current, action.content);
42
- if (content !== action.content)
43
- reason = `${action.reason} (preserve block kept)`;
44
- if (current === content) {
44
+ if (typeof action.content === 'string') {
45
+ const current = currentBytes.toString('utf8');
46
+ // Carry user content marked with yoke preserve markers into the new file.
47
+ content = carryPreserved(current, action.content);
48
+ if (content !== action.content)
49
+ reason = `${action.reason} (preserve block kept)`;
50
+ if (current === content) {
51
+ results.push({ target: action.target, status: 'unchanged', reason: action.reason });
52
+ continue;
53
+ }
54
+ }
55
+ else if (currentBytes.equals(Buffer.from(action.content))) {
45
56
  results.push({ target: action.target, status: 'unchanged', reason: action.reason });
46
57
  continue;
47
58
  }
@@ -55,6 +66,10 @@ export function applyActions(actions, targetDir, opts) {
55
66
  }
56
67
  mkdirSync(dirname(dest), { recursive: true });
57
68
  writeFileSync(dest, content);
69
+ if (action.executable !== undefined && process.platform !== 'win32') {
70
+ const currentMode = statSync(dest).mode;
71
+ chmodSync(dest, action.executable ? currentMode | 0o111 : currentMode & ~0o111);
72
+ }
58
73
  results.push({ target: action.target, status, backedUp, reason });
59
74
  }
60
75
  return results;
@@ -31,8 +31,11 @@ export function runRetrofit(targetDir, opts) {
31
31
  agents: mergedAgents,
32
32
  loop: { ...existing?.loop, enabled: opts.loop },
33
33
  codeGraph,
34
+ ...(existing?.design
35
+ ? { design: existing.design }
36
+ : detection.ui.detected ? { design: { mode: 'auto', max: 4 } } : {}),
34
37
  };
35
38
  saveConfig(targetDir, config);
36
- console.log(formatReport(applied, { loopEnabled: config.loop.enabled, detectedAgents: detection.agents }));
39
+ console.log(formatReport(applied, { loopEnabled: config.loop.enabled, detectedAgents: detection.agents, ui: detection.ui }));
37
40
  return 0;
38
41
  }
@@ -80,6 +80,10 @@ export const YokeConfigSchema = z.object({
80
80
  // Optional performance budget gate: a benchmark command that must exit 0 for a
81
81
  // story to land (runs after verify). Benchmarks are noisy → retried like verify.
82
82
  perf: z.object({ command: z.string().min(1), retries: z.number().int().nonnegative().optional() }).optional(),
83
+ design: z.object({
84
+ mode: z.enum(['off', 'auto', 'on']),
85
+ max: z.number().int().positive().default(4),
86
+ }).optional(),
83
87
  codeGraph: CodeGraphSchema.optional(),
84
88
  smoke: SmokeSchema.optional(),
85
89
  quality: ProjectQualityDefaultsSchema.optional(),
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- const CONTEXT_FILES = ['PROJECT.md', 'DECISIONS.md', 'KNOWLEDGE.md'];
3
+ const CONTEXT_FILES = ['PROJECT.md', 'DECISIONS.md', 'KNOWLEDGE.md', 'GLOSSARY.md'];
4
4
  export function baseContextActions(canonDir) {
5
5
  return CONTEXT_FILES.map(name => {
6
6
  let content;
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { detectUiProject } from './ui-detect.js';
3
4
  export function detectProject(targetDir) {
4
5
  const has = (...parts) => existsSync(join(targetDir, ...parts));
5
6
  const agents = [];
@@ -13,5 +14,6 @@ export function detectProject(targetDir) {
13
14
  agents,
14
15
  hasAgentsMd: has('AGENTS.md'),
15
16
  hasYokeConfig: has('.yoke', 'config.yaml'),
17
+ ui: detectUiProject(targetDir),
16
18
  };
17
19
  }
@@ -5,32 +5,28 @@ import { mcpServers, rtkInstruction } from '../tools.js';
5
5
  import { hasWsl } from '../wsl.js';
6
6
  import { detectGstack } from '../gstack.js';
7
7
  import { PRESERVE_SCAFFOLD } from '../preserve.js';
8
- const GSTACK_COMPOSE = `## Composed tools (gstack detected)
9
-
10
- This project also has [gstack](https://github.com/garrytan/gstack) installed. For capabilities Yoke does not ship, prefer gstack's skills:
11
-
12
- - Live-browser QA → \`/qa\`
13
- - Security audit → \`/cso\`
14
- - Ship / deploy → \`/ship\`, \`/land-and-deploy\`
8
+ import { skillPackageActions } from '../skill-actions.js';
9
+ const GSTACK_COMPOSE = `## Composed tools (gstack detected)
10
+
11
+ This project also has [gstack](https://github.com/garrytan/gstack) installed. For capabilities Yoke does not ship, prefer gstack's skills:
12
+
13
+ - Live-browser QA → \`/qa\`
14
+ - Security audit → \`/cso\`
15
+ - Ship / deploy → \`/ship\`, \`/land-and-deploy\`
15
16
  `;
16
- const claudeMd = (rtkNote, composeNote) => `# Project Instructions
17
-
18
- This project uses the Yoke harness. Baseline instructions:
19
-
20
- @AGENTS.md
21
- ${rtkNote ? `\n${rtkNote}\n` : ''}${composeNote ? `\n${composeNote}\n` : ''}
22
- ${PRESERVE_SCAFFOLD}
17
+ const claudeMd = (rtkNote, composeNote) => `# Project Instructions
18
+
19
+ This project uses the Yoke harness. Baseline instructions:
20
+
21
+ @AGENTS.md
22
+ ${rtkNote ? `\n${rtkNote}\n` : ''}${composeNote ? `\n${composeNote}\n` : ''}
23
+ ${PRESERVE_SCAFFOLD}
23
24
  `;
24
25
  export function planClaude(canonDir, targetDir, wslAvailable = hasWsl(), codeGraph = 'graphify', gstackDetected = detectGstack(targetDir)) {
25
26
  const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
26
27
  const actions = [];
27
28
  for (const skill of manifest.skills) {
28
- actions.push({
29
- kind: 'write',
30
- target: `.claude/skills/${skill.id}/SKILL.md`,
31
- content: readFileSync(join(canonDir, skill.path, 'SKILL.md'), 'utf8'),
32
- reason: `skill: ${skill.id}`,
33
- });
29
+ actions.push(...skillPackageActions(canonDir, skill, 'claude'));
34
30
  }
35
31
  actions.push({
36
32
  kind: 'write',
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { loadManifest } from '../../canon/manifest.js';
4
4
  import { mcpServers, rtkInstruction } from '../tools.js';
5
+ import { skillPackageActions } from '../skill-actions.js';
5
6
  function tomlMcp(codeGraph) {
6
7
  const servers = mcpServers(codeGraph);
7
8
  // Codex reads MCP servers from ~/.codex/config.toml. This project-level file is a
@@ -16,17 +17,12 @@ function tomlMcp(codeGraph) {
16
17
  export function planCodex(canonDir, _targetDir, codeGraph = 'graphify') {
17
18
  const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
18
19
  const baseline = readFileSync(join(canonDir, 'AGENTS.md'), 'utf8');
19
- const actions = manifest.skills.map(skill => ({
20
- kind: 'write',
21
- target: `.agents/skills/${skill.id}/SKILL.md`,
22
- content: readFileSync(join(canonDir, skill.path, 'SKILL.md'), 'utf8'),
23
- reason: `skill: ${skill.id}`,
24
- }));
20
+ const actions = manifest.skills.flatMap(skill => skillPackageActions(canonDir, skill, 'codex'));
25
21
  const roles = [
26
22
  ['implementer', 'Implementation specialist for one scoped story.', 'workspace-write', 'Implement only the assigned scope. Use tests first, run verification, and do not review or commit your own work.'],
27
23
  ['reviewer', 'Read-only reviewer for correctness and acceptance criteria.', 'read-only', 'Review observed diffs and test evidence. Do not modify files. Return only findings grounded in evidence.'],
28
24
  ['security', 'Read-only security reviewer for changed code.', 'read-only', 'Inspect changed code for exploitable security regressions. Do not modify files and avoid speculative findings.'],
29
- ['docs', 'Documentation specialist for release and API consistency.', 'workspace-write', 'Update only documentation required by the assigned change. Verify commands and version references against the repository.'],
25
+ ['docs', 'Documentation specialist for release and API consistency.', 'workspace-write', 'Update only documentation required by the assigned change. Verify commands and version references against the repository. Use no-ai-slop Detect mode before prose edits, preserve the writer\'s voice, and apply only observed fixes.'],
30
26
  ];
31
27
  actions.push({
32
28
  kind: 'write',
@@ -4,6 +4,7 @@ import { loadManifest } from '../../canon/manifest.js';
4
4
  import { parseFrontmatter } from '../../canon/frontmatter.js';
5
5
  import { mcpServers, rtkInstruction } from '../tools.js';
6
6
  import { PRESERVE_SCAFFOLD } from '../preserve.js';
7
+ import { skillPackageActions } from '../skill-actions.js';
7
8
  function tomlString(s) {
8
9
  return '"""\n' + s.replace(/\\/g, '\\\\').replace(/"""/g, '\\"\\"\\"') + '\n"""';
9
10
  }
@@ -12,10 +13,18 @@ export function planGemini(canonDir, _targetDir, codeGraph = 'graphify') {
12
13
  const actions = [];
13
14
  // GEMINI.md: baseline + rtk instruction (Gemini has no rewrite hook).
14
15
  const baseline = readFileSync(join(canonDir, 'AGENTS.md'), 'utf8');
16
+ const autoSkillIndex = manifest.skills
17
+ .filter(skill => skill.invocation === 'auto')
18
+ .map(skill => {
19
+ const source = readFileSync(join(canonDir, skill.path, 'SKILL.md'), 'utf8');
20
+ const description = String(parseFrontmatter(source)?.description ?? skill.id).replace(/\s+/gu, ' ').trim();
21
+ return `- \`/${skill.id}\` — ${description}`;
22
+ })
23
+ .join('\n');
15
24
  actions.push({
16
25
  kind: 'write',
17
26
  target: 'GEMINI.md',
18
- content: `${baseline}\n${rtkInstruction()}\n\n${PRESERVE_SCAFFOLD}\n`,
27
+ content: `${baseline}\n${rtkInstruction()}\n\n## Yoke automatic skills\n\nUse the matching command when its capability is relevant:\n\n${autoSkillIndex}\n\n${PRESERVE_SCAFFOLD}\n`,
19
28
  reason: 'baseline + rtk instruction (no hook on Gemini)',
20
29
  });
21
30
  // One TOML slash command per skill.
@@ -33,6 +42,7 @@ export function planGemini(canonDir, _targetDir, codeGraph = 'graphify') {
33
42
  content: `description = "${description.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"\nprompt = ${tomlString(prompt)}\n`,
34
43
  reason: `gemini command: ${skill.id}`,
35
44
  });
45
+ actions.push(...skillPackageActions(canonDir, skill, 'gemini'));
36
46
  }
37
47
  // settings.json: MCP servers + read AGENTS.md as context.
38
48
  actions.push({
@@ -1,7 +1,7 @@
1
1
  export const PRESERVE_START = '<!-- yoke:preserve:start -->';
2
2
  export const PRESERVE_END = '<!-- yoke:preserve:end -->';
3
- export const PRESERVE_SCAFFOLD = `${PRESERVE_START}
4
- <!-- Project-specific instructions go here. Yoke keeps this block across retrofits. -->
3
+ export const PRESERVE_SCAFFOLD = `${PRESERVE_START}
4
+ <!-- Project-specific instructions go here. Yoke keeps this block across retrofits. -->
5
5
  ${PRESERVE_END}`;
6
6
  /**
7
7
  * Extract the inner content of every balanced preserve-marker pair, in order.
@@ -8,6 +8,11 @@ export function formatReport(applied, meta) {
8
8
  }
9
9
  lines.push('');
10
10
  lines.push(`Detected agents: ${meta.detectedAgents.length ? meta.detectedAgents.join(', ') : 'none'}`);
11
+ if (meta.ui) {
12
+ lines.push(meta.ui.detected
13
+ ? `UI project: detected (${meta.ui.signals.join('; ')})`
14
+ : 'UI project: not detected');
15
+ }
11
16
  lines.push(`Summary: ${count('created')} created, ${count('overwritten')} overwritten, ${count('merged')} merged, ${count('unchanged')} unchanged`);
12
17
  lines.push(`Loop: ${meta.loopEnabled ? 'enabled' : 'disabled'}`);
13
18
  return lines.join('\n');
@@ -0,0 +1,66 @@
1
+ import { parse, stringify } from 'yaml';
2
+ import { enumerateSkillPackage } from '../canon/skill-package.js';
3
+ const roots = {
4
+ claude: '.claude/skills',
5
+ codex: '.agents/skills',
6
+ gemini: '.gemini/skills',
7
+ };
8
+ function manualClaudeSkill(content, skill) {
9
+ const source = content.toString('utf8');
10
+ const match = source.match(/^(---\r?\n)([\s\S]*?)(\r?\n---)/u);
11
+ if (!match)
12
+ throw new Error(`skill ${skill.id}: cannot adapt manual invocation without frontmatter`);
13
+ const newline = match[1].includes('\r\n') ? '\r\n' : '\n';
14
+ const frontmatter = match[2];
15
+ const adapted = /(?:^|\r?\n)disable-model-invocation:/u.test(frontmatter)
16
+ ? frontmatter.replace(/(^|\r?\n)disable-model-invocation:\s*[^\r\n]*/u, `$1disable-model-invocation: true`)
17
+ : `${frontmatter}${newline}disable-model-invocation: true`;
18
+ return `${match[1]}${adapted}${match[3]}${source.slice(match[0].length)}`;
19
+ }
20
+ function codexPolicy(skill, existing) {
21
+ const allow = skill.invocation === 'auto';
22
+ const document = existing ? parse(existing.content.toString('utf8')) : {};
23
+ if (document === null || typeof document !== 'object' || Array.isArray(document)) {
24
+ throw new Error(`skill ${skill.id}: agents/openai.yaml must contain a YAML object`);
25
+ }
26
+ const record = document;
27
+ const policy = record.policy;
28
+ if (policy !== undefined && (typeof policy !== 'object' || policy === null || Array.isArray(policy))) {
29
+ throw new Error(`skill ${skill.id}: agents/openai.yaml policy must be a YAML object`);
30
+ }
31
+ const policyRecord = (policy ?? {});
32
+ const declared = policyRecord.allow_implicit_invocation;
33
+ if (declared !== undefined && declared !== allow) {
34
+ throw new Error(`skill ${skill.id}: agents/openai.yaml invocation policy conflicts with manifest`);
35
+ }
36
+ return stringify({ ...record, policy: { ...policyRecord, allow_implicit_invocation: allow } });
37
+ }
38
+ function portableContent(file) {
39
+ const text = file.content.toString('utf8');
40
+ return !text.includes('\0') && Buffer.from(text, 'utf8').equals(file.content) ? text : file.content;
41
+ }
42
+ export function skillPackageActions(canonDir, skill, provider) {
43
+ const files = enumerateSkillPackage(canonDir, skill);
44
+ const policyPath = 'agents/openai.yaml';
45
+ const existingPolicy = provider === 'codex' ? files.find(file => file.relativePath === policyPath) : undefined;
46
+ const actions = files
47
+ .filter(file => provider !== 'codex' || file.relativePath !== policyPath)
48
+ .map(file => ({
49
+ kind: 'write',
50
+ target: `${roots[provider]}/${skill.id}/${file.relativePath}`,
51
+ content: provider === 'claude' && skill.invocation === 'manual' && file.relativePath === 'SKILL.md'
52
+ ? manualClaudeSkill(file.content, skill)
53
+ : portableContent(file),
54
+ executable: file.executable,
55
+ reason: `skill: ${skill.id} (${file.relativePath})`,
56
+ }));
57
+ if (provider === 'codex') {
58
+ actions.push({
59
+ kind: 'write',
60
+ target: `${roots.codex}/${skill.id}/${policyPath}`,
61
+ content: codexPolicy(skill, existingPolicy),
62
+ reason: `skill invocation policy: ${skill.id}`,
63
+ });
64
+ }
65
+ return actions;
66
+ }
@@ -0,0 +1,83 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import { parse } from 'yaml';
4
+ const UI_DEPENDENCIES = new Set([
5
+ '@angular/core', '@astrojs/react', '@astrojs/svelte', '@astrojs/vue',
6
+ '@sveltejs/kit', '@vitejs/plugin-react', '@vitejs/plugin-react-swc',
7
+ '@vitejs/plugin-vue', '@vitejs/plugin-vue-jsx', 'astro', 'next', 'nuxt',
8
+ 'react', 'react-dom', 'svelte', 'vue',
9
+ ]);
10
+ const UI_EXTENSIONS = new Set(['.tsx', '.jsx', '.vue', '.svelte', '.astro']);
11
+ const SOURCE_ROOTS = ['src', 'app', 'pages', 'components', 'web', 'frontend'];
12
+ const IGNORED_DIRECTORIES = new Set([
13
+ '.git', '.next', '.nuxt', '.svelte-kit', '.yoke', '__fixtures__', 'build',
14
+ 'coverage', 'dist', 'fixtures', 'node_modules', 'test', 'tests',
15
+ ]);
16
+ function packageSignals(targetDir) {
17
+ const file = join(targetDir, 'package.json');
18
+ if (!existsSync(file))
19
+ return [];
20
+ try {
21
+ const pkg = JSON.parse(readFileSync(file, 'utf8'));
22
+ const names = new Set();
23
+ for (const key of ['dependencies', 'devDependencies', 'peerDependencies']) {
24
+ const section = pkg[key];
25
+ if (section && typeof section === 'object' && !Array.isArray(section)) {
26
+ for (const name of Object.keys(section))
27
+ if (UI_DEPENDENCIES.has(name))
28
+ names.add(name);
29
+ }
30
+ }
31
+ return [...names].sort().map(name => `dependency: ${name}`);
32
+ }
33
+ catch {
34
+ return [];
35
+ }
36
+ }
37
+ function sourceSignals(targetDir) {
38
+ const signals = [];
39
+ let visited = 0;
40
+ const visit = (directory) => {
41
+ if (visited >= 10_000)
42
+ return;
43
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
44
+ if (visited++ >= 10_000)
45
+ return;
46
+ if (entry.isDirectory()) {
47
+ if (!IGNORED_DIRECTORIES.has(entry.name))
48
+ visit(join(directory, entry.name));
49
+ continue;
50
+ }
51
+ if (!entry.isFile())
52
+ continue;
53
+ const extension = entry.name.slice(entry.name.lastIndexOf('.')).toLowerCase();
54
+ if (UI_EXTENSIONS.has(extension)) {
55
+ signals.push(`source: ${relative(targetDir, join(directory, entry.name)).replaceAll('\\', '/')}`);
56
+ }
57
+ }
58
+ };
59
+ for (const root of SOURCE_ROOTS) {
60
+ const directory = join(targetDir, root);
61
+ if (existsSync(directory))
62
+ visit(directory);
63
+ }
64
+ return signals.sort();
65
+ }
66
+ function hasSmokeFlows(targetDir) {
67
+ const file = join(targetDir, '.yoke', 'config.yaml');
68
+ if (!existsSync(file))
69
+ return false;
70
+ try {
71
+ const document = parse(readFileSync(file, 'utf8'));
72
+ return Array.isArray(document?.smoke?.flows) && document.smoke.flows.length > 0;
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ }
78
+ export function detectUiProject(targetDir) {
79
+ const signals = [...packageSignals(targetDir), ...sourceSignals(targetDir)];
80
+ if (hasSmokeFlows(targetDir))
81
+ signals.push('config: smoke flows');
82
+ return { detected: signals.length > 0, signals };
83
+ }
@@ -0,0 +1,36 @@
1
+ import { relative } from 'node:path';
2
+ import { compactCommandOutput } from '../output/compact.js';
3
+ import { writeOutputArtifact } from '../output/artifact.js';
4
+ import { DEFAULT_OUTPUT_POLICY } from '../output/types.js';
5
+ import { scanDir } from './design.js';
6
+ export function designVerifier(max, options = {}) {
7
+ return (targetDir) => {
8
+ const { findings, score } = scanDir(targetDir);
9
+ const label = `design ${score <= max ? 'passed' : 'failed'}: score ${score} (${findings.length} tell${findings.length === 1 ? '' : 's'}), budget ${max}`;
10
+ if (score <= max)
11
+ return { passed: true, summary: label };
12
+ const raw = findings.map(finding => {
13
+ const file = relative(targetDir, finding.file).replaceAll('\\', '/');
14
+ return `${file}:${finding.line} ${finding.tell} — ${finding.hint}\n ${finding.text}`;
15
+ }).join('\n');
16
+ const policy = options.policy ?? DEFAULT_OUTPUT_POLICY;
17
+ const compacted = compactCommandOutput(raw, { previewBytes: policy.previewBytes });
18
+ const parts = [label];
19
+ if (compacted.preview)
20
+ parts.push(compacted.preview);
21
+ if (compacted.originalBytes > policy.artifactThresholdBytes) {
22
+ try {
23
+ const artifact = (options.artifactWriter ?? writeOutputArtifact)(targetDir, raw, {
24
+ phase: 'design',
25
+ storyId: process.env.YOKE_STORY,
26
+ });
27
+ parts.push(artifact.marker);
28
+ }
29
+ catch (error) {
30
+ const message = error instanceof Error ? error.message : String(error);
31
+ parts.push(`[artifact unavailable: ${message.replace(/[\u0000-\u001F\u007F]/gu, ' ').slice(0, 240)}]`);
32
+ }
33
+ }
34
+ return { passed: false, summary: parts.join('\n') };
35
+ };
36
+ }
@@ -1,33 +1,33 @@
1
- # Migrating to Yoke 1.0
2
-
3
- Yoke 1.0 changes unsafe implicit behavior into explicit policy.
4
-
5
- ## Runner permissions
6
-
7
- The default is `runner.permissions: safe`. Automation that intentionally requires a full
8
- sandbox bypass must pass `--unsafe` or configure `runner.permissions: unsafe`. Use
9
- `read-only` for planning and probing.
10
-
11
- ## Reviews
12
-
13
- Reviewers write `.yoke/review-verdict.json`; Yoke validates and consumes it. The reviewer must
14
- differ from the implementer unless `--allow-self-review` is explicit. CI can add `--json`.
15
-
16
- ## Commit ownership
17
-
18
- Yoke resolves identity before implementation. Configure it when Git has no identity:
19
-
20
- ```yaml
21
- commit:
22
- authorName: HECer
23
- authorEmail: hec_er@web.de
24
- allowCoAuthors: false
25
- ```
26
-
27
- ## PRDs, audit, and cleanup
28
-
29
- Existing PRDs remain valid. Optional `needs`, `area`, and `agent` fields add dependencies,
30
- collision domains, and affinity. Enable the story audit gate with `audit.enabled: true` and
31
- version suppressions with `suppressionsVersion: 1`.
32
-
33
- `yoke loop cleanup` now reports retained worktrees. Add `--remove-worktrees` for deletion.
1
+ # Migrating to Yoke 1.0
2
+
3
+ Yoke 1.0 changes unsafe implicit behavior into explicit policy.
4
+
5
+ ## Runner permissions
6
+
7
+ The default is `runner.permissions: safe`. Automation that intentionally requires a full
8
+ sandbox bypass must pass `--unsafe` or configure `runner.permissions: unsafe`. Use
9
+ `read-only` for planning and probing.
10
+
11
+ ## Reviews
12
+
13
+ Reviewers write `.yoke/review-verdict.json`; Yoke validates and consumes it. The reviewer must
14
+ differ from the implementer unless `--allow-self-review` is explicit. CI can add `--json`.
15
+
16
+ ## Commit ownership
17
+
18
+ Yoke resolves identity before implementation. Configure it when Git has no identity:
19
+
20
+ ```yaml
21
+ commit:
22
+ authorName: HECer
23
+ authorEmail: hec_er@web.de
24
+ allowCoAuthors: false
25
+ ```
26
+
27
+ ## PRDs, audit, and cleanup
28
+
29
+ Existing PRDs remain valid. Optional `needs`, `area`, and `agent` fields add dependencies,
30
+ collision domains, and affinity. Enable the story audit gate with `audit.enabled: true` and
31
+ version suppressions with `suppressionsVersion: 1`.
32
+
33
+ `yoke loop cleanup` now reports retained worktrees. Add `--remove-worktrees` for deletion.
@@ -1,27 +1,27 @@
1
- # Migrating to Yoke 1.1
2
-
3
- Yoke 1.1 makes Claude, Codex, and Gemini share the same setup, planning, runner-selection, and decision behavior.
4
-
5
- Run the setup wizard once in an existing project:
6
-
7
- ```bash
8
- npx @hecer/yoke@1.1.0 setup .
9
- ```
10
-
11
- It preserves existing Yoke configuration and asks for target agents, code graph, loop state, default runner, and decision mode. A non-interactive agent can apply explicit choices with `--yes`.
12
-
13
- The new config fields are optional and backward-compatible:
14
-
15
- ```yaml
16
- runner:
17
- agent: codex
18
- permissions: safe
19
- loop:
20
- enabled: true
21
- timeoutMinutes: 30
22
- decisionPolicy: critical # auto | critical
23
- ```
24
-
25
- `loop.onAmbiguity: resolve|abort` and `--on-ambiguity=` still work. Prefer `decisionPolicy` for new projects: `auto` resolves routine choices without asking; `critical` pauses only for high-impact decisions and continues after `yoke loop answer`. Resume restores the original isolation, review, runner, permissions, timeout, and policy flags instead of silently weakening the run. If the restart cannot begin, `yoke loop resume` retries from the request-bound state stored under Git's private state directory.
26
-
27
- Restart an already-open Codex task after retrofit if it does not discover the newly generated `.agents/skills/yoke-workflow/SKILL.md`.
1
+ # Migrating to Yoke 1.1
2
+
3
+ Yoke 1.1 makes Claude, Codex, and Gemini share the same setup, planning, runner-selection, and decision behavior.
4
+
5
+ Run the setup wizard once in an existing project:
6
+
7
+ ```bash
8
+ npx @hecer/yoke@1.1.0 setup .
9
+ ```
10
+
11
+ It preserves existing Yoke configuration and asks for target agents, code graph, loop state, default runner, and decision mode. A non-interactive agent can apply explicit choices with `--yes`.
12
+
13
+ The new config fields are optional and backward-compatible:
14
+
15
+ ```yaml
16
+ runner:
17
+ agent: codex
18
+ permissions: safe
19
+ loop:
20
+ enabled: true
21
+ timeoutMinutes: 30
22
+ decisionPolicy: critical # auto | critical
23
+ ```
24
+
25
+ `loop.onAmbiguity: resolve|abort` and `--on-ambiguity=` still work. Prefer `decisionPolicy` for new projects: `auto` resolves routine choices without asking; `critical` pauses only for high-impact decisions and continues after `yoke loop answer`. Resume restores the original isolation, review, runner, permissions, timeout, and policy flags instead of silently weakening the run. If the restart cannot begin, `yoke loop resume` retries from the request-bound state stored under Git's private state directory.
26
+
27
+ Restart an already-open Codex task after retrofit if it does not discover the newly generated `.agents/skills/yoke-workflow/SKILL.md`.