@vibe-forge/workspace-assets 3.1.0 → 3.1.1-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.
@@ -467,7 +467,7 @@
467
467
  ]
468
468
  },
469
469
  "options": {
470
- "systemPrompt": "<system-prompt>\nThe project system rules are:\n# review\n\n> 必须检查发布改动的回归风险。\n\n# demo/security\n\n> Use when: 插件安全规则\n> Rule file path: node_modules/@vibe-forge/plugin-demo/rules/security.md\n> Only read this rule file when the task matches the scenario above.\n</system-prompt>\n\n\n<system-prompt>\nThe following skill modules are loaded for the project:\n# research\n\n> Skill description: 检索资料\n> Skill file path: .ai/skills/research/SKILL.md\n> Resolve relative paths in the resource content relative to the directory containing this skill file.\n\n<skill-content>\n先阅读 README.md,再补充结论。\n</skill-content>\n</system-prompt>\n\n\n<system-prompt>\nThe project includes the following entities:\n - architect: 负责拆解方案的实体\n\nWhen solving user problems, you may specify entities through `VibeForge.StartTasks` as needed and have them coordinate multiple entity types to complete the work; use `VibeForge.GetTaskInfo` and `wait` to track progress.\nTask tool guide:\n- Use `VibeForge.StartTasks` to start a new child task when the work should run in a separate entity or workspace, or when it needs to continue independently from the current turn.\n- After starting a task, use `VibeForge.GetTaskInfo` with `{ taskId }` to inspect one task. It is also the right tool when a task seems stalled, failed, or might be waiting for input.\n- By default, `GetTaskInfo` returns the 10 most recent log entries in descending order, so newer entries appear earlier in the `logs` array. Pass `logLimit` to inspect a different number of recent logs, and set `logOrder` to `\"asc\"` when you want the selected log window in oldest-to-newest order.\n- Use `VibeForge.ListTasks` with the same `logLimit` and `logOrder` fields when you need to find a taskId or inspect multiple tasks at once.\n- Use `VibeForge.SendTaskMessage` with `{ taskId, message }` when you need to give a task another instruction. Running tasks continue immediately; completed or failed tasks resume the same conversation instead of forcing a replacement task.\n- Use `VibeForge.SubmitTaskInput` only when `VibeForge.GetTaskInfo` or `VibeForge.ListTasks` shows `pendingInput` or `pendingInteraction`, or the task status is `waiting_input`. Do not use it for ordinary follow-up instructions.\n- If a task is `completed` or `failed` but you want to keep working in that same thread of execution, prefer `VibeForge.SendTaskMessage` to resume it. Start a new task only when you actually want a replacement task.\n- When a task is still making progress, use `wait` between checks instead of repeatedly restarting it.\n</system-prompt>\n\n\n<system-prompt>\nYou are a professional project execution manager who can skillfully direct other entities to work toward your goal. Expectations:\n\n- Never complete code development work alone\n- You must coordinate other developers to complete tasks\n- You must keep them aligned with the goal and verify that their completion reports meet the requirements\n\nChoose the appropriate workflow based on the user's needs and the actual development goal, and use the workflow identifier to locate and load the corresponding definition.\n- Pass the identifier based on the actual need. This is not a path; use the standard workflow loading capability to resolve it.\n- Decide how to pass parameters based on their descriptions and actual usage scenarios.\nThe project includes the following workflows:\n- Workflow name: release\n - Description: 正式发布流程\n - Identifier: release\n - Parameters:\n - None\n\n- Workflow name: demo/release\n - Description: 插件发布流程\n - Identifier: demo/release\n - Parameters:\n - None\n\n</system-prompt>\n\n\n执行正式发布,并整理变更摘要。",
470
+ "systemPrompt": "<system-prompt>\nThe project system rules are:\n# review\n\n> 必须检查发布改动的回归风险。\n\n# demo/security\n\n> Use when: 插件安全规则\n> Rule file path: node_modules/@vibe-forge/plugin-demo/rules/security.md\n> Only read this rule file when the task matches the scenario above.\n</system-prompt>\n\n\n<system-prompt>\nThe following skill modules are loaded for the project:\n# research\n\n> Skill description: 检索资料\n> Skill file path: .ai/skills/research/SKILL.md\n> Resolve relative paths in the resource content relative to the directory containing this skill file.\n\n<skill-content>\n先阅读 README.md,再补充结论。\n</skill-content>\n</system-prompt>\n\n\n<system-prompt>\nThe project includes the following entities:\n - architect: 负责拆解方案的实体\n\nWhen solving user problems, you may start child runtime sessions with `vf run --input-format stream-json --output-format stream-json` and a `session.start` protocol envelope as needed; use `session.status`, `session.events`, and `wait` to track progress.\nAgent runtime guide:\n- 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.\n- 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.\n- 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.\n- 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.\n- 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.\n- Copyable JSONL example; write one `session.start` line per child task, and use multiple lines for multiple subtasks:\n```bash\ncat <<'JSONL' | vf run --input-format stream-json --output-format stream-json\n{\"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}\n{\"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}\nJSONL\n```\n- 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.\n- 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.\n- 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.\n- Use a follow/read-events workflow when you need to watch progress instead of repeatedly restarting work.\n- Use a `session.message` protocol command to give an existing session another instruction. Running sessions continue immediately; completed or failed sessions resume the same conversation when the runtime allows resume.\n- 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.\n- 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.\n- Use a `session.stop` protocol command for graceful cancellation and set `mode` to `force` only when stop cannot recover the session.\n- 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.\n- When a session is still making progress, use `wait` between checks and inspect status/events instead of starting a replacement session.\n</system-prompt>\n\n\n<system-prompt>\nYou are a professional project execution manager who can skillfully direct other entities to work toward your goal. Expectations:\n\n- Never complete code development work alone\n- You must coordinate other developers to complete tasks\n- You must keep them aligned with the goal and verify that their completion reports meet the requirements\n\nChoose the appropriate workflow based on the user's needs and the actual development goal, and use the workflow identifier to locate and load the corresponding definition.\n- Pass the identifier based on the actual need. This is not a path; use the standard workflow loading capability to resolve it.\n- Decide how to pass parameters based on their descriptions and actual usage scenarios.\nThe project includes the following workflows:\n- Workflow name: release\n - Description: 正式发布流程\n - Identifier: release\n - Parameters:\n - None\n\n- Workflow name: demo/release\n - Description: 插件发布流程\n - Identifier: demo/release\n - Parameters:\n - None\n\n</system-prompt>\n\n\n执行正式发布,并整理变更摘要。",
471
471
  "tools": {
472
472
  "include": [
473
473
  "Edit",
@@ -280,64 +280,6 @@ describe('buildAdapterAssetPlan', () => {
280
280
  ]))
281
281
  })
282
282
 
