@su-record/vibe 2.16.2 → 2.16.4

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 (156) hide show
  1. package/.env.example +15 -0
  2. package/README.en.md +1 -1
  3. package/README.md +1 -1
  4. package/agents/architect-low.md +1 -1
  5. package/agents/architect-medium.md +1 -1
  6. package/agents/architect.md +1 -1
  7. package/agents/build-error-resolver.md +1 -1
  8. package/agents/compounder.md +4 -0
  9. package/agents/diagrammer.md +4 -0
  10. package/agents/docs/api-documenter.md +1 -1
  11. package/agents/docs/changelog-writer.md +1 -1
  12. package/agents/e2e-tester.md +4 -0
  13. package/agents/explorer-low.md +1 -1
  14. package/agents/explorer-medium.md +1 -1
  15. package/agents/explorer.md +2 -2
  16. package/agents/figma/figma-analyst.md +4 -0
  17. package/agents/figma/figma-architect.md +4 -0
  18. package/agents/figma/figma-auditor.md +4 -0
  19. package/agents/figma/figma-builder.md +4 -0
  20. package/agents/implementer-low.md +1 -1
  21. package/agents/implementer-medium.md +1 -1
  22. package/agents/implementer.md +2 -2
  23. package/agents/planning/requirements-analyst.md +1 -1
  24. package/agents/planning/ux-advisor.md +1 -1
  25. package/agents/qa/acceptance-tester.md +1 -1
  26. package/agents/qa/edge-case-finder.md +1 -1
  27. package/agents/refactor-cleaner.md +1 -1
  28. package/agents/research/best-practices.md +1 -1
  29. package/agents/research/codebase-patterns.md +1 -1
  30. package/agents/research/framework-docs.md +1 -1
  31. package/agents/research/security-advisory.md +1 -1
  32. package/agents/searcher.md +1 -1
  33. package/agents/simplifier.md +4 -0
  34. package/agents/teams/debug-team.md +4 -0
  35. package/agents/teams/dev-team.md +4 -0
  36. package/agents/teams/docs-team.md +4 -0
  37. package/agents/teams/figma-team.md +4 -0
  38. package/agents/teams/fullstack-team.md +4 -0
  39. package/agents/teams/lite-team.md +4 -0
  40. package/agents/teams/migration-team.md +4 -0
  41. package/agents/teams/refactor-team.md +4 -0
  42. package/agents/teams/research-team.md +4 -0
  43. package/agents/teams/review-debate-team.md +4 -0
  44. package/agents/teams/security-team.md +4 -0
  45. package/agents/tester.md +2 -2
  46. package/agents/ui/ui-a11y-auditor.md +1 -1
  47. package/agents/ui/ui-antipattern-detector.md +1 -1
  48. package/agents/ui/ui-dataviz-advisor.md +1 -1
  49. package/agents/ui/ui-design-system-gen.md +1 -1
  50. package/agents/ui/ui-industry-analyzer.md +1 -1
  51. package/agents/ui/ui-layout-architect.md +1 -1
  52. package/agents/ui/ui-stack-implementer.md +1 -1
  53. package/agents/ui/ux-compliance-reviewer.md +1 -1
  54. package/agents/ui-previewer.md +4 -0
  55. package/dist/__tests__/agent-model-sync.test.d.ts +2 -0
  56. package/dist/__tests__/agent-model-sync.test.d.ts.map +1 -0
  57. package/dist/__tests__/agent-model-sync.test.js +71 -0
  58. package/dist/__tests__/agent-model-sync.test.js.map +1 -0
  59. package/dist/cli/auth.d.ts.map +1 -1
  60. package/dist/cli/auth.js +8 -1
  61. package/dist/cli/auth.js.map +1 -1
  62. package/dist/cli/commands/index.d.ts +1 -0
  63. package/dist/cli/commands/index.d.ts.map +1 -1
  64. package/dist/cli/commands/index.js +1 -0
  65. package/dist/cli/commands/index.js.map +1 -1
  66. package/dist/cli/commands/info.d.ts.map +1 -1
  67. package/dist/cli/commands/info.js +2 -0
  68. package/dist/cli/commands/info.js.map +1 -1
  69. package/dist/cli/commands/llm.d.ts +11 -0
  70. package/dist/cli/commands/llm.d.ts.map +1 -0
  71. package/dist/cli/commands/llm.js +62 -0
  72. package/dist/cli/commands/llm.js.map +1 -0
  73. package/dist/cli/commands/setup.d.ts.map +1 -1
  74. package/dist/cli/commands/setup.js +35 -1
  75. package/dist/cli/commands/setup.js.map +1 -1
  76. package/dist/cli/index.d.ts.map +1 -1
  77. package/dist/cli/index.js +59 -2
  78. package/dist/cli/index.js.map +1 -1
  79. package/dist/cli/llm/config.d.ts.map +1 -1
  80. package/dist/cli/llm/config.js +7 -0
  81. package/dist/cli/llm/config.js.map +1 -1
  82. package/dist/cli/llm/index.d.ts +1 -0
  83. package/dist/cli/llm/index.d.ts.map +1 -1
  84. package/dist/cli/llm/index.js +1 -0
  85. package/dist/cli/llm/index.js.map +1 -1
  86. package/dist/cli/llm/model-refresh.d.ts +29 -0
  87. package/dist/cli/llm/model-refresh.d.ts.map +1 -0
  88. package/dist/cli/llm/model-refresh.js +139 -0
  89. package/dist/cli/llm/model-refresh.js.map +1 -0
  90. package/dist/cli/llm/zai-commands.d.ts +15 -0
  91. package/dist/cli/llm/zai-commands.d.ts.map +1 -0
  92. package/dist/cli/llm/zai-commands.js +64 -0
  93. package/dist/cli/llm/zai-commands.js.map +1 -0
  94. package/dist/cli/postinstall/constants.d.ts.map +1 -1
  95. package/dist/cli/postinstall/constants.js +1 -0
  96. package/dist/cli/postinstall/constants.js.map +1 -1
  97. package/dist/cli/types.d.ts +15 -0
  98. package/dist/cli/types.d.ts.map +1 -1
  99. package/dist/infra/lib/config/GlobalConfigManager.d.ts +2 -0
  100. package/dist/infra/lib/config/GlobalConfigManager.d.ts.map +1 -1
  101. package/dist/infra/lib/config/GlobalConfigManager.js +6 -0
  102. package/dist/infra/lib/config/GlobalConfigManager.js.map +1 -1
  103. package/dist/infra/lib/llm-availability.d.ts +4 -0
  104. package/dist/infra/lib/llm-availability.d.ts.map +1 -1
  105. package/dist/infra/lib/llm-availability.js +9 -1
  106. package/dist/infra/lib/llm-availability.js.map +1 -1
  107. package/dist/infra/lib/zai/client.d.ts +30 -0
  108. package/dist/infra/lib/zai/client.d.ts.map +1 -0
  109. package/dist/infra/lib/zai/client.js +124 -0
  110. package/dist/infra/lib/zai/client.js.map +1 -0
  111. package/dist/infra/lib/zai/index.d.ts +11 -0
  112. package/dist/infra/lib/zai/index.d.ts.map +1 -0
  113. package/dist/infra/lib/zai/index.js +10 -0
  114. package/dist/infra/lib/zai/index.js.map +1 -0
  115. package/dist/infra/lib/zai/orchestration.d.ts +15 -0
  116. package/dist/infra/lib/zai/orchestration.d.ts.map +1 -0
  117. package/dist/infra/lib/zai/orchestration.js +54 -0
  118. package/dist/infra/lib/zai/orchestration.js.map +1 -0
  119. package/dist/infra/lib/zai/types.d.ts +40 -0
  120. package/dist/infra/lib/zai/types.d.ts.map +1 -0
  121. package/dist/infra/lib/zai/types.js +5 -0
  122. package/dist/infra/lib/zai/types.js.map +1 -0
  123. package/dist/infra/orchestrator/SmartRouter.d.ts.map +1 -1
  124. package/dist/infra/orchestrator/SmartRouter.js +16 -34
  125. package/dist/infra/orchestrator/SmartRouter.js.map +1 -1
  126. package/dist/infra/orchestrator/index.d.ts +1 -0
  127. package/dist/infra/orchestrator/index.d.ts.map +1 -1
  128. package/dist/infra/orchestrator/index.js +11 -9
  129. package/dist/infra/orchestrator/index.js.map +1 -1
  130. package/dist/infra/orchestrator/types.d.ts +32 -9
  131. package/dist/infra/orchestrator/types.d.ts.map +1 -1
  132. package/dist/infra/orchestrator/types.js +68 -37
  133. package/dist/infra/orchestrator/types.js.map +1 -1
  134. package/dist/infra/orchestrator/types.test.d.ts +2 -0
  135. package/dist/infra/orchestrator/types.test.d.ts.map +1 -0
  136. package/dist/infra/orchestrator/types.test.js +57 -0
  137. package/dist/infra/orchestrator/types.test.js.map +1 -0
  138. package/hooks/scripts/__tests__/clone-behaviors.test.js +56 -0
  139. package/hooks/scripts/clone-extract.js +196 -11
  140. package/hooks/scripts/clone-spec.js +63 -12
  141. package/hooks/scripts/llm-orchestrate.js +49 -0
  142. package/package.json +4 -2
  143. package/skills/clone/SKILL.md +19 -4
  144. package/skills/create-prd/agents/edge-case-finder.md +4 -4
  145. package/skills/create-prd/agents/prioritizer.md +4 -4
  146. package/skills/create-prd/agents/requirements-writer.md +3 -3
  147. package/skills/create-prd/agents/researcher.md +3 -3
  148. package/skills/create-prd/agents/reviewer.md +3 -3
  149. package/skills/create-prd/orchestrator.md +2 -0
  150. package/skills/design-audit/orchestrator.md +2 -0
  151. package/skills/exec-plan/orchestrator.md +2 -0
  152. package/skills/parallel-research/orchestrator.md +2 -0
  153. package/skills/techdebt/orchestrator.md +2 -0
  154. package/skills/vibe.clone/SKILL.md +6 -3
  155. package/skills/vibe.llm/SKILL.md +50 -0
  156. package/vibe/rules/orchestrator-contract.md +56 -0
