@pellux/goodvibes-agent 0.1.2 → 0.1.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 (46) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +12 -1
  4. package/docs/README.md +2 -0
  5. package/docs/getting-started.md +19 -1
  6. package/docs/release-and-publishing.md +3 -1
  7. package/package.json +10 -1
  8. package/src/agent/persona-registry.ts +379 -0
  9. package/src/agent/skill-registry.ts +360 -0
  10. package/src/audio/spoken-turn-model-routing.ts +2 -1
  11. package/src/cli/agent-knowledge-command.ts +46 -10
  12. package/src/cli/management-commands.ts +3 -1
  13. package/src/config/surface.ts +1 -0
  14. package/src/input/agent-workspace.ts +32 -2
  15. package/src/input/command-registry.ts +4 -1
  16. package/src/input/commands/agent-skills-runtime.ts +216 -0
  17. package/src/input/commands/knowledge.ts +18 -18
  18. package/src/input/commands/personas-runtime.ts +219 -0
  19. package/src/input/commands/skills-runtime.ts +7 -2
  20. package/src/input/commands.ts +4 -0
  21. package/src/input/panel-integration-actions.ts +0 -52
  22. package/src/main.ts +2 -1
  23. package/src/panels/builtin/session.ts +4 -3
  24. package/src/panels/index.ts +0 -5
  25. package/src/panels/orchestration-panel.ts +4 -5
  26. package/src/panels/qr-panel.ts +3 -2
  27. package/src/panels/tasks-panel.ts +4 -4
  28. package/src/renderer/agent-workspace.ts +2 -0
  29. package/src/runtime/bootstrap-command-context.ts +3 -0
  30. package/src/runtime/bootstrap-command-parts.ts +6 -2
  31. package/src/runtime/bootstrap-core.ts +9 -5
  32. package/src/runtime/bootstrap-shell.ts +3 -1
  33. package/src/runtime/bootstrap.ts +10 -2
  34. package/src/runtime/cloudflare-control-plane.ts +2 -1
  35. package/src/runtime/services.ts +3 -3
  36. package/src/version.ts +1 -1
  37. package/src/daemon/cli.ts +0 -55
  38. package/src/daemon/safe-serve.ts +0 -61
  39. package/src/panels/diff-panel.ts +0 -520
  40. package/src/panels/file-explorer-panel.ts +0 -584
  41. package/src/panels/file-preview-panel.ts +0 -434
  42. package/src/panels/git-panel.ts +0 -638
  43. package/src/panels/sandbox-panel.ts +0 -283
  44. package/src/panels/symbol-outline-panel.ts +0 -486
  45. package/src/panels/worktree-panel.ts +0 -182
  46. package/src/panels/wrfc-panel.ts +0 -609
