@hecer/yoke 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +494 -0
  3. package/canon/AGENTS.md +28 -0
  4. package/canon/context/DECISIONS.md +4 -0
  5. package/canon/context/KNOWLEDGE.md +4 -0
  6. package/canon/context/PROJECT.md +15 -0
  7. package/canon/loop/loop-spec.md +30 -0
  8. package/canon/loop/prd.schema.md +14 -0
  9. package/canon/manifest.yaml +47 -0
  10. package/canon/policy/gates.md +7 -0
  11. package/canon/policy/roles.md +9 -0
  12. package/canon/skills/ATTRIBUTION.md +71 -0
  13. package/canon/skills/authoring-prd/SKILL.md +44 -0
  14. package/canon/skills/brainstorming/SKILL.md +164 -0
  15. package/canon/skills/dispatching-parallel-agents/SKILL.md +182 -0
  16. package/canon/skills/document-release/SKILL.md +297 -0
  17. package/canon/skills/executing-plans/SKILL.md +70 -0
  18. package/canon/skills/finishing-a-development-branch/SKILL.md +200 -0
  19. package/canon/skills/health/SKILL.md +177 -0
  20. package/canon/skills/maintaining-context/SKILL.md +34 -0
  21. package/canon/skills/minimal-code/SKILL.md +21 -0
  22. package/canon/skills/plan-ceo-review/SKILL.md +541 -0
  23. package/canon/skills/plan-eng-review/SKILL.md +362 -0
  24. package/canon/skills/receiving-code-review/SKILL.md +213 -0
  25. package/canon/skills/requesting-code-review/SKILL.md +105 -0
  26. package/canon/skills/retro/SKILL.md +397 -0
  27. package/canon/skills/review/SKILL.md +246 -0
  28. package/canon/skills/ship/SKILL.md +696 -0
  29. package/canon/skills/subagent-driven-development/SKILL.md +277 -0
  30. package/canon/skills/systematic-debugging/SKILL.md +296 -0
  31. package/canon/skills/tdd/SKILL.md +371 -0
  32. package/canon/skills/unslop-ui/SKILL.md +34 -0
  33. package/canon/skills/using-git-worktrees/SKILL.md +218 -0
  34. package/canon/skills/verification-before-completion/SKILL.md +139 -0
  35. package/canon/skills/visual-verification/SKILL.md +54 -0
  36. package/canon/skills/workflow/SKILL.md +18 -0
  37. package/canon/skills/writing-plans/SKILL.md +152 -0
  38. package/canon/skills/writing-skills/SKILL.md +655 -0
  39. package/canon/skills/yoke-retrofit/SKILL.md +18 -0
  40. package/canon/tools/graphify.md +3 -0
  41. package/canon/tools/playwright-mcp.md +3 -0
  42. package/canon/tools/rtk.md +7 -0
  43. package/canon/tools/serena.md +7 -0
  44. package/dist/canon/frontmatter.js +10 -0
  45. package/dist/canon/manifest.js +26 -0
  46. package/dist/canon/validate.js +73 -0
  47. package/dist/cli.js +244 -0
  48. package/dist/context/command.js +33 -0
  49. package/dist/context/context.js +57 -0
  50. package/dist/loop/cleanup.js +42 -0
  51. package/dist/loop/gates.js +12 -0
  52. package/dist/loop/git.js +25 -0
  53. package/dist/loop/lock.js +45 -0
  54. package/dist/loop/loop.js +190 -0
  55. package/dist/loop/prd.js +29 -0
  56. package/dist/loop/reporter.js +91 -0
  57. package/dist/loop/run-command.js +134 -0
  58. package/dist/loop/runner.js +157 -0
  59. package/dist/loop/verify.js +38 -0
  60. package/dist/loop/watchdog.js +86 -0
  61. package/dist/new/command.js +53 -0
  62. package/dist/prd/command.js +129 -0
  63. package/dist/retrofit/apply.js +54 -0
  64. package/dist/retrofit/canon-dir.js +22 -0
  65. package/dist/retrofit/command.js +37 -0
  66. package/dist/retrofit/config.js +53 -0
  67. package/dist/retrofit/context-actions.js +21 -0
  68. package/dist/retrofit/detect.js +17 -0
  69. package/dist/retrofit/gitignore.js +26 -0
  70. package/dist/retrofit/gstack.js +19 -0
  71. package/dist/retrofit/merge-json.js +38 -0
  72. package/dist/retrofit/plan.js +29 -0
  73. package/dist/retrofit/planners/claude.js +67 -0
  74. package/dist/retrofit/planners/codex.js +36 -0
  75. package/dist/retrofit/planners/gemini.js +54 -0
  76. package/dist/retrofit/report.js +14 -0
  77. package/dist/retrofit/tools.js +23 -0
  78. package/dist/retrofit/wsl.js +15 -0
  79. package/dist/review/command.js +43 -0
  80. package/dist/scan/design.js +79 -0
  81. package/dist/smoke/command.js +141 -0
  82. package/package.json +61 -0