@@ -12,6 +12,9 @@
12
12
  * screenshot.png — full-page screenshot
13
13
  * stylesheets.json — @font-face + @keyframes harvested from all sheets
14
14
  * states.json — non-default state rules (hover/focus/active/checked/tab/aria/data-state)
15
+ * behaviors.json — ACTIVE interaction sweep: scroll-triggered header/nav diffs +
16
+ * click-driven tab groups (content-swap detection). Captures JS-set
17
+ * state that static CSS harvesting can't see. Skip with --no-interact.
15
18
  * asset-map.json — remote URL → local path mapping
16
19
  * assets/images/*, assets/fonts/*
17
20
  *
@@ -78,7 +81,7 @@ const CSS_PROPS = [
78
81
  // ─── CLI parse ──────────────────────────────────────────────────────
79
82
  function parseArgs(argv) {
80
83
  const [, , cmd, urlArg, ...rest] = argv;
81
- const opts = { stealth: false, ignoreRobots: false };
84
+ const opts = { stealth: false, ignoreRobots: false, interact: true };
82
85
  for (const arg of rest) {
83
86
  if (arg.startsWith('--out=')) opts.out = arg.slice(6);
84
87
  else if (arg.startsWith('--viewport=')) opts.viewport = arg.slice(11);
@@ -86,6 +89,7 @@ function parseArgs(argv) {
86
89
  else if (arg.startsWith('--wait=')) opts.wait = Number(arg.slice(7));
87
90
  else if (arg === '--stealth') opts.stealth = true;
88
91
  else if (arg === '--ignore-robots') opts.ignoreRobots = true;
92
+ else if (arg === '--no-interact') opts.interact = false;
89
93
  }
90
94
  return { cmd, url: urlArg, opts };
91
95
  }
@@ -241,7 +245,7 @@ const PAGE_EXTRACT = `
241
245
  // Deterministic: read state-dependent declarations straight from the stylesheets
242
246
  // (no scripted clicking/hovering, so the output stays reproducible).
243
247
  const propSet = new Set(props);
244
- const STATE_RE = /:hover|:focus(?:-visible|-within)?|:active|:checked|:target|\\[aria-(?:expanded|selected|current|pressed)|\\[data-(?:state|active|open|selected)|\\[open\\]|\\.is-[a-z-]+|\\.(?:active|open|selected|expanded|current|show|visible)(?![\\w-])/i;
248
+ const STATE_RE = /:hover|:focus(?:-visible|-within)?|:active|:checked|:target|\\[aria-(?:expanded|selected|current|pressed)|\\[data-(?:state|active|open|selected)|\\[open\\]|\\.is-[a-z-]+|\\.has-[a-z-]+|\\.(?:active|open|selected|expanded|current|show|visible|scrolled|sticky|stuck|pinned|fixed|shrink|shrunk|compact|affix|headroom|scrolling)(?![\\w-])/i;
245
249
  const harvestStateRule = (rule, media) => {
246
250
  if (rule.selectorText && STATE_RE.test(rule.selectorText)) {
247
251
  const decl = {};
@@ -556,6 +560,155 @@ async function freezeAnimations(page) {
556
560
  });
557
561
  }
558
562
 
563
+ // ─── Active interaction sweep ───────────────────────────────────────
564
+ // Static CSS harvesting (states.json) only sees declared :hover/:active/[data-state]
565
+ // rules. It is blind to JS-set state: a header that gains a class on scroll, a tab
566
+ // group that swaps content on click, Framer/GSAP inline-style animations. This sweep
567
+ // actually drives the page and diffs computed styles before/after — the #1 accuracy fix.
568
+
569
+ // Subset of props worth diffing for scroll/click state changes (kept small on purpose).
570
+ const BEHAVIOR_PROPS = [
571
+ 'background-color', 'background-image', 'box-shadow', 'backdrop-filter',
572
+ 'height', 'min-height', 'padding-top', 'padding-bottom',
573
+ 'transform', 'opacity', 'position', 'top',
574
+ 'border-bottom-width', 'border-bottom-color', 'color',
575
+ ];
576
+
577
+ // Pure style differ — exported for testing. Returns { prop: { from, to } } for changed props.
578
+ function diffStyles(a, b) {
579
+ const out = {};
580
+ if (!a || !b) return out;
581
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
582
+ for (const k of keys) {
583
+ const from = a[k] ?? null;
584
+ const to = b[k] ?? null;
585
+ if (from !== to && (from || to)) out[k] = { from, to };
586
+ }
587
+ return out;
588
+ }
589
+
590
+ // In-page: tag sticky/fixed/top-bar elements with a probe attr so we can re-find them.
591
+ const TAG_SCROLL_CANDIDATES = `(function () {
592
+ const els = Array.from(document.querySelectorAll(
593
+ 'header, nav, [class*="header" i], [class*="nav" i], [class*="sticky" i], [class*="fixed" i]'
594
+ ));
595
+ const out = []; let i = 0;
596
+ for (const el of els) {
597
+ if (i >= 6) break;
598
+ const cs = getComputedStyle(el);
599
+ const r = el.getBoundingClientRect();
600
+ const sticky = cs.position === 'fixed' || cs.position === 'sticky';
601
+ const topBar = r.top >= 0 && r.top < 120 && r.width > window.innerWidth * 0.5;
602
+ if (!sticky && !topBar) continue;
603
+ if (el.closest('[data-clone-probe]')) continue;
604
+ el.setAttribute('data-clone-probe', 'sc' + i);
605
+ const cls = (el.getAttribute('class') || '').trim().split(/\\s+/).filter(Boolean)[0] || '';
606
+ out.push({ probe: 'sc' + i, tag: el.tagName.toLowerCase(), cls });
607
+ i++;
608
+ }
609
+ return out;
610
+ })`;
611
+
612
+ const SNAP_PROBES = `(function (props) {
613
+ const out = {};
614
+ document.querySelectorAll('[data-clone-probe]').forEach((el) => {
615
+ const cs = getComputedStyle(el); const o = {};
616
+ for (const p of props) { const v = cs.getPropertyValue(p); if (v) o[p] = v.trim(); }
617
+ out[el.getAttribute('data-clone-probe')] = o;
618
+ });
619
+ return out;
620
+ })`;
621
+
622
+ // In-page: tag tab-like groups (≥2 siblings) so we can click them.
623
+ const TAG_TAB_GROUPS = `(function () {
624
+ const els = Array.from(document.querySelectorAll(
625
+ '[role="tab"], [aria-selected], button[class*="tab" i], li[class*="tab" i], [data-state="active"], [data-state="inactive"]'
626
+ ));
627
+ const groups = new Map();
628
+ for (const el of els) {
629
+ const p = el.parentElement; if (!p) continue;
630
+ if (!groups.has(p)) groups.set(p, []);
631
+ groups.get(p).push(el);
632
+ }
633
+ const out = []; let g = 0;
634
+ for (const [, items] of groups) {
635
+ if (g >= 4) break;
636
+ if (items.length < 2) continue;
637
+ items.forEach((el, idx) => el.setAttribute('data-clone-tab', g + '_' + idx));
638
+ out.push({ group: g, count: items.length, labels: items.map((el) => (el.textContent || '').trim().slice(0, 40)).filter(Boolean) });
639
+ g++;
640
+ }
641
+ return out;
642
+ })`;
643
+
644
+ // Fingerprint the visible text near a tab group, to detect content swaps on click.
645
+ const TAB_CONTENT_FP = `(function (g) {
646
+ const first = document.querySelector('[data-clone-tab="' + g + '_0"]');
647
+ if (!first) return null;
648
+ const box = first.closest('section, main, div');
649
+ const root = box ? (box.parentElement || box) : document.body;
650
+ return (root.innerText || '').replace(/\\s+/g, ' ').trim().slice(0, 4000);
651
+ })`;
652
+
653
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
654
+
655
+ async function runInteractionSweep(page) {
656
+ const behaviors = { scroll: [], interactive: [] };
657
+
658
+ // ── Scroll-triggered state (header/nav shrink, background, shadow on scroll) ──
659
+ const candidates = await page.evaluate(`(${TAG_SCROLL_CANDIDATES})()`);
660
+ if (candidates.length) {
661
+ const before = await page.evaluate(`(${SNAP_PROBES})(${JSON.stringify(BEHAVIOR_PROPS)})`);
662
+ const innerH = await page.evaluate('window.innerHeight');
663
+ const triggerY = Math.min(700, Math.max(200, Math.round(innerH * 0.9)));
664
+ await page.evaluate(`window.scrollTo(0, ${triggerY})`);
665
+ await wait(500);
666
+ const after = await page.evaluate(`(${SNAP_PROBES})(${JSON.stringify(BEHAVIOR_PROPS)})`);
667
+ await page.evaluate('window.scrollTo(0, 0)');
668
+ await wait(300);
669
+ for (const c of candidates) {
670
+ const changed = diffStyles(before[c.probe], after[c.probe]);
671
+ if (Object.keys(changed).length) {
672
+ behaviors.scroll.push({
673
+ label: c.cls ? `${c.tag}.${c.cls}` : c.tag,
674
+ tag: c.tag, cls: c.cls, triggerScrollY: triggerY, changed,
675
+ });
676
+ }
677
+ }
678
+ }
679
+
680
+ // ── Click-driven tab groups (content swap detection) ──
681
+ const groups = await page.evaluate(`(${TAG_TAB_GROUPS})()`);
682
+ for (const grp of groups) {
683
+ try {
684
+ const beforeFp = await page.evaluate(`(${TAB_CONTENT_FP})(${grp.group})`);
685
+ const clicked = await page.evaluate(
686
+ `(function (g) { const t = document.querySelector('[data-clone-tab="' + g + '_1"]'); if (!t) return false; t.click(); return true; })(${grp.group})`
687
+ );
688
+ if (!clicked) continue;
689
+ await wait(450);
690
+ const afterFp = await page.evaluate(`(${TAB_CONTENT_FP})(${grp.group})`);
691
+ // restore default state
692
+ await page.evaluate(
693
+ `(function (g) { const t = document.querySelector('[data-clone-tab="' + g + '_0"]'); if (t) t.click(); })(${grp.group})`
694
+ );
695
+ await wait(200);
696
+ behaviors.interactive.push({
697
+ kind: 'tab-group',
698
+ count: grp.count,
699
+ tabLabels: grp.labels,
700
+ contentSwapsOnClick: beforeFp != null && afterFp != null && beforeFp !== afterFp,
701
+ });
702
+ } catch { /* one bad group must not abort the whole sweep */ }
703
+ }
704
+
705
+ // Clean up probe attributes BEFORE static extraction so they don't pollute output.
706
+ await page.evaluate(
707
+ 'document.querySelectorAll("[data-clone-probe],[data-clone-tab]").forEach((e) => { e.removeAttribute("data-clone-probe"); e.removeAttribute("data-clone-tab"); })'
708
+ );
709
+ return behaviors;
710
+ }
711
+
559
712
  // HTML sanitization: strip scripts/analytics before saving
