@hongmaple0820/scale-engine 0.40.0 → 0.40.2

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 (47) hide show
  1. package/dist/api/cli.js +421 -26
  2. package/dist/api/cli.js.map +1 -1
  3. package/dist/api/doctor.js +1 -1
  4. package/dist/api/doctor.js.map +1 -1
  5. package/dist/bootstrap/DependencyBootstrap.d.ts +2 -0
  6. package/dist/bootstrap/DependencyBootstrap.js +250 -39
  7. package/dist/bootstrap/DependencyBootstrap.js.map +1 -1
  8. package/dist/bootstrap/DependencyBootstrapRenderer.js +2 -4
  9. package/dist/bootstrap/DependencyBootstrapRenderer.js.map +1 -1
  10. package/dist/capabilities/InstalledSkillsIntegration.js +29 -9
  11. package/dist/capabilities/InstalledSkillsIntegration.js.map +1 -1
  12. package/dist/context/ContextBudget.js +2 -2
  13. package/dist/core/GbrainRuntime.d.ts +25 -0
  14. package/dist/core/GbrainRuntime.js +270 -0
  15. package/dist/core/GbrainRuntime.js.map +1 -0
  16. package/dist/env/EnvironmentDoctor.js +221 -5
  17. package/dist/env/EnvironmentDoctor.js.map +1 -1
  18. package/dist/memory/MemoryProviders.js +38 -91
  19. package/dist/memory/MemoryProviders.js.map +1 -1
  20. package/dist/runtime/ModelUsageLedger.d.ts +53 -2
  21. package/dist/runtime/ModelUsageLedger.js +243 -39
  22. package/dist/runtime/ModelUsageLedger.js.map +1 -1
  23. package/dist/setup/SetupVerification.d.ts +42 -0
  24. package/dist/setup/SetupVerification.js +180 -0
  25. package/dist/setup/SetupVerification.js.map +1 -0
  26. package/dist/setup/SetupWizard.d.ts +3 -0
  27. package/dist/setup/SetupWizard.js +79 -19
  28. package/dist/setup/SetupWizard.js.map +1 -1
  29. package/dist/skills/SkillDoctor.js +2 -2
  30. package/dist/skills/SkillDoctor.js.map +1 -1
  31. package/dist/skills/SkillRepository.js +2 -2
  32. package/dist/skills/SkillRepository.js.map +1 -1
  33. package/dist/tools/ToolCapabilityRegistry.js +12 -2
  34. package/dist/tools/ToolCapabilityRegistry.js.map +1 -1
  35. package/dist/workflow/VerificationProfile.js +1 -1
  36. package/dist/workflow/VerificationProfile.js.map +1 -1
  37. package/docs/CONTEXT_BUDGET.md +12 -2
  38. package/docs/GOVERNANCE_DASHBOARD.md +7 -0
  39. package/docs/THIRD_PARTY_SKILLS.md +12 -4
  40. package/docs/start/README.md +2 -2
  41. package/docs/start/quickstart.md +54 -44
  42. package/docs/start/workflow-upgrade.md +8 -1
  43. package/package.json +3 -2
  44. package/scripts/workflow/lib/gbrain-runtime.mjs +185 -0
  45. package/scripts/workflow/lib/report-output.mjs +107 -0
  46. package/scripts/workflow/provider-rehearsal.mjs +129 -48
  47. package/scripts/workflow/setup-smoke.mjs +142 -8
@@ -1,4 +1,4 @@
1
- import { existsSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join, resolve } from 'node:path';
4
4
  import { execa, execaSync } from 'execa';
@@ -9,14 +9,22 @@ import { wrapShellCommandWithRtk } from '../tools/RtkRuntime.js';
9
9
  import { inspectToolCapabilities } from '../tools/ToolCapabilityRegistry.js';