@@ -0,0 +1,36 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { mcpServers, rtkInstruction } from '../tools.js';
4
+ function tomlMcp(codeGraph) {
5
+ const servers = mcpServers(codeGraph);
6
+ // Codex reads MCP servers from ~/.codex/config.toml. This project-level file is a
7
+ // ready-to-merge snippet; users append these blocks to their global config.
8
+ return Object.entries(servers)
9
+ .map(([name, cfg]) => {
10
+ const args = cfg.args.map(a => `"${a}"`).join(', ');
11
+ return `[mcp_servers.${name}]\ncommand = "${cfg.command}"\nargs = [${args}]\n`;
12
+ })
13
+ .join('\n');
14
+ }
15
+ export function planCodex(canonDir, _targetDir, codeGraph = 'graphify') {
16
+ return [
17
+ {
18
+ kind: 'write',
19
+ target: 'AGENTS.md',
20
+ content: readFileSync(join(canonDir, 'AGENTS.md'), 'utf8'),
21
+ reason: 'baseline instructions (Codex reads AGENTS.md natively)',
22
+ },
23
+ {
24
+ kind: 'write',
25
+ target: '.codex/config.toml',
26
+ content: `# Yoke: MCP servers for Codex. Merge into ~/.codex/config.toml.\n\n${tomlMcp(codeGraph)}`,
27
+ reason: 'MCP servers (code-graph + playwright)',
28
+ },
29
+ {
30
+ kind: 'write',
31
+ target: 'RTK.md',
32
+ content: rtkInstruction() + '\n',
33
+ reason: 'rtk instruction (Codex has no rewrite hook)',
34
+ },
35
+ ];
36
+ }
@@ -0,0 +1,54 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { loadManifest } from '../../canon/manifest.js';
4
+ import { parseFrontmatter } from '../../canon/frontmatter.js';
5
+ import { mcpServers, rtkInstruction } from '../tools.js';
6
+ function tomlString(s) {
7
+ return '"""\n' + s.replace(/\\/g, '\\\\').replace(/"""/g, '\\"\\"\\"') + '\n"""';
8
+ }
9
+ export function planGemini(canonDir, _targetDir, codeGraph = 'graphify') {
10
+ const manifest = loadManifest(join(canonDir, 'manifest.yaml'));
11
+ const actions = [];
12
+ // GEMINI.md: baseline + rtk instruction (Gemini has no rewrite hook).
13
+ const baseline = readFileSync(join(canonDir, 'AGENTS.md'), 'utf8');
14
+ actions.push({
15
+ kind: 'write',
16
+ target: 'GEMINI.md',
17
+ content: `${baseline}\n${rtkInstruction()}\n`,
18
+ reason: 'baseline + rtk instruction (no hook on Gemini)',
19
+ });
20
+ // One TOML slash command per skill.
21
+ for (const skill of manifest.skills) {
22
+ const body = readFileSync(join(canonDir, skill.path, 'SKILL.md'), 'utf8');
23
+ const fm = parseFrontmatter(body) ?? {};
24
+ // Collapse any newlines so the single-line TOML `description = "..."` stays valid
25
+ // even for ported skills whose frontmatter description spans multiple lines.
26
+ const description = String(fm.description ?? skill.id).replace(/\s*\r?\n\s*/g, ' ').trim();
27
+ const skillBody = body.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '').trim();
28
+ const prompt = `You are using the "${skill.id}" skill.\n\n${skillBody}\n\nFollow it for the current task.`;
29
+ actions.push({
30
+ kind: 'write',
31
+ target: `.gemini/commands/${skill.id}.toml`,
32
+ content: `description = "${description.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"\nprompt = ${tomlString(prompt)}\n`,
33
+ reason: `gemini command: ${skill.id}`,
34
+ });
35
+ }
36
+ // settings.json: MCP servers + read AGENTS.md as context.
37
+ actions.push({
38
+ kind: 'write',
39
+ target: '.gemini/settings.json',
40
+ content: JSON.stringify({
41
+ mcpServers: mcpServers(codeGraph),
42
+ context: { fileName: ['AGENTS.md', 'GEMINI.md'] },
43
+ }, null, 2) + '\n',
44
+ reason: 'MCP servers + AGENTS.md context',
45
+ });
46
+ // Also ship AGENTS.md so the context.fileName entry resolves.
47
+ actions.push({
48
+ kind: 'write',
49
+ target: 'AGENTS.md',
50
+ content: baseline,
51
+ reason: 'baseline instructions (shared)',
52
+ });
53
+ return actions;
54
+ }
@@ -0,0 +1,14 @@
1
+ export function formatReport(applied, meta) {
2
+ const count = (s) => applied.filter(a => a.status === s).length;
3
+ const lines = [];
4
+ lines.push('Yoke retrofit (Claude Code):');
5
+ for (const a of applied) {
6
+ const note = a.backedUp ? ` (backup: ${a.backedUp})` : '';
7
+ lines.push(` ${a.status.padEnd(11)} ${a.target}${note}`);
8
+ }
9
+ lines.push('');
10
+ lines.push(`Detected agents: ${meta.detectedAgents.length ? meta.detectedAgents.join(', ') : 'none'}`);
11
+ lines.push(`Summary: ${count('created')} created, ${count('overwritten')} overwritten, ${count('merged')} merged, ${count('unchanged')} unchanged`);
12
+ lines.push(`Loop: ${meta.loopEnabled ? 'enabled' : 'disabled'}`);
13
+ return lines.join('\n');
14
+ }
@@ -0,0 +1,23 @@
1
+ // Best-effort launch commands per code-graph tool. Users may need to adjust these
2
+ // to match their local install (graphify: `uv tool install graphify`; serena: `uv`,
3
+ // e.g. `uvx --from git+https://github.com/oraios/serena serena-mcp-server`).
4
+ const CODE_GRAPH_SERVERS = {
5
+ graphify: { command: 'graphify', args: ['serve'] },
6
+ serena: { command: 'serena', args: ['start-mcp-server'] },
7
+ };
8
+ export function mcpServers(codeGraph = 'graphify') {
9
+ return {
10
+ [codeGraph]: CODE_GRAPH_SERVERS[codeGraph],
11
+ playwright: { command: 'npx', args: ['@playwright/mcp@latest'] },
12
+ };
13
+ }
14
+ // rtk has no transparent-rewrite hook on Codex/Gemini; those agents get this
15
+ // instruction instead. On Claude (Windows) it is also the WSL-less fallback.
16
+ export function rtkInstruction() {
17
+ return [
18
+ '## Token efficiency (rtk)',
19
+ '',
20
+ 'Prefix shell/dev commands with `rtk` to compress their output before it enters context.',
21
+ 'Example: `rtk git status`, `rtk npm test`. See https://github.com/rtk-ai/rtk.',
22
+ ].join('\n');
23
+ }
@@ -0,0 +1,15 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ // True only on Windows where a WSL distribution responds. Used to decide whether
3
+ // rtk can use its transparent PreToolUse hook (needs WSL) or must fall back to
4
+ // instruction mode. Never throws.
5
+ export function hasWsl() {
6
+ if (process.platform !== 'win32')
7
+ return false;
8
+ try {
9
+ execFileSync('wsl', ['--status'], { stdio: 'pipe', timeout: 5000 });
10
+ return true;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
@@ -0,0 +1,43 @@
1
+ import { agentInvocation, buildStandaloneReviewPrompt, buildWatchdogInvocation, runAgent, isAgentAvailable, } from '../loop/runner.js';
2
+ import { resolveIdleMs } from '../loop/run-command.js';
3
+ // Resolve to the first available agent, preferring a *second* model so the review
4
+ // is genuinely cross-model. claude last => a Claude-only box degrades to self-review.
5
+ const RESOLUTION_ORDER = ['codex', 'gemini', 'claude'];
6
+ export function runReview(targetDir, opts = {}) {
7
+ const available = opts.isAvailable ?? isAgentAvailable;
8
+ let reviewer = opts.reviewer;
9
+ if (reviewer) {
10
+ if (!available(reviewer)) {
11
+ console.error(`Reviewer agent CLI "${reviewer}" was not found on PATH. Install it, or pick another with --reviewer=<claude|codex|gemini>.`);
12
+ return 2;
13
+ }
14
+ }
15
+ else {
16
+ reviewer = RESOLUTION_ORDER.find(a => available(a));
17
+ if (!reviewer) {
18
+ console.error('No agent CLI (claude|codex|gemini) found on PATH. Install one to run a review.');
19
+ return 2;
20
+ }
21
+ if (reviewer === 'claude') {
22
+ console.log('Note: only Claude is available — this is a self-review, not cross-model.');
23
+ }
24
+ }
25
+ const scope = opts.base
26
+ ? `the diff ${opts.base}..HEAD`
27
+ : 'the uncommitted working-tree changes (working tree + staged)';
28
+ const prompt = buildStandaloneReviewPrompt(scope, opts.focus);
29
+ const idleMs = resolveIdleMs(opts.timeoutMinutes, undefined);
30
+ // Pass the *agent* invocation to the runner so callers (and tests) see the
31
+ // reviewer command. The default runner adds the watchdog wrapper before exec;
32
+ // an injected run() gets the raw invocation.
33
+ const inv = agentInvocation(reviewer, prompt, targetDir);
34
+ console.log(`Reviewing ${scope} with ${reviewer}...`);
35
+ const run = opts.run ?? ((i) => runAgent(buildWatchdogInvocation(i, idleMs)));
36
+ const result = run(inv);
37
+ if (result.success) {
38
+ console.log(`✓ ${reviewer} approved`);
39
+ return 0;
40
+ }
41
+ console.log(`✗ ${reviewer} found issues (${result.summary})`);
42
+ return 1;
43
+ }
@@ -0,0 +1,79 @@
1
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join, extname } from 'node:path';
3
+ const AI_PURPLE_HEX = /#(6c5ce7|7c3aed|8b5cf6|a855f7|9333ea)\b/i;
4
+ const AI_PURPLE_TW = /\b(from|via|to)-(purple|violet|fuchsia)-(4|5|6|7)00\b/i;
5
+ const NEON_TW = /\b(shadow|drop-shadow)-\[0_0_/i;
6
+ const NEON_CSS = /box-shadow:[^;]*\b0\s+0\s+\d{2,}px/i;
7
+ const EMOJI = /\p{Extended_Pictographic}/u;
8
+ const JSX_ICON_CTX = /<button|<a\s|aria-hidden|(icon|emoji)/i;
9
+ export const TELLS = [
10
+ { name: 'ai-purple', weight: 2, hint: 'AI-purple is the #1 vibecoded tell — pick a real brand color',
11
+ test: (l) => AI_PURPLE_HEX.test(l) || AI_PURPLE_TW.test(l) },
12
+ { name: 'gradient-clip-text', weight: 2, hint: 'Gradient hero text reads as AI-slop — use a solid color + weight',
13
+ test: (l) => (/bg-clip-text/.test(l) && /text-transparent/.test(l)) || /-webkit-background-clip:\s*text/i.test(l) },
14
+ { name: 'neon-glow', weight: 2, hint: 'Neon glow is a tell — use subtle, neutral elevation',
15
+ test: (l) => NEON_TW.test(l) || NEON_CSS.test(l) },
16
+ // A purple gradient legitimately trips BOTH ai-purple and gradient-overload — that double-count
17
+ // is intentional (two distinct tells), and the weights are kept small so it never dominates.
18
+ { name: 'gradient-overload', weight: 1, hint: 'Gradients everywhere flatten hierarchy — use them sparingly',
19
+ test: (l) => /bg-gradient-to-/.test(l) || /linear-gradient\(/i.test(l) },
20
+ { name: 'emoji-icon', weight: 1, hint: 'Emoji-as-icons is a tell — use a real icon set',
21
+ test: (l) => EMOJI.test(l) && JSX_ICON_CTX.test(l) },
22
+ ];
23
+ // One match per (line, tell) at most, so a line with three purple classes counts once.
24
+ export function scanText(text, tells = TELLS) {
25
+ const matches = [];
26
+ const lines = text.split(/\r?\n/);
27
+ lines.forEach((line, i) => {
28
+ for (const tell of tells) {
29
+ if (tell.test(line))
30
+ matches.push({ line: i + 1, tell, text: line.trim().slice(0, 200) });
31
+ }
32
+ });
33
+ return matches;
34
+ }
35
+ const EXT = new Set(['.css', '.scss', '.tsx', '.jsx', '.ts', '.js', '.html', '.vue', '.svelte', '.astro']);
36
+ const SKIP = new Set(['node_modules', 'dist', '.next', 'build', '.yoke', 'coverage', '.git', 'out', '__fixtures__', '__mocks__']);
37
+ // Test/spec/story files routinely embed slop patterns as FIXTURES (a purple gradient in a
38
+ // snapshot, a neon shadow in a render test). Scanning them would make the gate flag test data
39
+ // as if it were shipped UI, so they're excluded — the scanner gates real source, not fixtures.
40
+ const FIXTURE = /\.(test|spec|stories|story)\.[cm]?[jt]sx?$/i;
41
+ function walk(dir, acc) {
42
+ for (const entry of readdirSync(dir)) {
43
+ const full = join(dir, entry);
44
+ let s;
45
+ try {
46
+ s = statSync(full);
47
+ }
48
+ catch {
49
+ continue;
50
+ }
51
+ if (s.isDirectory()) {
52
+ if (!SKIP.has(entry))
53
+ walk(full, acc);
54
+ }
55
+ else if (EXT.has(extname(entry).toLowerCase()) && !FIXTURE.test(entry)) {
56
+ acc.push(full);
57
+ }
58
+ }
59
+ }
60
+ export function scanDir(dir, tells = TELLS) {
61
+ const files = [];
62
+ walk(dir, files);
63
+ const findings = [];
64
+ let score = 0;
65
+ for (const file of files) {
66
+ let text;
67
+ try {
68
+ text = readFileSync(file, 'utf8');
69
+ }
70
+ catch {
71
+ continue;
72
+ }
73
+ for (const m of scanText(text, tells)) {
74
+ findings.push({ file, line: m.line, tell: m.tell.name, hint: m.tell.hint, text: m.text });
75
+ score += m.tell.weight;
76
+ }
77
+ }
78
+ return { findings, score };
79
+ }
@@ -0,0 +1,141 @@
1
+ import { mkdirSync, rmSync, renameSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { loadConfig } from '../retrofit/config.js';
5
+ const CONFIG_GUIDANCE = [
6
+ 'No smoke flows configured. Add a smoke section to .yoke/config.yaml, e.g.:',
7
+ '',
8
+ 'smoke:',
9
+ ' baseUrl: http://localhost:3000',
10
+ ' flows:',
11
+ ' - name: home',
12
+ ' path: /',
13
+ ' landmark: "main h1"',
14
+ ].join('\n');
15
+ export async function launchPlaywright(targetDir) {
16
+ try {
17
+ // createRequire needs an absolute anchor — a relative targetDir (the CLI
18
+ // default '.') would throw and masquerade as "playwright not found".
19
+ // Playwright is CJS, so load it with native require() rather than a
20
+ // file:// dynamic import — the URL round-trip breaks under Windows 8.3
21
+ // short paths (e.g. RUNNER~1 on CI) and test-runner import interception.
22
+ const req = createRequire(join(resolve(targetDir), 'package.json'));
23
+ const pw = req('playwright');
24
+ const chromium = pw.chromium ?? pw.default?.chromium;
25
+ if (!chromium)
26
+ return null;
27
+ return await chromium.launch({ headless: true });
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ // Flow names come from user config and become filenames — keep them safe.
34
+ function safeName(name) {
35
+ return name.replace(/[^\w.-]+/g, '-');
36
+ }
37
+ // The label names a directory that gets rmSync'd recursively — it must never
38
+ // carry path semantics ('..', separators). Dots are stripped entirely so a
39
+ // bare '..' cannot survive; an emptied label falls back to 'latest'.
40
+ export function safeLabel(label) {
41
+ const cleaned = label.replace(/[^\w-]+/g, '-').replace(/^-+|-+$/g, '');
42
+ return cleaned || 'latest';
43
+ }
44
+ export async function runFlowSmoke(targetDir, opts = {}) {
45
+ const config = loadConfig(targetDir);
46
+ const smoke = config?.smoke;
47
+ if (!smoke) {
48
+ console.error(CONFIG_GUIDANCE);
49
+ return 2;
50
+ }
51
+ const baseUrl = opts.url ?? smoke.baseUrl;
52
+ const label = safeLabel(opts.label ?? process.env.YOKE_STORY ?? 'latest');
53
+ const proofRel = join('.yoke', 'proof', label);
54
+ const proofDir = join(targetDir, proofRel);
55
+ const launch = opts.launch ?? launchPlaywright;
56
+ const browser = await launch(targetDir);
57
+ if (!browser) {
58
+ console.error(`Playwright not found in ${targetDir}. Install it: npm i -D playwright && npx playwright install chromium`);
59
+ return 2;
60
+ }
61
+ // Wipe only once the run is actually going to happen — an exit-2 run must
62
+ // not destroy the previous run's evidence.
63
+ rmSync(proofDir, { recursive: true, force: true }); // fresh evidence per run
64
+ mkdirSync(proofDir, { recursive: true });
65
+ const videoTmp = join(proofDir, '.video-tmp');
66
+ let green = 0;
67
+ try {
68
+ for (const flow of smoke.flows) {
69
+ const context = await browser.newContext({ recordVideo: { dir: videoTmp } });
70
+ const page = await context.newPage();
71
+ const errors = [];
72
+ page.on('console', (msg) => {
73
+ const m = msg;
74
+ if (m.type?.() === 'error')
75
+ errors.push(String(m.text?.() ?? msg));
76
+ });
77
+ page.on('pageerror', (err) => {
78
+ errors.push(String(err?.message ?? err));
79
+ });
80
+ let reason;
81
+ try {
82
+ const resp = await page.goto(baseUrl + flow.path, { waitUntil: 'load', timeout: 30_000 });
83
+ if (resp && !resp.ok())
84
+ reason = `HTTP ${resp.status()}`;
85
+ if (!reason && flow.landmark) {
86
+ try {
87
+ await page.waitForSelector(flow.landmark, { timeout: 10_000 });
88
+ }
89
+ catch {
90
+ reason = `landmark "${flow.landmark}" not found`;
91
+ }
92
+ }
93
+ if (!reason && errors.length > 0) {
94
+ reason = `${errors.length} console error(s): ${errors[0].slice(0, 200)}`;
95
+ }
96
+ }
97
+ catch (e) {
98
+ reason = e.message.split('\n')[0];
99
+ }
100
+ // The screenshot IS the evidence — taken on success AND failure; a crashed
101
+ // page must not mask the original failure.
102
+ const shotName = `${safeName(flow.name)}.png`;
103
+ let shotOk = false;
104
+ try {
105
+ await page.screenshot({ path: join(proofDir, shotName), fullPage: true });
106
+ shotOk = true;
107
+ }
108
+ catch { /* keep the original reason */ }
109
+ const video = page.video();
110
+ await context.close();
111
+ let videoKept = false;
112
+ if (video) {
113
+ try {
114
+ const vpath = await video.path();
115
+ if (reason) {
116
+ renameSync(vpath, join(proofDir, `${safeName(flow.name)}.webm`));
117
+ videoKept = true;
118
+ }
119
+ else {
120
+ rmSync(vpath, { force: true });
121
+ }
122
+ }
123
+ catch { /* video is best-effort evidence */ }
124
+ }
125
+ if (reason) {
126
+ const saved = [shotOk ? 'screenshot' : null, videoKept ? 'video' : null].filter(Boolean).join(' + ');
127
+ console.log(`✘ ${flow.name} — ${reason}${saved ? ` (${saved} saved under ${proofRel})` : ''}`);
128
+ }
129
+ else {
130
+ green++;
131
+ console.log(`✔ ${flow.name} (screenshot: ${join(proofRel, shotName)})`);
132
+ }
133
+ }
134
+ }
135
+ finally {
136
+ await browser.close();
137
+ rmSync(videoTmp, { recursive: true, force: true });
138
+ }
139
+ console.log(`Flow-smoke: ${green}/${smoke.flows.length} flows green — proof: ${proofRel}`);
140
+ return green === smoke.flows.length ? 0 : 1;
141
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@hecer/yoke",
3
+ "version": "0.2.0",
4
+ "description": "One harness, three agents, zero trust in \"done\" — cross-agent coding harness for Claude Code, Codex CLI, and Gemini CLI: one skill canon, mechanical safety gates, an autonomous loop with screenshot/video proofs.",
5
+ "type": "module",
6
+ "bin": {
7
+ "yoke": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "canon",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/HECer/yoke.git"
21
+ },
22
+ "homepage": "https://github.com/HECer/yoke#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/HECer/yoke/issues"
25
+ },
26
+ "keywords": [
27
+ "claude-code",
28
+ "codex",
29
+ "gemini-cli",
30
+ "agents",
31
+ "agentic-coding",
32
+ "harness",
33
+ "autonomous",
34
+ "ralph-loop",
35
+ "code-review",
36
+ "skills",
37
+ "agents-md",
38
+ "tdd"
39
+ ],
40
+ "license": "MIT",
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc",
46
+ "test": "vitest run",
47
+ "yoke": "tsx src/cli.ts",
48
+ "prepublishOnly": "npm run build && vitest run"
49
+ },
50
+ "dependencies": {
51
+ "yaml": "^2.5.0",
52
+ "zod": "^3.23.8"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^22.7.0",
56
+ "smol-toml": "^1.7.0",
57
+ "tsx": "^4.19.1",
58
+ "typescript": "^5.6.2",
59
+ "vitest": "^2.1.1"
60
+ }
61
+ }