283
- it('keeps explicit skills CLI dependencies ahead of preselected home-bridged skills in overlays', async () => {
284
- const workspace = await createWorkspace()
285
- const realHome = process.env.__VF_PROJECT_REAL_HOME__
286
- const tempInstallDir = join(workspace, '.tmp-install-skills-cli-foo')
287
- const installedSkillDir = join(tempInstallDir, '.agents', 'skills', 'foo')
288
- await writeDocument(
289
- join(installedSkillDir, 'SKILL.md'),
290
- '---\nname: foo\ndescription: Registry foo\n---\nUse the registry definition.\n'
291
- )
292
- skillsCliMocks.installSkillsCliSkillToTemp.mockResolvedValue({
293
- tempDir: tempInstallDir,
294
- installedSkill: {
295
- dirName: 'foo',
296
- name: 'foo',
297
- sourcePath: installedSkillDir
298
- }
299
- })
300
-
301
- await writeDocument(
302
- join(realHome!, '.agents/skills/foo/SKILL.md'),
303
- '---\ndescription: Home foo\n---\nUse the home definition.\n'
304
- )
305
- await writeDocument(
306
- join(workspace, '.ai/skills/app-builder/SKILL.md'),
307
- [
308
- '---',
309
- 'name: app-builder',
310
- 'description: Build apps',
311
- 'dependencies:',
312
- ' - anthropics/skills@foo',
313
- '---',
314
- 'Build the app.'
315
- ].join('\n')
316
- )
317
-
318
- const bundle = await resolveWorkspaceAssetBundle({
319
- cwd: workspace,
320
- configs: [undefined, undefined],
321
- useDefaultVibeForgeMcpServer: false
322
- })
323
- const plan = await buildAdapterAssetPlan({
324
- adapter: 'opencode',
325
- bundle,
326
- options: {
327
- skills: {
328
- include: ['foo', 'app-builder']
329
- }
330
- }
331
- })
332
-
333
- const fooOverlays = plan.overlays.filter(entry => entry.kind === 'skill' && entry.targetPath === 'skills/foo')
334
- expect(fooOverlays).toHaveLength(1)
335
- expect(fooOverlays[0]?.sourcePath).toContain(
336
- '/.ai/caches/skill-dependencies/skills-cli/skills/latest/default/anthropics/skills/latest/foo'
337
- )
338
- expect(fooOverlays[0]?.sourcePath).not.toBe(join(realHome!, '.agents/skills/foo'))
339
- })
340
-
341
283
  it('prunes excluded skill dependency subtrees from selected native overlays', async () => {
342
284
  const workspace = await createWorkspace()
343
285
 
@@ -386,79 +386,6 @@ describe('resolveWorkspaceAssetBundle', () => {
386
386
  }))
387
387
  })
388
388
 
