@vibe-forge/workspace-assets 1.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.
@@ -0,0 +1,99 @@
1
+ import { access } from 'node:fs/promises'
2
+ import process from 'node:process'
3
+
4
+ import type { Config, ConfiguredSkillInstallConfig, SkillsCliConfig } from '@vibe-forge/types'
5
+ import {
6
+ installProjectSkill,
7
+ normalizeProjectSkillInstall,
8
+ resolveConfiguredSkillInstalls as resolveDeclaredConfiguredSkillInstalls,
9
+ resolveProjectAiPath,
10
+ resolveSkillsCliRuntimeConfig
11
+ } from '@vibe-forge/utils'
12
+ import type { NormalizedProjectSkillInstall } from '@vibe-forge/utils'
13
+
14
+ const resolveConfiguredSkillsCliConfig = (configs: [Config?, Config?]) => {
15
+ const [projectConfig, userConfig] = configs
16
+ const merged = {
17
+ ...(resolveSkillsCliRuntimeConfig(projectConfig) ?? {}),
18
+ ...(resolveSkillsCliRuntimeConfig(userConfig) ?? {})
19
+ } satisfies SkillsCliConfig
20
+
21
+ return Object.keys(merged).length === 0 ? undefined : merged
22
+ }
23
+
24
+ const resolveConfiguredSkillInstalls = (configs: [Config?, Config?]) => (
25
+ [
26
+ ...resolveDeclaredConfiguredSkillInstalls(configs[0]?.skills),
27
+ ...resolveDeclaredConfiguredSkillInstalls(configs[1]?.skills)
28
+ ]
29
+ .map((item) => normalizeProjectSkillInstall(item as string | ConfiguredSkillInstallConfig))
30
+ .filter((item): item is NormalizedProjectSkillInstall => item != null)
31
+ )
32
+
33
+ const pathExists = async (targetPath: string) => {
34
+ try {
35
+ await access(targetPath)
36
+ return true
37
+ } catch {
38
+ return false
39
+ }
40
+ }
41
+
42
+ const ensureUniqueTargets = (skills: NormalizedProjectSkillInstall[]) => {
43
+ const seen = new Map<string, string>()
44
+
45
+ for (const skill of skills) {
46
+ const previous = seen.get(skill.targetDirName)
47
+ if (previous != null) {
48
+ throw new Error(
49
+ `Configured skills "${previous}" and "${skill.ref}" resolve to the same target "${skill.targetDirName}"`
50
+ )
51
+ }
52
+ seen.set(skill.targetDirName, skill.ref)
53
+ }
54
+ }
55
+
56
+ export const ensureConfiguredProjectSkills = async (params: {
57
+ configs: [Config?, Config?]
58
+ updateInstalledSkills?: boolean
59
+ workspaceFolder: string
60
+ }) => {
61
+ const installs = resolveConfiguredSkillInstalls(params.configs)
62
+ if (installs.length === 0) {
63
+ return []
64
+ }
65
+
66
+ ensureUniqueTargets(installs)
67
+
68
+ const skillsCliConfig = resolveConfiguredSkillsCliConfig(params.configs)
69
+ const ensured: Array<{ dirName: string; skillPath: string }> = []
70
+
71
+ for (const skill of installs) {
72
+ const skillPath = resolveProjectAiPath(
73
+ params.workspaceFolder,
74
+ process.env,
75
+ 'skills',
76
+ skill.targetDirName,
77
+ 'SKILL.md'
78
+ )
79
+ if (params.updateInstalledSkills !== true && await pathExists(skillPath)) {
80
+ ensured.push({
81
+ dirName: skill.targetDirName,
82
+ skillPath
83
+ })
84
+ continue
85
+ }
86
+
87
+ ensured.push(
88
+ await installProjectSkill({
89
+ config: skillsCliConfig,
90
+ force: true,
91
+ registry: undefined,
92
+ skill,
93
+ workspaceFolder: params.workspaceFolder
94
+ })
95
+ )
96
+ }
97
+
98
+ return ensured
99
+ }
@@ -0,0 +1 @@
1
+ export const HOME_BRIDGE_RESOLVED_BY = 'home-bridge'
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  export { buildAdapterAssetPlan } from './adapter-asset-plan'
2
+ export { resolveWorkspaceAssetSource } from './asset-source'
2
3
  export { resolveWorkspaceAssetBundle } from './bundle'
