@vibe-forge/core 0.7.4 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-forge/core",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
4
4
  "imports": {
5
5
  "#~/*.js": {
6
6
  "__vibe-forge__": {
@@ -1,6 +1,7 @@
1
1
  import type { Cache, Config } from '@vibe-forge/core'
2
2
 
3
3
  import type { Logger } from '#~/utils/create-logger.js'
4
+ import type { AdapterAssetPlan, AssetDiagnostic, WorkspaceAssetBundle } from '#~/utils/workspace-assets.js'
4
5
 
5
6
  import type { ChatMessage, ChatMessageContent } from '../types'
6
7
 
@@ -35,6 +36,7 @@ export interface SessionInitInfo {
35
36
  cwd: string
36
37
  agents: string[]
37
38
  title?: string
39
+ assetDiagnostics?: AssetDiagnostic[]
38
40
  }
39
41
 
40
42
  export interface SessionSummaryInfo {
@@ -62,6 +64,7 @@ export interface AdapterCtx {
62
64
  logger: Logger
63
65
 
64
66
  configs: [Config?, Config?]
67
+ assets?: WorkspaceAssetBundle
65
68
  }
66
69
 
67
70
  export interface AdapterQueryOptions {
@@ -91,6 +94,8 @@ export interface AdapterQueryOptions {
91
94
  }
92
95
 
93
96
  extraOptions?: string[]
97
+ promptAssetIds?: string[]
98
+ assetPlan?: AdapterAssetPlan
94
99
 
95
100
  onEvent: (event: AdapterOutputEvent) => void
96
101
  }
@@ -1,102 +1,7 @@
1
1
  import process from 'node:process'
2
- import { basename, dirname } from 'node:path'
3
2
 
4
3
  import type { AdapterQueryOptions } from '#~/adapter/type.js'
5
- import { DefinitionLoader } from '#~/utils/definition-loader.js'
6
- import type { Definition, Entity, Filter, Skill, Spec } from '#~/utils/definition-loader.js'
7
-
8
- const filterSkills = (
9
- skills: Definition<Skill>[],
10
- selection?: AdapterQueryOptions['skills']
11
- ) => {
12
- if (selection == null) return skills
13
-
14
- const include = selection.include != null && selection.include.length > 0
15
- ? new Set(selection.include)
16
- : undefined
17
- const exclude = new Set(selection.exclude ?? [])
18
-
19
- return skills.filter((skill) => {
20
- const name = basename(dirname(skill.path))
21
- return (include == null || include.has(name)) && !exclude.has(name)
22
- })
23
- }
24
-
25
- const dedupeSkills = (skills: Definition<Skill>[]) => {
26
- const seen = new Set<string>()
27
- return skills.filter((skill) => {
28
- if (seen.has(skill.path)) return false
29
- seen.add(skill.path)
30
- return true
31
- })
32
- }
33
-
34
- type SkillSelectionInput =
35
- | AdapterQueryOptions['skills']
36
- | Entity['skills']
37
- | Spec['skills']
38
-
39
- const toNormalizedSkillSelection = (
40
- selection?: SkillSelectionInput
41
- ): AdapterQueryOptions['skills'] | undefined => {
42
- if (selection == null) return undefined
43
-
44
- if (Array.isArray(selection)) {
45
- return selection.length > 0
46
- ? {
47
- include: selection
48
- }
49
- : undefined
50
- }
51
-
52
- if ('type' in selection && Array.isArray(selection.list)) {
53
- const list = selection.list.filter((item): item is string => typeof item === 'string')
54
- return selection.type === 'include'
55
- ? {
56
- include: list
57
- }
58
- : {
59
- exclude: list
60
- }
61
- }
62
-
63
- return selection
64
- }
65
-
66
- const mergeSkillSelections = (
67
- ...selections: Array<SkillSelectionInput | undefined>
68
- ): AdapterQueryOptions['skills'] | undefined => {
69
- let include: Set<string> | undefined
70
- const exclude = new Set<string>()
71
-
72
- for (const selection of selections) {
73
- const normalized = toNormalizedSkillSelection(selection)
74
- if (normalized == null) continue
75
-
76
- if (normalized.include != null && normalized.include.length > 0) {
77
- const current = new Set(normalized.include)
78
- include = include == null
79
- ? current
80
- : new Set([...include].filter(item => current.has(item)))
81
- }
82
-
83
- for (const item of normalized.exclude ?? []) {
84
- exclude.add(item)
85
- }
86
- }
87
-
88
- if (include == null && exclude.size === 0) return undefined
89
-
90
- return {
91
- include: include == null ? undefined : [...include],
92
- exclude: exclude.size === 0 ? undefined : [...exclude]
93
- }
94
- }
95
-
96
- const getIncludedSkillNames = (selection?: SkillSelectionInput): string[] => {
97
- const normalized = toNormalizedSkillSelection(selection)
98
- return normalized?.include ?? []
99
- }
4
+ import { resolvePromptAssetSelection, resolveWorkspaceAssetBundle } from '#~/utils/workspace-assets.js'
100
5
 
101
6
  export async function generateAdapterQueryOptions(
102
7
  type: 'spec' | 'entity' | undefined,
@@ -106,113 +11,15 @@ export async function generateAdapterQueryOptions(
106
11
  skills?: AdapterQueryOptions['skills']
107
12
  }
108
13
  ) {
109
- const loader = new DefinitionLoader(cwd)
110
- const options: Partial<AdapterQueryOptions> = {}
111
- const systemPromptParts: string[] = []
112
- let effectiveSkillSelection = toNormalizedSkillSelection(input?.skills)
113
-
114
- // 1. 获取数据
115
- // 1.1 获取默认数据
116
- const entities = type !== 'entity'
117
- ? await loader.loadDefaultEntities()
118
- : []
119
- const defaultSkills = await loader.loadDefaultSkills()
120
- const rules = await loader.loadDefaultRules()
121
- const specs = await loader.loadDefaultSpecs()
122
-
123
- // 1.2 获取指定数据
124
- const targetSkills: Definition<Skill>[] = []
125
- let targetBody = ''
126
- let targetToolsFilter: Filter | undefined
127
- let targetMcpServersFilter: Filter | undefined
128
- if (type && name) {
129
- const data = {
130
- spec: await loader.loadSpec(name),
131
- entity: await loader.loadEntity(name)
132
- }[type]
133
- if (!data) {
134
- throw new Error(`Failed to load ${type} ${name}`)
135
- }
136
- const { attributes, body } = data
137
- if (
138
- attributes.rules
139
- ) {
140
- // always load spec or entity tagged rules
141
- rules.push(
142
- ...(
143
- await loader.loadRules(attributes.rules, {
144
- baseDir: dirname(data.path)
145
- })
146
- ).map((rule) => ({
147
- ...rule,
148
- attributes: {
149
- ...rule.attributes,
150
- // 实体或流程中的规则为默认加载
151
- always: true
152
- }
153
- }))
154
- )
155
- }
156
- if (
157
- attributes.skills
158
- ) {
159
- effectiveSkillSelection = mergeSkillSelections(
160
- input?.skills,
161
- attributes.skills
162
- )
163
- targetSkills.push(
164
- ...filterSkills(
165
- await loader.loadSkills(getIncludedSkillNames(attributes.skills)),
166
- effectiveSkillSelection
167
- )
168
- )
169
- }
170
-
171
- targetBody = body
172
- targetToolsFilter = attributes.tools
173
- targetMcpServersFilter = attributes.mcpServers
174
- }
175
- const skills = filterSkills(defaultSkills, effectiveSkillSelection)
176
- let selectedSkillsPrompt: Definition<Skill>[] = []
177
- if (input?.skills?.include != null && input.skills.include.length > 0) {
178
- selectedSkillsPrompt = dedupeSkills(
179
- filterSkills(
180
- await loader.loadSkills(input.skills.include),
181
- effectiveSkillSelection
182
- )
183
- )
184
- }
185
-
186
- // 2. 基于数据生成上下文
187
- // 2.1 加载关联上下文
188
- systemPromptParts.push(loader.generateRulesPrompt(rules))
189
- systemPromptParts.push(loader.generateSkillsPrompt(dedupeSkills(targetSkills)))
190
- systemPromptParts.push(loader.generateSkillsPrompt(
191
- selectedSkillsPrompt.filter(skill => !targetSkills.some(target => target.path === skill.path))
192
- ))
193
- // 2.2 加载上下文路由
194
- systemPromptParts.push(loader.generateEntitiesRoutePrompt(entities))
195
- systemPromptParts.push(loader.generateSkillsRoutePrompt(skills))
196
- systemPromptParts.push(loader.generateSpecRoutePrompt(specs))
197
- // 2.3 加载目标上下文与配置
198
- systemPromptParts.push(targetBody)
199
- targetToolsFilter && (
200
- options.tools = targetToolsFilter
201
- )
202
- targetMcpServersFilter && (
203
- options.mcpServers = targetMcpServersFilter
204
- )
205
-
206
- options.systemPrompt = systemPromptParts.join('\n\n')
14
+ const bundle = await resolveWorkspaceAssetBundle({ cwd })
15
+ const [data, resolvedOptions] = await resolvePromptAssetSelection({
16
+ bundle,
17
+ type,
18
+ name,
19
+ input
20
+ })
207
21
  return [
208
- {
209
- rules,
210
- targetSkills,
211
- entities,
212
- skills,
213
- specs,
214
- targetBody
215
- },
216
- options
22
+ data,
23
+ resolvedOptions as Partial<AdapterQueryOptions>
217
24
  ] as const
218
25
  }
@@ -5,6 +5,7 @@ import type { AdapterCtx, AdapterQueryOptions } from '@vibe-forge/core/adapter'
5
5
  import { getCache, setCache } from '@vibe-forge/core/utils/cache'
6
6
  import { createLogger } from '@vibe-forge/core/utils/create-logger'
7
7
  import { uuid } from '@vibe-forge/core/utils/uuid'
8
+ import { resolveWorkspaceAssetBundle } from '@vibe-forge/core/utils/workspace-assets'
8
9
 
9
10
  import { resolveServerLogLevel } from '#~/env.js'
10
11
 
@@ -52,6 +53,10 @@ export const prepare = async (
52
53
  __VF_PROJECT_WORKSPACE_FOLDER__: cwd
53
54
  }
54
55
  const [config, userConfig] = await loadConfig({ jsonVariables })
56
+ const assets = await resolveWorkspaceAssetBundle({
57
+ cwd,
58
+ configs: [config, userConfig]
59
+ })
55
60
  return [
56
61
  {
57
62
  ctxId,
@@ -62,7 +67,8 @@ export const prepare = async (
62
67
  get: (key) => getCache(cwd, ctxId, sessionId, key)
63
68
  },
64
69
  logger,
65
- configs: [config, userConfig]
70
+ configs: [config, userConfig],
71
+ assets
66
72
  } satisfies AdapterCtx
67
73
  ] as const
68
74
  }
@@ -1,7 +1,9 @@
1
1
  import type { AdapterCtx, AdapterOutputEvent, AdapterQueryOptions } from '#~/adapter/index.js'
2
2
  import { loadAdapter } from '#~/adapter/index.js'
3
3
  import type { ModelServiceConfig } from '#~/config.js'
4
+ import { createAdapterHookBridge } from '#~/hooks/bridge.js'
4
5
  import { callHook } from '#~/hooks/call.js'
6
+ import { buildAdapterAssetPlan } from '#~/utils/workspace-assets.js'
5
7
  import type { TaskDetail } from '#~/types.js'
6
8
 
7
9
  import { prepare } from './prepare'
@@ -127,14 +129,54 @@ export const run = async (
127
129
  return adapterNames[0]
128
130
  })()
129
131
 
132
+ const adapter = await loadAdapter(adapterType)
133
+ await adapter.init?.(ctx)
134
+
135
+ const resolvedModel = resolveQueryModel({
136
+ config,
137
+ userConfig,
138
+ inputModel: adapterOptions.model
139
+ })
140
+
130
141
  const originalOnEvent = adapterOptions.onEvent
142
+ const supportedAssetPlanAdapters = new Set(['claude-code', 'codex', 'opencode'])
143
+ const assetPlan = ctx.assets == null || !supportedAssetPlanAdapters.has(adapterType)
144
+ ? undefined
145
+ : buildAdapterAssetPlan({
146
+ adapter: adapterType as 'claude-code' | 'codex' | 'opencode',
147
+ bundle: ctx.assets,
148
+ options: {
149
+ mcpServers: adapterOptions.mcpServers,
150
+ skills: adapterOptions.skills,
151
+ promptAssetIds: adapterOptions.promptAssetIds
152
+ }
153
+ })
154
+ const nativeBridgeDisabledEvents = adapterType === 'codex' && ctx.env.__VF_PROJECT_AI_CODEX_NATIVE_HOOKS_AVAILABLE__ === '1'
155
+ ? ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop']
156
+ : adapterType === 'claude-code' && ctx.env.__VF_PROJECT_AI_CLAUDE_NATIVE_HOOKS_AVAILABLE__ === '1'
157
+ ? ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop']
158
+ : adapterType === 'opencode' && ctx.env.__VF_PROJECT_AI_OPENCODE_NATIVE_HOOKS_AVAILABLE__ === '1'
159
+ ? ['SessionStart', 'PreToolUse', 'PostToolUse', 'Stop']
160
+ : []
161
+ const hookBridge = createAdapterHookBridge({
162
+ ctx,
163
+ adapter: adapterType,
164
+ runtime: adapterOptions.runtime,
165
+ sessionId: adapterOptions.sessionId,
166
+ type: adapterOptions.type,
167
+ model: resolvedModel,
168
+ disabledEvents: nativeBridgeDisabledEvents
169
+ })
131
170
  const wrappedOnEvent = (event: AdapterOutputEvent) => {
171
+ hookBridge.handleOutput(event)
172
+
132
173
  if (event.type === 'init') {
133
174
  originalOnEvent({
134
175
  ...event,
135
176
  data: {
136
177
  ...event.data,
137
- adapter: adapterType
178
+ adapter: adapterType,
179
+ assetDiagnostics: assetPlan?.diagnostics ?? event.data.assetDiagnostics
138
180
  }
139
181
  })
140
182
  return
@@ -161,16 +203,7 @@ export const run = async (
161
203
  originalOnEvent(event)
162
204
  }
163
205
 
164
- const adapter = await loadAdapter(adapterType)
165
- await adapter.init?.(ctx)
166
-
167
- const resolvedModel = resolveQueryModel({
168
- config,
169
- userConfig,
170
- inputModel: adapterOptions.model
171
- })
172
-
173
- await callHook('TaskStart', {
206
+ const taskStartOutput = await callHook('TaskStart', {
174
207
  adapter: adapterType,
175
208
  cwd: ctx.cwd,
176
209
  sessionId: adapterOptions.sessionId,
@@ -178,14 +211,21 @@ export const run = async (
178
211
  options,
179
212
  adapterOptions
180
213
  }, ctx.env)
214
+ if (taskStartOutput?.continue === false) {
215
+ throw new Error(taskStartOutput.stopReason ?? 'TaskStart hook blocked task startup')
216
+ }
217
+ await hookBridge.start()
218
+ const description = await hookBridge.prepareInitialPrompt(adapterOptions.description)
181
219
  const session = await adapter.query(
182
220
  ctx,
183
221
  {
184
222
  ...adapterOptions,
223
+ assetPlan,
224
+ description,
185
225
  model: resolvedModel,
186
226
  onEvent: wrappedOnEvent
187
227
  }
188
228
  )
189
229
 
190
- return { session, ctx }
230
+ return { session: hookBridge.wrapSession(session), ctx }
191
231
  }