389
- it('installs selected missing skill dependencies from the skills CLI cache', async () => {
390
- const workspace = await createWorkspace()
391
- const realHome = process.env.__VF_PROJECT_REAL_HOME__
392
- const tempInstallDir = join(workspace, '.tmp-install-skills-cli')
393
- const installedSkillDir = join(tempInstallDir, '.agents', 'skills', 'frontend-design')
394
- await writeDocument(
395
- join(installedSkillDir, 'SKILL.md'),
396
- '---\nname: frontend-design\ndescription: UI design guidance\n---\nUse strong visual hierarchy.\n'
397
- )
398
- skillsCliMocks.installSkillsCliSkillToTemp.mockResolvedValue({
399
- tempDir: tempInstallDir,
400
- installedSkill: {
401
- dirName: 'frontend-design',
402
- name: 'frontend-design',
403
- sourcePath: installedSkillDir
404
- }
405
- })
406
-
407
- await writeDocument(
408
- join(realHome!, '.agents/skills/frontend-design/SKILL.md'),
409
- '---\ndescription: home frontend design\n---\nUse the home definition.'
410
- )
411
- await writeDocument(
412
- join(workspace, '.ai/skills/app-builder/SKILL.md'),
413
- [
414
- '---',
415
- 'name: app-builder',
416
- 'description: Build apps',
417
- 'dependencies:',
418
- ' - anthropics/skills@frontend-design',
419
- '---',
420
- 'Build the app.'
421
- ].join('\n')
422
- )
423
-
424
- const bundle = await resolveWorkspaceAssetBundle({
425
- cwd: workspace,
426
- configs: [undefined, undefined],
427
- useDefaultVibeForgeMcpServer: false
428
- })
429
-
430
- expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['app-builder', 'frontend-design'])
431
- expect(bundle.skills.find(asset => asset.name === 'frontend-design')).toEqual(expect.objectContaining({
432
- resolvedBy: 'home-bridge',
433
- sourcePath: join(realHome!, '.agents/skills/frontend-design/SKILL.md')
434
- }))
435
-
436
- await buildAdapterAssetPlan({
437
- adapter: 'opencode',
438
- bundle,
439
- options: {
440
- skills: {
441
- include: ['app-builder']
442
- }
443
- }
444
- })
445
-
446
- const dependency = bundle.skills.find(asset => asset.name === 'frontend-design')
447
- expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['app-builder', 'frontend-design'])
448
- expect(dependency?.sourcePath).toContain(
449
- '/.ai/caches/skill-dependencies/skills-cli/skills/latest/default/anthropics/skills/latest/frontend-design/'
450
- )
451
- expect(bundle.skills.find(asset => (
452
- asset.name === 'frontend-design' && asset.resolvedBy === 'home-bridge'
453
- ))).toBeUndefined()
454
- expect(skillsCliMocks.findSkillsCli).not.toHaveBeenCalled()
455
- expect(skillsCliMocks.installSkillsCliSkillToTemp).toHaveBeenCalledWith({
456
- config: undefined,
457
- skill: 'frontend-design',
458
- source: 'anthropics/skills'
459
- })
460
- })
461
-
462
389
  it('installs configured project skills before bundle resolution and rewrites renamed skill names', async () => {
463
390
  const workspace = await createWorkspace()
464
391
  const tempInstallDir = join(workspace, '.tmp-configured-install')
@@ -564,171 +491,6 @@ describe('resolveWorkspaceAssetBundle', () => {
564
491
  )
565
492
  })
566
493
 
567
- it('installs skill dependencies into the primary workspace shared cache', async () => {
568
- const primary = await createWorkspace()
569
- const worktree = await createWorkspace()
570
- const previousPrimaryWorkspace = process.env.__VF_PROJECT_PRIMARY_WORKSPACE_FOLDER__
571
- const tempInstallDir = join(worktree, '.tmp-install-skills-cli')
572
- const installedSkillDir = join(tempInstallDir, '.agents', 'skills', 'frontend-design')
573
- await writeDocument(
574
- join(installedSkillDir, 'SKILL.md'),
575
- '---\nname: frontend-design\ndescription: UI design guidance\n---\nUse primary cache.\n'
576
- )
577
- skillsCliMocks.installSkillsCliSkillToTemp.mockResolvedValue({
578
- tempDir: tempInstallDir,
579
- installedSkill: {
580
- dirName: 'frontend-design',
581
- name: 'frontend-design',
582
- sourcePath: installedSkillDir
583
- }
584
- })
585
-
586
- try {
587
- process.env.__VF_PROJECT_PRIMARY_WORKSPACE_FOLDER__ = primary
588
- await writeDocument(
589
- join(worktree, '.ai/skills/app-builder/SKILL.md'),
590
- [
591
- '---',
592
- 'name: app-builder',
593
- 'description: Build apps',
594
- 'dependencies:',
595
- ' - anthropics/skills@frontend-design',
596
- '---',
597
- 'Build the app.'
598
- ].join('\n')
599
- )
600
-
601
- const bundle = await resolveWorkspaceAssetBundle({
602
- cwd: worktree,
603
- configs: [undefined, undefined],
604
- useDefaultVibeForgeMcpServer: false
605
- })
606
-
607
- await buildAdapterAssetPlan({
608
- adapter: 'opencode',
609
- bundle,
610
- options: {
611
- skills: {
612
- include: ['app-builder']
613
- }
614
- }
615
- })
616
-
617
- const dependency = bundle.skills.find(asset => asset.name === 'frontend-design')
618
- expect(dependency?.sourcePath).toContain(join(
619
- primary,
620
- '.ai/caches/skill-dependencies/skills-cli/skills/latest/default/anthropics/skills/latest/frontend-design/'
621
- ))
622
- expect(dependency?.sourcePath).not.toContain(join(worktree, '.ai/caches'))
623
- } finally {
624
- if (previousPrimaryWorkspace == null) {
625
- delete process.env.__VF_PROJECT_PRIMARY_WORKSPACE_FOLDER__
626
- } else {
627
- process.env.__VF_PROJECT_PRIMARY_WORKSPACE_FOLDER__ = previousPrimaryWorkspace
628
- }
629
- }
630
- })
631
-
632
- it('reuses complete skill dependency caches without deleting or downloading them again', async () => {
633
- const workspace = await createWorkspace()
634
-
635
- const cachedSkillPath = join(
636
- workspace,
637
- '.ai/caches/skill-dependencies/skills-cli/skills/latest/default/anthropics/skills/latest/frontend-design/SKILL.md'
638
- )
639
- await writeDocument(
640
- cachedSkillPath,
641
- '---\nname: frontend-design\ndescription: Cached UI guidance\n---\nUse the cached copy.\n'
642
- )
643
- await writeDocument(
644
- join(workspace, '.ai/skills/app-builder/SKILL.md'),
645
- [
646
- '---',
647
- 'name: app-builder',
648
- 'description: Build apps',
649
- 'dependencies:',
650
- ' - anthropics/skills@frontend-design',
651
- '---',
652
- 'Build the app.'
653
- ].join('\n')
654
- )
655
-
656
- const bundle = await resolveWorkspaceAssetBundle({
657
- cwd: workspace,
658
- configs: [undefined, undefined],
659
- useDefaultVibeForgeMcpServer: false
660
- })
661
-
662
- await buildAdapterAssetPlan({
663
- adapter: 'opencode',
664
- bundle,
665
- options: {
666
- skills: {
667
- include: ['app-builder']
668
- }
669
- }
670
- })
671
-
672
- expect(skillsCliMocks.findSkillsCli).not.toHaveBeenCalled()
673
- expect(skillsCliMocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
674
- expect(await readFile(cachedSkillPath, 'utf8')).toContain('Use the cached copy.')
675
- expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['app-builder', 'frontend-design'])
676
- })
677
-
678
- it('parses registry/source/version dependency specs and forwards them to the skills CLI installer', async () => {
679
- const workspace = await createWorkspace()
680
- const tempInstallDir = join(workspace, '.tmp-install-skills-cli')
681
- const installedSkillDir = join(tempInstallDir, '.agents', 'skills', 'frontend-design')
682
- await writeDocument(
683
- join(installedSkillDir, 'SKILL.md'),
684
- '---\nname: frontend-design\ndescription: UI design guidance\n---\nUse internal design tokens.\n'
685
- )
686
- skillsCliMocks.installSkillsCliSkillToTemp.mockResolvedValue({
687
- tempDir: tempInstallDir,
688
- installedSkill: {
689
- dirName: 'frontend-design',
690
- name: 'frontend-design',
691
- sourcePath: installedSkillDir
692
- }
693
- })
694
-
695
- await writeDocument(
696
- join(workspace, '.ai/skills/app-builder/SKILL.md'),
697
- [
698
- '---',
699
- 'name: app-builder',
700
- 'description: Build apps',
701
- 'dependencies:',
702
- ' - https://registry.example.com@example-source/default/public@frontend-design@1.0.3',
703
- '---',
704
- 'Build the app.'
705
- ].join('\n')
706
- )
707
-
708
- const bundle = await resolveWorkspaceAssetBundle({
709
- cwd: workspace,
710
- configs: [undefined, undefined],
711
- useDefaultVibeForgeMcpServer: false
712
- })
713
-
714
- await buildAdapterAssetPlan({
715
- adapter: 'opencode',
716
- bundle,
717
- options: {
718
- skills: {
719
- include: ['app-builder']
720
- }
721
- }
722
- })
723
-
724
- expect(skillsCliMocks.installSkillsCliSkillToTemp).toHaveBeenCalledWith({
725
- registry: 'https://registry.example.com',
726
- skill: 'frontend-design',
727
- source: 'example-source/default/public',
728
- version: '1.0.3'
729
- })
730
- })
731
-
732
494
  it('loads workspace entities from the env-configured entities dir', async () => {
733
495
  const workspace = await createWorkspace()
734
496
  const previousEntitiesDir = process.env.__VF_PROJECT_AI_ENTITIES_DIR__
@@ -174,20 +174,35 @@ describe('workspace asset prompt builders', () => {
174
174
  ])
175
175
 
176
176
  expect(prompt).toContain('reviewer: 负责代码审查')
177
- expect(prompt).toContain('`VibeForge.StartTasks`')
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 }`')
186
- expect(prompt).toContain('`VibeForge.SubmitTaskInput`')
177
+ expect(prompt).toContain('Agent runtime guide:')
178
+ expect(prompt).toContain('`vf run --input-format stream-json --output-format stream-json`')
179
+ expect(prompt).toContain("cat <<'JSONL' | vf run --input-format stream-json --output-format stream-json")
180
+ expect(prompt).toContain('"commandId":"start-planner"')
181
+ expect(prompt).toContain('"type":"session.start"')
182
+ expect(prompt).toContain('"payload":{"title":"Plan Agent Room UI fix"')
183
+ expect(prompt).toContain('"entity":"dev-planner"')
184
+ expect(prompt).toContain('"background":true')
185
+ expect(prompt).toContain('write one `session.start` line per child task')
186
+ expect(prompt).toContain('typed runtime protocol envelopes')
187
+ expect(prompt).toContain('do not treat dedicated `vf agent ...` subcommands as the standard integration surface')
188
+ expect(prompt).toContain('Read the returned `sessionId`')
189
+ expect(prompt).toContain('Ordinary new sessions stay session-scoped')
190
+ expect(prompt).toContain('Do not use MCP task tools, `vf agent ...`, legacy StartTasks')
191
+ expect(prompt).toContain('hand-written DB edits')
192
+ expect(prompt).toContain('ad-hoc TS scripts')
193
+ expect(prompt).toContain('server-managed host session')
194
+ expect(prompt).toContain('server projects runtime store metadata/events')
195
+ expect(prompt).toContain('`session.status` protocol command')
196
+ expect(prompt).toContain('`session.events` protocol command')
197
+ expect(prompt).toContain('`session.message` protocol command')
198
+ expect(prompt).toContain('[ROOM_TASK_MESSAGE]')
199
+ expect(prompt).toContain('`mode: interaction`')
200
+ expect(prompt).toContain('`waiting_input`')
201
+ expect(prompt).toContain('`session.submit` protocol command')
187
202
  expect(prompt).toContain('Do not use it for ordinary follow-up instructions')
188
- expect(prompt).toContain('completed or failed tasks resume the same conversation')
189
- expect(prompt).toContain('keep working in that same thread of execution')
203
+ expect(prompt).toContain('completed or failed sessions resume the same conversation')
190
204
  expect(prompt).toContain('`wait`')
205
+ expect(prompt).not.toContain('VibeForge.StartTasks')
191
206
  expect(prompt).not.toContain('run-tasks')
192
207
  expect(prompt).not.toContain('需要关注变更风险')
193
208
  expect(prompt).not.toContain('hidden')
@@ -207,14 +222,19 @@ describe('workspace asset prompt builders', () => {
207
222
 
208
223
  expect(prompt).toContain('The project includes the following registered workspaces')
209
224
  expect(prompt).toContain('Identifier: billing')
210
- expect(prompt).toContain('`VibeForge.StartTasks`')
211
- expect(prompt).toContain('type: "workspace"')
212
- expect(prompt).toContain('Task tool guide:')
213
- expect(prompt).toContain('`VibeForge.GetTaskInfo`')
214
- expect(prompt).toContain('`VibeForge.ListTasks`')
215
- expect(prompt).toContain('`VibeForge.SendTaskMessage`')
216
- expect(prompt).toContain('`VibeForge.SubmitTaskInput`')
225
+ expect(prompt).toContain('workspace identifier and path')
226
+ expect(prompt).toContain('Agent runtime guide:')
227
+ expect(prompt).toContain('`vf run --input-format stream-json --output-format stream-json`')
228
+ expect(prompt).toContain('"payload":{"title":"Plan Agent Room UI fix"')
229
+ expect(prompt).toContain('"payload":{"title":"Review Agent Room UI fix"')
230
+ expect(prompt).toContain('payload.background: true')
231
+ expect(prompt).toContain('multiple subtasks')
232
+ expect(prompt).toContain('`session.status` protocol command')
233
+ expect(prompt).toContain('`session.events` protocol command')
234
+ expect(prompt).toContain('`session.message` protocol command')
235
+ expect(prompt).toContain('`session.submit`')
217
236
  expect(prompt).toContain('Do not directly edit files inside a registered workspace')
237
+ expect(prompt).not.toContain('VibeForge.StartTasks')
218
238
  })
219
239
 
220
240
  it('builds skill route prompts without preloading content', () => {
@@ -1,9 +1,7 @@
1
1
  /* eslint-disable import/first -- hoisted vitest mocks must be declared before importing the bundle entrypoint */
2
- import { access, mkdir, mkdtemp, rm, utimes, writeFile } from 'node:fs/promises'
3
- import os from 'node:os'
4
- import path, { join } from 'node:path'
2
+ import { join } from 'node:path'
5
3
 
6
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
7
5
 
8
6
  const mocks = vi.hoisted(() => ({
9
7
  findSkillsCli: vi.fn(),
@@ -23,54 +21,15 @@ vi.mock('@vibe-forge/utils/skills-cli', async () => {
23
21
 
24
22
  import { buildAdapterAssetPlan, resolveWorkspaceAssetBundle } from '#~/index.js'
25
23
 
26
- import { createWorkspace, writeDocument } from './test-helpers'
24
+ import { createWorkspace, installPluginPackage, writeDocument } from './test-helpers'
27
25
 
28
- describe('skills CLI dependency resolution', () => {
29
- let installWorkspace: string
30
-
31
- const pathExists = async (targetPath: string) => {
32
- try {
33
- await access(targetPath)
34
- return true
35
- } catch {
36
- return false
37
- }
38
- }
39
-
40
- beforeEach(async () => {
41
- installWorkspace = await mkdtemp(path.join(os.tmpdir(), 'vf-skills-cli-dependency-'))
26
+ describe('materialized skill dependency resolution', () => {
27
+ beforeEach(() => {
42
28
  vi.clearAllMocks()
43
29
  })
44
30
 
45
- afterEach(async () => {
46
- await rm(installWorkspace, { recursive: true, force: true })
47
- })
48
-
49
- it('installs missing bare-name dependencies through skills CLI by default', async () => {
31
+ it('fails missing dependencies without downloading during runtime preparation', async () => {
50
32
  const workspace = await createWorkspace()
51
- const installedSkillDir = join(installWorkspace, '.agents', 'skills', 'frontend-design')
52
- await mkdir(installedSkillDir, { recursive: true })
53
- await writeFile(
54
- join(installedSkillDir, 'SKILL.md'),
55
- '---\nname: frontend-design\ndescription: UI design guidance\n---\nUse strong visual hierarchy.\n'
56
- )
57
-
58
- mocks.findSkillsCli.mockResolvedValue([
59
- {
60
- installRef: 'anthropics/skills@frontend-design',
61
- source: 'anthropics/skills',
62
- skill: 'frontend-design'
63
- }
64
- ])
65
- mocks.installSkillsCliRefToTemp.mockResolvedValue({
66
- tempDir: installWorkspace,
67
- installedSkill: {
68
- dirName: 'frontend-design',
69
- name: 'frontend-design',
70
- sourcePath: installedSkillDir
71
- }
72
- })
73
-
74
33
  await writeDocument(
75
34
  join(workspace, '.ai/skills/app-builder/SKILL.md'),
76
35
  [
@@ -90,57 +49,6 @@ describe('skills CLI dependency resolution', () => {
90
49
  useDefaultVibeForgeMcpServer: false
91
50
  })
92
51
 
93
- await buildAdapterAssetPlan({
94
- adapter: 'opencode',
95
- bundle,
96
- options: {
97
- skills: {
98
- include: ['app-builder']
99
- }
100
- }
101
- })
102
-
103
- const dependency = bundle.skills.find(asset => asset.name === 'frontend-design')
104
- expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['app-builder', 'frontend-design'])
105
- expect(dependency?.sourcePath).toContain(
106
- '/.ai/caches/skill-dependencies/skills-cli/skills/latest/default/anthropics/skills/latest/frontend-design/'
107
- )
108
- expect(mocks.findSkillsCli).toHaveBeenCalledWith({
109
- query: 'frontend-design'
110
- })
111
- expect(mocks.installSkillsCliRefToTemp).toHaveBeenCalledWith({
112
- installRef: 'anthropics/skills@frontend-design'
113
- })
114
- })
115
-
116
- it('blocks missing dependency installs when auto downloads are disabled', async () => {
117
- const workspace = await createWorkspace()
118
-
119
- await writeDocument(
120
- join(workspace, '.ai/skills/app-builder/SKILL.md'),
121
- [
122
- '---',
123
- 'name: app-builder',
124
- 'description: Build apps',
125
- 'dependencies:',
126
- ' - name: frontend-design',
127
- ' source: anthropics/skills',
128
- ' registry: https://dependency-registry.example.test',
129
- '---',
130
- 'Build the app.'
131
- ].join('\n')
132
- )
133
-
134
- const bundle = await resolveWorkspaceAssetBundle({
135
- cwd: workspace,
136
- configs: [{
137
- skills: {
138
- autoDownloadDependencies: false
139
- }
140
- }, undefined],
141
- useDefaultVibeForgeMcpServer: false
142
- })
143
-
144
52
  await expect(buildAdapterAssetPlan({
145
53
  adapter: 'opencode',
146
54
  bundle,
@@ -149,17 +57,15 @@ describe('skills CLI dependency resolution', () => {
149
57
  include: ['app-builder']
150
58
  }
151
59
  }
152
- })).rejects.toThrow('Skill dependency automatic downloads are disabled; cache not found')
60
+ })).rejects.toThrow('Run vf skills install or vf skills update')
153
61
 
154
62
  expect(mocks.findSkillsCli).not.toHaveBeenCalled()
155
63
  expect(mocks.installSkillsCliRefToTemp).not.toHaveBeenCalled()
156
64
  expect(mocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
157
- expect(bundle.skills.map(asset => asset.name)).toEqual(['app-builder'])
158
65
  })
159
66
 
160
- it('blocks bare-name dependency searches when auto downloads are disabled', async () => {
67
+ it('uses project-materialized dependencies from .ai/skills', async () => {
161
68
  const workspace = await createWorkspace()
162
-
163
69
  await writeDocument(
164
70
  join(workspace, '.ai/skills/app-builder/SKILL.md'),
165
71
  [
@@ -172,67 +78,17 @@ describe('skills CLI dependency resolution', () => {
172
78
  'Build the app.'
173
79
  ].join('\n')
174
80
  )
175
-
176
- const bundle = await resolveWorkspaceAssetBundle({
177
- cwd: workspace,
178
- configs: [{
179
- skills: {
180
- autoDownloadDependencies: false
181
- }
182
- }, undefined],
183
- useDefaultVibeForgeMcpServer: false
184
- })
185
-
186
- await expect(buildAdapterAssetPlan({
187
- adapter: 'opencode',
188
- bundle,
189
- options: {
190
- skills: {
191
- include: ['app-builder']
192
- }
193
- }
194
- })).rejects.toThrow(
195
- 'Skill dependency automatic downloads are disabled; cannot resolve frontend-design without a source'
196
- )
197
-
198
- expect(mocks.findSkillsCli).not.toHaveBeenCalled()
199
- expect(mocks.installSkillsCliRefToTemp).not.toHaveBeenCalled()
200
- expect(mocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
201
- })
202
-
203
- it('reuses source-qualified dependency caches when auto downloads are disabled', async () => {
204
- const workspace = await createWorkspace()
205
- await writeDocument(
206
- join(
207
- workspace,
208
- '.ai/caches/skill-dependencies/skills-cli/skills/latest/default/anthropics/skills/latest/frontend-design/SKILL.md'
209
- ),
210
- '---\nname: frontend-design\ndescription: Cached UI guidance\n---\nUse the cached dependency.\n'
211
- )
212
81
  await writeDocument(
213
- join(workspace, '.ai/skills/app-builder/SKILL.md'),
214
- [
215
- '---',
216
- 'name: app-builder',
217
- 'description: Build apps',
218
- 'dependencies:',
219
- ' - anthropics/skills@frontend-design',
220
- '---',
221
- 'Build the app.'
222
- ].join('\n')
82
+ join(workspace, '.ai/skills/frontend-design/SKILL.md'),
83
+ '---\nname: frontend-design\ndescription: UI design guidance\n---\nUse strong visual hierarchy.\n'
223
84
  )
224
85
 
225
86
  const bundle = await resolveWorkspaceAssetBundle({
226
87
  cwd: workspace,
227
- configs: [{
228
- skills: {
229
- autoDownloadDependencies: false
230
- }
231
- }, undefined],
88
+ configs: [undefined, undefined],
232
89
  useDefaultVibeForgeMcpServer: false
233
90
  })
234
-
235
- await buildAdapterAssetPlan({
91
+ const plan = await buildAdapterAssetPlan({
236
92
  adapter: 'opencode',
237
93
  bundle,
238
94
  options: {
@@ -242,128 +98,79 @@ describe('skills CLI dependency resolution', () => {
242
98
  }
243
99
  })
244
100
 
101
+ expect(plan.overlays.filter(entry => entry.kind === 'skill').map(entry => entry.targetPath).sort()).toEqual([
102
+ 'skills/app-builder',
103
+ 'skills/frontend-design'
104
+ ])
245
105
  expect(mocks.findSkillsCli).not.toHaveBeenCalled()
246
- expect(mocks.installSkillsCliRefToTemp).not.toHaveBeenCalled()
247
- expect(mocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
248
- expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['app-builder', 'frontend-design'])
249
106
  })
250
107
 
251
- it('parses registry and version from dependency specs', async () => {
108
+ it('loads plugin dependencies from .ai/skills/.plugins through the lockfile', async () => {
252
109
  const workspace = await createWorkspace()
253
- const installedSkillDir = join(installWorkspace, '.agents', 'skills', 'frontend-design')
254
- await mkdir(installedSkillDir, { recursive: true })
255
- await writeFile(
256
- join(installedSkillDir, 'SKILL.md'),
257
- '---\nname: frontend-design\ndescription: UI design guidance\n---\nUse internal design system.\n'
258
- )
259
-
260
- mocks.installSkillsCliSkillToTemp.mockResolvedValue({
261
- tempDir: installWorkspace,
262
- installedSkill: {
263
- dirName: 'frontend-design',
264
- name: 'frontend-design',
265
- sourcePath: installedSkillDir
266
- }
267
- })
268
-
269
- await writeDocument(
270
- join(workspace, '.ai/skills/app-builder/SKILL.md'),
271
- [
110
+ await installPluginPackage(workspace, '@vibe-forge/plugin-review', {
111
+ 'package.json': JSON.stringify({ name: '@vibe-forge/plugin-review', version: '1.0.0' }, null, 2),
112
+ 'skills/review-helper/SKILL.md': [
272
113
  '---',
273
- 'name: app-builder',
274
- 'description: Build apps',
114
+ 'name: review-helper',
115
+ 'description: Review helper',
275
116
  'dependencies:',
276
- ' - https://registry.example.com@example-source/default/public@frontend-design@1.0.3',
117
+ ' - shared-runtime',
277
118
  '---',
278
- 'Build the app.'
119
+ 'Review code.'
279
120
  ].join('\n')
280
- )
281
-
282
- const bundle = await resolveWorkspaceAssetBundle({
283
- cwd: workspace,
284
- configs: [undefined, undefined],
285
- useDefaultVibeForgeMcpServer: false
286
- })
287
-
288
- await buildAdapterAssetPlan({
289
- adapter: 'opencode',
290
- bundle,
291
- options: {
292
- skills: {
293
- include: ['app-builder']
294
- }
295
- }
296
121
  })
297
-
298
- expect(mocks.installSkillsCliSkillToTemp).toHaveBeenCalledWith({
299
- registry: 'https://registry.example.com',
300
- skill: 'frontend-design',
301
- source: 'example-source/default/public',
302
- version: '1.0.3'
303
- })
304
- })
305
-
306
- it('clears stale dependency install locks before retrying a direct startup install', async () => {
307
- const workspace = await createWorkspace()
308
- const installedSkillDir = join(installWorkspace, '.agents', 'skills', 'lynx-cat')
309
- await mkdir(installedSkillDir, { recursive: true })
310
- await writeFile(
311
- join(installedSkillDir, 'SKILL.md'),
312
- '---\nname: lynx-cat\ndescription: Lynx helper\n---\nDebug Lynx apps.\n'
122
+ await writeDocument(
123
+ join(workspace, '.ai/skills/.plugins/review/shared-runtime/SKILL.md'),
124
+ '---\nname: shared-runtime\ndescription: Shared runtime\n---\nShared plugin dependency.\n'
313
125
  )
314
-
315
- mocks.installSkillsCliSkillToTemp.mockResolvedValue({
316
- tempDir: installWorkspace,
317
- installedSkill: {
318
- dirName: 'lynx-cat',
319
- name: 'lynx-cat',
320
- sourcePath: installedSkillDir
321
- }
322
- })
323
-
324
126
  await writeDocument(
325
- join(workspace, '.ai/skills/lynx-miniapp/SKILL.md'),
127
+ join(workspace, '.ai/skills.lock.yaml'),
326
128
  [
327
- '---',
328
- 'name: lynx-miniapp',
329
- 'description: lynx 调试使用',
330
- 'dependencies:',
331
- ' - https://registry.example.com@example-source/lynx/skills@lynx-cat@latest',
332
- '---',
333
- '这是一个测试的 lynx 调试技能'
129
+ 'version: 1',
130
+ 'pluginSkills:',
131
+ ' review/shared-runtime:',
132
+ ' name: shared-runtime',
133
+ ' requested: false',
134
+ ' pluginInstance: review',
135
+ ' pluginInstancePath: "0"',
136
+ ' installPath: .ai/skills/.plugins/review/shared-runtime',
137
+ ' dependencyOf:',
138
+ ' - plugin:review/review-helper',
139
+ ' source: vendor/shared-skills',
140
+ ' version: 1.0.0',
141
+ ' hash: sha256:test',
142
+ ' installedAt: "2026-05-13T00:00:00.000Z"'
334
143
  ].join('\n')
335
144
  )
336
145
 
337
- const lockDir = join(
338
- workspace,
339
- '.ai/caches/skill-dependencies/skills-cli/skills/latest/https-registry.example.com/example-source/lynx/skills/latest/lynx-cat.lock'
340
- )
341
- await mkdir(lockDir, { recursive: true })
342
- await utimes(lockDir, new Date(Date.now() - 120_000), new Date(Date.now() - 120_000))
343
-
344
146
  const bundle = await resolveWorkspaceAssetBundle({
345
147
  cwd: workspace,
346
- configs: [undefined, undefined],
148
+ configs: [{
149
+ plugins: [{
150
+ id: '@vibe-forge/plugin-review',
151
+ scope: 'review'
152
+ }]
153
+ }, undefined],
347
154
  useDefaultVibeForgeMcpServer: false
348
155
  })
349
-
350
- await buildAdapterAssetPlan({
156
+ const plan = await buildAdapterAssetPlan({
351
157
  adapter: 'opencode',
352
158
  bundle,
353
159
  options: {
354
160
  skills: {
355
- include: ['lynx-miniapp']
161
+ include: ['review/review-helper']
356
162
  }
357
163
  }
358
164
  })
359
165
 
360
- expect(mocks.installSkillsCliSkillToTemp).toHaveBeenCalledWith({
361
- registry: 'https://registry.example.com',
362
- skill: 'lynx-cat',
363
- source: 'example-source/lynx/skills',
364
- version: 'latest'
365
- })
366
- await expect(pathExists(lockDir)).resolves.toBe(false)
367
- expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['lynx-cat', 'lynx-miniapp'])
166
+ expect(bundle.skills.map(asset => asset.displayName).sort()).toEqual([
167
+ 'review/review-helper',
168
+ 'review/shared-runtime'
169
+ ])
170
+ expect(plan.overlays.filter(entry => entry.kind === 'skill').map(entry => entry.targetPath).sort()).toEqual([
171
+ 'skills/review__review-helper',
172
+ 'skills/review__shared-runtime'
173
+ ])
174
+ expect(mocks.installSkillsCliSkillToTemp).not.toHaveBeenCalled()
368
175
  })
369
176
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-forge/workspace-assets",
3
- "version": "3.1.0",
3
+ "version": "3.1.1-alpha.0",
4
4
  "description": "Workspace asset resolution and adapter asset planning for Vibe Forge",
5
5
  "imports": {
6
6
  "#~/*.js": {
@@ -29,10 +29,10 @@
29
29
  "fast-glob": "^3.3.3",
30
30
  "front-matter": "^4.0.2",
31
31
  "js-yaml": "^4.1.1",
32
- "@vibe-forge/types": "3.1.0",
33
- "@vibe-forge/config": "3.1.0",
34
- "@vibe-forge/utils": "3.1.0",
35
- "@vibe-forge/definition-core": "3.1.0"
32
+ "@vibe-forge/config": "3.1.5",
33
+ "@vibe-forge/definition-core": "3.1.0",
34
+ "@vibe-forge/types": "3.1.6-alpha.0",
35
+ "@vibe-forge/utils": "3.1.6-alpha.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/js-yaml": "^4.0.9"
@@ -1,4 +1,4 @@
1
- import { readFile } from 'node:fs/promises'
1
+ import { access, readFile } from 'node:fs/promises'
2
2
  import { basename, dirname, extname, isAbsolute, resolve } from 'node:path'
3
3
  import process from 'node:process'
4
4
 
@@ -11,6 +11,7 @@ import {
11
11
  import type { Config, Definition, Entity, PluginConfig, WorkspaceAsset, WorkspaceAssetKind } from '@vibe-forge/types'
12
12
  import {
13
13
  isLegacySkillsConfig,
14
+ readProjectSkillsLockfile,
14
15
  resolveProjectAiBaseDir,
15
16
  resolveProjectAiEntitiesDir,
16
17
  resolveRelativePath
@@ -35,6 +36,7 @@ import {
35
36
  } from '@vibe-forge/definition-core'
36
37
  import { ensureConfiguredProjectSkills } from './configured-skills'
37
38
  import { HOME_BRIDGE_RESOLVED_BY } from './home-bridge'
39
+ import { PLUGIN_SKILL_DEPENDENCY_RESOLVED_BY } from './plugin-skill-dependencies'
38
40
  import { resolveConfiguredWorkspaceAssets } from './workspaces'
39
41
 
40
42
  type DocumentAssetKind = Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>
@@ -440,6 +442,40 @@ const scanInstanceDocuments = async (instance: ResolvedPluginInstance) => {
440
442
  }
441
443
  }
442
444
 
445
+ const pathExists = async (path: string) => {
446
+ try {
447
+ await access(path)
448
+ return true
449
+ } catch {
450
+ return false
451
+ }
452
+ }
453
+
454
+ const scanPluginDependencySkillDocuments = async (
455
+ cwd: string,
456
+ instances: ResolvedPluginInstance[]
457
+ ) => {
458
+ const lockfile = await readProjectSkillsLockfile(cwd)
459
+ const documents: Array<{ instance: ResolvedPluginInstance; path: string }> = []
460
+ for (const entry of Object.values(lockfile.pluginSkills ?? {})) {
461
+ const instance = instances.find(candidate => (
462
+ candidate.instancePath === entry.pluginInstancePath ||
463
+ candidate.scope === entry.pluginInstance
464
+ ))
465
+ if (instance == null) continue
466
+
467
+ const skillPath = resolve(cwd, entry.installPath, 'SKILL.md')
468
+ if (await pathExists(skillPath)) {
469
+ documents.push({
470
+ instance,
471
+ path: skillPath
472
+ })
473
+ }
474
+ }
475
+
476
+ return documents
477
+ }
478
+
443
479
  const toOpenCodeOverlayEntries = (
444
480
  kind: OpenCodeOverlayKind,
445
481
  targetDir: 'agents' | 'commands' | 'modes' | 'plugins',
@@ -578,6 +614,7 @@ export async function collectWorkspaceAssets(params: {
578
614
  ])
579
615
  const flattenedPluginInstances = flattenPluginInstances(pluginInstances)
580
616
  const pluginScans = await Promise.all(flattenedPluginInstances.map(instance => scanInstanceDocuments(instance)))
617
+ const pluginDependencySkillDocs = await scanPluginDependencySkillDocuments(params.cwd, flattenedPluginInstances)
581
618
  const pluginOverlayScans = await Promise.all(
582
619
  flattenedPluginInstances.map(instance => scanInstanceOpenCodeOverlays(instance))
583
620
  )
@@ -631,6 +668,16 @@ export async function collectWorkspaceAssets(params: {
631
668
  await pushDocumentAssets('entity', scan.entityDocPaths, 'plugin', instance, parseEntityMarkdownDocument)
632
669
  await pushDocumentAssets('entity', scan.entityJsonPaths, 'plugin', instance, parseEntityIndexJson)
633
670
  }
671
+ for (const entry of pluginDependencySkillDocs) {
672
+ await pushDocumentAssets(
673
+ 'skill',
674
+ [entry.path],
675
+ 'plugin',
676
+ entry.instance,
677
+ undefined,
678
+ PLUGIN_SKILL_DEPENDENCY_RESOLVED_BY
679
+ )
680
+ }
634
681
  await pushDocumentAssets('skill', homeSkillPaths, 'workspace', undefined, undefined, HOME_BRIDGE_RESOLVED_BY)
635
682
 
636
683
  const skills = mergeSkillAssets(skillAssets)
@@ -0,0 +1 @@
1
+ export const PLUGIN_SKILL_DEPENDENCY_RESOLVED_BY = 'plugin-skill-dependency-lock'
@@ -5,7 +5,7 @@ import {
5
5
  resolveSpecIdentifier
6
6
  } from '@vibe-forge/definition-core'
7
7
  import type { Definition, Entity, Rule, Skill, Spec } from '@vibe-forge/types'
8
- import { CANONICAL_VIBE_FORGE_MCP_SERVER_NAME, resolvePromptPath } from '@vibe-forge/utils'
8
+ import { resolvePromptPath } from '@vibe-forge/utils'
9
9
 
10
10
  import { buildManagedTaskToolGuidance } from './task-tool-guidance'
11
11
 
@@ -167,7 +167,7 @@ export const generateSpecRoutePrompt = (
167
167
  }
168
168
 
169
169
  export const generateEntitiesRoutePrompt = (entities: Definition<Entity>[]) => {
170
- const taskToolGuidance = buildManagedTaskToolGuidance(CANONICAL_VIBE_FORGE_MCP_SERVER_NAME)
170
+ const taskToolGuidance = buildManagedTaskToolGuidance()
171
171
  return (
172
172
  '<system-prompt>\n' +
173
173
  'The project includes the following entities:\n' +
@@ -181,7 +181,7 @@ export const generateEntitiesRoutePrompt = (entities: Definition<Entity>[]) => {
181
181
  })
182
182
  .join('')
183
183
  }\n` +
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` +
184
+ 'When solving user problems, you may start child runtime sessions with `vf run --input-format stream-json --output-format stream-json` and a `session.start` protocol envelope as needed; use `session.status`, `session.events`, and `wait` to track progress.\n' +
185
185
  `${taskToolGuidance}\n` +
186
186
  '</system-prompt>\n'
187
187
  )
@@ -26,6 +26,7 @@ import {
26
26
  isRemoteRuleReference,
27
27
  parseScopedReference
28
28
  } from '@vibe-forge/definition-core'
29
+ import { PLUGIN_SKILL_DEPENDENCY_RESOLVED_BY } from './plugin-skill-dependencies'
29
30
  import { expandSkillAssetDependencies, expandSkillAssetDependenciesWithRemoteResolution } from './skill-dependencies'
30
31
 
31
32
  type DocumentAssetKind = Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>
@@ -359,6 +360,10 @@ const mergeEntityDefinitions = (
359
360
 
360
361
  const uniqueAssetIds = (values: string[]) => toUniqueValues(values, value => value)
361
362
 
363
+ const isPluginSkillDependencyAsset = (asset: Extract<WorkspaceAsset, { kind: 'skill' }>) => (
364
+ asset.resolvedBy === PLUGIN_SKILL_DEPENDENCY_RESOLVED_BY
365
+ )
366
+
362
367
  const formatEntityCycle = (stack: EntityAsset[], asset: EntityAsset) => (
363
368
  [...stack.slice(stack.findIndex(item => item.id === asset.id)), asset]
364
369
  .map(item => item.displayName)
@@ -517,9 +522,10 @@ export const resolveSelectedSkillAssets = (
517
522
  ): Array<Extract<WorkspaceAsset, { kind: 'skill' }>> => {
518
523
  if (selection == null) return assets
519
524
 
525
+ const rootAssets = assets.filter(asset => !isPluginSkillDependencyAsset(asset))
520
526
  const included = selection.include != null && selection.include.length > 0
521
527
  ? resolveNamedAssets(assets, selection.include)
522
- : assets
528
+ : rootAssets
523
529
  const excluded = new Set(
524
530
  resolveNamedAssets(assets, selection.exclude).map(asset => asset.id)
525
531
  )
@@ -532,9 +538,10 @@ export const resolveSelectedSkillAssetsWithDependencies = async (
532
538
  bundle: WorkspaceAssetBundle,
533
539
  selection?: WorkspaceSkillSelection
534
540
  ): Promise<Array<Extract<WorkspaceAsset, { kind: 'skill' }>>> => {
541
+ const rootAssets = bundle.skills.filter(asset => !isPluginSkillDependencyAsset(asset))
535
542
  const included = selection?.include != null && selection.include.length > 0
536
543
  ? resolveNamedAssets(bundle.skills, selection.include)
537
- : bundle.skills
544
+ : rootAssets
538
545
  const excluded = new Set(
539
546
  resolveNamedAssets(bundle.skills, selection?.exclude).map(asset => asset.id)
540
547
  )
@@ -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,13 +1,27 @@
1
- export const buildManagedTaskToolGuidance = (serverName: string) => {
1
+ export const buildManagedTaskToolGuidance = () => {
2
2
  return [
3
- 'Task tool guide:',
4
- `- Use \`${serverName}.StartTasks\` to start a new child task when the work should run in a separate entity or workspace, or when it needs to continue independently from the current turn.`,
5
- `- After starting a task, use \`${serverName}.GetTaskInfo\` with \`{ taskId }\` to inspect one task. It is also the right tool when a task seems stalled, failed, or might be waiting for input.`,
6
- '- By default, `GetTaskInfo` returns the 10 most recent log entries in descending order, so newer entries appear earlier in the `logs` array. Pass `logLimit` to inspect a different number of recent logs, and set `logOrder` to `"asc"` when you want the selected log window in oldest-to-newest order.',
7
- `- Use \`${serverName}.ListTasks\` with the same \`logLimit\` and \`logOrder\` fields when you need to find a taskId or inspect multiple tasks at once.`,
8
- `- Use \`${serverName}.SendTaskMessage\` with \`{ taskId, message }\` when you need to give a task another instruction. Running tasks continue immediately; completed or failed tasks resume the same conversation instead of forcing a replacement task.`,
9
- `- Use \`${serverName}.SubmitTaskInput\` only when \`${serverName}.GetTaskInfo\` or \`${serverName}.ListTasks\` shows \`pendingInput\` or \`pendingInteraction\`, or the task status is \`waiting_input\`. Do not use it for ordinary follow-up instructions.`,
10
- `- If a task is \`completed\` or \`failed\` but you want to keep working in that same thread of execution, prefer \`${serverName}.SendTaskMessage\` to resume it. Start a new task only when you actually want a replacement task.`,
11
- '- When a task is still making progress, use `wait` between checks instead of repeatedly restarting it.'
3
+ '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.',
6
+ '- 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.',
8
+ '- 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
+ '- Copyable JSONL example; write one `session.start` line per child task, and use multiple lines for multiple subtasks:',
10
+ '```bash',
11
+ "cat <<'JSONL' | vf run --input-format stream-json --output-format stream-json",
12
+ '{"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
+ '{"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
+ 'JSONL',
15
+ '```',
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.',
17
+ '- 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
+ '- 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
+ '- Use a follow/read-events workflow when you need to watch progress instead of repeatedly restarting work.',
20
+ '- Use a `session.message` protocol command to give an existing session another instruction. Running sessions continue immediately; completed or failed sessions resume the same conversation when the runtime allows resume.',
21
+ '- 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
+ '- 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
+ '- 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.',
25
+ '- When a session is still making progress, use `wait` between checks and inspect status/events instead of starting a replacement session.'
12
26
  ].join('\n')
13
27
  }