4
+ export { HOME_BRIDGE_RESOLVED_BY } from './home-bridge'
3
5
  export { resolvePromptAssetSelection } from './prompt-selection'
6
+ export { findWorkspaceAsset, resolveConfiguredWorkspaceAssets } from './workspaces'
@@ -1,10 +1,13 @@
1
+ /* eslint-disable max-lines -- prompt asset selection coordinates routing, overlays, and dependency expansion */
1
2
  import type {
2
3
  Definition,
4
+ Entity,
3
5
  Filter,
4
6
  PluginOverlayConfig,
5
7
  Rule,
6
8
  RuleReference,
7
9
  SkillSelection,
10
+ Spec,
8
11
  WorkspaceAsset,
9
12
  WorkspaceAssetBundle,
10
13
  WorkspaceMcpSelection,
@@ -23,14 +26,17 @@ import {
23
26
  import {
24
27
  definitionWithResolvedName,
25
28
  pickDocumentAsset,
29
+ resolveEntityInheritance,
26
30
  resolveExcludedSkillRefs,
27
31
  resolveIncludedSkillRefs,
28
32
  resolveNamedAssets,
29
33
  resolvePluginOverlay,
30
34
  resolveRuleSelection,
31
- resolveSelectedSkillAssets,
35
+ resolveSelectedSkillAssetsWithDependencies,
32
36
  toDocumentDefinitions
33
37
  } from './selection-internal'
38
+ import { expandSkillAssetDependenciesWithRemoteResolution } from './skill-dependencies'
39
+ import { generateWorkspaceRoutePrompt } from './workspace-prompt'
34
40
 
35
41
  export async function resolvePromptAssetSelection(params: {
36
42
  bundle: WorkspaceAssetBundle
@@ -57,6 +63,8 @@ export async function resolvePromptAssetSelection(params: {
57
63
  let targetToolsFilter: Filter | undefined
58
64
  let targetMcpServersFilter: Filter | undefined
59
65
  let targetInstancePath: string | undefined
66
+ let targetAssetIds: string[] = []
67
+ let targetDefinition: Definition<Entity | Spec> | undefined
60
68
 
61
69
  if (params.type && params.name) {
62
70
  const baseTarget = params.type === 'spec'
@@ -77,18 +85,30 @@ export async function resolvePromptAssetSelection(params: {
77
85
  }
78
86
 
79
87
  pinnedTargetAsset = baseTarget
80
- targetBody = baseTarget.payload.definition.body
81
- targetToolsFilter = baseTarget.payload.definition.attributes.tools
82
- targetMcpServersFilter = baseTarget.payload.definition.attributes.mcpServers
88
+ if (params.type === 'entity') {
89
+ const resolvedEntity = resolveEntityInheritance(
90
+ effectiveBundle,
91
+ baseTarget as Extract<WorkspaceAsset, { kind: 'entity' }>
92
+ )
93
+ targetDefinition = resolvedEntity.definition
94
+ targetAssetIds = resolvedEntity.assetIds
95
+ } else {
96
+ targetDefinition = baseTarget.payload.definition
97
+ targetAssetIds = [baseTarget.id]
98
+ }
99
+ targetBody = targetDefinition.body
100
+ targetToolsFilter = targetDefinition.attributes.tools
101
+ targetMcpServersFilter = targetDefinition.attributes.mcpServers
83
102
  targetInstancePath = baseTarget.instancePath
84
103
  options.assetBundle = effectiveBundle
85
104
  }
86
105
 
87
- const selectedSkillAssets = resolveSelectedSkillAssets(effectiveBundle.skills, params.input?.skills)
106
+ const selectedSkillAssets = await resolveSelectedSkillAssetsWithDependencies(effectiveBundle, params.input?.skills)
88
107
  const useNativeProjectSkills = supportsNativeProjectSkills(params.adapter)
89
108
  const promptAssetIds = new Set<string>([
90
109
  ...effectiveBundle.rules.map(asset => asset.id),
91
110
  ...effectiveBundle.specs.map(asset => asset.id),
111
+ ...effectiveBundle.workspaces.map(asset => asset.id),
92
112
  ...(useNativeProjectSkills ? [] : selectedSkillAssets.map(asset => asset.id)),
93
113
  ...(params.type !== 'entity' ? effectiveBundle.entities.map(asset => asset.id) : [])
94
114
  ])
@@ -101,8 +121,8 @@ export async function resolvePromptAssetSelection(params: {
101
121
  const targetSkillsAssets: Array<Extract<WorkspaceAsset, { kind: 'skill' }>> = []
102
122
 
103
123
  if (pinnedTargetAsset != null) {
104
- promptAssetIds.add(pinnedTargetAsset.id)
105
- const attributes = pinnedTargetAsset.payload.definition.attributes
124
+ targetAssetIds.forEach(assetId => promptAssetIds.add(assetId))
125
+ const attributes = targetDefinition?.attributes ?? pinnedTargetAsset.payload.definition.attributes
106
126
 
107
127
  if (attributes.rules != null) {
108
128
  const selection = await resolveRuleSelection(
@@ -144,12 +164,18 @@ export async function resolvePromptAssetSelection(params: {
144
164
  resolveNamedAssets(effectiveBundle.skills, excludedRefs, targetInstancePath).map(asset => asset.id)
145
165
  )
146
166
 
147
- includedAssets
148
- .filter(asset => !excludedIds.has(asset.id))
149
- .forEach((asset) => {
150
- targetSkillsAssets.push(asset)
151
- promptAssetIds.add(asset.id)
152
- })
167
+ const expandedTargetSkills = await expandSkillAssetDependenciesWithRemoteResolution({
168
+ allAssets: effectiveBundle.assets,
169
+ configs: effectiveBundle.configs ?? [undefined, undefined],
170
+ cwd: effectiveBundle.cwd,
171
+ excludedIds,
172
+ selectedAssets: includedAssets,
173
+ skillAssets: effectiveBundle.skills
174
+ })
175
+ expandedTargetSkills.forEach((asset) => {
176
+ targetSkillsAssets.push(asset)
177
+ promptAssetIds.add(asset.id)
178
+ })
153
179
  }
154
180
 
155
181
  const rules = Array.from(ruleDefinitions.values())
@@ -162,11 +188,13 @@ export async function resolvePromptAssetSelection(params: {
162
188
  : []
163
189
  const skills = toDocumentDefinitions(selectedSkillAssets)
164
190
  const specs = toDocumentDefinitions(effectiveBundle.specs)
191
+ const workspaces = effectiveBundle.workspaces.map(asset => asset.payload)
165
192
 
166
193
  options.systemPrompt = [
167
194
  generateRulesPrompt(effectiveBundle.cwd, rules),
168
195
  generateSkillsPrompt(effectiveBundle.cwd, targetSkills),
169
196
  generateEntitiesRoutePrompt(entities),
197
+ generateWorkspaceRoutePrompt(effectiveBundle.cwd, workspaces),
170
198
  useNativeProjectSkills ? '' : generateSkillsRoutePrompt(effectiveBundle.cwd, routedSkills),
171
199
  generateSpecRoutePrompt(specs, { active: params.type === 'spec' }),
172
200
  targetBody
@@ -174,12 +202,8 @@ export async function resolvePromptAssetSelection(params: {
174
202
  .filter(section => section !== '')
175
203
  .join('\n\n')
176
204
 
177
- if (targetToolsFilter != null) {
178
- options.tools = targetToolsFilter
179
- }
180
- if (targetMcpServersFilter != null) {
181
- options.mcpServers = targetMcpServersFilter
182
- }
205
+ if (targetToolsFilter != null) options.tools = targetToolsFilter
206
+ if (targetMcpServersFilter != null) options.mcpServers = targetMcpServersFilter
183
207
  options.promptAssetIds = Array.from(promptAssetIds)
184
208
 
185
209
  return [
@@ -189,6 +213,7 @@ export async function resolvePromptAssetSelection(params: {
189
213
  entities,
190
214
  skills,
191
215
  specs,
216
+ workspaces,
192
217
  targetBody,
193
218
  promptAssetIds: Array.from(promptAssetIds)
194
219
  },
@@ -1,5 +1,9 @@
1
1
  import type {
2
2
  Definition,
3
+ Entity,
4
+ EntityInheritance,
5
+ EntityInheritanceMode,
6
+ Filter,
3
7
  PluginConfig,
4
8
  PluginOverlayConfig,
5
9
  Rule,
@@ -22,6 +26,7 @@ import {
22
26
  isRemoteRuleReference,
23
27
  parseScopedReference
24
28
  } from '@vibe-forge/definition-core'
29
+ import { expandSkillAssetDependencies, expandSkillAssetDependenciesWithRemoteResolution } from './skill-dependencies'
25
30
 
26
31
  type DocumentAssetKind = Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>
27
32
  type DocumentAsset<TDefinition> = Extract<WorkspaceAsset, { kind: DocumentAssetKind }> & {
@@ -29,8 +34,28 @@ type DocumentAsset<TDefinition> = Extract<WorkspaceAsset, { kind: DocumentAssetK
29
34
  definition: TDefinition & { path: string }
30
35
  }
31
36
  }
37
+ type EntityAsset = Extract<WorkspaceAsset, { kind: 'entity' }>
38
+ type EntityInheritanceField = Exclude<keyof EntityInheritance, 'default'>
32
39
 
33
40
  const ASSET_REFERENCE_PATH_SUFFIXES = ['.md', '.json', '.yaml', '.yml']
41
+ const ENTITY_INHERITANCE_FIELDS = ['prompt', 'tags', 'rules', 'skills', 'tools', 'mcpServers'] as const
42
+ const ENTITY_INHERITANCE_MODES = new Set<EntityInheritanceMode>(['append', 'prepend', 'merge', 'replace', 'none'])
43
+ const DEFAULT_CHILD_ENTITY_INHERITANCE: Record<EntityInheritanceField, EntityInheritanceMode> = {
44
+ prompt: 'append',
45
+ tags: 'merge',
46
+ rules: 'merge',
47
+ skills: 'merge',
48
+ tools: 'replace',
49
+ mcpServers: 'replace'
50
+ }
51
+ const PARENT_ENTITY_INHERITANCE: Record<EntityInheritanceField, EntityInheritanceMode> = {
52
+ prompt: 'append',
53
+ tags: 'merge',
54
+ rules: 'merge',
55
+ skills: 'merge',
56
+ tools: 'replace',
57
+ mcpServers: 'replace'
58
+ }
34
59
 
35
60
  export const definitionWithResolvedName = <TDefinition>(
36
61
  definition: Definition<TDefinition>,
@@ -122,6 +147,293 @@ export const resolveNamedAssets = <TAsset extends Extract<WorkspaceAsset, { kind
122
147
  return selected
123
148
  }
124
149
 
150
+ const normalizeEntityExtends = (value: Entity['extends']) => {
151
+ if (typeof value === 'string') return value.trim() !== '' ? [value.trim()] : []
152
+ if (!Array.isArray(value)) return []
153
+
154
+ return value
155
+ .map(ref => ref.trim())
156
+ .filter(Boolean)
157
+ }
158
+
159
+ const parseEntityInheritanceMode = (
160
+ value: unknown,
161
+ label: string
162
+ ): EntityInheritanceMode | undefined => {
163
+ if (value == null) return undefined
164
+ if (typeof value !== 'string' || !ENTITY_INHERITANCE_MODES.has(value as EntityInheritanceMode)) {
165
+ throw new Error(`Invalid entity inherit mode for ${label}: ${String(value)}`)
166
+ }
167
+ return value as EntityInheritanceMode
168
+ }
169
+
170
+ const resolveEntityInheritanceModes = (
171
+ value: Entity['inherit'],
172
+ defaults: Record<EntityInheritanceField, EntityInheritanceMode>
173
+ ) => {
174
+ const modes = { ...defaults }
175
+ if (value == null) return modes
176
+
177
+ if (typeof value === 'string') {
178
+ const defaultMode = parseEntityInheritanceMode(value, 'inherit')
179
+ for (const field of ENTITY_INHERITANCE_FIELDS) {
180
+ modes[field] = defaultMode ?? modes[field]
181
+ }
182
+ return modes
183
+ }
184
+
185
+ const defaultMode = parseEntityInheritanceMode(value.default, 'inherit.default')
186
+ for (const field of ENTITY_INHERITANCE_FIELDS) {
187
+ modes[field] = parseEntityInheritanceMode(value[field], `inherit.${field}`) ?? defaultMode ?? modes[field]
188
+ }
189
+ return modes
190
+ }
191
+
192
+ const toUniqueStrings = (values: string[]) => Array.from(new Set(values))
193
+
194
+ const toUniqueValues = <TValue>(values: TValue[], keyOf: (value: TValue) => string) => {
195
+ const seen = new Set<string>()
196
+ const result: TValue[] = []
197
+
198
+ for (const value of values) {
199
+ const key = keyOf(value)
200
+ if (seen.has(key)) continue
201
+ seen.add(key)
202
+ result.push(value)
203
+ }
204
+
205
+ return result
206
+ }
207
+
208
+ const keyRuleReference = (rule: RuleReference) => (
209
+ typeof rule === 'string' ? `string:${rule}` : `object:${JSON.stringify(rule)}`
210
+ )
211
+
212
+ const qualifyEntityReference = (
213
+ asset: EntityAsset,
214
+ ref: string
215
+ ) => {
216
+ const value = ref.trim()
217
+ if (value === '' || asset.scope == null) return value
218
+ if (parseScopedReference(value, { pathSuffixes: ASSET_REFERENCE_PATH_SUFFIXES }) != null) return value
219
+ if (
220
+ isPathLikeReference(value, {
221
+ pathSuffixes: ASSET_REFERENCE_PATH_SUFFIXES,
222
+ allowGlob: true
223
+ })
224
+ ) {
225
+ return value
226
+ }
227
+
228
+ return `${asset.scope}/${value}`
229
+ }
230
+
231
+ const qualifyEntityRuleReferences = (
232
+ asset: EntityAsset,
233
+ rules: Entity['rules']
234
+ ) => rules?.map(rule => typeof rule === 'string' ? qualifyEntityReference(asset, rule) : rule)
235
+
236
+ const qualifyEntitySkillSelection = (
237
+ asset: EntityAsset,
238
+ selection: Entity['skills']
239
+ ): Entity['skills'] => {
240
+ if (selection == null) return undefined
241
+ if (Array.isArray(selection)) return selection.map(ref => qualifyEntityReference(asset, ref))
242
+
243
+ return {
244
+ ...selection,
245
+ list: selection.list.map(ref => qualifyEntityReference(asset, ref))
246
+ }
247
+ }
248
+
249
+ const qualifyEntityInternalReferences = (
250
+ asset: EntityAsset,
251
+ definition: Definition<Entity>
252
+ ): Definition<Entity> => ({
253
+ ...definition,
254
+ attributes: {
255
+ ...definition.attributes,
256
+ rules: qualifyEntityRuleReferences(asset, definition.attributes.rules),
257
+ skills: qualifyEntitySkillSelection(asset, definition.attributes.skills)
258
+ }
259
+ })
260
+
261
+ const selectInheritedValue = <TValue>(
262
+ parent: TValue | undefined,
263
+ child: TValue | undefined,
264
+ mode: EntityInheritanceMode,
265
+ merge: (left: TValue, right: TValue) => TValue
266
+ ) => {
267
+ if (mode === 'none') return child
268
+ if (mode === 'replace') return child ?? parent
269
+ if (parent == null) return child
270
+ if (child == null) return parent
271
+ return mode === 'prepend' ? merge(child, parent) : merge(parent, child)
272
+ }
273
+
274
+ const mergeEntityBody = (
275
+ parent: string,
276
+ child: string,
277
+ mode: EntityInheritanceMode
278
+ ) => {
279
+ if (mode === 'none' || mode === 'replace') return child
280
+
281
+ const values = mode === 'prepend' ? [child, parent] : [parent, child]
282
+ return values
283
+ .map(value => value.trim())
284
+ .filter(Boolean)
285
+ .join('\n\n')
286
+ }
287
+
288
+ const getSkillIncludeRefs = (selection: Entity['skills']) => {
289
+ if (Array.isArray(selection)) return selection
290
+ return selection?.type === 'include' ? selection.list : undefined
291
+ }
292
+
293
+ const mergeSkillSelections = (
294
+ parent: Entity['skills'],
295
+ child: Entity['skills']
296
+ ): Entity['skills'] => {
297
+ const parentRefs = getSkillIncludeRefs(parent)
298
+ const childRefs = getSkillIncludeRefs(child)
299
+ if (parentRefs != null && childRefs != null) return toUniqueStrings([...parentRefs, ...childRefs])
300
+
301
+ return child ?? parent
302
+ }
303
+
304
+ const mergeFilters = (
305
+ parent: Filter,
306
+ child: Filter
307
+ ): Filter => {
308
+ const include = toUniqueStrings([
309
+ ...(parent.include ?? []),
310
+ ...(child.include ?? [])
311
+ ])
312
+ const exclude = toUniqueStrings([
313
+ ...(parent.exclude ?? []),
314
+ ...(child.exclude ?? [])
315
+ ])
316
+
317
+ return {
318
+ ...(include.length > 0 ? { include } : {}),
319
+ ...(exclude.length > 0 ? { exclude } : {})
320
+ }
321
+ }
322
+
323
+ const mergeEntityDefinitions = (
324
+ parent: Definition<Entity>,
325
+ child: Definition<Entity>,
326
+ modes: Record<EntityInheritanceField, EntityInheritanceMode>
327
+ ): Definition<Entity> => ({
328
+ ...child,
329
+ body: mergeEntityBody(parent.body, child.body, modes.prompt),
330
+ attributes: {
331
+ ...parent.attributes,
332
+ ...child.attributes,
333
+ name: child.attributes.name,
334
+ description: child.attributes.description ?? parent.attributes.description,
335
+ always: child.attributes.always ?? parent.attributes.always,
336
+ tags: selectInheritedValue(
337
+ parent.attributes.tags,
338
+ child.attributes.tags,
339
+ modes.tags,
340
+ (left, right) => toUniqueStrings([...left, ...right])
341
+ ),
342
+ rules: selectInheritedValue(
343
+ parent.attributes.rules,
344
+ child.attributes.rules,
345
+ modes.rules,
346
+ (left, right) => toUniqueValues([...left, ...right], keyRuleReference)
347
+ ),
348
+ skills: selectInheritedValue(parent.attributes.skills, child.attributes.skills, modes.skills, mergeSkillSelections),
349
+ tools: selectInheritedValue(parent.attributes.tools, child.attributes.tools, modes.tools, mergeFilters),
350
+ mcpServers: selectInheritedValue(
351
+ parent.attributes.mcpServers,
352
+ child.attributes.mcpServers,
353
+ modes.mcpServers,
354
+ mergeFilters
355
+ ),
356
+ plugins: child.attributes.plugins
357
+ }
358
+ })
359
+
360
+ const uniqueAssetIds = (values: string[]) => toUniqueValues(values, value => value)
361
+
362
+ const formatEntityCycle = (stack: EntityAsset[], asset: EntityAsset) => (
363
+ [...stack.slice(stack.findIndex(item => item.id === asset.id)), asset]
364
+ .map(item => item.displayName)
365
+ .join(' -> ')
366
+ )
367
+
368
+ const createAvailableEntitiesMessage = (entities: EntityAsset[]) => (
369
+ entities
370
+ .map(asset => asset.displayName)
371
+ .sort((left, right) => left.localeCompare(right))
372
+ .join(', ')
373
+ )
374
+
375
+ export const resolveEntityInheritance = (
376
+ bundle: WorkspaceAssetBundle,
377
+ asset: EntityAsset
378
+ ): {
379
+ assetIds: string[]
380
+ definition: Definition<Entity>
381
+ } => {
382
+ const resolveAsset = (
383
+ current: EntityAsset,
384
+ stack: EntityAsset[]
385
+ ): {
386
+ assetIds: string[]
387
+ definition: Definition<Entity>
388
+ } => {
389
+ if (stack.some(item => item.id === current.id)) {
390
+ throw new Error(`Circular entity inheritance detected: ${formatEntityCycle(stack, current)}`)
391
+ }
392
+
393
+ const currentDefinition = definitionWithResolvedName(
394
+ current.payload.definition,
395
+ current.displayName,
396
+ current.instancePath
397
+ )
398
+ const qualifiedCurrentDefinition = qualifyEntityInternalReferences(current, currentDefinition)
399
+ const parentRefs = normalizeEntityExtends(qualifiedCurrentDefinition.attributes.extends)
400
+ if (parentRefs.length === 0) {
401
+ return {
402
+ assetIds: [current.id],
403
+ definition: qualifiedCurrentDefinition
404
+ }
405
+ }
406
+
407
+ let inheritedBase: Definition<Entity> | undefined
408
+ const inheritedAssetIds: string[] = []
409
+ for (const ref of parentRefs) {
410
+ const parentAsset = findNamedAsset(bundle.entities, ref, current.instancePath)
411
+ if (parentAsset == null) {
412
+ throw new Error(
413
+ `Failed to resolve entity ${ref}. Available entities: ${createAvailableEntitiesMessage(bundle.entities)}`
414
+ )
415
+ }
416
+
417
+ const parent = resolveAsset(parentAsset, [...stack, current])
418
+ inheritedAssetIds.push(...parent.assetIds)
419
+ inheritedBase = inheritedBase == null
420
+ ? parent.definition
421
+ : mergeEntityDefinitions(inheritedBase, parent.definition, PARENT_ENTITY_INHERITANCE)
422
+ }
423
+
424
+ return {
425
+ assetIds: uniqueAssetIds([...inheritedAssetIds, current.id]),
426
+ definition: mergeEntityDefinitions(
427
+ inheritedBase ?? qualifiedCurrentDefinition,
428
+ qualifiedCurrentDefinition,
429
+ resolveEntityInheritanceModes(qualifiedCurrentDefinition.attributes.inherit, DEFAULT_CHILD_ENTITY_INHERITANCE)
430
+ )
431
+ }
432
+ }
433
+
434
+ return resolveAsset(asset, [])
435
+ }
436
+
125
437
  const resolvePathMatchedRules = async (
126
438
  bundle: WorkspaceAssetBundle,
127
439
  ref: string
@@ -211,7 +523,29 @@ export const resolveSelectedSkillAssets = (
211
523
  const excluded = new Set(
212
524
  resolveNamedAssets(assets, selection.exclude).map(asset => asset.id)
213
525
  )
214
- return included.filter(asset => !excluded.has(asset.id))
526
+ return expandSkillAssetDependencies(assets, included, {
527
+ excludedIds: excluded
528
+ })
529
+ }
530
+
531
+ export const resolveSelectedSkillAssetsWithDependencies = async (
532
+ bundle: WorkspaceAssetBundle,
533
+ selection?: WorkspaceSkillSelection
534
+ ): Promise<Array<Extract<WorkspaceAsset, { kind: 'skill' }>>> => {
535
+ const included = selection?.include != null && selection.include.length > 0
536
+ ? resolveNamedAssets(bundle.skills, selection.include)
537
+ : bundle.skills
538
+ const excluded = new Set(
539
+ resolveNamedAssets(bundle.skills, selection?.exclude).map(asset => asset.id)
540
+ )
541
+ return await expandSkillAssetDependenciesWithRemoteResolution({
542
+ allAssets: bundle.assets,
543
+ configs: bundle.configs ?? [undefined, undefined],
544
+ cwd: bundle.cwd,
545
+ excludedIds: excluded,
546
+ selectedAssets: included,
547
+ skillAssets: bundle.skills
548
+ })
215
549
  }
216
550
 
217
551
  export const resolveSelectedMcpNames = (