@pellux/goodvibes-agent 0.1.1 → 0.1.3

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 (52) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +12 -1
  3. package/docs/README.md +2 -0
  4. package/docs/getting-started.md +19 -1
  5. package/docs/release-and-publishing.md +3 -1
  6. package/package.json +10 -1
  7. package/src/agent/persona-registry.ts +379 -0
  8. package/src/agent/skill-registry.ts +360 -0
  9. package/src/audio/spoken-turn-model-routing.ts +2 -1
  10. package/src/cli/agent-knowledge-command.ts +525 -0
  11. package/src/cli/help.ts +35 -0
  12. package/src/cli/management-commands.ts +3 -1
  13. package/src/cli/management.ts +33 -9
  14. package/src/cli/parser.ts +7 -0
  15. package/src/cli/types.ts +3 -0
  16. package/src/config/surface.ts +1 -0
  17. package/src/input/agent-workspace.ts +33 -3
  18. package/src/input/command-registry.ts +4 -1
  19. package/src/input/commands/agent-skills-runtime.ts +216 -0
  20. package/src/input/commands/delegation-runtime.ts +129 -0
  21. package/src/input/commands/knowledge.ts +18 -18
  22. package/src/input/commands/personas-runtime.ts +219 -0
  23. package/src/input/commands/shell-core.ts +9 -6
  24. package/src/input/commands/skills-runtime.ts +7 -2
  25. package/src/input/commands.ts +6 -0
  26. package/src/input/panel-integration-actions.ts +0 -52
  27. package/src/input/submission-router.ts +1 -1
  28. package/src/main.ts +2 -1
  29. package/src/panels/builtin/agent.ts +0 -14
  30. package/src/panels/builtin/session.ts +4 -3
  31. package/src/panels/index.ts +0 -5
  32. package/src/panels/orchestration-panel.ts +4 -5
  33. package/src/panels/qr-panel.ts +3 -2
  34. package/src/panels/tasks-panel.ts +4 -4
  35. package/src/renderer/agent-workspace.ts +2 -0
  36. package/src/runtime/bootstrap-command-context.ts +3 -0
  37. package/src/runtime/bootstrap-command-parts.ts +6 -2
  38. package/src/runtime/bootstrap-core.ts +8 -4
  39. package/src/runtime/bootstrap-shell.ts +5 -2
  40. package/src/runtime/bootstrap.ts +10 -2
  41. package/src/runtime/cloudflare-control-plane.ts +2 -1
  42. package/src/version.ts +1 -1
  43. package/src/daemon/cli.ts +0 -55
  44. package/src/daemon/safe-serve.ts +0 -61
  45. package/src/panels/diff-panel.ts +0 -520
  46. package/src/panels/file-explorer-panel.ts +0 -584
  47. package/src/panels/file-preview-panel.ts +0 -434
  48. package/src/panels/git-panel.ts +0 -638
  49. package/src/panels/sandbox-panel.ts +0 -283
  50. package/src/panels/symbol-outline-panel.ts +0 -486
  51. package/src/panels/worktree-panel.ts +0 -182
  52. package/src/panels/wrfc-panel.ts +0 -609
@@ -21,6 +21,7 @@ import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibe
21
21
  import { inspectProviderAuth } from '@/runtime/index.ts';
22
22
  import { getOrCreateCompanionToken, buildCompanionConnectionInfo, encodeConnectionPayload, formatConnectionBlock } from '@pellux/goodvibes-sdk/platform/pairing';
23
23
  import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing';
24
+ import { UserAuthManager } from '@pellux/goodvibes-sdk/platform/security';
24
25
  import type { GoodVibesCliParseResult } from './types.ts';
25
26
  import { formatProviderAuthRoute, summarizeProviderAuthRoutes } from './provider-auth-routes.ts';
26
27
  import { classifyProviderSetup } from './provider-classification.ts';
@@ -31,6 +32,7 @@ import { handleServiceCommand } from './service-command.ts';
31
32
  import { handleBundleCommand } from './bundle-command.ts';
