@swarmclawai/swarmclaw 0.9.2 → 0.9.4

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.
Files changed (75) hide show
  1. package/README.md +12 -10
  2. package/bundled-skills/google-workspace/SKILL.md +2 -0
  3. package/package.json +1 -1
  4. package/src/app/agents/page.tsx +2 -1
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +1 -1
  6. package/src/app/api/clawhub/install/route.ts +2 -0
  7. package/src/app/api/skills/[id]/route.ts +4 -0
  8. package/src/app/api/skills/route.ts +4 -0
  9. package/src/app/globals.css +28 -0
  10. package/src/app/home/page.tsx +11 -0
  11. package/src/app/settings/page.tsx +12 -5
  12. package/src/components/agents/agent-sheet.tsx +5 -5
  13. package/src/components/connectors/connector-list.tsx +2 -5
  14. package/src/components/logs/log-list.tsx +2 -5
  15. package/src/components/providers/provider-list.tsx +2 -5
  16. package/src/components/runs/run-list.tsx +2 -6
  17. package/src/components/schedules/schedule-list.tsx +7 -1
  18. package/src/components/ui/full-screen-loader.tsx +0 -29
  19. package/src/components/ui/page-loader.tsx +69 -0
  20. package/src/lib/runtime/runtime-loop.ts +21 -1
  21. package/src/lib/server/agents/agent-thread-session.test.ts +64 -0
  22. package/src/lib/server/agents/agent-thread-session.ts +1 -1
  23. package/src/lib/server/agents/main-agent-loop-advanced.test.ts +77 -0
  24. package/src/lib/server/agents/main-agent-loop.ts +259 -0
  25. package/src/lib/server/agents/orchestrator-lg.ts +12 -8
  26. package/src/lib/server/agents/orchestrator.ts +11 -7
  27. package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +11 -10
  28. package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +116 -3
  29. package/src/lib/server/chat-execution/chat-execution-utils.test.ts +56 -0
  30. package/src/lib/server/chat-execution/chat-execution-utils.ts +24 -0
  31. package/src/lib/server/chat-execution/chat-execution.ts +116 -29
  32. package/src/lib/server/chat-execution/chat-streaming-utils.ts +1 -38
  33. package/src/lib/server/chat-execution/stream-agent-chat.test.ts +67 -76
  34. package/src/lib/server/chat-execution/stream-agent-chat.ts +119 -110
  35. package/src/lib/server/chat-execution/stream-continuation.ts +1 -1
  36. package/src/lib/server/chatrooms/chatroom-helpers.test.ts +26 -0
  37. package/src/lib/server/chatrooms/chatroom-helpers.ts +11 -8
  38. package/src/lib/server/connectors/contact-boundaries.ts +101 -0
  39. package/src/lib/server/connectors/manager.test.ts +504 -73
  40. package/src/lib/server/connectors/manager.ts +41 -10
  41. package/src/lib/server/connectors/session-consolidation.ts +2 -0
  42. package/src/lib/server/connectors/session-kind.ts +7 -0
  43. package/src/lib/server/connectors/session.test.ts +104 -0
  44. package/src/lib/server/connectors/session.ts +5 -2
  45. package/src/lib/server/identity-continuity.test.ts +4 -3
  46. package/src/lib/server/identity-continuity.ts +8 -4
  47. package/src/lib/server/memory/memory-policy.test.ts +5 -15
  48. package/src/lib/server/memory/memory-policy.ts +11 -41
  49. package/src/lib/server/memory/session-archive-memory.ts +2 -1
  50. package/src/lib/server/runtime/heartbeat-service.test.ts +46 -0
  51. package/src/lib/server/runtime/heartbeat-service.ts +5 -1
  52. package/src/lib/server/runtime/runtime-settings.test.ts +4 -4
  53. package/src/lib/server/runtime/runtime-settings.ts +4 -0
  54. package/src/lib/server/runtime/session-run-manager.ts +2 -0
  55. package/src/lib/server/session-reset-policy.test.ts +17 -3
  56. package/src/lib/server/session-reset-policy.ts +4 -2
  57. package/src/lib/server/session-tools/connector.ts +11 -10
  58. package/src/lib/server/session-tools/crud.ts +41 -7
  59. package/src/lib/server/session-tools/delegate.ts +3 -3
  60. package/src/lib/server/session-tools/index.ts +2 -0
  61. package/src/lib/server/session-tools/manage-skills.test.ts +194 -0
  62. package/src/lib/server/session-tools/memory.ts +209 -48
  63. package/src/lib/server/session-tools/skill-runtime.test.ts +175 -0
  64. package/src/lib/server/session-tools/skill-runtime.ts +382 -0
  65. package/src/lib/server/session-tools/skills.ts +575 -0
  66. package/src/lib/server/skills/runtime-skill-resolver.test.ts +162 -0
  67. package/src/lib/server/skills/runtime-skill-resolver.ts +750 -0
  68. package/src/lib/server/skills/skill-discovery.ts +4 -0
  69. package/src/lib/server/skills/skills-normalize.test.ts +28 -0
  70. package/src/lib/server/skills/skills-normalize.ts +93 -1
  71. package/src/lib/server/storage.ts +1 -1
  72. package/src/lib/server/tasks/task-followups.test.ts +124 -0
  73. package/src/lib/server/tasks/task-followups.ts +88 -13
  74. package/src/types/index.ts +30 -2
  75. package/src/views/settings/section-runtime-loop.tsx +38 -0