@@ -0,0 +1,216 @@
1
+ import { AgentSkillRegistry, type AgentSkillRecord } from '../../agent/skill-registry.ts';
2
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
3
+ import { requireShellPaths } from './runtime-services.ts';
4
+
5
+ interface ParsedSkillArgs {
6
+ readonly rest: readonly string[];
7
+ readonly flags: ReadonlyMap<string, string>;
8
+ readonly yes: boolean;
9
+ }
10
+
11
+ function parseSkillArgs(args: readonly string[]): ParsedSkillArgs {
12
+ const flags = new Map<string, string>();
13
+ const rest: string[] = [];
14
+ let yes = false;
15
+ for (let index = 0; index < args.length; index += 1) {
16
+ const token = args[index] ?? '';
17
+ if (token === '--yes') {
18
+ yes = true;
19
+ continue;
20
+ }
21
+ if (token.startsWith('--')) {
22
+ const key = token.slice(2);
23
+ const next = args[index + 1];
24
+ if (next !== undefined && !next.startsWith('--')) {
25
+ flags.set(key, next);
26
+ index += 1;
27
+ } else {
28
+ flags.set(key, 'true');
29
+ }
30
+ continue;
31
+ }
32
+ rest.push(token);
33
+ }
34
+ return { rest, flags, yes };
35
+ }
36
+
37
+ function splitList(value: string | undefined): readonly string[] {
38
+ if (!value) return [];
39
+ return value.split(',').map((entry) => entry.trim()).filter(Boolean);
40
+ }
41
+
42
+ function registryFromContext(ctx: CommandContext): AgentSkillRegistry {
43
+ return AgentSkillRegistry.fromShellPaths(requireShellPaths(ctx));
44
+ }
45
+
46
+ function requiredFlag(flags: ReadonlyMap<string, string>, key: string): string {
47
+ const value = flags.get(key)?.trim();
48
+ if (!value) throw new Error(`Missing --${key}.`);
49
+ return value;
50
+ }
51
+
52
+ function summarizeSkill(skill: AgentSkillRecord): string {
53
+ const enabled = skill.enabled ? 'enabled' : 'disabled';
54
+ const tags = skill.tags.length > 0 ? ` tags=${skill.tags.join(',')}` : '';
55
+ return ` ${skill.id} ${enabled} ${skill.reviewState} ${skill.name} - ${skill.description}${tags}`;
56
+ }
57
+
58
+ function renderList(title: string, registry: AgentSkillRegistry, skills: readonly AgentSkillRecord[]): string {
59
+ const snapshot = registry.snapshot();
60
+ if (skills.length === 0) {
61
+ return `${title}\n No local Agent skills yet. Create one with /agent-skills create --name <name> --description <summary> --procedure <steps>.`;
62
+ }
63
+ return [
64
+ `${title} (${skills.length})`,
65
+ ` store: ${snapshot.path}`,
66
+ ` enabled: ${snapshot.enabledSkills.length}`,
67
+ ...skills.map(summarizeSkill),
68
+ ].join('\n');
69
+ }
70
+
71
+ function renderSkill(skill: AgentSkillRecord): string {
72
+ return [
73
+ `Skill ${skill.name}`,
74
+ ` id: ${skill.id}`,
75
+ ` enabled: ${skill.enabled ? 'yes' : 'no'}`,
76
+ ` review: ${skill.reviewState}`,
77
+ ` source: ${skill.source}`,
78
+ ` provenance: ${skill.provenance}`,
79
+ ` tags: ${skill.tags.join(', ') || '(none)'}`,
80
+ ` triggers: ${skill.triggers.join(', ') || '(manual)'}`,
81
+ ` created: ${skill.createdAt}`,
82
+ ` updated: ${skill.updatedAt}`,
83
+ skill.staleReason ? ` stale reason: ${skill.staleReason}` : '',
84
+ '',
85
+ skill.description,
86
+ '',
87
+ skill.procedure,
88
+ ].filter(Boolean).join('\n');
89
+ }
90
+
91
+ function printError(ctx: CommandContext, error: unknown): void {
92
+ ctx.print(`Error: ${error instanceof Error ? error.message : String(error)}`);
93
+ }
94
+
95
+ export async function runAgentSkillsRuntimeCommand(args: readonly string[], ctx: CommandContext): Promise<void> {
96
+ const sub = (args[0] ?? 'list').toLowerCase();
97
+ const skillRegistry = registryFromContext(ctx);
98
+ try {
99
+ if (sub === 'list' || sub === 'open') {
100
+ ctx.print(renderList('Agent Skills', skillRegistry, skillRegistry.list()));
101
+ return;
102
+ }
103
+ if (sub === 'enabled') {
104
+ const snapshot = skillRegistry.snapshot();
105
+ ctx.print(renderList('Enabled Agent Skills', skillRegistry, snapshot.enabledSkills));
106
+ return;
107
+ }
108
+ if (sub === 'search') {
109
+ const query = args.slice(1).join(' ').trim();
110
+ ctx.print(renderList(query ? `Agent Skills matching "${query}"` : 'Agent Skills', skillRegistry, skillRegistry.search(query)));
111
+ return;
112
+ }
113
+ if (sub === 'show') {
114
+ const id = args[1];
115
+ if (!id) {
116
+ ctx.print('Usage: /agent-skills show <id>');
117
+ return;
118
+ }
119
+ const skill = skillRegistry.get(id);
120
+ ctx.print(skill ? renderSkill(skill) : `Unknown Agent skill: ${id}`);
121
+ return;
122
+ }
123
+ if (sub === 'create') {
124
+ const parsed = parseSkillArgs(args.slice(1));
125
+ const procedure = parsed.flags.get('procedure')?.trim() || parsed.rest.join(' ').trim();
126
+ const skill = skillRegistry.create({
127
+ name: requiredFlag(parsed.flags, 'name'),
128
+ description: requiredFlag(parsed.flags, 'description'),
129
+ procedure,
130
+ triggers: splitList(parsed.flags.get('triggers')),
131
+ tags: splitList(parsed.flags.get('tags')),
132
+ enabled: parsed.flags.get('enabled') === 'true',
133
+ source: 'user',
134
+ provenance: 'slash-command',
135
+ });
136
+ ctx.print(`Created Agent skill ${skill.id}: ${skill.name}`);
137
+ return;
138
+ }
139
+ if (sub === 'update') {
140
+ const id = args[1];
141
+ if (!id) {
142
+ ctx.print('Usage: /agent-skills update <id> [--name ...] [--description ...] [--procedure ...]');
143
+ return;
144
+ }
145
+ const parsed = parseSkillArgs(args.slice(2));
146
+ const updated = skillRegistry.update(id, {
147
+ name: parsed.flags.get('name'),
148
+ description: parsed.flags.get('description'),
149
+ procedure: parsed.flags.get('procedure'),
150
+ triggers: parsed.flags.has('triggers') ? splitList(parsed.flags.get('triggers')) : undefined,
151
+ tags: parsed.flags.has('tags') ? splitList(parsed.flags.get('tags')) : undefined,
152
+ provenance: 'slash-command',
153
+ });
154
+ ctx.print(`Updated Agent skill ${updated.id}: ${updated.name}`);
155
+ return;
156
+ }
157
+ if (sub === 'enable' || sub === 'disable') {
158
+ const id = args[1];
159
+ if (!id) {
160
+ ctx.print(`Usage: /agent-skills ${sub} <id>`);
161
+ return;
162
+ }
163
+ const skill = skillRegistry.setEnabled(id, sub === 'enable');
164
+ ctx.print(`${sub === 'enable' ? 'Enabled' : 'Disabled'} Agent skill ${skill.id}: ${skill.name}`);
165
+ return;
166
+ }
167
+ if (sub === 'review') {
168
+ const id = args[1];
169
+ if (!id) {
170
+ ctx.print('Usage: /agent-skills review <id>');
171
+ return;
172
+ }
173
+ const skill = skillRegistry.markReviewed(id);
174
+ ctx.print(`Reviewed Agent skill ${skill.id}.`);
175
+ return;
176
+ }
177
+ if (sub === 'stale') {
178
+ const id = args[1];
179
+ if (!id) {
180
+ ctx.print('Usage: /agent-skills stale <id> <reason...>');
181
+ return;
182
+ }
183
+ const skill = skillRegistry.markStale(id, args.slice(2).join(' '));
184
+ ctx.print(`Marked Agent skill ${skill.id} stale.`);
185
+ return;
186
+ }
187
+ if (sub === 'delete' || sub === 'remove') {
188
+ const parsed = parseSkillArgs(args.slice(1));
189
+ const id = parsed.rest[0];
190
+ if (!id) {
191
+ ctx.print('Usage: /agent-skills delete <id> --yes');
192
+ return;
193
+ }
194
+ if (!parsed.yes) {
195
+ ctx.print(`Refusing to delete Agent skill ${id} without --yes.`);
196
+ return;
197
+ }
198
+ const removed = skillRegistry.deleteSkill(id);
199
+ ctx.print(`Deleted Agent skill ${removed.id}: ${removed.name}`);
200
+ return;
201
+ }
202
+ ctx.print('Usage: /agent-skills [list|enabled|search|show|create|update|enable|disable|review|stale|delete]');
203
+ } catch (error) {
204
+ printError(ctx, error);
205
+ }
206
+ }
207
+
208
+ export function registerAgentSkillsRuntimeCommands(registry: CommandRegistry): void {
209
+ registry.register({
210
+ name: 'agent-skills',
211
+ aliases: ['askills', 'local-skills'],
212
+ 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]',
214
+ handler: runAgentSkillsRuntimeCommand,
215
+ });
216
+ }
@@ -8,10 +8,10 @@ type KnowledgeAskInput = Parameters<KnowledgeService['ask']>[0];
8
8
  type KnowledgeAskResult = Awaited<ReturnType<KnowledgeService['ask']>>;
