@hecer/yoke 1.5.1 → 1.6.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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +29 -14
- package/README.md +56 -35
- package/canon/context/GLOSSARY.md +11 -0
- package/canon/manifest.yaml +36 -30
- package/canon/skills/ATTRIBUTION.md +28 -0
- package/canon/skills/codebase-design/DEEPENING.md +15 -0
- package/canon/skills/codebase-design/DESIGN-IT-TWICE.md +12 -0
- package/canon/skills/codebase-design/SKILL.md +39 -0
- package/canon/skills/document-release/SKILL.md +5 -0
- package/canon/skills/domain-modeling/ADR-FORMAT.md +19 -0
- package/canon/skills/domain-modeling/CONTEXT-FORMAT.md +39 -0
- package/canon/skills/domain-modeling/SKILL.md +35 -0
- package/canon/skills/no-ai-slop/SKILL.md +103 -0
- package/canon/skills/no-ai-slop/eval.md +43 -0
- package/canon/skills/resolving-merge-conflicts/SKILL.md +18 -0
- package/canon/skills/writing-for-agents/SKILL-MECHANICS.md +27 -0
- package/canon/skills/writing-for-agents/SKILL.md +42 -0
- package/dist/canon/manifest.js +2 -0
- package/dist/canon/skill-package.js +113 -0
- package/dist/canon/validate.js +16 -1
- package/dist/context/command.js +4 -1
- package/dist/context/context.js +6 -0
- package/dist/loop/dispatcher.js +1 -1
- package/dist/loop/loop.js +26 -0
- package/dist/loop/parallel-command.js +3 -0
- package/dist/loop/run-command.js +11 -0
- package/dist/loop/watchdog.js +28 -11
- package/dist/loop/worker.js +11 -0
- package/dist/retrofit/apply.js +22 -7
- package/dist/retrofit/command.js +4 -1
- package/dist/retrofit/config.js +4 -0
- package/dist/retrofit/context-actions.js +1 -1
- package/dist/retrofit/detect.js +2 -0
- package/dist/retrofit/planners/claude.js +2 -6
- package/dist/retrofit/planners/codex.js +3 -7
- package/dist/retrofit/planners/gemini.js +11 -1
- package/dist/retrofit/report.js +5 -0
- package/dist/retrofit/skill-actions.js +66 -0
- package/dist/retrofit/ui-detect.js +83 -0
- package/dist/scan/gate.js +36 -0
- package/docs/PUBLISHING.md +2 -2
- package/docs/superpowers/plans/2026-08-20-automatic-ui-design-gate.md +59 -0
- package/docs/superpowers/plans/2026-08-20-capability-skills-and-context.md +51 -0
- package/docs/superpowers/plans/2026-08-20-complete-skill-packages-and-invocation.md +59 -0
- package/docs/superpowers/plans/2026-08-20-windows-reliability-and-release.md +67 -0
- package/docs/superpowers/specs/2026-08-20-skill-capabilities-and-reliability-design.md +391 -0
- package/gemini-extension.json +1 -1
- package/package.json +4 -4
package/dist/retrofit/apply.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
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
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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;
|
package/dist/retrofit/command.js
CHANGED
|
@@ -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
|
}
|
package/dist/retrofit/config.js
CHANGED
|
@@ -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;
|
package/dist/retrofit/detect.js
CHANGED
|
@@ -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,6 +5,7 @@ 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
|
+
import { skillPackageActions } from '../skill-actions.js';
|
|
8
9
|
const GSTACK_COMPOSE = `## Composed tools (gstack detected)
|
|
9
10
|
|
|
10
11
|
This project also has [gstack](https://github.com/garrytan/gstack) installed. For capabilities Yoke does not ship, prefer gstack's skills:
|
|
@@ -25,12 +26,7 @@ export function planClaude(canonDir, targetDir, wslAvailable = hasWsl(), codeGra
|
|
|
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.
|
|
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({
|
package/dist/retrofit/report.js
CHANGED
|
@@ -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
|
+
}
|
package/docs/PUBLISHING.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Publishing channels — status & playbook
|
|
2
2
|
|
|
3
|
-
Where Yoke is published, and how each channel gets updated. (Reviewed 2026-08-
|
|
3
|
+
Where Yoke is published, and how each channel gets updated. (Reviewed 2026-08-20.)
|
|
4
4
|
|
|
5
5
|
## Live
|
|
6
6
|
|
|
@@ -33,7 +33,7 @@ release object is created. Use this idempotent check after the version commit re
|
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
35
|
set -euo pipefail
|
|
36
|
-
VERSION=1.
|
|
36
|
+
VERSION=1.6.0
|
|
37
37
|
TARGET=$(git rev-parse HEAD)
|
|
38
38
|
git fetch --tags origin
|
|
39
39
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Automatic UI Design Gate Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan.
|
|
4
|
+
|
|
5
|
+
**Goal:** Automatically run Yoke's existing design scanner inside configured UI-project loops while preserving current behavior for non-UI and already-configured projects.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Add one deterministic local detector, one default-preserving config section, and a small verifier adapter over the existing scanner. Thread that verifier through the serial and parallel loop gate contracts in the same position and evidence model as existing mechanical gates.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Zod, Vitest, existing design scanner and loop abstractions.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Task 1: Detect UI projects with explained evidence
|
|
14
|
+
|
|
15
|
+
**Files:** `src/retrofit/ui-detect.ts`, `src/retrofit/detect.ts`, `tests/retrofit/ui-detect.test.ts`, `tests/retrofit/detect.test.ts`
|
|
16
|
+
|
|
17
|
+
1. Add failing tests for supported dependencies, `.tsx`, `.jsx`, `.vue`, `.svelte`, `.astro`, an existing smoke-flow config, non-UI TypeScript, and ignored dependency/generated/fixture/Yoke directories.
|
|
18
|
+
2. Implement bounded traversal of normal source roots and package manifests. Return `{ detected, signals }` with stable, human-readable signals.
|
|
19
|
+
3. Attach UI evidence to Retrofit detection without changing existing agent detection semantics.
|
|
20
|
+
4. Run the detector tests.
|
|
21
|
+
|
|
22
|
+
## Task 2: Add default-preserving design configuration
|
|
23
|
+
|
|
24
|
+
**Files:** `src/retrofit/config.ts`, `src/retrofit/command.ts`, `tests/retrofit/config.test.ts`, `tests/retrofit/integration.test.ts`
|
|
25
|
+
|
|
26
|
+
1. Add failing schema tests for `mode: off|auto|on`, positive integer `max`, invalid values, and omitted design config.
|
|
27
|
+
2. Add optional `design` to the config type and schema. Do not place it in the universal legacy default.
|
|
28
|
+
3. During new/retrofit setup, write `{ mode: 'auto', max: 4 }` only when UI detection succeeds and no user design choice exists.
|
|
29
|
+
4. Include the detector's evidence in setup output and test that an existing `off`, `on`, or custom budget is preserved.
|
|
30
|
+
5. Run config and Retrofit integration tests.
|
|
31
|
+
|
|
32
|
+
## Task 3: Adapt design scan results to a mechanical gate
|
|
33
|
+
|
|
34
|
+
**Files:** `src/scan/gate.ts`, `src/scan/design.ts`, `src/cli.ts`, `tests/scan/gate.test.ts`, `tests/scan/design.test.ts`
|
|
35
|
+
|
|
36
|
+
1. Add failing tests for pass/fail at a configured budget, stable finding names, bounded preview text, and a full artifact path for overflow details.
|
|
37
|
+
2. Extract only shared result formatting from the standalone command; do not change its public output or scanner weights.
|
|
38
|
+
3. Implement a verifier adapter that runs `scanDir`, returns the existing verify-result contract, and writes full findings through the existing artifact helper.
|
|
39
|
+
4. Run scan tests and a CLI design-scan regression test.
|
|
40
|
+
|
|
41
|
+
## Task 4: Thread the gate through serial and parallel loops
|
|
42
|
+
|
|
43
|
+
**Files:** `src/loop/run-command.ts`, `src/loop/loop.ts`, `src/loop/worker-contracts.ts`, `src/loop/worker.ts`, `src/loop/parallel-command.ts`, `src/loop/parallel.ts`, `src/loop/dispatcher.ts`, `src/loop/parallel-adapters.ts`, `src/output/types.ts`, matching `tests/loop/*.test.ts`
|
|
44
|
+
|
|
45
|
+
1. Add failing serial tests showing order `criteria -> verify -> design -> perf -> audit` and proving omitted or `off` design leaves the old call sequence unchanged.
|
|
46
|
+
2. Add failing worker and dispatcher tests for design evidence, stage-specific failure, retry/quality-rerun behavior, and cancellation.
|
|
47
|
+
3. Extend gate stage unions, callbacks, evidence, and output phases with `design`; make the verifier optional throughout.
|
|
48
|
+
4. Construct the verifier in `run-command` for `on`, for detected `auto`, and never for `off` or missing config.
|
|
49
|
+
5. Run all loop tests, then `rtk npm run lint`.
|
|
50
|
+
|
|
51
|
+
## Task 5: Verify setup-to-loop behavior
|
|
52
|
+
|
|
53
|
+
**Files:** `tests/retrofit/integration.test.ts`, `tests/loop/run-command.test.ts`
|
|
54
|
+
|
|
55
|
+
1. Add an integration fixture for a detected UI project and one for a non-UI project.
|
|
56
|
+
2. Prove Retrofit writes auto config only to the UI fixture and the next loop run selects the design verifier only there.
|
|
57
|
+
3. Prove `mode: on` overrides failed detection and `mode: off` overrides successful detection.
|
|
58
|
+
4. Run `rtk npm exec -- vitest run tests/retrofit tests/scan tests/loop`.
|
|
59
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Capability Skills and Context Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan.
|
|
4
|
+
|
|
5
|
+
**Goal:** Add the prose, domain, codebase-design, merge-resolution, and agent-writing capabilities identified in the source review, with durable terminology context and clear attribution.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Keep capabilities as independent Canon packages selected through the manifest. Use resource files for evaluation material, keep `.yoke/context` as the durable project source of truth, and connect prose review to documentation workflows without making style a release gate.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Markdown skill packages, TypeScript context loader, YAML manifest, Vitest, provenance audit tools.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Task 1: Import and adapt `no-ai-slop`
|
|
14
|
+
|
|
15
|
+
**Files:** `canon/skills/no-ai-slop/SKILL.md`, `canon/skills/no-ai-slop/eval.md`, `canon/ATTRIBUTION.md`, `canon/manifest.yaml`, `tests/canon/real-canon.test.ts`
|
|
16
|
+
|
|
17
|
+
1. Run a read-only provenance audit on the upstream package and record unknown signals as unknown.
|
|
18
|
+
2. Add failing real-Canon assertions for the package, its `eval.md` reference, auto invocation, and Peter Yang/MIT attribution.
|
|
19
|
+
3. Import the upstream workflow and evaluation checklist. Adapt only the trigger wording, Yoke paths, and the precise-domain-term exception; preserve Edit and Detect modes and prohibit authorship guesses.
|
|
20
|
+
4. Run the package tests and a second read-only provenance audit on the local package.
|
|
21
|
+
|
|
22
|
+
## Task 2: Add the four complementary capabilities
|
|
23
|
+
|
|
24
|
+
**Files:** `canon/skills/domain-modeling/SKILL.md`, `canon/skills/codebase-design/SKILL.md`, `canon/skills/resolving-merge-conflicts/SKILL.md`, `canon/skills/writing-for-agents/SKILL.md`, `canon/ATTRIBUTION.md`, `canon/manifest.yaml`, `tests/canon/real-canon.test.ts`
|
|
25
|
+
|
|
26
|
+
1. Add failing assertions for all four skill ids, frontmatter, auto invocation, and source attribution where material is adapted from Matt Pocock's repository.
|
|
27
|
+
2. Write `domain-modeling` around terminology discovery, scenario checks, code comparison, glossary updates, and the existing ADR threshold.
|
|
28
|
+
3. Write `codebase-design` around deep modules, small interfaces, real seams, public-interface tests, and local change.
|
|
29
|
+
4. Write `resolving-merge-conflicts` around current-operation discovery, commit/spec evidence, preservation of both compatible intents, scoped checks, and no implicit abort.
|
|
30
|
+
5. Write `writing-for-agents` around observable outcomes, concrete paths and commands, bounded context, explicit triggers, and deduplicated durable instructions.
|
|
31
|
+
6. Validate Canon and run the real-Canon tests.
|
|
32
|
+
|
|
33
|
+
## Task 3: Add glossary-aware project context
|
|
34
|
+
|
|
35
|
+
**Files:** `canon/context/GLOSSARY.md`, `src/retrofit/context-actions.ts`, `src/context/context.ts`, `src/context/command.ts`, `tests/retrofit/context-actions.test.ts`, `tests/context/context.test.ts`, `tests/context/command.test.ts`
|
|
36
|
+
|
|
37
|
+
1. Add failing tests that Retrofit introduces `GLOSSARY.md` with `ifAbsent`, existing context is unchanged, and `CONTEXT-MAP.md` is loaded only when present.
|
|
38
|
+
2. Add a concise glossary template for canonical terms, aliases, rejected terms, and concrete examples.
|
|
39
|
+
3. Extend context loading and formatted prompt context with bounded glossary content and optional bounded context-map content.
|
|
40
|
+
4. Extend context status output so required glossary state and optional context-map state are distinguishable.
|
|
41
|
+
5. Run the context and context-action tests.
|
|
42
|
+
|
|
43
|
+
## Task 4: Connect prose quality to documentation workflows
|
|
44
|
+
|
|
45
|
+
**Files:** `canon/skills/document-release/SKILL.md`, `canon/roles/docs.yaml`, `CONTRIBUTING.md`, `tests/canon/real-canon.test.ts`
|
|
46
|
+
|
|
47
|
+
1. Add failing assertions that documentation release guidance invokes `no-ai-slop` evaluation without turning it into a mechanical pass/fail gate.
|
|
48
|
+
2. Update the skill and docs role to request minimal voice-preserving edits and explicit reporting of observed patterns.
|
|
49
|
+
3. Use the new skill in Detect mode on changed prose, then Edit mode only where a listed pattern is actually present.
|
|
50
|
+
4. Run Canon validation and provenance-audit the changed documentation set.
|
|
51
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Complete Skill Packages and Invocation Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan.
|
|
4
|
+
|
|
5
|
+
**Goal:** Install every Canon skill as a complete, safe resource package and preserve explicit automatic or manual invocation intent across Claude, Codex, and Gemini.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Extend the parsed Canon manifest with a defaulted invocation policy, enumerate package files once behind a fail-closed boundary, and make every provider planner consume the same normalized package model. Extend Retrofit actions only enough to preserve byte content and executable intent; provider-specific policy remains generated output, not Canon source.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Zod, YAML, Vitest, Node filesystem APIs.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Task 1: Add invocation policy to the manifest contract
|
|
14
|
+
|
|
15
|
+
**Files:** `src/canon/manifest.ts`, `tests/canon/manifest.test.ts`, `tests/canon/real-canon.test.ts`, `canon/manifest.yaml`
|
|
16
|
+
|
|
17
|
+
1. Add failing tests proving omitted `invocation` parses as `auto`, `manual` parses, and unknown values fail.
|
|
18
|
+
2. Add `InvocationSchema = z.enum(['auto', 'manual'])` and default `invocation` to `auto` on each skill entry.
|
|
19
|
+
3. Add explicit `invocation` values to every real Canon skill and assert the real manifest has no implicit entries.
|
|
20
|
+
4. Run `rtk npm exec -- vitest run tests/canon/manifest.test.ts tests/canon/real-canon.test.ts`.
|
|
21
|
+
|
|
22
|
+
## Task 2: Enumerate and validate complete skill packages
|
|
23
|
+
|
|
24
|
+
**Files:** `src/canon/skill-package.ts`, `src/canon/validate.ts`, `tests/canon/skill-package.test.ts`, `tests/canon/validate.test.ts`
|
|
25
|
+
|
|
26
|
+
1. Write failing tests for stable relative-path ordering, nested Markdown and binary resources, executable metadata, symlink rejection, path escape rejection, unsupported node types, duplicate normalized targets, and missing relative Markdown references.
|
|
27
|
+
2. Implement `enumerateSkillPackage(canonDir, skill)` returning immutable records with `relativePath`, `content: Buffer`, and `executable`.
|
|
28
|
+
3. Normalize separators to `/`, resolve every candidate beneath the skill root, use `lstat` to reject links and special files, and never follow a path outside the root.
|
|
29
|
+
4. Extend Canon validation to enumerate each package and validate local relative Markdown links without fetching HTTP links or validating anchors.
|
|
30
|
+
5. Run `rtk npm exec -- vitest run tests/canon/skill-package.test.ts tests/canon/validate.test.ts` and `rtk npm run yoke -- validate canon`.
|
|
31
|
+
|
|
32
|
+
## Task 3: Let Retrofit actions preserve package bytes and executable intent
|
|
33
|
+
|
|
34
|
+
**Files:** `src/retrofit/plan.ts`, `src/retrofit/apply.ts`, `tests/retrofit/apply.test.ts`
|
|
35
|
+
|
|
36
|
+
1. Add failing tests for a binary action, unchanged binary content, and executable-mode preservation where the platform supports it.
|
|
37
|
+
2. Change write actions to accept `string | Uint8Array` and optional `executable`; retain string-only JSON merge and carry-preserved behavior with explicit guards.
|
|
38
|
+
3. Compare and write buffers without UTF-8 conversion. Apply executable bits on non-Windows after the atomic content write.
|
|
39
|
+
4. Run `rtk npm exec -- vitest run tests/retrofit/apply.test.ts`.
|
|
40
|
+
|
|
41
|
+
## Task 4: Generate provider-specific skill outputs
|
|
42
|
+
|
|
43
|
+
**Files:** `src/retrofit/skill-actions.ts`, `src/retrofit/planners/claude.ts`, `src/retrofit/planners/codex.ts`, `src/retrofit/planners/gemini.ts`, `tests/retrofit/planners/claude.test.ts`, `tests/retrofit/planners/codex.test.ts`, `tests/retrofit/planners/gemini.test.ts`
|
|
44
|
+
|
|
45
|
+
1. Add fixtures containing nested Markdown and a binary resource, plus one manual skill.
|
|
46
|
+
2. Add failing Claude tests for complete package copying and `disable-model-invocation: true` only on generated manual `SKILL.md` frontmatter.
|
|
47
|
+
3. Add failing Codex tests for complete copying and generated or merged `agents/openai.yaml` with `policy.allow_implicit_invocation` matching the manifest.
|
|
48
|
+
4. Add failing Gemini tests for copied resources, unchanged command generation, manual command-only behavior, and a compact auto-skill index that excludes manual skills.
|
|
49
|
+
5. Implement shared action generation from the package enumerator. Ensure generated policy files cannot silently conflict with a Canon-provided file.
|
|
50
|
+
6. Run all three planner test files.
|
|
51
|
+
|
|
52
|
+
## Task 5: Prove backward compatibility end to end
|
|
53
|
+
|
|
54
|
+
**Files:** `tests/retrofit/integration.test.ts`, `tests/canon/real-canon.test.ts`
|
|
55
|
+
|
|
56
|
+
1. Add an integration test that an existing one-file skill installs byte-for-byte as before.
|
|
57
|
+
2. Add an integration test that one resource-bearing skill installs its referenced resource for all three agents.
|
|
58
|
+
3. Run `rtk npm exec -- vitest run tests/canon tests/retrofit` and `rtk npm run lint`.
|
|
59
|
+
|