560
713
  function sanitizeHtml(html) {
561
714
  return html
@@ -617,6 +770,17 @@ async function capture({ url, opts }) {
617
770
  if (opts.wait) await new Promise((r) => setTimeout(r, opts.wait));
618
771
  else await new Promise((r) => setTimeout(r, 1200));
619
772
 
773
+ // Active interaction sweep (before freezing — it observes real transitions/JS state).
774
+ let behaviors = null;
775
+ if (opts.interact) {
776
+ try {
777
+ behaviors = await runInteractionSweep(page);
778
+ console.log(`[clone-extract] interaction sweep: ${behaviors.scroll.length} scroll-state, ${behaviors.interactive.length} tab-group(s)`);
779
+ } catch (e) {
780
+ console.log(`[clone-extract] interaction sweep skipped: ${e.message}`);
781
+ }
782
+ }
783
+
620
784
  await freezeAnimations(page);
621
785
 
622
786
  // PAGE_EXTRACT is a parenthesized function-expression string. page.evaluate treats a
@@ -731,6 +895,22 @@ async function capture({ url, opts }) {
731
895
  }, null, 2),
732
896
  );
733
897
  fs.writeFileSync(path.join(opts.out, 'asset-map.json'), JSON.stringify(assetMap, null, 2));
898
+ if (behaviors) {
899
+ // Rewrite any remote asset URLs captured in scroll-state background-image diffs.
900
+ for (const s of behaviors.scroll) {
901
+ for (const ch of Object.values(s.changed)) {
902
+ if (typeof ch.from === 'string') ch.from = rewriteCssValue(ch.from);
903
+ if (typeof ch.to === 'string') ch.to = rewriteCssValue(ch.to);
904
+ }
905
+ }
906
+ fs.writeFileSync(
907
+ path.join(opts.out, 'behaviors.json'),
908
+ JSON.stringify({
909
+ meta: { url, bp: opts.bp || null, capturedAt: new Date().toISOString() },
910
+ ...behaviors,
911
+ }, null, 2),
912
+ );
913
+ }
734
914
 
