@pellux/goodvibes-agent 0.1.48 → 0.1.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,10 +2,20 @@
2
2
 
3
3
  All notable changes to GoodVibes Agent will be recorded here.
4
4
 
5
+ ## 0.1.50 - 2026-05-31
6
+
7
+ - bdb654a Improve local library workspaces
8
+
9
+ ## 0.1.49 - 2026-05-31
10
+
11
+ - 445e694 Show isolated Agent Knowledge in TUI panel
12
+ - 632d951 Add agent-local registry tool
13
+ - 4832355 Make agent setup workspace actionable
14
+
5
15
  ## 0.1.48 - 2026-05-31
6
16
 
7
17
  - 67f8ce0 Remove audit remnants and surface setup checklist
8
- - 34c3d0b Remove internal capability audit surfaces
18
+ - 34c3d0b Remove internal development-only surfaces
9
19
 
10
20
  ## 0.1.47 - 2026-05-31
11
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "private": false,
5
5
  "description": "Near-fork GoodVibes operator assistant with the GoodVibes TUI shell, renderer, input, fullscreen workspace, and daemon-connected Agent product brain.",
6
6
  "type": "module",
@@ -1,9 +1,9 @@
1
1
  import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
2
2
  import { basename, sep } from 'node:path';
3
3
  import type { CommandContext } from './command-registry.ts';
4
- import { AgentPersonaRegistry } from '../agent/persona-registry.ts';
5
- import { AgentRoutineRegistry } from '../agent/routine-registry.ts';
6
- import { AgentSkillRegistry } from '../agent/skill-registry.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
7
  import { getAgentRuntimeProfilesRoot, listAgentRuntimeProfiles, listAgentRuntimeProfileTemplates } from '../agent/runtime-profile.ts';