10
10
  const UI_SKILL_INSTALLS = {
11
11
  'awesome-design-md': (ctx) => {
12
- const installDir = quotePath(join(ctx.homeDir, '.scale', 'vendor', 'awesome-design-md'));
13
- return `npx degit VoltAgent/awesome-design-md ${installDir}`;
12
+ const skillDir = quotePath(join(ctx.homeDir, '.agents', 'skills', 'awesome-design-md'));
13
+ const vendorDir = quotePath(join(ctx.homeDir, '.scale', 'vendor', 'awesome-design-md'));
14
+ return `install skill adapter ${skillDir} from VoltAgent/awesome-design-md into ${vendorDir}`;
15
+ },
16
+ 'ui-ux-pro-max': (ctx) => {
17
+ const skillDir = quotePath(join(ctx.homeDir, '.agents', 'skills', 'ui-ux-pro-max'));
18
+ const vendorDir = quotePath(join(ctx.homeDir, '.scale', 'vendor', 'ui-ux-pro-max'));
19
+ return `install skill adapter ${skillDir} from nextlevelbuilder/ui-ux-pro-max-skill into ${vendorDir}`;
14
20
  },
15
- 'ui-ux-pro-max': 'npx uipro-cli init --ai codex',
16
21
  'frontend-design': 'npx skills add anthropics/skills --skill frontend-design',
17
22
  };
18
23
  const RTK_INSTALL = 'cargo install --git https://github.com/rtk-ai/rtk';
24
+ const RTK_INIT = 'rtk init -g --codex';
19
25
  const CODEGRAPH_INSTALL = 'npm install -g @colbymchenry/codegraph';
26
+ const GRAPHIFY_CODEX_INSTALL = 'graphify install --platform codex';
27
+ const GRAPHIFY_HOOK_INSTALL = 'graphify hook install';
20
28
  const GRAPHIFY_UV_INSTALL = 'uv tool install graphify && graphify install --platform codex';
21
29
  const GRAPHIFY_PIPX_INSTALL = 'pipx install graphify && graphify install --platform codex';
22
30
  const GRAPHIFY_PIP_INSTALL = 'pip install graphify && graphify install --platform codex';
@@ -33,10 +41,20 @@ const DEPENDENCY_BOOTSTRAP_DEFINITIONS = [
33
41
  packs: ['ui'],
34
42
  source: 'https://github.com/VoltAgent/awesome-design-md',
35
43
  detectSkillId: 'awesome-design-md',
36
- detectPaths: ctx => [join(ctx.homeDir, '.scale', 'vendor', 'awesome-design-md', 'README.md')],
37
- prerequisites: ['npx'],
38
- manualReason: 'Requires npm/npx to sync the upstream DESIGN.md catalog that drives brand and visual-language decisions.',
39
- installCommand: ctx => ctx.commandExists('npx') ? UI_SKILL_INSTALLS['awesome-design-md'](ctx) : null,
44
+ prerequisites: ['git|npx'],
45
+ manualReason: 'Requires Git or npm/npx to sync the upstream DESIGN.md catalog and create a local SCALE skill adapter.',
46
+ installCommand: ctx => requirementSatisfied('git|npx', ctx.commandExists) ? UI_SKILL_INSTALLS['awesome-design-md'](ctx) : null,
47
+ install: ctx => installSkillAdapter(ctx, {
48
+ id: 'awesome-design-md',
49
+ sourceUrl: 'https://github.com/VoltAgent/awesome-design-md.git',
50
+ repoSpecifier: 'VoltAgent/awesome-design-md',
51
+ description: 'Brand, visual language, and DESIGN.md source skill for SCALE UI work.',
52
+ instructions: [
53
+ 'Use this skill before UI implementation when brand direction, visual language, DESIGN.md, color, typography, layout mood, or product identity must be defined.',
54
+ 'Read the upstream vendor repository under ~/.scale/vendor/awesome-design-md, then create or update the project DESIGN.md with concrete visual decisions.',
55
+ 'Do not use it as the accessibility or responsive QA gate; route those checks to ui-ux-pro-max.',
56
+ ],
57
+ }),
40
58
  },