@@ -1,5 +1,5 @@
1
1
  import type { WorkspaceDefinitionPayload } from '@vibe-forge/types'
2
- import { CANONICAL_VIBE_FORGE_MCP_SERVER_NAME, resolvePromptPath } from '@vibe-forge/utils'
2
+ import { resolvePromptPath } from '@vibe-forge/utils'
3
3
 
4
4
  import { buildManagedTaskToolGuidance } from './task-tool-guidance'
5
5
 
@@ -8,7 +8,7 @@ export const generateWorkspaceRoutePrompt = (
8
8
  workspaces: WorkspaceDefinitionPayload[]
9
9
  ) => {
10
10
  if (workspaces.length === 0) return ''
11
- const taskToolGuidance = buildManagedTaskToolGuidance(CANONICAL_VIBE_FORGE_MCP_SERVER_NAME)
11
+ const taskToolGuidance = buildManagedTaskToolGuidance()
12
12
 
13
13
  const workspaceList = workspaces
14
14
  .map((workspace) => {
@@ -25,7 +25,7 @@ export const generateWorkspaceRoutePrompt = (
25
25
  '<system-prompt>\n' +
26
26
  'The project includes the following registered workspaces:\n' +
27
27
  `${workspaceList}\n` +
28
- `When a user request targets one of these workspaces, start a child task with \`${CANONICAL_VIBE_FORGE_MCP_SERVER_NAME}.StartTasks\` using \`type: "workspace"\` and \`name\` set to the workspace identifier. ` +
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. ' +
29
29
  '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
30
  `${taskToolGuidance}\n` +
31
31
  '</system-prompt>\n'