@vibe-forge/workspace-assets 2.0.0 → 2.0.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.
@@ -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 {
@@ -28,10 +28,13 @@ import {
28
28
  resolveSkillIdentifier,
29
29
  resolveSpecIdentifier
30
30
  } from '@vibe-forge/definition-core'
31
+ import { HOME_BRIDGE_RESOLVED_BY } from './home-bridge'
32
+ import { resolveConfiguredWorkspaceAssets } from './workspaces'
31
33
 
32
34
  type DocumentAssetKind = Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>
33
35
  type OpenCodeOverlayKind = Extract<WorkspaceAssetKind, 'agent' | 'command' | 'mode' | 'nativePlugin'>
34
36
  type OpenCodeOverlayAsset<TKind extends OpenCodeOverlayKind> = Extract<WorkspaceAsset, { kind: TKind }>
37
+ type SkillAsset = Extract<WorkspaceAsset, { kind: 'skill' }>
35
38
 
36
39
  type DocumentAsset<TDefinition> = Extract<WorkspaceAsset, { kind: DocumentAssetKind }> & {
37
40
  payload: {
@@ -50,10 +53,147 @@ const isRecord = (value: unknown): value is Record<string, unknown> => (
50
53
  value != null && typeof value === 'object' && !Array.isArray(value)
51
54
  )
52
55
 
56
+ const ENTITY_DIRECTORY_ENTRY_FILES = new Set(['readme.md', 'index.json'])
57
+ const DEFAULT_HOME_SKILL_ROOTS = [
58
+ '~/.agents/skills',
59
+ '~/.claude/skills',
60
+ '~/.config/opencode/skills',
61
+ '~/.gemini/skills'
62
+ ] as const
63
+
64
+ const DEFAULT_ENTITY_PROMPT_FILE_SECTIONS = [
65
+ {
66
+ heading: 'Introduction',
67
+ fileNames: ['INTRODUCTION.md', 'introduction.md', '介绍.md']
68
+ },
69
+ {
70
+ heading: 'Personality',
71
+ fileNames: ['PERSONALITY.md', 'personality.md', '人格.md']
72
+ },
73
+ {
74
+ heading: 'Memory',
75
+ fileNames: ['MEMORY.md', 'memory.md', '记忆.md']
76
+ }
77
+ ] as const
78
+
79
+ const isMissingFileError = (error: unknown) => (
80
+ error != null &&
81
+ typeof error === 'object' &&
82
+ 'code' in error &&
83
+ (error as { code?: unknown }).code === 'ENOENT'
84
+ )
85
+
86
+ const readOptionalMarkdownBody = async (path: string) => {
87
+ try {
88
+ const content = await readFile(path, 'utf-8')
89
+ return fm<Record<string, never>>(content).body.trim()
90
+ } catch (err) {
91
+ if (isMissingFileError(err)) return undefined
92
+ throw err
93
+ }
94
+ }
95
+
96
+ const loadDefaultEntityPromptSection = async (
97
+ entityDir: string,
98
+ section: (typeof DEFAULT_ENTITY_PROMPT_FILE_SECTIONS)[number]
99
+ ) => {
100
+ for (const fileName of section.fileNames) {
101
+ const body = await readOptionalMarkdownBody(resolve(entityDir, fileName))
102
+ if (body == null || body === '') continue
103
+
104
+ return `## ${section.heading}\n\n${body}`
105
+ }
106
+
107
+ return undefined
108
+ }
109
+
110
+ const appendDefaultEntityPromptFiles = async (path: string, body: string) => {
111
+ if (!ENTITY_DIRECTORY_ENTRY_FILES.has(basename(path).toLowerCase())) return body
112
+
113
+ const sections = await Promise.all(
114
+ DEFAULT_ENTITY_PROMPT_FILE_SECTIONS.map(section => loadDefaultEntityPromptSection(dirname(path), section))
115
+ )
116
+
117
+ return [
118
+ body.trim(),
119
+ ...sections
120
+ ]
121
+ .filter((section): section is string => section != null && section !== '')
122
+ .join('\n\n')
123
+ }
124
+
53
125
  const resolveDisplayName = (name: string, scope?: string) => (
54
126
  scope != null && scope.trim() !== '' ? `${scope}/${name}` : name
55
127
  )
56
128
 
129
+ const toStringList = (value: string | string[] | undefined) => {
130
+ if (typeof value === 'string' && value.trim() !== '') {
131
+ return [value.trim()]
132
+ }
133
+
134
+ if (!Array.isArray(value)) return [] as string[]
135
+
136
+ return value
137
+ .filter((item): item is string => typeof item === 'string' && item.trim() !== '')
138
+ .map(item => item.trim())
139
+ }
140
+
141
+ const resolveRealHomeDir = (env: NodeJS.ProcessEnv) => {
142
+ const value = env.__VF_PROJECT_REAL_HOME__?.trim() || env.HOME?.trim()
143
+ if (value == null || value === '') return undefined
144
+ return resolve(value)
145
+ }
146
+
147
+ const warnInvalidHomeSkillRoot = (root: string) => {
148
+ console.warn(
149
+ `[vibe-forge] Ignoring invalid skills.homeBridge root "${root}". ` +
150
+ 'Use an absolute path or a path starting with "~".'
151
+ )
152
+ }
153
+
154
+ const resolveHomeBridgeConfig = (configs: [Config?, Config?]) => {
155
+ const [config, userConfig] = configs
156
+ const projectHomeBridge = config?.skills?.homeBridge
157
+ const userHomeBridge = userConfig?.skills?.homeBridge
158
+
159
+ return {
160
+ enabled: userHomeBridge?.enabled ?? projectHomeBridge?.enabled ?? true,
161
+ roots: toStringList(userHomeBridge?.roots ?? projectHomeBridge?.roots)
162
+ }
163
+ }
164
+
165
+ const resolveHomeSkillRoots = (configs: [Config?, Config?], env: NodeJS.ProcessEnv = process.env) => {
166
+ const homeBridge = resolveHomeBridgeConfig(configs)
167
+ if (homeBridge.enabled === false) return [] as string[]
168
+
169
+ const realHome = resolveRealHomeDir(env)
170
+ if (realHome == null) return [] as string[]
171
+
172
+ const rawRoots = homeBridge.roots.length > 0 ? homeBridge.roots : Array.from(DEFAULT_HOME_SKILL_ROOTS)
173
+ const roots: string[] = []
174
+ const seen = new Set<string>()
175
+
176
+ for (const rawRoot of rawRoots) {
177
+ let resolvedRoot: string | undefined
178
+
179
+ if (rawRoot === '~') {
180
+ resolvedRoot = realHome
181
+ } else if (rawRoot.startsWith('~/')) {
182
+ resolvedRoot = resolve(realHome, rawRoot.slice(2))
183
+ } else if (isAbsolute(rawRoot)) {
184
+ resolvedRoot = resolve(rawRoot)
185
+ } else if (homeBridge.roots.length > 0) {
186
+ warnInvalidHomeSkillRoot(rawRoot)
187
+ }
188
+
189
+ if (resolvedRoot == null || seen.has(resolvedRoot)) continue
190
+ seen.add(resolvedRoot)
191
+ roots.push(resolvedRoot)
192
+ }
193
+
194
+ return roots
195
+ }
196
+
57
197
  const loadWorkspaceConfig = async (cwd: string) => (
58
198
  loadConfig({
59
199
  cwd,
@@ -73,6 +213,15 @@ const parseFrontmatterDocument = async <TDefinition extends object>(
73
213
  }
74
214
  }
75
215
 
216
+ const parseEntityMarkdownDocument = async (path: string): Promise<Definition<Entity>> => {
217
+ const definition = await parseFrontmatterDocument<Entity>(path)
218
+
219
+ return {
220
+ ...definition,
221
+ body: await appendDefaultEntityPromptFiles(path, definition.body)
222
+ }
223
+ }
224
+
76
225
  const parseEntityIndexJson = async (path: string): Promise<Definition<Entity>> => {
77
226
  const raw = JSON.parse(await readFile(path, 'utf-8')) as Record<string, unknown>
78
227
  const promptPath = typeof raw.promptPath === 'string'
@@ -86,7 +235,7 @@ const parseEntityIndexJson = async (path: string): Promise<Definition<Entity>> =
86
235
 
87
236
  return {
88
237
  path,
89
- body: prompt,
238
+ body: await appendDefaultEntityPromptFiles(path, prompt),
90
239
  attributes: raw as Entity
91
240
  }
92
241
  }
@@ -109,6 +258,7 @@ const createDocumentAsset = <
109
258
  origin: 'workspace' | 'plugin'
110
259
  scope?: string
111
260
  instance?: ResolvedPluginInstance
261
+ resolvedBy?: string
112
262
  }) => {
113
263
  const name = ({
114
264
  rule: resolveDocumentName,
@@ -130,7 +280,7 @@ const createDocumentAsset = <
130
280
  sourcePath: params.definition.path,
131
281
  instancePath: params.instance?.instancePath,
132
282
  packageId: params.instance?.packageId,
133
- resolvedBy: params.instance?.resolvedBy,
283
+ resolvedBy: params.resolvedBy ?? params.instance?.resolvedBy,
134
284
  taskOverlaySource: params.instance?.overlaySource,
135
285
  payload: {
136
286
  definition: params.definition
@@ -238,6 +388,19 @@ const scanWorkspaceDocuments = async (cwd: string) => {
238
388
  }
239
389
  }
240
390
 
391
+ const scanHomeSkillDocuments = async (configs: [Config?, Config?]) => {
392
+ const roots = resolveHomeSkillRoots(configs)
393
+ if (roots.length === 0) return [] as string[]
394
+
395
+ const scans = await Promise.all(
396
+ roots.map(async root => (
397
+ await glob(['*/SKILL.md'], { cwd: root, absolute: true }).catch(() => [] as string[])
398
+ ))
399
+ )
400
+
401
+ return scans.flatMap(paths => [...paths].sort((left, right) => left.localeCompare(right)))
402
+ }
403
+
241
404
  const scanInstanceDocuments = async (instance: ResolvedPluginInstance) => {
242
405
  const assets = instance.manifest?.assets
243
406
  const resolveAssetRoot = (dir: string | undefined, fallback: string) => resolve(instance.rootDir, dir ?? fallback)
@@ -326,6 +489,24 @@ const assertNoDocumentConflicts = (
326
489
  }
327
490
  }
328
491
 
492
+ const mergeSkillAssets = (assets: SkillAsset[]) => {
493
+ const directAssets = assets.filter(asset => asset.resolvedBy !== HOME_BRIDGE_RESOLVED_BY)
494
+ const bridgedAssets = assets.filter(asset => asset.resolvedBy === HOME_BRIDGE_RESOLVED_BY)
495
+
496
+ assertNoDocumentConflicts(directAssets)
497
+
498
+ const seen = new Set(directAssets.map(asset => asset.displayName))
499
+ const merged = [...directAssets]
500
+
501
+ for (const asset of bridgedAssets) {
502
+ if (seen.has(asset.displayName)) continue
503
+ seen.add(asset.displayName)
504
+ merged.push(asset)
505
+ }
506
+
507
+ return merged
508
+ }
509
+
329
510
  const assertNoMcpConflicts = (
330
511
  assets: Array<Extract<WorkspaceAsset, { kind: 'mcpServer' }>>
331
512
  ) => {
@@ -348,6 +529,7 @@ export async function collectWorkspaceAssets(params: {
348
529
  useDefaultVibeForgeMcpServer?: boolean
349
530
  }): Promise<{
350
531
  assets: WorkspaceAsset[]
532
+ configs: [Config?, Config?]
351
533
  defaultExcludeMcpServers: string[]
352
534
  defaultIncludeMcpServers: string[]
353
535
  entities: Array<Extract<WorkspaceAsset, { kind: 'entity' }>>
@@ -359,6 +541,7 @@ export async function collectWorkspaceAssets(params: {
359
541
  rules: Array<Extract<WorkspaceAsset, { kind: 'rule' }>>
360
542
  skills: Array<Extract<WorkspaceAsset, { kind: 'skill' }>>
361
543
  specs: Array<Extract<WorkspaceAsset, { kind: 'spec' }>>
544
+ workspaces: Array<Extract<WorkspaceAsset, { kind: 'workspace' }>>
362
545
  }> {
363
546
  const [config, userConfig] = params.configs ?? await loadWorkspaceConfig(params.cwd)
364
547
  const managedPluginConfigs = params.includeManagedPlugins === false
@@ -374,7 +557,10 @@ export async function collectWorkspaceAssets(params: {
374
557
  overlaySource: params.overlaySource
375
558
  })
376
559
 
377
- const localScan = await scanWorkspaceDocuments(params.cwd)
560
+ const [localScan, homeSkillPaths] = await Promise.all([
561
+ scanWorkspaceDocuments(params.cwd),
562
+ scanHomeSkillDocuments([config, userConfig])
563
+ ])
378
564
  const flattenedPluginInstances = flattenPluginInstances(pluginInstances)
379
565
  const pluginScans = await Promise.all(flattenedPluginInstances.map(instance => scanInstanceDocuments(instance)))
380
566
  const pluginOverlayScans = await Promise.all(
@@ -382,35 +568,43 @@ export async function collectWorkspaceAssets(params: {
382
568
  )
383
569
 
384
570
  const assets: WorkspaceAsset[] = []
571
+ const skillAssets: SkillAsset[] = []
385
572
 
386
573
  const pushDocumentAssets = async <TKind extends DocumentAssetKind>(
387
574
  kind: TKind,
388
575
  paths: string[],
389
576
  origin: 'workspace' | 'plugin',
390
577
  instance?: ResolvedPluginInstance,
391
- parser?: (path: string) => Promise<any>
578
+ parser?: (path: string) => Promise<any>,
579
+ resolvedBy?: string
392
580
  ) => {
393
581
  const definitions = await Promise.all(paths.map(path => (
394
582
  parser != null ? parser(path) : parseFrontmatterDocument(path)
395
583
  )))
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
- )
584
+ const createdAssets = definitions.map(definition =>
585
+ createDocumentAsset({
586
+ cwd: params.cwd,
587
+ kind,
588
+ definition,
589
+ origin,
590
+ scope: instance?.scope,
591
+ instance,
592
+ resolvedBy
593
+ })
407
594
  )
595
+
596
+ if (kind === 'skill') {
597
+ skillAssets.push(...createdAssets as SkillAsset[])
598
+ return
599
+ }
600
+
601
+ assets.push(...createdAssets)
408
602
  }
409
603
 
410
604
  await pushDocumentAssets('rule', localScan.rulePaths, 'workspace')
411
605
  await pushDocumentAssets('skill', localScan.skillPaths, 'workspace')
412
606
  await pushDocumentAssets('spec', localScan.specPaths, 'workspace')
413
- await pushDocumentAssets('entity', localScan.entityDocPaths, 'workspace')
607
+ await pushDocumentAssets('entity', localScan.entityDocPaths, 'workspace', undefined, parseEntityMarkdownDocument)
414
608
  await pushDocumentAssets('entity', localScan.entityJsonPaths, 'workspace', undefined, parseEntityIndexJson)
415
609
 
416
610
  for (let index = 0; index < flattenedPluginInstances.length; index++) {
@@ -419,9 +613,13 @@ export async function collectWorkspaceAssets(params: {
419
613
  await pushDocumentAssets('rule', scan.rulePaths, 'plugin', instance)
420
614
  await pushDocumentAssets('skill', scan.skillPaths, 'plugin', instance)
421
615
  await pushDocumentAssets('spec', scan.specPaths, 'plugin', instance)
422
- await pushDocumentAssets('entity', scan.entityDocPaths, 'plugin', instance)
616
+ await pushDocumentAssets('entity', scan.entityDocPaths, 'plugin', instance, parseEntityMarkdownDocument)
423
617
  await pushDocumentAssets('entity', scan.entityJsonPaths, 'plugin', instance, parseEntityIndexJson)
424
618
  }
619
+ await pushDocumentAssets('skill', homeSkillPaths, 'workspace', undefined, undefined, HOME_BRIDGE_RESOLVED_BY)
620
+
621
+ const skills = mergeSkillAssets(skillAssets)
622
+ assets.push(...skills)
425
623
 
426
624
  const mcpAssets = new Map<string, Extract<WorkspaceAsset, { kind: 'mcpServer' }>>()
427
625
  const addMcpAsset = (
@@ -507,6 +705,12 @@ export async function collectWorkspaceAssets(params: {
507
705
  .map(instance => createHookPluginAsset(instance))
508
706
  assets.push(...hookPlugins)
509
707
 
708
+ const workspaces = await resolveConfiguredWorkspaceAssets({
709
+ cwd: params.cwd,
710
+ configs: [config, userConfig]
711
+ })
712
+ assets.push(...workspaces)
713
+
510
714
  const opencodeOverlayAssets = flattenedPluginInstances.flatMap((instance, index) => (
511
715
  pluginOverlayScans[index].map((entry) =>
512
716
  createOpenCodeOverlayAsset({
@@ -528,13 +732,13 @@ export async function collectWorkspaceAssets(params: {
528
732
  const entities = assets.filter((asset): asset is Extract<WorkspaceAsset, { kind: 'entity' }> =>
529
733
  asset.kind === 'entity'
530
734
  )
531
- const skills = assets.filter((asset): asset is Extract<WorkspaceAsset, { kind: 'skill' }> => asset.kind === 'skill')
532
735
 
533
- assertNoDocumentConflicts([...rules, ...specs, ...entities, ...skills])
736
+ assertNoDocumentConflicts([...rules, ...specs, ...entities])
534
737
  assertNoMcpConflicts(Array.from(mcpAssets.values()))
535
738
 
536
739
  return {
537
740
  assets,
741
+ configs: [config, userConfig],
538
742
  defaultExcludeMcpServers: [
539
743
  ...(config?.defaultExcludeMcpServers ?? []),
540
744
  ...(userConfig?.defaultExcludeMcpServers ?? [])
@@ -551,6 +755,7 @@ export async function collectWorkspaceAssets(params: {
551
755
  pluginInstances,
552
756
  rules,
553
757
  skills,
554
- specs
758
+ specs,
759
+ workspaces
555
760
  }
556
761
  }
package/src/bundle.ts CHANGED
@@ -14,6 +14,7 @@ export async function resolveWorkspaceAssetBundle(params: {
14
14
 
15
15
  return {
16
16
  cwd: params.cwd,
17
+ configs: collected.configs,
17
18
  pluginConfigs: collected.pluginConfigs,
18
19
  pluginInstances: collected.pluginInstances,
19
20
  assets: collected.assets,
@@ -21,6 +22,7 @@ export async function resolveWorkspaceAssetBundle(params: {
21
22
  specs: collected.specs,
22
23
  entities: collected.entities,
23
24
  skills: collected.skills,
25
+ workspaces: collected.workspaces,
24
26
  mcpServers: collected.mcpServers,
25
27
  hookPlugins: collected.hookPlugins,
26
28
  opencodeOverlayAssets: collected.opencodeOverlayAssets,
@@ -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'
@@ -7,6 +7,8 @@ import {
7
7
  import type { Definition, Entity, Rule, Skill, Spec } from '@vibe-forge/types'
8
8
  import { CANONICAL_VIBE_FORGE_MCP_SERVER_NAME, resolvePromptPath } from '@vibe-forge/utils'
9
9
 
10
+ import { buildManagedTaskToolGuidance } from './task-tool-guidance'
11
+
10
12
  const toMarkdownBlockquote = (content: string) => (
11
13
  content
12
14
  .trim()
@@ -165,6 +167,7 @@ export const generateSpecRoutePrompt = (
165
167
  }
166
168
 
167
169
  export const generateEntitiesRoutePrompt = (entities: Definition<Entity>[]) => {
170
+ const taskToolGuidance = buildManagedTaskToolGuidance(CANONICAL_VIBE_FORGE_MCP_SERVER_NAME)
168
171
  return (
169
172
  '<system-prompt>\n' +
170
173
  'The project includes the following entities:\n' +
@@ -179,6 +182,7 @@ export const generateEntitiesRoutePrompt = (entities: Definition<Entity>[]) => {
179
182
  .join('')
180
183
  }\n` +
181
184
  `When solving user problems, you may specify entities through \`${CANONICAL_VIBE_FORGE_MCP_SERVER_NAME}.StartTasks\` as needed and have them coordinate multiple entity types to complete the work; use \`${CANONICAL_VIBE_FORGE_MCP_SERVER_NAME}.GetTaskInfo\` and \`wait\` to track progress.\n` +
185
+ `${taskToolGuidance}\n` +
182
186
  '</system-prompt>\n'
183
187
  )
184
188
  }
@@ -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 { expandSkillAssetDependenciesWithRegistry } 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 expandSkillAssetDependenciesWithRegistry({
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
  },