@vibe-forge/workspace-assets 2.0.0 → 2.0.1

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.
@@ -11,9 +11,10 @@ import type {
11
11
  } from '@vibe-forge/types'
12
12
 
13
13
  import { resolveNativeSkillDiagnosticReason, supportsNativeProjectSkills } from './adapter-capabilities'
14
- import { resolveSelectedMcpNames, resolveSelectedSkillAssets } from './selection-internal'
14
+ import { resolveWorkspaceAssetSource } from './asset-source'
15
+ import { resolveSelectedMcpNames, resolveSelectedSkillAssetsWithDependencies } from './selection-internal'
15
16
 
16
- export function buildAdapterAssetPlan(params: {
17
+ export async function buildAdapterAssetPlan(params: {
17
18
  adapter: WorkspaceAssetAdapter
18
19
  bundle: WorkspaceAssetBundle
19
20
  options: {
@@ -21,17 +22,24 @@ export function buildAdapterAssetPlan(params: {
21
22
  skills?: WorkspaceSkillSelection
22
23
  promptAssetIds?: string[]
23
24
  }
24
- }): AdapterAssetPlan {
25
+ }): Promise<AdapterAssetPlan> {
25
26
  const diagnostics: AssetDiagnostic[] = []
26
-
27
- for (const assetId of params.options.promptAssetIds ?? []) {
28
- const asset = params.bundle.assets.find(item => item.id === assetId)
29
- if (asset == null || asset.kind === 'mcpServer') continue
27
+ const pushDiagnostic = (
28
+ asset: Parameters<typeof resolveWorkspaceAssetSource>[0] & {
29
+ id: string
30
+ packageId?: string
31
+ scope?: string
32
+ instancePath?: string
33
+ taskOverlaySource?: string
34
+ },
35
+ diagnostic: Pick<AssetDiagnostic, 'adapter' | 'status' | 'reason'>
36
+ ) => {
30
37
  diagnostics.push({
31
- assetId,
32
- adapter: params.adapter,
33
- status: 'prompt',
34
- reason: 'Mapped into the generated system prompt.',
38
+ assetId: asset.id,
39
+ adapter: diagnostic.adapter,
40
+ status: diagnostic.status,
41
+ reason: diagnostic.reason,
42
+ source: resolveWorkspaceAssetSource(asset),
35
43
  packageId: asset.packageId,
36
44
  scope: asset.scope,
37
45
  instancePath: asset.instancePath,
@@ -41,6 +49,16 @@ export function buildAdapterAssetPlan(params: {
41
49
  })
42
50
  }
43
51
 
52
+ for (const assetId of params.options.promptAssetIds ?? []) {
53
+ const asset = params.bundle.assets.find(item => item.id === assetId)
54
+ if (asset == null || asset.kind === 'mcpServer') continue
55
+ pushDiagnostic(asset, {
56
+ adapter: params.adapter,
57
+ status: 'prompt',
58
+ reason: 'Mapped into the generated system prompt.'
59
+ })
60
+ }
61
+
44
62
  const selectedMcpNames = resolveSelectedMcpNames(params.bundle, params.options.mcpServers)
45
63
  const mcpServers = Object.fromEntries(
46
64
  selectedMcpNames.map(name => [name, params.bundle.mcpServers[name].payload.config])
@@ -48,25 +66,17 @@ export function buildAdapterAssetPlan(params: {
48
66
 
49
67
  selectedMcpNames.forEach((name) => {
50
68
  const asset = params.bundle.mcpServers[name]
51
- diagnostics.push({
52
- assetId: asset.id,
69
+ pushDiagnostic(asset, {
53
70
  adapter: params.adapter,
54
71
  status: params.adapter === 'claude-code' ? 'native' : 'translated',
55
72
  reason: params.adapter === 'claude-code'
56
73
  ? 'Mapped into adapter MCP settings.'
57
- : 'Translated into adapter-specific MCP configuration.',
58
- packageId: asset.packageId,
59
- scope: asset.scope,
60
- instancePath: asset.instancePath,
61
- origin: asset.origin,
62
- resolvedBy: asset.resolvedBy,
63
- taskOverlaySource: asset.taskOverlaySource
74
+ : 'Translated into adapter-specific MCP configuration.'
64
75
  })
65
76
  })
66
77
 
67
78
  params.bundle.hookPlugins.forEach((asset) => {
68
- diagnostics.push({
69
- assetId: asset.id,
79
+ pushDiagnostic(asset, {
70
80
  adapter: params.adapter,
71
81
  status: params.adapter === 'copilot' ? 'translated' : 'native',
72
82
  reason: params.adapter === 'claude-code'
@@ -79,80 +89,46 @@ export function buildAdapterAssetPlan(params: {
79
89
  ? 'Handled by the Vibe Forge task hook bridge.'
80
90
  : params.adapter === 'kimi'
81
91
  ? 'Mapped into the Kimi native hooks bridge.'
82
- : 'Mapped into the OpenCode native hooks bridge.',
83
- packageId: asset.packageId,
84
- scope: asset.scope,
85
- instancePath: asset.instancePath,
86
- origin: asset.origin,
87
- resolvedBy: asset.resolvedBy,
88
- taskOverlaySource: asset.taskOverlaySource
92
+ : 'Mapped into the OpenCode native hooks bridge.'
89
93
  })
90
94
  })
91
95
 
92
- const selectedSkillAssets = resolveSelectedSkillAssets(params.bundle.skills, params.options.skills)
96
+ const selectedSkillAssets = await resolveSelectedSkillAssetsWithDependencies(params.bundle, params.options.skills)
93
97
  if (supportsNativeProjectSkills(params.adapter)) {
94
98
  selectedSkillAssets.forEach((asset) => {
95
- diagnostics.push({
96
- assetId: asset.id,
99
+ pushDiagnostic(asset, {
97
100
  adapter: params.adapter,
98
101
  status: 'native',
99
- reason: resolveNativeSkillDiagnosticReason(params.adapter),
100
- packageId: asset.packageId,
101
- scope: asset.scope,
102
- instancePath: asset.instancePath,
103
- origin: asset.origin,
104
- resolvedBy: asset.resolvedBy,
105
- taskOverlaySource: asset.taskOverlaySource
102
+ reason: resolveNativeSkillDiagnosticReason(params.adapter)
106
103
  })
107
104
  })
108
105
  }
109
106
  if (params.adapter === 'opencode') {
110
107
  params.bundle.opencodeOverlayAssets.forEach((asset) => {
111
- diagnostics.push({
112
- assetId: asset.id,
108
+ pushDiagnostic(asset, {
113
109
  adapter: params.adapter,
114
110
  status: 'native',
115
- reason: 'Mirrored into OPENCODE_CONFIG_DIR as a native OpenCode asset.',
116
- packageId: asset.packageId,
117
- scope: asset.scope,
118
- instancePath: asset.instancePath,
119
- origin: asset.origin,
120
- resolvedBy: asset.resolvedBy,
121
- taskOverlaySource: asset.taskOverlaySource
111
+ reason: 'Mirrored into OPENCODE_CONFIG_DIR as a native OpenCode asset.'
122
112
  })
123
113
  })
124
114
  } else if (params.adapter === 'codex' || params.adapter === 'copilot' || params.adapter === 'kimi') {
125
115
  params.bundle.opencodeOverlayAssets.forEach((asset) => {
126
- diagnostics.push({
127
- assetId: asset.id,
116
+ pushDiagnostic(asset, {
128
117
  adapter: params.adapter,
129
118
  status: 'skipped',
130
119
  reason: params.adapter === 'codex'
131
120
  ? 'No stable native Codex mapping exists for this asset kind in V1.'
132
121
  : params.adapter === 'copilot'
133
122
  ? 'No stable native Copilot mapping exists for this asset kind in V1.'
134
- : 'No stable native Kimi mapping exists for this asset kind in V1.',
135
- packageId: asset.packageId,
136
- scope: asset.scope,
137
- instancePath: asset.instancePath,
138
- origin: asset.origin,
139
- resolvedBy: asset.resolvedBy,
140
- taskOverlaySource: asset.taskOverlaySource
123
+ : 'No stable native Kimi mapping exists for this asset kind in V1.'
141
124
  })
142
125
  })
143
126
  } else if (params.adapter === 'gemini') {
144
127
  params.bundle.opencodeOverlayAssets.forEach((asset) => {
145
- diagnostics.push({
146
- assetId: asset.id,
128
+ pushDiagnostic(asset, {
147
129
  adapter: params.adapter,
148
130
  status: 'skipped',
149
- reason: 'No stable native Gemini mapping exists for this asset kind in V1.',
150
- packageId: asset.packageId,
151
- scope: asset.scope,
152
- instancePath: asset.instancePath,
153
- origin: asset.origin,
154
- resolvedBy: asset.resolvedBy,
155
- taskOverlaySource: asset.taskOverlaySource
131
+ reason: 'No stable native Gemini mapping exists for this asset kind in V1.'
156
132
  })
157
133
  })
158
134
  }
@@ -0,0 +1,13 @@
1
+ import type { DefinitionSource, WorkspaceAsset } from '@vibe-forge/types'
2
+
3
+ import { HOME_BRIDGE_RESOLVED_BY } from './home-bridge'
4
+
5
+ export const resolveWorkspaceAssetSource = (
6
+ asset: Pick<WorkspaceAsset, 'origin' | 'resolvedBy'>
7
+ ): DefinitionSource => (
8
+ asset.resolvedBy === HOME_BRIDGE_RESOLVED_BY
9
+ ? 'home'
10
+ : asset.origin === 'plugin'
11
+ ? 'plugin'
12
+ : 'project'
13
+ )
@@ -1,5 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises'
2
- import { basename, dirname, extname, resolve } from 'node:path'
2
+ import { basename, dirname, extname, isAbsolute, resolve } from 'node:path'
3
3
  import process from 'node:process'
4
4
 
5
5
  import {
@@ -9,7 +9,12 @@ import {
9
9
  resolveDefaultVibeForgeMcpServerConfig
10
10
  } from '@vibe-forge/config'
11
11
  import type { Config, Definition, Entity, PluginConfig, WorkspaceAsset, WorkspaceAssetKind } from '@vibe-forge/types'
12
- import { resolveProjectAiBaseDir, resolveProjectAiEntitiesDir, resolveRelativePath } from '@vibe-forge/utils'
12
+ import {
13
+ isLegacySkillsConfig,
14
+ resolveProjectAiBaseDir,
15
+ resolveProjectAiEntitiesDir,
16
+ resolveRelativePath
17
+ } from '@vibe-forge/utils'
13
18
  import { listManagedPluginInstalls, toManagedPluginConfig } from '@vibe-forge/utils/managed-plugin'
14
19
  import {
15
20
  flattenPluginInstances,
@@ -28,10 +33,14 @@ import {
28
33
  resolveSkillIdentifier,
29
34
  resolveSpecIdentifier
30
35
  } from '@vibe-forge/definition-core'
36
+ import { ensureConfiguredProjectSkills } from './configured-skills'
37
+ import { HOME_BRIDGE_RESOLVED_BY } from './home-bridge'
38
+ import { resolveConfiguredWorkspaceAssets } from './workspaces'
31
39
 
32
40
  type DocumentAssetKind = Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>
33
41
  type OpenCodeOverlayKind = Extract<WorkspaceAssetKind, 'agent' | 'command' | 'mode' | 'nativePlugin'>
34
42
  type OpenCodeOverlayAsset<TKind extends OpenCodeOverlayKind> = Extract<WorkspaceAsset, { kind: TKind }>
43
+ type SkillAsset = Extract<WorkspaceAsset, { kind: 'skill' }>
35
44
 
36
45
  type DocumentAsset<TDefinition> = Extract<WorkspaceAsset, { kind: DocumentAssetKind }> & {
37
46
  payload: {
@@ -50,10 +59,147 @@ const isRecord = (value: unknown): value is Record<string, unknown> => (
50
59
  value != null && typeof value === 'object' && !Array.isArray(value)
51
60
  )
52
61
 
62
+ const ENTITY_DIRECTORY_ENTRY_FILES = new Set(['readme.md', 'index.json'])
63
+ const DEFAULT_HOME_SKILL_ROOTS = [
64
+ '~/.agents/skills',
65
+ '~/.claude/skills',
66
+ '~/.config/opencode/skills',
67
+ '~/.gemini/skills'
68
+ ] as const
69
+
70
+ const DEFAULT_ENTITY_PROMPT_FILE_SECTIONS = [
71
+ {
72
+ heading: 'Introduction',
73
+ fileNames: ['INTRODUCTION.md', 'introduction.md', '介绍.md']
74
+ },
75
+ {
76
+ heading: 'Personality',
77
+ fileNames: ['PERSONALITY.md', 'personality.md', '人格.md']
78
+ },
79
+ {
80
+ heading: 'Memory',
81
+ fileNames: ['MEMORY.md', 'memory.md', '记忆.md']
82
+ }
83
+ ] as const
84
+
85
+ const isMissingFileError = (error: unknown) => (
86
+ error != null &&
87
+ typeof error === 'object' &&
88
+ 'code' in error &&
89
+ (error as { code?: unknown }).code === 'ENOENT'
90
+ )
91
+
92
+ const readOptionalMarkdownBody = async (path: string) => {
93
+ try {
94
+ const content = await readFile(path, 'utf-8')
95
+ return fm<Record<string, never>>(content).body.trim()
96
+ } catch (err) {
97
+ if (isMissingFileError(err)) return undefined
98
+ throw err
99
+ }
100
+ }
101
+
102
+ const loadDefaultEntityPromptSection = async (
103
+ entityDir: string,
104
+ section: (typeof DEFAULT_ENTITY_PROMPT_FILE_SECTIONS)[number]
105
+ ) => {
106
+ for (const fileName of section.fileNames) {
107
+ const body = await readOptionalMarkdownBody(resolve(entityDir, fileName))
108
+ if (body == null || body === '') continue
109
+
110
+ return `## ${section.heading}\n\n${body}`
111
+ }
112
+
113
+ return undefined
114
+ }
115
+
116
+ const appendDefaultEntityPromptFiles = async (path: string, body: string) => {
117
+ if (!ENTITY_DIRECTORY_ENTRY_FILES.has(basename(path).toLowerCase())) return body
118
+
119
+ const sections = await Promise.all(
120
+ DEFAULT_ENTITY_PROMPT_FILE_SECTIONS.map(section => loadDefaultEntityPromptSection(dirname(path), section))
121
+ )
122
+
123
+ return [
124
+ body.trim(),
125
+ ...sections
126
+ ]
127
+ .filter((section): section is string => section != null && section !== '')
128
+ .join('\n\n')
129
+ }
130
+
53
131
  const resolveDisplayName = (name: string, scope?: string) => (
54
132
  scope != null && scope.trim() !== '' ? `${scope}/${name}` : name
55
133
  )
56
134
 
135
+ const toStringList = (value: string | string[] | undefined) => {
136
+ if (typeof value === 'string' && value.trim() !== '') {
137
+ return [value.trim()]
138
+ }
139
+
140
+ if (!Array.isArray(value)) return [] as string[]
141
+
142
+ return value
143
+ .filter((item): item is string => typeof item === 'string' && item.trim() !== '')
144
+ .map(item => item.trim())
145
+ }
146
+
147
+ const resolveRealHomeDir = (env: NodeJS.ProcessEnv) => {
148
+ const value = env.__VF_PROJECT_REAL_HOME__?.trim() || env.HOME?.trim()
149
+ if (value == null || value === '') return undefined
150
+ return resolve(value)
151
+ }
152
+
153
+ const warnInvalidHomeSkillRoot = (root: string) => {
154
+ console.warn(
155
+ `[vibe-forge] Ignoring invalid skills.homeBridge root "${root}". ` +
156
+ 'Use an absolute path or a path starting with "~".'
157
+ )
158
+ }
159
+
160
+ const resolveHomeBridgeConfig = (configs: [Config?, Config?]) => {
161
+ const [config, userConfig] = configs
162
+ const projectHomeBridge = isLegacySkillsConfig(config?.skills) ? config.skills.homeBridge : undefined
163
+ const userHomeBridge = isLegacySkillsConfig(userConfig?.skills) ? userConfig.skills.homeBridge : undefined
164
+
165
+ return {
166
+ enabled: userHomeBridge?.enabled ?? projectHomeBridge?.enabled ?? true,
167
+ roots: toStringList(userHomeBridge?.roots ?? projectHomeBridge?.roots)
168
+ }
169
+ }
170
+
171
+ const resolveHomeSkillRoots = (configs: [Config?, Config?], env: NodeJS.ProcessEnv = process.env) => {
172
+ const homeBridge = resolveHomeBridgeConfig(configs)
173
+ if (homeBridge.enabled === false) return [] as string[]
174
+
175
+ const realHome = resolveRealHomeDir(env)
176
+ if (realHome == null) return [] as string[]
177
+
178
+ const rawRoots = homeBridge.roots.length > 0 ? homeBridge.roots : Array.from(DEFAULT_HOME_SKILL_ROOTS)
179
+ const roots: string[] = []
180
+ const seen = new Set<string>()
181
+
182
+ for (const rawRoot of rawRoots) {
183
+ let resolvedRoot: string | undefined
184
+
185
+ if (rawRoot === '~') {
186
+ resolvedRoot = realHome
187
+ } else if (rawRoot.startsWith('~/')) {
188
+ resolvedRoot = resolve(realHome, rawRoot.slice(2))
189
+ } else if (isAbsolute(rawRoot)) {
190
+ resolvedRoot = resolve(rawRoot)
191
+ } else if (homeBridge.roots.length > 0) {
192
+ warnInvalidHomeSkillRoot(rawRoot)
193
+ }
194
+
195
+ if (resolvedRoot == null || seen.has(resolvedRoot)) continue
196
+ seen.add(resolvedRoot)
197
+ roots.push(resolvedRoot)
198
+ }
199
+
200
+ return roots
201
+ }
202
+
57
203
  const loadWorkspaceConfig = async (cwd: string) => (
58
204
  loadConfig({
59
205
  cwd,
@@ -73,6 +219,15 @@ const parseFrontmatterDocument = async <TDefinition extends object>(
73
219
  }
74
220
  }
75
221
 
222
+ const parseEntityMarkdownDocument = async (path: string): Promise<Definition<Entity>> => {
223
+ const definition = await parseFrontmatterDocument<Entity>(path)
224
+
225
+ return {
226
+ ...definition,
227
+ body: await appendDefaultEntityPromptFiles(path, definition.body)
228
+ }
229
+ }
230
+
76
231
  const parseEntityIndexJson = async (path: string): Promise<Definition<Entity>> => {
77
232
  const raw = JSON.parse(await readFile(path, 'utf-8')) as Record<string, unknown>
78
233
  const promptPath = typeof raw.promptPath === 'string'
@@ -86,7 +241,7 @@ const parseEntityIndexJson = async (path: string): Promise<Definition<Entity>> =
86
241
 
87
242
  return {
88
243
  path,
89
- body: prompt,
244
+ body: await appendDefaultEntityPromptFiles(path, prompt),
90
245
  attributes: raw as Entity
91
246
  }
92
247
  }
@@ -109,6 +264,7 @@ const createDocumentAsset = <
109
264
  origin: 'workspace' | 'plugin'
110
265
  scope?: string
111
266
  instance?: ResolvedPluginInstance
267
+ resolvedBy?: string
112
268
  }) => {
113
269
  const name = ({
114
270
  rule: resolveDocumentName,
@@ -130,7 +286,7 @@ const createDocumentAsset = <
130
286
  sourcePath: params.definition.path,
131
287
  instancePath: params.instance?.instancePath,
132
288
  packageId: params.instance?.packageId,
133
- resolvedBy: params.instance?.resolvedBy,
289
+ resolvedBy: params.resolvedBy ?? params.instance?.resolvedBy,
134
290
  taskOverlaySource: params.instance?.overlaySource,
135
291
  payload: {
136
292
  definition: params.definition
@@ -238,6 +394,19 @@ const scanWorkspaceDocuments = async (cwd: string) => {
238
394
  }
239
395
  }
240
396
 
397
+ const scanHomeSkillDocuments = async (configs: [Config?, Config?]) => {
398
+ const roots = resolveHomeSkillRoots(configs)
399
+ if (roots.length === 0) return [] as string[]
400
+
401
+ const scans = await Promise.all(
402
+ roots.map(async root => (
403
+ await glob(['*/SKILL.md'], { cwd: root, absolute: true }).catch(() => [] as string[])
404
+ ))
405
+ )
406
+
407
+ return scans.flatMap(paths => [...paths].sort((left, right) => left.localeCompare(right)))
408
+ }
409
+
241
410
  const scanInstanceDocuments = async (instance: ResolvedPluginInstance) => {
242
411
  const assets = instance.manifest?.assets
243
412
  const resolveAssetRoot = (dir: string | undefined, fallback: string) => resolve(instance.rootDir, dir ?? fallback)
@@ -326,6 +495,24 @@ const assertNoDocumentConflicts = (
326
495
  }
327
496
  }
328
497
 
498
+ const mergeSkillAssets = (assets: SkillAsset[]) => {
499
+ const directAssets = assets.filter(asset => asset.resolvedBy !== HOME_BRIDGE_RESOLVED_BY)
500
+ const bridgedAssets = assets.filter(asset => asset.resolvedBy === HOME_BRIDGE_RESOLVED_BY)
501
+
502
+ assertNoDocumentConflicts(directAssets)
503
+
504
+ const seen = new Set(directAssets.map(asset => asset.displayName))
505
+ const merged = [...directAssets]
506
+
507
+ for (const asset of bridgedAssets) {
508
+ if (seen.has(asset.displayName)) continue
509
+ seen.add(asset.displayName)
510
+ merged.push(asset)
511
+ }
512
+
513
+ return merged
514
+ }
515
+
329
516
  const assertNoMcpConflicts = (
330
517
  assets: Array<Extract<WorkspaceAsset, { kind: 'mcpServer' }>>
331
518
  ) => {
@@ -345,9 +532,12 @@ export async function collectWorkspaceAssets(params: {
345
532
  plugins?: PluginConfig
346
533
  overlaySource?: string
347
534
  includeManagedPlugins?: boolean
535
+ syncConfiguredSkills?: boolean
536
+ updateConfiguredSkills?: boolean
348
537
  useDefaultVibeForgeMcpServer?: boolean
349
538
  }): Promise<{
350
539
  assets: WorkspaceAsset[]
540
+ configs: [Config?, Config?]
351
541
  defaultExcludeMcpServers: string[]
352
542
  defaultIncludeMcpServers: string[]
353
543
  entities: Array<Extract<WorkspaceAsset, { kind: 'entity' }>>
@@ -359,8 +549,16 @@ export async function collectWorkspaceAssets(params: {
359
549
  rules: Array<Extract<WorkspaceAsset, { kind: 'rule' }>>
360
550
  skills: Array<Extract<WorkspaceAsset, { kind: 'skill' }>>
361
551
  specs: Array<Extract<WorkspaceAsset, { kind: 'spec' }>>
552
+ workspaces: Array<Extract<WorkspaceAsset, { kind: 'workspace' }>>
362
553
  }> {
363
554
  const [config, userConfig] = params.configs ?? await loadWorkspaceConfig(params.cwd)
555
+ if (params.syncConfiguredSkills === true) {
556
+ await ensureConfiguredProjectSkills({
557
+ configs: [config, userConfig],
558
+ updateInstalledSkills: params.updateConfiguredSkills,
559
+ workspaceFolder: params.cwd
560
+ })
561
+ }
364
562
  const managedPluginConfigs = params.includeManagedPlugins === false
365
563
  ? undefined
366
564
  : toManagedPluginConfig(await listManagedPluginInstalls(params.cwd))
@@ -374,7 +572,10 @@ export async function collectWorkspaceAssets(params: {
374
572
  overlaySource: params.overlaySource
375
573
  })
376
574
 
377
- const localScan = await scanWorkspaceDocuments(params.cwd)
575
+ const [localScan, homeSkillPaths] = await Promise.all([
576
+ scanWorkspaceDocuments(params.cwd),
577
+ scanHomeSkillDocuments([config, userConfig])
578
+ ])
378
579
  const flattenedPluginInstances = flattenPluginInstances(pluginInstances)
379
580
  const pluginScans = await Promise.all(flattenedPluginInstances.map(instance => scanInstanceDocuments(instance)))
380
581
  const pluginOverlayScans = await Promise.all(
@@ -382,35 +583,43 @@ export async function collectWorkspaceAssets(params: {
382
583
  )
383
584
 
384
585
  const assets: WorkspaceAsset[] = []
586
+ const skillAssets: SkillAsset[] = []
385
587
 
386
588
  const pushDocumentAssets = async <TKind extends DocumentAssetKind>(
387
589
  kind: TKind,
388
590
  paths: string[],
389
591
  origin: 'workspace' | 'plugin',
390
592
  instance?: ResolvedPluginInstance,
391
- parser?: (path: string) => Promise<any>
593
+ parser?: (path: string) => Promise<any>,
594
+ resolvedBy?: string
392
595
  ) => {
393
596
  const definitions = await Promise.all(paths.map(path => (
394
597
  parser != null ? parser(path) : parseFrontmatterDocument(path)
395
598
  )))
396
- assets.push(
397
- ...definitions.map(definition =>
398
- createDocumentAsset({
399
- cwd: params.cwd,
400
- kind,
401
- definition,
402
- origin,
403
- scope: instance?.scope,
404
- instance
405
- })
406
- )
599
+ const createdAssets = definitions.map(definition =>
600
+ createDocumentAsset({
601
+ cwd: params.cwd,
602
+ kind,
603
+ definition,
604
+ origin,
605
+ scope: instance?.scope,
606
+ instance,
607
+ resolvedBy
608
+ })
407
609
  )
610
+
611
+ if (kind === 'skill') {
612
+ skillAssets.push(...createdAssets as SkillAsset[])
613
+ return
614
+ }
615
+
616
+ assets.push(...createdAssets)
408
617
  }
409
618
 
410
619
  await pushDocumentAssets('rule', localScan.rulePaths, 'workspace')
411
620
  await pushDocumentAssets('skill', localScan.skillPaths, 'workspace')
412
621
  await pushDocumentAssets('spec', localScan.specPaths, 'workspace')
413
- await pushDocumentAssets('entity', localScan.entityDocPaths, 'workspace')
622
+ await pushDocumentAssets('entity', localScan.entityDocPaths, 'workspace', undefined, parseEntityMarkdownDocument)
414
623
  await pushDocumentAssets('entity', localScan.entityJsonPaths, 'workspace', undefined, parseEntityIndexJson)
415
624
 
416
625
  for (let index = 0; index < flattenedPluginInstances.length; index++) {
@@ -419,9 +628,13 @@ export async function collectWorkspaceAssets(params: {
419
628
  await pushDocumentAssets('rule', scan.rulePaths, 'plugin', instance)
420
629
  await pushDocumentAssets('skill', scan.skillPaths, 'plugin', instance)
421
630
  await pushDocumentAssets('spec', scan.specPaths, 'plugin', instance)
422
- await pushDocumentAssets('entity', scan.entityDocPaths, 'plugin', instance)
631
+ await pushDocumentAssets('entity', scan.entityDocPaths, 'plugin', instance, parseEntityMarkdownDocument)
423
632
  await pushDocumentAssets('entity', scan.entityJsonPaths, 'plugin', instance, parseEntityIndexJson)
424
633
  }
634
+ await pushDocumentAssets('skill', homeSkillPaths, 'workspace', undefined, undefined, HOME_BRIDGE_RESOLVED_BY)
635
+
636
+ const skills = mergeSkillAssets(skillAssets)
637
+ assets.push(...skills)
425
638
 
426
639
  const mcpAssets = new Map<string, Extract<WorkspaceAsset, { kind: 'mcpServer' }>>()
427
640
  const addMcpAsset = (
@@ -507,6 +720,12 @@ export async function collectWorkspaceAssets(params: {
507
720
  .map(instance => createHookPluginAsset(instance))
508
721
  assets.push(...hookPlugins)
509
722
 
723
+ const workspaces = await resolveConfiguredWorkspaceAssets({
724
+ cwd: params.cwd,
725
+ configs: [config, userConfig]
726
+ })
727
+ assets.push(...workspaces)
728
+
510
729
  const opencodeOverlayAssets = flattenedPluginInstances.flatMap((instance, index) => (
511
730
  pluginOverlayScans[index].map((entry) =>
512
731
  createOpenCodeOverlayAsset({
@@ -528,13 +747,13 @@ export async function collectWorkspaceAssets(params: {
528
747
  const entities = assets.filter((asset): asset is Extract<WorkspaceAsset, { kind: 'entity' }> =>
529
748
  asset.kind === 'entity'
530
749
  )
531
- const skills = assets.filter((asset): asset is Extract<WorkspaceAsset, { kind: 'skill' }> => asset.kind === 'skill')
532
750
 
533
- assertNoDocumentConflicts([...rules, ...specs, ...entities, ...skills])
751
+ assertNoDocumentConflicts([...rules, ...specs, ...entities])
534
752
  assertNoMcpConflicts(Array.from(mcpAssets.values()))
535
753
 
536
754
  return {
537
755
  assets,
756
+ configs: [config, userConfig],
538
757
  defaultExcludeMcpServers: [
539
758
  ...(config?.defaultExcludeMcpServers ?? []),
540
759
  ...(userConfig?.defaultExcludeMcpServers ?? [])
@@ -551,6 +770,7 @@ export async function collectWorkspaceAssets(params: {
551
770
  pluginInstances,
552
771
  rules,
553
772
  skills,
554
- specs
773
+ specs,
774
+ workspaces
555
775
  }
556
776
  }
package/src/bundle.ts CHANGED
@@ -8,12 +8,15 @@ export async function resolveWorkspaceAssetBundle(params: {
8
8
  plugins?: PluginConfig
9
9
  overlaySource?: string
10
10
  includeManagedPlugins?: boolean
11
+ syncConfiguredSkills?: boolean
12
+ updateConfiguredSkills?: boolean
11
13
  useDefaultVibeForgeMcpServer?: boolean
12
14
  }): Promise<WorkspaceAssetBundle> {
13
15
  const collected = await collectWorkspaceAssets(params)
14
16
 
15
17
  return {
16
18
  cwd: params.cwd,
19
+ configs: collected.configs,
17
20
  pluginConfigs: collected.pluginConfigs,
18
21
  pluginInstances: collected.pluginInstances,
19
22
  assets: collected.assets,
@@ -21,6 +24,7 @@ export async function resolveWorkspaceAssetBundle(params: {
21
24
  specs: collected.specs,
22
25
  entities: collected.entities,
23
26
  skills: collected.skills,
27
+ workspaces: collected.workspaces,
24
28
  mcpServers: collected.mcpServers,
25
29
  hookPlugins: collected.hookPlugins,
26
30
  opencodeOverlayAssets: collected.opencodeOverlayAssets,