@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.
- package/__tests__/__snapshots__/workspace-assets-rich.snapshot.json +56 -1
- package/__tests__/adapter-asset-plan.spec.ts +218 -6
- package/__tests__/bundle.spec.ts +602 -2
- package/__tests__/prompt-builders.spec.ts +39 -0
- package/__tests__/prompt-selection.spec.ts +307 -0
- package/__tests__/snapshot.ts +1 -0
- package/__tests__/test-helpers.ts +9 -0
- package/__tests__/workspace-assets.snapshot.spec.ts +2 -2
- package/package.json +4 -4
- package/src/adapter-asset-plan.ts +42 -66
- package/src/asset-source.ts +13 -0
- package/src/bundle-internal.ts +226 -21
- package/src/bundle.ts +2 -0
- package/src/home-bridge.ts +1 -0
- package/src/index.ts +3 -0
- package/src/prompt-builders.ts +4 -0
- package/src/prompt-selection.ts +44 -19
- package/src/selection-internal.ts +335 -1
- package/src/skill-dependencies.ts +361 -0
- package/src/skill-registry.ts +329 -0
- package/src/task-tool-guidance.ts +15 -0
- package/src/workspace-config.ts +132 -0
- package/src/workspace-prompt.ts +33 -0
- package/src/workspaces.ts +188 -0
- package/vibe-forge-workspace-assets-2.0.2.tgz +0 -0
package/src/bundle-internal.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
|
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'
|
package/src/prompt-builders.ts
CHANGED
|
@@ -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
|
}
|
package/src/prompt-selection.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
.
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
},
|