@vibe-forge/workspace-assets 2.0.0 → 2.0.1

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.
@@ -435,6 +435,313 @@ describe('resolvePromptAssetSelection', () => {
435
435
  expect(options.systemPrompt).not.toContain('<skill-content>\n先读 README.md\n</skill-content>')
436
436
  })
437
437
 
438
+ it('appends conventional prompt files when loading a directory entity', async () => {
439
+ const workspace = await createWorkspace()
440
+
441
+ await writeDocument(
442
+ join(workspace, '.ai/entities/reviewer/README.md'),
443
+ [
444
+ '---',
445
+ 'description: 代码评审实体',
446
+ '---',
447
+ '基础身份。'
448
+ ].join('\n')
449
+ )
450
+ await writeDocument(
451
+ join(workspace, '.ai/entities/reviewer/INTRODUCTION.md'),
452
+ '负责发现行为回归。'
453
+ )
454
+ await writeDocument(
455
+ join(workspace, '.ai/entities/reviewer/PERSONALITY.md'),
456
+ '说话克制,先给出高风险问题。'
457
+ )
458
+ await writeDocument(
459
+ join(workspace, '.ai/entities/reviewer/MEMORY.md'),
460
+ '记住上次评审指出过缺少验证。'
461
+ )
462
+
463
+ const bundle = await resolveWorkspaceAssetBundle({
464
+ cwd: workspace,
465
+ useDefaultVibeForgeMcpServer: false
466
+ })
467
+ const [data, options] = await resolvePromptAssetSelection({
468
+ bundle,
469
+ type: 'entity',
470
+ name: 'reviewer'
471
+ })
472
+
473
+ expect(data.targetBody).toContain('基础身份。')
474
+ expect(data.targetBody).toContain('## Introduction\n\n负责发现行为回归。')
475
+ expect(data.targetBody).toContain('## Personality\n\n说话克制,先给出高风险问题。')
476
+ expect(data.targetBody).toContain('## Memory\n\n记住上次评审指出过缺少验证。')
477
+ expect(options.systemPrompt).toContain('## Introduction\n\n负责发现行为回归。')
478
+ expect(data.targetBody.indexOf('基础身份。')).toBeLessThan(data.targetBody.indexOf('## Introduction'))
479
+ expect(data.targetBody.indexOf('## Introduction')).toBeLessThan(data.targetBody.indexOf('## Personality'))
480
+ expect(data.targetBody.indexOf('## Personality')).toBeLessThan(data.targetBody.indexOf('## Memory'))
481
+ })
482
+
483
+ it('appends alias prompt files for index json entities', async () => {
484
+ const workspace = await createWorkspace()
485
+
486
+ await writeDocument(
487
+ join(workspace, '.ai/entities/reviewer/index.json'),
488
+ JSON.stringify(
489
+ {
490
+ description: '代码评审实体',
491
+ prompt: '基础身份。'
492
+ },
493
+ null,
494
+ 2
495
+ )
496
+ )
497
+ await writeDocument(
498
+ join(workspace, '.ai/entities/reviewer/介绍.md'),
499
+ '负责发现行为回归。'
500
+ )
501
+ await writeDocument(
502
+ join(workspace, '.ai/entities/reviewer/personality.md'),
503
+ '说话克制,先给出高风险问题。'
504
+ )
505
+ await writeDocument(
506
+ join(workspace, '.ai/entities/reviewer/记忆.md'),
507
+ '记住上次评审指出过缺少验证。'
508
+ )
509
+
510
+ const bundle = await resolveWorkspaceAssetBundle({
511
+ cwd: workspace,
512
+ useDefaultVibeForgeMcpServer: false
513
+ })
514
+ const [data] = await resolvePromptAssetSelection({
515
+ bundle,
516
+ type: 'entity',
517
+ name: 'reviewer'
518
+ })
519
+
520
+ expect(data.targetBody).toContain('基础身份。')
521
+ expect(data.targetBody).toContain('## Introduction\n\n负责发现行为回归。')
522
+ expect(data.targetBody).toContain('## Personality\n\n说话克制,先给出高风险问题。')
523
+ expect(data.targetBody).toContain('## Memory\n\n记住上次评审指出过缺少验证。')
524
+ })
525
+
526
+ it('inherits prompt, rules, and skills from scoped plugin entities', async () => {
527
+ const workspace = await createWorkspace()
528
+
529
+ await installPluginPackage(workspace, '@vibe-forge/plugin-standard-dev', {
530
+ 'package.json': JSON.stringify(
531
+ {
532
+ name: '@vibe-forge/plugin-standard-dev',
533
+ version: '1.0.0'
534
+ },
535
+ null,
536
+ 2
537
+ ),
538
+ 'rules/base-review.md': '---\ndescription: 标准评审规则\n---\n父规则正文',
539
+ 'skills/base-review/SKILL.md': '---\ndescription: 标准评审技能\n---\n父技能正文',
540
+ 'entities/base-reviewer/README.md': [
541
+ '---',
542
+ 'description: 标准评审实体',
543
+ 'rules:',
544
+ ' - base-review',
545
+ 'skills:',
546
+ ' - base-review',
547
+ 'tools:',
548
+ ' include:',
549
+ ' - Read',
550
+ '---',
551
+ '父实体提示。'
552
+ ].join('\n')
553
+ })
554
+ await writeDocument(
555
+ join(workspace, '.ai/rules/base-review.md'),
556
+ '---\ndescription: 同名本地规则\n---\n同名本地规则正文'
557
+ )
558
+ await writeDocument(
559
+ join(workspace, '.ai/rules/frontend.md'),
560
+ '---\ndescription: 前端规则\n---\n子规则正文'
561
+ )
562
+ await writeDocument(
563
+ join(workspace, '.ai/skills/base-review/SKILL.md'),
564
+ '---\ndescription: 同名本地技能\n---\n同名本地技能正文'
565
+ )
566
+ await writeDocument(
567
+ join(workspace, '.ai/skills/frontend/SKILL.md'),
568
+ '---\ndescription: 前端技能\n---\n子技能正文'
569
+ )
570
+ await writeDocument(
571
+ join(workspace, '.ai/entities/frontend-reviewer/README.md'),
572
+ [
573
+ '---',
574
+ 'description: 前端评审实体',
575
+ 'extends: std/base-reviewer',
576
+ 'rules:',
577
+ ' - frontend',
578
+ 'skills:',
579
+ ' - frontend',
580
+ 'tools:',
581
+ ' include:',
582
+ ' - Grep',
583
+ '---',
584
+ '子实体提示。'
585
+ ].join('\n')
586
+ )
587
+
588
+ const bundle = await resolveWorkspaceAssetBundle({
589
+ cwd: workspace,
590
+ configs: [{
591
+ plugins: [
592
+ { id: 'standard-dev', scope: 'std' }
593
+ ]
594
+ }, undefined],
595
+ useDefaultVibeForgeMcpServer: false
596
+ })
597
+ const [data, options] = await resolvePromptAssetSelection({
598
+ bundle,
599
+ type: 'entity',
600
+ name: 'frontend-reviewer'
601
+ })
602
+ const parentEntityId = bundle.entities.find(asset => asset.displayName === 'std/base-reviewer')?.id
603
+ const childEntityId = bundle.entities.find(asset => asset.displayName === 'frontend-reviewer')?.id
604
+
605
+ expect(data.targetBody).toContain('父实体提示。\n\n子实体提示。')
606
+ expect(data.targetSkills.map(skill => skill.resolvedName)).toEqual(['std/base-review', 'frontend'])
607
+ expect(options.systemPrompt).toContain('> 父规则正文')
608
+ expect(options.systemPrompt).toContain('> 子规则正文')
609
+ expect(options.systemPrompt).not.toContain('> 同名本地规则正文')
610
+ expect(options.systemPrompt).not.toContain('<skill-content>\n同名本地技能正文\n</skill-content>')
611
+ expect(options.tools).toEqual({ include: ['Grep'] })
612
+ expect(options.promptAssetIds).toEqual(expect.arrayContaining([parentEntityId, childEntityId]))
613
+ })
614
+
615
+ it('composes multiple parent entities in extends order', async () => {
616
+ const workspace = await createWorkspace()
617
+
618
+ await writeDocument(
619
+ join(workspace, '.ai/entities/base-a.md'),
620
+ [
621
+ '---',
622
+ 'description: 父实体 A',
623
+ 'tools:',
624
+ ' include:',
625
+ ' - Read',
626
+ '---',
627
+ '父实体 A 提示。'
628
+ ].join('\n')
629
+ )
630
+ await writeDocument(
631
+ join(workspace, '.ai/entities/base-b.md'),
632
+ [
633
+ '---',
634
+ 'description: 父实体 B',
635
+ 'tools:',
636
+ ' include:',
637
+ ' - Bash',
638
+ '---',
639
+ '父实体 B 提示。'
640
+ ].join('\n')
641
+ )
642
+ await writeDocument(
643
+ join(workspace, '.ai/entities/child.md'),
644
+ [
645
+ '---',
646
+ 'description: 子实体',
647
+ 'extends:',
648
+ ' - base-a',
649
+ ' - base-b',
650
+ '---',
651
+ '子实体提示。'
652
+ ].join('\n')
653
+ )
654
+
655
+ const bundle = await resolveWorkspaceAssetBundle({
656
+ cwd: workspace,
657
+ useDefaultVibeForgeMcpServer: false
658
+ })
659
+ const [data, options] = await resolvePromptAssetSelection({
660
+ bundle,
661
+ type: 'entity',
662
+ name: 'child'
663
+ })
664
+
665
+ expect(data.targetBody).toContain('父实体 A 提示。\n\n父实体 B 提示。\n\n子实体提示。')
666
+ expect(options.tools).toEqual({ include: ['Bash'] })
667
+ })
668
+
669
+ it('lets the current entity replace inherited rules', async () => {
670
+ const workspace = await createWorkspace()
671
+
672
+ await writeDocument(
673
+ join(workspace, '.ai/rules/base.md'),
674
+ '---\ndescription: 父规则\n---\n父规则正文'
675
+ )
676
+ await writeDocument(
677
+ join(workspace, '.ai/rules/child.md'),
678
+ '---\ndescription: 子规则\n---\n子规则正文'
679
+ )
680
+ await writeDocument(
681
+ join(workspace, '.ai/entities/base.md'),
682
+ [
683
+ '---',
684
+ 'description: 父实体',
685
+ 'rules:',
686
+ ' - base',
687
+ '---',
688
+ '父实体提示。'
689
+ ].join('\n')
690
+ )
691
+ await writeDocument(
692
+ join(workspace, '.ai/entities/child.md'),
693
+ [
694
+ '---',
695
+ 'description: 子实体',
696
+ 'extends: base',
697
+ 'inherit:',
698
+ ' rules: replace',
699
+ 'rules:',
700
+ ' - child',
701
+ '---',
702
+ '子实体提示。'
703
+ ].join('\n')
704
+ )
705
+
706
+ const bundle = await resolveWorkspaceAssetBundle({
707
+ cwd: workspace,
708
+ useDefaultVibeForgeMcpServer: false
709
+ })
710
+ const [, options] = await resolvePromptAssetSelection({
711
+ bundle,
712
+ type: 'entity',
713
+ name: 'child'
714
+ })
715
+
716
+ expect(options.systemPrompt).toContain('> 子规则正文')
717
+ expect(options.systemPrompt).toContain('> Use when: 父规则')
718
+ expect(options.systemPrompt).not.toContain('> 父规则正文')
719
+ })
720
+
721
+ it('rejects circular entity inheritance', async () => {
722
+ const workspace = await createWorkspace()
723
+
724
+ await writeDocument(
725
+ join(workspace, '.ai/entities/a.md'),
726
+ '---\ndescription: A\nextends: b\n---\nA'
727
+ )
728
+ await writeDocument(
729
+ join(workspace, '.ai/entities/b.md'),
730
+ '---\ndescription: B\nextends: a\n---\nB'
731
+ )
732
+
733
+ const bundle = await resolveWorkspaceAssetBundle({
734
+ cwd: workspace,
735
+ useDefaultVibeForgeMcpServer: false
736
+ })
737
+
738
+ await expect(resolvePromptAssetSelection({
739
+ bundle,
740
+ type: 'entity',
741
+ name: 'a'
742
+ })).rejects.toThrow('Circular entity inheritance detected: a -> b -> a')
743
+ })
744
+
438
745
  it('does not preload all skills when the target entity omits skill references', async () => {
439
746
  const workspace = await createWorkspace()
440
747
 
@@ -0,0 +1,175 @@
1
+ /* eslint-disable import/first -- hoisted vitest mocks must be declared before importing the bundle entrypoint */
2
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
3
+ import os from 'node:os'
4
+ import path, { join } from 'node:path'
5
+
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7
+
8
+ const mocks = vi.hoisted(() => ({
9
+ findSkillsCli: vi.fn(),
10
+ installSkillsCliRefToTemp: vi.fn(),
11
+ installSkillsCliSkillToTemp: vi.fn()
12
+ }))
13
+
14
+ vi.mock('@vibe-forge/utils/skills-cli', async () => {
15
+ const actual = await vi.importActual<typeof import('@vibe-forge/utils/skills-cli')>('@vibe-forge/utils/skills-cli')
16
+ return {
17
+ ...actual,
18
+ findSkillsCli: mocks.findSkillsCli,
19
+ installSkillsCliRefToTemp: mocks.installSkillsCliRefToTemp,
20
+ installSkillsCliSkillToTemp: mocks.installSkillsCliSkillToTemp
21
+ }
22
+ })
23
+
24
+ import { buildAdapterAssetPlan, resolveWorkspaceAssetBundle } from '#~/index.js'
25
+
26
+ import { createWorkspace, writeDocument } from './test-helpers'
27
+
28
+ describe('skills CLI dependency resolution', () => {
29
+ let installWorkspace: string
30
+
31
+ beforeEach(async () => {
32
+ installWorkspace = await mkdtemp(path.join(os.tmpdir(), 'vf-skills-cli-dependency-'))
33
+ vi.clearAllMocks()
34
+ })
35
+
36
+ afterEach(async () => {
37
+ await rm(installWorkspace, { recursive: true, force: true })
38
+ })
39
+
40
+ it('installs missing bare-name dependencies through skills CLI by default', async () => {
41
+ const workspace = await createWorkspace()
42
+ const installedSkillDir = join(installWorkspace, '.agents', 'skills', 'frontend-design')
43
+ await mkdir(installedSkillDir, { recursive: true })
44
+ await writeFile(
45
+ join(installedSkillDir, 'SKILL.md'),
46
+ '---\nname: frontend-design\ndescription: UI design guidance\n---\nUse strong visual hierarchy.\n'
47
+ )
48
+
49
+ mocks.findSkillsCli.mockResolvedValue([
50
+ {
51
+ installRef: 'anthropics/skills@frontend-design',
52
+ source: 'anthropics/skills',
53
+ skill: 'frontend-design'
54
+ }
55
+ ])
56
+ mocks.installSkillsCliRefToTemp.mockResolvedValue({
57
+ tempDir: installWorkspace,
58
+ installedSkill: {
59
+ dirName: 'frontend-design',
60
+ name: 'frontend-design',
61
+ sourcePath: installedSkillDir
62
+ }
63
+ })
64
+
65
+ await writeDocument(
66
+ join(workspace, '.ai/skills/app-builder/SKILL.md'),
67
+ [
68
+ '---',
69
+ 'name: app-builder',
70
+ 'description: Build apps',
71
+ 'dependencies:',
72
+ ' - frontend-design',
73
+ '---',
74
+ 'Build the app.'
75
+ ].join('\n')
76
+ )
77
+
78
+ const bundle = await resolveWorkspaceAssetBundle({
79
+ cwd: workspace,
80
+ configs: [undefined, undefined],
81
+ useDefaultVibeForgeMcpServer: false
82
+ })
83
+
84
+ await buildAdapterAssetPlan({
85
+ adapter: 'opencode',
86
+ bundle,
87
+ options: {
88
+ skills: {
89
+ include: ['app-builder']
90
+ }
91
+ }
92
+ })
93
+
94
+ const dependency = bundle.skills.find(asset => asset.name === 'frontend-design')
95
+ expect(bundle.skills.map(asset => asset.name).sort()).toEqual(['app-builder', 'frontend-design'])
96
+ expect(dependency?.sourcePath).toContain(
97
+ '/.ai/caches/skill-dependencies/skills-cli/skills/latest/default/anthropics/skills/frontend-design/'
98
+ )
99
+ expect(mocks.findSkillsCli).toHaveBeenCalledWith({
100
+ config: undefined,
101
+ query: 'frontend-design'
102
+ })
103
+ expect(mocks.installSkillsCliRefToTemp).toHaveBeenCalledWith({
104
+ config: undefined,
105
+ installRef: 'anthropics/skills@frontend-design'
106
+ })
107
+ })
108
+
109
+ it('merges top-level skillsCli config ahead of legacy skills.cli aliases', async () => {
110
+ const workspace = await createWorkspace()
111
+ const installedSkillDir = join(installWorkspace, '.agents', 'skills', 'frontend-design')
112
+ await mkdir(installedSkillDir, { recursive: true })
113
+ await writeFile(
114
+ join(installedSkillDir, 'SKILL.md'),
115
+ '---\nname: frontend-design\ndescription: UI design guidance\n---\nUse internal design system.\n'
116
+ )
117
+
118
+ mocks.installSkillsCliSkillToTemp.mockResolvedValue({
119
+ tempDir: installWorkspace,
120
+ installedSkill: {
121
+ dirName: 'frontend-design',
122
+ name: 'frontend-design',
123
+ sourcePath: installedSkillDir
124
+ }
125
+ })
126
+
127
+ await writeDocument(
128
+ join(workspace, '.ai/skills/app-builder/SKILL.md'),
129
+ [
130
+ '---',
131
+ 'name: app-builder',
132
+ 'description: Build apps',
133
+ 'dependencies:',
134
+ ' - example-source/default/public@frontend-design',
135
+ '---',
136
+ 'Build the app.'
137
+ ].join('\n')
138
+ )
139
+
140
+ const bundle = await resolveWorkspaceAssetBundle({
141
+ cwd: workspace,
142
+ configs: [{
143
+ skills: {
144
+ cli: {
145
+ package: 'legacy-skills'
146
+ }
147
+ },
148
+ skillsCli: {
149
+ package: '@byted/skills',
150
+ registry: 'https://registry.example.com'
151
+ }
152
+ }, undefined],
153
+ useDefaultVibeForgeMcpServer: false
154
+ })
155
+
156
+ await buildAdapterAssetPlan({
157
+ adapter: 'opencode',
158
+ bundle,
159
+ options: {
160
+ skills: {
161
+ include: ['app-builder']
162
+ }
163
+ }
164
+ })
165
+
166
+ expect(mocks.installSkillsCliSkillToTemp).toHaveBeenCalledWith({
167
+ config: {
168
+ package: '@byted/skills',
169
+ registry: 'https://registry.example.com'
170
+ },
171
+ skill: 'frontend-design',
172
+ source: 'example-source/default/public'
173
+ })
174
+ })
175
+ })
@@ -169,6 +169,7 @@ const summarizeDiagnostics = (
169
169
  assetId: assetIdMap.get(diagnostic.assetId) ?? diagnostic.assetId,
170
170
  adapter: diagnostic.adapter,
171
171
  status: diagnostic.status,
172
+ source: diagnostic.source,
172
173
  reason: diagnostic.reason,
173
174
  scope: diagnostic.scope,
174
175
  packageId: diagnostic.packageId
@@ -5,10 +5,14 @@ import { dirname, join } from 'node:path'
5
5
  import { afterEach } from 'vitest'
6
6
 
7
7
  const tempDirs: string[] = []
8
+ const originalRealHome = process.env.__VF_PROJECT_REAL_HOME__
8
9
 
9
10
  export const createWorkspace = async () => {
10
11
  const dir = await mkdtemp(join(tmpdir(), 'workspace-assets-'))
12
+ const realHome = await mkdtemp(join(tmpdir(), 'workspace-assets-home-'))
11
13
  tempDirs.push(dir)
14
+ tempDirs.push(realHome)
15
+ process.env.__VF_PROJECT_REAL_HOME__ = realHome
12
16
  return dir
13
17
  }
14
18
 
@@ -32,4 +36,9 @@ export const installPluginPackage = async (
32
36
 
33
37
  afterEach(async () => {
34
38
  await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
39
+ if (originalRealHome == null) {
40
+ delete process.env.__VF_PROJECT_REAL_HOME__
41
+ } else {
42
+ process.env.__VF_PROJECT_REAL_HOME__ = originalRealHome
43
+ }
35
44
  })
@@ -189,7 +189,7 @@ describe('workspace assets snapshots', () => {
189
189
  })
190
190
 
191
191
  const adapters = ['claude-code', 'codex', 'gemini', 'opencode'] as const
192
- const plans = adapters.map(adapter => (
192
+ const plans = await Promise.all(adapters.map(adapter => (
193
193
  buildAdapterAssetPlan({
194
194
  adapter,
195
195
  bundle,
@@ -201,7 +201,7 @@ describe('workspace assets snapshots', () => {
201
201
  }
202
202
  }
203
203
  })
204
- ))
204
+ )))
205
205
 
206
206
  await expect(serializeWorkspaceAssetsSnapshot({
207
207
  cwd: workspace,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-forge/workspace-assets",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Workspace asset resolution and adapter asset planning for Vibe Forge",
5
5
  "imports": {
6
6
  "#~/*.js": {
@@ -26,13 +26,13 @@
26
26
  "./package.json": "./package.json"
27
27
  },
28
28
  "dependencies": {
29
+ "@vibe-forge/config": "^2.0.2",
29
30
  "fast-glob": "^3.3.3",
30
31
  "front-matter": "^4.0.2",
31
32
  "js-yaml": "^4.1.1",
32
- "@vibe-forge/utils": "^2.0.0",
33
- "@vibe-forge/definition-core": "^2.0.0",
34
- "@vibe-forge/types": "^2.0.0",
35
- "@vibe-forge/config": "^2.0.0"
33
+ "@vibe-forge/utils": "^2.0.1",
34
+ "@vibe-forge/types": "^2.0.1",
35
+ "@vibe-forge/definition-core": "^2.0.1"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/js-yaml": "^4.0.9"