@pellux/goodvibes-agent 0.1.47 → 0.1.49

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 (36) hide show
  1. package/CHANGELOG.md +18 -10
  2. package/README.md +3 -23
  3. package/docs/README.md +0 -2
  4. package/package.json +1 -3
  5. package/src/cli/completion.ts +0 -1
  6. package/src/cli/help.ts +0 -12
  7. package/src/cli/management.ts +0 -6
  8. package/src/cli/package-verification.ts +0 -1
  9. package/src/cli/parser.ts +0 -4
  10. package/src/cli/types.ts +0 -1
  11. package/src/input/agent-workspace-channels.ts +214 -0
  12. package/src/input/agent-workspace-setup.ts +121 -0
  13. package/src/input/agent-workspace.ts +75 -210
  14. package/src/input/commands/experience-runtime.ts +2 -25
  15. package/src/input/commands/remote-runtime.ts +5 -5
  16. package/src/input/commands.ts +0 -2
  17. package/src/input/onboarding/onboarding-wizard-steps.ts +7 -7
  18. package/src/panels/builtin/knowledge.ts +3 -3
  19. package/src/panels/builtin/shared.ts +3 -0
  20. package/src/panels/knowledge-panel.ts +80 -9
  21. package/src/panels/provider-health-domains.ts +1 -1
  22. package/src/panels/remote-panel.ts +2 -2
  23. package/src/renderer/agent-workspace.ts +39 -10
  24. package/src/runtime/bootstrap-core.ts +2 -0
  25. package/src/runtime/bootstrap-shell.ts +1 -0
  26. package/src/runtime/bootstrap.ts +1 -0
  27. package/src/tools/agent-context-policy.ts +2 -2
  28. package/src/tools/agent-local-registry-tool.ts +341 -0
  29. package/src/verification/live-verifier.ts +0 -15
  30. package/src/version.ts +1 -1
  31. package/docs/operator-capability-benchmark.md +0 -106
  32. package/src/cli/capabilities-command.ts +0 -173
  33. package/src/config/goodvibes-home-audit.ts +0 -465
  34. package/src/input/commands/capabilities-runtime.ts +0 -102
  35. package/src/operator/capability-benchmark.ts +0 -244
  36. package/src/operator/daemon-capability-audit.ts +0 -1534
@@ -2,9 +2,9 @@ import type { Line } from '../types/grid.ts';
2
2
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
3
3
  import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
4
4
  import type { MemoryClass, MemoryRecord, MemoryRegistry, MemoryReviewState } from '@pellux/goodvibes-sdk/platform/state';
