@pellux/goodvibes-agent 0.1.80 → 0.1.81

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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to GoodVibes Agent will be recorded here.
4
4
 
5
+ ## 0.1.81 - 2026-06-01
6
+
7
+ - Added local Agent skill bundles so users can group related local skills, enable or disable the bundle, and inject the bundle's member procedures into the same serial assistant conversation.
8
+ - Added `/agent-skills bundle ...` commands plus Agent workspace visibility for skill bundle counts, enabled bundle state, active skill count, and bundle membership.
9
+ - Restored the GitHub CI eval gate script and replaced the copied TUI/daemon release workflow with an Agent package release workflow that validates Bun global install, package contents, compiled binary launch, and optional npm publish.
10
+ - Kept bundles Agent-local and reviewable with no daemon lifecycle behavior, hidden background agents, or non-Agent knowledge fallback.
11
+
5
12
  ## 0.1.80 - 2026-06-01
6
13
 
7
14
  - Added an Agent day-one readiness checklist to the onboarding review step covering runtime connection, default model route, profile setup, isolated Agent Knowledge, local behavior, channels, routines/schedules, and explicit build delegation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "0.1.80",
3
+ "version": "0.1.81",
4
4
  "private": false,
5
5
  "description": "GoodVibes personal operator assistant TUI with a proactive Agent product brain, isolated Agent Knowledge, local profiles, routines, skills, personas, and explicit build delegation.",
6
6
  "type": "module",
@@ -92,6 +92,8 @@
92
92
  "publish:dry-run": "bun run scripts/publish-package.ts --dry-run",
93
93
  "publish:check": "bun run scripts/publish-check.ts",
94
94
  "package:install-check": "bun run scripts/package-install-check.ts",
95
+ "eval:gate": "bun test src/test/runtime/eval/runner.test.ts",
96
+ "ci:gate": "bun run typecheck && bun run test && bun run architecture:check && bun run perf:check && bun run eval:gate && bun run build && bun run publish:check && bun run package:install-check && bun run verification:ledger",
95
97
  "build:prod": "bun run scripts/build.ts",
96
98
  "build:all": "bun run scripts/build.ts --all",
97
99
  "perf:check": "bun run scripts/perf-check.ts",
@@ -24,6 +24,21 @@ export interface AgentSkillRecord {
24
24
  readonly reviewedAt?: string;
25
25
  }
26
26
 
27
+ export interface AgentSkillBundleRecord {
28
+ readonly id: string;
29
+ readonly name: string;
30
+ readonly description: string;
31
+ readonly skillIds: readonly string[];
32
+ readonly enabled: boolean;
33
+ readonly source: AgentSkillSource;
34
+ readonly provenance: string;
35
+ readonly reviewState: AgentSkillReviewState;
36
+ readonly staleReason?: string;
37
+ readonly createdAt: string;
38
+ readonly updatedAt: string;
39
+ readonly reviewedAt?: string;
40
+ }
41
+
27
42
  export interface AgentSkillCreateInput {
28
43
  readonly name: string;
29
44
  readonly description: string;
@@ -35,6 +50,15 @@ export interface AgentSkillCreateInput {
35
50
  readonly provenance?: string;
36
51
  }
37
52
 
53
+ export interface AgentSkillBundleCreateInput {
54
+ readonly name: string;
55
+ readonly description: string;
56
+ readonly skillIds: readonly string[];
57
+ readonly enabled?: boolean;
58
+ readonly source?: AgentSkillSource;
59
+ readonly provenance?: string;
60
+ }
61
+
38
62
  export interface AgentSkillUpdateInput {
39
63
  readonly name?: string;
40
64
  readonly description?: string;
@@ -44,15 +68,26 @@ export interface AgentSkillUpdateInput {
44
68
  readonly provenance?: string;
45
69
  }
46
70
 
71
+ export interface AgentSkillBundleUpdateInput {
72
+ readonly name?: string;
73
+ readonly description?: string;
74
+ readonly skillIds?: readonly string[];
75
+ readonly provenance?: string;
76
+ }
77
+
47
78
  export interface AgentSkillSnapshot {
48
79
  readonly path: string;
49
80
  readonly skills: readonly AgentSkillRecord[];
50
81
  readonly enabledSkills: readonly AgentSkillRecord[];
82
+ readonly bundles: readonly AgentSkillBundleRecord[];
83
+ readonly enabledBundles: readonly AgentSkillBundleRecord[];
84
+ readonly activeSkills: readonly AgentSkillRecord[];
51
85
  }
52
86
 
53
87
  interface SkillStoreFile {
54
88
  readonly version: 1;
55
89
  readonly skills: readonly AgentSkillRecord[];
90
+ readonly bundles: readonly AgentSkillBundleRecord[];
56
91
  }
57
92
 
58
93
  const STORE_VERSION = 1;
@@ -126,14 +161,44 @@ function parseSkill(value: unknown): AgentSkillRecord | null {
126
161
  };
127
162
  }
128
163
 
164
+ function parseBundle(value: unknown): AgentSkillBundleRecord | null {
165
+ if (!isRecord(value)) return null;
166
+ const id = readString(value.id).trim();
167
+ const name = normalizeName(readString(value.name));
168
+ const description = readString(value.description).trim();
169
+ const skillIds = readStringArray(value.skillIds).map(slugify).filter(Boolean);
170
+ if (!id || !name || !description || skillIds.length === 0) return null;
171
+ const reviewState = value.reviewState === 'reviewed' || value.reviewState === 'stale' ? value.reviewState : 'fresh';
172
+ const source = value.source === 'agent' || value.source === 'imported' || value.source === 'system' ? value.source : 'user';
173
+ const staleReason = readString(value.staleReason).trim();
174
+ const reviewedAt = readString(value.reviewedAt).trim();
175
+ return {
176
+ id,
177
+ name,
178
+ description,
179
+ skillIds: normalizeList(skillIds).map(slugify),
180
+ enabled: value.enabled === true,
181
+ source,
182
+ provenance: readString(value.provenance, source).trim() || source,
183
+ reviewState,
184
+ staleReason: staleReason || undefined,
185
+ createdAt: readString(value.createdAt, nowIso()),
186
+ updatedAt: readString(value.updatedAt, nowIso()),
187
+ reviewedAt: reviewedAt || undefined,
188
+ };
189
+ }
190
+
129
191
  function parseStore(raw: string): SkillStoreFile {
130
192
  const parsed: unknown = JSON.parse(raw);
131
- if (!isRecord(parsed)) return { version: STORE_VERSION, skills: [] };
193
+ if (!isRecord(parsed)) return { version: STORE_VERSION, skills: [], bundles: [] };
132
194
  return {
133
195
  version: STORE_VERSION,
134
196
  skills: Array.isArray(parsed.skills)
135
197
  ? parsed.skills.map(parseSkill).filter((entry): entry is AgentSkillRecord => entry !== null)
136
198
  : [],
199
+ bundles: Array.isArray(parsed.bundles)
200
+ ? parsed.bundles.map(parseBundle).filter((entry): entry is AgentSkillBundleRecord => entry !== null)
201
+ : [],
137
202
  };
138
203
  }
139
204
 
@@ -154,10 +219,18 @@ export class AgentSkillRegistry {
154
219
 
155
220
  public snapshot(): AgentSkillSnapshot {
156
221
  const store = this.readStore();
222
+ const enabledBundles = store.bundles.filter((bundle) => bundle.enabled);
223
+ const activeSkillIds = new Set<string>(store.skills.filter((skill) => skill.enabled).map((skill) => skill.id));
224
+ for (const bundle of enabledBundles) {
225
+ for (const skillId of bundle.skillIds) activeSkillIds.add(skillId);
226
+ }
157
227
  return {
158
228
  path: this.storePath,
159
229
  skills: [...store.skills],
160
230
  enabledSkills: store.skills.filter((skill) => skill.enabled),
231
+ bundles: [...store.bundles],
232
+ enabledBundles,
233
+ activeSkills: store.skills.filter((skill) => activeSkillIds.has(skill.id)),
161
234
  };
162
235
  }
163
236
 
@@ -178,12 +251,33 @@ export class AgentSkillRegistry {
178
251
  ].some((field) => field.toLowerCase().includes(normalized)));
179
252
  }