41
59
  {
42
60
  id: 'ui-ux-pro-max',
@@ -45,9 +63,20 @@ const DEPENDENCY_BOOTSTRAP_DEFINITIONS = [
45
63
  packs: ['ui'],
46
64
  source: 'https://github.com/nextlevelbuilder/ui-ux-pro-max-skill',
47
65
  detectSkillId: 'ui-ux-pro-max',
48
- prerequisites: ['npx'],
49
- manualReason: 'Requires npm/npx so the official uipro-cli installer can configure the UX, state, accessibility, and responsive-review skill.',
50
- installCommand: ctx => ctx.commandExists('npx') ? UI_SKILL_INSTALLS['ui-ux-pro-max'] : null,
66
+ prerequisites: ['git|npx'],
67
+ manualReason: 'Requires Git or npm/npx to sync the upstream UX skill and create a local SCALE skill adapter.',
68
+ installCommand: ctx => requirementSatisfied('git|npx', ctx.commandExists) ? UI_SKILL_INSTALLS['ui-ux-pro-max'](ctx) : null,
69
+ install: ctx => installSkillAdapter(ctx, {
70
+ id: 'ui-ux-pro-max',
71
+ sourceUrl: 'https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git',
72
+ repoSpecifier: 'nextlevelbuilder/ui-ux-pro-max-skill',
73
+ description: 'UX flow, UI state, accessibility, and responsive acceptance skill for SCALE UI work.',
74
+ instructions: [
75
+ 'Use this skill after visual direction is selected to review UX flow, state coverage, empty/error/loading behavior, keyboard access, accessibility, and responsive acceptance.',
76
+ 'Read the upstream vendor repository under ~/.scale/vendor/ui-ux-pro-max for the current checklist and apply it as an acceptance gate.',
77
+ 'Do not replace awesome-design-md for brand or visual-language decisions.',
78
+ ],
79
+ }),
51
80
  },
52
81
  {
53
82
  id: 'frontend-design',
@@ -70,6 +99,7 @@ const DEPENDENCY_BOOTSTRAP_DEFINITIONS = [
70
99
  prerequisites: ['cargo'],
71
100
  manualReason: 'RTK currently installs from the official Rust toolchain path and needs Cargo on PATH.',
72
101
  installCommand: ctx => ctx.commandExists('cargo') ? RTK_INSTALL : null,
102
+ initializeCommands: () => [RTK_INIT],
73
103
  healthCheck: checkRtkHealth,
74
104
  },
75
105
  {
@@ -82,6 +112,7 @@ const DEPENDENCY_BOOTSTRAP_DEFINITIONS = [
82
112
  prerequisites: ['bun'],
83
113
  manualReason: 'The official standalone GBrain install needs Bun and then a configured brain (`gbrain init --pglite`) before cross-session recall is usable.',
84
114
  installCommand: ctx => buildGbrainInstallCommand(ctx),
115
+ initializeCommands: () => ['gbrain init --pglite --no-embedding'],
85
116
  healthCheck: checkGbrainHealth,
86
117
  },
87
118
  {
@@ -94,6 +125,11 @@ const DEPENDENCY_BOOTSTRAP_DEFINITIONS = [
94
125
  prerequisites: ['uv|pipx|pip|pip3|python|python3'],
95
126
  manualReason: 'Graphify requires Python 3.10+ and a supported installer; uv tool install is preferred to avoid polluting project virtualenvs.',
96
127
  installCommand: ctx => buildGraphifyInstallCommand(ctx),
128
+ initializeCommands: ctx => [
129
+ GRAPHIFY_CODEX_INSTALL,
130
+ GRAPHIFY_HOOK_INSTALL,
131
+ `graphify update ${quotePath(ctx.projectDir)} --no-cluster`,
132
+ ],
97
133
  healthCheck: checkGraphifyHealth,
98
134
  },
99
135
  {
@@ -106,6 +142,7 @@ const DEPENDENCY_BOOTSTRAP_DEFINITIONS = [
106
142
  prerequisites: ['npm'],
107
143
  manualReason: 'CodeGraph installs through npm and needs Node.js/npm available on PATH.',
108
144
  installCommand: ctx => ctx.commandExists('npm') ? CODEGRAPH_INSTALL : null,
145
+ initializeCommands: ctx => [codegraphInitCommand(ctx.projectDir)],
109
146
  healthCheck: checkCodeGraphHealth,
110
147
  },
111
148
  ];
@@ -115,7 +152,8 @@ export async function bootstrapDependencies(options = {}) {
115
152
  const homeDir = homedir();
116
153
  const packIds = normalizePackIds(options.packIds);
117
154
  const includeIds = unique((options.includeIds ?? []).map(value => value.trim()).filter(Boolean));
118
- const definitions = selectDefinitions(packIds, includeIds);
155
+ const onlyIds = unique((options.onlyIds ?? []).map(value => value.trim()).filter(Boolean));
156
+ const definitions = selectDefinitions(packIds, includeIds, onlyIds);
119
157
  const context = {
120
158
  projectDir,
121
159
  homeDir,
@@ -127,10 +165,12 @@ export async function bootstrapDependencies(options = {}) {
127
165
  for (const item of reports.filter(entry => entry.status === 'ready')) {
128
166
  if (!item.installCommand)
129
167
  continue;
130
- const result = await runInstallCommand(item.installCommand);
168
+ const definition = definitions.find(entry => entry.id === item.id);
169
+ const result = definition?.install
170
+ ? await definition.install(context)
171
+ : await runInstallCommand(item.installCommand, context.projectDir);
131
172
  item.output = result.output;
132
173
  item.error = result.error;
133
- const definition = definitions.find(entry => entry.id === item.id);
134
174
  const rechecked = definition ? inspectDefinition(definition, context) : item;
135
175
  item.installed = rechecked.installed;
136
176
  item.detectedBy = rechecked.detectedBy;
@@ -143,6 +183,7 @@ export async function bootstrapDependencies(options = {}) {
143
183
  if (!result.ok)
144
184
  item.installSupported = true;
145
185
  }
186
+ await reconcileDependencyBootstrapItems(definitions, reports, context);
146
187
  }
147
188
  const postActions = options.apply ? applyDependencyBootstrapPostActions(projectDir, scaleDir, reports) : [];
148
189
  const postChecks = options.apply
@@ -151,17 +192,22 @@ export async function bootstrapDependencies(options = {}) {
151
192
  return buildReport(projectDir, scaleDir, packIds, includeIds, Boolean(options.apply), runtimeChecks, reports, postActions, postChecks);
152
193
  }
153
194
  function buildReport(projectDir, scaleDir, packIds, includeIds, apply, runtimeChecks, items, postActions, postChecks) {
195
+ const nonBlockingItems = new Set(items
196
+ .filter(item => isNonBlockingOptionalItem(item, postChecks))
197
+ .map(item => item.id));
154
198
  const summary = {
155
199
  total: items.length,
156
200
  installed: items.filter(item => item.status === 'installed').length,
157
201
  ready: items.filter(item => item.status === 'ready').length,
158
202
  manualReview: items.filter(item => item.status === 'manual-review').length,
159
- needsInit: items.filter(item => item.status === 'needs-init').length,
203
+ needsInit: items.filter(item => item.status === 'needs-init' && !nonBlockingItems.has(item.id)).length,
160
204
  versionDrift: items.filter(item => item.status === 'version-drift').length,
161
205
  installedNow: items.filter(item => item.status === 'installed-now').length,
162
206
  failed: items.filter(item => item.status === 'failed').length,
163
207
  };
164
- const complete = items.length > 0 && items.every(item => item.status === 'installed' || item.status === 'installed-now');
208
+ const complete = items.length > 0 && items.every(item => item.status === 'installed'
209
+ || item.status === 'installed-now'
210
+ || nonBlockingItems.has(item.id));
165
211
  const postCheckSummary = {
166
212
  total: postChecks.length,
167
213
  passed: postChecks.filter(item => item.status === 'passed').length,
@@ -463,6 +509,12 @@ export function runDependencyBootstrapPostChecks(input, deps = {}) {
463
509
  const inspectCode = deps.inspectCode ?? inspectCodeIntelligence;
464
510
  const toolIds = unique(input.items.map(item => item.id).filter(id => ['awesome-design-md', 'ui-ux-pro-max', 'frontend-design', 'rtk', 'gbrain', 'codegraph', 'graphify'].includes(id)));
465
511
  const results = [];
512
+ const memoryReport = input.items.some(item => item.id === 'gbrain')
513
+ ? inspectMemory({ projectDir: input.projectDir, scaleDir: input.scaleDir })
514
+ : undefined;
515
+ const gbrain = memoryReport?.providers.find(provider => provider.id === 'gbrain');
516
+ const memoryFallback = memoryReport?.providers.find(provider => provider.kind === 'scale-local' && provider.available);
517
+ const softBlockedGbrain = Boolean(gbrain && !gbrain.available && memoryFallback);
466
518
  if (toolIds.length > 0) {
467
519
  const toolReport = inspectTools({
468
520
  projectDir: input.projectDir,
@@ -470,23 +522,31 @@ export function runDependencyBootstrapPostChecks(input, deps = {}) {
470
522
  toolIds,
471
523
  });
472
524
  const missing = toolReport.tools.filter(tool => !tool.installed).map(tool => tool.id);
525
+ const degraded = missing.filter(id => id === 'gbrain' && softBlockedGbrain);
526
+ const blockingMissing = missing.filter(id => !degraded.includes(id));
473
527
  results.push({
474
528
  id: 'tool-capabilities',
475
529
  label: 'Tool Doctor',
476
530
  command: `scale tool doctor --tools ${toolIds.join(',')} --json`,
477
- status: toolReport.ok ? 'passed' : 'failed',
478
- summary: `${toolReport.summary.installed}/${toolReport.summary.total} selected tools are available${missing.length > 0 ? `; missing: ${missing.join(', ')}` : ''}`,
531
+ status: blockingMissing.length > 0 ? 'failed' : degraded.length > 0 ? 'warn' : 'passed',
532
+ summary: [
533
+ `${toolReport.summary.installed}/${toolReport.summary.total} selected tools are available`,
534
+ blockingMissing.length > 0 ? `missing: ${blockingMissing.join(', ')}` : undefined,
535
+ degraded.length > 0 ? `degraded: ${degraded.join(', ')} (scale-local fallback active)` : undefined,
536
+ ].filter(Boolean).join('; '),
479
537
  details: {
480
538
  toolIds,
481
- missing,
539
+ missing: blockingMissing,
540
+ degraded,
541
+ fallbackProvider: memoryFallback?.id,
482
542
  },
483
543
  });
484
544
  }
485
- if (input.items.some(item => item.id === 'gbrain')) {
486
- const memoryReport = inspectMemory({ projectDir: input.projectDir, scaleDir: input.scaleDir });
487
- const gbrain = memoryReport.providers.find(provider => provider.id === 'gbrain');
545
+ if (memoryReport) {
488
546
  const warnings = [...memoryReport.warnings];
489
- const status = !gbrain?.available ? 'failed' : warnings.length > 0 ? 'warn' : 'passed';
547
+ if (softBlockedGbrain)
548
+ warnings.push(`gbrain is unavailable in this runtime; falling back to ${memoryFallback.id}`);
549
+ const status = !gbrain?.available ? (softBlockedGbrain ? 'warn' : 'failed') : warnings.length > 0 ? 'warn' : 'passed';
490
550
  results.push({
491
551
  id: 'memory-provider',
492
552
  label: 'Memory Provider',
@@ -498,6 +558,7 @@ export function runDependencyBootstrapPostChecks(input, deps = {}) {
498
558
  details: {
499
559
  warnings,
500
560
  gbrainReason: gbrain?.reason,
561
+ fallbackProvider: memoryFallback?.id,
501
562
  },
502
563
  });
503
564
  }
@@ -533,6 +594,13 @@ export function runDependencyBootstrapPostChecks(input, deps = {}) {
533
594
  }
534
595
  return results;
535
596
  }
597
+ function isNonBlockingOptionalItem(item, postChecks) {
598
+ if (item.id !== 'gbrain' || item.status !== 'needs-init')
599
+ return false;
600
+ return postChecks.some(check => check.id === 'memory-provider'
601
+ && check.status === 'warn'
602
+ && check.details?.fallbackProvider === 'scale-local');
603
+ }
536
604
  function inspectDefinition(definition, context) {
537
605
  const detectedBy = detectDefinition(definition, context.projectDir, context.homeDir);
538
606
  const installed = detectedBy !== 'missing';
@@ -607,7 +675,25 @@ function buildGbrainInstallCommand(context) {
607
675
  return null;
608
676
  return GBRAIN_INSTALL;
609
677
  }
610
- function checkRtkHealth() {
678
+ function codegraphInitCommand(projectDir) {
679
+ const command = `codegraph init -i ${quotePath(projectDir)}`;
680
+ return process.platform === 'win32' ? `cmd /d /c ${command}` : command;
681
+ }
682
+ export function hasCodexRtkInstructions(homeDir) {
683
+ const codexDir = join(homeDir, '.codex');
684
+ const rtkPath = join(codexDir, 'RTK.md');
685
+ const agentsPath = join(codexDir, 'AGENTS.md');
686
+ if (!existsSync(rtkPath) || !existsSync(agentsPath))
687
+ return false;
688
+ try {
689
+ const agents = readFileSync(agentsPath, 'utf-8');
690
+ return /RTK\.md/i.test(agents);
691
+ }
692
+ catch {
693
+ return false;
694
+ }
695
+ }
696
+ function checkRtkHealth(context) {
611
697
  const gain = runHealthCommand('rtk', ['gain']);
612
698
  if (!gain.ok) {
613
699
  return {
@@ -619,11 +705,15 @@ function checkRtkHealth() {
619
705
  }
620
706
  const output = `${gain.stdout}\n${gain.stderr}`;
621
707
  if (/no hook installed/i.test(output)) {
708
+ if (hasCodexRtkInstructions(context.homeDir)) {
709
+ return {
710
+ status: 'ok',
711
+ reason: 'rtk gain evidence is available; Codex mode uses RTK.md instructions instead of an automatic shell hook.',
712
+ };
713
+ }
622
714
  return {
623
- status: 'warn',
624
- bootstrapStatus: 'needs-init',
625
- reason: 'rtk CLI is installed, but the shell hook is not installed so command-output compression is not automatic yet.',
626
- nextCommands: ['rtk init -g --codex', 'rtk gain'],
715
+ status: 'ok',
716
+ reason: 'rtk gain evidence is available; automatic hook installation is not active, so SCALE should keep using the explicit `rtk <command>` proxy path.',
627
717
  };
628
718
  }
629
719
  return { status: 'ok', reason: 'rtk CLI and gain evidence are available.' };
@@ -633,7 +723,6 @@ function checkGbrainHealth() {
633
723
  if (health.available) {
634
724
  return {
635
725
  status: health.degraded ? 'warn' : 'ok',
636
- bootstrapStatus: health.degraded ? 'manual-review' : undefined,
637
726
  reason: health.degraded
638
727
  ? `${health.reason}; provider can still be used for read-only recall.`
639
728
  : 'gbrain doctor passed; provider can be used for default memory routing.',
@@ -648,9 +737,52 @@ function checkGbrainHealth() {
648
737
  nextCommands: ['gbrain init --pglite', 'gbrain doctor --json', 'scale memory provider status --json'],
649
738
  };
650
739
  }
651
- function checkGraphifyHealth() {
652
- const version = runHealthCommand('graphify', ['--version']);
653
- const hook = runHealthCommand('graphify', ['hook', 'status'], 10_000);
740
+ async function reconcileDependencyBootstrapItems(definitions, reports, context) {
741
+ const definitionsById = new Map(definitions.map(definition => [definition.id, definition]));
742
+ for (const item of reports) {
743
+ if (item.status !== 'needs-init' && item.status !== 'version-drift')
744
+ continue;
745
+ const definition = definitionsById.get(item.id);
746
+ const commands = definition?.initializeCommands?.(context) ?? [];
747
+ if (commands.length === 0)
748
+ continue;
749
+ const outputs = [item.output].filter(Boolean);
750
+ const errors = [item.error].filter(Boolean);
751
+ let ok = true;
752
+ for (const command of commands) {
753
+ const result = await runInstallCommand(command, context.projectDir);
754
+ if (result.output)
755
+ outputs.push(result.output);
756
+ if (result.error)
757
+ errors.push(result.error);
758
+ if (!result.ok) {
759
+ ok = false;
760
+ if (!result.error)
761
+ errors.push(`Initialization command failed: ${command}`);
762
+ break;
763
+ }
764
+ }
765
+ item.output = outputs.join('\n') || undefined;
766
+ item.error = errors.join('\n') || undefined;
767
+ if (!ok) {
768
+ const rechecked = definition ? inspectDefinition(definition, context) : item;
769
+ item.installed = rechecked.installed;
770
+ item.detectedBy = rechecked.detectedBy;
771
+ item.health = rechecked.health;
772
+ item.status = definition?.id === 'gbrain' && rechecked.installed ? rechecked.status : 'failed';
773
+ continue;
774
+ }
775
+ const rechecked = definition ? inspectDefinition(definition, context) : item;
776
+ item.installed = rechecked.installed;
777
+ item.detectedBy = rechecked.detectedBy;
778
+ item.health = rechecked.health;
779
+ item.status = rechecked.status === 'installed' ? 'installed-now' : rechecked.status;
780
+ }
781
+ }
782
+ function checkGraphifyHealth(context) {
783
+ const graphPath = join(context.projectDir, 'graphify-out', 'graph.json');
784
+ const version = runHealthCommand('graphify', ['--version'], 5_000, context.projectDir);
785
+ const hook = runHealthCommand('graphify', ['hook', 'status'], 10_000, context.projectDir);
654
786
  const output = `${version.stdout}\n${version.stderr}\n${hook.stdout}\n${hook.stderr}`;
655
787
  if (/skill.*version|package.*version|drift|outdated/i.test(output)) {
656
788
  return {
@@ -668,7 +800,15 @@ function checkGraphifyHealth() {
668
800
  nextCommands: ['graphify install --platform codex', 'graphify hook status'],
669
801
  };
670
802
  }
671
- return { status: 'ok', reason: 'graphify CLI and hook status are available.' };
803
+ if (!existsSync(graphPath)) {
804
+ return {
805
+ status: 'warn',
806
+ bootstrapStatus: 'needs-init',
807
+ reason: 'graphify CLI is installed but graphify-out/graph.json is not present yet.',
808
+ nextCommands: [`graphify update ${quotePath(context.projectDir)} --no-cluster`, 'scale codegraph status --json'],
809
+ };
810
+ }
811
+ return { status: 'ok', reason: 'graphify CLI, hooks, and graph artifact are available.' };
672
812
  }
673
813
  function checkCodeGraphHealth(context) {
674
814
  const indexPath = join(context.projectDir, '.codegraph');
@@ -691,11 +831,12 @@ function checkCodeGraphHealth(context) {
691
831
  }
692
832
  return { status: 'ok', reason: 'codegraph CLI and project index are available.' };
693
833
  }
694
- function runHealthCommand(command, args, timeout = 5_000) {
834
+ function runHealthCommand(command, args, timeout = 5_000, cwd) {
695
835
  try {
696
836
  const result = execaSync(command, args, {
697
837
  reject: false,
698
838
  timeout,
839
+ cwd,
699
840
  });
700
841
  return {
701
842
  ok: (result.exitCode ?? 1) === 0,
@@ -718,17 +859,85 @@ function firstLine(value) {
718
859
  function quotePath(path) {
719
860
  return `"${path.replace(/"/g, '\\"')}"`;
720
861
  }
721
- async function runInstallCommand(shellCommand) {
862
+ async function runInstallCommand(shellCommand, cwd) {
722
863
  const wrapped = wrapShellCommandWithRtk(shellCommand);
723
864
  const result = wrapped
724
- ? await execa(wrapped.command, wrapped.args, { reject: false, timeout: 300_000, all: false })
725
- : await execa(shellCommand, { shell: true, reject: false, timeout: 300_000, all: false });
865
+ ? await execa(wrapped.command, wrapped.args, { reject: false, timeout: 300_000, all: false, cwd })
866
+ : await execa(shellCommand, { shell: true, reject: false, timeout: 300_000, all: false, cwd });
726
867
  return {
727
868
  ok: (result.exitCode ?? 1) === 0,
728
869
  output: result.stdout ?? '',
729
870
  error: result.stderr ?? '',
730
871
  };
731
872
  }
873
+ async function installSkillAdapter(context, input) {
874
+ const vendorDir = join(context.homeDir, '.scale', 'vendor', input.id);
875
+ const skillDir = join(context.homeDir, '.agents', 'skills', input.id);
876
+ const outputs = [];
877
+ const errors = [];
878
+ mkdirSync(join(context.homeDir, '.scale', 'vendor'), { recursive: true });
879
+ mkdirSync(skillDir, { recursive: true });
880
+ if (!existsSync(vendorDir) || isEmptyDirectory(vendorDir)) {
881
+ if (context.commandExists('git')) {
882
+ const clone = await execa('git', ['clone', '--depth', '1', input.sourceUrl, vendorDir], { reject: false });
883
+ outputs.push(clone.stdout ?? '');
884
+ errors.push(clone.stderr ?? '');
885
+ if ((clone.exitCode ?? 1) !== 0)
886
+ return { ok: false, output: outputs.join('\n'), error: errors.join('\n') };
887
+ }
888
+ else if (context.commandExists('npx')) {
889
+ const degit = await execa('npx', ['degit', input.repoSpecifier, vendorDir], { reject: false });
890
+ outputs.push(degit.stdout ?? '');
891
+ errors.push(degit.stderr ?? '');
892
+ if ((degit.exitCode ?? 1) !== 0)
893
+ return { ok: false, output: outputs.join('\n'), error: errors.join('\n') };
894
+ }
895
+ else {
896
+ return { ok: false, error: 'Git or npx is required to install third-party skills.' };
897
+ }
898
+ }
899
+ else if (existsSync(join(vendorDir, '.git')) && context.commandExists('git')) {
900
+ const pull = await execa('git', ['-C', vendorDir, 'pull', '--ff-only'], { reject: false });
901
+ outputs.push(pull.stdout ?? '');
902
+ errors.push(pull.stderr ?? '');
903
+ if ((pull.exitCode ?? 1) !== 0)
904
+ return { ok: false, output: outputs.join('\n'), error: errors.join('\n') };
905
+ }
906
+ else {
907
+ outputs.push(`Reused existing vendor directory: ${vendorDir}`);
908
+ }
909
+ writeFileSync(join(skillDir, 'SKILL.md'), renderSkillAdapter(input, vendorDir), 'utf-8');
910
+ outputs.push(`Wrote skill adapter: ${join(skillDir, 'SKILL.md')}`);
911
+ return { ok: true, output: outputs.filter(Boolean).join('\n'), error: errors.filter(Boolean).join('\n') };
912
+ }
913
+ function isEmptyDirectory(path) {
914
+ return existsSync(path) && readdirSync(path).length === 0;
915
+ }
916
+ function renderSkillAdapter(input, vendorDir) {
917
+ const lines = [
918
+ '---',
919
+ `name: ${input.id}`,
920
+ `description: ${input.description}`,
921
+ '---',
922
+ '',
923
+ `# ${input.id}`,
924
+ '',
925
+ `Source: ${input.sourceUrl}`,
926
+ `Vendor path: ${vendorDir}`,
927
+ '',
928
+ '## When To Use',
929
+ '',
930
+ ...input.instructions.map(item => `- ${item}`),
931
+ '',
932
+ '## Operating Rules',
933
+ '',
934
+ '- Prefer concrete project artifacts over generic advice.',
935
+ '- If the vendor directory is missing or outdated, run `scale setup --pack ui --apply` again.',
936
+ '- Keep generated project outputs, such as DESIGN.md, inside the target project rather than this skill directory.',
937
+ '',
938
+ ];
939
+ return `${lines.join('\n')}\n`;
940
+ }
732
941
  export function applyDependencyBootstrapPostActions(projectDir, scaleDir, items, deps = {}) {
733
942
  const writeMemoryConfig = deps.writeMemoryConfig ?? writeMemoryProvidersConfig;
734
943
  const switchMemoryProvider = deps.switchMemoryProvider ?? useMemoryProvider;
@@ -751,10 +960,12 @@ export function applyDependencyBootstrapPostActions(projectDir, scaleDir, items,
751
960
  }
752
961
  return actions;
753
962
  }
754
- function selectDefinitions(packIds, includeIds) {
963
+ function selectDefinitions(packIds, includeIds, onlyIds = []) {
755
964
  const selectedPacks = new Set(packIds.includes('full') ? ['ui', 'memory', 'knowledge', 'external-cli'] : packIds);
756
965
  const selectedIds = new Set(includeIds);
757
- return DEPENDENCY_BOOTSTRAP_DEFINITIONS.filter(definition => definition.packs.some(pack => selectedPacks.has(pack)) || selectedIds.has(definition.id));
966
+ const only = new Set(onlyIds);
967
+ return DEPENDENCY_BOOTSTRAP_DEFINITIONS.filter(definition => (only.size === 0 || only.has(definition.id)) &&
968
+ (definition.packs.some(pack => selectedPacks.has(pack)) || selectedIds.has(definition.id)));
758
969
  }
759
970
  function buildPostCheckCommands(packIds, items) {
760
971
  const commands = new Set();