8
8
  import {
9
9
  buildAgentWorkspaceChannels,
@@ -20,13 +20,14 @@ export const AGENT_WORKSPACE_MODAL_NAME = 'agentWorkspace';
20
20
 
21
21
  export type AgentWorkspaceFocusPane = 'categories' | 'actions';
22
22
 
23
- export type AgentWorkspaceActionKind = 'command' | 'guidance';
23
+ export type AgentWorkspaceActionKind = 'command' | 'guidance' | 'workspace';
24
24
 
25
25
  export interface AgentWorkspaceAction {
26
26
  readonly id: string;
27
27
  readonly label: string;
28
28
  readonly detail: string;
29
29
  readonly command?: string;
30
+ readonly targetCategoryId?: string;
30
31
  readonly kind: AgentWorkspaceActionKind;
31
32
  readonly safety: 'safe' | 'read-only' | 'delegates' | 'blocked';
32
33
  }
@@ -52,6 +53,19 @@ export interface AgentWorkspaceActionResult {
52
53
  readonly safety?: AgentWorkspaceAction['safety'];
53
54
  }
54
55
 
56
+ export interface AgentWorkspaceLocalLibraryItem {
57
+ readonly id: string;
58
+ readonly name: string;
59
+ readonly description: string;
60
+ readonly reviewState: string;
61
+ readonly source: string;
62
+ readonly tags: readonly string[];
63
+ readonly triggers: readonly string[];
64
+ readonly active?: boolean;
65
+ readonly enabled?: boolean;
66
+ readonly startCount?: number;
67
+ }
68
+
55
69
  type AgentWorkspaceConfigReader = {
56
70
  get(key: string): unknown;
57
71
  };
@@ -68,10 +82,13 @@ export interface AgentWorkspaceRuntimeSnapshot {
68
82
  readonly sessionMemoryCount: number;
69
83
  readonly localRoutineCount: number;
70
84
  readonly enabledRoutineCount: number;
85
+ readonly localRoutines: readonly AgentWorkspaceLocalLibraryItem[];
71
86
  readonly localSkillCount: number;
72
87
  readonly enabledSkillCount: number;
88
+ readonly localSkills: readonly AgentWorkspaceLocalLibraryItem[];
73
89
  readonly localPersonaCount: number;
74
90
  readonly activePersonaName: string;
91
+ readonly localPersonas: readonly AgentWorkspaceLocalLibraryItem[];
75
92
  readonly knowledgeRoute: '/api/goodvibes-agent/knowledge';
76
93
  readonly knowledgeIsolation: 'agent-only';
77
94
  readonly executionPolicy: 'serial-proactive';
@@ -142,6 +159,46 @@ function inferActiveRuntimeProfile(homeDirectory: string): string {
142
159
  return homeDirectory.includes(marker) ? basename(homeDirectory) : '(default home)';
143
160
  }
144
161
 
162
+ function summarizePersonaItem(persona: AgentPersonaRecord, activePersonaId: string | null): AgentWorkspaceLocalLibraryItem {
163
+ return {
164
+ id: persona.id,
165
+ name: persona.name,
166
+ description: persona.description,
167
+ reviewState: persona.reviewState,
168
+ source: persona.source,
169
+ tags: persona.tags,
170
+ triggers: persona.triggers,
171
+ active: persona.id === activePersonaId,
172
+ };
173
+ }
174
+
175
+ function summarizeSkillItem(skill: AgentSkillRecord): AgentWorkspaceLocalLibraryItem {
176
+ return {
177
+ id: skill.id,
178
+ name: skill.name,
179
+ description: skill.description,
180
+ reviewState: skill.reviewState,
181
+ source: skill.source,
182
+ tags: skill.tags,
183
+ triggers: skill.triggers,
184
+ enabled: skill.enabled,
185
+ };
186
+ }
187
+
188
+ function summarizeRoutineItem(routine: AgentRoutineRecord): AgentWorkspaceLocalLibraryItem {
189
+ return {
190
+ id: routine.id,
191
+ name: routine.name,
192
+ description: routine.description,
193
+ reviewState: routine.reviewState,
194
+ source: routine.source,
195
+ tags: routine.tags,
196
+ triggers: routine.triggers,
197
+ enabled: routine.enabled,
198
+ startCount: routine.startCount,
199
+ };
200
+ }
201
+
145
202
  export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): AgentWorkspaceRuntimeSnapshot {
146
203
  const host = readConfigString(context, 'controlPlane.host', '127.0.0.1');
147
204
  const port = readConfigNumber(context, 'controlPlane.port', 3421);
@@ -164,31 +221,43 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
164
221
  const personaSnapshot = (() => {
165
222
  try {
166
223
  const shellPaths = context.workspace?.shellPaths;
167
- if (!shellPaths) return { count: 0, activeName: '(none)' };
224
+ if (!shellPaths) return { count: 0, activeName: '(none)', items: [] };
168
225
  const snapshot = AgentPersonaRegistry.fromShellPaths(shellPaths).snapshot();
169
- return { count: snapshot.personas.length, activeName: snapshot.activePersona?.name ?? '(none)' };
226
+ return {
227
+ count: snapshot.personas.length,
228
+ activeName: snapshot.activePersona?.name ?? '(none)',
229
+ items: snapshot.personas.map((persona) => summarizePersonaItem(persona, snapshot.activePersonaId)),
230
+ };
170
231
  } catch {
171
- return { count: 0, activeName: '(unavailable)' };
232
+ return { count: 0, activeName: '(unavailable)', items: [] };
172
233
  }
173
234
  })();
174
235
  const skillSnapshot = (() => {
175
236
  try {
176
237
  const shellPaths = context.workspace?.shellPaths;
177
- if (!shellPaths) return { count: 0, enabled: 0 };
238
+ if (!shellPaths) return { count: 0, enabled: 0, items: [] };
178
239
  const snapshot = AgentSkillRegistry.fromShellPaths(shellPaths).snapshot();
179
- return { count: snapshot.skills.length, enabled: snapshot.enabledSkills.length };
240
+ return {
241
+ count: snapshot.skills.length,
242
+ enabled: snapshot.enabledSkills.length,
243
+ items: snapshot.skills.map(summarizeSkillItem),
244
+ };
180
245
  } catch {
181
- return { count: 0, enabled: 0 };
246
+ return { count: 0, enabled: 0, items: [] };
182
247
  }
183
248
  })();
184
249
  const routineSnapshot = (() => {
185
250
  try {
186
251
  const shellPaths = context.workspace?.shellPaths;
187
- if (!shellPaths) return { count: 0, enabled: 0 };
252
+ if (!shellPaths) return { count: 0, enabled: 0, items: [] };
188
253
  const snapshot = AgentRoutineRegistry.fromShellPaths(shellPaths).snapshot();
189
- return { count: snapshot.routines.length, enabled: snapshot.enabledRoutines.length };
254
+ return {
255
+ count: snapshot.routines.length,
256
+ enabled: snapshot.enabledRoutines.length,
257
+ items: snapshot.routines.map(summarizeRoutineItem),
258
+ };
190
259
  } catch {
191
- return { count: 0, enabled: 0 };
260
+ return { count: 0, enabled: 0, items: [] };
192
261
  }
193
262
  })();
194
263
  const runtimeProfiles = (() => {
@@ -264,10 +333,13 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
264
333
  sessionMemoryCount,
265
334
  localRoutineCount: routineSnapshot.count,
266
335
  enabledRoutineCount: routineSnapshot.enabled,
336
+ localRoutines: routineSnapshot.items,
267
337
  localSkillCount: skillSnapshot.count,
268
338
  enabledSkillCount: skillSnapshot.enabled,
339
+ localSkills: skillSnapshot.items,
269
340
  localPersonaCount: personaSnapshot.count,
270
341
  activePersonaName: personaSnapshot.activeName,
342
+ localPersonas: personaSnapshot.items,
271
343
  knowledgeRoute: '/api/goodvibes-agent/knowledge',
272
344
  knowledgeIsolation: 'agent-only',
273
345
  executionPolicy: 'serial-proactive',
@@ -307,6 +379,11 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
307
379
  actions: [
308
380
  { id: 'chat', label: 'Continue assistant chat', detail: 'Close this workspace and type a normal message. Agent work stays serial in the main conversation.', kind: 'guidance', safety: 'safe' },
309
381
  { id: 'model', label: 'Choose model', detail: 'Open the model/provider workspace for the Agent chat route.', command: '/model', kind: 'command', safety: 'safe' },
382
+ { id: 'setup-home', label: 'Setup checklist', detail: 'Jump to the first-run checklist for provider, knowledge, personas, skills, routines, memory, channels, and voice/media.', targetCategoryId: 'setup', kind: 'workspace', safety: 'safe' },
383
+ { id: 'knowledge-home', label: 'Agent Knowledge', detail: 'Jump to isolated Agent Knowledge status, ingest, search, and review flows.', targetCategoryId: 'knowledge', kind: 'workspace', safety: 'read-only' },
384
+ { id: 'memory-home', label: 'Memory, skills, routines', detail: 'Jump to local memory, persona, skill, and routine setup. These are core Agent product features.', targetCategoryId: 'memory', kind: 'workspace', safety: 'safe' },
385
+ { id: 'channels-home', label: 'Channels', detail: 'Jump to companion pairing and channel readiness without changing daemon lifecycle.', targetCategoryId: 'channels', kind: 'workspace', safety: 'read-only' },
386
+ { id: 'voice-home', label: 'Voice and media', detail: 'Jump to voice, TTS, image input, browser, and node posture setup.', targetCategoryId: 'voice-media', kind: 'workspace', safety: 'safe' },
310
387
  { id: 'help', label: 'Browse commands', detail: 'Open registry-driven command help.', command: '/help', kind: 'command', safety: 'safe' },
311
388
  { id: 'health', label: 'Review health', detail: 'Show the local health review surface without starting or mutating daemon services.', command: '/health review', kind: 'command', safety: 'read-only' },
312
389
  ],
@@ -320,6 +397,15 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
320
397
  actions: [
321
398
  { id: 'config', label: 'Open config workspace', detail: 'Use the TUI-derived fullscreen settings workspace.', command: '/config', kind: 'command', safety: 'safe' },
322
399
  { id: 'onboarding', label: 'Open setup wizard', detail: 'Review Agent runtime settings in the fullscreen setup flow.', command: '/onboarding', kind: 'command', safety: 'safe' },
400
+ { id: 'setup-provider-model', label: 'Provider and model', detail: 'Choose the provider/model route for normal assistant chat.', command: '/model', kind: 'command', safety: 'safe' },
401
+ { id: 'setup-agent-knowledge', label: 'Agent Knowledge', detail: 'Inspect the isolated Agent Knowledge store before ingesting source-backed material.', command: '/knowledge status', kind: 'command', safety: 'read-only' },
402
+ { id: 'setup-runtime-profiles', label: 'Runtime profiles', detail: 'Browse starter templates for isolated Agent homes and operator identities.', command: '/agent-profile templates', kind: 'command', safety: 'read-only' },
403
+ { id: 'setup-personas', label: 'Personas', detail: 'Create or select the active local Agent persona.', targetCategoryId: 'personas', kind: 'workspace', safety: 'safe' },
404
+ { id: 'setup-skills', label: 'Skills', detail: 'Create, review, and enable reusable local Agent skills.', targetCategoryId: 'skills', kind: 'workspace', safety: 'safe' },
405
+ { id: 'setup-routines', label: 'Routines', detail: 'Create, review, and enable local Agent routines before any explicit schedule promotion.', targetCategoryId: 'routines', kind: 'workspace', safety: 'safe' },
406
+ { id: 'setup-memory', label: 'Local memory', detail: 'Inspect local/session memory; secrets stay rejected or redacted.', command: '/memory', kind: 'command', safety: 'read-only' },
407
+ { id: 'setup-channels', label: 'Channels', detail: 'Open companion pairing and channel readiness setup.', command: '/pair', kind: 'command', safety: 'safe' },
408
+ { id: 'setup-voice-media', label: 'Voice and media', detail: 'Open TTS/media settings for voice and image-capable Agent flows.', command: '/config tts', kind: 'command', safety: 'safe' },
323
409
  { id: 'provider', label: 'Provider status', detail: 'Review provider/model posture.', command: '/provider', kind: 'command', safety: 'read-only' },
324
410
  { id: 'auth', label: 'Auth review', detail: 'Review authentication posture without printing token values.', command: '/auth review', kind: 'command', safety: 'read-only' },
325
411
  ],
@@ -395,9 +481,53 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
395
481
  detail: 'Memory, routines, skills, and personas stay Agent-local until stable shared daemon registry contracts exist. Secrets must not be stored as memory.',
396
482
  actions: [
397
483
  { id: 'memory', label: 'Open memory', detail: 'Inspect local/session memory commands and surfaces.', command: '/memory', kind: 'command', safety: 'read-only' },
398
- { id: 'routines', label: 'Routine library', detail: 'Create, review, enable, and start local Agent routines in the main conversation.', command: '/routines', kind: 'command', safety: 'safe' },
399
- { id: 'skills', label: 'Local skill library', detail: 'Create, review, and enable local Agent reusable procedures.', command: '/agent-skills', kind: 'command', safety: 'safe' },
400
- { 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' },
484
+ { id: 'personas', label: 'Persona library', detail: 'Open the local persona workspace for active role selection and review.', targetCategoryId: 'personas', kind: 'workspace', safety: 'safe' },
485
+ { id: 'skills', label: 'Local skill library', detail: 'Open the local skill workspace for reusable procedures and review.', targetCategoryId: 'skills', kind: 'workspace', safety: 'safe' },
486
+ { id: 'routines', label: 'Routine library', detail: 'Open the local routine workspace for repeatable workflows and schedule promotion review.', targetCategoryId: 'routines', kind: 'workspace', safety: 'safe' },
487
+ ],
488
+ },
489
+ {
490
+ id: 'personas',
491
+ group: 'LEARN',
492
+ label: 'Personas',
493
+ summary: 'Local behavior profiles for the main assistant.',
494
+ detail: 'Personas shape the serial Agent in the main conversation. They are not background agents and they never spawn specialist roots.',
495
+ actions: [
496
+ { id: 'personas-list', label: 'List personas', detail: 'Print the full local persona library.', command: '/personas list', kind: 'command', safety: 'read-only' },
497
+ { id: 'personas-active', label: 'Show active persona', detail: 'Inspect the active local persona applied to new turns.', command: '/personas active', kind: 'command', safety: 'read-only' },
498
+ { id: 'personas-create', label: 'Create persona', detail: 'Create a local persona with real name, summary, and instructions. Placeholder commands are never dispatched.', command: '/personas create --name <name> --description <summary> --body <instructions>', kind: 'command', safety: 'safe' },
499
+ { id: 'personas-use', label: 'Use persona', detail: 'Activate a local persona by id or name.', command: '/personas use <id>', kind: 'command', safety: 'safe' },
500
+ { id: 'personas-review', label: 'Review persona', detail: 'Mark a local persona reviewed after inspecting it.', command: '/personas review <id>', kind: 'command', safety: 'safe' },
501
+ { id: 'personas-clear', label: 'Clear active persona', detail: 'Return to the default Agent policy without deleting any persona.', command: '/personas clear', kind: 'command', safety: 'safe' },
502
+ ],
503
+ },
504
+ {
505
+ id: 'skills',
506
+ group: 'LEARN',
507
+ label: 'Skills',
508
+ summary: 'Reusable local procedures the assistant can apply on demand.',
509
+ detail: 'Skills are local, reviewable procedures. Enabled skills inform the main conversation; secret-looking content is rejected.',
510
+ actions: [
511
+ { id: 'skills-list', label: 'List skills', detail: 'Print the full local Agent skill library.', command: '/agent-skills list', kind: 'command', safety: 'read-only' },
512
+ { id: 'skills-enabled', label: 'Enabled skills', detail: 'Show only skills currently injected into Agent guidance.', command: '/agent-skills enabled', kind: 'command', safety: 'read-only' },
513
+ { id: 'skills-create', label: 'Create skill', detail: 'Create a reusable local procedure with real details. Placeholder commands are never dispatched.', command: '/agent-skills create --name <name> --description <summary> --procedure <steps>', kind: 'command', safety: 'safe' },
514
+ { id: 'skills-enable', label: 'Enable skill', detail: 'Enable a local Agent skill by id or name.', command: '/agent-skills enable <id>', kind: 'command', safety: 'safe' },
515
+ { id: 'skills-review', label: 'Review skill', detail: 'Mark a local skill reviewed after inspecting it.', command: '/agent-skills review <id>', kind: 'command', safety: 'safe' },
516
+ ],
517
+ },
518
+ {
519
+ id: 'routines',
520
+ group: 'LEARN',
521
+ label: 'Routines',
522
+ summary: 'Repeatable workflows for the main conversation.',
523
+ detail: 'Routines run in the main conversation by default. Promotion to an external daemon schedule requires a real schedule command and --yes.',
524
+ actions: [
525
+ { id: 'routines-list', label: 'List routines', detail: 'Print the full local Agent routine library.', command: '/routines list', kind: 'command', safety: 'read-only' },
526
+ { id: 'routines-enabled', label: 'Enabled routines', detail: 'Show routines available for direct use.', command: '/routines enabled', kind: 'command', safety: 'read-only' },
527
+ { id: 'routines-create', label: 'Create routine', detail: 'Create a repeatable workflow with real steps. Placeholder commands are never dispatched.', command: '/routines create --name <name> --description <summary> --steps <steps>', kind: 'command', safety: 'safe' },
528
+ { id: 'routines-start', label: 'Start routine', detail: 'Start a local routine in the main conversation without creating a hidden job.', command: '/routines start <id>', kind: 'command', safety: 'safe' },
529
+ { id: 'routines-promote', label: 'Promote to schedule', detail: 'Create an external daemon schedule from a reviewed routine only with real timing and --yes.', command: '/routines promote <id> --cron <expr> --yes', kind: 'command', safety: 'safe' },
530
+ { id: 'routines-receipts', label: 'Promotion receipts', detail: 'Inspect local redacted routine schedule promotion receipts.', command: '/routines receipts', kind: 'command', safety: 'read-only' },
401
531
  ],
402
532
  },
403
533
  {
@@ -566,6 +696,31 @@ export class AgentWorkspace {
566
696
  const action = this.selectedAction;
567
697
  if (!action) return;
568
698
  if (action.kind === 'guidance' || !action.command) {
699
+ if (action.kind === 'workspace' && action.targetCategoryId) {
700
+ const targetIndex = this.categories.findIndex((category) => category.id === action.targetCategoryId);
701
+ if (targetIndex >= 0) {
702
+ this.selectedCategoryIndex = targetIndex;
703
+ this.selectedActionIndex = 0;
704
+ this.focusActions();
705
+ this.status = `Opened ${this.selectedCategory.label}.`;
706
+ this.lastActionResult = {
707
+ kind: 'refreshed',
708
+ title: `Opened ${this.selectedCategory.label}`,
709
+ detail: action.detail,
710
+ safety: action.safety,
711
+ };
712
+ this.clampSelection();
713
+ return;
714
+ }
715
+ this.status = `Workspace area unavailable: ${action.targetCategoryId}.`;
716
+ this.lastActionResult = {
717
+ kind: 'error',
718
+ title: 'Workspace area unavailable',
719
+ detail: `No Agent workspace category exists for ${action.targetCategoryId}.`,
720
+ safety: action.safety,
721
+ };
722
+ return;
723
+ }
569
724
  this.status = action.detail;
570
725
  this.lastActionResult = {
571
726
  kind: 'guidance',
@@ -6,14 +6,14 @@ import type { ResolvedBuiltinPanelDeps } from './shared.ts';
6
6
  export function registerKnowledgePanels(manager: PanelManager, deps: ResolvedBuiltinPanelDeps): void {
7
7
  if (!deps.memoryRegistry) return;
8
8
 
9
- const { memoryRegistry } = deps;
9
+ const { agentKnowledgeService, memoryRegistry } = deps;
10
10
  manager.registerType({
11
11
  id: 'knowledge',
12
12
  name: 'Knowledge',
13
13
  icon: 'K',
14
14
  category: 'agent',
15
- description: 'Structured project knowledge: risks, runbooks, architecture notes, incidents, and durable facts',
16
- factory: () => new KnowledgePanel(memoryRegistry),
15
+ description: 'Isolated Agent Knowledge plus local non-secret memory review',
16
+ factory: () => new KnowledgePanel(memoryRegistry, agentKnowledgeService ?? null),
17
17
  });
18
18
  manager.registerType({
19
19
  id: 'memory',
@@ -21,6 +21,7 @@ import type { SessionMemoryStore } from '@pellux/goodvibes-sdk/platform/core';
21
21
  import type { ExecutionPlanManager } from '@pellux/goodvibes-sdk/platform/core';
22
22
  import type { AdaptivePlanner } from '@pellux/goodvibes-sdk/platform/core';
23
23
  import type { ProjectPlanningService } from '@pellux/goodvibes-sdk/platform/knowledge';
24
+ import type { KnowledgeService } from '@pellux/goodvibes-sdk/platform/knowledge';
24
25
  import type { ApiTokenAuditor } from '@pellux/goodvibes-sdk/platform/security';
25
26
  import type { ComponentHealthMonitor } from '../../runtime/perf/panel-health-monitor.ts';
26
27
  import type { WorktreeRegistry } from '@/runtime/index.ts';
@@ -65,6 +66,8 @@ export interface BuiltinPanelDeps {
65
66
  evalRegistry?: import('../eval-panel.ts').EvalRegistry;
66
67
  /** MemoryRegistry for the Memory panel. */
67
68
  memoryRegistry?: MemoryRegistry;
69
+ /** Isolated Agent Knowledge service for the Agent Knowledge panel. */
70
+ agentKnowledgeService?: Pick<KnowledgeService, 'getStatus'>;
68
71
  /** Shared policy runtime state for governance/policy diagnostics. */
69
72
  policyRuntimeState?: import('@/runtime/index.ts').PolicyRuntimeState;
70
73
  /** Approval broker for control-plane/operator panels. */
@@ -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
  }
@@ -67,6 +67,7 @@ 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
 
@@ -99,6 +100,40 @@ function setupChecklistLines(snapshot: AgentWorkspaceRuntimeSnapshot): ContextLi
99
100
  return lines;
100
101
  }
101
102
 
103
+ function localLibraryLines(
104
+ title: string,
105
+ items: readonly AgentWorkspaceRuntimeSnapshot['localPersonas'][number][],
106
+ emptyText: string,
107
+ ): ContextLine[] {
108
+ const lines: ContextLine[] = [
109
+ { text: title, fg: PALETTE.title, bold: true },
110
+ ];
111
+ if (items.length === 0) {
112
+ lines.push({ text: emptyText, fg: PALETTE.warn });
113
+ return lines;
114
+ }
115
+ for (const item of items.slice(0, 8)) {
116
+ const status = [
117
+ item.active ? 'active' : '',
118
+ item.enabled === true ? 'enabled' : item.enabled === false ? 'disabled' : '',
119
+ item.reviewState,
120
+ item.startCount !== undefined ? `starts ${item.startCount}` : '',
121
+ ].filter(Boolean).join(' / ');
122
+ const tags = item.tags.length > 0 ? ` tags=${item.tags.join(',')}` : '';
123
+ const triggers = item.triggers.length > 0 ? ` triggers=${item.triggers.join(',')}` : '';
124
+ lines.push({
125
+ text: `${item.id}: ${item.name} (${status})`,
126
+ fg: item.reviewState === 'stale' ? PALETTE.warn : PALETTE.info,
127
+ bold: item.active === true,
128
+ });
129
+ lines.push({ text: ` ${item.description}${tags}${triggers}`, fg: PALETTE.muted });
130
+ }
131
+ if (items.length > 8) {
132
+ lines.push({ text: `${items.length - 8} more item(s). Open the library command for the full list.`, fg: PALETTE.dim });
133
+ }
134
+ return lines;
135
+ }
136
+
102
137
  function snapshotLines(category: AgentWorkspaceCategory, snapshot: AgentWorkspaceRuntimeSnapshot | null): ContextLine[] {
103
138
  if (!snapshot) return [{ text: 'Runtime context is not loaded yet.', fg: PALETTE.warn }];
104
139
  const base: ContextLine[] = [{ text: 'Live Agent Context', fg: PALETTE.title, bold: true }];
@@ -177,6 +212,30 @@ function snapshotLines(category: AgentWorkspaceCategory, snapshot: AgentWorkspac
177
212
  { text: 'Durable memory, routines, skills, and personas remain Agent-local until shared registry contracts exist.', fg: PALETTE.good },
178
213
  { text: 'Secrets are rejected/redacted; store secret references instead of secret values.', fg: PALETTE.warn },
179
214
  );
215
+ } else if (category.id === 'personas') {
216
+ base.push(
217
+ { text: `Personas: ${snapshot.localPersonaCount}; active: ${snapshot.activePersonaName}`, fg: PALETTE.info },
218
+ { text: 'Personas are local behavior profiles for the serial main-conversation assistant, not spawned agents.', fg: PALETTE.good },
219
+ { text: 'Use them for tone, role, domain constraints, tool posture, and repeatable operating preferences.', fg: PALETTE.muted },
220
+ { text: '' },
221
+ ...localLibraryLines('Persona Library', snapshot.localPersonas, 'No local personas yet. Create one with /personas create ...'),
222
+ );
223
+ } else if (category.id === 'skills') {
224
+ base.push(
225
+ { text: `Skills: ${snapshot.localSkillCount}; enabled: ${snapshot.enabledSkillCount}`, fg: PALETTE.info },
226
+ { text: 'Skills are reusable local procedures the assistant can apply from the main conversation.', fg: PALETTE.good },
227
+ { text: 'Enabled skills are injected as operating guidance; secret-looking content is rejected.', fg: PALETTE.warn },
228
+ { text: '' },
229
+ ...localLibraryLines('Skill Library', snapshot.localSkills, 'No local skills yet. Create one with /agent-skills create ...'),
230
+ );
231
+ } else if (category.id === 'routines') {
232
+ base.push(
233
+ { text: `Routines: ${snapshot.localRoutineCount}; enabled: ${snapshot.enabledRoutineCount}`, fg: PALETTE.info },
234
+ { text: 'Routines are repeatable main-conversation workflows. Starting one does not create hidden jobs.', fg: PALETTE.good },
235
+ { text: 'Scheduling a reviewed routine is explicit and writes to the externally owned daemon only with --yes.', fg: PALETTE.warn },
236
+ { text: '' },
237
+ ...localLibraryLines('Routine Library', snapshot.localRoutines, 'No local routines yet. Create one with /routines create ...'),
238
+ );
180
239
  } else if (category.id === 'work') {
181
240
  base.push(
182
241
  { text: 'Work plan and approvals are read or explicitly confirmed through public operator routes.', fg: PALETTE.info },
@@ -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.',
@@ -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
+ }
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.1.48';
9
+ let _version = '0.1.50';
10
10
  let _sdkVersion = '0.33.35';
11
11
  try {
12
12
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8')) as {