@su-record/vibe 2.9.40 → 2.10.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 (126) hide show
  1. package/CLAUDE.md +18 -5
  2. package/README.md +31 -24
  3. package/agents/{teams/figma → figma}/figma-analyst.md +2 -2
  4. package/agents/research/{best-practices-agent.md → best-practices.md} +1 -1
  5. package/agents/research/{codebase-patterns-agent.md → codebase-patterns.md} +1 -1
  6. package/agents/research/{framework-docs-agent.md → framework-docs.md} +1 -1
  7. package/agents/research/{security-advisory-agent.md → security-advisory.md} +1 -1
  8. package/agents/teams/research-team.md +4 -4
  9. package/agents/teams/review-debate-team.md +2 -2
  10. package/agents/teams/security-team.md +4 -4
  11. package/dist/cli/postinstall/claude-agents.d.ts +3 -1
  12. package/dist/cli/postinstall/claude-agents.d.ts.map +1 -1
  13. package/dist/cli/postinstall/claude-agents.js +47 -9
  14. package/dist/cli/postinstall/claude-agents.js.map +1 -1
  15. package/dist/cli/postinstall/constants.d.ts +5 -0
  16. package/dist/cli/postinstall/constants.d.ts.map +1 -1
  17. package/dist/cli/postinstall/constants.js +165 -23
  18. package/dist/cli/postinstall/constants.js.map +1 -1
  19. package/dist/cli/postinstall/cursor-skills.js +2 -2
  20. package/dist/cli/postinstall/main.d.ts.map +1 -1
  21. package/dist/cli/postinstall/main.js +19 -10
  22. package/dist/cli/postinstall/main.js.map +1 -1
  23. package/dist/infra/lib/OrchestrateWorkflow.js +1 -1
  24. package/dist/infra/lib/OrchestrateWorkflow.js.map +1 -1
  25. package/dist/infra/lib/telemetry/SkillTelemetry.test.js +4 -4
  26. package/dist/infra/lib/telemetry/SkillTelemetry.test.js.map +1 -1
  27. package/dist/infra/orchestrator/parallelResearch.js +4 -4
  28. package/dist/infra/orchestrator/parallelResearch.js.map +1 -1
  29. package/hooks/scripts/clone-extract.js +712 -0
  30. package/hooks/scripts/clone-refine.js +510 -0
  31. package/hooks/scripts/clone-to-scss.js +275 -0
  32. package/hooks/scripts/clone-validate.js +280 -0
  33. package/hooks/scripts/step-counter.js +1 -1
  34. package/package.json +2 -1
  35. package/skills/agents-md/SKILL.md +2 -0
  36. package/skills/arch-guard/SKILL.md +2 -0
  37. package/skills/brand-assets/SKILL.md +1 -0
  38. package/skills/capability-loop/SKILL.md +2 -0
  39. package/skills/characterization-test/SKILL.md +2 -0
  40. package/skills/chub-usage/SKILL.md +1 -0
  41. package/skills/claude-md-guide/SKILL.md +2 -0
  42. package/skills/clone/SKILL.md +361 -0
  43. package/skills/commerce-patterns/SKILL.md +1 -0
  44. package/skills/commit-push-pr/SKILL.md +1 -0
  45. package/skills/context7-usage/SKILL.md +1 -0
  46. package/skills/{vibe-contract → contract}/SKILL.md +7 -8
  47. package/skills/create-prd/SKILL.md +1 -0
  48. package/skills/design-audit/SKILL.md +1 -0
  49. package/skills/design-critique/SKILL.md +1 -0
  50. package/skills/design-distill/SKILL.md +1 -0
  51. package/skills/design-normalize/SKILL.md +1 -0
  52. package/skills/design-polish/SKILL.md +1 -0
  53. package/skills/design-teach/SKILL.md +2 -0
  54. package/skills/devlog/SKILL.md +1 -0
  55. package/skills/{vibe-docs → docs}/SKILL.md +5 -5
  56. package/skills/e2e-commerce/SKILL.md +1 -0
  57. package/skills/event-comms/SKILL.md +1 -0
  58. package/skills/event-ops/SKILL.md +1 -0
  59. package/skills/event-planning/SKILL.md +1 -0
  60. package/skills/exec-plan/SKILL.md +2 -0
  61. package/skills/{vibe-figma → figma}/SKILL.md +4 -3
  62. package/skills/{vibe-figma-convert → figma-convert}/SKILL.md +4 -3
  63. package/skills/{vibe-figma-extract → figma-extract}/SKILL.md +4 -3
  64. package/skills/git-worktree/SKILL.md +1 -0
  65. package/skills/handoff/SKILL.md +2 -0
  66. package/skills/{vibe-interview → interview}/SKILL.md +16 -16
  67. package/skills/parallel-research/SKILL.md +2 -0
  68. package/skills/{vibe-plan → plan}/SKILL.md +9 -9
  69. package/skills/prioritization-frameworks/SKILL.md +1 -0
  70. package/skills/priority-todos/SKILL.md +2 -0
  71. package/skills/{vibe-regress → regress}/SKILL.md +5 -6
  72. package/skills/rob-pike/SKILL.md +2 -0
  73. package/skills/seo-checklist/SKILL.md +1 -0
  74. package/skills/{vibe-spec → spec}/SKILL.md +14 -14
  75. package/skills/{vibe-spec-review → spec-review}/SKILL.md +8 -9
  76. package/skills/systematic-debugging/SKILL.md +2 -0
  77. package/skills/techdebt/SKILL.md +2 -0
  78. package/skills/{vibe-test → test}/SKILL.md +12 -12
  79. package/skills/tool-fallback/SKILL.md +1 -0
  80. package/skills/typescript-advanced-types/SKILL.md +1 -0
  81. package/skills/ui-ux-pro-max/SKILL.md +1 -0
  82. package/skills/user-personas/SKILL.md +1 -0
  83. package/skills/vercel-react-best-practices/SKILL.md +1 -0
  84. package/skills/vibe/SKILL.md +266 -0
  85. package/{commands/vibe.analyze.md → skills/vibe.analyze/SKILL.md} +2 -0
  86. package/skills/vibe.clone/SKILL.md +117 -0
  87. package/{commands/vibe.contract.md → skills/vibe.contract/SKILL.md} +3 -1
  88. package/{commands/vibe.docs.md → skills/vibe.docs/SKILL.md} +3 -1
  89. package/{commands/vibe.event.md → skills/vibe.event/SKILL.md} +2 -0
  90. package/{commands/vibe.figma.md → skills/vibe.figma/SKILL.md} +25 -23
  91. package/{commands/vibe.harness.md → skills/vibe.harness/SKILL.md} +2 -0
  92. package/{commands/vibe.reason.md → skills/vibe.reason/SKILL.md} +2 -0
  93. package/{commands/vibe.regress.md → skills/vibe.regress/SKILL.md} +5 -3
  94. package/{commands/vibe.review.md → skills/vibe.review/SKILL.md} +2 -0
  95. package/{commands/vibe.run.md → skills/vibe.run/SKILL.md} +3 -1
  96. package/{commands/vibe.scaffold.md → skills/vibe.scaffold/SKILL.md} +2 -0
  97. package/{commands/vibe.spec.md → skills/vibe.spec/SKILL.md} +36 -34
  98. package/{commands/vibe.test.md → skills/vibe.test/SKILL.md} +4 -2
  99. package/{commands/vibe.trace.md → skills/vibe.trace/SKILL.md} +7 -0
  100. package/{commands/vibe.utils.md → skills/vibe.utils/SKILL.md} +2 -0
  101. package/{commands/vibe.verify.md → skills/vibe.verify/SKILL.md} +4 -2
  102. package/skills/video-production/SKILL.md +1 -0
  103. /package/agents/{teams/figma → figma}/figma-architect.md +0 -0
  104. /package/agents/{teams/figma → figma}/figma-auditor.md +0 -0
  105. /package/agents/{teams/figma → figma}/figma-builder.md +0 -0
  106. /package/skills/{vibe-docs → docs}/templates/architecture.md +0 -0
  107. /package/skills/{vibe-docs → docs}/templates/behavioral-principles.md +0 -0
  108. /package/skills/{vibe-docs → docs}/templates/readme.md +0 -0
  109. /package/skills/{vibe-docs → docs}/templates/release-notes.md +0 -0
  110. /package/skills/{vibe-figma → figma}/rubrics/extraction-checklist.md +0 -0
  111. /package/skills/{vibe-figma → figma}/templates/component-index.md +0 -0
  112. /package/skills/{vibe-figma → figma}/templates/component-spec.md +0 -0
  113. /package/skills/{vibe-figma → figma}/templates/figma-handoff.md +0 -0
  114. /package/skills/{vibe-figma → figma}/templates/remapped-tree.md +0 -0
  115. /package/skills/{vibe-figma-convert → figma-convert}/rubrics/conversion-rules.md +0 -0
  116. /package/skills/{vibe-figma-convert → figma-convert}/templates/component.md +0 -0
  117. /package/skills/{vibe-figma-extract → figma-extract}/rubrics/image-rules.md +0 -0
  118. /package/skills/{vibe-interview → interview}/checklists/api.md +0 -0
  119. /package/skills/{vibe-interview → interview}/checklists/feature.md +0 -0
  120. /package/skills/{vibe-interview → interview}/checklists/library.md +0 -0
  121. /package/skills/{vibe-interview → interview}/checklists/mobile.md +0 -0
  122. /package/skills/{vibe-interview → interview}/checklists/webapp.md +0 -0
  123. /package/skills/{vibe-interview → interview}/checklists/website.md +0 -0
  124. /package/skills/{vibe-regress → regress}/templates/bug.md +0 -0
  125. /package/skills/{vibe-regress → regress}/templates/test-jest.md +0 -0
  126. /package/skills/{vibe-regress → regress}/templates/test-vitest.md +0 -0
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * clone-to-scss.js — sections.json → SCSS partials + class plan
5
+ *
6
+ * Usage:
7
+ * node clone-to-scss.js <sections.json> --out=<styles-dir> [--feature=<name>] [--asset-root=<public-prefix>]
8
+ *
9
+ * Output:
10
+ * <styles-dir>/
11
+ * index.scss (master orchestrator)
12
+ * _tokens.scss (CSS variables from tokens)
13
+ * _base.scss (@font-face + body defaults from stylesheets.json hints)
14
+ * _shared.scss (placeholder for cross-section utilities)
15
+ * sections/_<name>.scss (per-section partial)
16
+ * <styles-dir>/class-plan.json (node.id → BEM class name; HTML scaffolder applies these)
17
+ *
18
+ * Rules:
19
+ * - CSS values copied verbatim from sections.json — no eyeballing
20
+ * - Token-referenced values stay as var(--xxx)
21
+ * - Selectors are class-based, BEM-flavored: .{feature}__{section}__{role}
22
+ * - One class per node; nested rules use SCSS &__ syntax
23
+ */
24
+
25
+ import fs from 'fs';
26
+ import path from 'path';
27
+
28
+ // ─── CLI ────────────────────────────────────────────────────────────
29
+ function parseArgs(argv) {
30
+ const [, , inputPath, ...rest] = argv;
31
+ const opts = {};
32
+ for (const a of rest) {
33
+ if (a.startsWith('--out=')) opts.out = a.slice(6);
34
+ else if (a.startsWith('--feature=')) opts.feature = a.slice(10);
35
+ else if (a.startsWith('--asset-root=')) opts.assetRoot = a.slice(13);
36
+ }
37
+ return { inputPath, opts };
38
+ }
39
+
40
+ const { inputPath, opts } = parseArgs(process.argv);
41
+ if (!inputPath || !opts.out) {
42
+ console.error('Usage: node clone-to-scss.js <sections.json> --out=<styles-dir> [--feature=<name>] [--asset-root=<public-prefix>]');
43
+ process.exit(1);
44
+ }
45
+
46
+ // ─── Helpers ────────────────────────────────────────────────────────
47
+ function kebab(s) {
48
+ return String(s)
49
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
50
+ .replace(/[^a-zA-Z0-9]+/g, '-')
51
+ .replace(/^-+|-+$/g, '')
52
+ .toLowerCase() || 'x';
53
+ }
54
+
55
+ function shorten(id) {
56
+ // node id like "0.1.2.3" → "n-0-1-2-3"
57
+ return 'n-' + id.replace(/[^0-9]/g, '-').replace(/^-+|-+$/g, '').slice(0, 20);
58
+ }
59
+
60
+ // Detect a semantic role for a node based on tag/class/text/attrs
61
+ function semanticRole(node) {
62
+ const cls = (node.classes || '').toLowerCase();
63
+ const tag = node.tag;
64
+ if (tag === 'h1' || /\bheadline\b|\bhero-title\b/.test(cls)) return 'title';
65
+ if (/^h[2-6]$/.test(tag)) return `heading-${tag.slice(1)}`;
66
+ if (tag === 'p' || /\bsubtitle\b|\bdescription\b/.test(cls)) return 'body';
67
+ if (tag === 'a' && (/\bbutton\b|\bbtn\b|\bcta\b/.test(cls) || (node.attrs && /button/.test(node.attrs.role || '')))) return 'button';
68
+ if (tag === 'button') return 'button';
69
+ if (tag === 'img' || (node.css && node.css['background-image'] && node.css['background-image'] !== 'none')) return 'media';
70
+ if (tag === 'ul' || tag === 'ol') return 'list';
71
+ if (tag === 'li') return 'item';
72
+ if (tag === 'nav' || /\bnav\b/.test(cls)) return 'nav';
73
+ if (tag === 'form') return 'form';
74
+ if (tag === 'input' || tag === 'textarea' || tag === 'select') return 'field';
75
+ if (tag === 'header') return 'header';
76
+ if (tag === 'footer') return 'footer';
77
+ if (tag === 'section' || tag === 'article') return 'content';
78
+ if (tag === 'div' && node.children && node.children.length > 1) return 'group';
79
+ return 'el';
80
+ }
81
+
82
+ // ─── Class plan: assign one BEM class per visible node ──────────────
83
+ function buildClassPlan(sections, feature) {
84
+ const plan = {}; // id → class name
85
+ const usedNames = new Map(); // base name → count for disambiguation
86
+ const sectionUsedRoles = new Map();
87
+
88
+ const assign = (sectionName, node, parentRole) => {
89
+ if (!node || node.pseudo) return;
90
+ const sectionKey = kebab(sectionName);
91
+ let role = semanticRole(node);
92
+ if (parentRole === 'list' && role !== 'item') role = 'item';
93
+ // disambiguate within section by role
94
+ const k = `${sectionKey}::${role}`;
95
+ const idx = (sectionUsedRoles.get(k) || 0) + 1;
96
+ sectionUsedRoles.set(k, idx);
97
+ const suffix = idx === 1 ? role : `${role}-${idx}`;
98
+ const cls = `${feature}__${sectionKey}__${suffix}`;
99
+ plan[node.id] = cls;
100
+ for (const child of (node.children || [])) assign(sectionName, child, role);
101
+ };
102
+
103
+ for (const sec of sections) {
104
+ // Root section class
105
+ const sectionKey = kebab(sec.name);
106
+ plan[sec.nodeRef] = `${feature}__${sectionKey}`;
107
+ for (const child of (sec.children || [])) assign(sec.name, child, 'section');
108
+ }
109
+
110
+ return plan;
111
+ }
112
+
113
+ // ─── CSS emission ───────────────────────────────────────────────────
114
+ const SKIP_DEFAULT_VALUES = {
115
+ 'pointer-events': 'auto',
116
+ 'visibility': 'visible',
117
+ 'opacity': '1',
118
+ 'transform': 'none',
119
+ 'background-repeat': 'repeat',
120
+ 'background-attachment': 'scroll',
121
+ 'background-blend-mode': 'normal',
122
+ 'mix-blend-mode': 'normal',
123
+ 'filter': 'none',
124
+ 'backdrop-filter': 'none',
125
+ 'overflow': 'visible',
126
+ 'overflow-x': 'visible',
127
+ 'overflow-y': 'visible',
128
+ };
129
+
130
+ function emitDeclarations(css, indent) {
131
+ const out = [];
132
+ for (const [prop, val] of Object.entries(css)) {
133
+ if (!val || prop.startsWith('--')) continue;
134
+ if (SKIP_DEFAULT_VALUES[prop] === val) continue;
135
+ out.push(`${indent}${prop}: ${val};`);
136
+ }
137
+ return out.join('\n');
138
+ }
139
+
140
+ function emitNodeRule(node, plan, indent) {
141
+ const cls = plan[node.id];
142
+ if (!cls) return '';
143
+ const lines = [];
144
+ lines.push(`${indent}.${cls} {`);
145
+ const decl = emitDeclarations(node.css || {}, indent + ' ');
146
+ if (decl) lines.push(decl);
147
+ // pseudo-elements
148
+ for (const child of (node.children || [])) {
149
+ if (child.pseudo) {
150
+ const kind = child.tag.replace('::', '');
151
+ lines.push('');
152
+ lines.push(`${indent} &::${kind} {`);
153
+ lines.push(emitDeclarations(child.css || {}, indent + ' '));
154
+ lines.push(`${indent} }`);
155
+ }
156
+ }
157
+ lines.push(`${indent}}`);
158
+
159
+ // Non-pseudo children — emit at same level (BEM flat)
160
+ const childRules = (node.children || [])
161
+ .filter((c) => !c.pseudo)
162
+ .map((c) => emitNodeRule(c, plan, indent))
163
+ .filter(Boolean);
164
+
165
+ return [lines.join('\n'), ...childRules].join('\n\n');
166
+ }
167
+
168
+ // ─── File emission ──────────────────────────────────────────────────
169
+ function emitTokens(tokens) {
170
+ const lines = [':root {'];
171
+ for (const c of tokens.colors) lines.push(` --${c.name}: ${c.value};`);
172
+ for (const t of tokens.typography) {
173
+ lines.push(` --${t.name}-family: ${t.family};`);
174
+ lines.push(` --${t.name}-size: ${t.size};`);
175
+ lines.push(` --${t.name}-weight: ${t.weight};`);
176
+ if (t.lineHeight && t.lineHeight !== 'normal') lines.push(` --${t.name}-line-height: ${t.lineHeight};`);
177
+ if (t.letterSpacing && t.letterSpacing !== 'normal') lines.push(` --${t.name}-letter-spacing: ${t.letterSpacing};`);
178
+ }
179
+ for (const s of tokens.spacing) lines.push(` --${s.name}: ${s.value};`);
180
+ for (const r of tokens.radius) lines.push(` --${r.name}: ${r.value};`);
181
+ for (const s of tokens.shadow) lines.push(` --${s.name}: ${s.value};`);
182
+ lines.push('}');
183
+ return lines.join('\n') + '\n';
184
+ }
185
+
186
+ function emitBase(feature, stylesheets) {
187
+ const lines = [];
188
+ lines.push('// Auto-generated by clone-to-scss.js — do not edit by hand.');
189
+ lines.push('');
190
+ if (stylesheets && stylesheets.fontFaces) {
191
+ for (const ff of stylesheets.fontFaces) {
192
+ lines.push('@font-face {');
193
+ lines.push(` font-family: '${ff.family}';`);
194
+ if (ff.weight) lines.push(` font-weight: ${ff.weight};`);
195
+ if (ff.style) lines.push(` font-style: ${ff.style};`);
196
+ if (ff.display) lines.push(` font-display: ${ff.display};`);
197
+ const srcs = ff.sources.map((s) => s.format
198
+ ? `url('${s.url}') format('${s.format}')`
199
+ : `url('${s.url}')`).join(',\n ');
200
+ lines.push(` src: ${srcs};`);
201
+ lines.push('}');
202
+ lines.push('');
203
+ }
204
+ }
205
+ return lines.join('\n');
206
+ }
207
+
208
+ function emitSection(section, plan) {
209
+ const lines = [];
210
+ lines.push(`// Section: ${section.name}`);
211
+ lines.push('');
212
+ // root + descendants
213
+ lines.push(emitNodeRule({ ...section, id: section.nodeRef, children: section.children }, plan, ''));
214
+ return lines.join('\n') + '\n';
215
+ }
216
+
217
+ function emitIndex(feature, sections) {
218
+ const lines = [];
219
+ lines.push(`// ${feature} — auto-generated by clone-to-scss.js`);
220
+ lines.push('');
221
+ lines.push(`@use './tokens';`);
222
+ lines.push(`@use './base';`);
223
+ lines.push(`@use './shared';`);
224
+ for (const sec of sections) {
225
+ lines.push(`@use './sections/${kebab(sec.name)}';`);
226
+ }
227
+ return lines.join('\n') + '\n';
228
+ }
229
+
230
+ // ─── Main ───────────────────────────────────────────────────────────
231
+ function main() {
232
+ const data = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
233
+ const feature = opts.feature || data.meta?.feature || 'clone';
234
+ const featureClass = kebab(feature);
235
+
236
+ // Read sibling stylesheets.json if present (from clone-extract.js output)
237
+ const sectionsDir = path.dirname(inputPath);
238
+ const stylesheetsPath = path.join(sectionsDir, 'stylesheets.json');
239
+ let stylesheets = null;
240
+ if (fs.existsSync(stylesheetsPath)) {
241
+ stylesheets = JSON.parse(fs.readFileSync(stylesheetsPath, 'utf8'));
242
+ }
243
+
244
+ const outDir = opts.out;
245
+ fs.mkdirSync(outDir, { recursive: true });
246
+ fs.mkdirSync(path.join(outDir, 'sections'), { recursive: true });
247
+
248
+ const plan = buildClassPlan(data.sections, featureClass);
249
+
250
+ // Write files
251
+ fs.writeFileSync(path.join(outDir, '_tokens.scss'), emitTokens(data.tokens));
252
+ fs.writeFileSync(path.join(outDir, '_base.scss'), emitBase(featureClass, stylesheets));
253
+ fs.writeFileSync(
254
+ path.join(outDir, '_shared.scss'),
255
+ `// Cross-section utilities — extend as needed.\n`,
256
+ );
257
+
258
+ for (const sec of data.sections) {
259
+ const fname = `_${kebab(sec.name)}.scss`;
260
+ fs.writeFileSync(path.join(outDir, 'sections', fname), emitSection(sec, plan));
261
+ }
262
+
263
+ fs.writeFileSync(path.join(outDir, 'index.scss'), emitIndex(featureClass, data.sections));
264
+ fs.writeFileSync(path.join(outDir, 'class-plan.json'), JSON.stringify(plan, null, 2));
265
+
266
+ console.log(`[clone-to-scss] done → ${outDir}`);
267
+ console.log(` sections: ${data.sections.length}, classes: ${Object.keys(plan).length}`);
268
+ }
269
+
270
+ try { main(); }
271
+ catch (e) {
272
+ console.error(`[clone-to-scss] FAIL: ${e.message}`);
273
+ if (process.env.DEBUG) console.error(e.stack);
274
+ process.exit(1);
275
+ }
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * clone-validate.js — written SCSS vs sections.json (source of truth)
5
+ *
6
+ * Usage:
7
+ * node clone-validate.js <styles-dir> <sections.json> [--section=<name>]
8
+ *
9
+ * Reads:
10
+ * <styles-dir>/sections/_<section>.scss
11
+ * <styles-dir>/class-plan.json
12
+ * <sections.json>
13
+ *
14
+ * Compares emitted CSS declarations against expectations:
15
+ * - Missing property → P1
16
+ * - Mismatched value → P1 (if box prop and delta > 4px), else P2
17
+ * - Extra property → P3 (informational)
18
+ *
19
+ * Exit codes:
20
+ * 0 PASS, 1 FAIL (P1 found), 2 usage error
21
+ */
22
+
23
+ import fs from 'fs';
24
+ import path from 'path';
25
+
26
+ function parseArgs(argv) {
27
+ const [, , stylesDir, sectionsPath, ...rest] = argv;
28
+ const opts = {};
29
+ for (const a of rest) {
30
+ if (a.startsWith('--section=')) opts.section = a.slice(10);
31
+ else if (a === '--quiet') opts.quiet = true;
32
+ }
33
+ return { stylesDir, sectionsPath, opts };
34
+ }
35
+
36
+ const { stylesDir, sectionsPath, opts } = parseArgs(process.argv);
37
+ if (!stylesDir || !sectionsPath) {
38
+ console.error('Usage: node clone-validate.js <styles-dir> <sections.json> [--section=<name>]');
39
+ process.exit(2);
40
+ }
41
+
42
+ // ─── Minimal SCSS parser ────────────────────────────────────────────
43
+ // Strips comments, expands `&::pseudo` (one level), flattens nested rules.
44
+ // NOT a full SCSS engine — assumes clone-to-scss.js output style.
45
+
46
+ function stripComments(src) {
47
+ return src
48
+ .replace(/\/\*[\s\S]*?\*\//g, '')
49
+ .replace(/\/\/[^\n]*/g, '');
50
+ }
51
+
52
+ function parseScss(src) {
53
+ const clean = stripComments(src);
54
+ const rules = new Map(); // selector → { prop: value }
55
+ let i = 0;
56
+ const len = clean.length;
57
+
58
+ const skipWs = () => { while (i < len && /\s/.test(clean[i])) i++; };
59
+
60
+ const readUntil = (chars) => {
61
+ let buf = '';
62
+ while (i < len && !chars.includes(clean[i])) buf += clean[i++];
63
+ return buf;
64
+ };
65
+
66
+ const stack = []; // selector stack
67
+
68
+ while (i < len) {
69
+ skipWs();
70
+ if (i >= len) break;
71
+ const ch = clean[i];
72
+ if (ch === '}') { stack.pop(); i++; continue; }
73
+
74
+ // Lookahead: is this a rule (has '{' before ';' or end) or declaration?
75
+ let depth = 0;
76
+ let j = i;
77
+ let kind = 'decl';
78
+ while (j < len) {
79
+ const c = clean[j];
80
+ if (c === '"' || c === "'") {
81
+ const q = c; j++;
82
+ while (j < len && clean[j] !== q) { if (clean[j] === '\\') j++; j++; }
83
+ }
84
+ if (c === '(') depth++;
85
+ else if (c === ')') depth--;
86
+ else if (depth === 0 && c === ';') { kind = 'decl'; break; }
87
+ else if (depth === 0 && c === '{') { kind = 'rule'; break; }
88
+ else if (depth === 0 && c === '}') { break; }
89
+ j++;
90
+ }
91
+
92
+ if (kind === 'rule') {
93
+ const selector = clean.slice(i, j).trim();
94
+ i = j + 1; // consume '{'
95
+ // Resolve nested selector
96
+ const parent = stack[stack.length - 1] || '';
97
+ let resolved;
98
+ if (selector.startsWith('&')) {
99
+ resolved = parent + selector.slice(1);
100
+ } else if (parent) {
101
+ // descendant
102
+ resolved = selector.split(',').map((s) => parent + ' ' + s.trim()).join(', ');
103
+ } else {
104
+ resolved = selector;
105
+ }
106
+ stack.push(resolved);
107
+ if (!rules.has(resolved)) rules.set(resolved, {});
108
+ } else {
109
+ // declaration
110
+ const text = clean.slice(i, j).trim();
111
+ i = j + 1;
112
+ if (!text) continue;
113
+ const colon = text.indexOf(':');
114
+ if (colon < 0) continue;
115
+ const prop = text.slice(0, colon).trim();
116
+ const val = text.slice(colon + 1).trim();
117
+ const sel = stack[stack.length - 1];
118
+ if (sel) {
119
+ if (!rules.has(sel)) rules.set(sel, {});
120
+ rules.get(sel)[prop] = val;
121
+ }
122
+ }
123
+ }
124
+
125
+ return rules;
126
+ }
127
+
128
+ // ─── Comparison ─────────────────────────────────────────────────────
129
+ const BOX_PROPS = new Set([
130
+ 'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
131
+ 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
132
+ 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
133
+ 'top', 'right', 'bottom', 'left', 'gap', 'row-gap', 'column-gap',
134
+ 'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
135
+ ]);
136
+
137
+ function pxOf(v) {
138
+ if (typeof v !== 'string') return null;
139
+ const m = /^(-?\d+(?:\.\d+)?)px$/.exec(v.trim());
140
+ return m ? Number(m[1]) : null;
141
+ }
142
+
143
+ function normalize(v) {
144
+ if (typeof v !== 'string') return v;
145
+ return v.trim().toLowerCase().replace(/\s+/g, ' ');
146
+ }
147
+
148
+ function compareValue(prop, expected, actual) {
149
+ if (normalize(expected) === normalize(actual)) return { ok: true };
150
+ if (BOX_PROPS.has(prop)) {
151
+ const ep = pxOf(expected), ap = pxOf(actual);
152
+ if (ep !== null && ap !== null) {
153
+ const delta = Math.abs(ep - ap);
154
+ return { ok: delta <= 4, severity: delta > 4 ? 'P1' : 'P2', delta };
155
+ }
156
+ }
157
+ return { ok: false, severity: 'P1' };
158
+ }
159
+
160
+ function walkNodes(section) {
161
+ const flat = [];
162
+ const recur = (n) => {
163
+ flat.push(n);
164
+ for (const c of (n.children || [])) recur(c);
165
+ };
166
+ flat.push({ id: section.nodeRef, css: section.css, tag: section.tag, classes: section.classes });
167
+ for (const c of (section.children || [])) recur(c);
168
+ return flat;
169
+ }
170
+
171
+ // ─── Main ───────────────────────────────────────────────────────────
172
+ function main() {
173
+ const sectionsJson = JSON.parse(fs.readFileSync(sectionsPath, 'utf8'));
174
+ const classPlanPath = path.join(stylesDir, 'class-plan.json');
175
+ if (!fs.existsSync(classPlanPath)) {
176
+ console.error(`class-plan.json not found at ${classPlanPath}. Run clone-to-scss.js first.`);
177
+ process.exit(2);
178
+ }
179
+ const classPlan = JSON.parse(fs.readFileSync(classPlanPath, 'utf8'));
180
+
181
+ const targets = opts.section
182
+ ? sectionsJson.sections.filter((s) => s.name === opts.section)
183
+ : sectionsJson.sections;
184
+
185
+ if (opts.section && targets.length === 0) {
186
+ console.error(`Section not found: ${opts.section}`);
187
+ process.exit(2);
188
+ }
189
+
190
+ const issues = [];
191
+ let totalChecked = 0;
192
+
193
+ for (const section of targets) {
194
+ const kebabName = section.name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
195
+ const scssPath = path.join(stylesDir, 'sections', `_${kebabName}.scss`);
196
+ if (!fs.existsSync(scssPath)) {
197
+ issues.push({ section: section.name, severity: 'P1', kind: 'missing-file', path: scssPath });
198
+ continue;
199
+ }
200
+ const scss = fs.readFileSync(scssPath, 'utf8');
201
+ const rules = parseScss(scss);
202
+
203
+ const flat = walkNodes(section);
204
+ for (const node of flat) {
205
+ const cls = classPlan[node.id];
206
+ if (!cls || !node.css) continue;
207
+ const selector = `.${cls}`;
208
+ const declared = rules.get(selector) || {};
209
+
210
+ for (const [prop, expected] of Object.entries(node.css)) {
211
+ if (prop.startsWith('--')) continue;
212
+ if (!expected || expected === 'normal' || expected === 'auto' || expected === 'none') continue;
213
+ totalChecked++;
214
+ const actual = declared[prop];
215
+ if (actual === undefined) {
216
+ issues.push({
217
+ section: section.name,
218
+ nodeId: node.id,
219
+ class: cls,
220
+ prop,
221
+ severity: 'P1',
222
+ kind: 'missing-prop',
223
+ expected,
224
+ });
225
+ continue;
226
+ }
227
+ const cmp = compareValue(prop, expected, actual);
228
+ if (!cmp.ok) {
229
+ issues.push({
230
+ section: section.name,
231
+ nodeId: node.id,
232
+ class: cls,
233
+ prop,
234
+ severity: cmp.severity,
235
+ kind: 'value-mismatch',
236
+ expected,
237
+ actual,
238
+ delta: cmp.delta,
239
+ });
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ const p1 = issues.filter((i) => i.severity === 'P1');
246
+ const p2 = issues.filter((i) => i.severity === 'P2');
247
+ const p3 = issues.filter((i) => i.severity === 'P3');
248
+
249
+ if (!opts.quiet) {
250
+ console.log(`[clone-validate] checked ${totalChecked} declarations across ${targets.length} section(s)`);
251
+ console.log(` P1: ${p1.length}, P2: ${p2.length}, P3: ${p3.length}`);
252
+ const show = (label, list, limit = 20) => {
253
+ if (list.length === 0) return;
254
+ console.log(`\n${label}:`);
255
+ for (const it of list.slice(0, limit)) {
256
+ const loc = it.class ? `.${it.class}` : it.path || '?';
257
+ if (it.kind === 'missing-file') console.log(` ${it.severity} ${it.section}: missing ${it.path}`);
258
+ else if (it.kind === 'missing-prop') console.log(` ${it.severity} ${loc} { ${it.prop}: MISSING (expected ${it.expected}) }`);
259
+ else console.log(` ${it.severity} ${loc} { ${it.prop}: ${it.actual} ≠ ${it.expected}${it.delta != null ? ` (Δ${it.delta}px)` : ''} }`);
260
+ }
261
+ if (list.length > limit) console.log(` …and ${list.length - limit} more`);
262
+ };
263
+ show('P1 issues', p1);
264
+ show('P2 issues', p2);
265
+ }
266
+
267
+ if (p1.length > 0) {
268
+ if (!opts.quiet) console.error(`\n[clone-validate] FAIL: ${p1.length} P1 issue(s)`);
269
+ process.exit(1);
270
+ }
271
+ if (!opts.quiet) console.log('\n[clone-validate] PASS');
272
+ process.exit(0);
273
+ }
274
+
275
+ try { main(); }
276
+ catch (e) {
277
+ console.error(`[clone-validate] FAIL: ${e.message}`);
278
+ if (process.env.DEBUG) console.error(e.stack);
279
+ process.exit(2);
280
+ }
@@ -68,7 +68,7 @@ function isResponseError(toolResponse) {
68
68
  }
69
69
 
70
70
  // ─────────────────────────────────────────────────────
71
- // 책임 3a: error_category 분류 (regex 만 — vibe-regress tag enum 일부)
71
+ // 책임 3a: error_category 분류 (regex 만 — regress tag enum 일부)
72
72
  // ─────────────────────────────────────────────────────
73
73
  const ERROR_CATEGORIES = [
74
74
  { tag: 'nullability', re: /Cannot read propert(?:y|ies) of (?:undefined|null)|TypeError[^\n]*(?:undefined|null)/i },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@su-record/vibe",
3
- "version": "2.9.40",
3
+ "version": "2.10.0",
4
4
  "description": "AI Coding Framework for Claude Code — 56 agents, 45 skills, multi-LLM orchestration",
5
5
  "type": "module",
6
6
  "main": "dist/cli/index.js",
@@ -27,6 +27,7 @@
27
27
  "dev": "tsc --watch",
28
28
  "gen:skill-docs": "npx tsx scripts/gen-skill-docs.ts",
29
29
  "gen:skill-docs:check": "npx tsx scripts/gen-skill-docs.ts --check",
30
+ "validate:skill-invocation": "npx tsx scripts/validate-skill-invocation.ts",
30
31
  "test": "vitest run",
31
32
  "test:watch": "vitest",
32
33
  "prepublishOnly": "pnpm build",
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: agents-md
3
+ user-invocable: false
4
+ invocation: [auto, chain]
3
5
  tier: standard
4
6
  description: "Optimize AGENTS.md / CLAUDE.md by removing discoverable info and keeping only gotchas. Based on Addy Osmani's AGENTS.md principles. Activates on agents.md, claude.md, context file optimization."
5
7
  triggers: [agents.md, claude.md, context file, optimize agents, optimize claude]
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: arch-guard
3
+ user-invocable: false
4
+ invocation: [auto]
3
5
  tier: core
4
6
  description: "Generate architecture boundary tests that mechanically enforce layer constraints. Use when adding new modules, refactoring layers, or after detecting circular dependencies. Creates import-rule tests (e.g., 'UI must not import DB') that fail CI on violation. Must use this skill when user mentions layer enforcement, dependency rules, or architectural boundaries — even casually like 'make sure services don't import controllers'."
5
7
  triggers: [arch guard, architecture test, layer test, boundary test, structural test, arch validation]
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: brand-assets
3
+ invocation: [auto]
3
4
  tier: standard
4
5
  description: "Auto-generate app icons (iOS/Android/PWA), favicons, and OG images from SPEC brand information using Gemini Image API. Use when the project needs visual brand assets, when user mentions 'icon', 'favicon', 'logo', or 'brand assets', or when a SPEC defines brand colors/identity but no assets exist yet. Outputs multiple sizes and formats ready for deployment. Not for complex illustration or marketing graphics — focused on app identity assets."
5
6
  triggers: [icon, favicon, brand, logo, app icon, branding, assets]
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: capability-loop
3
+ user-invocable: false
4
+ invocation: [auto]
3
5
  tier: standard
4
6
  description: "When an agent fails, diagnose which capability is missing and build it into the repo. Activates after repeated agent failures, tool errors, or when a task keeps failing in the same way. Analyzes failure transcripts, identifies the missing guardrail/tool/abstraction/doc, and creates it permanently. Use this skill whenever you see 3+ similar failures, an agent hitting the same wall repeatedly, or the user asking 'why does this keep failing'."
5
7
  triggers: [capability loop, failure loop, build capability, missing capability, agent failed, why did it fail]
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: characterization-test
3
+ user-invocable: false
4
+ invocation: [auto]
3
5
  tier: core
4
6
  description: "Lock existing behavior with characterization tests before modifying code. Use BEFORE any refactor, rewrite, or large-scale modification of existing code — especially legacy code without tests. Captures current input/output behavior as test cases so regressions are caught immediately. Must use this skill when touching files >200 lines with no existing tests, when user says 'refactor', 'rewrite', 'modernize', or 'clean up' existing code."
5
7
  triggers: [legacy, characterization test, lock behavior, regression prevention, before refactor, large file]
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: chub-usage
3
+ invocation: [auto]
3
4
  tier: optional
4
5
  description: "Context Hub (chub) — fetch vetted, up-to-date API documentation. Write accurate code based on the latest docs instead of training data when working with external APIs/SDKs."
5
6
  triggers: [chub, context hub, API docs, latest API, deprecated API, SDK documentation, api reference, 최신 문서]
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: claude-md-guide
3
+ user-invocable: false
4
+ invocation: [auto]
3
5
  tier: standard
4
6
  description: "Guide for writing effective CLAUDE.md files from scratch. Evidence-based methodology from 40+ sources including research papers, official docs, and real-world examples. Covers 3-layer architecture, Curse of Instructions mitigation, progressive disclosure, and maintenance. Use when creating new CLAUDE.md, improving existing ones, or teaching team members how to write project instructions for AI agents."
5
7
  triggers: [claude-md guide, write claude.md, create claude.md, claude.md 작성, 클로드 문서, project instructions, claude-md]