@vibe-forge/workspace-assets 3.2.0 → 3.2.2-alpha.0

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,14 +1,9 @@
1
1
  /* eslint-disable max-lines -- dependency normalization and graph expansion share the same local helpers */
2
- import { readFile } from 'node:fs/promises'
3
-
4
- import fm from 'front-matter'
5
-
6
- import { parseScopedReference, resolveSkillIdentifier } from '@vibe-forge/definition-core'
7
- import type { Config, Definition, Skill, WorkspaceAsset } from '@vibe-forge/types'
8
- import { formatSkillsSpec, parseSkillsSpec, resolveRelativePath } from '@vibe-forge/utils'
2
+ import { parseScopedReference } from '@vibe-forge/definition-core'
3
+ import type { Config, Skill, WorkspaceAsset } from '@vibe-forge/types'
4
+ import { formatSkillsSpec, parseSkillsSpec } from '@vibe-forge/utils'
9
5
 
10
6
  import { HOME_BRIDGE_RESOLVED_BY } from './home-bridge'
11
- import { installSkillsCliDependency } from './skills-cli-dependency'
12
7
 
13
8
  type SkillAsset = Extract<WorkspaceAsset, { kind: 'skill' }>
14
9
 
@@ -106,39 +101,6 @@ const findSkillAssetByRef = (
106
101
  return resolveUniqueSkillByName(searchableAssets, ref)
107
102
  }
108
103
 