5
+ import type { KnowledgeStatus } from '@pellux/goodvibes-sdk/platform/knowledge';
5
6
  import {
6
7
  buildBodyText,
7
- buildEmptyState,
8
8
  buildGuidanceLine,
9
9
  buildKeyValueLine,
10
10
  buildPanelLine,
@@ -12,6 +12,10 @@ import {
12
12
  DEFAULT_PANEL_PALETTE,
13
13
  } from './polish.ts';
14
14
 
15
+ export interface AgentKnowledgePanelService {
16
+ readonly getStatus: () => Promise<KnowledgeStatus & { readonly note?: string }>;
17
+ }
18
+
15
19
  function summarize(records: MemoryRecord[], cls: MemoryClass): MemoryRecord[] {
16
20
  return records.filter((record) => record.cls === cls).slice(0, 3);
17
21
  }
@@ -42,19 +46,25 @@ function formatConfidence(confidence: number): string {
42
46
 
43
47
  export class KnowledgePanel extends ScrollableListPanel<MemoryRecord> {
44
48
  private readonly registry: MemoryRegistry;
49
+ private readonly agentKnowledgeService: AgentKnowledgePanelService | null;
45
50
  private unsubscribe?: () => void;
46
51
  private records: MemoryRecord[] = [];
52
+ private agentKnowledgeStatus: (KnowledgeStatus & { readonly note?: string }) | null = null;
53
+ private agentKnowledgeError: string | null = null;
54
+ private agentKnowledgeLoading = false;
47
55
  // I1: confirm for destructive review-state mutations
48
56
  private confirm: ConfirmState<{ id: string; action: 'stale' | 'contradicted' }> | null = null;
49
57
 
50
- public constructor(registry: MemoryRegistry) {
58
+ public constructor(registry: MemoryRegistry, agentKnowledgeService: AgentKnowledgePanelService | null = null) {
51
59
  super('knowledge', 'Knowledge', 'K', 'agent');
52
60
  this.registry = registry;
61
+ this.agentKnowledgeService = agentKnowledgeService;
53
62
  }
54
63
 
55
64
  public override onActivate(): void {
56
65
  super.onActivate();
57
66
  this.refresh();
67
+ this.refreshAgentKnowledgeStatus();
58
68
  this.unsubscribe = this.registry.subscribe(() => {
59
69
  this.refresh();
60
70
  this.markDirty();
@@ -89,12 +99,13 @@ export class KnowledgePanel extends ScrollableListPanel<MemoryRecord> {
89
99
  }
90
100
 
91
101
  protected override getPalette() { return C; }
92
- protected override getEmptyStateMessage() { return 'No durable project knowledge'; }
102
+ protected override getEmptyStateMessage() { return 'No Agent Knowledge sources or local memory review records'; }
93
103
  protected override getEmptyStateActions() {
94
104
  return [
95
- { command: '/recall add fact <summary>', summary: 'capture a durable fact directly' },
96
- { command: '/recall capture incident latest', summary: 'promote the latest incident into project memory' },
97
- { command: '/recall capture policy', summary: 'store the current policy posture as durable evidence' },
105
+ { command: '/knowledge status', summary: 'inspect the isolated Agent Knowledge store' },
106
+ { command: '/knowledge ingest-url <url> --yes', summary: 'ingest source-backed material into Agent Knowledge only' },
107
+ { command: '/knowledge queue', summary: 'review Agent Knowledge issues' },
108
+ { command: '/recall add fact <summary>', summary: 'capture a local non-secret memory record when appropriate' },
98
109
  ];
99
110
  }
100
111
 
@@ -198,6 +209,64 @@ export class KnowledgePanel extends ScrollableListPanel<MemoryRecord> {
198
209
  this.clampSelection();
199
210
  }
200
211
 
212
+ private refreshAgentKnowledgeStatus(): void {
213
+ if (!this.agentKnowledgeService || this.agentKnowledgeLoading) return;
214
+ this.agentKnowledgeLoading = true;
215
+ this.agentKnowledgeError = null;
216
+ this.agentKnowledgeService.getStatus()
217
+ .then((status) => {
218
+ this.agentKnowledgeStatus = status;
219
+ this.agentKnowledgeError = null;
220
+ })
221
+ .catch((error: unknown) => {
222
+ this.agentKnowledgeError = error instanceof Error ? error.message : String(error);
223
+ })
224
+ .finally(() => {
225
+ this.agentKnowledgeLoading = false;
226
+ this.markDirty();
227
+ });
228
+ }
229
+
230
+ private buildAgentKnowledgeHeader(width: number): Line[] {
231
+ const lines: Line[] = [
232
+ buildPanelLine(width, [[' Agent Knowledge Segment', C.label]]),
233
+ buildPanelLine(width, [
234
+ [' route ', C.label],
235
+ ['/api/goodvibes-agent/knowledge/*', C.info],
236
+ [' isolated: no default Knowledge/Wiki or HomeGraph fallback', C.dim],
237
+ ]),
238
+ ];
239
+ if (this.agentKnowledgeLoading && !this.agentKnowledgeStatus) {
240
+ lines.push(buildPanelLine(width, [[' loading isolated Agent Knowledge status...', C.dim]]));
241
+ } else if (this.agentKnowledgeStatus) {
242
+ const status = this.agentKnowledgeStatus;
243
+ lines.push(buildKeyValueLine(width, [
244
+ { label: 'Ready', value: status.ready ? 'yes' : 'no', valueColor: status.ready ? C.good : C.warn },
245
+ { label: 'Sources', value: String(status.sourceCount), valueColor: status.sourceCount > 0 ? C.info : C.dim },
246
+ { label: 'Nodes', value: String(status.nodeCount), valueColor: status.nodeCount > 0 ? C.info : C.dim },
247
+ { label: 'Issues', value: String(status.issueCount), valueColor: status.issueCount > 0 ? C.warn : C.good },
248
+ ], C));
249
+ const note = status.note ? ` note: ${status.note}` : '';
250
+ lines.push(buildPanelLine(width, [[' storage: ', C.label], [status.storagePath, C.dim], [note, C.dim]]));
251
+ } else {
252
+ lines.push(buildPanelLine(width, [[' status: not loaded; /knowledge status uses the same isolated route.', C.dim]]));
253
+ }
254
+ if (this.agentKnowledgeError) {
255
+ lines.push(...buildBodyText(width, `Agent Knowledge status warning: ${this.agentKnowledgeError}`, C, C.warn));
256
+ }
257
+ lines.push(buildPanelLine(width, [
258
+ [' actions ', C.label],
259
+ ['/knowledge status', C.value],
260
+ [' | ', C.dim],
261
+ ['/knowledge ingest-url <url> --yes', C.value],
262
+ [' | ', C.dim],
263
+ ['/knowledge search <query>', C.value],
264
+ [' | ', C.dim],
265
+ ['/knowledge queue', C.value],
266
+ ]));
267
+ return lines;
268
+ }
269
+
201
270
  public render(width: number, height: number): Line[] {
202
271
  this.clampSelection();
203
272
 
@@ -214,12 +283,14 @@ export class KnowledgePanel extends ScrollableListPanel<MemoryRecord> {
214
283
 
215
284
  if (this.records.length === 0) this.refresh();
216
285
 
217
- const intro = 'Typed project knowledge, reviewed evidence, and operator-governed memory across session, project, and team scopes.';
286
+ const intro = 'Isolated Agent Knowledge plus local non-secret memory review. This surface never falls back to default Knowledge/Wiki or HomeGraph.';
218
287
  const records = this.registry.search({ limit: 200 });
288
+ const agentKnowledgeHeader = this.buildAgentKnowledgeHeader(width);
219
289
 
220
290
  if (records.length === 0) {
221
291
  return this.renderList(width, height, {
222
292
  title: 'Knowledge Control Room',
293
+ header: agentKnowledgeHeader,
223
294
  footer: [buildPanelLine(width, [[' Review keys: Up/Down move r/Enter review s stale c contradicted f fresh', C.dim]])],
224
295
  });
225
296
  }
@@ -334,11 +405,11 @@ export class KnowledgePanel extends ScrollableListPanel<MemoryRecord> {
334
405
 
335
406
  return this.renderList(width, height, {
336
407
  title: 'Knowledge Control Room',
337
- header: [...classLines, ...reviewLines],
408
+ header: [...agentKnowledgeHeader, ...classLines, ...reviewLines],
338
409
  footer: [
410
+ buildPanelLine(width, [[' Up/Down move r/Enter reviewed s stale c contradicted f fresh', C.dim]]),
339
411
  ...(selectedLines.length > 0 ? selectedLines : []),
340
412
  ...recentSummaryLines,
341
- buildPanelLine(width, [[' Up/Down move r/Enter reviewed s stale c contradicted f fresh', C.dim]]),
342
413
  ],
343
414
  });
344
415
  }
@@ -104,7 +104,7 @@ export function buildProviderHealthDomainSummaries(
104
104
  .slice(0, 3)
105
105
  .map((entry) => `${entry.runnerId}: transport=${entry.transportState} heartbeat=${entry.heartbeat.status}${entry.lastError ? ` error=${entry.lastError}` : ''}`),
106
106
  nextSteps: remote.supervisor.degradedConnections > 0
107
- ? ['/remote supervisor', '/remote recover <runnerId>', '/remote capabilities']
107
+ ? ['/remote supervisor', '/remote recover <runnerId>', '/remote support']
108
108
  : ['/remote supervisor'],
109
109
  });
110
110
 
@@ -204,8 +204,8 @@ export class RemotePanel extends BasePanel {
204
204
  ]));
205
205
  }
206
206
  postureLines.push(
207
- buildGuidanceLine(width, '/remote recover', 'resume remote state with runner, capability, and disconnect recovery hints', C),
208
- buildGuidanceLine(width, '/remote capabilities', 'inspect transport support before routing remote work or reattaching a session', C),
207
+ buildGuidanceLine(width, '/remote recover', 'resume remote state with runner support and disconnect recovery hints', C),
208
+ buildGuidanceLine(width, '/remote support', 'inspect transport support before routing remote work or reattaching a session', C),
209
209
  );
210
210
 
211
211
  const footerLines = [
@@ -67,11 +67,39 @@ function buildLeftRows(workspace: AgentWorkspace, height: number): WorkspaceRow[
67
67
  }
68
68
 
69
69
  function actionCommand(action: AgentWorkspaceAction): string {
70
+ if (action.kind === 'workspace') return action.targetCategoryId ? `open ${action.targetCategoryId}` : '(workspace)';
70
71
  return action.command ?? '(guidance)';
71
72
  }
72
73
 
73
74
  type ContextLine = { readonly text: string; readonly fg?: string; readonly bold?: boolean; readonly dim?: boolean };
74
75
 
76
+ function setupStatusColor(status: AgentWorkspaceRuntimeSnapshot['setupChecklist'][number]['status']): string {
77
+ if (status === 'ready') return PALETTE.good;
78
+ if (status === 'recommended') return PALETTE.warn;
79
+ if (status === 'blocked') return PALETTE.bad;
80
+ return PALETTE.muted;
81
+ }
82
+
83
+ function setupChecklistLines(snapshot: AgentWorkspaceRuntimeSnapshot): ContextLine[] {
84
+ const readyCount = snapshot.setupChecklist.filter((item) => item.status === 'ready').length;
85
+ const recommendedCount = snapshot.setupChecklist.filter((item) => item.status === 'recommended').length;
86
+ const blockedCount = snapshot.setupChecklist.filter((item) => item.status === 'blocked').length;
87
+ const lines: ContextLine[] = [
88
+ { text: 'Setup Checklist', fg: PALETTE.title, bold: true },
89
+ { text: `${readyCount}/${snapshot.setupChecklist.length} ready; ${recommendedCount} recommended; ${blockedCount} blocked`, fg: blockedCount > 0 ? PALETTE.warn : PALETTE.info },
90
+ ];
91
+ for (const item of snapshot.setupChecklist) {
92
+ const command = item.command ? ` -> ${item.command}` : '';
93
+ lines.push({
94
+ text: `${item.status.toUpperCase()} ${item.label}${command}`,
95
+ fg: setupStatusColor(item.status),
96
+ bold: item.status === 'blocked',
97
+ });
98
+ lines.push({ text: ` ${item.detail}`, fg: PALETTE.muted });
99
+ }
100
+ return lines;
101
+ }
102
+
75
103
  function snapshotLines(category: AgentWorkspaceCategory, snapshot: AgentWorkspaceRuntimeSnapshot | null): ContextLine[] {
76
104
  if (!snapshot) return [{ text: 'Runtime context is not loaded yet.', fg: PALETTE.warn }];
77
105
  const base: ContextLine[] = [{ text: 'Live Agent Context', fg: PALETTE.title, bold: true }];
@@ -87,14 +115,8 @@ function snapshotLines(category: AgentWorkspaceCategory, snapshot: AgentWorkspac
87
115
  { text: `Daemon ownership: ${snapshot.daemonOwnership}; Agent never starts or restarts it`, fg: PALETTE.good },
88
116
  { text: `Workspace: ${snapshot.workingDirectory}`, fg: PALETTE.muted },
89
117
  { text: `Home: ${snapshot.homeDirectory}`, fg: PALETTE.muted },
90
- );
91
- } else if (category.id === 'capabilities') {
92
- base.push(
93
- { text: `External daemon: ${snapshot.daemonBaseUrl}`, fg: PALETTE.info },
94
- { text: 'Live audit source: /api/control-plane/methods plus /api/goodvibes-agent/knowledge/status.', fg: PALETTE.info },
95
- { text: 'Isolation: no default Knowledge/Wiki, HomeGraph, or Home Assistant route is used for Agent Knowledge coverage.', fg: PALETTE.good },
96
- { text: 'Readiness meaning: daemon route coverage is platform capability; missing Agent UX remains a product gap to close here.', fg: PALETTE.muted },
97
- { text: 'Use filtered audits for knowledge, channels, automation, voice/media/nodes, providers, MCP/tools, approvals, or sessions.', fg: PALETTE.muted },
118
+ { text: '' },
119
+ ...setupChecklistLines(snapshot),
98
120
  );
99
121
  } else if (category.id === 'channels') {
100
122
  const enabledCount = snapshot.channels.filter((channel) => channel.enabled).length;
@@ -275,10 +297,17 @@ function footerText(workspace: AgentWorkspace): string {
275
297
  }
276
298
 
277
299
  export function renderAgentWorkspace(workspace: AgentWorkspace, width: number, height: number): Line[] {
278
- const layoutOptions = { width, height, leftWidth: width < 90 ? undefined : 30, contextRatio: 0.62, minContextRows: 10 };
279
- const metrics = getFullscreenWorkspaceMetrics(layoutOptions);
280
300
  const category = workspace.selectedCategory;
281
301
  const action = workspace.selectedAction;
302
+ const setupCategory = category.id === 'setup';
303
+ const layoutOptions = {
304
+ width,
305
+ height,
306
+ leftWidth: width < 90 ? undefined : 30,
307
+ contextRatio: setupCategory ? 0.86 : 0.62,
308
+ minContextRows: setupCategory ? 18 : 10,
309
+ };
310
+ const metrics = getFullscreenWorkspaceMetrics(layoutOptions);
282
311
 
283
312
  return renderFullscreenWorkspace({
284
313
  width,
@@ -30,6 +30,7 @@ import { registerBootstrapRuntimeEvents } from '@/runtime/index.ts';
30
30
  import { createRuntimeServices, type RuntimeServices } from './services.ts';
31
31
  import { createUiRuntimeServices, type UiRuntimeServices } from './ui-services.ts';
32
32
  import { installAgentToolPolicyGuard } from '../tools/wrfc-agent-guard.ts';
33
+ import { registerAgentLocalRegistryTool } from '../tools/agent-local-registry-tool.ts';
33
34
  import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
34
35
 
35
36
  export interface BootstrapCoreState {
@@ -227,6 +228,7 @@ export async function initializeBootstrapCore(
227
228
  overflowHandler: services.overflowHandler,
228
229
  changeTracker: services.sessionChangeTracker,
229
230
  });
231
+ registerAgentLocalRegistryTool(toolRegistry, services.shellPaths);
230
232
  installAgentToolPolicyGuard(toolRegistry, {
231
233
  getLastUserMessage: () => conversation.getLastUserMessage(),
232
234
  });
@@ -135,6 +135,7 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
135
135
  sandboxSessionRegistry: services.sandboxSessionRegistry,
136
136
  systemMessagesPanel,
137
137
  memoryRegistry: services.memoryRegistry,
138
+ agentKnowledgeService: services.agentKnowledgeService,
138
139
  uiServices,
139
140
  pluginManager: services.pluginManager,
140
141
  hookDispatcher: services.hookDispatcher,
@@ -48,6 +48,7 @@ const GOODVIBES_AGENT_OPERATOR_POLICY = [
48
48
  '## GoodVibes Agent Operator Policy',
49
49
  '- Default to serial, proactive assistant work in the main conversation. Answer, inspect, summarize, remember useful non-secret facts, configure local Agent state, use read-only daemon/operator routes, and take safe non-destructive actions without spawning local agents or WRFC.',
50
50
  '- GoodVibes Agent connects to an externally managed GoodVibes daemon. Do not start, stop, restart, install, expose, or mutate daemon/listener/control-plane surface posture from Agent runtime.',
51
+ '- Use the `agent_local_registry` tool when a reusable persona, skill, or routine would improve future work. Keep those records local, non-secret, source/provenance tagged, and reviewable. Starting a routine means applying its steps in this same serial conversation, not creating a background job.',
51
52
  '- WRFC is never the default Agent reasoning path. Do not create local WRFC chains for planning, research, operations, knowledge, memory, configuration, approvals, automation observability, or ordinary assistant work.',
52
53
  '- GoodVibes Agent is not the coding TUI. Do not use the `agent` tool to spawn local Engineer, Reviewer, Tester, Verifier, or batch-spawn roots from Agent.',
53
54
  '- When the user explicitly asks to build, implement, fix, patch, or review code, preserve the full original user ask and delegate one build request to GoodVibes TUI through the public shared-session/build-delegation contract. Include clear executionIntent and request WRFC only for explicit build/fix/review work or when the user explicitly asks for WRFC/agent review.',
@@ -3,13 +3,13 @@ import type { Tool } from '@pellux/goodvibes-sdk/platform/types';
3
3
  const CONTEXT_TOOL_DENIAL = [
4
4
  'GoodVibes Agent does not expose copied GoodVibes runtime context through model tools in the main conversation.',
5
5
  'The copied context tool can describe TUI/default runtime assumptions that are not the Agent product boundary.',
6
- 'Use explicit Agent CLI/slash commands such as status, compat, capabilities, and isolated Agent Knowledge instead.',
6
+ 'Use explicit Agent CLI/slash commands such as status, compat, setup, and isolated Agent Knowledge instead.',
7
7
  ].join(' ');
8
8
 
9
9
  export function wrapBlockedContextToolForAgentPolicy(tool: Tool): void {
10
10
  tool.definition.description = [
11
11
  'Blocked in GoodVibes Agent main conversation: copied runtime context.',
12
- 'Use explicit Agent CLI/slash status, compat, capabilities, and Agent Knowledge commands for product-scoped context.',
12
+ 'Use explicit Agent CLI/slash status, compat, setup, and Agent Knowledge commands for product-scoped context.',
13
13
  'Default Knowledge/Wiki, HomeGraph, and copied TUI runtime assumptions are not Agent fallbacks.',
14
14
  ].join(' ');
15
15
  tool.definition.sideEffects = [];
@@ -0,0 +1,341 @@
1
+ import type { Tool } from '@pellux/goodvibes-sdk/platform/types';
2
+ import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
3
+ import type { ShellPathService } from '@/runtime/index.ts';
4
+ import { AgentPersonaRegistry, type AgentPersonaRecord } from '../agent/persona-registry.ts';
5
+ import { AgentRoutineRegistry, type AgentRoutineRecord } from '../agent/routine-registry.ts';
6
+ import { AgentSkillRegistry, type AgentSkillRecord } from '../agent/skill-registry.ts';
7
+
8
+ export type AgentLocalRegistryDomain = 'persona' | 'skill' | 'routine';
9
+ export type AgentLocalRegistryAction =
10
+ | 'list'
11
+ | 'search'
12
+ | 'get'
13
+ | 'create'
14
+ | 'update'
15
+ | 'enable'
16
+ | 'disable'
17
+ | 'review'
18
+ | 'stale'
19
+ | 'use'
20
+ | 'clear_active'
21
+ | 'start';
22
+
23
+ export interface AgentLocalRegistryToolArgs {
24
+ readonly domain?: unknown;
25
+ readonly action?: unknown;
26
+ readonly id?: unknown;
27
+ readonly query?: unknown;
28
+ readonly name?: unknown;
29
+ readonly description?: unknown;
30
+ readonly body?: unknown;
31
+ readonly procedure?: unknown;
32
+ readonly steps?: unknown;
33
+ readonly triggers?: unknown;
34
+ readonly tags?: unknown;
35
+ readonly reason?: unknown;
36
+ readonly enabled?: unknown;
37
+ readonly provenance?: unknown;
38
+ }
39
+
40
+ const DOMAINS: readonly AgentLocalRegistryDomain[] = ['persona', 'skill', 'routine'];
41
+ const ACTIONS: readonly AgentLocalRegistryAction[] = [
42
+ 'list',
43
+ 'search',
44
+ 'get',
45
+ 'create',
46
+ 'update',
47
+ 'enable',
48
+ 'disable',
49
+ 'review',
50
+ 'stale',
51
+ 'use',
52
+ 'clear_active',
53
+ 'start',
54
+ ];
55
+
56
+ function isDomain(value: unknown): value is AgentLocalRegistryDomain {
57
+ return typeof value === 'string' && DOMAINS.includes(value as AgentLocalRegistryDomain);
58
+ }
59
+
60
+ function isAction(value: unknown): value is AgentLocalRegistryAction {
61
+ return typeof value === 'string' && ACTIONS.includes(value as AgentLocalRegistryAction);
62
+ }
63
+
64
+ function readString(value: unknown): string {
65
+ return typeof value === 'string' ? value.trim() : '';
66
+ }
67
+
68
+ function readStringList(value: unknown): readonly string[] {
69
+ if (typeof value === 'string') {
70
+ return value.split(',').map((entry) => entry.trim()).filter(Boolean);
71
+ }
72
+ if (!Array.isArray(value)) return [];
73
+ return value.filter((entry): entry is string => typeof entry === 'string').map((entry) => entry.trim()).filter(Boolean);
74
+ }
75
+
76
+ function registryError(message: string): { readonly success: false; readonly error: string } {
77
+ return { success: false, error: message };
78
+ }
79
+
80
+ function registryOutput(output: string): { readonly success: true; readonly output: string } {
81
+ return { success: true, output };
82
+ }
83
+
84
+ function requireId(args: AgentLocalRegistryToolArgs): string {
85
+ const id = readString(args.id);
86
+ if (!id) throw new Error('id is required.');
87
+ return id;
88
+ }
89
+
90
+ function requireName(args: AgentLocalRegistryToolArgs): string {
91
+ const name = readString(args.name);
92
+ if (!name) throw new Error('name is required.');
93
+ return name;
94
+ }
95
+
96
+ function requireDescription(args: AgentLocalRegistryToolArgs): string {
97
+ const description = readString(args.description);
98
+ if (!description) throw new Error('description is required.');
99
+ return description;
100
+ }
101
+
102
+ function formatPersona(persona: AgentPersonaRecord, activeId: string | null): string {
103
+ const active = persona.id === activeId ? 'active' : 'inactive';
104
+ return `${persona.id} ${active} ${persona.reviewState} ${persona.name} - ${persona.description}`;
105
+ }
106
+
107
+ function formatSkill(skill: AgentSkillRecord): string {
108
+ const enabled = skill.enabled ? 'enabled' : 'disabled';
109
+ return `${skill.id} ${enabled} ${skill.reviewState} ${skill.name} - ${skill.description}`;
110
+ }
111
+
112
+ function formatRoutine(routine: AgentRoutineRecord): string {
113
+ const enabled = routine.enabled ? 'enabled' : 'disabled';
114
+ return `${routine.id} ${enabled} ${routine.reviewState} starts=${routine.startCount} ${routine.name} - ${routine.description}`;
115
+ }
116
+
117
+ function listPersonas(registry: AgentPersonaRegistry, records: readonly AgentPersonaRecord[], title: string): string {
118
+ const snapshot = registry.snapshot();
119
+ return records.length === 0
120
+ ? `${title}\nNo Agent-local personas.`
121
+ : [title, ...records.map((persona) => formatPersona(persona, snapshot.activePersonaId))].join('\n');
122
+ }
123
+
124
+ function listSkills(records: readonly AgentSkillRecord[], title: string): string {
125
+ return records.length === 0
126
+ ? `${title}\nNo Agent-local skills.`
127
+ : [title, ...records.map(formatSkill)].join('\n');
128
+ }
129
+
130
+ function listRoutines(records: readonly AgentRoutineRecord[], title: string): string {
131
+ return records.length === 0
132
+ ? `${title}\nNo Agent-local routines.`
133
+ : [title, ...records.map(formatRoutine)].join('\n');
134
+ }
135
+
136
+ function handlePersona(shellPaths: ShellPathService, action: AgentLocalRegistryAction, args: AgentLocalRegistryToolArgs): string {
137
+ const registry = AgentPersonaRegistry.fromShellPaths(shellPaths);
138
+ if (action === 'list') return listPersonas(registry, registry.list(), 'Agent-local personas');
139
+ if (action === 'search') return listPersonas(registry, registry.search(readString(args.query)), 'Agent-local personas search');
140
+ if (action === 'get') {
141
+ const persona = registry.get(requireId(args));
142
+ if (!persona) return `Unknown Agent-local persona: ${readString(args.id)}`;
143
+ return [
144
+ formatPersona(persona, registry.snapshot().activePersonaId),
145
+ `triggers: ${persona.triggers.join(', ') || '(manual)'}`,
146
+ `tags: ${persona.tags.join(', ') || '(none)'}`,
147
+ '',
148
+ persona.body,
149
+ ].join('\n');
150
+ }
151
+ if (action === 'create') {
152
+ const persona = registry.create({
153
+ name: requireName(args),
154
+ description: requireDescription(args),
155
+ body: readString(args.body),
156
+ tags: readStringList(args.tags),
157
+ triggers: readStringList(args.triggers),
158
+ source: 'agent',
159
+ provenance: readString(args.provenance) || 'agent-local-registry-tool',
160
+ });
161
+ return `Created Agent-local persona ${persona.id}: ${persona.name}`;
162
+ }
163
+ if (action === 'update') {
164
+ const persona = registry.update(requireId(args), {
165
+ name: readString(args.name) || undefined,
166
+ description: readString(args.description) || undefined,
167
+ body: readString(args.body) || undefined,
168
+ tags: args.tags === undefined ? undefined : readStringList(args.tags),
169
+ triggers: args.triggers === undefined ? undefined : readStringList(args.triggers),
170
+ provenance: readString(args.provenance) || 'agent-local-registry-tool',
171
+ });
172
+ return `Updated Agent-local persona ${persona.id}: ${persona.name}`;
173
+ }
174
+ if (action === 'use') {
175
+ const persona = registry.setActive(requireId(args));
176
+ return `Active Agent-local persona set to ${persona.id}: ${persona.name}`;
177
+ }
178
+ if (action === 'clear_active') {
179
+ registry.clearActive();
180
+ return 'Cleared active Agent-local persona.';
181
+ }
182
+ if (action === 'review') return `Reviewed Agent-local persona ${registry.markReviewed(requireId(args)).id}.`;
183
+ if (action === 'stale') return `Marked Agent-local persona ${registry.markStale(requireId(args), readString(args.reason)).id} stale.`;
184
+ throw new Error(`Action ${action} is not valid for personas.`);
185
+ }
186
+
187
+ function handleSkill(shellPaths: ShellPathService, action: AgentLocalRegistryAction, args: AgentLocalRegistryToolArgs): string {
188
+ const registry = AgentSkillRegistry.fromShellPaths(shellPaths);
189
+ if (action === 'list') return listSkills(registry.list(), 'Agent-local skills');
190
+ if (action === 'search') return listSkills(registry.search(readString(args.query)), 'Agent-local skills search');
191
+ if (action === 'get') {
192
+ const skill = registry.get(requireId(args));
193
+ if (!skill) return `Unknown Agent-local skill: ${readString(args.id)}`;
194
+ return [
195
+ formatSkill(skill),
196
+ `triggers: ${skill.triggers.join(', ') || '(manual)'}`,
197
+ `tags: ${skill.tags.join(', ') || '(none)'}`,
198
+ '',
199
+ skill.procedure,
200
+ ].join('\n');
201
+ }
202
+ if (action === 'create') {
203
+ const skill = registry.create({
204
+ name: requireName(args),
205
+ description: requireDescription(args),
206
+ procedure: readString(args.procedure),
207
+ triggers: readStringList(args.triggers),
208
+ tags: readStringList(args.tags),
209
+ enabled: args.enabled === true,
210
+ source: 'agent',
211
+ provenance: readString(args.provenance) || 'agent-local-registry-tool',
212
+ });
213
+ return `Created Agent-local skill ${skill.id}: ${skill.name}`;
214
+ }
215
+ if (action === 'update') {
216
+ const skill = registry.update(requireId(args), {
217
+ name: readString(args.name) || undefined,
218
+ description: readString(args.description) || undefined,
219
+ procedure: readString(args.procedure) || undefined,
220
+ triggers: args.triggers === undefined ? undefined : readStringList(args.triggers),
221
+ tags: args.tags === undefined ? undefined : readStringList(args.tags),
222
+ provenance: readString(args.provenance) || 'agent-local-registry-tool',
223
+ });
224
+ return `Updated Agent-local skill ${skill.id}: ${skill.name}`;
225
+ }
226
+ if (action === 'enable' || action === 'disable') {
227
+ const skill = registry.setEnabled(requireId(args), action === 'enable');
228
+ return `${action === 'enable' ? 'Enabled' : 'Disabled'} Agent-local skill ${skill.id}: ${skill.name}`;
229
+ }
230
+ if (action === 'review') return `Reviewed Agent-local skill ${registry.markReviewed(requireId(args)).id}.`;
231
+ if (action === 'stale') return `Marked Agent-local skill ${registry.markStale(requireId(args), readString(args.reason)).id} stale.`;
232
+ throw new Error(`Action ${action} is not valid for skills.`);
233
+ }
234
+
235
+ function handleRoutine(shellPaths: ShellPathService, action: AgentLocalRegistryAction, args: AgentLocalRegistryToolArgs): string {
236
+ const registry = AgentRoutineRegistry.fromShellPaths(shellPaths);
237
+ if (action === 'list') return listRoutines(registry.list(), 'Agent-local routines');
238
+ if (action === 'search') return listRoutines(registry.search(readString(args.query)), 'Agent-local routines search');
239
+ if (action === 'get') {
240
+ const routine = registry.get(requireId(args));
241
+ if (!routine) return `Unknown Agent-local routine: ${readString(args.id)}`;
242
+ return [
243
+ formatRoutine(routine),
244
+ `triggers: ${routine.triggers.join(', ') || '(manual)'}`,
245
+ `tags: ${routine.tags.join(', ') || '(none)'}`,
246
+ '',
247
+ routine.steps,
248
+ ].join('\n');
249
+ }
250
+ if (action === 'create') {
251
+ const routine = registry.create({
252
+ name: requireName(args),
253
+ description: requireDescription(args),
254
+ steps: readString(args.steps),
255
+ triggers: readStringList(args.triggers),
256
+ tags: readStringList(args.tags),
257
+ enabled: args.enabled === true,
258
+ source: 'agent',
259
+ provenance: readString(args.provenance) || 'agent-local-registry-tool',
260
+ });
261
+ return `Created Agent-local routine ${routine.id}: ${routine.name}`;
262
+ }
263
+ if (action === 'update') {
264
+ const routine = registry.update(requireId(args), {
265
+ name: readString(args.name) || undefined,
266
+ description: readString(args.description) || undefined,
267
+ steps: readString(args.steps) || undefined,
268
+ triggers: args.triggers === undefined ? undefined : readStringList(args.triggers),
269
+ tags: args.tags === undefined ? undefined : readStringList(args.tags),
270
+ provenance: readString(args.provenance) || 'agent-local-registry-tool',
271
+ });
272
+ return `Updated Agent-local routine ${routine.id}: ${routine.name}`;
273
+ }
274
+ if (action === 'enable' || action === 'disable') {
275
+ const routine = registry.setEnabled(requireId(args), action === 'enable');
276
+ return `${action === 'enable' ? 'Enabled' : 'Disabled'} Agent-local routine ${routine.id}: ${routine.name}`;
277
+ }
278
+ if (action === 'start') {
279
+ const routine = registry.markStarted(requireId(args));
280
+ return [
281
+ `Started Agent-local routine ${routine.id}: ${routine.name}`,
282
+ 'Policy: same main conversation; no hidden background job, daemon mutation, or external side effect was started.',
283
+ '',
284
+ routine.steps,
285
+ ].join('\n');
286
+ }
287
+ if (action === 'review') return `Reviewed Agent-local routine ${registry.markReviewed(requireId(args)).id}.`;
288
+ if (action === 'stale') return `Marked Agent-local routine ${registry.markStale(requireId(args), readString(args.reason)).id} stale.`;
289
+ throw new Error(`Action ${action} is not valid for routines.`);
290
+ }
291
+
292
+ export function createAgentLocalRegistryTool(shellPaths: ShellPathService): Tool {
293
+ return {
294
+ definition: {
295
+ name: 'agent_local_registry',
296
+ description: [
297
+ 'Inspect and maintain GoodVibes Agent-local personas, skills, and routines from the main conversation.',
298
+ 'Use this for safe self-improvement: create or refine reusable behavior, enable skills/routines, choose personas, review/stale records, and start routines in the same serial conversation.',
299
+ 'This tool cannot delete records, create schedules, mutate the daemon, send messages, run background jobs, or delegate build work.',
300
+ ].join(' '),
301
+ parameters: {
302
+ type: 'object',
303
+ properties: {
304
+ domain: { type: 'string', enum: [...DOMAINS] },
305
+ action: { type: 'string', enum: [...ACTIONS] },
306
+ id: { type: 'string' },
307
+ query: { type: 'string' },
308
+ name: { type: 'string' },
309
+ description: { type: 'string' },
310
+ body: { type: 'string', description: 'Persona body/instructions.' },
311
+ procedure: { type: 'string', description: 'Skill procedure.' },
312
+ steps: { type: 'string', description: 'Routine steps.' },
313
+ triggers: { type: 'array', items: { type: 'string' } },
314
+ tags: { type: 'array', items: { type: 'string' } },
315
+ reason: { type: 'string' },
316
+ enabled: { type: 'boolean' },
317
+ provenance: { type: 'string' },
318
+ },
319
+ required: ['domain', 'action'],
320
+ additionalProperties: false,
321
+ },
322
+ sideEffects: ['state'],
323
+ },
324
+ execute: async (rawArgs: unknown) => {
325
+ const args = rawArgs as AgentLocalRegistryToolArgs;
326
+ if (!isDomain(args.domain)) return registryError(`Unknown domain. Valid: ${DOMAINS.join(', ')}.`);
327
+ if (!isAction(args.action)) return registryError(`Unknown action. Valid: ${ACTIONS.join(', ')}.`);
328
+ try {
329
+ if (args.domain === 'persona') return registryOutput(handlePersona(shellPaths, args.action, args));
330
+ if (args.domain === 'skill') return registryOutput(handleSkill(shellPaths, args.action, args));
331
+ return registryOutput(handleRoutine(shellPaths, args.action, args));
332
+ } catch (error) {
333
+ return registryError(error instanceof Error ? error.message : String(error));
334
+ }
335
+ },
336
+ };
337
+ }
338
+
339
+ export function registerAgentLocalRegistryTool(registry: ToolRegistry, shellPaths: ShellPathService): void {
340
+ registry.register(createAgentLocalRegistryTool(shellPaths));
341
+ }