9
9
  type KnowledgeAskMode = NonNullable<KnowledgeAskInput['mode']>;
10
10
 
11
- function requireKnowledgeApi(context: CommandContext) {
12
- const knowledgeApi = context.clients?.knowledgeApi;
11
+ function requireAgentKnowledgeApi(context: CommandContext) {
12
+ const knowledgeApi = context.clients?.agentKnowledgeApi;
13
13
  if (!knowledgeApi) {
14
- context.print('[knowledge] Knowledge API is not available in this runtime.');
14
+ context.print('[knowledge] Agent Knowledge API is not available in this runtime. Refusing to use default Knowledge/Wiki or HomeGraph fallback.');
15
15
  return null;
16
16
  }
17
17
  return knowledgeApi;
@@ -57,14 +57,11 @@ function positionalArgs(args: string[], valuedFlags: readonly string[] = []): st
57
57
  });
58
58
  }
59
59
 
60
- function requireKnowledgeAsk(context: CommandContext): ((input: KnowledgeAskInput) => Promise<KnowledgeAskResult>) | null {
61
- const serviceAsk = context.extensions.knowledgeService?.ask?.bind(context.extensions.knowledgeService);
60
+ function requireAgentKnowledgeAsk(context: CommandContext): ((input: KnowledgeAskInput) => Promise<KnowledgeAskResult>) | null {
61
+ const serviceAsk = context.extensions.agentKnowledgeService?.ask?.bind(context.extensions.agentKnowledgeService);
62
62
  if (serviceAsk) return serviceAsk;
63
63
 
64
- const clientAsk = (context.clients?.knowledgeApi as unknown as { ask?: (input: KnowledgeAskInput) => Promise<KnowledgeAskResult> } | undefined)?.ask;
65
- if (clientAsk) return clientAsk;
66
-
67
- context.print('[knowledge] Knowledge ask is not available in this runtime.');
64
+ context.print('[knowledge] Agent Knowledge ask is not available in this runtime. Refusing to use default Knowledge/Wiki or HomeGraph fallback.');
68
65
  return null;
69
66
  }
70
67
 
@@ -132,11 +129,11 @@ function renderKnowledgeAskResult(result: KnowledgeAskResult): string {
132
129
  export const knowledgeCommand: SlashCommand = {
133
130
  name: 'knowledge',
134
131
  aliases: ['know', 'kb'],
135
- description: 'Structured knowledge graph: ingest URLs/bookmarks, inspect issues, and build compact prompt packets.',
132
+ description: 'Agent Knowledge/Wiki: isolated Agent-owned sources, graph, review queue, and compact prompt packets.',
136
133
  usage: '<subcommand> [args]',
137
134
  argsHint: 'status|ask|ingest-url|import-bookmarks|import-urls|list|search|get|queue|review-issue|candidates|reports|schedules|lint|packet|explain|reindex|consolidate',
138
135
  handler: async (args: string[], context: CommandContext): Promise<void> => {
139
- const knowledge = requireKnowledgeApi(context);
136
+ const knowledge = requireAgentKnowledgeApi(context);
140
137
  if (!knowledge) {
141
138
  return;
142
139
  }
@@ -149,12 +146,16 @@ export const knowledgeCommand: SlashCommand = {
149
146
 
150
147
  switch (sub) {
151
148
  case 'ask': {
152
- const ask = requireKnowledgeAsk(context);
149
+ const ask = requireAgentKnowledgeAsk(context);
153
150
  if (!ask) return;
154
- const valuedFlags = ['--space', '--knowledge-space', '--limit', '--mode'];
151
+ const valuedFlags = ['--limit', '--mode'];
155
152
  const query = positionalArgs(rest, valuedFlags).join(' ').trim();
156
153
  if (!query) {
157
- context.print('[knowledge] Usage: /knowledge ask <query> [--space <knowledgeSpaceId>] [--limit <n>] [--mode <concise|standard|detailed>]');
154
+ context.print('[knowledge] Usage: /knowledge ask <query> [--limit <n>] [--mode <concise|standard|detailed>]');
155
+ return;
156
+ }
157
+ if (readFlag(rest, '--space') || readFlag(rest, '--knowledge-space')) {
158
+ context.print('[knowledge] Agent Knowledge is isolated. --space/--knowledge-space is not accepted because Agent must not fall back to default Knowledge/Wiki or HomeGraph.');
158
159
  return;
159
160
  }
160
161
  const requestedMode = readFlag(rest, '--mode') as KnowledgeAskMode | undefined;
@@ -163,7 +164,6 @@ export const knowledgeCommand: SlashCommand = {
163
164
  : 'standard';
164
165
  const result = await ask({
165
166
  query,
166
- knowledgeSpaceId: readFlag(rest, '--space') ?? readFlag(rest, '--knowledge-space'),
167
167
  limit: readPositiveIntFlag(rest, '--limit', 10),
168
168
  mode,
169
169
  includeSources: true,
@@ -177,7 +177,7 @@ export const knowledgeCommand: SlashCommand = {
177
177
  case 'status': {
178
178
  const status = await knowledge.status.get();
179
179
  context.print([
180
- '[knowledge] Structured knowledge status',
180
+ '[knowledge] Agent Knowledge status',
181
181
  ` ready: ${status.ready ? 'yes' : 'no'}`,
182
182
  ` storage: ${status.storagePath}`,
183
183
  ` sources: ${status.sourceCount}`,
@@ -385,7 +385,7 @@ export const knowledgeCommand: SlashCommand = {
385
385
  const result = await knowledge.graph.issues.review({
386
386
  issueId,
387
387
  action: action as KnowledgeReviewAction,
388
- reviewer: readFlag(rest, '--reviewer') ?? 'tui',
388
+ reviewer: readFlag(rest, '--reviewer') ?? 'agent',
389
389
  ...(value ? { value } : {}),
390
390
  });
391
391
  context.print([
@@ -508,7 +508,7 @@ export const knowledgeCommand: SlashCommand = {
508
508
  context.print([
509
509
  'Usage: /knowledge <subcommand>',
510
510
  ' status',
511
- ' ask <query> [--space <knowledgeSpaceId>] [--limit <n>] [--mode <concise|standard|detailed>]',
511
+ ' ask <query> [--limit <n>] [--mode <concise|standard|detailed>]',
512
512
  ' ingest-url <url> [--title <title>] [--tags <a,b>] [--folder <path>]',
513
513
  ' import-bookmarks <path>',
514
514
  ' import-urls <path>',
@@ -0,0 +1,219 @@
1
+ import { AgentPersonaRegistry, type AgentPersonaRecord } from '../../agent/persona-registry.ts';
2
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
3
+ import { requireShellPaths } from './runtime-services.ts';
4
+
5
+ interface ParsedPersonaArgs {
6
+ readonly rest: readonly string[];
7
+ readonly flags: ReadonlyMap<string, string>;
8
+ readonly yes: boolean;
9
+ }
10
+
11
+ function parsePersonaArgs(args: readonly string[]): ParsedPersonaArgs {
12
+ const flags = new Map<string, string>();
13
+ const rest: string[] = [];
14
+ let yes = false;
15
+ for (let index = 0; index < args.length; index += 1) {
16
+ const token = args[index] ?? '';
17
+ if (token === '--yes') {
18
+ yes = true;
19
+ continue;
20
+ }
21
+ if (token.startsWith('--')) {
22
+ const key = token.slice(2);
23
+ const next = args[index + 1];
24
+ if (next !== undefined && !next.startsWith('--')) {
25
+ flags.set(key, next);
26
+ index += 1;
27
+ } else {
28
+ flags.set(key, 'true');
29
+ }
30
+ continue;
31
+ }
32
+ rest.push(token);
33
+ }
34
+ return { rest, flags, yes };
35
+ }
36
+
37
+ function splitList(value: string | undefined): readonly string[] {
38
+ if (!value) return [];
39
+ return value.split(',').map((entry) => entry.trim()).filter(Boolean);
40
+ }
41
+
42
+ function registryFromContext(ctx: CommandContext): AgentPersonaRegistry {
43
+ return AgentPersonaRegistry.fromShellPaths(requireShellPaths(ctx));
44
+ }
45
+
46
+ function summarizePersona(persona: AgentPersonaRecord, activePersonaId: string | null): string {
47
+ const active = persona.id === activePersonaId ? 'active' : 'inactive';
48
+ const tags = persona.tags.length > 0 ? ` tags=${persona.tags.join(',')}` : '';
49
+ return ` ${persona.id} ${active} ${persona.reviewState} ${persona.name} - ${persona.description}${tags}`;
50
+ }
51
+
52
+ function renderList(title: string, registry: AgentPersonaRegistry, personas: readonly AgentPersonaRecord[]): string {
53
+ const snapshot = registry.snapshot();
54
+ if (personas.length === 0) {
55
+ return `${title}\n No local Agent personas yet. Create one with /personas create --name <name> --description <summary> --body <instructions>.`;
56
+ }
57
+ return [
58
+ `${title} (${personas.length})`,
59
+ ` store: ${snapshot.path}`,
60
+ ` active: ${snapshot.activePersona?.name ?? '(none)'}`,
61
+ ...personas.map((persona) => summarizePersona(persona, snapshot.activePersonaId)),
62
+ ].join('\n');
63
+ }
64
+
65
+ function renderPersona(persona: AgentPersonaRecord, activePersonaId: string | null): string {
66
+ return [
67
+ `Persona ${persona.name}`,
68
+ ` id: ${persona.id}`,
69
+ ` active: ${persona.id === activePersonaId ? 'yes' : 'no'}`,
70
+ ` review: ${persona.reviewState}`,
71
+ ` source: ${persona.source}`,
72
+ ` provenance: ${persona.provenance}`,
73
+ ` tags: ${persona.tags.join(', ') || '(none)'}`,
74
+ ` triggers: ${persona.triggers.join(', ') || '(none)'}`,
75
+ ` created: ${persona.createdAt}`,
76
+ ` updated: ${persona.updatedAt}`,
77
+ persona.staleReason ? ` stale reason: ${persona.staleReason}` : '',
78
+ '',
79
+ persona.description,
80
+ '',
81
+ persona.body,
82
+ ].filter(Boolean).join('\n');
83
+ }
84
+
85
+ function requiredFlag(flags: ReadonlyMap<string, string>, key: string): string {
86
+ const value = flags.get(key)?.trim();
87
+ if (!value) throw new Error(`Missing --${key}.`);
88
+ return value;
89
+ }
90
+
91
+ function printError(ctx: CommandContext, error: unknown): void {
92
+ ctx.print(`Error: ${error instanceof Error ? error.message : String(error)}`);
93
+ }
94
+
95
+ export function registerPersonasRuntimeCommands(registry: CommandRegistry): void {
96
+ registry.register({
97
+ name: 'personas',
98
+ aliases: ['persona'],
99
+ description: 'Manage local GoodVibes Agent personas',
100
+ usage: '[list|search <query>|show <id>|create --name <name> --description <summary> --body <instructions>|update <id> [--name ...] [--description ...] [--body ...]|use <id>|active|clear|review <id>|stale <id> <reason...>|delete <id> --yes]',
101
+ async handler(args, ctx) {
102
+ const sub = (args[0] ?? 'list').toLowerCase();
103
+ const registryStore = registryFromContext(ctx);
104
+ try {
105
+ if (sub === 'list' || sub === 'open') {
106
+ ctx.print(renderList('Agent Personas', registryStore, registryStore.list()));
107
+ return;
108
+ }
109
+ if (sub === 'search') {
110
+ const query = args.slice(1).join(' ').trim();
111
+ ctx.print(renderList(query ? `Agent Personas matching "${query}"` : 'Agent Personas', registryStore, registryStore.search(query)));
112
+ return;
113
+ }
114
+ if (sub === 'show') {
115
+ const id = args[1];
116
+ if (!id) {
117
+ ctx.print('Usage: /personas show <id>');
118
+ return;
119
+ }
120
+ const snapshot = registryStore.snapshot();
121
+ const persona = registryStore.get(id);
122
+ ctx.print(persona ? renderPersona(persona, snapshot.activePersonaId) : `Unknown persona: ${id}`);
123
+ return;
124
+ }
125
+ if (sub === 'create') {
126
+ const parsed = parsePersonaArgs(args.slice(1));
127
+ const body = parsed.flags.get('body')?.trim() || parsed.rest.join(' ').trim();
128
+ const persona = registryStore.create({
129
+ name: requiredFlag(parsed.flags, 'name'),
130
+ description: requiredFlag(parsed.flags, 'description'),
131
+ body,
132
+ tags: splitList(parsed.flags.get('tags')),
133
+ triggers: splitList(parsed.flags.get('triggers')),
134
+ source: 'user',
135
+ provenance: 'slash-command',
136
+ });
137
+ ctx.print(`Created Agent persona ${persona.id}: ${persona.name}`);
138
+ return;
139
+ }
140
+ if (sub === 'update') {
141
+ const id = args[1];
142
+ if (!id) {
143
+ ctx.print('Usage: /personas update <id> [--name ...] [--description ...] [--body ...]');
144
+ return;
145
+ }
146
+ const parsed = parsePersonaArgs(args.slice(2));
147
+ const updated = registryStore.update(id, {
148
+ name: parsed.flags.get('name'),
149
+ description: parsed.flags.get('description'),
150
+ body: parsed.flags.get('body'),
151
+ tags: parsed.flags.has('tags') ? splitList(parsed.flags.get('tags')) : undefined,
152
+ triggers: parsed.flags.has('triggers') ? splitList(parsed.flags.get('triggers')) : undefined,
153
+ provenance: 'slash-command',
154
+ });
155
+ ctx.print(`Updated Agent persona ${updated.id}: ${updated.name}`);
156
+ return;
157
+ }
158
+ if (sub === 'use') {
159
+ const id = args[1];
160
+ if (!id) {
161
+ ctx.print('Usage: /personas use <id>');
162
+ return;
163
+ }
164
+ const persona = registryStore.setActive(id);
165
+ ctx.print(`Active Agent persona: ${persona.name} (${persona.id})`);
166
+ return;
167
+ }
168
+ if (sub === 'active') {
169
+ const snapshot = registryStore.snapshot();
170
+ ctx.print(snapshot.activePersona ? renderPersona(snapshot.activePersona, snapshot.activePersonaId) : 'No active Agent persona.');
171
+ return;
172
+ }
173
+ if (sub === 'clear') {
174
+ registryStore.clearActive();
175
+ ctx.print('Cleared active Agent persona.');
176
+ return;
177
+ }
178
+ if (sub === 'review') {
179
+ const id = args[1];
180
+ if (!id) {
181
+ ctx.print('Usage: /personas review <id>');
182
+ return;
183
+ }
184
+ const persona = registryStore.markReviewed(id);
185
+ ctx.print(`Reviewed Agent persona ${persona.id}.`);
186
+ return;
187
+ }
188
+ if (sub === 'stale') {
189
+ const id = args[1];
190
+ if (!id) {
191
+ ctx.print('Usage: /personas stale <id> <reason...>');
192
+ return;
193
+ }
194
+ const persona = registryStore.markStale(id, args.slice(2).join(' '));
195
+ ctx.print(`Marked Agent persona ${persona.id} stale.`);
196
+ return;
197
+ }
198
+ if (sub === 'delete' || sub === 'remove') {
199
+ const parsed = parsePersonaArgs(args.slice(1));
200
+ const id = parsed.rest[0];
201
+ if (!id) {
202
+ ctx.print('Usage: /personas delete <id> --yes');
203
+ return;
204
+ }
205
+ if (!parsed.yes) {
206
+ ctx.print(`Refusing to delete Agent persona ${id} without --yes.`);
207
+ return;
208
+ }
209
+ const removed = registryStore.deletePersona(id);
210
+ ctx.print(`Deleted Agent persona ${removed.id}: ${removed.name}`);
211
+ return;
212
+ }
213
+ ctx.print('Usage: /personas [list|search <query>|show <id>|create|update|use|active|clear|review|stale|delete]');
214
+ } catch (error) {
215
+ printError(ctx, error);
216
+ }
217
+ },
218
+ });
219
+ }
@@ -12,15 +12,20 @@ import {
12
12
  upsertEcosystemCatalogEntry,
13
13
  } from '@/runtime/index.ts';
14
14
  import { requireEcosystemCatalogPaths, requirePanelManager, requireShellPaths } from './runtime-services.ts';
15
+ import { runAgentSkillsRuntimeCommand } from './agent-skills-runtime.ts';
15
16
 
16
17
  export function registerSkillsRuntimeCommands(registry: CommandRegistry): void {
17
18
  registry.register({
18
19
  name: 'skills',
19
20
  aliases: ['skill'],
20
21
  description: 'Inspect installed skill packs',
21
- usage: '[open|list|show <name>|origins|browse [query]|installed|catalog-review <id>|publish-local <id> <path> <summary...>|unpublish <id>|install-hint <catalog-id>|install <id> [project|user]|update <id> [project|user]|uninstall <id> [project|user]]',
22
+ usage: '[open|local ...|list|show <name>|origins|browse [query]|installed|catalog-review <id>|publish-local <id> <path> <summary...>|unpublish <id>|install-hint <catalog-id>|install <id> [project|user]|update <id> [project|user]|uninstall <id> [project|user]]',
22
23
  async handler(args, ctx) {
23
24
  const sub = args[0] ?? 'open';
25
+ if (sub === 'local' || sub === 'agent') {
26
+ await runAgentSkillsRuntimeCommand(args.slice(1), ctx);
27
+ return;
28
+ }
24
29
  if (sub === 'open' || sub === 'panel') {
25
30
  if (ctx.showPanel) ctx.showPanel('skills');
26
31
  else {
@@ -215,7 +220,7 @@ export function registerSkillsRuntimeCommands(registry: CommandRegistry): void {
215
220
  ctx.print(result.ok ? `Uninstalled curated skill ${entryId} from ${result.removedPath}` : `Error: ${result.error}`);
216
221
  return;
217
222
  }
218
- ctx.print('Usage: /skills [open|list|show <name>|origins|browse [query]|installed|catalog-review <id>|publish-local <id> <path> <summary...>|unpublish <id>|install-hint <catalog-id>|install <id> [project|user]|update <id> [project|user]|uninstall <id> [project|user]]');
223
+ ctx.print('Usage: /skills [open|local ...|list|show <name>|origins|browse [query]|installed|catalog-review <id>|publish-local <id> <path> <summary...>|unpublish <id>|install-hint <catalog-id>|install <id> [project|user]|update <id> [project|user]|uninstall <id> [project|user]]');
219
224
  },
220
225
  });
221
226
  }
@@ -55,6 +55,8 @@ import { registerWorkPlanRuntimeCommands } from './commands/work-plan-runtime.ts
55
55
  import { registerAgentWorkspaceRuntimeCommands } from './commands/agent-workspace-runtime.ts';
56
56
  import { registerAgentExternalizedTuiCommands } from './commands/agent-externalized-tui.ts';
57
57
  import { registerDelegationRuntimeCommands } from './commands/delegation-runtime.ts';
58
+ import { registerPersonasRuntimeCommands } from './commands/personas-runtime.ts';
59
+ import { registerAgentSkillsRuntimeCommands } from './commands/agent-skills-runtime.ts';
58
60
 
59
61
  /**
60
62
  * registerBuiltinCommands - Register all built-in slash commands into the registry.
@@ -63,6 +65,8 @@ import { registerDelegationRuntimeCommands } from './commands/delegation-runtime
63
65
  export function registerBuiltinCommands(registry: CommandRegistry): void {
64
66
  registerShellCoreCommands(registry);
65
67
  registerAgentWorkspaceRuntimeCommands(registry);
68
+ registerPersonasRuntimeCommands(registry);
69
+ registerAgentSkillsRuntimeCommands(registry);
66
70
  registerDelegationRuntimeCommands(registry);
67
71
  registerConfigCommand(registry);
68
72
  registerOperatorRuntimeCommands(registry);
@@ -2,37 +2,8 @@ import type { CommandContext } from './command-registry.ts';
2
2
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
3
3
  import type { Panel } from '../panels/types.ts';
4
4
  import type { PanelManager } from '../panels/panel-manager.ts';
5
- import { FileExplorerPanel } from '../panels/file-explorer-panel.ts';
6
- import { FilePreviewPanel } from '../panels/file-preview-panel.ts';
7
- import { SymbolOutlinePanel } from '../panels/symbol-outline-panel.ts';
8
5
  import { ApprovalPanel } from '../panels/approval-panel.ts';
9
6
 
10
- function ensurePreviewPanel(panelManager: PanelManager): FilePreviewPanel | null {
11
- const existing = panelManager.getPanel('preview');
12
- if (existing instanceof FilePreviewPanel) {
13
- const pane = panelManager.getPaneOf('preview');
14
- panelManager.activateById('preview');
15
- if (pane) panelManager.focusPane(pane);
16
- return existing;
17
- }
18
- const targetPane: 'top' | 'bottom' = panelManager.isBottomPaneVisible()
19
- ? (panelManager.getFocusedPane() === 'top' ? 'bottom' : 'top')
20
- : 'bottom';
21
- const opened = panelManager.open('preview', targetPane);
22
- panelManager.show();
23
- panelManager.focusPane(targetPane);
24
- return opened instanceof FilePreviewPanel ? opened : null;
25
- }
26
-
27
- function syncSymbolOutlineFromPreview(panelManager: PanelManager, previewPanel: FilePreviewPanel): void {
28
- const symbols = panelManager.getPanel('symbols');
29
- const filePath = previewPanel.getCurrentFilePath();
30
- const source = previewPanel.getSource();
31
- if (symbols instanceof SymbolOutlinePanel && filePath && source !== null) {
32
- symbols.loadFile(filePath, source);
33
- }
34
- }
35
-
36
7
  export function handlePanelIntegrationAction(
37
8
  panelManager: PanelManager,
38
9
  activePanel: Panel | null,
@@ -41,29 +12,6 @@ export function handlePanelIntegrationAction(
41
12
  ): boolean {
42
13
  if (!activePanel) return false;
43
14
 
44
- if ((key === 'enter' || key === 'return' || key === 'right') && activePanel instanceof FileExplorerPanel) {
45
- const filePath = activePanel.getFocusedFilePath();
46
- if (!filePath) return false;
47
- const previewPanel = ensurePreviewPanel(panelManager);
48
- if (!previewPanel) return false;
49
- previewPanel.openFile(filePath);
50
- syncSymbolOutlineFromPreview(panelManager, previewPanel);
51
- return true;
52
- }
53
-
54
- if ((key === 'enter' || key === 'return') && activePanel instanceof SymbolOutlinePanel) {
55
- const location = activePanel.getSelectedLocation();
56
- if (!location) return false;
57
- const previewPanel = ensurePreviewPanel(panelManager);
58
- if (!previewPanel) return false;
59
- if (previewPanel.getCurrentFilePath() !== location.path) {
60
- previewPanel.openFile(location.path);
61
- syncSymbolOutlineFromPreview(panelManager, previewPanel);
62
- }
63
- previewPanel.goToLine(location.line);
64
- return true;
65
- }
66
-
67
15
  if ((key === 'enter' || key === 'return') && activePanel instanceof ApprovalPanel) {
68
16
  const command = activePanel.getSelectedCommand();
69
17
  if (!command || !commandContext?.executeCommand) return false;
package/src/main.ts CHANGED
@@ -54,6 +54,7 @@ import { allowTerminalWrite, installTuiTerminalOutputGuard } from './runtime/ter
54
54
  import { ProjectPlanningCoordinator } from './planning/project-planning-coordinator.ts';
55
55
  import { buildCommandArgsHint } from './input/command-args-hint.ts';
56
56
  import { summarizeRunningAgents } from './renderer/process-summary.ts';
57
+ import { GOODVIBES_AGENT_PAIRING_SURFACE } from './config/surface.ts';
57
58
 
58
59
  const ALT_SCREEN_ENTER = '\x1b[?1049h';
59
60
  const ALT_SCREEN_EXIT = '\x1b[?1049l';
@@ -320,7 +321,7 @@ async function main() {
320
321
  inputOptions = {
321
322
  origin: {
322
323
  source: 'project-planning',
323
- surface: 'tui',
324
+ surface: GOODVIBES_AGENT_PAIRING_SURFACE,
324
325
  metadata: {
325
326
  projectId: ctx.services.projectPlanningProjectId,
326
327
  knowledgeSpaceId: planning.state.knowledgeSpaceId,