@@ -0,0 +1,750 @@
1
+ import type {
2
+ Skill,
3
+ SkillCommandDispatch,
4
+ SkillInstallOption,
5
+ SkillInvocationConfig,
6
+ SkillRequirements,
7
+ SkillSecuritySummary,
8
+ } from '@/types'
9
+ import { dedup } from '@/lib/shared-utils'
10
+ import { expandPluginIds, getPluginAliases, normalizePluginId } from '@/lib/server/tool-aliases'
11
+ import { loadSettings, loadSkills } from '@/lib/server/storage'
12
+ import { discoverSkills, type DiscoveredSkill } from './skill-discovery'
13
+ import { evaluateSkillEligibility } from './skill-eligibility'
14
+ import {
15
+ MAX_SKILLS_IN_PROMPT,
16
+ MAX_SKILLS_PROMPT_CHARS,
17
+ } from './skill-prompt-budget'
18
+
19
+ export type RuntimeSkillSource = 'stored' | 'bundled' | 'workspace' | 'project'
20
+
21
+ type SkillSeed = {
22
+ runtimeId: string
23
+ storageId?: string
24
+ name: string
25
+ key: string
26
+ filename: string
27
+ content: string
28
+ description?: string
29
+ tags: string[]
30
+ toolNames: string[]
31
+ capabilities: string[]
32
+ source: RuntimeSkillSource
33
+ sourcePath?: string
34
+ sourceUrl?: string
35
+ sourceFormat?: Skill['sourceFormat']
36
+ author?: string
37
+ version?: string
38
+ homepage?: string
39
+ primaryEnv?: string | null
40
+ skillKey?: string | null
41
+ always: boolean
42
+ attached: boolean
43
+ installOptions?: SkillInstallOption[]
44
+ skillRequirements?: SkillRequirements
45
+ detectedEnvVars?: string[]
46
+ security?: SkillSecuritySummary | null
47
+ invocation?: SkillInvocationConfig | null
48
+ commandDispatch?: SkillCommandDispatch | null
49
+ frontmatter?: Record<string, unknown> | null
50
+ priority: number
51
+ }
52
+
53
+ export interface RuntimeSkillConfigCheck {
54
+ key: string
55
+ ok: boolean
56
+ }
57
+
58
+ export interface RuntimeSkillStatus {
59
+ eligible: boolean
60
+ missingBins: string[]
61
+ missingAnyBins: string[][]
62
+ missingEnv: string[]
63
+ missingConfig: string[]
64
+ unsupportedOs: boolean
65
+ reasons: string[]
66
+ configChecks: RuntimeSkillConfigCheck[]
67
+ installRequired: boolean
68
+ }
69
+
70
+ export interface ResolvedRuntimeSkill {
71
+ id: string
72
+ key: string
73
+ storageId?: string
74
+ name: string
75
+ filename: string
76
+ content: string
77
+ description?: string
78
+ tags: string[]
79
+ toolNames: string[]
80
+ capabilities: string[]
81
+ source: RuntimeSkillSource
82
+ sourcePath?: string
83
+ sourceUrl?: string
84
+ sourceFormat?: Skill['sourceFormat']
85
+ author?: string
86
+ version?: string
87
+ homepage?: string
88
+ primaryEnv?: string | null
89
+ skillKey?: string | null
90
+ always: boolean
91
+ attached: boolean
92
+ managed: boolean
93
+ installOptions?: SkillInstallOption[]
94
+ skillRequirements?: SkillRequirements
95
+ detectedEnvVars?: string[]
96
+ security?: SkillSecuritySummary | null
97
+ invocation?: SkillInvocationConfig | null
98
+ commandDispatch?: SkillCommandDispatch | null
99
+ frontmatter?: Record<string, unknown> | null
100
+ eligible: boolean
101
+ missing: string[]
102
+ reasons: string[]
103
+ status: 'ready' | 'needs_install' | 'blocked'
104
+ configChecks: RuntimeSkillConfigCheck[]
105
+ autoMatch: boolean
106
+ matchReasons: string[]
107
+ score: number
108
+ selected: boolean
109
+ executionMode: 'dispatch' | 'prompt'
110
+ runnable: boolean
111
+ dispatchToolAvailable: boolean
112
+ dispatchBlocker?: string | null
113
+ }
114
+
115
+ export interface RuntimeSkillSnapshot {
116
+ skills: ResolvedRuntimeSkill[]
117
+ promptSkills: ResolvedRuntimeSkill[]
118
+ availableSkills: ResolvedRuntimeSkill[]
119
+ autoMatchedSkills: ResolvedRuntimeSkill[]
120
+ attachedSkills: ResolvedRuntimeSkill[]
121
+ selectedSkill: ResolvedRuntimeSkill | null
122
+ }
123
+
124
+ export interface ResolveRuntimeSkillsOptions {
125
+ cwd?: string | null
126
+ enabledPlugins?: string[] | null
127
+ agentSkillIds?: string[] | null
128
+ storedSkills?: Record<string, Skill>
129
+ selectedSkillId?: string | null
130
+ }
131
+
132
+ export interface RuntimeSkillRecommendation {
133
+ skill: ResolvedRuntimeSkill
134
+ score: number
135
+ reasons: string[]
136
+ }
137
+
138
+ const SOURCE_PRIORITY: Record<RuntimeSkillSource, number> = {
139
+ bundled: 10,
140
+ stored: 20,
141
+ workspace: 30,
142
+ project: 40,
143
+ }
144
+
145
+ function normalizeKey(value: string | null | undefined): string {
146
+ return String(value || '')
147
+ .trim()
148
+ .toLowerCase()
149
+ .replace(/[^a-z0-9]+/g, '_')
150
+ .replace(/^_+|_+$/g, '')
151
+ }
152
+
153
+ function tokenize(value: string | null | undefined): string[] {
154
+ return dedup(
155
+ String(value || '')
156
+ .toLowerCase()
157
+ .split(/[^a-z0-9]+/g)
158
+ .map((part) => part.trim())
159
+ .filter((part) => part.length >= 2),
160
+ )
161
+ }
162
+
163
+ function scoreOverlap(a: string[], b: Set<string>): number {
164
+ let total = 0
165
+ for (const token of a) {
166
+ if (b.has(token)) total += 1
167
+ }
168
+ return total
169
+ }
170
+
171
+ function buildSkillKey(input: {
172
+ skillKey?: string | null
173
+ name: string
174
+ filename?: string | null
175
+ }): string {
176
+ return normalizeKey(input.skillKey || input.name || input.filename || 'skill')
177
+ }
178
+
179
+ function inferToolNames(input: {
180
+ name: string
181
+ skillKey?: string | null
182
+ explicit?: string[]
183
+ }): string[] {
184
+ const explicit = dedup((input.explicit || []).map((value) => normalizePluginId(value)).filter(Boolean))
185
+ if (explicit.length > 0) return explicit
186
+
187
+ const inferred = new Set<string>()
188
+ for (const candidate of [input.skillKey, input.name]) {
189
+ const normalized = normalizeKey(candidate || '')
190
+ if (!normalized) continue
191
+ const aliases = getPluginAliases(normalized)
192
+ if (aliases.length > 1) {
193
+ for (const alias of aliases) inferred.add(normalizePluginId(alias))
194
+ continue
195
+ }
196
+ const dashed = normalized.replace(/_/g, '-')
197
+ const dashedAliases = getPluginAliases(dashed)
198
+ if (dashedAliases.length > 1) {
199
+ for (const alias of dashedAliases) inferred.add(normalizePluginId(alias))
200
+ }
201
+ }
202
+ return [...inferred].filter(Boolean)
203
+ }
204
+
205
+ function inferCapabilities(input: {
206
+ name: string
207
+ description?: string
208
+ tags?: string[]
209
+ toolNames?: string[]
210
+ explicit?: string[]
211
+ }): string[] {
212
+ return dedup([
213
+ ...(input.explicit || []),
214
+ ...(input.tags || []),
215
+ ...(input.toolNames || []),
216
+ ...tokenize(input.name),
217
+ ...tokenize(input.description),
218
+ ].map((value) => value.trim()).filter(Boolean))
219
+ }
220
+
221
+ function asPlainRecord(value: unknown): Record<string, unknown> | null {
222
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null
223
+ return value as Record<string, unknown>
224
+ }
225
+
226
+ function readConfigPath(source: Record<string, unknown>, keyPath: string): unknown {
227
+ const parts = keyPath
228
+ .split('.')
229
+ .map((part) => part.trim())
230
+ .filter(Boolean)
231
+ let current: unknown = source
232
+ for (const part of parts) {
233
+ const record = asPlainRecord(current)
234
+ if (!record || !Object.prototype.hasOwnProperty.call(record, part)) return undefined
235
+ current = record[part]
236
+ }
237
+ return current
238
+ }
239
+
240
+ function isTruthyConfigValue(value: unknown): boolean {
241
+ if (value === undefined || value === null) return false
242
+ if (typeof value === 'boolean') return value
243
+ if (typeof value === 'number') return Number.isFinite(value)
244
+ if (typeof value === 'string') return value.trim().length > 0
245
+ if (Array.isArray(value)) return value.length > 0
246
+ if (typeof value === 'object') return Object.keys(value as Record<string, unknown>).length > 0
247
+ return true
248
+ }
249
+
250
+ function evaluateSkillStatus(seed: SkillSeed): RuntimeSkillStatus {
251
+ const baseSkill = {
252
+ id: seed.storageId || seed.runtimeId,
253
+ name: seed.name,
254
+ filename: seed.filename,
255
+ content: seed.content,
256
+ skillRequirements: seed.skillRequirements,
257
+ } satisfies Pick<Skill, 'id' | 'name' | 'filename' | 'content' | 'skillRequirements'>
258
+ const eligibility = evaluateSkillEligibility(baseSkill as Skill)
259
+ const settings = loadSettings() as Record<string, unknown>
260
+ const configKeys = Array.isArray(seed.skillRequirements?.config) ? seed.skillRequirements?.config : []
261
+ const configChecks = configKeys.map((key) => ({
262
+ key,
263
+ ok: isTruthyConfigValue(readConfigPath(settings, key)),
264
+ }))
265
+ const missingConfig = configChecks.filter((check) => !check.ok).map((check) => check.key)
266
+ const reasons = [
267
+ ...eligibility.reasons,
268
+ ...(missingConfig.length > 0 ? [`Missing config: ${missingConfig.join(', ')}`] : []),
269
+ ]
270
+ const installRequired = reasons.length > 0
271
+ && ((seed.installOptions?.length || 0) > 0 || eligibility.missingBins.length > 0 || eligibility.missingAnyBins.length > 0)
272
+
273
+ return {
274
+ ...eligibility,
275
+ missingConfig,
276
+ reasons,
277
+ configChecks,
278
+ installRequired,
279
+ eligible: eligibility.eligible && missingConfig.length === 0,
280
+ }
281
+ }
282
+
283
+ function formatMissing(status: RuntimeSkillStatus): string[] {
284
+ const missing: string[] = []
285
+ for (const value of status.missingBins) missing.push(value)
286
+ for (const group of status.missingAnyBins) {
287
+ if (group.length > 0) missing.push(`one of: ${group.join(' | ')}`)
288
+ }
289
+ for (const value of status.missingEnv) missing.push(`env ${value}`)
290
+ for (const value of status.missingConfig) missing.push(`config ${value}`)
291
+ if (status.unsupportedOs) missing.push(`os ${process.platform}`)
292
+ return missing
293
+ }
294
+
295
+ function buildSeedFromStored(skill: Skill, attachedIds: Set<string>): SkillSeed {
296
+ const explicitToolNames = Array.isArray(skill.toolNames) ? skill.toolNames : []
297
+ const toolNames = inferToolNames({
298
+ name: skill.name,
299
+ skillKey: skill.skillKey,
300
+ explicit: explicitToolNames,
301
+ })
302
+
303
+ return {
304
+ runtimeId: `runtime:stored:${buildSkillKey(skill)}`,
305
+ storageId: skill.id,
306
+ name: skill.name,
307
+ key: buildSkillKey(skill),
308
+ filename: skill.filename,
309
+ content: skill.content || '',
310
+ description: skill.description || '',
311
+ tags: dedup(Array.isArray(skill.tags) ? skill.tags : []),
312
+ toolNames,
313
+ capabilities: inferCapabilities({
314
+ name: skill.name,
315
+ description: skill.description,
316
+ tags: skill.tags,
317
+ toolNames,
318
+ explicit: Array.isArray(skill.capabilities) ? skill.capabilities : [],
319
+ }),
320
+ source: 'stored',
321
+ sourceUrl: skill.sourceUrl,
322
+ sourceFormat: skill.sourceFormat,
323
+ author: skill.author,
324
+ version: skill.version,
325
+ homepage: skill.homepage,
326
+ primaryEnv: skill.primaryEnv,
327
+ skillKey: skill.skillKey,
328
+ always: skill.always === true,
329
+ attached: attachedIds.has(skill.id),
330
+ installOptions: skill.installOptions,
331
+ skillRequirements: skill.skillRequirements,
332
+ detectedEnvVars: skill.detectedEnvVars,
333
+ security: skill.security,
334
+ invocation: skill.invocation,
335
+ commandDispatch: skill.commandDispatch,
336
+ frontmatter: skill.frontmatter,
337
+ priority: SOURCE_PRIORITY.stored,
338
+ }
339
+ }
340
+
341
+ function buildSeedFromDiscovered(skill: DiscoveredSkill): SkillSeed {
342
+ const explicitToolNames = Array.isArray(skill.toolNames) ? skill.toolNames : []
343
+ const toolNames = inferToolNames({
344
+ name: skill.name,
345
+ skillKey: skill.skillKey,
346
+ explicit: explicitToolNames,
347
+ })
348
+
349
+ return {
350
+ runtimeId: `runtime:${skill.source}:${buildSkillKey(skill)}`,
351
+ name: skill.name,
352
+ key: buildSkillKey(skill),
353
+ filename: skill.filename,
354
+ content: skill.content || '',
355
+ description: skill.description || '',
356
+ tags: dedup(Array.isArray(skill.tags) ? skill.tags : []),
357
+ toolNames,
358
+ capabilities: inferCapabilities({
359
+ name: skill.name,
360
+ description: skill.description,
361
+ tags: skill.tags,
362
+ toolNames,
363
+ explicit: Array.isArray(skill.capabilities) ? skill.capabilities : [],
364
+ }),
365
+ source: skill.source,
366
+ sourcePath: skill.sourcePath,
367
+ sourceUrl: skill.sourceUrl,
368
+ sourceFormat: skill.sourceFormat,
369
+ author: skill.author,
370
+ version: skill.version,
371
+ homepage: skill.homepage,
372
+ primaryEnv: skill.primaryEnv,
373
+ skillKey: skill.skillKey,
374
+ always: skill.always === true,
375
+ attached: false,
376
+ installOptions: skill.installOptions,
377
+ skillRequirements: skill.skillRequirements,
378
+ detectedEnvVars: skill.detectedEnvVars,
379
+ security: skill.security,
380
+ invocation: skill.invocation,
381
+ commandDispatch: skill.commandDispatch,
382
+ frontmatter: skill.frontmatter,
383
+ priority: SOURCE_PRIORITY[skill.source],
384
+ }
385
+ }
386
+
387
+ function mergeSeeds(seeds: SkillSeed[]): SkillSeed {
388
+ const ordered = [...seeds].sort((a, b) => a.priority - b.priority)
389
+ const winner = ordered[ordered.length - 1]
390
+ const storedSeed = [...ordered].reverse().find((entry) => entry.storageId)
391
+ return {
392
+ ...winner,
393
+ storageId: winner.storageId || storedSeed?.storageId,
394
+ attached: ordered.some((entry) => entry.attached),
395
+ always: ordered.some((entry) => entry.always),
396
+ tags: dedup(ordered.flatMap((entry) => entry.tags || [])),
397
+ toolNames: dedup(ordered.flatMap((entry) => entry.toolNames || [])),
398
+ capabilities: dedup(ordered.flatMap((entry) => entry.capabilities || [])),
399
+ invocation: [...ordered].reverse().find((entry) => entry.invocation)?.invocation || null,
400
+ commandDispatch: [...ordered].reverse().find((entry) => entry.commandDispatch)?.commandDispatch || null,
401
+ }
402
+ }
403
+
404
+ function scoreSkillForRuntime(seed: SkillSeed, status: RuntimeSkillStatus, enabledPluginSet: Set<string>): {
405
+ autoMatch: boolean
406
+ score: number
407
+ matchReasons: string[]
408
+ } {
409
+ let score = 0
410
+ const reasons: string[] = []
411
+ const matchingTools = seed.toolNames.filter((toolName) => enabledPluginSet.has(normalizePluginId(toolName)))
412
+ if (matchingTools.length > 0) {
413
+ score += 45 + matchingTools.length
414
+ reasons.push(`matches tools: ${matchingTools.join(', ')}`)
415
+ }
416
+ if (seed.attached) {
417
+ score += 80
418
+ reasons.push('attached to agent')
419
+ }
420
+ if (seed.always) {
421
+ score += 40
422
+ reasons.push('always-on')
423
+ }
424
+ if (status.eligible) score += 8
425
+ if (!status.eligible && status.installRequired) score += 2
426
+ return {
427
+ autoMatch: matchingTools.length > 0,
428
+ score,
429
+ matchReasons: reasons,
430
+ }
431
+ }
432
+
433
+ function toResolvedSkill(seed: SkillSeed, status: RuntimeSkillStatus, match: {
434
+ autoMatch: boolean
435
+ score: number
436
+ matchReasons: string[]
437
+ }, options: {
438
+ enabledPluginSet: Set<string>
439
+ selected: boolean
440
+ }): ResolvedRuntimeSkill {
441
+ const missing = formatMissing(status)
442
+ const state: ResolvedRuntimeSkill['status'] = status.eligible
443
+ ? 'ready'
444
+ : status.installRequired
445
+ ? 'needs_install'
446
+ : 'blocked'
447
+
448
+ const dispatch = seed.commandDispatch?.kind === 'tool' ? seed.commandDispatch : null
449
+ const dispatchToolAvailable = dispatch
450
+ ? options.enabledPluginSet.has(normalizePluginId(dispatch.toolName))
451
+ || options.enabledPluginSet.has(dispatch.toolName)
452
+ : false
453
+ const dispatchBlocker = dispatch
454
+ ? !dispatchToolAvailable
455
+ ? `dispatch tool ${dispatch.toolName} is not enabled in this session`
456
+ : !status.eligible
457
+ ? missing.length > 0
458
+ ? `skill is not ready: ${missing.join(', ')}`
459
+ : 'skill is not ready in this environment'
460
+ : null
461
+ : null
462
+ const executionMode: ResolvedRuntimeSkill['executionMode'] = dispatch ? 'dispatch' : 'prompt'
463
+ const runnable = executionMode === 'dispatch' && status.eligible && dispatchToolAvailable
464
+
465
+ return {
466
+ id: seed.runtimeId,
467
+ key: seed.key,
468
+ storageId: seed.storageId,
469
+ name: seed.name,
470
+ filename: seed.filename,
471
+ content: seed.content,
472
+ description: seed.description,
473
+ tags: seed.tags,
474
+ toolNames: seed.toolNames,
475
+ capabilities: seed.capabilities,
476
+ source: seed.source,
477
+ sourcePath: seed.sourcePath,
478
+ sourceUrl: seed.sourceUrl,
479
+ sourceFormat: seed.sourceFormat,
480
+ author: seed.author,
481
+ version: seed.version,
482
+ homepage: seed.homepage,
483
+ primaryEnv: seed.primaryEnv,
484
+ skillKey: seed.skillKey,
485
+ always: seed.always,
486
+ attached: seed.attached,
487
+ managed: Boolean(seed.storageId),
488
+ installOptions: seed.installOptions,
489
+ skillRequirements: seed.skillRequirements,
490
+ detectedEnvVars: seed.detectedEnvVars,
491
+ security: seed.security,
492
+ invocation: seed.invocation,
493
+ commandDispatch: seed.commandDispatch,
494
+ frontmatter: seed.frontmatter,
495
+ eligible: status.eligible,
496
+ missing,
497
+ reasons: status.reasons,
498
+ status: state,
499
+ configChecks: status.configChecks,
500
+ autoMatch: match.autoMatch,
501
+ matchReasons: match.matchReasons,
502
+ score: match.score,
503
+ selected: options.selected,
504
+ executionMode,
505
+ runnable,
506
+ dispatchToolAvailable,
507
+ dispatchBlocker,
508
+ }
509
+ }
510
+
511
+ export function resolveRuntimeSkills(options: ResolveRuntimeSkillsOptions = {}): RuntimeSkillSnapshot {
512
+ const storedSkills = options.storedSkills || loadSkills()
513
+ const attachedIds = new Set(Array.isArray(options.agentSkillIds) ? options.agentSkillIds.filter(Boolean) : [])
514
+ const discovered = discoverSkills({ cwd: options.cwd || undefined })
515
+ const seeds = [
516
+ ...Object.values(storedSkills).map((skill) => buildSeedFromStored(skill, attachedIds)),
517
+ ...discovered.map((skill) => buildSeedFromDiscovered(skill)),
518
+ ]
519
+
520
+ const grouped = new Map<string, SkillSeed[]>()
521
+ for (const seed of seeds) {
522
+ const current = grouped.get(seed.key) || []
523
+ current.push(seed)
524
+ grouped.set(seed.key, current)
525
+ }
526
+
527
+ const enabledPluginSet = new Set(
528
+ expandPluginIds(Array.isArray(options.enabledPlugins) ? options.enabledPlugins : [])
529
+ .map((entry) => normalizePluginId(entry))
530
+ .filter(Boolean),
531
+ )
532
+ const selectedSkillSelector = normalizeKey(options.selectedSkillId || '')
533
+
534
+ const skills = [...grouped.values()]
535
+ .map((entries) => {
536
+ const merged = mergeSeeds(entries)
537
+ const status = evaluateSkillStatus(merged)
538
+ const match = scoreSkillForRuntime(merged, status, enabledPluginSet)
539
+ const selected = Boolean(
540
+ selectedSkillSelector
541
+ && [
542
+ merged.runtimeId,
543
+ merged.storageId,
544
+ merged.key,
545
+ merged.name,
546
+ merged.skillKey,
547
+ ].some((value) => normalizeKey(value || '') === selectedSkillSelector),
548
+ )
549
+ return toResolvedSkill(merged, status, match, {
550
+ enabledPluginSet,
551
+ selected,
552
+ })
553
+ })
554
+ .sort((a, b) => {
555
+ if (b.score !== a.score) return b.score - a.score
556
+ return a.name.localeCompare(b.name)
557
+ })
558
+
559
+ const promptSkills = selectPromptSkills(skills)
560
+ const selectedSkill = skills.find((skill) => skill.selected) || null
561
+ const promptIds = new Set(promptSkills.map((skill) => skill.id))
562
+ return {
563
+ skills,
564
+ promptSkills,
565
+ selectedSkill,
566
+ attachedSkills: skills.filter((skill) => skill.attached),
567
+ autoMatchedSkills: skills.filter((skill) => skill.autoMatch),
568
+ availableSkills: skills.filter((skill) => !promptIds.has(skill.id) && !skill.selected),
569
+ }
570
+ }
571
+
572
+ function selectPromptSkills(skills: ResolvedRuntimeSkill[]): ResolvedRuntimeSkill[] {
573
+ const ordered = [...skills]
574
+ .filter((skill) =>
575
+ (skill.attached || skill.always)
576
+ && typeof skill.content === 'string'
577
+ && skill.content.trim(),
578
+ )
579
+ .sort((a, b) => {
580
+ const priorityA = (a.attached ? 1000 : 0) + (a.always ? 500 : 0) + a.score
581
+ const priorityB = (b.attached ? 1000 : 0) + (b.always ? 500 : 0) + b.score
582
+ if (priorityB !== priorityA) return priorityB - priorityA
583
+ return a.name.localeCompare(b.name)
584
+ })
585
+
586
+ const selected: ResolvedRuntimeSkill[] = []
587
+ let totalChars = 0
588
+ for (const skill of ordered) {
589
+ if (selected.length >= MAX_SKILLS_IN_PROMPT) break
590
+ const contentLen = skill.name.length + skill.content.length + 12
591
+ if (totalChars + contentLen > MAX_SKILLS_PROMPT_CHARS) continue
592
+ totalChars += contentLen
593
+ selected.push(skill)
594
+ }
595
+ return selected
596
+ }
597
+
598
+ function sectionFromSkills(params: {
599
+ title: string
600
+ preface: string
601
+ skills: ResolvedRuntimeSkill[]
602
+ }): string {
603
+ const usable = params.skills.filter((skill) => skill.content.trim())
604
+ if (usable.length === 0) return ''
605
+ const body = usable
606
+ .map((skill) => `### ${skill.name}\n${skill.content}`)
607
+ .join('\n\n')
608
+ return [params.title, params.preface, '', body].join('\n')
609
+ }
610
+
611
+ export function buildRuntimeSkillPromptBlocks(snapshot: RuntimeSkillSnapshot): string[] {
612
+ const selectedId = snapshot.selectedSkill?.id || null
613
+ const blocks = [
614
+ buildSkillRuntimeInstructionBlock(snapshot),
615
+ sectionFromSkills({
616
+ title: '## Active Selected Skill',
617
+ preface: [
618
+ 'This skill was already selected for the current task.',
619
+ 'Keep using it unless the task materially changes or the tool result proves it is the wrong fit.',
620
+ ].join('\n'),
621
+ skills: snapshot.selectedSkill ? [snapshot.selectedSkill] : [],
622
+ }),
623
+ sectionFromSkills({
624
+ title: '## Pinned Skills',
625
+ preface: [
626
+ 'Before responding, check these pinned or always-on skills first.',
627
+ 'They are the only skills included in the prompt before explicit selection.',
628
+ 'Other skills stay discoverable below and should be selected on demand through `use_skill`.',
629
+ ].join('\n'),
630
+ skills: snapshot.promptSkills.filter((skill) => !selectedId || skill.id !== selectedId),
631
+ }),
632
+ ...buildAvailableSkillBlocks(snapshot.availableSkills),
633
+ ]
634
+ return blocks.filter(Boolean)
635
+ }
636
+
637
+ function buildSkillRuntimeInstructionBlock(snapshot: RuntimeSkillSnapshot): string {
638
+ const availableCount = snapshot.availableSkills.length + (snapshot.selectedSkill ? 1 : 0) + snapshot.promptSkills.length
639
+ if (availableCount === 0) return ''
640
+ return [
641
+ '## Skill Runtime',
642
+ 'Before replying: scan the available skill names and descriptions below.',
643
+ '- If exactly one skill clearly applies, call `use_skill` with `action=\"select\"` for that skill.',
644
+ '- If the selected skill shows `mode=dispatch`, call `use_skill` with `action=\"run\"` so it dispatches through its bound tool.',
645
+ '- If the selected skill shows `mode=prompt`, call `use_skill` with `action=\"load\"` once, then follow the loaded guidance.',
646
+ '- Do not load more than one non-pinned skill up front.',
647
+ '- Use `manage_skills` only for installation, attachment, or broader discovery/install management.',
648
+ ].join('\n')
649
+ }
650
+
651
+ function buildAvailableSkillBlocks(skills: ResolvedRuntimeSkill[]): string[] {
652
+ const lines = skills
653
+ .slice(0, 12)
654
+ .map((skill) => {
655
+ const status = skill.status === 'ready'
656
+ ? 'ready'
657
+ : skill.missing.length > 0
658
+ ? `needs ${skill.missing.join(', ')}`
659
+ : skill.status
660
+ const hint = skill.description ? `: ${(skill.description || '').slice(0, 120)}` : ''
661
+ const mode = skill.executionMode === 'dispatch'
662
+ ? skill.dispatchToolAvailable
663
+ ? `mode=dispatch tool=${skill.commandDispatch?.toolName}`
664
+ : `mode=dispatch blocked=${skill.dispatchBlocker || 'tool unavailable'}`
665
+ : 'mode=prompt'
666
+ return `- **${skill.name}** [${status}; ${mode}]${hint}`
667
+ })
668
+ return lines.length > 0
669
+ ? [[
670
+ '## Available Skills',
671
+ 'Local skills are discoverable by default. Select one on demand with `use_skill` instead of loading many skill bodies into the prompt.',
672
+ '',
673
+ lines.join('\n'),
674
+ ].join('\n')]
675
+ : []
676
+ }
677
+
678
+ export function recommendRuntimeSkillsForTask(
679
+ skills: ResolvedRuntimeSkill[],
680
+ task: string,
681
+ enabledPlugins?: string[] | null,
682
+ ): RuntimeSkillRecommendation[] {
683
+ const queryTerms = new Set(tokenize(task))
684
+ const enabledPluginSet = new Set(
685
+ expandPluginIds(Array.isArray(enabledPlugins) ? enabledPlugins : [])
686
+ .map((entry) => normalizePluginId(entry))
687
+ .filter(Boolean),
688
+ )
689
+
690
+ return skills
691
+ .map((skill) => {
692
+ let score = skill.score
693
+ const reasons = [...skill.matchReasons]
694
+ const exactNameHit = queryTerms.has(normalizeKey(skill.name))
695
+ || queryTerms.has(normalizeKey(skill.skillKey || ''))
696
+ if (exactNameHit) {
697
+ score += 35
698
+ reasons.push('task mentions the skill directly')
699
+ }
700
+ const nameOverlap = scoreOverlap(tokenize(skill.name), queryTerms)
701
+ if (nameOverlap > 0) {
702
+ score += nameOverlap * 10
703
+ reasons.push('name overlaps task keywords')
704
+ }
705
+ const capabilityOverlap = scoreOverlap(skill.capabilities, queryTerms)
706
+ if (capabilityOverlap > 0) {
707
+ score += capabilityOverlap * 6
708
+ reasons.push('capabilities overlap task keywords')
709
+ }
710
+ const tagOverlap = scoreOverlap(skill.tags, queryTerms)
711
+ if (tagOverlap > 0) {
712
+ score += tagOverlap * 4
713
+ }
714
+ const descriptionOverlap = scoreOverlap(tokenize(skill.description), queryTerms)
715
+ if (descriptionOverlap > 0) {
716
+ score += descriptionOverlap * 3
717
+ }
718
+ const toolOverlap = skill.toolNames.filter((toolName) => enabledPluginSet.has(normalizePluginId(toolName)))
719
+ if (toolOverlap.length > 0) {
720
+ score += toolOverlap.length * 8
721
+ }
722
+ if (skill.attached) score += 12
723
+ if (skill.eligible) score += 6
724
+ return {
725
+ skill,
726
+ score,
727
+ reasons: dedup(reasons),
728
+ }
729
+ })
730
+ .filter((entry) => entry.score > 0)
731
+ .sort((a, b) => {
732
+ if (b.score !== a.score) return b.score - a.score
733
+ return a.skill.name.localeCompare(b.skill.name)
734
+ })
735
+ }
736
+
737
+ export function findResolvedSkill(
738
+ skills: ResolvedRuntimeSkill[],
739
+ selector: string,
740
+ ): ResolvedRuntimeSkill | null {
741
+ const normalized = normalizeKey(selector)
742
+ if (!normalized) return null
743
+ return skills.find((skill) =>
744
+ normalizeKey(skill.id) === normalized
745
+ || normalizeKey(skill.storageId || '') === normalized
746
+ || normalizeKey(skill.key) === normalized
747
+ || normalizeKey(skill.name) === normalized
748
+ || normalizeKey(skill.skillKey || '') === normalized,
749
+ ) || null
750
+ }