32
33
  import { buildListenerTestResult, formatListenerTestResult, handleSurfacesCommand } from './surface-command.ts';
33
34
  import { buildControlPlaneStatusResult, formatControlPlaneStatus, handleSecrets, handleSessions, handleTasks, renderPairing, renderRemote, renderSubscriptions, renderWeb } from './management-commands.ts';
35
+ import { handleAgentKnowledgeCommand, handleCompatCommand, handleDelegateCommand } from './agent-knowledge-command.ts';
34
36
  import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
35
37
 
36
38
  export interface CliCommandRuntime {
@@ -289,6 +291,14 @@ export function readAuthPaths(runtime: CliCommandRuntime) {
289
291
  };
290
292
  }
291
293
 
294
+ function createCliLocalUserAuthManager(runtime: CliCommandRuntime): UserAuthManager {
295
+ const paths = readAuthPaths(runtime);
296
+ return new UserAuthManager({
297
+ bootstrapFilePath: paths.userStorePath,
298
+ bootstrapCredentialPath: paths.bootstrapCredentialPath,
299
+ });
300
+ }
301
+
292
302
  export async function runNonInteractiveAgent(runtime: CliCommandRuntime): Promise<number> {
293
303
  const prompt = runtime.cli.flags.prompt ?? runtime.cli.positionals.join(' ').trim();
294
304
  if (!prompt) {
@@ -574,7 +584,7 @@ async function renderModels(runtime: CliCommandRuntime): Promise<string> {
574
584
  }
575
585
 
576
586
  async function renderAuth(runtime: CliCommandRuntime): Promise<string> {
577
- return await withRuntimeServices(runtime, (services) => {
587
+ const localUserAuthManager = createCliLocalUserAuthManager(runtime);
578
588
  const [sub = 'status', ...rawRest] = runtime.cli.commandArgs;
579
589
  const rest = commandValues(rawRest);
580
590
  if (sub === 'add-user' || sub === 'add') {
@@ -583,13 +593,13 @@ async function renderAuth(runtime: CliCommandRuntime): Promise<string> {
583
593
  const password = readPassword(rawRest);
584
594
  if (!password) return 'Usage: goodvibes auth add-user <username> [--password <value>|--password-stdin] [--role <role>]';
585
595
  const roles = readOptionValues(rawRest, '--role').filter((role) => role.length > 0);
586
- const user = services.localUserAuthManager.addUser(username, password, roles.length > 0 ? roles : ['user']);
596
+ const user = localUserAuthManager.addUser(username, password, roles.length > 0 ? roles : ['user']);
587
597
  return `Auth user added: ${user.username} (${user.roles.join(', ') || 'no roles'})`;
588
598
  }
589
599
  if (sub === 'delete-user' || sub === 'remove-user') {
590
600
  const username = rest[0];
591
601
  if (!username) return 'Usage: goodvibes auth delete-user <username>';
592
- return services.localUserAuthManager.deleteUser(username)
602
+ return localUserAuthManager.deleteUser(username)
593
603
  ? `Auth user deleted: ${username}`
594
604
  : `No auth user found: ${username}`;
595
605
  }
@@ -598,31 +608,31 @@ async function renderAuth(runtime: CliCommandRuntime): Promise<string> {
598
608
  if (!username) return 'Usage: goodvibes auth rotate-password <username> [--password <value>|--password-stdin]';
599
609
  const password = readPassword(rawRest);
600
610
  if (!password) return 'Usage: goodvibes auth rotate-password <username> [--password <value>|--password-stdin]';
601
- services.localUserAuthManager.rotatePassword(username, password);
611
+ localUserAuthManager.rotatePassword(username, password);
602
612
  return `Auth password rotated: ${username}`;
603
613
  }
604
614
  if (sub === 'revoke-session') {
605
615
  const token = rest[0];
606
616
  if (!token) return 'Usage: goodvibes auth revoke-session <token-or-fingerprint>';
607
- return services.localUserAuthManager.revokeSession(token)
617
+ return localUserAuthManager.revokeSession(token)
608
618
  ? 'Auth session revoked.'
609
619
  : 'No auth session found.';
610
620
  }
611
621
  if (sub === 'revoke-sessions') {
612
622
  const username = rest[0];
613
623
  if (!username) return 'Usage: goodvibes auth revoke-sessions <username>';
614
- const count = services.localUserAuthManager.revokeSessionsForUser(username);
624
+ const count = localUserAuthManager.revokeSessionsForUser(username);
615
625
  return `Auth sessions revoked for ${username}: ${count}`;
616
626
  }
617
627
  if (sub === 'clear-bootstrap') {
618
- return services.localUserAuthManager.clearBootstrapCredentialFile()
628
+ return localUserAuthManager.clearBootstrapCredentialFile()
619
629
  ? 'Bootstrap credential file removed.'
620
630
  : 'Bootstrap credential file was already absent.';
621
631
  }
622
632
  if (sub !== 'status' && sub !== 'list' && sub !== 'users' && sub !== 'sessions') {
623
633
  return 'Usage: goodvibes auth [status|users|sessions|add-user|delete-user|rotate-password|revoke-session|revoke-sessions|clear-bootstrap]';
624
634
  }
625
- const snapshot = services.localUserAuthManager.inspect();
635
+ const snapshot = localUserAuthManager.inspect();
626
636
  const paths = readAuthPaths(runtime);
627
637
  const value = {
628
638
  ...paths,
@@ -652,7 +662,6 @@ async function renderAuth(runtime: CliCommandRuntime): Promise<string> {
652
662
  ` bootstrap credential: ${paths.bootstrapCredentialPresent ? 'present' : 'missing'} (${paths.bootstrapCredentialPath})`,
653
663
  ` operator tokens: ${paths.operatorTokenPresent ? 'present' : 'missing'} (${paths.operatorTokenPath})`,
654
664
  ].join('\n'));
655
- });
656
665
  }
657
666
 
658
667
 
@@ -684,6 +693,21 @@ export async function handleGoodVibesCliCommand(runtime: CliCommandRuntime): Pro
684
693
  console.log(output);
685
694
  return { handled: true, exitCode: exitCodeForText(output) };
686
695
  }
696
+ case 'compat': {
697
+ const result = await handleCompatCommand(runtime);
698
+ console.log(result.output);
699
+ return { handled: true, exitCode: result.exitCode };
700
+ }
701
+ case 'knowledge': {
702
+ const result = await handleAgentKnowledgeCommand(runtime);
703
+ console.log(result.output);
704
+ return { handled: true, exitCode: result.exitCode };
705
+ }
706
+ case 'delegate': {
707
+ const result = await handleDelegateCommand(runtime);
708
+ console.log(result.output);
709
+ return { handled: true, exitCode: result.exitCode };
710
+ }
687
711
  case 'subscription': {
688
712
  const output = await renderSubscriptions(runtime);
689
713
  console.log(output);
package/src/cli/parser.ts CHANGED
@@ -26,6 +26,13 @@ const COMMAND_ALIASES: Readonly<Record<string, GoodVibesCliCommand>> = {
26
26
  providers: 'providers',
27
27
  provider: 'providers',
28
28
  auth: 'auth',
29
+ compat: 'compat',
30
+ compatibility: 'compat',
31
+ knowledge: 'knowledge',
32
+ know: 'knowledge',
33
+ kb: 'knowledge',
34
+ delegate: 'delegate',
35
+ build: 'delegate',
29
36
  subscription: 'subscription',
30
37
  subscriptions: 'subscription',
31
38
  secrets: 'secrets',
package/src/cli/types.ts CHANGED
@@ -10,6 +10,9 @@ export type GoodVibesCliCommand =
10
10
  | 'models'
11
11
  | 'providers'
12
12
  | 'auth'
13
+ | 'compat'
14
+ | 'knowledge'
15
+ | 'delegate'
13
16
  | 'subscription'
14
17
  | 'secrets'
15
18
  | 'sessions'
@@ -1 +1,2 @@
1
1
  export const GOODVIBES_AGENT_SURFACE_ROOT = 'agent';
2
+ export const GOODVIBES_AGENT_PAIRING_SURFACE = 'goodvibes-agent';
@@ -1,5 +1,7 @@
1
1
  import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
2
2
  import type { CommandContext } from './command-registry.ts';
3
+ import { AgentPersonaRegistry } from '../agent/persona-registry.ts';
4
+ import { AgentSkillRegistry } from '../agent/skill-registry.ts';
3
5
 
4
6
  export const AGENT_WORKSPACE_MODAL_NAME = 'agentWorkspace';
5
7
 
@@ -51,6 +53,10 @@ export interface AgentWorkspaceRuntimeSnapshot {
51
53
  readonly daemonBaseUrl: string;
52
54
  readonly daemonOwnership: 'external';
53
55
  readonly sessionMemoryCount: number;
56
+ readonly localSkillCount: number;
57
+ readonly enabledSkillCount: number;
58
+ readonly localPersonaCount: number;
59
+ readonly activePersonaName: string;
54
60
  readonly knowledgeRoute: '/api/goodvibes-agent/knowledge';
55
61
  readonly knowledgeIsolation: 'agent-only';
56
62
  readonly executionPolicy: 'serial-proactive';
@@ -98,6 +104,26 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
98
104
  return 0;
99
105
  }
100
106
  })();
107
+ const personaSnapshot = (() => {
108
+ try {
109
+ const shellPaths = context.workspace?.shellPaths;
110
+ if (!shellPaths) return { count: 0, activeName: '(none)' };
111
+ const snapshot = AgentPersonaRegistry.fromShellPaths(shellPaths).snapshot();
112
+ return { count: snapshot.personas.length, activeName: snapshot.activePersona?.name ?? '(none)' };
113
+ } catch {
114
+ return { count: 0, activeName: '(unavailable)' };
115
+ }
116
+ })();
117
+ const skillSnapshot = (() => {
118
+ try {
119
+ const shellPaths = context.workspace?.shellPaths;
120
+ if (!shellPaths) return { count: 0, enabled: 0 };
121
+ const snapshot = AgentSkillRegistry.fromShellPaths(shellPaths).snapshot();
122
+ return { count: snapshot.skills.length, enabled: snapshot.enabledSkills.length };
123
+ } catch {
124
+ return { count: 0, enabled: 0 };
125
+ }
126
+ })();
101
127
  const warnings: string[] = [];
102
128
  if (provider === 'unknown' || model === 'unknown') warnings.push('Provider/model unavailable in this runtime context.');
103
129
  if (!context.executeCommand) warnings.push('Command dispatch is unavailable; workspace actions will show guidance only.');
@@ -112,6 +138,10 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
112
138
  daemonBaseUrl: `http://${host}:${port}`,
113
139
  daemonOwnership: 'external',
114
140
  sessionMemoryCount,
141
+ localSkillCount: skillSnapshot.count,
142
+ enabledSkillCount: skillSnapshot.enabled,
143
+ localPersonaCount: personaSnapshot.count,
144
+ activePersonaName: personaSnapshot.activeName,
115
145
  knowledgeRoute: '/api/goodvibes-agent/knowledge',
116
146
  knowledgeIsolation: 'agent-only',
117
147
  executionPolicy: 'serial-proactive',
@@ -167,8 +197,8 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
167
197
  detail: 'Memory, skills, and personas stay Agent-local until stable shared daemon registry contracts exist. Secrets must not be stored as memory.',
168
198
  actions: [
169
199
  { id: 'memory', label: 'Open memory', detail: 'Inspect local/session memory commands and surfaces.', command: '/memory', kind: 'command', safety: 'read-only' },
170
- { id: 'skills', label: 'Open skills', detail: 'Inspect discovered skills and skill catalog state.', command: '/skills open', kind: 'command', safety: 'read-only' },
171
- { id: 'personas', label: 'Persona library', detail: 'Use local Agent personas to shape serial assistant behavior without spawning background agents.', kind: 'guidance', safety: 'safe' },
200
+ { id: 'skills', label: 'Local skill library', detail: 'Create, review, and enable local Agent reusable procedures.', command: '/agent-skills', kind: 'command', safety: 'safe' },
201
+ { id: 'personas', label: 'Persona library', detail: 'Use local Agent personas to shape serial assistant behavior without spawning background agents.', command: '/personas', kind: 'command', safety: 'safe' },
172
202
  ],
173
203
  },
174
204
  {
@@ -203,7 +233,7 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
203
233
  detail: 'Agent does not become the coding TUI. Build, implement, fix, patch, and review work must be handed to GoodVibes TUI with the full original ask and WRFC only when explicitly requested.',
204
234
  actions: [
205
235
  { id: 'delegate-guidance', label: 'Delegation rule', detail: 'For build/fix/review work, delegate one request to GoodVibes TUI instead of spawning local Engineer/Reviewer/Tester roots.', kind: 'guidance', safety: 'delegates' },
206
- { id: 'review-command', label: 'Review delegation command', detail: 'Use /review or /wrfc only when the user explicitly asks for code review/build execution.', command: '/review', kind: 'command', safety: 'delegates' },
236
+ { id: 'review-command', label: 'Review delegation command', detail: 'Use /delegate --wrfc only when the user explicitly asks for code review/build execution.', command: '/delegate --wrfc <task>', kind: 'command', safety: 'delegates' },
207
237
  { id: 'remote-policy', label: 'Remote runner policy', detail: 'Remote dispatch/rerun is blocked in Agent; TUI owns runner topology for delegated build work.', command: '/remote dispatch', kind: 'command', safety: 'blocked' },
208
238
  ],
209
239
  },
@@ -180,7 +180,9 @@ export interface CommandExtensionRegistryServices {
180
180
 
181
181
  export interface CommandExtensionServices
182
182
  extends CommandExtensionRegistryServices,
183
- CommandExtensionShellServices {}
183
+ CommandExtensionShellServices {
184
+ readonly agentKnowledgeService?: import('@pellux/goodvibes-sdk/platform/knowledge').KnowledgeService;
185
+ }
184
186
 
185
187
  /**
186
188
  * CommandContext - Passed to every slash command handler so commands can
@@ -200,6 +202,7 @@ export interface CommandContext
200
202
  readonly operator?: OperatorClient;
201
203
  readonly peer?: PeerClient;
202
204
  readonly providerApi?: ProviderApi;
205
+ readonly agentKnowledgeApi?: KnowledgeApi;
203
206
  readonly knowledgeApi?: KnowledgeApi;
204
207
  readonly hookApi?: HookApi;
205
208
  readonly mcpApi?: McpApi;
@@ -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
+ }
@@ -0,0 +1,129 @@
1
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
2
+ import type { SharedSessionParticipant } from '@pellux/goodvibes-sdk/platform/control-plane';
3
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
4
+
5
+ function hasFlag(args: readonly string[], flag: string): boolean {
6
+ return args.includes(flag);
7
+ }
8
+
9
+ function delegationTaskValues(args: readonly string[]): string[] {
10
+ const values: string[] = [];
11
+ for (let index = 0; index < args.length; index += 1) {
12
+ const token = args[index]!;
13
+ if (token === '--wrfc') continue;
14
+ if (!token.startsWith('--')) {
15
+ values.push(token);
16
+ continue;
17
+ }
18
+ }
19
+ return values;
20
+ }
21
+
22
+ function buildDelegationBody(task: string, wrfcRequested: boolean): string {
23
+ return [
24
+ 'GoodVibes Agent explicit build delegation.',
25
+ '',
26
+ 'Original user ask:',
27
+ task,
28
+ '',
29
+ 'Agent policy:',
30
+ '- GoodVibes Agent is not the coding TUI.',
31
+ '- Preserve the full original ask.',
32
+ '- GoodVibes TUI owns file edits, git/worktree flows, sandbox/QEMU UX, and any WRFC owner chain.',
33
+ wrfcRequested
34
+ ? '- WRFC was explicitly requested by the Agent user for this build/fix/review delegation.'
35
+ : '- WRFC was not explicitly requested; do not turn this into WRFC solely because it came from Agent.',
36
+ ].join('\n');
37
+ }
38
+
39
+ export function registerDelegationRuntimeCommands(registry: CommandRegistry): void {
40
+ const makeHandler = (defaultWrfc: boolean) => async (args: string[], ctx: CommandContext): Promise<void> => {
41
+ const wrfcRequested = defaultWrfc || hasFlag(args, '--wrfc');
42
+ const task = delegationTaskValues(args).join(' ').trim();
43
+ if (!task) {
44
+ ctx.print(defaultWrfc ? 'Usage: /wrfc <build/fix/review task>' : 'Usage: /delegate [--wrfc] <build/fix/review task>');
45
+ return;
46
+ }
47
+ const operator = ctx.clients?.operator;
48
+ if (!operator) {
49
+ ctx.print([
50
+ 'Delegation unavailable: no operator client is attached.',
51
+ 'Use the external daemon/shared-session route from a configured Agent runtime, or open GoodVibes TUI in the target workspace.',
52
+ ].join('\n'));
53
+ return;
54
+ }
55
+ try {
56
+ const participant = {
57
+ surfaceKind: 'service',
58
+ surfaceId: 'goodvibes-agent',
59
+ externalId: ctx.session.runtime.sessionId,
60
+ displayName: 'GoodVibes Agent',
61
+ lastSeenAt: Date.now(),
62
+ } satisfies SharedSessionParticipant;
63
+ const session = await operator.sessions.ensureSession({
64
+ title: `Agent delegation: ${task.slice(0, 72)}`,
65
+ participant,
66
+ metadata: {
67
+ originSurface: 'goodvibes-agent',
68
+ sourceSessionId: ctx.session.runtime.sessionId,
69
+ task,
70
+ wrfcRequested,
71
+ },
72
+ });
73
+ await operator.sessions.submitMessage({
74
+ sessionId: session.id,
75
+ body: buildDelegationBody(task, wrfcRequested),
76
+ surfaceKind: participant.surfaceKind,
77
+ surfaceId: participant.surfaceId,
78
+ externalId: participant.externalId,
79
+ displayName: participant.displayName,
80
+ title: `Agent delegation: ${task.slice(0, 72)}`,
81
+ metadata: {
82
+ originSurface: 'goodvibes-agent',
83
+ sourceSessionId: ctx.session.runtime.sessionId,
84
+ kind: 'task',
85
+ task,
86
+ wrfcRequested,
87
+ },
88
+ routing: {
89
+ executionIntent: {
90
+ riskClass: 'elevated',
91
+ requiresApproval: true,
92
+ networkPolicy: 'inherit',
93
+ filesystemPolicy: 'workspace-write',
94
+ },
95
+ },
96
+ });
97
+ ctx.print([
98
+ 'Delegation submitted to GoodVibes TUI/shared-session routes.',
99
+ ` session: ${session.id}`,
100
+ ` mode: ${wrfcRequested ? 'WRFC requested' : 'direct build delegation'}`,
101
+ ` task: ${task}`,
102
+ ' next: check GoodVibes TUI shared-session/task status for the result.',
103
+ ].join('\n'));
104
+ } catch (error) {
105
+ ctx.print([
106
+ 'Delegation failed.',
107
+ ` error: ${summarizeError(error)}`,
108
+ ' fallback: open GoodVibes TUI in the target workspace and paste the original task there.',
109
+ ].join('\n'));
110
+ }
111
+ };
112
+
113
+ registry.register({
114
+ name: 'delegate',
115
+ aliases: ['build'],
116
+ description: 'Explicitly delegate build/fix/review work to GoodVibes TUI through shared-session routes',
117
+ usage: '[--wrfc] <task>',
118
+ argsHint: '[--wrfc] <task>',
119
+ handler: makeHandler(false),
120
+ });
121
+ registry.register({
122
+ name: 'wrfc',
123
+ aliases: ['review'],
124
+ description: 'Explicitly delegate build/fix/review work to GoodVibes TUI with WRFC requested',
125
+ usage: '<task>',
126
+ argsHint: '<task>',
127
+ handler: makeHandler(true),
128
+ });
129
+ }