@vibe-forge/workspace-assets 2.0.1 → 2.0.3

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.
@@ -9,6 +9,7 @@ import {
9
9
  generateSkillsRoutePrompt,
10
10
  generateSpecRoutePrompt
11
11
  } from '#~/prompt-builders.js'
12
+ import { generateWorkspaceRoutePrompt } from '#~/workspace-prompt.js'
12
13
 
13
14
  describe('workspace asset prompt builders', () => {
14
15
  it('builds skill prompts with stable names, descriptions, and relative paths', () => {
@@ -175,12 +176,50 @@ describe('workspace asset prompt builders', () => {
175
176
  expect(prompt).toContain('reviewer: 负责代码审查')
176
177
  expect(prompt).toContain('`VibeForge.StartTasks`')
177
178
  expect(prompt).toContain('`VibeForge.GetTaskInfo`')
179
+ expect(prompt).toContain('Task tool guide:')
180
+ expect(prompt).toContain('After starting a task')
181
+ expect(prompt).toContain('10 most recent log entries')
182
+ expect(prompt).toContain('`logLimit`')
183
+ expect(prompt).toContain('`logOrder`')
184
+ expect(prompt).toContain('`VibeForge.SendTaskMessage`')
185
+ expect(prompt).toContain('`{ taskId, message, mode }`')
186
+ expect(prompt).toContain('Choose `mode: "direct"`')
187
+ expect(prompt).toContain('Choose `mode: "steer"`')
188
+ expect(prompt).toContain('`VibeForge.SubmitTaskInput`')
189
+ expect(prompt).toContain('Do not use it for ordinary follow-up instructions or queued steer turns')
190
+ expect(prompt).toContain('If a task is `completed` or `failed`')
178
191
  expect(prompt).toContain('`wait`')
179
192
  expect(prompt).not.toContain('run-tasks')
180
193
  expect(prompt).not.toContain('需要关注变更风险')
181
194
  expect(prompt).not.toContain('hidden')
182
195
  })
183
196
 
197
+ it('builds workspace routes with managed task guidance', () => {
198
+ const cwd = '/tmp/project'
199
+
200
+ const prompt = generateWorkspaceRoutePrompt(cwd, [
201
+ {
202
+ id: 'billing',
203
+ cwd: join(cwd, 'packages/billing'),
204
+ path: join(cwd, '.ai/workspaces/billing.md'),
205
+ description: 'Billing workspace'
206
+ }
207
+ ])
208
+
209
+ expect(prompt).toContain('The project includes the following registered workspaces')
210
+ expect(prompt).toContain('Identifier: billing')
211
+ expect(prompt).toContain('`VibeForge.StartTasks`')
212
+ expect(prompt).toContain('type: "workspace"')
213
+ expect(prompt).toContain('Task tool guide:')
214
+ expect(prompt).toContain('`VibeForge.GetTaskInfo`')
215
+ expect(prompt).toContain('`VibeForge.ListTasks`')
216
+ expect(prompt).toContain('`VibeForge.SendTaskMessage`')
217
+ expect(prompt).toContain('`VibeForge.SubmitTaskInput`')
218
+ expect(prompt).toContain('Choose `mode: "direct"`')
219
+ expect(prompt).toContain('Choose `mode: "steer"`')
220
+ expect(prompt).toContain('Do not directly edit files inside a registered workspace')
221
+ })
222
+
184
223
  it('builds skill route prompts without preloading content', () => {
185
224
  const cwd = '/tmp/project'
186
225
 
@@ -0,0 +1,80 @@
1
+ import { join } from 'node:path'
2
+
3
+ import { afterEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ import { buildAdapterAssetPlan, resolveWorkspaceAssetBundle } from '#~/index.js'
6
+ import { normalizeSkillDependency } from '#~/skill-dependencies.js'
7
+
8
+ import { createWorkspace, writeDocument } from './test-helpers'
9
+
10
+ afterEach(() => {
11
+ vi.unstubAllGlobals()
12
+ })
13
+
14
+ describe('skill dependency normalization', () => {
15
+ it('parses legacy non-structured registry strings', () => {
16
+ expect(
17
+ normalizeSkillDependency('https://bnpm.byted.org@skills.byted.org/lynx/skills@lynx-devtool@1.0.3')
18
+ ).toEqual({
19
+ ref: 'https://bnpm.byted.org@skills.byted.org/lynx/skills@lynx-devtool@1.0.3',
20
+ name: 'lynx-devtool',
21
+ source: 'lynx/skills',
22
+ registry: 'https://skills.byted.org',
23
+ version: '1.0.3',
24
+ packageRegistry: 'https://bnpm.byted.org'
25
+ })
26
+ })
27
+
28
+ it('installs legacy non-structured registry dependencies through the skill registry API', async () => {
29
+ const workspace = await createWorkspace()
30
+ const fetchMock = vi.fn(async (url: string) => {
31
+ if (url === 'https://skills.byted.org/api/download/lynx/skills/lynx-devtool?version=1.0.3') {
32
+ return new Response(JSON.stringify({
33
+ files: [{
34
+ path: 'SKILL.md',
35
+ contents: '---\nname: lynx-devtool\ndescription: Lynx device debugging\n---\nUse devtool.\n'
36
+ }]
37
+ }))
38
+ }
39
+ return new Response('not found', { status: 404 })
40
+ })
41
+ vi.stubGlobal('fetch', fetchMock)
42
+
43
+ await writeDocument(
44
+ join(workspace, '.ai/skills/debug-lynx/SKILL.md'),
45
+ [
46
+ '---',
47
+ 'name: debug-lynx',
48
+ 'description: Debug Lynx on device',
49
+ 'dependencies:',
50
+ ' - https://bnpm.byted.org@skills.byted.org/lynx/skills@lynx-devtool@1.0.3',
51
+ '---',
52
+ 'Run the full debugging workflow.'
53
+ ].join('\n')
54
+ )
55
+
56
+ const bundle = await resolveWorkspaceAssetBundle({
57
+ cwd: workspace,
58
+ configs: [undefined, undefined],
59
+ useDefaultVibeForgeMcpServer: false
60
+ })
61
+
62
+ await buildAdapterAssetPlan({
63
+ adapter: 'opencode',
64
+ bundle,
65
+ options: {
66
+ skills: {
67
+ include: ['debug-lynx']
68
+ }
69
+ }
70
+ })
71
+
72
+ expect(fetchMock).toHaveBeenCalledWith(
73
+ 'https://skills.byted.org/api/download/lynx/skills/lynx-devtool?version=1.0.3',
74
+ expect.any(Object)
75
+ )
76
+ expect(bundle.skills.find(asset => asset.name === 'lynx-devtool')?.sourcePath).toContain(
77
+ '/.ai/caches/skill-dependencies/skills.byted.org/lynx/skills/lynx-devtool/1.0.3/'
78
+ )
79
+ })
80
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-forge/workspace-assets",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Workspace asset resolution and adapter asset planning for Vibe Forge",
5
5
  "imports": {
6
6
  "#~/*.js": {
@@ -30,9 +30,9 @@
30
30
  "fast-glob": "^3.3.3",
31
31
  "front-matter": "^4.0.2",
32
32
  "js-yaml": "^4.1.1",
33
- "@vibe-forge/utils": "^2.0.1",
34
- "@vibe-forge/types": "^2.0.1",
35
- "@vibe-forge/definition-core": "^2.0.1"
33
+ "@vibe-forge/types": "^2.0.2",
34
+ "@vibe-forge/utils": "^2.0.3",
35
+ "@vibe-forge/definition-core": "^2.0.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/js-yaml": "^4.0.9"
@@ -9,12 +9,7 @@ import {
9
9
  resolveDefaultVibeForgeMcpServerConfig
10
10
  } from '@vibe-forge/config'
11
11
  import type { Config, Definition, Entity, PluginConfig, WorkspaceAsset, WorkspaceAssetKind } from '@vibe-forge/types'
12
- import {
13
- isLegacySkillsConfig,
14
- resolveProjectAiBaseDir,
15
- resolveProjectAiEntitiesDir,
16
- resolveRelativePath
17
- } from '@vibe-forge/utils'
12
+ import { resolveProjectAiBaseDir, resolveProjectAiEntitiesDir, resolveRelativePath } from '@vibe-forge/utils'
18
13
  import { listManagedPluginInstalls, toManagedPluginConfig } from '@vibe-forge/utils/managed-plugin'
19
14
  import {
20
15
  flattenPluginInstances,
@@ -33,7 +28,6 @@ import {
33
28
  resolveSkillIdentifier,
34
29
  resolveSpecIdentifier
35
30
  } from '@vibe-forge/definition-core'
36
- import { ensureConfiguredProjectSkills } from './configured-skills'
37
31
  import { HOME_BRIDGE_RESOLVED_BY } from './home-bridge'
38
32
  import { resolveConfiguredWorkspaceAssets } from './workspaces'
39
33
 
@@ -159,8 +153,8 @@ const warnInvalidHomeSkillRoot = (root: string) => {
159
153
 
160
154
  const resolveHomeBridgeConfig = (configs: [Config?, Config?]) => {
161
155
  const [config, userConfig] = configs
162
- const projectHomeBridge = isLegacySkillsConfig(config?.skills) ? config.skills.homeBridge : undefined
163
- const userHomeBridge = isLegacySkillsConfig(userConfig?.skills) ? userConfig.skills.homeBridge : undefined
156
+ const projectHomeBridge = config?.skills?.homeBridge
157
+ const userHomeBridge = userConfig?.skills?.homeBridge
164
158
 
165
159
  return {
166
160
  enabled: userHomeBridge?.enabled ?? projectHomeBridge?.enabled ?? true,
@@ -532,8 +526,6 @@ export async function collectWorkspaceAssets(params: {
532
526
  plugins?: PluginConfig
533
527
  overlaySource?: string
534
528
  includeManagedPlugins?: boolean
535
- syncConfiguredSkills?: boolean
536
- updateConfiguredSkills?: boolean
537
529
  useDefaultVibeForgeMcpServer?: boolean
538
530
  }): Promise<{
539
531
  assets: WorkspaceAsset[]
@@ -552,13 +544,6 @@ export async function collectWorkspaceAssets(params: {
552
544
  workspaces: Array<Extract<WorkspaceAsset, { kind: 'workspace' }>>
553
545
  }> {
554
546
  const [config, userConfig] = params.configs ?? await loadWorkspaceConfig(params.cwd)
555
- if (params.syncConfiguredSkills === true) {
556
- await ensureConfiguredProjectSkills({
557
- configs: [config, userConfig],
558
- updateInstalledSkills: params.updateConfiguredSkills,
559
- workspaceFolder: params.cwd
560
- })
561
- }
562
547
  const managedPluginConfigs = params.includeManagedPlugins === false
563
548
  ? undefined
564
549
  : toManagedPluginConfig(await listManagedPluginInstalls(params.cwd))
package/src/bundle.ts CHANGED
@@ -8,8 +8,6 @@ export async function resolveWorkspaceAssetBundle(params: {
8
8
  plugins?: PluginConfig
9
9
  overlaySource?: string
10
10
  includeManagedPlugins?: boolean
11
- syncConfiguredSkills?: boolean
12
- updateConfiguredSkills?: boolean
13
11
  useDefaultVibeForgeMcpServer?: boolean
14
12
  }): Promise<WorkspaceAssetBundle> {
15
13
  const collected = await collectWorkspaceAssets(params)
@@ -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
  }
@@ -35,7 +35,7 @@ import {
35
35
  resolveSelectedSkillAssetsWithDependencies,
36
36
  toDocumentDefinitions
37
37
  } from './selection-internal'
38
- import { expandSkillAssetDependenciesWithRemoteResolution } from './skill-dependencies'
38
+ import { expandSkillAssetDependenciesWithRegistry } from './skill-dependencies'
39
39
  import { generateWorkspaceRoutePrompt } from './workspace-prompt'
40
40
 
41
41
  export async function resolvePromptAssetSelection(params: {
@@ -164,7 +164,7 @@ export async function resolvePromptAssetSelection(params: {
164
164
  resolveNamedAssets(effectiveBundle.skills, excludedRefs, targetInstancePath).map(asset => asset.id)
165
165
  )
166
166
 
167
- const expandedTargetSkills = await expandSkillAssetDependenciesWithRemoteResolution({
167
+ const expandedTargetSkills = await expandSkillAssetDependenciesWithRegistry({
168
168
  allAssets: effectiveBundle.assets,
169
169
  configs: effectiveBundle.configs ?? [undefined, undefined],
170
170
  cwd: effectiveBundle.cwd,
@@ -26,7 +26,7 @@ import {
26
26
  isRemoteRuleReference,
27
27
  parseScopedReference
28
28
  } from '@vibe-forge/definition-core'
29
- import { expandSkillAssetDependencies, expandSkillAssetDependenciesWithRemoteResolution } from './skill-dependencies'
29
+ import { expandSkillAssetDependencies, expandSkillAssetDependenciesWithRegistry } from './skill-dependencies'
30
30
 
31
31
  type DocumentAssetKind = Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>
32
32
  type DocumentAsset<TDefinition> = Extract<WorkspaceAsset, { kind: DocumentAssetKind }> & {
@@ -538,7 +538,7 @@ export const resolveSelectedSkillAssetsWithDependencies = async (
538
538
  const excluded = new Set(
539
539
  resolveNamedAssets(bundle.skills, selection?.exclude).map(asset => asset.id)
540
540
  )
541
- return await expandSkillAssetDependenciesWithRemoteResolution({
541
+ return await expandSkillAssetDependenciesWithRegistry({
542
542
  allAssets: bundle.assets,
543
543
  configs: bundle.configs ?? [undefined, undefined],
544
544
  cwd: bundle.cwd,
@@ -8,7 +8,7 @@ import type { Config, Definition, Skill, WorkspaceAsset } from '@vibe-forge/type
8
8
  import { resolveRelativePath } from '@vibe-forge/utils'
9
9
 
10
10
  import { HOME_BRIDGE_RESOLVED_BY } from './home-bridge'
11
- import { installSkillsCliDependency } from './skills-cli-dependency'
11
+ import { installRegistrySkillDependency } from './skill-registry'
12
12
 
13
13
  type SkillAsset = Extract<WorkspaceAsset, { kind: 'skill' }>
14
14
 
@@ -16,6 +16,9 @@ export interface NormalizedSkillDependency {
16
16
  ref: string
17
17
  name: string
18
18
  source?: string
19
+ registry?: string
20
+ version?: string
21
+ packageRegistry?: string
19
22
  }
20
23
 
21
24
  interface DependencyExpansionParams {
@@ -41,6 +44,29 @@ const toSkillSlug = (value: string) => (
41
44
  .replace(/^-|-$/g, '')
42
45
  )
43
46
 
47
+ const trimSlashes = (value: string) => value.replace(/^\/+|\/+$/g, '')
48
+
49
+ const normalizeLegacyUrl = (value: string) => {
50
+ const trimmed = value.trim()
51
+ if (trimmed === '') return undefined
52
+ const candidate = /^[a-z][a-z\d+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`
53
+ try {
54
+ return new URL(candidate)
55
+ } catch {
56
+ return undefined
57
+ }
58
+ }
59
+
60
+ const buildDependencyRef = (params: {
61
+ name: string
62
+ source?: string
63
+ version?: string
64
+ }) => (
65
+ params.source == null
66
+ ? (params.version == null ? params.name : `${params.name}@${params.version}`)
67
+ : `${params.source}@${params.name}${params.version == null ? '' : `@${params.version}`}`
68
+ )
69
+
44
70
  const resolveUniqueSkillByName = (assets: SkillAsset[], name: string) => {
45
71
  const nameSlug = toSkillSlug(name)
46
72
  const matches = assets.filter(asset => asset.name === name || toSkillSlug(asset.name) === nameSlug)
@@ -118,7 +144,7 @@ const parseFrontmatterSkill = async (path: string): Promise<Definition<Skill>> =
118
144
  }
119
145
  }
120
146
 
121
- const createResolvedSkillAsset = (params: {
147
+ const createRegistrySkillAsset = (params: {
122
148
  cwd: string
123
149
  definition: Definition<Skill>
124
150
  }) => {
@@ -137,8 +163,38 @@ const createResolvedSkillAsset = (params: {
137
163
  } satisfies SkillAsset
138
164
  }
139
165
 
166
+ const parseLegacyRegistryDependency = (value: string): NormalizedSkillDependency | undefined => {
167
+ const ref = value.trim()
168
+ const segments = ref.split('@').map(segment => segment.trim())
169
+ if (segments.length < 3) return undefined
170
+
171
+ const hasVersion = segments.length >= 4
172
+ const nameIndex = hasVersion ? segments.length - 2 : segments.length - 1
173
+ const packageRegistry = normalizeLegacyUrl(segments[0] ?? '')
174
+ const sourceUrl = normalizeLegacyUrl(segments.slice(1, nameIndex).join('@'))
175
+ const name = asNonEmptyString(segments[nameIndex])
176
+ const version = hasVersion ? asNonEmptyString(segments[segments.length - 1]) : undefined
177
+
178
+ if (packageRegistry == null || sourceUrl == null || name == null) return undefined
179
+
180
+ const source = trimSlashes(sourceUrl.pathname)
181
+ if (source === '') return undefined
182
+
183
+ return {
184
+ ref,
185
+ name,
186
+ source,
187
+ registry: sourceUrl.origin,
188
+ ...(version == null ? {} : { version }),
189
+ packageRegistry: packageRegistry.toString().replace(/\/+$/, '')
190
+ }
191
+ }
192
+
140
193
  const parseStringDependency = (value: string): NormalizedSkillDependency => {
141
194
  const ref = value.trim()
195
+ const legacyRegistryDependency = parseLegacyRegistryDependency(ref)
196
+ if (legacyRegistryDependency != null) return legacyRegistryDependency
197
+
142
198
  const atIndex = ref.lastIndexOf('@')
143
199
  if (atIndex > 0 && atIndex < ref.length - 1) {
144
200
  return {
@@ -148,15 +204,12 @@ const parseStringDependency = (value: string): NormalizedSkillDependency => {
148
204
  }
149
205
  }
150
206
 
151
- const sourcePathSegments = ref.split('/').filter(segment => segment.trim() !== '')
152
- if (
153
- sourcePathSegments.length >= 3 &&
154
- sourcePathSegments.every(segment => !segment.includes(' '))
155
- ) {
207
+ const sourcePathMatch = ref.match(/^([^/\s]+\/[^/\s]+)\/([^/\s]+)$/)
208
+ if (sourcePathMatch != null) {
156
209
  return {
157
210
  ref,
158
- source: sourcePathSegments.slice(0, -1).join('/'),
159
- name: sourcePathSegments[sourcePathSegments.length - 1]
211
+ source: sourcePathMatch[1],
212
+ name: sourcePathMatch[2]
160
213
  }
161
214
  }
162
215
 
@@ -176,10 +229,17 @@ export const normalizeSkillDependency = (value: unknown): NormalizedSkillDepende
176
229
  if (name == null) return undefined
177
230
 
178
231
  const source = asNonEmptyString(record.source)
232
+ const version = asNonEmptyString(record.version)
179
233
  return {
180
- ref: source == null ? name : `${source}@${name}`,
234
+ ref: buildDependencyRef({
235
+ name,
236
+ ...(source == null ? {} : { source }),
237
+ ...(version == null ? {} : { version })
238
+ }),
181
239
  name,
182
- ...(source == null ? {} : { source })
240
+ ...(source == null ? {} : { source }),
241
+ ...(asNonEmptyString(record.registry) == null ? {} : { registry: asNonEmptyString(record.registry) }),
242
+ ...(version == null ? {} : { version })
183
243
  }
184
244
  }
185
245
 
@@ -243,7 +303,7 @@ export const expandSkillAssetDependencies = (
243
303
  return selected
244
304
  }
245
305
 
246
- export const expandSkillAssetDependenciesWithRemoteResolution = async (
306
+ export const expandSkillAssetDependenciesWithRegistry = async (
247
307
  params: DependencyExpansionParams
248
308
  ) => {
249
309
  const selected: SkillAsset[] = []
@@ -261,16 +321,16 @@ export const expandSkillAssetDependenciesWithRemoteResolution = async (
261
321
  dependency: NormalizedSkillDependency,
262
322
  currentInstancePath?: string
263
323
  ) => {
264
- const fetchKey = dependency.ref
324
+ const fetchKey = `${dependency.registry ?? ''}:${dependency.ref}`
265
325
  if (!fetchedDependencyRefs.has(fetchKey)) {
266
326
  fetchedDependencyRefs.add(fetchKey)
267
- const installed = await installSkillsCliDependency({
327
+ const installed = await installRegistrySkillDependency({
268
328
  cwd: params.cwd,
269
329
  configs: params.configs,
270
330
  dependency
271
331
  })
272
332
  const definition = await parseFrontmatterSkill(installed.skillPath)
273
- const dependencyAsset = createResolvedSkillAsset({
333
+ const dependencyAsset = createRegistrySkillAsset({
274
334
  cwd: params.cwd,
275
335
  definition
276
336
  })
@@ -335,13 +395,14 @@ export const expandSkillAssetDependenciesWithRemoteResolution = async (
335
395
  const dependencyAsset = await installDependencyAsset(dependency, asset.instancePath).catch((error: unknown) => {
336
396
  if (
337
397
  localOrBridgedDependency != null &&
338
- dependency.source == null
398
+ dependency.source == null &&
399
+ dependency.registry == null
339
400
  ) {
340
401
  return localOrBridgedDependency
341
402
  }
342
403
  throw error
343
404
  }) ?? (
344
- dependency.source == null
405
+ dependency.source == null && dependency.registry == null
345
406
  ? localOrBridgedDependency
346
407
  : undefined
347
408
  )