735
915
  const okCount = Object.values(assetMap).filter((a) => a.status === 'ok').length;
736
916
  console.log(`[clone-extract] done → ${opts.out}`);
@@ -742,15 +922,20 @@ async function capture({ url, opts }) {
742
922
  }
743
923
 
744
924
  // ─── Entry ──────────────────────────────────────────────────────────
745
- const { cmd, url, opts } = parseArgs(process.argv);
925
+ const isMain = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('clone-extract.js');
926
+ if (isMain) {
927
+ const { cmd, url, opts } = parseArgs(process.argv);
928
+
929
+ if (cmd !== 'capture') {
930
+ console.error('Usage: node clone-extract.js capture <URL> --out=<dir> --viewport=WxH[@DPR] --bp=mo|pc [--stealth] [--ignore-robots] [--no-interact] [--wait=ms]');
931
+ process.exit(1);
932
+ }
746
933
 
747
- if (cmd !== 'capture') {
748
- console.error('Usage: node clone-extract.js capture <URL> --out=<dir> --viewport=WxH[@DPR] --bp=mo|pc [--stealth] [--ignore-robots] [--wait=ms]');
749
- process.exit(1);
934
+ capture({ url, opts }).catch((err) => {
935
+ console.error(`[clone-extract] FAIL: ${err.message}`);
936
+ if (process.env.DEBUG) console.error(err.stack);
937
+ process.exit(1);
938
+ });
750
939
  }
751
940
 
752
- capture({ url, opts }).catch((err) => {
753
- console.error(`[clone-extract] FAIL: ${err.message}`);
754
- if (process.env.DEBUG) console.error(err.stack);
755
- process.exit(1);
756
- });
941
+ export { diffStyles };
@@ -27,15 +27,12 @@ function parseArgs(argv) {
27
27
  if (a.startsWith('--out=')) opts.out = a.slice(6);
28
28
  else if (a.startsWith('--section=')) opts.section = a.slice(10);
29
29
  else if (a.startsWith('--feature=')) opts.feature = a.slice(10);
30
+ else if (a.startsWith('--behaviors=')) opts.behaviors = a.slice(12);
30
31
  }
31
32
  return { sectionsPath, opts };
32
33
  }