180
253
 
254
+ public listBundles(): readonly AgentSkillBundleRecord[] {
255
+ return this.snapshot().bundles;
256
+ }
257
+
258
+ public searchBundles(query: string): readonly AgentSkillBundleRecord[] {
259
+ const normalized = query.trim().toLowerCase();
260
+ if (!normalized) return this.listBundles();
261
+ return this.listBundles().filter((bundle) => [
262
+ bundle.id,
263
+ bundle.name,
264
+ bundle.description,
265
+ ...bundle.skillIds,
266
+ ].some((field) => field.toLowerCase().includes(normalized)));
267
+ }
268
+
181
269
  public get(idOrName: string): AgentSkillRecord | null {
182
270
  const lookup = idOrName.trim().toLowerCase();
183
271
  if (!lookup) return null;
184
272
  return this.list().find((skill) => skill.id.toLowerCase() === lookup || skill.name.toLowerCase() === lookup) ?? null;
185
273
  }
186
274
 
275
+ public getBundle(idOrName: string): AgentSkillBundleRecord | null {
276
+ const lookup = idOrName.trim().toLowerCase();
277
+ if (!lookup) return null;
278
+ return this.listBundles().find((bundle) => bundle.id.toLowerCase() === lookup || bundle.name.toLowerCase() === lookup) ?? null;
279
+ }
280
+
187
281
  public create(input: AgentSkillCreateInput): AgentSkillRecord {
188
282
  const store = this.readStore();
189
283
  const name = normalizeName(input.name);
@@ -212,6 +306,32 @@ export class AgentSkillRegistry {
212
306
  return skill;
213
307
  }
214
308
 
309
+ public createBundle(input: AgentSkillBundleCreateInput): AgentSkillBundleRecord {
310
+ const store = this.readStore();
311
+ const name = normalizeName(input.name);
312
+ const description = input.description.trim();
313
+ const skillIds = this.normalizeExistingSkillIds(store, input.skillIds);
314
+ this.validateBundleRequired(name, description, skillIds);
315
+ assertNoSecretLikeText([name, description, ...skillIds]);
316
+ const duplicate = store.bundles.find((bundle) => bundle.name.toLowerCase() === name.toLowerCase());
317
+ if (duplicate) throw new Error(`Skill bundle already exists: ${duplicate.id}`);
318
+ const timestamp = nowIso();
319
+ const bundle: AgentSkillBundleRecord = {
320
+ id: this.nextBundleId(name, store.bundles),
321
+ name,
322
+ description,
323
+ skillIds,
324
+ enabled: input.enabled === true,
325
+ source: input.source ?? 'user',
326
+ provenance: input.provenance?.trim() || input.source || 'user',
327
+ reviewState: 'fresh',
328
+ createdAt: timestamp,
329
+ updatedAt: timestamp,
330
+ };
331
+ this.writeStore({ ...store, bundles: [...store.bundles, bundle] });
332
+ return bundle;
333
+ }
334
+
215
335
  public update(idOrName: string, input: AgentSkillUpdateInput): AgentSkillRecord {
216
336
  const store = this.readStore();
217
337
  const existing = this.findInStore(store, idOrName);
@@ -243,6 +363,35 @@ export class AgentSkillRegistry {
243
363
  return updated;
244
364
  }
245
365
 
366
+ public updateBundle(idOrName: string, input: AgentSkillBundleUpdateInput): AgentSkillBundleRecord {
367
+ const store = this.readStore();
368
+ const existing = this.findBundleInStore(store, idOrName);
369
+ if (!existing) throw new Error(`Unknown skill bundle: ${idOrName}`);
370
+ const name = input.name === undefined ? existing.name : normalizeName(input.name);
371
+ const description = input.description === undefined ? existing.description : input.description.trim();
372
+ const skillIds = input.skillIds === undefined ? existing.skillIds : this.normalizeExistingSkillIds(store, input.skillIds);
373
+ this.validateBundleRequired(name, description, skillIds);
374
+ assertNoSecretLikeText([name, description, ...skillIds]);
375
+ const duplicate = store.bundles.find((bundle) => bundle.id !== existing.id && bundle.name.toLowerCase() === name.toLowerCase());
376
+ if (duplicate) throw new Error(`Skill bundle already exists: ${duplicate.id}`);
377
+ const updated: AgentSkillBundleRecord = {
378
+ ...existing,
379
+ name,
380
+ description,
381
+ skillIds,
382
+ provenance: input.provenance === undefined ? existing.provenance : input.provenance.trim() || existing.provenance,
383
+ reviewState: 'fresh',
384
+ staleReason: undefined,
385
+ reviewedAt: undefined,
386
+ updatedAt: nowIso(),
387
+ };
388
+ this.writeStore({
389
+ ...store,
390
+ bundles: store.bundles.map((bundle) => bundle.id === existing.id ? updated : bundle),
391
+ });
392
+ return updated;
393
+ }
394
+
246
395
  public setEnabled(idOrName: string, enabled: boolean): AgentSkillRecord {
247
396
  const store = this.readStore();
248
397
  const existing = this.findInStore(store, idOrName);
@@ -255,6 +404,18 @@ export class AgentSkillRegistry {
255
404
  return updated;
256
405
  }
257
406
 
407
+ public setBundleEnabled(idOrName: string, enabled: boolean): AgentSkillBundleRecord {
408
+ const store = this.readStore();
409
+ const existing = this.findBundleInStore(store, idOrName);
410
+ if (!existing) throw new Error(`Unknown skill bundle: ${idOrName}`);
411
+ const updated: AgentSkillBundleRecord = { ...existing, enabled, updatedAt: nowIso() };
412
+ this.writeStore({
413
+ ...store,
414
+ bundles: store.bundles.map((bundle) => bundle.id === existing.id ? updated : bundle),
415
+ });
416
+ return updated;
417
+ }
418
+
258
419
  public markReviewed(idOrName: string): AgentSkillRecord {
259
420
  const store = this.readStore();
260
421
  const existing = this.findInStore(store, idOrName);
@@ -273,6 +434,24 @@ export class AgentSkillRegistry {
273
434
  return updated;
274
435
  }
275
436
 
437
+ public markBundleReviewed(idOrName: string): AgentSkillBundleRecord {
438
+ const store = this.readStore();
439
+ const existing = this.findBundleInStore(store, idOrName);
440
+ if (!existing) throw new Error(`Unknown skill bundle: ${idOrName}`);
441
+ const updated: AgentSkillBundleRecord = {
442
+ ...existing,
443
+ reviewState: 'reviewed',
444
+ staleReason: undefined,
445
+ reviewedAt: nowIso(),
446
+ updatedAt: nowIso(),
447
+ };
448
+ this.writeStore({
449
+ ...store,
450
+ bundles: store.bundles.map((bundle) => bundle.id === existing.id ? updated : bundle),
451
+ });
452
+ return updated;
453
+ }
454
+
276
455
  public markStale(idOrName: string, reason: string): AgentSkillRecord {
277
456
  const store = this.readStore();
278
457
  const existing = this.findInStore(store, idOrName);
@@ -290,6 +469,23 @@ export class AgentSkillRegistry {
290
469
  return updated;
291
470
  }
292
471
 
472
+ public markBundleStale(idOrName: string, reason: string): AgentSkillBundleRecord {
473
+ const store = this.readStore();
474
+ const existing = this.findBundleInStore(store, idOrName);
475
+ if (!existing) throw new Error(`Unknown skill bundle: ${idOrName}`);
476
+ const updated: AgentSkillBundleRecord = {
477
+ ...existing,
478
+ reviewState: 'stale',
479
+ staleReason: reason.trim() || 'Marked stale by user.',
480
+ updatedAt: nowIso(),
481
+ };
482
+ this.writeStore({
483
+ ...store,
484
+ bundles: store.bundles.map((bundle) => bundle.id === existing.id ? updated : bundle),
485
+ });
486
+ return updated;
487
+ }
488
+
293
489
  public deleteSkill(idOrName: string): AgentSkillRecord {
294
490
  const store = this.readStore();
295
491
  const existing = this.findInStore(store, idOrName);
@@ -297,6 +493,20 @@ export class AgentSkillRegistry {
297
493
  this.writeStore({
298
494
  ...store,
299
495
  skills: store.skills.filter((skill) => skill.id !== existing.id),
496
+ bundles: store.bundles
497
+ .map((bundle) => ({ ...bundle, skillIds: bundle.skillIds.filter((skillId) => skillId !== existing.id) }))
498
+ .filter((bundle) => bundle.skillIds.length > 0),
499
+ });
500
+ return existing;
501
+ }
502
+
503
+ public deleteBundle(idOrName: string): AgentSkillBundleRecord {
504
+ const store = this.readStore();
505
+ const existing = this.findBundleInStore(store, idOrName);
506
+ if (!existing) throw new Error(`Unknown skill bundle: ${idOrName}`);
507
+ this.writeStore({
508
+ ...store,
509
+ bundles: store.bundles.filter((bundle) => bundle.id !== existing.id),
300
510
  });
301
511
  return existing;
302
512
  }
@@ -307,6 +517,12 @@ export class AgentSkillRegistry {
307
517
  if (!procedure) throw new Error('Skill procedure is required.');
308
518
  }
309
519
 
520
+ private validateBundleRequired(name: string, description: string, skillIds: readonly string[]): void {
521
+ if (!name) throw new Error('Skill bundle name is required.');
522
+ if (!description) throw new Error('Skill bundle description is required.');
523
+ if (skillIds.length === 0) throw new Error('Skill bundle must include at least one existing skill.');
524
+ }
525
+
310
526
  private nextId(name: string, skills: readonly AgentSkillRecord[]): string {
311
527
  const base = slugify(name);
312
528
  const ids = new Set(skills.map((skill) => skill.id));
@@ -318,14 +534,40 @@ export class AgentSkillRegistry {
318
534
  throw new Error(`Could not allocate skill id for ${name}.`);
319
535
  }
320
536
 
537
+ private nextBundleId(name: string, bundles: readonly AgentSkillBundleRecord[]): string {
538
+ const base = slugify(name);
539
+ const ids = new Set(bundles.map((bundle) => bundle.id));
540
+ if (!ids.has(base)) return base;
541
+ for (let index = 2; index < 1000; index += 1) {
542
+ const candidate = `${base}-${index}`;
543
+ if (!ids.has(candidate)) return candidate;
544
+ }
545
+ throw new Error(`Could not allocate skill bundle id for ${name}.`);
546
+ }
547
+
321
548
  private findInStore(store: SkillStoreFile, idOrName: string): AgentSkillRecord | null {
322
549
  const lookup = idOrName.trim().toLowerCase();
323
550
  if (!lookup) return null;
324
551
  return store.skills.find((skill) => skill.id.toLowerCase() === lookup || skill.name.toLowerCase() === lookup) ?? null;
325
552
  }
326
553
 
554
+ private findBundleInStore(store: SkillStoreFile, idOrName: string): AgentSkillBundleRecord | null {
555
+ const lookup = idOrName.trim().toLowerCase();
556
+ if (!lookup) return null;
557
+ return store.bundles.find((bundle) => bundle.id.toLowerCase() === lookup || bundle.name.toLowerCase() === lookup) ?? null;
558
+ }
559
+
560
+ private normalizeExistingSkillIds(store: SkillStoreFile, skillIds: readonly string[]): readonly string[] {
561
+ const normalized = normalizeList(skillIds).map(slugify);
562
+ const known = new Set(store.skills.map((skill) => skill.id));
563
+ for (const skillId of normalized) {
564
+ if (!known.has(skillId)) throw new Error(`Unknown skill for bundle: ${skillId}`);
565
+ }
566
+ return normalized;
567
+ }
568
+
327
569
  private readStore(): SkillStoreFile {
328
- if (!existsSync(this.storePath)) return { version: STORE_VERSION, skills: [] };
570
+ if (!existsSync(this.storePath)) return { version: STORE_VERSION, skills: [], bundles: [] };
329
571
  try {
330
572
  return parseStore(readFileSync(this.storePath, 'utf-8'));
331
573
  } catch (error) {
@@ -342,13 +584,21 @@ export class AgentSkillRegistry {
342
584
  }
343
585
 
344
586
  export function buildEnabledSkillsPrompt(shellPaths: ShellPathService): string | null {
345
- const enabled = AgentSkillRegistry.fromShellPaths(shellPaths).snapshot().enabledSkills;
346
- if (enabled.length === 0) return null;
587
+ const snapshot = AgentSkillRegistry.fromShellPaths(shellPaths).snapshot();
588
+ const active = snapshot.activeSkills;
589
+ if (active.length === 0 && snapshot.enabledBundles.length === 0) return null;
347
590
  return [
348
591
  '## Enabled GoodVibes Agent Skills',
349
592
  'Use these local reusable procedures inside the same serial assistant conversation when they fit the user request.',
350
593
  '',
351
- ...enabled.slice(0, 8).flatMap((skill) => [
594
+ ...snapshot.enabledBundles.slice(0, 4).flatMap((bundle) => [
595
+ `### Skill Bundle: ${bundle.name}`,
596
+ `Description: ${bundle.description}`,
597
+ `Review state: ${bundle.reviewState}`,
598
+ `Included skills: ${bundle.skillIds.join(', ')}`,
599
+ '',
600
+ ]),
601
+ ...active.slice(0, 8).flatMap((skill) => [
352
602
  `### ${skill.name}`,
353
603
  `Description: ${skill.description}`,
354
604
  `Review state: ${skill.reviewState}`,
@@ -144,6 +144,8 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
144
144
  actions: [
145
145
  { id: 'skills-list', label: 'List skills', detail: 'Print the full local Agent skill library.', command: '/agent-skills list', kind: 'command', safety: 'read-only' },
146
146
  { id: 'skills-enabled', label: 'Enabled skills', detail: 'Show only skills currently injected into Agent guidance.', command: '/agent-skills enabled', kind: 'command', safety: 'read-only' },
147
+ { id: 'skills-bundles', label: 'Skill bundles', detail: 'List reviewable groups of local skills that can be enabled together.', command: '/agent-skills bundle list', kind: 'command', safety: 'read-only' },
148
+ { id: 'skills-create-bundle', label: 'Create bundle', detail: 'Create a named skill bundle from existing skill ids with an explicit command.', command: '/agent-skills bundle create --name <name> --description <summary> --skills <id,id>', kind: 'command', safety: 'safe' },
147
149
  { id: 'skills-prev', label: 'Previous skill', detail: 'Move the local skill selection up without changing enabled state.', localKind: 'skill', selectionDelta: -1, kind: 'local-selection', safety: 'safe' },
148
150
  { id: 'skills-next', label: 'Next skill', detail: 'Move the local skill selection down without changing enabled state.', localKind: 'skill', selectionDelta: 1, kind: 'local-selection', safety: 'safe' },
149
151
  { id: 'skills-create', label: 'Create skill', detail: 'Open an in-workspace form for a reusable local procedure. No placeholder command is dispatched.', editorKind: 'skill', kind: 'editor', safety: 'safe' },
@@ -17,6 +17,8 @@ export interface AgentWorkspaceSetupChecklistInput {
17
17
  readonly enabledRoutineCount: number;
18
18
  readonly skillCount: number;
19
19
  readonly enabledSkillCount: number;
20
+ readonly skillBundleCount: number;
21
+ readonly enabledSkillBundleCount: number;
20
22
  readonly activePersonaName: string;
21
23
  readonly readyChannelCount: number;
22
24
  readonly voiceProviderCount: number;
@@ -77,10 +79,10 @@ export function buildAgentWorkspaceSetupChecklist(input: AgentWorkspaceSetupChec
77
79
  {
78
80
  id: 'skills',
79
81
  label: 'Skills',
80
- status: setupStatusForCount(input.enabledSkillCount, 'ready', input.skillCount > 0 ? 'recommended' : 'optional'),
81
- detail: input.skillCount > 0
82
- ? `${input.enabledSkillCount}/${input.skillCount} local skill(s) enabled.`
83
- : 'Create reusable local skills for repeated workflows.',
82
+ status: input.enabledSkillCount > 0 || input.enabledSkillBundleCount > 0 ? 'ready' : input.skillCount > 0 || input.skillBundleCount > 0 ? 'recommended' : 'optional',
83
+ detail: input.skillCount > 0 || input.skillBundleCount > 0
84
+ ? `${input.enabledSkillCount}/${input.skillCount} local skill(s) enabled; ${input.enabledSkillBundleCount}/${input.skillBundleCount} bundle(s) enabled.`
85
+ : 'Create reusable local skills and bundles for repeated workflows.',
84
86
  command: '/agent-skills',
85
87
  },
86
88
  {
@@ -2,7 +2,7 @@ import { basename, sep } from 'node:path';
2
2
  import type { CommandContext } from './command-registry.ts';
3
3
  import { AgentPersonaRegistry, type AgentPersonaRecord } from '../agent/persona-registry.ts';
4
4
  import { AgentRoutineRegistry, type AgentRoutineRecord } from '../agent/routine-registry.ts';
5
- import { AgentSkillRegistry, type AgentSkillRecord } from '../agent/skill-registry.ts';
5
+ import { AgentSkillRegistry, type AgentSkillBundleRecord, type AgentSkillRecord } from '../agent/skill-registry.ts';
6
6
  import { getAgentRuntimeProfilesRoot, listAgentRuntimeProfiles, listAgentRuntimeProfileTemplates } from '../agent/runtime-profile.ts';
7
7
  import { buildAgentWorkspaceChannels } from './agent-workspace-channels.ts';
8
8
  import { buildAgentWorkspaceSetupChecklist } from './agent-workspace-setup.ts';
@@ -86,6 +86,19 @@ function summarizeSkillItem(skill: AgentSkillRecord): AgentWorkspaceLocalLibrary
86
86
  };
87
87
  }
88
88
 
89
+ function summarizeSkillBundleItem(bundle: AgentSkillBundleRecord): AgentWorkspaceLocalLibraryItem {
90
+ return {
91
+ id: bundle.id,
92
+ name: bundle.name,
93
+ description: `${bundle.description} Skills: ${bundle.skillIds.join(', ')}`,
94
+ reviewState: bundle.reviewState,
95
+ source: bundle.source,
96
+ tags: bundle.skillIds,
97
+ triggers: [],
98
+ enabled: bundle.enabled,
99
+ };
100
+ }
101
+
89
102
  function summarizeRoutineItem(routine: AgentRoutineRecord): AgentWorkspaceLocalLibraryItem {
90
103
  return {
91
104
  id: routine.id,
@@ -158,15 +171,19 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
158
171
  const skillSnapshot = (() => {
159
172
  try {
160
173
  const shellPaths = context.workspace?.shellPaths;
161
- if (!shellPaths) return { count: 0, enabled: 0, items: [] };
174
+ if (!shellPaths) return { count: 0, enabled: 0, active: 0, bundleCount: 0, enabledBundleCount: 0, items: [], bundleItems: [] };
162
175
  const snapshot = AgentSkillRegistry.fromShellPaths(shellPaths).snapshot();
163
176
  return {
164
177
  count: snapshot.skills.length,
165
178
  enabled: snapshot.enabledSkills.length,
179
+ active: snapshot.activeSkills.length,
180
+ bundleCount: snapshot.bundles.length,
181
+ enabledBundleCount: snapshot.enabledBundles.length,
166
182
  items: snapshot.skills.map(summarizeSkillItem),
183
+ bundleItems: snapshot.bundles.map(summarizeSkillBundleItem),
167
184
  };
168
185
  } catch {
169
- return { count: 0, enabled: 0, items: [] };
186
+ return { count: 0, enabled: 0, active: 0, bundleCount: 0, enabledBundleCount: 0, items: [], bundleItems: [] };
170
187
  }
171
188
  })();
172
189
  const routineSnapshot = (() => {
@@ -251,6 +268,8 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
251
268
  enabledRoutineCount: routineSnapshot.enabled,
252
269
  skillCount: skillSnapshot.count,
253
270
  enabledSkillCount: skillSnapshot.enabled,
271
+ skillBundleCount: skillSnapshot.bundleCount,
272
+ enabledSkillBundleCount: skillSnapshot.enabledBundleCount,
254
273
  activePersonaName: personaSnapshot.activeName,
255
274
  readyChannelCount: channels.filter((channel) => channel.ready).length,
256
275
  voiceProviderCount: voiceProviders.length,
@@ -274,6 +293,10 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
274
293
  localRoutines: routineSnapshot.items,
275
294
  localSkillCount: skillSnapshot.count,
276
295
  enabledSkillCount: skillSnapshot.enabled,
296
+ localSkillBundleCount: skillSnapshot.bundleCount,
297
+ enabledSkillBundleCount: skillSnapshot.enabledBundleCount,
298
+ activeSkillCount: skillSnapshot.active,
299
+ localSkillBundles: skillSnapshot.bundleItems,
277
300
  localSkills: skillSnapshot.items,
278
301
  localPersonaCount: personaSnapshot.count,
279
302
  activePersonaName: personaSnapshot.activeName,
@@ -128,6 +128,10 @@ export interface AgentWorkspaceRuntimeSnapshot {
128
128
  readonly localRoutines: readonly AgentWorkspaceLocalLibraryItem[];
129
129
  readonly localSkillCount: number;
130
130
  readonly enabledSkillCount: number;
131
+ readonly localSkillBundleCount: number;
132
+ readonly enabledSkillBundleCount: number;
133
+ readonly activeSkillCount: number;
134
+ readonly localSkillBundles: readonly AgentWorkspaceLocalLibraryItem[];
131
135
  readonly localSkills: readonly AgentWorkspaceLocalLibraryItem[];
132
136
  readonly localPersonaCount: number;
133
137
  readonly activePersonaName: string;
@@ -1,4 +1,4 @@
1
- import { AgentSkillRegistry, type AgentSkillRecord } from '../../agent/skill-registry.ts';
1
+ import { AgentSkillRegistry, type AgentSkillBundleRecord, type AgentSkillRecord } from '../../agent/skill-registry.ts';
2
2
  import type { CommandContext, CommandRegistry } from '../command-registry.ts';
3
3
  import { requireShellPaths } from './runtime-services.ts';
4
4
 
@@ -55,6 +55,11 @@ function summarizeSkill(skill: AgentSkillRecord): string {
55
55
  return ` ${skill.id} ${enabled} ${skill.reviewState} ${skill.name} - ${skill.description}${tags}`;
56
56
  }
57
57
 
58
+ function summarizeBundle(bundle: AgentSkillBundleRecord): string {
59
+ const enabled = bundle.enabled ? 'enabled' : 'disabled';
60
+ return ` ${bundle.id} ${enabled} ${bundle.reviewState} ${bundle.name} - ${bundle.description} skills=${bundle.skillIds.join(',')}`;
61
+ }
62
+
58
63
  function renderList(title: string, registry: AgentSkillRegistry, skills: readonly AgentSkillRecord[]): string {
59
64
  const snapshot = registry.snapshot();
60
65
  if (skills.length === 0) {
@@ -68,6 +73,20 @@ function renderList(title: string, registry: AgentSkillRegistry, skills: readonl
68
73
  ].join('\n');
69
74
  }
70
75
 
76
+ function renderBundleList(title: string, registry: AgentSkillRegistry, bundles: readonly AgentSkillBundleRecord[]): string {
77
+ const snapshot = registry.snapshot();
78
+ if (bundles.length === 0) {
79
+ return `${title}\n No local Agent skill bundles yet. Create one with /agent-skills bundle create --name <name> --description <summary> --skills <id,id>.`;
80
+ }
81
+ return [
82
+ `${title} (${bundles.length})`,
83
+ ` store: ${snapshot.path}`,
84
+ ` enabled bundles: ${snapshot.enabledBundles.length}`,
85
+ ` active skills: ${snapshot.activeSkills.length}`,
86
+ ...bundles.map(summarizeBundle),
87
+ ].join('\n');
88
+ }
89
+
71
90
  function renderSkill(skill: AgentSkillRecord): string {
72
91
  return [
73
92
  `Skill ${skill.name}`,
@@ -88,14 +107,143 @@ function renderSkill(skill: AgentSkillRecord): string {
88
107
  ].filter(Boolean).join('\n');
89
108
  }
90
109
 
110
+ function renderBundle(bundle: AgentSkillBundleRecord, registry: AgentSkillRegistry): string {
111
+ const skills = bundle.skillIds
112
+ .map((skillId) => registry.get(skillId))
113
+ .filter((skill): skill is AgentSkillRecord => skill !== null);
114
+ return [
115
+ `Skill Bundle ${bundle.name}`,
116
+ ` id: ${bundle.id}`,
117
+ ` enabled: ${bundle.enabled ? 'yes' : 'no'}`,
118
+ ` review: ${bundle.reviewState}`,
119
+ ` source: ${bundle.source}`,
120
+ ` provenance: ${bundle.provenance}`,
121
+ ` skills: ${bundle.skillIds.join(', ')}`,
122
+ ` created: ${bundle.createdAt}`,
123
+ ` updated: ${bundle.updatedAt}`,
124
+ bundle.staleReason ? ` stale reason: ${bundle.staleReason}` : '',
125
+ '',
126
+ bundle.description,
127
+ '',
128
+ ...skills.map((skill) => `- ${skill.id}: ${skill.name} - ${skill.description}`),
129
+ ].filter(Boolean).join('\n');
130
+ }
131
+
91
132
  function printError(ctx: CommandContext, error: unknown): void {
92
133
  ctx.print(`Error: ${error instanceof Error ? error.message : String(error)}`);
93
134
  }
94
135
 
136
+ function runBundleCommand(args: readonly string[], ctx: CommandContext, skillRegistry: AgentSkillRegistry): void {
137
+ const sub = (args[0] ?? 'list').toLowerCase();
138
+ if (sub === 'list' || sub === 'open') {
139
+ ctx.print(renderBundleList('Agent Skill Bundles', skillRegistry, skillRegistry.listBundles()));
140
+ return;
141
+ }
142
+ if (sub === 'enabled') {
143
+ const snapshot = skillRegistry.snapshot();
144
+ ctx.print(renderBundleList('Enabled Agent Skill Bundles', skillRegistry, snapshot.enabledBundles));
145
+ return;
146
+ }
147
+ if (sub === 'search') {
148
+ const query = args.slice(1).join(' ').trim();
149
+ ctx.print(renderBundleList(query ? `Agent Skill Bundles matching "${query}"` : 'Agent Skill Bundles', skillRegistry, skillRegistry.searchBundles(query)));
150
+ return;
151
+ }
152
+ if (sub === 'show') {
153
+ const id = args[1];
154
+ if (!id) {
155
+ ctx.print('Usage: /agent-skills bundle show <id>');
156
+ return;
157
+ }
158
+ const bundle = skillRegistry.getBundle(id);
159
+ ctx.print(bundle ? renderBundle(bundle, skillRegistry) : `Unknown Agent skill bundle: ${id}`);
160
+ return;
161
+ }
162
+ if (sub === 'create') {
163
+ const parsed = parseSkillArgs(args.slice(1));
164
+ const bundle = skillRegistry.createBundle({
165
+ name: requiredFlag(parsed.flags, 'name'),
166
+ description: requiredFlag(parsed.flags, 'description'),
167
+ skillIds: splitList(requiredFlag(parsed.flags, 'skills')),
168
+ enabled: parsed.flags.get('enabled') === 'true',
169
+ source: 'user',
170
+ provenance: 'slash-command',
171
+ });
172
+ ctx.print(`Created Agent skill bundle ${bundle.id}: ${bundle.name}`);
173
+ return;
174
+ }
175
+ if (sub === 'update') {
176
+ const id = args[1];
177
+ if (!id) {
178
+ ctx.print('Usage: /agent-skills bundle update <id> [--name ...] [--description ...] [--skills id,id]');
179
+ return;
180
+ }
181
+ const parsed = parseSkillArgs(args.slice(2));
182
+ const updated = skillRegistry.updateBundle(id, {
183
+ name: parsed.flags.get('name'),
184
+ description: parsed.flags.get('description'),
185
+ skillIds: parsed.flags.has('skills') ? splitList(parsed.flags.get('skills')) : undefined,
186
+ provenance: 'slash-command',
187
+ });
188
+ ctx.print(`Updated Agent skill bundle ${updated.id}: ${updated.name}`);
189
+ return;
190
+ }
191
+ if (sub === 'enable' || sub === 'disable') {
192
+ const id = args[1];
193
+ if (!id) {
194
+ ctx.print(`Usage: /agent-skills bundle ${sub} <id>`);
195
+ return;
196
+ }
197
+ const bundle = skillRegistry.setBundleEnabled(id, sub === 'enable');
198
+ ctx.print(`${sub === 'enable' ? 'Enabled' : 'Disabled'} Agent skill bundle ${bundle.id}: ${bundle.name}`);
199
+ return;
200
+ }
201
+ if (sub === 'review') {
202
+ const id = args[1];
203
+ if (!id) {
204
+ ctx.print('Usage: /agent-skills bundle review <id>');
205
+ return;
206
+ }
207
+ const bundle = skillRegistry.markBundleReviewed(id);
208
+ ctx.print(`Reviewed Agent skill bundle ${bundle.id}.`);
209
+ return;
210
+ }
211
+ if (sub === 'stale') {
212
+ const id = args[1];
213
+ if (!id) {
214
+ ctx.print('Usage: /agent-skills bundle stale <id> <reason...>');
215
+ return;
216
+ }
217
+ const bundle = skillRegistry.markBundleStale(id, args.slice(2).join(' '));
218
+ ctx.print(`Marked Agent skill bundle ${bundle.id} stale.`);
219
+ return;
220
+ }
221
+ if (sub === 'delete' || sub === 'remove') {
222
+ const parsed = parseSkillArgs(args.slice(1));
223
+ const id = parsed.rest[0];
224
+ if (!id) {
225
+ ctx.print('Usage: /agent-skills bundle delete <id> --yes');
226
+ return;
227
+ }
228
+ if (!parsed.yes) {
229
+ ctx.print(`Refusing to delete Agent skill bundle ${id} without --yes.`);
230
+ return;
231
+ }
232
+ const removed = skillRegistry.deleteBundle(id);
233
+ ctx.print(`Deleted Agent skill bundle ${removed.id}: ${removed.name}`);
234
+ return;
235
+ }
236
+ ctx.print('Usage: /agent-skills bundle [list|enabled|search|show|create|update|enable|disable|review|stale|delete]');
237
+ }
238
+
95
239
  export async function runAgentSkillsRuntimeCommand(args: readonly string[], ctx: CommandContext): Promise<void> {
96
240
  const sub = (args[0] ?? 'list').toLowerCase();
97
241
  const skillRegistry = registryFromContext(ctx);
98
242
  try {
243
+ if (sub === 'bundle' || sub === 'bundles') {
244
+ runBundleCommand(args.slice(1), ctx, skillRegistry);
245
+ return;
246
+ }
99
247
  if (sub === 'list' || sub === 'open') {
100
248
  ctx.print(renderList('Agent Skills', skillRegistry, skillRegistry.list()));
101
249
  return;
@@ -199,7 +347,7 @@ export async function runAgentSkillsRuntimeCommand(args: readonly string[], ctx:
199
347
  ctx.print(`Deleted Agent skill ${removed.id}: ${removed.name}`);
200
348
  return;
201
349
  }
202
- ctx.print('Usage: /agent-skills [list|enabled|search|show|create|update|enable|disable|review|stale|delete]');
350
+ ctx.print('Usage: /agent-skills [list|enabled|search|show|create|update|enable|disable|review|stale|delete|bundle]');
203
351
  } catch (error) {
204
352
  printError(ctx, error);
205
353
  }
@@ -210,7 +358,7 @@ export function registerAgentSkillsRuntimeCommands(registry: CommandRegistry): v
210
358
  name: 'agent-skills',
211
359
  aliases: ['askills', 'local-skills'],
212
360
  description: 'Manage local GoodVibes Agent skills',
213
- usage: '[list|enabled|search <query>|show <id>|create --name <name> --description <summary> --procedure <steps>|update <id> [--name ...] [--description ...] [--procedure ...]|enable <id>|disable <id>|review <id>|stale <id> <reason...>|delete <id> --yes]',
361
+ usage: '[list|enabled|search <query>|show <id>|create --name <name> --description <summary> --procedure <steps>|update <id> [--name ...] [--description ...] [--procedure ...]|enable <id>|disable <id>|review <id>|stale <id> <reason...>|delete <id> --yes|bundle ...]',
214
362
  handler: runAgentSkillsRuntimeCommand,
215
363
  });
216
364
  }
@@ -196,10 +196,10 @@ function snapshotLines(workspace: AgentWorkspace, category: AgentWorkspaceCatego
196
196
  base.push(
197
197
  { text: `GoodVibes runtime: ${snapshot.daemonBaseUrl}`, fg: PALETTE.info },
198
198
  { text: `Runtime owner: ${snapshot.daemonOwnership}; Agent connects but never starts or restarts it`, fg: PALETTE.good },
199
+ ...setupChecklistLines(snapshot),
200
+ { text: '' },
199
201
  { text: `Workspace: ${snapshot.workingDirectory}`, fg: PALETTE.muted },
200
202
  { text: `Home: ${snapshot.homeDirectory}`, fg: PALETTE.muted },
201
- { text: '' },
202
- ...setupChecklistLines(snapshot),
203
203
  );
204
204
  } else if (category.id === 'channels') {
205
205
  const enabledCount = snapshot.channels.filter((channel) => channel.enabled).length;
@@ -283,10 +283,11 @@ function snapshotLines(workspace: AgentWorkspace, category: AgentWorkspaceCatego
283
283
  base.push(
284
284
  { text: `Active Agent profile: ${snapshot.activeRuntimeProfile}`, fg: PALETTE.info },
285
285
  { text: `Agent profiles under this home: ${snapshot.runtimeProfileCount}`, fg: PALETTE.info },
286
- { text: `Agent profile root: ${snapshot.runtimeProfileRoot}`, fg: PALETTE.muted },
287
286
  { text: `Starter templates: ${snapshot.runtimeStarterTemplateCount}; local custom: ${snapshot.localStarterTemplateCount}`, fg: PALETTE.info },
288
287
  { text: `Config profiles: ${snapshot.configProfileCount}`, fg: PALETTE.info },
289
288
  { text: `Starter ids: ${snapshot.runtimeStarterTemplates.map((template) => template.id).join(', ') || 'none'}`, fg: PALETTE.info },
289
+ { text: 'Starter Templates', fg: PALETTE.title, bold: true },
290
+ { text: `Agent profile root: ${snapshot.runtimeProfileRoot}`, fg: PALETTE.muted },
290
291
  { text: '' },
291
292
  ...profileLines(snapshot),
292
293
  { text: '' },
@@ -301,7 +302,7 @@ function snapshotLines(workspace: AgentWorkspace, category: AgentWorkspaceCatego
301
302
  base.push(
302
303
  { text: `Session memories: ${snapshot.sessionMemoryCount}`, fg: PALETTE.info },
303
304
  { text: `Local routines: ${snapshot.localRoutineCount}; enabled: ${snapshot.enabledRoutineCount}`, fg: PALETTE.info },
304
- { text: `Local skills: ${snapshot.localSkillCount}; enabled: ${snapshot.enabledSkillCount}`, fg: PALETTE.info },
305
+ { text: `Local skills: ${snapshot.localSkillCount}; enabled: ${snapshot.enabledSkillCount}; bundles: ${snapshot.localSkillBundleCount}; active skills: ${snapshot.activeSkillCount}`, fg: PALETTE.info },
305
306
  { text: `Local personas: ${snapshot.localPersonaCount}; active: ${snapshot.activePersonaName}`, fg: PALETTE.info },
306
307
  { text: 'Durable memory, routines, skills, and personas remain Agent-local until shared registry contracts exist.', fg: PALETTE.good },
307
308
  { text: 'Secrets are rejected/redacted; store secret references instead of secret values.', fg: PALETTE.warn },
@@ -316,11 +317,13 @@ function snapshotLines(workspace: AgentWorkspace, category: AgentWorkspaceCatego
316
317
  );
317
318
  } else if (category.id === 'skills') {
318
319
  base.push(
319
- { text: `Skills: ${snapshot.localSkillCount}; enabled: ${snapshot.enabledSkillCount}`, fg: PALETTE.info },
320
+ { text: `Skills: ${snapshot.localSkillCount}; enabled: ${snapshot.enabledSkillCount}; bundles: ${snapshot.localSkillBundleCount}; enabled bundles: ${snapshot.enabledSkillBundleCount}; active skills: ${snapshot.activeSkillCount}`, fg: PALETTE.info },
320
321
  { text: 'Skills are reusable local procedures the assistant can apply from the main conversation.', fg: PALETTE.good },
321
- { text: 'Enabled skills are injected as operating guidance; secret-looking content is rejected.', fg: PALETTE.warn },
322
+ { text: 'Enabled skills and enabled bundles are injected as operating guidance; secret-looking content is rejected.', fg: PALETTE.warn },
322
323
  { text: '' },
323
324
  ...localLibraryLines('Skill Library', snapshot.localSkills, 'No local skills yet. Create one here with Create skill.', workspace.selectedLocalLibraryItem('skill')?.id ?? null),
325
+ { text: '' },
326
+ ...localLibraryLines('Skill Bundles', snapshot.localSkillBundles, 'No local skill bundles yet. Use Skill bundles and Create bundle after creating skills.', null),
324
327
  );
325
328
  } else if (category.id === 'routines') {
326
329
  base.push(
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.1.80';
9
+ let _version = '0.1.81';
10
10
  let _sdkVersion = '0.33.35';
11
11
  try {
12
12
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8')) as {