@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
|
@@ -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, expandSkillAssetDependenciesWithRegistry } 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
|
|
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 expandSkillAssetDependenciesWithRegistry({
|
|
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 = (
|