33
34
 
34
35
  const { sectionsPath, opts } = parseArgs(process.argv);
35
- if (!sectionsPath || !opts.out) {
36
- console.error('Usage: node clone-spec.js <sections.json> --out=<dir> [--section=Name] [--feature=name]');
37
- process.exit(1);
38
- }
39
36
 
40
37
  // ─── Helpers ────────────────────────────────────────────────────────
41
38
  function safeName(name) {
@@ -95,6 +92,40 @@ function componentsBlock(components) {
95
92
  }).join('\n');
96
93
  }
97
94
 
95
+ // True if any node in the section subtree matches a behavior's tag (+ optional class).
96
+ function sectionHasNode(section, tag, cls) {
97
+ const hit = (n) => {
98
+ if (n.tag === tag && (!cls || (n.classes || '').split(/\s+/).includes(cls))) return true;
99
+ return (n.children || []).some(hit);
100
+ };
101
+ return (section.children || []).some(hit);
102
+ }
103
+
104
+ function behaviorsBlock(section, behaviors) {
105
+ if (!behaviors) return null;
106
+ const scroll = (behaviors.scroll || []).filter((b) => sectionHasNode(section, b.tag, b.cls));
107
+ // Tab groups aren't tag-keyed; attach to any section that contains a [role=tab]-ish node.
108
+ const tabs = (behaviors.interactive || []).filter(() =>
109
+ sectionHasNode(section, 'button') || sectionHasNode(section, 'li') || sectionHasNode(section, 'a'),
110
+ );
111
+ if (!scroll.length && !tabs.length) return null;
112
+
113
+ const lines = [];
114
+ for (const b of scroll) {
115
+ const diffs = Object.entries(b.changed)
116
+ .map(([k, v]) => ` - ${k}: \`${v.from}\` → \`${v.to}\``).join('\n');
117
+ lines.push(`- **Scroll-triggered** on \`${b.label}\` (past ~${b.triggerScrollY}px scroll):\n${diffs}`);
118
+ }
119
+ for (const t of tabs) {
120
+ const labels = (t.tabLabels || []).map((l) => `"${l}"`).join(', ') || '(unlabeled)';
121
+ const swap = t.contentSwapsOnClick
122
+ ? '**content SWAPS on click** → extract every tab\'s content/assets, build click-driven'
123
+ : 'no content swap detected (styling-only tabs)';
124
+ lines.push(`- **Tab group** ×${t.count} [${labels}]: ${swap}`);
125
+ }
126
+ return lines.join('\n');
127
+ }
128
+
98
129
  function assetsBlock(images) {
99
130
  const bg = (images && images.bg) || [];
100
131
  const content = (images && images.content) || [];
@@ -105,12 +136,16 @@ function assetsBlock(images) {
105
136
  }
106
137
 
107
138
  // ─── Spec rendering ─────────────────────────────────────────────────
108
- function renderSpec(section, meta) {
139
+ function renderSpec(section, meta, behaviors) {
109
140
  const box = section.box || {};
110
141
  const texts = collectText(section);
111
142
  const textBlock = texts.length
112
143
  ? texts.map((t) => `- "${t}"`).join('\n')
113
144
  : '- (no direct text — likely visual/asset-only section)';
145
+ const behavior = behaviorsBlock(section, behaviors);
146
+ const behaviorSection = behavior
147
+ ? `\n## Dynamic behaviors — observed by ACTIVE capture (not guesses)\n_Captured by driving the live page (scroll/click). These override the static heuristic above._\n${behavior}\n`
148
+ : '';
114
149
  return `# ${section.name} — Clone Spec (${meta.bp})
115
150
 
116
151
  > Auto-generated skeleton from sections.json by clone-spec.js. **This is a build contract.**
@@ -123,7 +158,7 @@ function renderSpec(section, meta) {
123
158
 
124
159
  ## Interaction model — ⚠ confirm before building
125
160
  ${interactionBlock(section.interaction)}
126
-
161
+ ${behaviorSection}
127
162
  ## States to reproduce
128
163
  _Harvested non-default state rules (hover/focus/active/checked/tab/aria/data-state). Implement each._
129
164
  ${statesBlock(section.states)}
@@ -162,6 +197,13 @@ function main() {
162
197
  viewport: data.meta && data.meta.viewport,
163
198
  bp: opts.section ? (data.meta && data.meta.bp) : (data.meta && data.meta.bp) || '?',
164
199
  };
200
+ // behaviors.json (active interaction sweep) lives next to the source computed.json.
201
+ const behaviorsPath = opts.behaviors || path.join(path.dirname(sectionsPath), 'behaviors.json');
202
+ let behaviors = null;
203
+ if (fs.existsSync(behaviorsPath)) {
204
+ try { behaviors = JSON.parse(fs.readFileSync(behaviorsPath, 'utf8')); } catch { behaviors = null; }
205
+ }
206
+
165
207
  let sections = data.sections || [];
166
208
  if (opts.section) sections = sections.filter((s) => s.name === opts.section);
167
209
  if (!sections.length) {
@@ -172,15 +214,24 @@ function main() {
172
214
  fs.mkdirSync(opts.out, { recursive: true });
173
215
  for (const section of sections) {
174
216
  const file = path.join(opts.out, `${safeName(section.name)}.spec.md`);
175
- fs.writeFileSync(file, renderSpec(section, meta));
217
+ fs.writeFileSync(file, renderSpec(section, meta, behaviors));
176
218
  console.log(`[clone-spec] wrote ${file}`);
177
219
  }
178
220
  console.log(`[clone-spec] done → ${sections.length} spec(s) in ${opts.out}`);
179
221
  }
180
222
 
181
- try { main(); }
182
- catch (e) {
183
- console.error(`[clone-spec] FAIL: ${e.message}`);
184
- if (process.env.DEBUG) console.error(e.stack);
185
- process.exit(1);
223
+ const isMain = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('clone-spec.js');
224
+ if (isMain) {
225
+ if (!sectionsPath || !opts.out) {
226
+ console.error('Usage: node clone-spec.js <sections.json> --out=<dir> [--section=Name] [--feature=name] [--behaviors=path]');
227
+ process.exit(1);
228
+ }
229
+ try { main(); }
230
+ catch (e) {
231
+ console.error(`[clone-spec] FAIL: ${e.message}`);
232
+ if (process.env.DEBUG) console.error(e.stack);
233
+ process.exit(1);
234
+ }
186
235
  }
236
+
237
+ export { sectionHasNode, behaviorsBlock };
@@ -105,6 +105,7 @@ function resolveModel(providerName, config) {
105
105
  if (providerName === 'gpt-codex') return config.models?.gptCodex || 'gpt-5.3-codex';
106
106
  if (providerName === 'gpt') return config.models?.gpt || 'gpt-5.5';
107
107
  if (providerName === 'antigravity') return config.models?.antigravity || 'antigravity';
108
+ if (providerName === 'zai') return config.models?.zaiCoding || config.models?.zai || 'glm-5.2';
108
109
  if (providerName === 'claude') return 'claude';
109
110
  return providerName;
110
111
  }
@@ -271,6 +272,46 @@ function callCodexCli(prompt, sysPrompt, jsonMode, model, timeoutMs) {
271
272
  });
272
273
  }
273
274
 
275
+ // ZAI (Z.ai / GLM) — OpenAI 호환 HTTP API (CLI 없음)
276
+ const ZAI_BASE_GENERAL = 'https://api.z.ai/api/paas/v4';
277
+ const ZAI_BASE_CODING = 'https://api.z.ai/api/coding/paas/v4';
278
+
279
+ function callZaiApi(prompt, sysPrompt, jsonMode, model, timeoutMs) {
280
+ const vibeConfig = readVibeConfig();
281
+ const codingKey = vibeConfig.credentials?.zai?.codingApiKey || process.env.ZAI_CODING_API_KEY;
282
+ const generalKey = vibeConfig.credentials?.zai?.apiKey || process.env.ZAI_API_KEY;
283
+ const useCoding = Boolean(codingKey);
284
+ const apiKey = codingKey || generalKey;
285
+ if (!apiKey) return Promise.reject(new Error('ZAI API key not set (vibe zai coding-key <key>)'));
286
+
287
+ const base = useCoding ? ZAI_BASE_CODING : ZAI_BASE_GENERAL;
288
+ const system = sysPrompt && sysPrompt !== DEFAULT_SYSTEM_PROMPT ? sysPrompt : DEFAULT_SYSTEM_PROMPT;
289
+ const content = jsonMode ? `${prompt}\n\nRespond with valid JSON only.` : prompt;
290
+ const controller = new AbortController();
291
+ const timer = setTimeout(() => controller.abort(), timeoutMs || CLI_TIMEOUT_MS);
292
+
293
+ return fetch(`${base}/chat/completions`, {
294
+ method: 'POST',
295
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
296
+ body: JSON.stringify({
297
+ model,
298
+ messages: [{ role: 'system', content: system }, { role: 'user', content }],
299
+ max_tokens: 4096,
300
+ temperature: 0,
301
+ }),
302
+ signal: controller.signal,
303
+ }).then(async (res) => {
304
+ if (!res.ok) {
305
+ const body = await res.text().catch(() => '');
306
+ throw new Error(`zai api failed (code ${res.status}): ${body.slice(0, 500)}`);
307
+ }
308
+ const data = await res.json();
309
+ const text = data?.choices?.[0]?.message?.content?.trim();
310
+ if (!text) throw new Error('zai api returned empty response');
311
+ return text;
312
+ }).finally(() => clearTimeout(timer));
313
+ }
314
+
274
315
  function callAntigravityCli(prompt, sysPrompt, jsonMode, timeoutMs) {
275
316
  const fullPrompt = buildCliPrompt(prompt, sysPrompt, jsonMode);
276
317
  const args = ['-p', fullPrompt];
@@ -357,6 +398,14 @@ async function callProvider(providerName, prompt, sysPrompt, jsonMode, timeoutMs
357
398
  return await callAntigravityCli(prompt, sysPrompt, jsonMode, timeoutMs);
358
399
  }
359
400
 
401
+ if (providerName === 'zai') {
402
+ // coding 키가 있으면 coding plan flagship(5.2), 아니면 일반 API 상한(5.1)
403
+ const hasCoding = Boolean(vibeConfig.credentials?.zai?.codingApiKey || process.env.ZAI_CODING_API_KEY);
404
+ const fallback = hasCoding ? 'glm-5.2' : 'glm-5.1';
405
+ const model = vibeConfig.models?.zaiCoding || vibeConfig.models?.zai || process.env.ZAI_MODEL || fallback;
406
+ return await callZaiApi(prompt, sysPrompt, jsonMode, model, timeoutMs);
407
+ }
408
+
360
409
  if (providerName === 'claude') {
361
410
  return await callClaudeCli(prompt, sysPrompt, jsonMode, timeoutMs);
362
411
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@su-record/vibe",
3
- "version": "2.16.2",
4
- "description": "AI Coding Framework for Claude Code — 42+ agents, 70 skills, multi-LLM orchestration",
3
+ "version": "2.16.4",
4
+ "description": "AI Coding Framework for Claude Code — 42+ agents, 73 skills, multi-LLM orchestration",
5
5
  "type": "module",
6
6
  "main": "dist/cli/index.js",
7
7
  "exports": {
@@ -27,6 +27,8 @@
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
+ "sync:agent-models": "npx tsx scripts/sync-agent-models.ts",
31
+ "sync:agent-models:check": "npx tsx scripts/sync-agent-models.ts --check",
30
32
  "validate:skill-invocation": "npx tsx scripts/validate-skill-invocation.ts",
31
33
  "validate:counts": "npx tsx scripts/validate-counts.ts",
32
34
  "test": "vitest run",
@@ -41,6 +41,11 @@ The rendered DOM is the source of truth for markup. Screenshots are for pixel ve
41
41
 
42
42
  6. Do NOT ship default-state-only. Implement every harvested state (hover/focus/active/open/
43
43
  tab-switch) from states.json / the section spec.
44
+
45
+ 7. Do NOT ignore behaviors.json. The ACTIVE interaction sweep (scroll-state diffs +
46
+ click-driven tab content-swap detection) catches JS-set state that static CSS harvesting
47
+ is blind to. When the spec's "Dynamic behaviors" block conflicts with the static
48
+ interaction heuristic, the active capture wins.
44
49
  ```
45
50
 
46
51
  ## Full Flow
@@ -119,6 +124,7 @@ node {{VIBE_PATH}}/hooks/scripts/clone-extract.js capture <URL> \
119
124
  ├── computed.json — per-element computed CSS + bounding box
120
125
  ├── screenshot.png — full-page screenshot
121
126
  ├── states.json — non-default state rules (hover/focus/active/checked/tab/aria/data-state)
127
+ ├── behaviors.json — ACTIVE sweep: scroll-triggered header/nav diffs + click-driven tab groups
122
128
  ├── assets/
123
129
  │ ├── images/ — all <img>, background-image, <picture> sources
124
130
  │ └── fonts/ — @font-face srcs
@@ -136,8 +142,14 @@ node {{VIBE_PATH}}/hooks/scripts/clone-extract.js capture <URL> \
136
142
  6. Rewrite asset URLs in rendered.html and computed.json to local paths
137
143
  7. Strip inline analytics/tracking scripts before saving rendered.html
138
144
  8. Harvest non-default state rules from all stylesheets → states.json
139
- (deterministic: read :hover/:focus/:active/:checked/[aria-*]/[data-state]/.is-*/.active
140
- declarations straight from CSS — NO scripted clicking, so output stays reproducible)
145
+ (deterministic: read :hover/:focus/:active/:checked/[aria-*]/[data-state]/.is-*/.active/
146
+ .scrolled/.sticky/.pinned/… declarations straight from CSS — NO scripted clicking)
147
+ 9. ACTIVE interaction sweep → behaviors.json (runs after lazy-load, before freeze).
148
+ Drives the live page to capture JS-set state that CSS harvesting can't see:
149
+ - Scroll-state diff: tag sticky/fixed/top-bar headers+nav, snapshot computed CSS at
150
+ scroll 0, scroll past threshold, re-snapshot, diff → {prop: {from, to}, triggerScrollY}
151
+ - Tab groups: click tab-like sibling sets, detect whether content swaps on click
152
+ Disable with --no-interact (restores the old fully-deterministic, screenshot-stable capture).
141
153
  ```
142
154
 
143
155
  ---
@@ -256,7 +268,9 @@ node {{VIBE_PATH}}/hooks/scripts/clone-validate.js \
256
268
  ```
257
269
 
258
270
  ⛔ **No section is built without a completed spec.** Step 0 emits `_specs/{Section}.spec.md`
259
- (interaction model + states + computed CSS + assets + text + checklist). Before writing a
271
+ (interaction model + **active-capture Dynamic behaviors** + states + computed CSS + assets +
272
+ text + checklist). clone-spec.js auto-loads `behaviors.json` from the sections.json dir and
273
+ attaches matching scroll/tab behaviors per section. Before writing a
260
274
  section's component, Claude reviews its spec and resolves every `TODO` (confirm interaction
261
275
  model, list states to implement, choose tags, replace copyrighted text). The spec is the
262
276
  contract AND the audit trail — it forces extraction rigor before any code is written.
@@ -407,4 +421,5 @@ Claude must:
407
421
  | clone-refine.js produces empty sections | Site likely uses Shadow DOM or canvas rendering. Report and ask whether to fall back to screenshot-only mode. |
408
422
  | Pixel diff stuck > 0.05 after 5 rounds | Likely font fallback or anti-aliasing. Report metric, allow user to accept threshold. |
409
423
  | Interaction model guess wrong (Phase 5) | section.interaction is a static-DOM heuristic. Re-observe the live site, correct the model in the spec, rebuild the section for the confirmed model. |
410
- | states.json empty but site has hover/tabs | States may be set via inline JS, not CSS rules. Note in the spec and capture them manually from the live site during Phase 5. |
424
+ | states.json empty but site has hover/tabs | States may be set via inline JS, not CSS rules. Check behaviors.json (active sweep captures scroll/tab JS state). If still missing, note in the spec and capture manually during Phase 5. |
425
+ | behaviors.json missing or empty | Active sweep was disabled (--no-interact), hit no sticky/tab elements, or errored (logged, non-fatal). Falls back to static states.json. Re-run without --no-interact to retry. |
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: edge-case-finder
2
+ name: prd-edge-case-finder
3
3
  role: Identifies edge cases, constraints, and risks for each requirement
4
4
  tools: [Read, WebSearch]
5
5
  ---
@@ -17,8 +17,8 @@ Stress-tests the requirements by systematically exploring what happens at bounda
17
17
  - Flag security concerns: injection, over-fetching, authorization bypass
18
18
 
19
19
  ## Input
20
- - User stories and acceptance criteria from requirements-writer
21
- - Research brief from researcher (constraints section)
20
+ - User stories and acceptance criteria from prd-requirements-writer
21
+ - Research brief from prd-researcher (constraints section)
22
22
 
23
23
  ## Output
24
24
  Edge cases and risks appended to each user story, plus a global risks section:
@@ -41,7 +41,7 @@ Edge cases and risks appended to each user story, plus a global risks section:
41
41
  ```
42
42
 
43
43
  ## Communication
44
- - Reports findings to: prioritizer, reviewer
44
+ - Reports findings to: prd-prioritizer, prd-reviewer
45
45
  - Receives instructions from: orchestrator (create-prd skill)
46
46
 
47
47
  ## Domain Knowledge
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: prioritizer
2
+ name: prd-prioritizer
3
3
  role: Applies RICE and MoSCoW scoring to rank requirements for implementation
4
4
  tools: [Read]
5
5
  ---
@@ -17,8 +17,8 @@ Applies structured scoring frameworks to the full requirements list to produce a
17
17
  - Document prioritization rationale so it can be revisited
18
18
 
19
19
  ## Input
20
- - User stories from requirements-writer
21
- - Edge cases from edge-case-finder
20
+ - User stories from prd-requirements-writer
21
+ - Edge cases from prd-edge-case-finder
22
22
  - Optional: team velocity or sprint capacity
23
23
 
24
24
  ## Output
@@ -53,7 +53,7 @@ US-01 must ship before US-02 (filter requires results to exist)
53
53
  ```
54
54
 
55
55
  ## Communication
56
- - Reports findings to: reviewer
56
+ - Reports findings to: prd-reviewer
57
57
  - Receives instructions from: orchestrator (create-prd skill)
58
58
 
59
59
  ## Domain Knowledge
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: requirements-writer
2
+ name: prd-requirements-writer
3
3
  role: Writes user stories and acceptance criteria from research brief
4
4
  tools: [Read, Write]
5
5
  ---
@@ -17,7 +17,7 @@ Translates research findings and user goals into structured user stories with cl
17
17
  - Avoid implementation details — describe behavior, not code
18
18
 
19
19
  ## Input
20
- - Research brief from researcher
20
+ - Research brief from prd-researcher
21
21
  - User personas or target segment description
22
22
  - Optional: existing SPEC or feature file for context
23
23
 
@@ -41,7 +41,7 @@ so that I can find items without browsing all categories.
41
41
  ```
42
42
 
43
43
  ## Communication
44
- - Reports findings to: edge-case-finder, prioritizer, reviewer
44
+ - Reports findings to: prd-edge-case-finder, prd-prioritizer, prd-reviewer
45
45
  - Receives instructions from: orchestrator (create-prd skill)
46
46
 
47
47
  ## Domain Knowledge
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: researcher
2
+ name: prd-researcher
3
3
  role: Gathers market context and user needs using web search and codebase analysis
4
4
  tools: [WebSearch, Read, Glob, Grep, Bash]
5
5
  ---
@@ -14,7 +14,7 @@ Gathers external context — competitor patterns, industry standards, user expec
14
14
  - Identify common user expectations and mental models for this feature type
15
15
  - Review existing codebase for related patterns, conventions, or prior art
16
16
  - Surface relevant regulatory, accessibility, or compliance constraints
17
- - Compile source references so requirements-writer can cite evidence
17
+ - Compile source references so prd-requirements-writer can cite evidence
18
18
 
19
19
  ## Input
20
20
  - Feature or problem description from the user
@@ -48,7 +48,7 @@ Research brief:
48
48
  ```
49
49
 
50
50
  ## Communication
51
- - Reports findings to: requirements-writer, edge-case-finder
51
+ - Reports findings to: prd-requirements-writer, prd-edge-case-finder
52
52
  - Receives instructions from: orchestrator (create-prd skill)
53
53
 
54
54
  ## Domain Knowledge
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: reviewer
2
+ name: prd-reviewer
3
3
  role: Reviews PRD completeness and testability before sign-off
4
4
  tools: [Read]
5
5
  ---
@@ -18,8 +18,8 @@ Final quality gate for the PRD. Checks that every requirement is testable, every
18
18
 
19
19
  ## Input
20
20
  - Complete PRD draft assembled from all agents
21
- - Edge cases from edge-case-finder
22
- - Prioritized backlog from prioritizer
21
+ - Edge cases from prd-edge-case-finder
22
+ - Prioritized backlog from prd-prioritizer
23
23
 
24
24
  ## Output
25
25
  PRD review report:
@@ -6,6 +6,8 @@ agents: [researcher, edge-case-finder, requirements-writer, prioritizer, reviewe
6
6
 
7
7
  # Create-PRD Orchestrator
8
8
 
9
+ > 구조·섹션·네이밍은 vibe/rules/orchestrator-contract.md (SSOT) 를 따른다.
10
+
9
11
  ## Workflow
10
12
 
11
13
  ### Phase 1: Gather Context