109
- const resolveDisplayName = (name: string, scope?: string) => (
110
- scope != null && scope.trim() !== '' ? `${scope}/${name}` : name
111
- )
112
-
113
- const parseFrontmatterSkill = async (path: string): Promise<Definition<Skill>> => {
114
- const content = await readFile(path, 'utf-8')
115
- const { body, attributes } = fm<Skill>(content)
116
- return {
117
- path,
118
- body,
119
- attributes
120
- }
121
- }
122
-
123
- const createResolvedSkillAsset = (params: {
124
- cwd: string
125
- definition: Definition<Skill>
126
- }) => {
127
- const name = resolveSkillIdentifier(params.definition.path, params.definition.attributes.name)
128
- const displayName = resolveDisplayName(name)
129
- return {
130
- id: `skill:workspace:workspace:${displayName}:${resolveRelativePath(params.cwd, params.definition.path)}`,
131
- kind: 'skill',
132
- name,
133
- displayName,
134
- origin: 'workspace',
135
- sourcePath: params.definition.path,
136
- payload: {
137
- definition: params.definition
138
- }
139
- } satisfies SkillAsset
140
- }
141
-
142
104
  export const normalizeSkillDependency = (value: unknown): NormalizedSkillDependency | undefined => {
143
105
  const stringValue = asNonEmptyString(value)
144
106
  if (stringValue != null) return parseSkillsSpec(stringValue)
@@ -230,8 +192,6 @@ export const expandSkillAssetDependenciesWithRemoteResolution = async (
230
192
  ) => {
231
193
  const selected: SkillAsset[] = []
232
194
  const seen = new Set<string>()
233
- const fetchedDependencyRefs = new Set<string>()
234
-
235
195
  const removeSupersededHomeBridgeSkill = (displayName: string) => {
236
196
  removeHomeBridgeSkillDuplicates(params.allAssets, displayName)
237
197
  removeHomeBridgeSkillDuplicates(params.skillAssets, displayName)
@@ -239,54 +199,6 @@ export const expandSkillAssetDependenciesWithRemoteResolution = async (
239
199
  removeHomeBridgeSkillDuplicates(selected, displayName)
240
200
  }
241
201
 
242
- const installDependencyAsset = async (
243
- dependency: NormalizedSkillDependency,
244
- currentInstancePath?: string
245
- ) => {
246
- const fetchKey = dependency.ref
247
- if (!fetchedDependencyRefs.has(fetchKey)) {
248
- fetchedDependencyRefs.add(fetchKey)
249
- const installed = await installSkillsCliDependency({
250
- cwd: params.cwd,
251
- configs: params.configs,
252
- dependency
253
- })
254
- const definition = await parseFrontmatterSkill(installed.skillPath)
255
- const dependencyAsset = createResolvedSkillAsset({
256
- cwd: params.cwd,
257
- definition
258
- })
259
- const existingAsset = findSkillDependencyAsset(
260
- params.skillAssets,
261
- dependency,
262
- currentInstancePath,
263
- { includeHomeBridge: false }
264
- ) ??
265
- params.skillAssets.find(existing => (
266
- existing.resolvedBy !== HOME_BRIDGE_RESOLVED_BY &&
267
- existing.displayName === dependencyAsset.displayName
268
- ))
269
- if (existingAsset != null) {
270
- removeSupersededHomeBridgeSkill(existingAsset.displayName)
271
- return existingAsset
272
- }
273
-
274
- removeSupersededHomeBridgeSkill(dependencyAsset.displayName)
275
- params.allAssets.push(dependencyAsset)
276
- params.skillAssets.push(dependencyAsset)
277
- return dependencyAsset
278
- }
279
-
280
- // After the first fetch attempt, reuse whichever asset is now visible in the
281
- // skill set: a newly installed registry skill, or a home-bridge fallback
282
- // that was accepted by an earlier plain-name dependency resolution.
283
- const resolvedAsset = findSkillDependencyAsset(params.skillAssets, dependency, currentInstancePath)
284
- if (resolvedAsset != null && resolvedAsset.resolvedBy !== HOME_BRIDGE_RESOLVED_BY) {
285
- removeSupersededHomeBridgeSkill(resolvedAsset.displayName)
286
- }
287
- return resolvedAsset
288
- }
289
-
290
202
  const addAsset = async (asset: SkillAsset): Promise<void> => {
291
203
  if (params.excludedIds?.has(asset.id)) return
292
204
  if (seen.has(asset.id)) return
@@ -311,25 +223,15 @@ export const expandSkillAssetDependenciesWithRemoteResolution = async (
311
223
  continue
312
224
  }
313
225
 
314
- // Keep registry-backed dependencies ahead of home-bridge skills. We only
315
- // fall back to a bridged home skill for plain-name dependencies when the
316
- // registry path is unavailable.
317
- const dependencyAsset = await installDependencyAsset(dependency, asset.instancePath).catch((error: unknown) => {
318
- if (
319
- localOrBridgedDependency != null &&
320
- dependency.source == null
321
- ) {
322
- return localOrBridgedDependency
323
- }
324
- throw error
325
- }) ?? (
326
- dependency.source == null
327
- ? localOrBridgedDependency
328
- : undefined
329
- )
226
+ const dependencyAsset = dependency.source == null
227
+ ? localOrBridgedDependency
228
+ : undefined
330
229
 
331
230
  if (dependencyAsset == null) {
332
- throw new Error(`Failed to resolve skill dependency ${dependency.ref} declared by ${asset.displayName}`)
231
+ throw new Error(
232
+ `Skill dependency ${dependency.ref} declared by ${asset.displayName} is missing. ` +
233
+ 'Run vf skills install or vf skills update to materialize project skill dependencies.'
234
+ )
333
235
  }
334
236
  await addAsset(dependencyAsset)
335
237
  }
@@ -1,19 +1,32 @@
1
+ import process from 'node:process'
2
+
3
+ export const resolveRuntimeProtocolCliCommand = (env: NodeJS.ProcessEnv = process.env) => {
4
+ const prefix = env.__VF_CLI_RESUME_COMMAND_PREFIX__?.trim()
5
+ if (prefix == null || prefix === '') {
6
+ return 'vf run'
7
+ }
8
+ return prefix.split(/\s+/).at(-1) === 'run' ? prefix : `${prefix} run`
9
+ }
10
+
1
11
  export const buildManagedTaskToolGuidance = () => {
12
+ const runtimeCliCommand = resolveRuntimeProtocolCliCommand()
13
+ const runtimeProtocolCommand = `${runtimeCliCommand} --input-format stream-json --output-format stream-json`
14
+
2
15
  return [
3
16
  'Agent runtime guide:',
4
- '- Use unified CLI protocol mode, `vf run --input-format stream-json --output-format stream-json`, to start a child runtime session when the work should run in a separate entity or continue independently from the current turn.',
5
- '- Send typed runtime protocol envelopes such as `session.start`, `session.message`, `session.status`, `session.events`, `session.submit`, and `session.stop`; do not treat dedicated `vf agent ...` subcommands as the standard integration surface.',
17
+ `- Use unified CLI protocol mode, \`${runtimeProtocolCommand}\`, to start a child runtime session when the work should run in a separate entity or continue independently from the current turn.`,
18
+ '- Send typed runtime protocol envelopes such as `session.start`, `session.message`, `session.status`, `session.events`, `session.submit`, and `session.stop`; do not treat dedicated agent subcommands as the standard integration surface.',
6
19
  '- Ordinary new sessions stay session-scoped. A room is created or discovered only when a unified CLI runtime protocol start command launches a child runtime session from a server-managed host session and the server projects runtime store metadata/events.',
7
- '- Do not use MCP task tools, `vf agent ...`, legacy StartTasks, hand-written DB edits, or ad-hoc TS scripts as the task consumer surface. Use CLI protocol mode and the runtime protocol/store for start, status, events, follow-up messages, input submission, and cancellation.',
20
+ '- Do not use MCP task tools, dedicated agent subcommands, legacy StartTasks, hand-written DB edits, or ad-hoc TS scripts as the task consumer surface. Use CLI protocol mode and the runtime protocol/store for start, status, events, follow-up messages, input submission, and cancellation.',
8
21
  '- Server-managed host sessions inject the current adapter, model, effort, and permission mode as runtime protocol defaults. Omit these fields to inherit the host selection, or set them explicitly only when a child task must use a different runtime profile.',
9
22
  '- Copyable JSONL example; write one `session.start` line per child task, and use multiple lines for multiple subtasks:',
10
23
  '```bash',
11
- "cat <<'JSONL' | vf run --input-format stream-json --output-format stream-json",
24
+ `cat <<'JSONL' | ${runtimeProtocolCommand}`,
12
25
  '{"commandId":"start-planner","type":"session.start","payload":{"title":"Plan Agent Room UI fix","message":"Plan the frontend changes and tests for the Agent Room UI fix.","entity":"dev-planner","background":true},"title":"Plan Agent Room UI fix","message":"Plan the frontend changes and tests for the Agent Room UI fix.","entity":"dev-planner","background":true}',
13
26
  '{"commandId":"start-reviewer","type":"session.start","payload":{"title":"Review Agent Room UI fix","message":"Review the implemented Agent Room UI fix for regressions and missing tests.","entity":"dev-reviewer","background":true},"title":"Review Agent Room UI fix","message":"Review the implemented Agent Room UI fix for regressions and missing tests.","entity":"dev-reviewer","background":true}',
14
27
  'JSONL',
15
28
  '```',
16
- '- Keep `payload.title`, `payload.message`, `payload.entity`, and `payload.background: true` explicit in each start envelope. The mirrored top-level fields make the JSONL executable by the current `vf run` protocol reader.',
29
+ '- Keep `payload.title`, `payload.message`, `payload.entity`, and `payload.background: true` explicit in each start envelope. The mirrored top-level fields make the JSONL executable by the current runtime protocol reader.',
17
30
  '- Include a short `title` when the task prompt is long; it becomes the child session title and room run label. Put any room or workspace context in the title and initial message.',
18
31
  '- Read the returned `sessionId` and use it for follow-up protocol commands. Read the latest runtime snapshot from the runtime store or a `session.status` protocol command, and read progress from runtime events or a `session.events` protocol command.',
19
32
  '- Use a follow/read-events workflow when you need to watch progress instead of repeatedly restarting work.',
@@ -21,7 +34,7 @@ export const buildManagedTaskToolGuidance = () => {
21
34
  '- When the chat UI sends a `[ROOM_TASK_MESSAGE] ... [/ROOM_TASK_MESSAGE]` block, treat it as a runtime relay envelope instead of ordinary prose. Parse the `sessionId` or legacy `taskId`, `message`, and optional `mode` / `request` fields. If the envelope indicates `mode: interaction`, or runtime status shows `waiting_input` / pending input, use a `session.submit` protocol command. Otherwise use `session.message`. Do not reply inline instead of routing the relay.',
22
35
  '- Use `session.submit` only when a runtime session is waiting for an explicit input or approval request. Do not use it for ordinary follow-up instructions.',
23
36
  '- Use a `session.stop` protocol command for graceful cancellation and set `mode` to `force` only when stop cannot recover the session.',
24
- '- Compatibility aliases such as `vf agent start/status/events/send/submit/stop` may exist for debugging or legacy scripts, but they are not the primary guidance for new agent workflows.',
37
+ '- Compatibility aliases such as dedicated agent start/status/events/send/submit/stop subcommands may exist for debugging or legacy scripts, but they are not the primary guidance for new agent workflows.',
25
38
  '- When a session is still making progress, use `wait` between checks and inspect status/events instead of starting a replacement session.'
26
39
  ].join('\n')
27
40
  }
@@ -1,7 +1,7 @@
1
1
  import type { WorkspaceDefinitionPayload } from '@vibe-forge/types'
2
2
  import { resolvePromptPath } from '@vibe-forge/utils'
3
3
 
4
- import { buildManagedTaskToolGuidance } from './task-tool-guidance'
4
+ import { buildManagedTaskToolGuidance, resolveRuntimeProtocolCliCommand } from './task-tool-guidance'
5
5
 
6
6
  export const generateWorkspaceRoutePrompt = (
7
7
  cwd: string,
@@ -9,6 +9,8 @@ export const generateWorkspaceRoutePrompt = (
9
9
  ) => {
10
10
  if (workspaces.length === 0) return ''
11
11
  const taskToolGuidance = buildManagedTaskToolGuidance()
12
+ const runtimeProtocolCommand =
13
+ `${resolveRuntimeProtocolCliCommand()} --input-format stream-json --output-format stream-json`
12
14
 
13
15
  const workspaceList = workspaces
14
16
  .map((workspace) => {
@@ -25,7 +27,7 @@ export const generateWorkspaceRoutePrompt = (
25
27
  '<system-prompt>\n' +
26
28
  'The project includes the following registered workspaces:\n' +
27
29
  `${workspaceList}\n` +
28
- 'When a user request targets one of these workspaces, start a child runtime session with `vf run --input-format stream-json --output-format stream-json` and a `session.start` envelope; include the workspace identifier and path in the title and message. ' +
30
+ `When a user request targets one of these workspaces, start a child runtime session with \`${runtimeProtocolCommand}\` and a \`session.start\` envelope; include the workspace identifier and path in the title and message. ` +
29
31
  'Do not directly edit files inside a registered workspace from the current session unless the user explicitly asks this session to work in that directory.\n' +
30
32
  `${taskToolGuidance}\n` +
31
33
  '</system-prompt>\n'
@@ -1,94 +0,0 @@
1
- import { access, copyFile, lstat, mkdir, readdir } from 'node:fs/promises'
2
- import { dirname, resolve } from 'node:path'
3
- import process from 'node:process'
4
-
5
- import { withDirectoryInstallLock } from '@vibe-forge/utils/install-lock'
6
- import { resolveProjectSharedCachePath } from '@vibe-forge/utils/project-cache-path'
7
- import { toSkillSlug } from '@vibe-forge/utils/skills-cli'
8
-
9
- const toCacheSegment = (value: string) => (
10
- value
11
- .trim()
12
- .toLowerCase()
13
- .replace(/[^a-z0-9._-]+/g, '-')
14
- .replace(/^-+|-+$/g, '') || 'default'
15
- )
16
-
17
- export const pathExists = async (targetPath: string) => {
18
- try {
19
- await access(targetPath)
20
- return true
21
- } catch {
22
- return false
23
- }
24
- }
25
-
26
- export const withInstallLock = async <T>(lockDir: string, callback: () => Promise<T>) => {
27
- try {
28
- return await withDirectoryInstallLock({ lockDir }, callback)
29
- } catch (error) {
30
- const message = error instanceof Error ? error.message : String(error)
31
- throw new Error(
32
- message.replace('Timed out waiting for install lock', 'Timed out waiting for skill dependency install lock')
33
- )
34
- }
35
- }
36
-
37
- export const copyRegularFiles = async (sourceDir: string, targetDir: string) => {
38
- let fileCount = 0
39
- const entries = await readdir(sourceDir, { withFileTypes: true })
40
-
41
- await mkdir(targetDir, { recursive: true })
42
-
43
- for (const entry of entries) {
44
- const sourcePath = resolve(sourceDir, entry.name)
45
- const targetPath = resolve(targetDir, entry.name)
46
- const stat = await lstat(sourcePath)
47
-
48
- if (stat.isDirectory()) {
49
- fileCount += await copyRegularFiles(sourcePath, targetPath)
50
- continue
51
- }
52
-
53
- if (!stat.isFile()) continue
54
-
55
- await mkdir(dirname(targetPath), { recursive: true })
56
- await copyFile(sourcePath, targetPath)
57
- fileCount += 1
58
- }
59
-
60
- return fileCount
61
- }
62
-
63
- export const pickSearchResult = <T extends { skill: string }>(
64
- results: T[],
65
- name: string
66
- ) => {
67
- const slug = toSkillSlug(name)
68
- return results.find(result => (
69
- result.skill === name ||
70
- toSkillSlug(result.skill) === slug
71
- )) ?? results[0]
72
- }
73
-
74
- export const buildInstallDir = (params: {
75
- cwd: string
76
- registry?: string
77
- skill: string
78
- source: string
79
- version?: string
80
- }) => {
81
- const registry = params.registry ?? 'default'
82
- return resolveProjectSharedCachePath(
83
- params.cwd,
84
- process.env,
85
- 'skill-dependencies',
86
- 'skills-cli',
87
- toCacheSegment('skills'),
88
- toCacheSegment('latest'),
89
- toCacheSegment(registry),
90
- ...params.source.split('/').map(toCacheSegment),
91
- toCacheSegment(params.version ?? 'latest'),
92
- toCacheSegment(params.skill)
93
- )
94
- }
@@ -1,125 +0,0 @@
1
- import { mkdir, rename, rm } from 'node:fs/promises'
2
- import { resolve } from 'node:path'
3
- import process from 'node:process'
4
-
5
- import type { Config } from '@vibe-forge/types'
6
- import { findSkillsCli, installSkillsCliRefToTemp, installSkillsCliSkillToTemp } from '@vibe-forge/utils/skills-cli'
7
-
8
- import type { NormalizedSkillDependency } from './skill-dependencies'
9
- import {
10
- buildInstallDir,
11
- copyRegularFiles,
12
- pathExists,
13
- pickSearchResult,
14
- withInstallLock
15
- } from './skills-cli-dependency-helpers'
16
-
17
- const resolveAutoDownloadDependenciesEnabled = (
18
- projectConfig: Config | undefined,
19
- userConfig: Config | undefined
20
- ) => userConfig?.skills?.autoDownloadDependencies ?? projectConfig?.skills?.autoDownloadDependencies ?? true
21
-
22
- export const installSkillsCliDependency = async (params: {
23
- cwd: string
24
- configs: [Config?, Config?]
25
- dependency: NormalizedSkillDependency
26
- }) => {
27
- const [projectConfig, userConfig] = params.configs
28
- const autoDownloadDependenciesEnabled = resolveAutoDownloadDependenciesEnabled(projectConfig, userConfig)
29
- const resolvedTarget = await (async () => {
30
- if (params.dependency.source != null) {
31
- return {
32
- skill: params.dependency.name,
33
- source: params.dependency.source
34
- }
35
- }
36
-
37
- if (!autoDownloadDependenciesEnabled) {
38
- throw new Error(
39
- `Skill dependency automatic downloads are disabled; cannot resolve ${params.dependency.ref} without a source`
40
- )
41
- }
42
-
43
- return await (async () => {
44
- const searchResults = await findSkillsCli({
45
- registry: params.dependency.registry,
46
- query: params.dependency.name
47
- })
48
- const selected = pickSearchResult(searchResults, params.dependency.name)
49
- if (selected == null) {
50
- throw new Error(`Skill ${params.dependency.name} was not found by the skills CLI search.`)
51
- }
52
-
53
- return {
54
- installRef: selected.installRef,
55
- skill: selected.skill,
56
- source: selected.source
57
- }
58
- })()
59
- })()
60
-
61
- const installDir = buildInstallDir({
62
- cwd: params.cwd,
63
- registry: params.dependency.registry,
64
- skill: resolvedTarget.skill,
65
- source: resolvedTarget.source,
66
- version: params.dependency.version
67
- })
68
- const skillPath = resolve(installDir, 'SKILL.md')
69
-
70
- return await withInstallLock(`${installDir}.lock`, async () => {
71
- if (await pathExists(skillPath)) {
72
- return {
73
- installDir,
74
- skillPath
75
- }
76
- }
77
-
78
- if (!autoDownloadDependenciesEnabled) {
79
- throw new Error(`Skill dependency automatic downloads are disabled; cache not found for ${params.dependency.ref}`)
80
- }
81
-
82
- const tempInstallDir = `${installDir}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`
83
- await rm(tempInstallDir, { recursive: true, force: true })
84
- await mkdir(tempInstallDir, { recursive: true })
85
-
86
- const installResult = 'installRef' in resolvedTarget
87
- ? params.dependency.version == null
88
- ? await installSkillsCliRefToTemp({
89
- installRef: resolvedTarget.installRef,
90
- registry: params.dependency.registry
91
- })
92
- : await installSkillsCliSkillToTemp({
93
- registry: params.dependency.registry,
94
- skill: resolvedTarget.skill,
95
- source: resolvedTarget.source,
96
- version: params.dependency.version
97
- })
98
- : await installSkillsCliSkillToTemp({
99
- registry: params.dependency.registry,
100
- skill: resolvedTarget.skill,
101
- source: resolvedTarget.source,
102
- version: params.dependency.version
103
- })
104
-
105
- try {
106
- await copyRegularFiles(installResult.installedSkill.sourcePath, tempInstallDir)
107
- if (!await pathExists(resolve(tempInstallDir, 'SKILL.md'))) {
108
- throw new Error(`Skill dependency ${params.dependency.ref} did not include SKILL.md`)
109
- }
110
-
111
- await rm(installDir, { recursive: true, force: true })
112
- await rename(tempInstallDir, installDir)
113
- } catch (error) {
114
- await rm(tempInstallDir, { recursive: true, force: true })
115
- throw error
116
- } finally {
117
- await rm(installResult.tempDir, { recursive: true, force: true })
118
- }
119
-
120
- return {
121
- installDir,
122
- skillPath
123
- }
124
- })
125
- }